Files
Starseed/tests/Module/Transport/Api/CarrierWriteApiTest.php
T
Matthieu c1fcd9a7c8
Pull Request — Quality gate / Backend (PHP CS + PHPUnit) (pull_request) Failing after 54s
Pull Request — Quality gate / Frontend (lint + Vitest + build) (pull_request) Successful in 1m33s
test(transport) : rigueur RG sous-ressources (propertyPath, 404 parent, 401, certif)
Repond aux retours de review (rigueur d'assertion transversale) :
- mutualise assertViolationOnPath dans AbstractCarrierApiTestCase (au lieu d'un
  duplicata local a CarrierWriteApiTest) ;
- asserte le propertyPath des 422 des sous-ressources (adresses city/street/postalCode,
  contacts firstName/phones/email, prix clientDeliveryAddress/supplierSupplyAddress/price)
  -> evite les faux-verts du mapping inline (ERP-101) ;
- 404 parent (POST sur /carriers/999999/{addresses,contacts,prices}) ;
- 401 anonyme + filtre ?certificationType= sur la collection (trous releves sur le
  contrat de lecture).
2026-06-16 15:13:11 +02:00

225 lines
8.5 KiB
PHP

<?php
declare(strict_types=1);
namespace App\Tests\Module\Transport\Api;
/**
* Ecriture du formulaire principal transporteur (M4, WT4 — ERP-158). Couvre les
* RG du CarrierProcessor + contraintes conditionnelles : RG-4.01 (QUALIMAT + cas
* LIOT), RG-4.02 (AUTRE -> decharge), RG-4.03 (affrete -> indexation/benne/volume),
* RG-4.12 (doublon de nom -> 409), RG-4.13 (normalisation), RG-4.14 (archivage +
* mode strict). Jumeau des SupplierApiTest / SupplierPatchStrictTest (M2).
*
* @internal
*/
final class CarrierWriteApiTest extends AbstractCarrierApiTestCase
{
/**
* RG-4.01 : POST avec qualimatCarrier -> certificationType=QUALIMAT accepte et
* FK persistee (verifiee au detail, qualimatCarrier embarque).
*/
public function testPostQualimatPersistsCertificationAndForeignKey(): void
{
$client = $this->createAdminClient();
$qualimat = $this->seedQualimatCarrier('Transports Grelillier');
$created = $client->request('POST', '/api/carriers', [
'headers' => ['Content-Type' => self::LD],
'json' => [
'name' => 'Transports Grelillier',
'qualimatCarrier' => '/api/qualimat_carriers/'.$qualimat->getId(),
'certificationType' => 'QUALIMAT',
'isChartered' => false,
],
])->toArray();
self::assertResponseStatusCodeSame(201);
self::assertSame('QUALIMAT', $created['certificationType']);
$detail = $client->request('GET', $created['@id'], ['headers' => ['Accept' => self::LD]])->toArray();
self::assertIsArray($detail['qualimatCarrier']);
self::assertSame((int) $qualimat->getId(), (int) $detail['qualimatCarrier']['id']);
}
/**
* RG-4.01 (cas LIOT) : nom = LIOT -> certificationType non requis (champ masque)
* et liotPlates accepte (et normalise, RG-4.13).
*/
public function testPostLiotAcceptsPlatesWithoutCertification(): void
{
$client = $this->createAdminClient();
$created = $client->request('POST', '/api/carriers', [
'headers' => ['Content-Type' => self::LD],
'json' => [
'name' => 'LIOT',
'liotPlates' => 'ab-123-cd ; ef-456-gh',
'isChartered' => false,
],
])->toArray();
self::assertResponseStatusCodeSame(201);
self::assertNull($created['certificationType']);
self::assertSame('AB-123-CD; EF-456-GH', $created['liotPlates']);
}
/**
* RG-4.01 : hors cas LIOT, l'absence de certification est rejetee (422 cible
* sur certificationType).
*/
public function testPostWithoutCertificationOutsideLiotIsRejected(): void
{
$client = $this->createAdminClient();
$response = $client->request('POST', '/api/carriers', [
'headers' => ['Content-Type' => self::LD],
'json' => ['name' => 'Sans Certif', 'isChartered' => false],
]);
self::assertResponseStatusCodeSame(422);
self::assertViolationOnPath($response, 'certificationType');
}
/**
* RG-4.02 : certificationType=AUTRE sans dischargeDocument -> 422 cible ; une
* certification != AUTRE sans decharge passe (201).
*/
public function testAutreCertificationRequiresDischarge(): void
{
$client = $this->createAdminClient();
$response = $client->request('POST', '/api/carriers', [
'headers' => ['Content-Type' => self::LD],
'json' => ['name' => 'Sans Decharge', 'certificationType' => 'AUTRE', 'isChartered' => false],
]);
self::assertResponseStatusCodeSame(422);
self::assertViolationOnPath($response, 'dischargeDocument');
// Certification != AUTRE : pas de decharge requise.
$client->request('POST', '/api/carriers', [
'headers' => ['Content-Type' => self::LD],
'json' => ['name' => 'Avec GmpPlus', 'certificationType' => 'GMP_PLUS', 'isChartered' => false],
]);
self::assertResponseStatusCodeSame(201);
}
/**
* RG-4.03 : isChartered=true sans indexationRate / containerType / volumeM3 ->
* 422 (violations ciblees) ; complet -> 201.
*/
public function testCharteredRequiresIndexationContainerAndVolume(): void
{
$client = $this->createAdminClient();
$response = $client->request('POST', '/api/carriers', [
'headers' => ['Content-Type' => self::LD],
'json' => ['name' => 'Affrete Incomplet', 'certificationType' => 'GMP_PLUS', 'isChartered' => true],
]);
self::assertResponseStatusCodeSame(422);
self::assertViolationOnPath($response, 'indexationRate');
self::assertViolationOnPath($response, 'containerType');
self::assertViolationOnPath($response, 'volumeM3');
$client->request('POST', '/api/carriers', [
'headers' => ['Content-Type' => self::LD],
'json' => [
'name' => 'Affrete Complet',
'certificationType' => 'GMP_PLUS',
'isChartered' => true,
'indexationRate' => '5.00',
'containerType' => 'BENNE',
'volumeM3' => '90.00',
],
]);
self::assertResponseStatusCodeSame(201);
}
/**
* RG-4.12 : nom deja pris (parmi actifs) -> 409. Le meme nom redevient
* disponible apres archivage de l'ancien -> 201.
*/
public function testDuplicateNameReturns409AndIsFreedAfterArchive(): void
{
$client = $this->createAdminClient();
$existing = $this->seedCarrier('Doublon Co');
$client->request('POST', '/api/carriers', [
'headers' => ['Content-Type' => self::LD],
'json' => $this->validMainPayload('Doublon Co'),
]);
self::assertResponseStatusCodeSame(409);
// Archivage de l'ancien -> le nom se libere (index partiel sur actifs).
$client->request('PATCH', '/api/carriers/'.$existing->getId(), [
'headers' => ['Content-Type' => self::MERGE],
'json' => ['isArchived' => true],
]);
self::assertResponseStatusCodeSame(200);
$client->request('POST', '/api/carriers', [
'headers' => ['Content-Type' => self::LD],
'json' => $this->validMainPayload('Doublon Co'),
]);
self::assertResponseStatusCodeSame(201);
}
/**
* RG-4.13 : le nom est persiste en MAJUSCULES (normalisation serveur).
*/
public function testNameIsUpperCasedOnPersist(): void
{
$client = $this->createAdminClient();
$created = $client->request('POST', '/api/carriers', [
'headers' => ['Content-Type' => self::LD],
'json' => $this->validMainPayload('transports x'),
])->toArray();
self::assertResponseStatusCodeSame(201);
self::assertSame('TRANSPORTS X', $created['name']);
}
/**
* RG-4.14 : PATCH isArchived=true par Admin -> 200 + archivedAt rempli ;
* restauration -> archivedAt remis a null.
*/
public function testAdminArchiveSetsArchivedAtAndRestoreClearsIt(): void
{
$client = $this->createAdminClient();
$carrier = $this->seedCarrier('A Archiver');
$archived = $client->request('PATCH', '/api/carriers/'.$carrier->getId(), [
'headers' => ['Content-Type' => self::MERGE],
'json' => ['isArchived' => true],
])->toArray();
self::assertResponseStatusCodeSame(200);
self::assertTrue($archived['isArchived']);
self::assertNotNull($archived['archivedAt']);
$restored = $client->request('PATCH', '/api/carriers/'.$carrier->getId(), [
'headers' => ['Content-Type' => self::MERGE],
'json' => ['isArchived' => false],
])->toArray();
self::assertResponseStatusCodeSame(200);
self::assertFalse($restored['isArchived']);
self::assertNull($restored['archivedAt']);
}
/**
* RG-4.14 (mode strict) : une requete d'archivage ne peut modifier aucun autre
* champ ecrivable -> 422.
*/
public function testArchiveRequestMixingOtherFieldIsRejected(): void
{
$client = $this->createAdminClient();
$carrier = $this->seedCarrier('Strict Co');
$client->request('PATCH', '/api/carriers/'.$carrier->getId(), [
'headers' => ['Content-Type' => self::MERGE],
'json' => ['isArchived' => true, 'name' => 'Renamed While Archiving'],
]);
self::assertResponseStatusCodeSame(422);
}
}