Files
Starseed/tests/Module/Transport/Api/CarrierAddressApiTest.php
T
Matthieu 54ac034c1b feat(transport) : sous-ressource adresses transporteur (ERP-159)
POST /api/carriers/{id}/addresses + PATCH/DELETE /api/carrier_addresses/{id}
(security transport.carriers.manage), spec-back § 4.5. Jumelle de SupplierAddress
(M2) / ProviderAddress (M3), sans address_type ni M2M.

- CarrierAddress : ajout #[ApiResource] (Get/Post/Patch/Delete) + groupe
  d'ecriture carrier:write:addresses + contraintes FR. RG-4.06 : code postal
  ^[0-9]{4,5}$ (Assert\Regex). Mapping ORM/colonnes inchange.
- CarrierAddressProcessor : rattachement parent (404 si absent) + RG-4.05
  (transporteur affrete -> Pays/CP/Ville/Adresse obligatoires, 422 par champ).
  RG-4.05 portee par le processor car le parent est indisponible a la validation
  Symfony sur un POST sous-ressource read:false. RG-4.07 = front (PATCH accepte).
- EXCLUDED_LENGTH_MIRROR : CarrierAddress::postalCode (Regex borne la longueur).
- Tests : CP invalide 422, affrete incomplet 422, affrete complet 201,
  PATCH/DELETE OK (manage), 403 sans manage.
2026-06-16 09:19:20 +02:00

173 lines
6.3 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 App\Module\Transport\Domain\Entity\Carrier;
use App\Module\Transport\Domain\Entity\CarrierAddress;
use Symfony\Bundle\FrameworkBundle\Console\Application;
use Symfony\Component\Console\Input\ArrayInput;
use Symfony\Component\Console\Output\NullOutput;
/**
* Sous-ressource Adresse d'un transporteur (spec-back M4 § 4.5, ERP-159).
* POST /api/carriers/{id}/addresses, PATCH/DELETE /api/carrier_addresses/{id}.
*
* Contrat verifie :
* - RG-4.06 : code postal hors ^[0-9]{4,5}$ -> 422 ;
* - RG-4.05 : transporteur affrete + adresse incomplete -> 422 (par champ) ;
* - RG-4.05 : transporteur affrete + adresse complete -> 201 ;
* - PATCH / DELETE OK avec transport.carriers.manage, 403 sans (view seul).
*
* @internal
*/
final class CarrierAddressApiTest extends AbstractCarrierApiTestCase
{
private const string PWD = RbacDemoFixtures::DEMO_PASSWORD;
protected function setUp(): void
{
parent::setUp();
// Seed idempotent des roles + matrice § 5.2 + comptes demo (meme chemin
// qu'en recette), requis pour les tests de permission (bureau/commerciale).
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 (permissions transport.carriers.* synchronisees ?).');
self::ensureKernelShutdown();
}
public function testInvalidPostalCodeReturns422(): void
{
// Transporteur NON affrete : RG-4.05 ne s'applique pas, seule RG-4.06 joue.
$carrier = $this->seedCarrierWithChartered('Cp Invalide', false);
$client = $this->createAdminClient();
$client->request('POST', '/api/carriers/'.$carrier->getId().'/addresses', [
'headers' => ['Content-Type' => self::LD],
'json' => ['postalCode' => '123'], // 3 chiffres -> Regex KO
]);
self::assertResponseStatusCodeSame(422);
}
public function testCharteredCarrierIncompleteAddressReturns422(): void
{
// Transporteur affrete : RG-4.05 exige Pays/CP/Ville/Adresse. CP valide mais
// ville + rue manquantes -> 422 conditionnelle (CarrierAddressProcessor).
$carrier = $this->seedCarrierWithChartered('Affrete Incomplet', true);
$client = $this->createAdminClient();
$client->request('POST', '/api/carriers/'.$carrier->getId().'/addresses', [
'headers' => ['Content-Type' => self::LD],
'json' => ['postalCode' => '86000'],
]);
self::assertResponseStatusCodeSame(422);
}
public function testCharteredCarrierCompleteAddressIsCreated(): void
{
$carrier = $this->seedCarrierWithChartered('Affrete Complet', true);
$client = $this->createAdminClient();
$client->request('POST', '/api/carriers/'.$carrier->getId().'/addresses', [
'headers' => ['Content-Type' => self::LD],
'json' => [
'country' => 'France',
'postalCode' => '86000',
'city' => 'Poitiers',
'street' => '12 rue des Acacias',
],
]);
self::assertResponseStatusCodeSame(201);
}
public function testPatchAndDeleteSucceedWithManage(): void
{
$address = $this->seedAddress('Patch Delete', false);
$client = $this->authenticatedClient('bureau', self::PWD); // manage (matrice § 5.2)
// PATCH (manage) -> 200
$client->request('PATCH', '/api/carrier_addresses/'.$address->getId(), [
'headers' => ['Content-Type' => self::MERGE],
'json' => ['city' => 'Lyon'],
]);
self::assertResponseStatusCodeSame(200);
// DELETE (manage) -> 204
$client->request('DELETE', '/api/carrier_addresses/'.$address->getId());
self::assertResponseStatusCodeSame(204);
}
public function testWriteForbiddenWithoutManage(): void
{
$address = $this->seedAddress('Forbidden', false);
$carrier = $address->getCarrier();
self::assertNotNull($carrier);
$client = $this->authenticatedClient('commerciale', self::PWD); // view seul
$client->request('POST', '/api/carriers/'.$carrier->getId().'/addresses', [
'headers' => ['Content-Type' => self::LD],
'json' => ['postalCode' => '86000', 'city' => 'Poitiers', 'street' => '1 rue X'],
]);
self::assertResponseStatusCodeSame(403);
$client->request('PATCH', '/api/carrier_addresses/'.$address->getId(), [
'headers' => ['Content-Type' => self::MERGE],
'json' => ['city' => 'Lyon'],
]);
self::assertResponseStatusCodeSame(403);
$client->request('DELETE', '/api/carrier_addresses/'.$address->getId());
self::assertResponseStatusCodeSame(403);
}
/**
* Seede un transporteur minimal en controlant le flag affrete (RG-4.05).
*/
private function seedCarrierWithChartered(string $name, bool $isChartered): Carrier
{
$em = $this->getEm();
$carrier = new Carrier();
$carrier->setName(mb_strtoupper($name, 'UTF-8'));
$carrier->setCertificationType('GMP_PLUS');
$carrier->setIsChartered($isChartered);
$em->persist($carrier);
$em->flush();
return $carrier;
}
/**
* Seede un transporteur + une adresse rattachee (pour les tests PATCH/DELETE).
*/
private function seedAddress(string $name, bool $isChartered): CarrierAddress
{
$em = $this->getEm();
$carrier = $this->seedCarrierWithChartered($name, $isChartered);
$address = new CarrierAddress();
$address->setCarrier($carrier);
$address->setPostalCode('86000');
$address->setCity('Poitiers');
$address->setStreet('12 rue des Acacias');
$carrier->addAddress($address);
$em->persist($address);
$em->flush();
return $address;
}
}