cache = new PaginatedQueryCache( new TagAwareAdapter(new ArrayAdapter()), ); $this->middleware = new PaginatedCacheInvalidationMiddleware($this->cache); } #[Test] public function invalidatesUsersCacheOnUtilisateurInvite(): void { $this->warmCache('users', self::TENANT_ID); $event = new UtilisateurInvite( userId: UserId::generate(), email: 'test@example.com', role: 'ROLE_PROF', firstName: 'Jean', lastName: 'Dupont', tenantId: TenantId::fromString(self::TENANT_ID), occurredOn: new DateTimeImmutable(), ); $this->middleware->handle(new Envelope($event), $this->createPassthroughStack()); $this->assertCacheWasInvalidated('users', self::TENANT_ID); } #[Test] public function invalidatesUsersCacheOnCompteActive(): void { $this->warmCache('users', self::TENANT_ID); $event = new CompteActive( userId: Uuid::uuid4()->toString(), email: 'test@example.com', tenantId: TenantId::fromString(self::TENANT_ID), role: 'ROLE_PROF', occurredOn: new DateTimeImmutable(), aggregateId: Uuid::uuid4(), ); $this->middleware->handle(new Envelope($event), $this->createPassthroughStack()); $this->assertCacheWasInvalidated('users', self::TENANT_ID); } #[Test] public function invalidatesUsersCacheOnInvitationRenvoyee(): void { $this->warmCache('users', self::TENANT_ID); $event = new InvitationRenvoyee( userId: UserId::generate(), email: 'test@example.com', tenantId: TenantId::fromString(self::TENANT_ID), occurredOn: new DateTimeImmutable(), ); $this->middleware->handle(new Envelope($event), $this->createPassthroughStack()); $this->assertCacheWasInvalidated('users', self::TENANT_ID); } #[Test] public function invalidatesParentInvitationsCacheOnInvitationParentEnvoyee(): void { $this->warmCache('parent_invitations', self::TENANT_ID); $event = new InvitationParentEnvoyee( invitationId: ParentInvitationId::generate(), studentId: UserId::generate(), parentEmail: new Email('parent@example.com'), tenantId: TenantId::fromString(self::TENANT_ID), occurredOn: new DateTimeImmutable(), ); $this->middleware->handle(new Envelope($event), $this->createPassthroughStack()); $this->assertCacheWasInvalidated('parent_invitations', self::TENANT_ID); } #[Test] public function invalidatesParentInvitationsAndUsersCacheOnInvitationParentActivee(): void { $this->warmCache('parent_invitations', self::TENANT_ID); $this->warmCache('users', self::TENANT_ID); $event = new InvitationParentActivee( invitationId: ParentInvitationId::generate(), studentId: UserId::generate(), parentUserId: UserId::generate(), tenantId: TenantId::fromString(self::TENANT_ID), occurredOn: new DateTimeImmutable(), ); $this->middleware->handle(new Envelope($event), $this->createPassthroughStack()); $this->assertCacheWasInvalidated('parent_invitations', self::TENANT_ID); $this->assertCacheWasInvalidated('users', self::TENANT_ID); } #[Test] public function doesNotInvalidateOnReceivedStamp(): void { $this->warmCache('users', self::TENANT_ID); $event = new UtilisateurInvite( userId: UserId::generate(), email: 'test@example.com', role: 'ROLE_PROF', firstName: 'Jean', lastName: 'Dupont', tenantId: TenantId::fromString(self::TENANT_ID), occurredOn: new DateTimeImmutable(), ); $envelope = new Envelope($event, [new ReceivedStamp('async')]); $this->middleware->handle($envelope, $this->createPassthroughStack()); $this->assertCacheStillValid('users', self::TENANT_ID); } #[Test] public function doesNotInvalidateOnUnrelatedEvent(): void { $this->warmCache('users', self::TENANT_ID); $event = new UtilisateurBloque( userId: UserId::generate(), email: 'test@example.com', reason: 'test', tenantId: TenantId::fromString(self::TENANT_ID), occurredOn: new DateTimeImmutable(), ); $this->middleware->handle(new Envelope($event), $this->createPassthroughStack()); $this->assertCacheStillValid('users', self::TENANT_ID); } private function warmCache(string $entityType, string $tenantId): void { $this->cache->getOrLoad( $entityType, $tenantId, ['page' => 1, 'limit' => 30], static fn (): PaginatedResult => new PaginatedResult(items: ['original'], total: 1, page: 1, limit: 30), ); } private function assertCacheWasInvalidated(string $entityType, string $tenantId): void { $result = $this->cache->getOrLoad( $entityType, $tenantId, ['page' => 1, 'limit' => 30], static fn (): PaginatedResult => new PaginatedResult(items: ['fresh'], total: 1, page: 1, limit: 30), ); self::assertSame(['fresh'], $result->items, "Cache for {$entityType}/{$tenantId} should have been invalidated"); } private function assertCacheStillValid(string $entityType, string $tenantId): void { $result = $this->cache->getOrLoad( $entityType, $tenantId, ['page' => 1, 'limit' => 30], static fn (): PaginatedResult => new PaginatedResult(items: ['fresh'], total: 1, page: 1, limit: 30), ); self::assertSame(['original'], $result->items, "Cache for {$entityType}/{$tenantId} should still contain original data"); } private function createPassthroughStack(): StackInterface { $stack = $this->createMock(StackInterface::class); $nextMiddleware = $this->createMock(\Symfony\Component\Messenger\Middleware\MiddlewareInterface::class); $stack->method('next')->willReturn($nextMiddleware); $nextMiddleware->method('handle')->willReturnCallback( static fn (Envelope $envelope): Envelope => $envelope, ); return $stack; } }