get(EntityManagerInterface::class); $this->em = $em; // Creation de la table fake_site_aware_entity uniquement. // La base de test partage deja les autres tables (site, user, etc.). $metadata = $this->em->getClassMetadata(FakeSiteAwareEntity::class); $schema = new SchemaTool($this->em); // Drop si existe deja (re-run des tests), puis create. $schema->dropSchema([$metadata]); $schema->createSchema([$metadata]); // Fixtures locales : 2 entites sur siteA, 1 sur siteB. $this->siteA = $this->em->getRepository(Site::class)->findOneBy(['name' => 'Chatellerault']); $this->siteB = $this->em->getRepository(Site::class)->findOneBy(['name' => 'Saint-Jean']); self::assertNotNull($this->siteA); self::assertNotNull($this->siteB); $e1 = new FakeSiteAwareEntity('A-in-site-A'); $e1->setSite($this->siteA); $e2 = new FakeSiteAwareEntity('B-in-site-A'); $e2->setSite($this->siteA); $e3 = new FakeSiteAwareEntity('C-in-site-B'); $e3->setSite($this->siteB); $this->em->persist($e1); $this->em->persist($e2); $this->em->persist($e3); $this->em->flush(); $this->em->clear(); } protected function tearDown(): void { // Drop de la table fake entre tests pour eviter toute pollution. if (isset($this->em)) { $metadata = $this->em->getClassMetadata(FakeSiteAwareEntity::class); $schema = new SchemaTool($this->em); $schema->dropSchema([$metadata]); $this->em->close(); } parent::tearDown(); } public function testCollectionFilteredByCurrentSite(): void { $extension = $this->makeExtension($this->siteA); $results = $this->runQuery($extension, FakeSiteAwareEntity::class); self::assertCount(2, $results, '2 entites sur siteA doivent etre retournees.'); foreach ($results as $entity) { self::assertSame($this->siteA->getId(), $entity->getSite()->getId()); } } public function testCollectionSwitchesToSiteB(): void { $extension = $this->makeExtension($this->siteB); $results = $this->runQuery($extension, FakeSiteAwareEntity::class); self::assertCount(1, $results); self::assertSame($this->siteB->getId(), $results[0]->getSite()->getId()); } public function testNoOpIfNoCurrentSite(): void { // Decision assumee (ticket 4 spec Risque 1) : no-op plutot que // collection vide. L'user sans currentSite voit TOUTES les entites. $extension = $this->makeExtension(currentSite: null); $results = $this->runQuery($extension, FakeSiteAwareEntity::class); self::assertCount(3, $results); } public function testNoOpIfBypassScopePermission(): void { // User avec sites.bypass_scope voit TOUTES les entites, meme // avec un currentSite positionne. Comportement admin / audit. $extension = $this->makeExtension($this->siteA, bypassScope: true); $results = $this->runQuery($extension, FakeSiteAwareEntity::class); self::assertCount(3, $results); } public function testNoOpIfResourceClassNotSiteAware(): void { // Une resource qui n'implemente pas SiteAwareInterface ne doit // jamais etre filtree (l'extension se contente d'un `return` tot). $extension = $this->makeExtension($this->siteA); // On query les users (non SiteAware). Verification robuste : on // inspecte la partie WHERE du QueryBuilder avant et apres l'appel // a l'extension. Le before/after doit etre identique (idealement // null dans les deux cas vu qu'on n'a pas ajoute de WHERE). $qb = $this->em->createQueryBuilder()->select('u')->from(User::class, 'u'); $nameGen = new QueryNameGenerator(); $whereBefore = $qb->getDQLPart('where'); $extension->applyToCollection($qb, $nameGen, User::class); $whereAfter = $qb->getDQLPart('where'); self::assertEquals( $whereBefore, $whereAfter, 'La clause WHERE du QueryBuilder doit etre intacte pour une resource non SiteAware.', ); } public function testItemNotFoundIfWrongSite(): void { // GET /api/entity/{id} pour un item du siteB alors que l'user est // sur siteA -> le filtre ajoute `WHERE site = siteA`, la requete // retourne null -> API Platform renverra 404. $em = $this->em; $entityB = $em->getRepository(FakeSiteAwareEntity::class) ->findOneBy(['name' => 'C-in-site-B']) ; self::assertNotNull($entityB); $idB = $entityB->getId(); $em->clear(); $extension = $this->makeExtension($this->siteA); $qb = $this->em->createQueryBuilder() ->select('e') ->from(FakeSiteAwareEntity::class, 'e') ->andWhere('e.id = :id') ->setParameter('id', $idB) ; $nameGen = new QueryNameGenerator(); $extension->applyToItem($qb, $nameGen, FakeSiteAwareEntity::class, ['id' => $idB]); self::assertNull($qb->getQuery()->getOneOrNullResult()); } public function testItemFoundIfCorrectSite(): void { $entityA = $this->em->getRepository(FakeSiteAwareEntity::class) ->findOneBy(['name' => 'A-in-site-A']) ; self::assertNotNull($entityA); $idA = $entityA->getId(); $this->em->clear(); $extension = $this->makeExtension($this->siteA); $qb = $this->em->createQueryBuilder() ->select('e') ->from(FakeSiteAwareEntity::class, 'e') ->andWhere('e.id = :id') ->setParameter('id', $idA) ; $nameGen = new QueryNameGenerator(); $extension->applyToItem($qb, $nameGen, FakeSiteAwareEntity::class, ['id' => $idA]); $result = $qb->getQuery()->getOneOrNullResult(); self::assertNotNull($result); self::assertSame('A-in-site-A', $result->getName()); } /** * Construit une extension avec un provider et un security mockes selon * le scenario testé. Passe par reflection pour forcer le flag * sitesActive du provider sans toucher au filesystem. */ private function makeExtension(?Site $currentSite, bool $bypassScope = false): SiteScopedQueryExtension { // createStub : pas d'attentes sur le nombre d'appels, juste fixer // les valeurs de retour des methodes sollicitees. Evite les notices // PHPUnit "No expectations configured". $security = $this->createStub(Security::class); $security->method('isGranted')->willReturnCallback( fn (string $perm): bool => 'sites.bypass_scope' === $perm && $bypassScope, ); $provider = $this->createStub(CurrentSiteProviderInterface::class); $provider->method('get')->willReturn($currentSite); return new SiteScopedQueryExtension($provider, $security); } private function runQuery(SiteScopedQueryExtension $extension, string $resourceClass): array { $qb = $this->em->createQueryBuilder()->select('e')->from($resourceClass, 'e'); $nameGen = new QueryNameGenerator(); $extension->applyToCollection($qb, $nameGen, $resourceClass); return $qb->getQuery()->getResult(); } }