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 technique.providers.* sont-elles synchronisees (app:sync-permissions) ?', ); self::ensureKernelShutdown(); } public function testBureauHasViewAndManageButNoAccountingNoArchive(): void { $seed = $this->seedProvider('Bureau Cible'); $client = $this->authAs('bureau'); // view $client->request('GET', '/api/providers', ['headers' => ['Accept' => self::LD]]); self::assertResponseStatusCodeSame(200); // manage : creation OK (bypass_scope -> peut attacher le site 86) $client->request('POST', '/api/providers', [ 'headers' => ['Content-Type' => self::LD], 'json' => $this->validMainPayload('Bureau Cree'), ]); self::assertResponseStatusCodeSame(201); // manage : edition onglet principal OK $client->request('PATCH', '/api/providers/'.$seed->getId(), [ 'headers' => ['Content-Type' => self::MERGE], 'json' => ['companyName' => 'Bureau Renomme'], ]); self::assertResponseStatusCodeSame(200); // PAS accounting : edition onglet Comptabilite refusee $client->request('PATCH', '/api/providers/'.$seed->getId(), [ 'headers' => ['Content-Type' => self::MERGE], 'json' => ['siren' => '123456789'], ]); self::assertResponseStatusCodeSame(403); // PAS archive : archivage refuse $client->request('PATCH', '/api/providers/'.$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). $provider = $this->seedProvider('Bureau Gating Co', [self::SITE_86], siren: '123456789'); $client = $this->authAs('bureau'); $data = $client->request('GET', '/api/providers/'.$provider->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); } public function testComptaCanEditAccountingOnly(): void { $seed = $this->seedProvider('Compta Cible'); $client = $this->authAs('compta'); // view $client->request('GET', '/api/providers', ['headers' => ['Accept' => self::LD]]); self::assertResponseStatusCodeSame(200); // PAS manage : creation refusee $client->request('POST', '/api/providers', [ 'headers' => ['Content-Type' => self::LD], 'json' => $this->validMainPayload('Compta Post'), ]); self::assertResponseStatusCodeSame(403); // accounting.manage : edition onglet Comptabilite OK $client->request('PATCH', '/api/providers/'.$seed->getId(), [ 'headers' => ['Content-Type' => self::MERGE], 'json' => ['siren' => '123456789'], ]); self::assertResponseStatusCodeSame(200); // PAS manage : edition onglet principal refusee (mode strict RG-3.15) $client->request('PATCH', '/api/providers/'.$seed->getId(), [ 'headers' => ['Content-Type' => self::MERGE], 'json' => ['companyName' => 'Compta Renomme'], ]); self::assertResponseStatusCodeSame(403); // PAS archive : archivage refuse $client->request('PATCH', '/api/providers/'.$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. $provider = $this->seedProvider('Compta View Co', [self::SITE_86], siren: '987654321'); $this->addRib($provider); $client = $this->authAs('compta'); $data = $client->request('GET', '/api/providers/'.$provider->getId(), ['headers' => ['Accept' => self::LD]])->toArray(); self::assertArrayHasKey('siren', $data); self::assertSame('987654321', $data['siren']); self::assertArrayHasKey('ribs', $data); self::assertNotEmpty($data['ribs']); } public function testCommercialeHasViewAndManageButNoAccountingNoArchive(): void { $seed = $this->seedProvider('Commerciale Cible'); $client = $this->authAs('commerciale'); // view $client->request('GET', '/api/providers', ['headers' => ['Accept' => self::LD]]); self::assertResponseStatusCodeSame(200); // manage : creation OK $client->request('POST', '/api/providers', [ 'headers' => ['Content-Type' => self::LD], 'json' => $this->validMainPayload('Commerciale Cree'), ]); self::assertResponseStatusCodeSame(201); // PAS accounting : edition onglet Comptabilite refusee $client->request('PATCH', '/api/providers/'.$seed->getId(), [ 'headers' => ['Content-Type' => self::MERGE], 'json' => ['siren' => '123456789'], ]); self::assertResponseStatusCodeSame(403); // PAS archive : archivage refuse $client->request('PATCH', '/api/providers/'.$seed->getId(), [ 'headers' => ['Content-Type' => self::MERGE], 'json' => ['isArchived' => true], ]); self::assertResponseStatusCodeSame(403); } public function testCommercialeDetailHasNoAccountingFields(): void { $provider = $this->seedProvider('Commerciale Gating Co', [self::SITE_86], siren: '123456789'); $client = $this->authAs('commerciale'); $data = $client->request('GET', '/api/providers/'.$provider->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); } public function testUsineHasReadOnlyAccessScopedToItsSite(): void { // Usine a view (lecture seule), SANS manage / accounting / archive, et // SANS bypass_scope -> cloisonnee a son site courant (Chatellerault, // site 86, pose par ensureDemoUsers). $inScope = $this->seedProvider('Usine InScope', [self::SITE_86]); $client = $this->authAs('usine'); // view : liste OK (pas un 403 comme au M2) $client->request('GET', '/api/providers', ['headers' => ['Accept' => self::LD]]); self::assertResponseStatusCodeSame(200); // view : detail d'un prestataire de SON site OK $client->request('GET', '/api/providers/'.$inScope->getId(), ['headers' => ['Accept' => self::LD]]); self::assertResponseStatusCodeSame(200); // PAS manage : creation refusee $client->request('POST', '/api/providers', [ 'headers' => ['Content-Type' => self::LD], 'json' => $this->validMainPayload('Usine Post'), ]); self::assertResponseStatusCodeSame(403); // PAS manage : edition onglet principal refusee $client->request('PATCH', '/api/providers/'.$inScope->getId(), [ 'headers' => ['Content-Type' => self::MERGE], 'json' => ['companyName' => 'Renomme Par Usine'], ]); self::assertResponseStatusCodeSame(403); // PAS accounting : edition onglet Comptabilite refusee $client->request('PATCH', '/api/providers/'.$inScope->getId(), [ 'headers' => ['Content-Type' => self::MERGE], 'json' => ['siren' => '123456789'], ]); self::assertResponseStatusCodeSame(403); // PAS archive : archivage refuse $client->request('PATCH', '/api/providers/'.$inScope->getId(), [ 'headers' => ['Content-Type' => self::MERGE], 'json' => ['isArchived' => true], ]); self::assertResponseStatusCodeSame(403); } public function testUsineCannotSeeProviderOutOfItsSite(): void { // Cloisonnement § 2.13 : un prestataire hors du site courant de l'Usine // (site 17, l'Usine est sur le site 86) -> 404 (ne pas reveler la ligne). $outOfScope = $this->seedProvider('Usine OutOfScope', [self::SITE_17]); $client = $this->authAs('usine'); $client->request('GET', '/api/providers/'.$outOfScope->getId(), ['headers' => ['Accept' => self::LD]]); self::assertResponseStatusCodeSame(404); } private function authAs(string $role): Client { return $this->authenticatedClient($role, self::PWD); } }