createStub(DataStore::class); $this->mailboxes = new FakeMailboxStore($dataStore); $messages = new FakeMessageStore($dataStore); $files = new FakeMessageFileStore('/nonexistent'); $harmonizer = new HarmonizationService( $this->mailboxes, $messages, $files, new MessageIngestor($files, $messages, $this->mailboxes), ); $this->service = new CachedServiceTestDouble( $this->mailboxes, $harmonizer, new MessageDeltaService($this->mailboxes, $messages), ); $this->service->fromStore(['tid' => 'tenant', 'sid' => 'svc']); $this->live = new FakeLiveMailService($this->service); $this->live->uidValidity = 7; $this->live->uidNext = 10; $this->live->mailboxList = [new Mailbox('INBOX', '/', []), new Mailbox('Sent', '/', ['\\Sent'])]; $this->service->liveMail = $this->live; } public function testCollectionListComesFromTheCacheAfterHarmonizingTheList(): void { $list = $this->service->collectionList(null); $this->assertSame(['INBOX', 'Sent'], array_keys($list)); $this->assertSame(CollectionRoles::Sent, $list['Sent']->getProperties()->getRole()); $this->assertCount(1, $this->listCalls()); } public function testFreshListIsNotHarmonizedAgain(): void { $this->service->collectionList(null); $this->service->collectionList(null); $this->service->collectionFetch('INBOX'); $this->assertCount(1, $this->listCalls()); } public function testStaleListIsHarmonizedAgain(): void { $this->mailboxes->clock = time() - CachedService::FRESHNESS_WINDOW - 1; $this->service->collectionList(null); $this->mailboxes->clock = null; $this->live->mailboxList[] = new Mailbox('Archive', '/', []); $list = $this->service->collectionList(null); $this->assertSame(['INBOX', 'Sent', 'Archive'], array_keys($list)); $this->assertCount(2, $this->listCalls()); } public function testFilteredListGoesToTheServer(): void { $filter = $this->service->collectionListFilter(); $filter->condition('role', CollectionRoles::Sent->value); $this->service->collectionList(null, $filter); $this->assertSame([['collectionList', [null, $filter]]], $this->live->calls); } public function testCollectionSignatureIsTheDeltaSignature(): void { $this->live->flags = [1 => [], 2 => []]; $this->service->entityDelta('INBOX', ''); $this->assertSame('7:2', $this->service->collectionFetch('INBOX')?->signature()); } public function testCollectionExtantUsesTheCache(): void { $this->assertSame(['INBOX' => true, 'Missing' => false], $this->service->collectionExtant('INBOX', 'Missing')); } public function testDeltaHarmonizesWhenStaleOnly(): void { $this->live->flags = [1 => []]; $first = $this->service->entityDelta('INBOX', ''); $this->live->flags = [1 => [], 2 => []]; $second = $this->service->entityDelta('INBOX', $first->signature); $this->assertSame('7:1', $first->signature); $this->assertSame('7:1', $second->signature, 'fresh mailbox is not harmonized again'); $this->mailboxes->updateState('svc', 'INBOX', ['harmonizedAt' => time() - CachedService::FRESHNESS_WINDOW]); $third = $this->service->entityDelta('INBOX', $first->signature); $this->assertSame('7:2', $third->signature); $this->assertSame(['2'], json_decode(json_encode($third->additions), true)); } public function testDeltaIsAnsweredFromTheCacheWhenTheServerFails(): void { $this->live->flags = [1 => []]; $signature = $this->service->entityDelta('INBOX', '')->signature; $this->mailboxes->updateState('svc', 'INBOX', ['harmonizedAt' => 0]); $this->live->uidValidity = null; // harmonization throws $this->assertSame($signature, $this->service->entityDelta('INBOX', $signature)->signature); } public function testWritesGoThroughTheLiveService(): void { $properties = new MessageProperties([]); $properties->setFlag('seen', true); $this->service->entityPatch($properties, new EntityIdentifier('imap', 'svc', 'INBOX', '5')); $patches = array_values(array_filter($this->live->calls, static fn (array $call): bool => $call[0] === 'entityPatch')); $this->assertCount(1, $patches); [$collection, $added, , $uids] = $patches[0][1]; $this->assertSame('INBOX', $collection); $this->assertContains('seen', $added); $this->assertSame([5], $uids); } /** * @return list */ private function listCalls(): array { return array_values(array_filter($this->live->calls, static fn (array $call): bool => $call[0] === 'collectionList')); } } /** * CachedService with a replaceable IMAP side. */ final class CachedServiceTestDouble extends CachedService { public ?LiveMailService $liveMail = null; protected function liveMail(): LiveMailService { return $this->liveMail; } }