generated from Nodarx/template
feat: implement content cache
Signed-off-by: Sebastian Krupinski <krupinski01@gmail.com>
This commit is contained in:
@@ -81,6 +81,20 @@ final class CacheSerializationTest extends TestCase
|
||||
$this->assertSame(['flagged' => true], $restored->getProperties()->getFlags());
|
||||
}
|
||||
|
||||
public function testIncompleteSectionsRoundTripThroughContentButNotTheApi(): void
|
||||
{
|
||||
$entity = $this->entity();
|
||||
$entity->getProperties()->setIncompleteSections('1');
|
||||
|
||||
$content = json_decode(json_encode($entity->toCacheContent()), true);
|
||||
$restored = (new EntityResource('imap', 'svc'))->fromCacheContent($content);
|
||||
|
||||
$this->assertSame(['1'], $content['incomplete']);
|
||||
$this->assertArrayNotHasKey('incomplete', $content['properties']);
|
||||
$this->assertSame(['1'], $restored->getProperties()->getIncompleteSections());
|
||||
$this->assertStringNotContainsString('incomplete', json_encode($restored));
|
||||
}
|
||||
|
||||
public function testContentWithOtherSchemaVersionIsRejected(): void
|
||||
{
|
||||
$content = $this->entity()->toCacheContent();
|
||||
|
||||
@@ -78,6 +78,40 @@ final class FetchMessageParserTest extends TestCase
|
||||
$this->assertSame(['1' => 'Hello wor'], $message->bodySections());
|
||||
}
|
||||
|
||||
public function testTruncatedSectionsAreReported(): void
|
||||
{
|
||||
$complete = FetchMessageParser::parse($this->fetchResponse(
|
||||
self::MIXED_STRUCTURE,
|
||||
'BODY[TEXT]',
|
||||
self::ALTERNATIVE_PREFIX . "<p>Hello</p>\r\n--alt--\r\n--mix--\r\n",
|
||||
));
|
||||
$cutInHtml = FetchMessageParser::parse($this->fetchResponse(
|
||||
self::MIXED_STRUCTURE,
|
||||
'BODY[TEXT]<0>',
|
||||
self::ALTERNATIVE_PREFIX . '<p>Hello wor',
|
||||
));
|
||||
$cutInAttachment = FetchMessageParser::parse($this->fetchResponse(
|
||||
self::MIXED_STRUCTURE,
|
||||
'BODY[TEXT]<0>',
|
||||
self::ALTERNATIVE_PREFIX . "<p>Hello</p>\r\n--alt--\r\n--mix\r\nContent-Type: application/pdf\r\n\r\nJVBE",
|
||||
));
|
||||
|
||||
$this->assertSame([], $complete->truncatedSections());
|
||||
$this->assertSame(['1.2'], $cutInHtml->truncatedSections());
|
||||
$this->assertSame([], $cutInAttachment->truncatedSections());
|
||||
}
|
||||
|
||||
public function testTruncatedSinglePartBodyIsReportedBySize(): void
|
||||
{
|
||||
$structure = '("TEXT" "PLAIN" ("CHARSET" "UTF-8") NIL NIL "7BIT" 11 1)';
|
||||
|
||||
$partial = FetchMessageParser::parse($this->fetchResponse($structure, 'BODY[TEXT]<0>', 'Hello'));
|
||||
$complete = FetchMessageParser::parse($this->fetchResponse($structure, 'BODY[TEXT]', 'Hello world'));
|
||||
|
||||
$this->assertSame(['1'], $partial->truncatedSections());
|
||||
$this->assertSame([], $complete->truncatedSections());
|
||||
}
|
||||
|
||||
private function fetchResponse(string $bodyStructure, string $section, string $body): string
|
||||
{
|
||||
return sprintf(
|
||||
|
||||
@@ -0,0 +1,103 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace KTXT\ProviderImap\Tests\Unit;
|
||||
|
||||
use FilesystemIterator;
|
||||
use InvalidArgumentException;
|
||||
use KTXM\ProviderImap\Stores\MessageFileStore;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use RecursiveDirectoryIterator;
|
||||
use RecursiveIteratorIterator;
|
||||
|
||||
final class MessageFileStoreTest extends TestCase
|
||||
{
|
||||
private string $root;
|
||||
private MessageFileStore $store;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
$this->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, []);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace KTXT\ProviderImap\Tests\Unit;
|
||||
|
||||
use KTXM\ProviderImap\Providers\EntityResource;
|
||||
use KTXM\ProviderImap\Providers\MessageProperties;
|
||||
use KTXM\ProviderImap\Service\Cache\MessageIngestor;
|
||||
use KTXM\ProviderImap\Stores\MessageFileStore;
|
||||
use KTXM\ProviderImap\Stores\MessageStore;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use RuntimeException;
|
||||
|
||||
final class MessageIngestorTest extends TestCase
|
||||
{
|
||||
public function testWritesContentBeforeMetaForEachMessage(): void
|
||||
{
|
||||
$calls = [];
|
||||
|
||||
$fileStore = $this->createStub(MessageFileStore::class);
|
||||
$fileStore->method('write')->willReturnCallback(
|
||||
function (string $tid, string $sid, string $mailbox, int $uidValidity, int $uid, array $content) use (&$calls): void {
|
||||
$calls[] = ['file', $tid, $sid, $mailbox, $uidValidity, $uid, $content['schemaVersion']];
|
||||
},
|
||||
);
|
||||
$messageStore = $this->createStub(MessageStore::class);
|
||||
$messageStore->method('upsert')->willReturnCallback(
|
||||
function (string $tid, string $sid, int $uidValidity, EntityResource $entity) use (&$calls): void {
|
||||
$calls[] = ['meta', $tid, $sid, $entity->collection(), $uidValidity, $entity->identifier()];
|
||||
},
|
||||
);
|
||||
|
||||
$ingested = (new MessageIngestor($fileStore, $messageStore))
|
||||
->ingest('tenant', 'svc', 7, $this->entity(41), $this->entity(42));
|
||||
|
||||
$this->assertSame([41, 42], $ingested);
|
||||
$this->assertSame([
|
||||
['file', 'tenant', 'svc', 'INBOX', 7, 41, EntityResource::CACHE_SCHEMA_VERSION],
|
||||
['meta', 'tenant', 'svc', 'INBOX', 7, 41],
|
||||
['file', 'tenant', 'svc', 'INBOX', 7, 42, EntityResource::CACHE_SCHEMA_VERSION],
|
||||
['meta', 'tenant', 'svc', 'INBOX', 7, 42],
|
||||
], $calls);
|
||||
}
|
||||
|
||||
public function testMetaIsNotWrittenWhenContentFails(): void
|
||||
{
|
||||
$fileStore = $this->createStub(MessageFileStore::class);
|
||||
$fileStore->method('write')->willThrowException(new RuntimeException('disk full'));
|
||||
$messageStore = $this->createMock(MessageStore::class);
|
||||
$messageStore->expects($this->never())->method('upsert');
|
||||
|
||||
$this->expectException(RuntimeException::class);
|
||||
(new MessageIngestor($fileStore, $messageStore))->ingest('tenant', 'svc', 7, $this->entity(41));
|
||||
}
|
||||
|
||||
private function entity(int $uid): EntityResource
|
||||
{
|
||||
$properties = new MessageProperties([]);
|
||||
$properties->setSubject('Message ' . $uid);
|
||||
|
||||
return (new EntityResource('imap', 'svc'))->fromMutation('INBOX', $uid, $properties);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user