From 49cf798fc96a49775067773a5f948f07cab6598f Mon Sep 17 00:00:00 2001 From: Matthieu Date: Mon, 1 Jun 2026 22:26:56 +0200 Subject: [PATCH] test(commercial) : full RBAC matrix (M1) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ClientRBACMatrixTest : matrice § 2.7 par role metier via les comptes demo seedes par la commande reelle app:seed-rbac --with-demo-users (idempotente). Valide 200/403 par verbe et par onglet pour bureau / compta / commerciale / usine : - usine : 403 partout ; - bureau : view + manage, sans accounting ni archive ; - compta : view + edition accounting (200), POST/main/information/archive -> 403 ; - commerciale : view + manage, sans accounting ni archive ; - RG-1.04 : POST Commerciale incomplet -> 422, meme POST par Admin -> 201. --- .../Commercial/Api/ClientRBACMatrixTest.php | 245 ++++++++++++++++++ 1 file changed, 245 insertions(+) create mode 100644 tests/Module/Commercial/Api/ClientRBACMatrixTest.php diff --git a/tests/Module/Commercial/Api/ClientRBACMatrixTest.php b/tests/Module/Commercial/Api/ClientRBACMatrixTest.php new file mode 100644 index 0000000..06107f5 --- /dev/null +++ b/tests/Module/Commercial/Api/ClientRBACMatrixTest.php @@ -0,0 +1,245 @@ +setAutoExit(false); + $exit = $application->run( + new ArrayInput([ + 'command' => 'app:seed-rbac', + '--with-demo-users' => true, + '--password' => self::PWD, + ]), + new NullOutput(), + ); + self::assertSame( + 0, + $exit, + 'app:seed-rbac a echoue : les permissions commercial.clients.* sont-elles synchronisees (app:sync-permissions) ?', + ); + + // Liberer le kernel pour que authenticatedClient()/createClient() reparte propre. + self::ensureKernelShutdown(); + } + + public function testUsineIsForbiddenEverywhere(): void + { + $seed = $this->seedClient('Usine Target'); + $client = $this->authAs('usine'); + + // Aucune permission : 403 sur tous les verbes. + $client->request('GET', '/api/clients', ['headers' => ['Accept' => self::LD]]); + self::assertResponseStatusCodeSame(403); + + $client->request('GET', '/api/clients/'.$seed->getId(), ['headers' => ['Accept' => self::LD]]); + self::assertResponseStatusCodeSame(403); + + $client->request('POST', '/api/clients', [ + 'headers' => ['Content-Type' => self::LD], + 'json' => $this->validMainPayload('Usine Post'), + ]); + self::assertResponseStatusCodeSame(403); + + $client->request('PATCH', '/api/clients/'.$seed->getId(), [ + 'headers' => ['Content-Type' => self::MERGE], + 'json' => ['companyName' => 'Renamed By Usine'], + ]); + self::assertResponseStatusCodeSame(403); + } + + public function testBureauHasViewAndManageButNoAccountingNoArchive(): void + { + $seed = $this->seedClient('Bureau Target'); + $cat = $this->createCategory('SECTEUR'); + $client = $this->authAs('bureau'); + + // view + $client->request('GET', '/api/clients', ['headers' => ['Accept' => self::LD]]); + self::assertResponseStatusCodeSame(200); + + // manage : creation OK + $client->request('POST', '/api/clients', [ + 'headers' => ['Content-Type' => self::LD], + 'json' => $this->validMainPayload('Bureau Created', $cat->getId()), + ]); + self::assertResponseStatusCodeSame(201); + + // manage : edition onglet principal OK + $client->request('PATCH', '/api/clients/'.$seed->getId(), [ + 'headers' => ['Content-Type' => self::MERGE], + 'json' => ['companyName' => 'Bureau Renamed'], + ]); + self::assertResponseStatusCodeSame(200); + + // PAS accounting : edition onglet Comptabilite refusee + $client->request('PATCH', '/api/clients/'.$seed->getId(), [ + 'headers' => ['Content-Type' => self::MERGE], + 'json' => ['siren' => '123456789'], + ]); + self::assertResponseStatusCodeSame(403); + + // PAS archive : archivage refuse + $client->request('PATCH', '/api/clients/'.$seed->getId(), [ + 'headers' => ['Content-Type' => self::MERGE], + 'json' => ['isArchived' => true], + ]); + self::assertResponseStatusCodeSame(403); + } + + public function testComptaCanEditAccountingOnly(): void + { + $seed = $this->seedClient('Compta Target'); + $client = $this->authAs('compta'); + + // view + $client->request('GET', '/api/clients', ['headers' => ['Accept' => self::LD]]); + self::assertResponseStatusCodeSame(200); + + // PAS manage : creation refusee + $client->request('POST', '/api/clients', [ + 'headers' => ['Content-Type' => self::LD], + 'json' => $this->validMainPayload('Compta Post'), + ]); + self::assertResponseStatusCodeSame(403); + + // accounting.manage : edition onglet Comptabilite OK + $client->request('PATCH', '/api/clients/'.$seed->getId(), [ + 'headers' => ['Content-Type' => self::MERGE], + 'json' => ['siren' => '123456789'], + ]); + self::assertResponseStatusCodeSame(200); + + // PAS manage : edition onglet principal refusee (guardManage) + $client->request('PATCH', '/api/clients/'.$seed->getId(), [ + 'headers' => ['Content-Type' => self::MERGE], + 'json' => ['companyName' => 'Compta Renamed'], + ]); + self::assertResponseStatusCodeSame(403); + + // PAS manage : edition onglet Information refusee (guardManage) + $client->request('PATCH', '/api/clients/'.$seed->getId(), [ + 'headers' => ['Content-Type' => self::MERGE], + 'json' => ['description' => 'Une description'], + ]); + self::assertResponseStatusCodeSame(403); + + // PAS archive : archivage refuse + $client->request('PATCH', '/api/clients/'.$seed->getId(), [ + 'headers' => ['Content-Type' => self::MERGE], + 'json' => ['isArchived' => true], + ]); + self::assertResponseStatusCodeSame(403); + } + + public function testCommercialeHasViewAndManageButNoAccountingNoArchive(): void + { + $seed = $this->seedClient('Commerciale Target'); + $client = $this->authAs('commerciale'); + + // view + $client->request('GET', '/api/clients', ['headers' => ['Accept' => self::LD]]); + self::assertResponseStatusCodeSame(200); + + // manage : la creation passe la security d'operation (pas un 403 comme + // Compta) mais bute sur RG-1.04 (onglet Information incomplet) -> 422. + // C'est la preuve que Commerciale porte `manage` (sinon 403). + $client->request('POST', '/api/clients', [ + 'headers' => ['Content-Type' => self::LD], + 'json' => $this->validMainPayload('Commerciale Post'), + ]); + self::assertResponseStatusCodeSame(422); + + // PAS accounting : edition onglet Comptabilite refusee + $client->request('PATCH', '/api/clients/'.$seed->getId(), [ + 'headers' => ['Content-Type' => self::MERGE], + 'json' => ['siren' => '123456789'], + ]); + self::assertResponseStatusCodeSame(403); + + // PAS archive : archivage refuse + $client->request('PATCH', '/api/clients/'.$seed->getId(), [ + 'headers' => ['Content-Type' => self::MERGE], + 'json' => ['isArchived' => true], + ]); + self::assertResponseStatusCodeSame(403); + } + + public function testRG104CommercialePostIncompleteIs422AdminIs201(): void + { + $cat = $this->createCategory('SECTEUR'); + + // RG-1.04 durcie : Commerciale POST sans onglet Information complet -> 422. + $commerciale = $this->authAs('commerciale'); + $commerciale->request('POST', '/api/clients', [ + 'headers' => ['Content-Type' => self::LD], + 'json' => $this->validMainPayload('RG104 Commerciale', $cat->getId()), + ]); + self::assertResponseStatusCodeSame(422); + + // Meme payload par un Admin (non gate par RG-1.04) -> 201. + $admin = $this->createAdminClient(); + $admin->request('POST', '/api/clients', [ + 'headers' => ['Content-Type' => self::LD], + 'json' => $this->validMainPayload('RG104 Admin', $cat->getId()), + ]); + self::assertResponseStatusCodeSame(201); + } + + private function authAs(string $role): Client + { + return $this->authenticatedClient($role, self::PWD); + } + + /** + * Payload minimal valide de l'onglet principal (RG-1.01 : un nom de contact ; + * une categorie SECTEUR). Si $categoryId est null, une categorie est creee a + * la volee. + * + * @return array + */ + private function validMainPayload(string $companyName, ?int $categoryId = null): array + { + $categoryId ??= $this->createCategory('SECTEUR')->getId(); + + return [ + 'companyName' => $companyName, + 'firstName' => 'Jean', + 'phonePrimary' => '0612345678', + 'email' => strtolower(str_replace(' ', '', $companyName)).'@matrix.test', + 'categories' => ['/api/categories/'.$categoryId], + ]; + } +}