cache = new PaginatedQueryCache( new TagAwareAdapter(new ArrayAdapter()), ); $this->invalidator = new PaginatedQueryCacheInvalidator($this->cache); } // === Users === #[Test] public function utilisateurInviteInvalidatesUsersCache(): void { $this->warmCache('users', self::TENANT_ID); $this->invalidator->onUtilisateurInvite($this->createUtilisateurInvite(self::TENANT_ID)); $this->assertCacheWasInvalidated('users', self::TENANT_ID); } #[Test] public function invitationRenvoyeeInvalidatesUsersCache(): 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->invalidator->onInvitationRenvoyee($event); $this->assertCacheWasInvalidated('users', self::TENANT_ID); } #[Test] public function eleveInscritInvalidatesUsersAndImageRightsCache(): void { $this->warmCache('users', self::TENANT_ID); $this->warmCache('students_image_rights', self::TENANT_ID); $event = new EleveInscrit( userId: UserId::generate(), firstName: 'Alice', lastName: 'Martin', tenantId: TenantId::fromString(self::TENANT_ID), occurredOn: new DateTimeImmutable(), ); $this->invalidator->onEleveInscrit($event); $this->assertCacheWasInvalidated('users', self::TENANT_ID); $this->assertCacheWasInvalidated('students_image_rights', self::TENANT_ID); } // === Classes === #[Test] public function classeCreeeInvalidatesClassesAndAssignmentsCache(): void { $this->warmCache('classes', self::TENANT_ID); $this->warmCache('assignments', self::TENANT_ID); $event = new ClasseCreee( classId: ClassId::generate(), tenantId: TenantId::fromString(self::TENANT_ID), name: new ClassName('6ème A'), level: null, occurredOn: new DateTimeImmutable(), ); $this->invalidator->onClasseCreee($event); $this->assertCacheWasInvalidated('classes', self::TENANT_ID); $this->assertCacheWasInvalidated('assignments', self::TENANT_ID); } // === Subjects === #[Test] public function matiereCreeeInvalidatesSubjectsAndAssignmentsCache(): void { $this->warmCache('subjects', self::TENANT_ID); $this->warmCache('assignments', self::TENANT_ID); $event = new MatiereCreee( subjectId: SubjectId::generate(), tenantId: TenantId::fromString(self::TENANT_ID), name: new SubjectName('Mathématiques'), code: new SubjectCode('MATH'), occurredOn: new DateTimeImmutable(), ); $this->invalidator->onMatiereCreee($event); $this->assertCacheWasInvalidated('subjects', self::TENANT_ID); $this->assertCacheWasInvalidated('assignments', self::TENANT_ID); } // === Assignments === #[Test] public function enseignantAffecteInvalidatesAssignmentsCache(): void { $this->warmCache('assignments', self::TENANT_ID); $event = new EnseignantAffecte( assignmentId: TeacherAssignmentId::generate(), teacherId: UserId::generate(), classId: ClassId::generate(), subjectId: SubjectId::generate(), tenantId: TenantId::fromString(self::TENANT_ID), occurredOn: new DateTimeImmutable(), ); $this->invalidator->onEnseignantAffecte($event); $this->assertCacheWasInvalidated('assignments', self::TENANT_ID); } #[Test] public function affectationRetireeInvalidatesAssignmentsCache(): void { $this->warmCache('assignments', self::TENANT_ID); $event = new AffectationRetiree( assignmentId: TeacherAssignmentId::generate(), teacherId: UserId::generate(), classId: ClassId::generate(), subjectId: SubjectId::generate(), tenantId: TenantId::fromString(self::TENANT_ID), occurredOn: new DateTimeImmutable(), ); $this->invalidator->onAffectationRetiree($event); $this->assertCacheWasInvalidated('assignments', self::TENANT_ID); } // === Parent invitations === #[Test] public function invitationParentActiveeInvalidatesParentInvitationsAndUsersCache(): 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->invalidator->onInvitationParentActivee($event); $this->assertCacheWasInvalidated('parent_invitations', self::TENANT_ID); $this->assertCacheWasInvalidated('users', self::TENANT_ID); } // === Image rights === #[Test] public function droitImageModifieInvalidatesImageRightsCache(): void { $this->warmCache('students_image_rights', self::TENANT_ID); $event = new DroitImageModifie( userId: UserId::generate(), email: 'alice@example.com', ancienStatut: ImageRightsStatus::NOT_SPECIFIED, nouveauStatut: ImageRightsStatus::AUTHORIZED, modifiePar: UserId::generate(), tenantId: TenantId::fromString(self::TENANT_ID), occurredOn: new DateTimeImmutable(), ); $this->invalidator->onDroitImageModifie($event); $this->assertCacheWasInvalidated('students_image_rights', self::TENANT_ID); } // === Imports === #[Test] public function importElevesTermineInvalidatesUsersImageRightsAndClassesCache(): void { $this->warmCache('users', self::TENANT_ID); $this->warmCache('students_image_rights', self::TENANT_ID); $this->warmCache('classes', self::TENANT_ID); $event = new ImportElevesTermine( batchId: ImportBatchId::generate(), tenantId: TenantId::fromString(self::TENANT_ID), importedCount: 10, errorCount: 0, occurredOn: new DateTimeImmutable(), ); $this->invalidator->onImportElevesTermine($event); $this->assertCacheWasInvalidated('users', self::TENANT_ID); $this->assertCacheWasInvalidated('students_image_rights', self::TENANT_ID); $this->assertCacheWasInvalidated('classes', self::TENANT_ID); } #[Test] public function importEnseignantsTermineInvalidatesUsersAndAssignmentsCache(): void { $this->warmCache('users', self::TENANT_ID); $this->warmCache('assignments', self::TENANT_ID); $event = new ImportEnseignantsTermine( batchId: ImportBatchId::generate(), tenantId: TenantId::fromString(self::TENANT_ID), importedCount: 5, errorCount: 0, occurredOn: new DateTimeImmutable(), ); $this->invalidator->onImportEnseignantsTermine($event); $this->assertCacheWasInvalidated('users', self::TENANT_ID); $this->assertCacheWasInvalidated('assignments', self::TENANT_ID); } // === Tenant isolation === #[Test] public function invalidationDoesNotAffectOtherTenants(): void { $otherTenantId = '550e8400-e29b-41d4-a716-446655440099'; $this->warmCache('users', self::TENANT_ID); $this->warmCache('users', $otherTenantId); $this->invalidator->onUtilisateurInvite($this->createUtilisateurInvite(self::TENANT_ID)); $this->assertCacheWasInvalidated('users', self::TENANT_ID); $this->assertCacheStillValid('users', $otherTenantId); } #[Test] public function invalidationDoesNotAffectOtherEntityTypes(): void { $this->warmCache('users', self::TENANT_ID); $this->warmCache('classes', self::TENANT_ID); $this->invalidator->onUtilisateurInvite($this->createUtilisateurInvite(self::TENANT_ID)); $this->assertCacheWasInvalidated('users', self::TENANT_ID); $this->assertCacheStillValid('classes', 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 createUtilisateurInvite(string $tenantId): UtilisateurInvite { return new UtilisateurInvite( userId: UserId::generate(), email: 'test@example.com', role: 'ROLE_PROF', firstName: 'Jean', lastName: 'Dupont', tenantId: TenantId::fromString($tenantId), occurredOn: new DateTimeImmutable(), ); } }