Files
Starseed/tests/Module/Transport/Api/CarrierRBACMatrixTest.php
T
Matthieu 13d4a08bc9 feat(transport) : CarrierProcessor + champs conditionnels formulaire principal (ERP-158)
Ecriture du formulaire principal transporteur (M4, WT4) : POST/PATCH via
CarrierProcessor + CarrierFieldNormalizer, contraintes conditionnelles sur
l'entite Carrier.

- RG-4.01 : POST qualimatCarrier -> certificationType=QUALIMAT + FK persistee ;
  cas LIOT (name=LIOT) -> certification non requise, liotPlates accepte.
- RG-4.02 : certificationType=AUTRE sans dischargeDocument -> 422 (Assert\Callback).
- RG-4.03 : isChartered=true sans indexationRate/containerType/volumeM3 -> 422.
- RG-4.12 : doublon de nom (parmi actifs) -> 409 (index partiel uq_carrier_name_active).
- RG-4.13 : normalisation serveur (name UPPER, liotPlates ;-split/trim/UPPER) +
  methodes personne/telephone/email pour les sous-ressources Contact (WT7).
- RG-4.14 : PATCH isArchived exige transport.carriers.archive (Admin seul),
  mode strict -> 403 + 422 si autre champ ; restauration en conflit -> 409.

Operations Post/Patch ajoutees a l'ApiResource (lecture posee au WT3 conservee).
RG conditionnelles portees par validateMainFormConsistency (Assert\Callback +
->atPath()) pour un propertyPath mappable inline (useFormErrors, ERP-101).

certificationType / containerType whitelistes dans EXCLUDED_LENGTH_MIRROR (Choice
borne deja les valeurs, miroir SupplierAddress::addressType).

Tests : CarrierWriteApiTest (RG-4.01->4.03/4.12->4.14), CarrierRBACMatrixTest
(matrice bureau/compta/commerciale/usine), CarrierArchiveTest (409 restauration),
CarrierFieldNormalizerTest (RG-4.13). make test vert (750).
2026-06-16 15:13:10 +02:00

159 lines
5.6 KiB
PHP

<?php
declare(strict_types=1);
namespace App\Tests\Module\Transport\Api;
use ApiPlatform\Symfony\Bundle\Test\Client;
use App\Module\Core\Infrastructure\DataFixtures\RbacDemoFixtures;
use Symfony\Bundle\FrameworkBundle\Console\Application;
use Symfony\Component\Console\Input\ArrayInput;
use Symfony\Component\Console\Output\NullOutput;
/**
* Matrice RBAC du repertoire transporteurs par role metier (spec-back M4 § 5.2 +
* ERP-153/158). Valide 200/403 par verbe pour bureau / compta / commerciale /
* usine ; l'archivage reste admin seul (gating CarrierProcessor, RG-4.14). Jumeau
* de SupplierRBACMatrixTest (M2).
*
* Matrice § 5.2 — rappel :
* - bureau : view + manage (PAS archive)
* - commerciale : view seul (ni manage ni archive)
* - compta : aucun acces (403 sur view ET manage)
* - usine : aucun acces (403 partout)
* - archive : admin seul
*
* @internal
*/
final class CarrierRBACMatrixTest extends AbstractCarrierApiTestCase
{
private const string PWD = RbacDemoFixtures::DEMO_PASSWORD;
protected function setUp(): void
{
parent::setUp();
// Seed idempotent via la commande applicative (roles + matrice § 5.2 +
// comptes demo) — meme chemin qu'en recette.
self::bootKernel();
$application = new Application(self::$kernel);
$application->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 transport.carriers.* sont-elles synchronisees (app:sync-permissions) ?',
);
self::ensureKernelShutdown();
}
public function testUsineIsForbiddenEverywhere(): void
{
$seed = $this->seedCarrier('Usine Target');
$client = $this->authAs('usine');
$client->request('GET', '/api/carriers', ['headers' => ['Accept' => self::LD]]);
self::assertResponseStatusCodeSame(403);
$client->request('GET', '/api/carriers/'.$seed->getId(), ['headers' => ['Accept' => self::LD]]);
self::assertResponseStatusCodeSame(403);
$client->request('POST', '/api/carriers', [
'headers' => ['Content-Type' => self::LD],
'json' => $this->validMainPayload('Usine Post'),
]);
self::assertResponseStatusCodeSame(403);
}
public function testComptaHasNoAccess(): void
{
$seed = $this->seedCarrier('Compta Target');
$client = $this->authAs('compta');
// PAS view (matrice § 5.2 : Compta sans acces transporteurs).
$client->request('GET', '/api/carriers', ['headers' => ['Accept' => self::LD]]);
self::assertResponseStatusCodeSame(403);
// PAS manage : creation refusee.
$client->request('POST', '/api/carriers', [
'headers' => ['Content-Type' => self::LD],
'json' => $this->validMainPayload('Compta Post'),
]);
self::assertResponseStatusCodeSame(403);
$client->request('PATCH', '/api/carriers/'.$seed->getId(), [
'headers' => ['Content-Type' => self::MERGE],
'json' => ['name' => 'Renamed By Compta'],
]);
self::assertResponseStatusCodeSame(403);
}
public function testBureauHasViewAndManageButNoArchive(): void
{
$seed = $this->seedCarrier('Bureau Target');
$client = $this->authAs('bureau');
// view
$client->request('GET', '/api/carriers', ['headers' => ['Accept' => self::LD]]);
self::assertResponseStatusCodeSame(200);
// manage : creation OK
$client->request('POST', '/api/carriers', [
'headers' => ['Content-Type' => self::LD],
'json' => $this->validMainPayload('Bureau Created'),
]);
self::assertResponseStatusCodeSame(201);
// manage : edition formulaire principal OK
$client->request('PATCH', '/api/carriers/'.$seed->getId(), [
'headers' => ['Content-Type' => self::MERGE],
'json' => ['name' => 'Bureau Renamed'],
]);
self::assertResponseStatusCodeSame(200);
// PAS archive : archivage refuse (RG-4.14, gating CarrierProcessor).
$client->request('PATCH', '/api/carriers/'.$seed->getId(), [
'headers' => ['Content-Type' => self::MERGE],
'json' => ['isArchived' => true],
]);
self::assertResponseStatusCodeSame(403);
}
public function testCommercialeHasViewOnly(): void
{
$seed = $this->seedCarrier('Commerciale Target');
$client = $this->authAs('commerciale');
// view (consultation « Tout »)
$client->request('GET', '/api/carriers', ['headers' => ['Accept' => self::LD]]);
self::assertResponseStatusCodeSame(200);
// PAS manage : creation refusee
$client->request('POST', '/api/carriers', [
'headers' => ['Content-Type' => self::LD],
'json' => $this->validMainPayload('Commerciale Post'),
]);
self::assertResponseStatusCodeSame(403);
// PAS manage : edition refusee
$client->request('PATCH', '/api/carriers/'.$seed->getId(), [
'headers' => ['Content-Type' => self::MERGE],
'json' => ['name' => 'Renamed By Commerciale'],
]);
self::assertResponseStatusCodeSame(403);
}
private function authAs(string $role): Client
{
return $this->authenticatedClient($role, self::PWD);
}
}