root = sys_get_temp_dir() . '/ktrix-imap-cache-' . bin2hex(random_bytes(4)); $this->store = new MessageFileStore($this->root); } protected function tearDown(): void { if (!is_dir($this->root)) { return; } $items = new RecursiveIteratorIterator( new RecursiveDirectoryIterator($this->root, FilesystemIterator::SKIP_DOTS), RecursiveIteratorIterator::CHILD_FIRST, ); foreach ($items as $item) { $item->isDir() ? rmdir($item->getPathname()) : unlink($item->getPathname()); } rmdir($this->root); } public function testWriteThenReadReturnsContent(): void { $content = ['schemaVersion' => 1, 'properties' => ['subject' => 'Grüße / ✓']]; $this->store->write('tenant', 'svc', 'INBOX/Reports', 7, 42, $content); $this->assertSame($content, $this->store->read('tenant', 'svc', 'INBOX/Reports', 7, 42)); $this->assertFileExists(sprintf( '%s/storage/tenant/provider_imap/svc/%s/7/42/message.json', $this->root, sha1('INBOX/Reports'), )); } public function testWriteReplacesAndLeavesNoTemporaryFiles(): void { $this->store->write('tenant', 'svc', 'INBOX', 7, 42, ['v' => 1]); $this->store->write('tenant', 'svc', 'INBOX', 7, 42, ['v' => 2]); $this->assertSame(['v' => 2], $this->store->read('tenant', 'svc', 'INBOX', 7, 42)); $folder = sprintf('%s/storage/tenant/provider_imap/svc/%s/7/42', $this->root, sha1('INBOX')); $this->assertSame(['message.json'], array_values(array_diff(scandir($folder), ['.', '..']))); } public function testReadOfMissingOrCorruptContentReturnsNull(): void { $this->assertNull($this->store->read('tenant', 'svc', 'INBOX', 7, 42)); $this->store->write('tenant', 'svc', 'INBOX', 7, 42, ['v' => 1]); file_put_contents( sprintf('%s/storage/tenant/provider_imap/svc/%s/7/42/message.json', $this->root, sha1('INBOX')), '{truncated', ); $this->assertNull($this->store->read('tenant', 'svc', 'INBOX', 7, 42)); } public function testDeleteScopes(): void { $this->store->write('tenant', 'svc', 'INBOX', 7, 1, ['v' => 1]); $this->store->write('tenant', 'svc', 'INBOX', 7, 2, ['v' => 2]); $this->store->write('tenant', 'svc', 'Sent', 3, 1, ['v' => 3]); $this->store->write('tenant', 'other', 'INBOX', 7, 1, ['v' => 4]); $this->store->delete('tenant', 'svc', 'INBOX', 7, 1); $this->assertNull($this->store->read('tenant', 'svc', 'INBOX', 7, 1)); $this->assertNotNull($this->store->read('tenant', 'svc', 'INBOX', 7, 2)); $this->store->deleteByMailbox('tenant', 'svc', 'INBOX'); $this->assertNull($this->store->read('tenant', 'svc', 'INBOX', 7, 2)); $this->assertNotNull($this->store->read('tenant', 'svc', 'Sent', 3, 1)); $this->store->deleteByService('tenant', 'svc'); $this->assertNull($this->store->read('tenant', 'svc', 'Sent', 3, 1)); $this->assertDirectoryDoesNotExist($this->root . '/storage/tenant/provider_imap/svc'); $this->assertNotNull($this->store->read('tenant', 'other', 'INBOX', 7, 1)); } public function testIdentifiersThatCouldEscapeTheRootAreRejected(): void { $this->expectException(InvalidArgumentException::class); $this->store->write('tenant', '../escape', 'INBOX', 7, 1, []); } }