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.suppliers.* sont-elles synchronisees (app:sync-permissions) ?', ); self::ensureKernelShutdown(); } public function testUsineIsForbiddenEverywhere(): void { $seed = $this->seedSupplier('Usine Target'); $client = $this->authAs('usine'); $client->request('GET', '/api/suppliers', ['headers' => ['Accept' => self::LD]]); self::assertResponseStatusCodeSame(403); $client->request('GET', '/api/suppliers/'.$seed->getId(), ['headers' => ['Accept' => self::LD]]); self::assertResponseStatusCodeSame(403); $client->request('POST', '/api/suppliers', [ 'headers' => ['Content-Type' => self::LD], 'json' => $this->validMainPayload('Usine Post'), ]); self::assertResponseStatusCodeSame(403); $client->request('PATCH', '/api/suppliers/'.$seed->getId(), [ 'headers' => ['Content-Type' => self::MERGE], 'json' => ['companyName' => 'Renamed By Usine'], ]); self::assertResponseStatusCodeSame(403); } public function testBureauHasViewAndManageButNoAccountingNoArchive(): void { $seed = $this->seedSupplier('Bureau Target'); $cat = $this->supplierCategory('NEGOCIANT'); $client = $this->authAs('bureau'); // view $client->request('GET', '/api/suppliers', ['headers' => ['Accept' => self::LD]]); self::assertResponseStatusCodeSame(200); // manage : creation OK $client->request('POST', '/api/suppliers', [ 'headers' => ['Content-Type' => self::LD], 'json' => $this->validMainPayload('Bureau Created', $cat->getId()), ]); self::assertResponseStatusCodeSame(201); // manage : edition onglet principal OK $client->request('PATCH', '/api/suppliers/'.$seed->getId(), [ 'headers' => ['Content-Type' => self::MERGE], 'json' => ['companyName' => 'Bureau Renamed'], ]); self::assertResponseStatusCodeSame(200); // PAS accounting : edition onglet Comptabilite refusee $client->request('PATCH', '/api/suppliers/'.$seed->getId(), [ 'headers' => ['Content-Type' => self::MERGE], 'json' => ['siren' => '123456789'], ]); self::assertResponseStatusCodeSame(403); // PAS archive : archivage refuse $client->request('PATCH', '/api/suppliers/'.$seed->getId(), [ 'headers' => ['Content-Type' => self::MERGE], 'json' => ['isArchived' => true], ]); self::assertResponseStatusCodeSame(403); } public function testBureauDetailHasNoAccountingFields(): void { // Bureau a view mais PAS accounting.view : les champs comptables sont // ABSENTS du JSON (gating par omission, pas null). $supplier = $this->seedCompleteSupplier('Bureau Gating Co'); $client = $this->authAs('bureau'); $data = $client->request('GET', '/api/suppliers/'.$supplier->getId(), ['headers' => ['Accept' => self::LD]])->toArray(); // Gating par omission sur l'ensemble des champs comptables (pas seulement // siren/ribs) : une regression reintroduisant accountNumber/nTva/tvaMode/ // paymentType dans le groupe bureau serait sinon invisible. self::assertArrayNotHasKey('siren', $data); self::assertArrayNotHasKey('accountNumber', $data); self::assertArrayNotHasKey('nTva', $data); self::assertArrayNotHasKey('tvaMode', $data); self::assertArrayNotHasKey('paymentType', $data); self::assertArrayNotHasKey('ribs', $data); } public function testComptaCanEditAccountingOnly(): void { $seed = $this->seedSupplier('Compta Target'); $client = $this->authAs('compta'); // view $client->request('GET', '/api/suppliers', ['headers' => ['Accept' => self::LD]]); self::assertResponseStatusCodeSame(200); // PAS manage : creation refusee $client->request('POST', '/api/suppliers', [ 'headers' => ['Content-Type' => self::LD], 'json' => $this->validMainPayload('Compta Post'), ]); self::assertResponseStatusCodeSame(403); // accounting.manage : edition onglet Comptabilite OK $client->request('PATCH', '/api/suppliers/'.$seed->getId(), [ 'headers' => ['Content-Type' => self::MERGE], 'json' => ['siren' => '123456789'], ]); self::assertResponseStatusCodeSame(200); // PAS manage : edition onglet principal refusee (guardManage) $client->request('PATCH', '/api/suppliers/'.$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/suppliers/'.$seed->getId(), [ 'headers' => ['Content-Type' => self::MERGE], 'json' => ['description' => 'Une description'], ]); self::assertResponseStatusCodeSame(403); // PAS archive : archivage refuse $client->request('PATCH', '/api/suppliers/'.$seed->getId(), [ 'headers' => ['Content-Type' => self::MERGE], 'json' => ['isArchived' => true], ]); self::assertResponseStatusCodeSame(403); } public function testComptaDetailHasAccountingFields(): void { // Compta a accounting.view : siren + ribs presents dans le JSON. $supplier = $this->seedCompleteSupplier('Compta View Co'); $client = $this->authAs('compta'); $data = $client->request('GET', '/api/suppliers/'.$supplier->getId(), ['headers' => ['Accept' => self::LD]])->toArray(); self::assertArrayHasKey('siren', $data); self::assertSame('123456789', $data['siren']); self::assertArrayHasKey('ribs', $data); self::assertNotEmpty($data['ribs']); } public function testCommercialeHasViewAndManageButNoAccountingNoArchive(): void { $seed = $this->seedSupplier('Commerciale Target'); $client = $this->authAs('commerciale'); // view $client->request('GET', '/api/suppliers', ['headers' => ['Accept' => self::LD]]); self::assertResponseStatusCodeSame(200); // manage : la creation passe la security d'operation (pas un 403 comme // Compta) -> 201. L'onglet Information est facultatif (RG-2.03 retiree, // miroir client M1) : une Commerciale cree avec le seul onglet principal. $client->request('POST', '/api/suppliers', [ 'headers' => ['Content-Type' => self::LD], 'json' => $this->validMainPayload('Commerciale Post'), ]); self::assertResponseStatusCodeSame(201); // PAS accounting : edition onglet Comptabilite refusee $client->request('PATCH', '/api/suppliers/'.$seed->getId(), [ 'headers' => ['Content-Type' => self::MERGE], 'json' => ['siren' => '123456789'], ]); self::assertResponseStatusCodeSame(403); // PAS archive : archivage refuse $client->request('PATCH', '/api/suppliers/'.$seed->getId(), [ 'headers' => ['Content-Type' => self::MERGE], 'json' => ['isArchived' => true], ]); self::assertResponseStatusCodeSame(403); } public function testCommercialeDetailHasNoAccountingFields(): void { $supplier = $this->seedCompleteSupplier('Commerciale Gating Co'); $client = $this->authAs('commerciale'); $data = $client->request('GET', '/api/suppliers/'.$supplier->getId(), ['headers' => ['Accept' => self::LD]])->toArray(); self::assertArrayNotHasKey('siren', $data); self::assertArrayNotHasKey('accountNumber', $data); self::assertArrayNotHasKey('nTva', $data); self::assertArrayNotHasKey('tvaMode', $data); self::assertArrayNotHasKey('paymentType', $data); self::assertArrayNotHasKey('ribs', $data); } private function authAs(string $role): Client { return $this->authenticatedClient($role, self::PWD); } }