setEnabled(false); $configRepo = $this->createMock(MailConfigurationRepositoryInterface::class); $configRepo->method('findSingleton')->willReturn($config); $provider = $this->createMock(MailProviderInterface::class); $folderRepo = $this->createMock(MailFolderRepositoryInterface::class); $messageRepo = $this->createMock(MailMessageRepositoryInterface::class); $em = $this->createMock(EntityManagerInterface::class); $lockFactory = $this->makeLockFactory(); $service = new MailSyncService( provider: $provider, configRepository: $configRepo, folderRepository: $folderRepo, messageRepository: $messageRepo, entityManager: $em, lockFactory: $lockFactory, logger: new NullLogger(), managerRegistry: $this->createMock(ManagerRegistry::class), ); $report = $service->syncAll(); self::assertSame(0, $report->createdCount); self::assertSame(0, $report->updatedCount); self::assertSame(0, $report->deletedCount); self::assertSame(0, $report->foldersScanned); } public function testSyncAllReturnsEmptyReportWhenLockNotAcquired(): void { $config = new MailConfiguration(); $config->setEnabled(true); $configRepo = $this->createMock(MailConfigurationRepositoryInterface::class); $configRepo->method('findSingleton')->willReturn($config); $provider = $this->createMock(MailProviderInterface::class); $folderRepo = $this->createMock(MailFolderRepositoryInterface::class); $messageRepo = $this->createMock(MailMessageRepositoryInterface::class); $em = $this->createMock(EntityManagerInterface::class); $lockFactory = $this->makeLockFactory(false); $service = new MailSyncService( provider: $provider, configRepository: $configRepo, folderRepository: $folderRepo, messageRepository: $messageRepo, entityManager: $em, lockFactory: $lockFactory, logger: new NullLogger(), managerRegistry: $this->createMock(ManagerRegistry::class), ); $report = $service->syncAll(); self::assertSame(0, $report->createdCount); self::assertContains('lock_not_acquired', $report->errors); } public function testSyncFolderStructureCreatesNewFolders(): void { $config = new MailConfiguration(); $config->setEnabled(true); $configRepo = $this->createMock(MailConfigurationRepositoryInterface::class); $configRepo->method('findSingleton')->willReturn($config); $folderDto = new MailFolderDto( path: 'INBOX', displayName: 'Inbox', parentPath: null, unreadCount: 5, totalCount: 42, ); $provider = $this->createMock(MailProviderInterface::class); $provider->method('listFolders')->willReturn([$folderDto]); $folderRepo = $this->createMock(MailFolderRepositoryInterface::class); $folderRepo->method('findByPath')->willReturn(null); $folderRepo->method('findAllOrderedByPath')->willReturn([]); $messageRepo = $this->createMock(MailMessageRepositoryInterface::class); $em = $this->createMock(EntityManagerInterface::class); $em->expects(self::once())->method('persist'); $em->expects(self::once())->method('flush'); $lockFactory = $this->makeLockFactory(); $service = new MailSyncService( provider: $provider, configRepository: $configRepo, folderRepository: $folderRepo, messageRepository: $messageRepo, entityManager: $em, lockFactory: $lockFactory, logger: new NullLogger(), managerRegistry: $this->createMock(ManagerRegistry::class), ); $service->syncFolderStructure(); } public function testSyncAllSkipsFoldersNoLongerPresentOnServer(): void { $config = new MailConfiguration(); $config->setEnabled(true); $configRepo = $this->createMock(MailConfigurationRepositoryInterface::class); $configRepo->method('findSingleton')->willReturn($config); // The server only exposes INBOX; "Trash/STALE" was deleted remotely but // still lingers in the DB. $inboxDto = new MailFolderDto( path: 'INBOX', displayName: 'Inbox', parentPath: null, unreadCount: 0, totalCount: 0, ); $inboxFolder = new MailFolder(); $inboxFolder->setPath('INBOX'); $staleFolder = new MailFolder(); $staleFolder->setPath('Trash/STALE'); $provider = $this->createMock(MailProviderInterface::class); $provider->method('listFolders')->willReturn([$inboxDto]); // listMessages must only ever be called for INBOX, never the stale folder. $provider->expects(self::once()) ->method('listMessages') ->with('INBOX', 5000, 0) ->willReturn([]) ; $folderRepo = $this->createMock(MailFolderRepositoryInterface::class); $folderRepo->method('findByPath')->willReturn($inboxFolder); $folderRepo->method('findAllOrderedByPath')->willReturn([$inboxFolder, $staleFolder]); $messageRepo = $this->createMock(MailMessageRepositoryInterface::class); $messageRepo->method('findMaxUidInFolder')->willReturn(0); $messageRepo->method('findAllUidsByFolder')->willReturn([]); $messageRepo->method('findLastNByFolder')->willReturn([]); $em = $this->createMock(EntityManagerInterface::class); $em->method('isOpen')->willReturn(true); $lockFactory = $this->makeLockFactory(); $service = new MailSyncService( provider: $provider, configRepository: $configRepo, folderRepository: $folderRepo, messageRepository: $messageRepo, entityManager: $em, lockFactory: $lockFactory, logger: new NullLogger(), managerRegistry: $this->createMock(ManagerRegistry::class), ); $report = $service->syncAll(); self::assertSame(1, $report->foldersScanned); self::assertSame([], $report->errors); } public function testSyncFolderAbortsSuppressionWhenOver50Percent(): void { $config = new MailConfiguration(); $config->setEnabled(true); $configRepo = $this->createMock(MailConfigurationRepositoryInterface::class); $configRepo->method('findSingleton')->willReturn($config); $folder = new MailFolder(); $folder->setPath('INBOX'); $messageRepo = $this->createMock(MailMessageRepositoryInterface::class); $messageRepo->method('findMaxUidInFolder')->willReturn(10); $messageRepo->method('findAllUidsByFolder')->willReturn([1, 2, 3, 4, 5, 6, 7, 8, 9, 10]); $messageRepo->method('findLastNByFolder')->willReturn([]); $provider = $this->createMock(MailProviderInterface::class); $provider->method('listMessages')->willReturn([]); $folderRepo = $this->createMock(MailFolderRepositoryInterface::class); $em = $this->createMock(EntityManagerInterface::class); $em->expects(self::never())->method('remove'); $lockFactory = $this->makeLockFactory(); $service = new MailSyncService( provider: $provider, configRepository: $configRepo, folderRepository: $folderRepo, messageRepository: $messageRepo, entityManager: $em, lockFactory: $lockFactory, logger: new NullLogger(), managerRegistry: $this->createMock(ManagerRegistry::class), ); $report = $service->syncFolder($folder); self::assertSame(0, $report->deletedCount); self::assertNotEmpty($report->errors); } private function makeLockFactory(bool $acquired = true): LockFactory { $lock = $this->createMock(SharedLockInterface::class); $lock->method('acquire')->willReturn($acquired); $factory = $this->createMock(LockFactory::class); $factory->method('createLock')->willReturn($lock); return $factory; } }