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); } public function testComptaFullRepresentationPatchWithUnchangedCategoriesIsNotForbidden(): void { // FIX review MR #40 : un Compta (accounting.manage, PAS manage) faisant un // PATCH representation complete de l'onglet Comptabilite et reincluant ses // categories INCHANGEES ne doit PAS prendre de 403. guardManage compare // desormais les categories par valeur (et non par simple presence) : seul // l'onglet Comptabilite change ici -> 200. $seed = $this->seedClient('Compta Cat Unchanged'); $category = $seed->getCategories()->first(); self::assertNotFalse($category); $catId = $category->getId(); $client = $this->authAs('compta'); $client->request('PATCH', '/api/clients/'.$seed->getId(), [ 'headers' => ['Content-Type' => self::MERGE], 'json' => [ 'siren' => '123456789', 'categories' => ['/api/categories/'.$catId], ], ]); self::assertResponseStatusCodeSame(200); } public function testComptaChangingCategoriesIsForbidden(): void { // Non-regression : si le Compta change REELLEMENT l'ensemble des // categories (sans manage) -> 403 via guardManage. La comparaison par // valeur detecte bien le changement. $seed = $this->seedClient('Compta Cat Change'); $newCat = $this->createCategory('SECTEUR'); $client = $this->authAs('compta'); $client->request('PATCH', '/api/clients/'.$seed->getId(), [ 'headers' => ['Content-Type' => self::MERGE], 'json' => ['categories' => ['/api/categories/'.$newCat->getId()]], ]); self::assertResponseStatusCodeSame(403); } public function testBureauChangingCategoriesIsAllowed(): void { // Non-regression : un role porteur de `manage` (Bureau) peut changer les // categories -> 200. $seed = $this->seedClient('Bureau Cat Change'); $newCat = $this->createCategory('SECTEUR'); $client = $this->authAs('bureau'); $client->request('PATCH', '/api/clients/'.$seed->getId(), [ 'headers' => ['Content-Type' => self::MERGE], 'json' => ['categories' => ['/api/categories/'.$newCat->getId()]], ]); self::assertResponseStatusCodeSame(200); } 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], ]; } }