getEm(); $saintJean = $em->getRepository(Site::class)->findOneBy(['name' => 'Saint-Jean']); self::assertNotNull($saintJean); $alice = $em->getRepository(User::class)->findOneBy(['username' => 'alice']); $aliceId = $alice->getId(); $em->clear(); $client = $this->authenticatedClient('admin', 'admin'); $client->request('PATCH', '/api/users/'.$aliceId.'/rbac', [ 'headers' => ['Content-Type' => 'application/merge-patch+json'], 'json' => [ 'sites' => ['/api/sites/'.$saintJean->getId()], ], ]); self::assertResponseIsSuccessful(); // Verification cote base. $em = $this->getEm(); $em->clear(); $reloaded = $em->getRepository(User::class)->find($aliceId); self::assertNotNull($reloaded); self::assertCount(1, $reloaded->getSites()); self::assertSame('Saint-Jean', $reloaded->getSites()->first()->getName()); // Restauration pour ne pas polluer les autres tests. $this->restoreAliceSites(); } public function testRemovingCurrentSiteResetsCurrentSiteToNullThenAutoSelectsFirst(): void { // alice a actuellement {Chatellerault}, currentSite=Chatellerault. // On lui attribue {Saint-Jean} : Chatellerault disparait → currentSite // devrait temporairement etre null, PUIS auto-select Saint-Jean (seul // site restant). $em = $this->getEm(); $saintJean = $em->getRepository(Site::class)->findOneBy(['name' => 'Saint-Jean']); $alice = $em->getRepository(User::class)->findOneBy(['username' => 'alice']); $aliceId = $alice->getId(); $em->clear(); $client = $this->authenticatedClient('admin', 'admin'); $client->request('PATCH', '/api/users/'.$aliceId.'/rbac', [ 'headers' => ['Content-Type' => 'application/merge-patch+json'], 'json' => [ 'sites' => ['/api/sites/'.$saintJean->getId()], ], ]); self::assertResponseIsSuccessful(); $em = $this->getEm(); $em->clear(); $reloaded = $em->getRepository(User::class)->find($aliceId); self::assertNotNull($reloaded->getCurrentSite()); self::assertSame('Saint-Jean', $reloaded->getCurrentSite()->getName()); $this->restoreAliceSites(); } public function testEmptySitesPayloadResetsCurrentSiteToNull(): void { $em = $this->getEm(); $alice = $em->getRepository(User::class)->findOneBy(['username' => 'alice']); $aliceId = $alice->getId(); $em->clear(); $client = $this->authenticatedClient('admin', 'admin'); $client->request('PATCH', '/api/users/'.$aliceId.'/rbac', [ 'headers' => ['Content-Type' => 'application/merge-patch+json'], 'json' => [ 'sites' => [], ], ]); self::assertResponseIsSuccessful(); $em = $this->getEm(); $em->clear(); $reloaded = $em->getRepository(User::class)->find($aliceId); self::assertCount(0, $reloaded->getSites()); self::assertNull($reloaded->getCurrentSite()); $this->restoreAliceSites(); } public function testCurrentSiteFieldInRbacPayloadIsSilentlyIgnored(): void { // Garde structurelle : `currentSite` n'est pas dans le groupe // user:rbac:write. Un client malveillant qui essaierait de set un // currentSite arbitraire via /rbac doit etre silencieusement // ignore (le seul flux autorise est PATCH /me/current-site). $em = $this->getEm(); $pommevic = $em->getRepository(Site::class)->findOneBy(['name' => 'Pommevic']); $alice = $em->getRepository(User::class)->findOneBy(['username' => 'alice']); $aliceId = $alice->getId(); $em->clear(); $client = $this->authenticatedClient('admin', 'admin'); $client->request('PATCH', '/api/users/'.$aliceId.'/rbac', [ 'headers' => ['Content-Type' => 'application/merge-patch+json'], 'json' => [ 'currentSite' => '/api/sites/'.$pommevic->getId(), ], ]); self::assertResponseIsSuccessful(); // alice n'a Pommevic ni dans ses sites ni en currentSite (le champ // a ete ignore par le denormalizer). Son currentSite reste son // Chatellerault d'origine. $em = $this->getEm(); $em->clear(); $reloaded = $em->getRepository(User::class)->find($aliceId); self::assertNotNull($reloaded); self::assertNotNull($reloaded->getCurrentSite()); self::assertSame('Chatellerault', $reloaded->getCurrentSite()->getName()); } public function testRbacPatchWithoutSitesFieldDoesNotChangeCurrentSite(): void { // Garde structurelle : si le payload /rbac ne contient pas le champ // `sites`, ensureCurrentSiteConsistency ne doit pas auto-modifier // le currentSite (alice avait deja Chatellerault). Un PATCH qui // change uniquement isAdmin ou roles ne doit pas remuer la // configuration site de l'user. $em = $this->getEm(); $alice = $em->getRepository(User::class)->findOneBy(['username' => 'alice']); $aliceId = $alice->getId(); $em->clear(); $client = $this->authenticatedClient('admin', 'admin'); $client->request('PATCH', '/api/users/'.$aliceId.'/rbac', [ 'headers' => ['Content-Type' => 'application/merge-patch+json'], 'json' => [ 'isAdmin' => false, ], ]); self::assertResponseIsSuccessful(); $em = $this->getEm(); $em->clear(); $reloaded = $em->getRepository(User::class)->find($aliceId); self::assertNotNull($reloaded->getCurrentSite()); self::assertSame('Chatellerault', $reloaded->getCurrentSite()->getName()); } /** * Remet alice dans l'etat des fixtures : un seul site Chatellerault, * currentSite Chatellerault. Evite la pollution inter-tests. */ private function restoreAliceSites(): void { $em = $this->getEm(); $chatellerault = $em->getRepository(Site::class)->findOneBy(['name' => 'Chatellerault']); $alice = $em->getRepository(User::class)->findOneBy(['username' => 'alice']); // Reset complet des sites foreach ($alice->getSites() as $existing) { $alice->removeSite($existing); } $alice->addSite($chatellerault); $alice->setCurrentSite($chatellerault); $em->flush(); } }