feat(transport) : schéma + entités Carrier + contrat lecture (ERP-155/157)
Schéma BDD du répertoire transporteurs (M4) + entités + contrat de lecture (liste + détail), socle du front. - Migration Version20260615150000 : tables carrier / carrier_address / carrier_contact / carrier_price (FK cross-module, CHECK enum, index partiel uq_carrier_name_active, COMMENT ON COLUMN). uploaded_document et qualimat_carrier réutilisées (non recréées). - Entités Carrier* (#[Auditable], Timestampable/Blamable) + ApiResource LECTURE seule (GetCollection + Get via CarrierProvider, anti-N+1, exclusion archivés + ?includeArchived). Écriture (POST/PATCH + Processor) reportée WT4+. - QualimatCarrier : mapping ORM lecture seule sur la table référentielle existante (sortie du schema_filter, mapping aligné DDL ERP-39, schema:update no-op) + endpoint de recherche read-only (§ 4.7). - Relations cross-module des prix (Client/Supplier/adresses) via contrats Shared (ClientInterface, SupplierInterface, ClientAddressInterface, SupplierAddressInterface) + resolve_target_entities — sans import inter-module (règle n°1). Ajout du groupe supplier_address:read aux champs de SupplierAddress pour l'embed. - Garde-fous : ColumnCommentsCatalog (carrier* + qualimat_carrier), makefile test-db-setup (index partiel carrier), i18n audit (transport_carrier*), EntitiesAreTimestampableBlamableTest (QualimatCarrier whitelisté). - CarrierSerializationContractTest : contrat JSON liste + détail vérifié (embeds objet, booléens, enveloppe Hydra) ; JSON réel capturé dans spec-back § 4.0.bis. make db-reset OK, make test vert (731), make nuxt-test vert (480), php-cs-fixer OK.
This commit is contained in:
@@ -14,6 +14,7 @@ use App\Module\Core\Domain\Entity\Permission;
|
||||
use App\Module\Core\Domain\Entity\Role;
|
||||
use App\Module\Core\Domain\Entity\User;
|
||||
use App\Module\Sites\Domain\Entity\Site;
|
||||
use App\Module\Transport\Domain\Entity\QualimatCarrier;
|
||||
use App\Shared\Domain\Contract\BlamableInterface;
|
||||
use App\Shared\Domain\Contract\TimestampableInterface;
|
||||
use Doctrine\ORM\Mapping\Entity;
|
||||
@@ -61,6 +62,10 @@ final class EntitiesAreTimestampableBlamableTest extends TestCase
|
||||
* spec-back M1 § 2.6 + § 3.5.
|
||||
* - Country (ERP-116) : referentiel statique des pays (id/code/name/position),
|
||||
* seede par migration, lecture seule. Meme justification que Bank.
|
||||
* - QualimatCarrier (M4, ERP-39/155) : mapping ORM LECTURE SEULE sur la table
|
||||
* referentielle qualimat_carrier, alimentee/soft-deletee exclusivement par
|
||||
* la commande `app:qualimat:sync` (pas de tracabilite user-driven, pas
|
||||
* d'ecriture API). Meme justification que les referentiels ci-dessus.
|
||||
*
|
||||
* Les futurs referentiels statiques s'ajoutent ici avec une justification.
|
||||
*/
|
||||
@@ -75,6 +80,7 @@ final class EntitiesAreTimestampableBlamableTest extends TestCase
|
||||
PaymentType::class,
|
||||
Bank::class,
|
||||
Country::class,
|
||||
QualimatCarrier::class,
|
||||
];
|
||||
|
||||
public function testAllBusinessEntitiesImplementBothInterfaces(): void
|
||||
|
||||
@@ -0,0 +1,241 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Tests\Module\Transport\Api;
|
||||
|
||||
use ApiPlatform\Symfony\Bundle\Test\Client;
|
||||
use App\Module\Commercial\Domain\Entity\Client as ClientEntity;
|
||||
use App\Module\Commercial\Domain\Entity\ClientAddress;
|
||||
use App\Module\Commercial\Domain\Entity\Supplier;
|
||||
use App\Module\Commercial\Domain\Entity\SupplierAddress;
|
||||
use App\Module\Sites\Domain\Entity\Site;
|
||||
use App\Module\Transport\Domain\Entity\Carrier;
|
||||
use App\Module\Transport\Domain\Entity\CarrierAddress;
|
||||
use App\Module\Transport\Domain\Entity\CarrierContact;
|
||||
use App\Module\Transport\Domain\Entity\CarrierPrice;
|
||||
use App\Module\Transport\Domain\Entity\QualimatCarrier;
|
||||
use App\Tests\Module\Core\Api\AbstractApiTestCase;
|
||||
use DateTimeImmutable;
|
||||
|
||||
/**
|
||||
* Base des tests fonctionnels du repertoire transporteurs (M4). Apporte les
|
||||
* factories de seed direct (sans passer par l'API : le flux d'ecriture arrive
|
||||
* au WT4) pour les tests de lecture / serialisation / contrat (DoD § 4.0.bis).
|
||||
*
|
||||
* Donnees (RETEX M1) : chaque test seede ses transporteurs ; le tearDown les
|
||||
* purge (cascade BDD sur les sous-collections) ainsi que les lignes
|
||||
* qualimat_carrier de test (prefixe SIRET dedie).
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
abstract class AbstractCarrierApiTestCase extends AbstractApiTestCase
|
||||
{
|
||||
protected const string LD = 'application/ld+json';
|
||||
|
||||
/** Prefixe SIRET des lignes qualimat_carrier seedees par les tests (purge ciblee). */
|
||||
private const string TEST_SIRET_PREFIX = 'TESTQ';
|
||||
|
||||
/** Prefixe des Client/Supplier de test (cross-module Prix) — purge ciblee. */
|
||||
private const string TEST_REF_PREFIX = 'TESTCARRIERREF';
|
||||
|
||||
protected function tearDown(): void
|
||||
{
|
||||
$em = $this->getEm();
|
||||
// Carrier d'abord : ON DELETE CASCADE purge carrier_price (FK RESTRICT vers
|
||||
// client/supplier), liberant les Client/Supplier de test pour leur purge.
|
||||
$em->createQuery('DELETE FROM '.Carrier::class)->execute();
|
||||
$em->createQuery('DELETE FROM '.ClientEntity::class.' c WHERE c.companyName LIKE :p')
|
||||
->setParameter('p', self::TEST_REF_PREFIX.'%')->execute();
|
||||
$em->createQuery('DELETE FROM '.Supplier::class.' s WHERE s.companyName LIKE :p')
|
||||
->setParameter('p', self::TEST_REF_PREFIX.'%')->execute();
|
||||
// qualimat_carrier : insere en DBAL brut (entite lecture seule) -> purge DBAL.
|
||||
$em->getConnection()->executeStatement(
|
||||
'DELETE FROM qualimat_carrier WHERE siret LIKE :p',
|
||||
['p' => self::TEST_SIRET_PREFIX.'%'],
|
||||
);
|
||||
|
||||
parent::tearDown();
|
||||
}
|
||||
|
||||
protected function createAdminClient(): Client
|
||||
{
|
||||
return $this->authenticatedClient('admin', 'admin');
|
||||
}
|
||||
|
||||
/**
|
||||
* Seede un transporteur minimal (nom en MAJUSCULES, comme le ferait le
|
||||
* futur Processor). Sert aux tests de liste / archivage.
|
||||
*/
|
||||
protected function seedCarrier(string $name, bool $isArchived = false): Carrier
|
||||
{
|
||||
$em = $this->getEm();
|
||||
$carrier = new Carrier();
|
||||
$carrier->setName(mb_strtoupper($name, 'UTF-8'));
|
||||
$carrier->setCertificationType('GMP_PLUS');
|
||||
$carrier->setIsArchived($isArchived);
|
||||
if ($isArchived) {
|
||||
$carrier->setArchivedAt(new DateTimeImmutable());
|
||||
}
|
||||
$em->persist($carrier);
|
||||
$em->flush();
|
||||
|
||||
return $carrier;
|
||||
}
|
||||
|
||||
/**
|
||||
* Seede un transporteur COMPLET (sans passer par l'API) : lien QUALIMAT,
|
||||
* 1 adresse, 1 contact, et 2 prix couvrant les deux branches (CLIENT avec
|
||||
* client + adresse de livraison + site de depart ; FOURNISSEUR avec
|
||||
* fournisseur + adresse d'appro + site de livraison). Socle du contrat de
|
||||
* serialisation et de la DoD (§ 4.0.bis).
|
||||
*/
|
||||
protected function seedCompleteCarrier(string $name): Carrier
|
||||
{
|
||||
$em = $this->getEm();
|
||||
$suffix = substr(bin2hex(random_bytes(3)), 0, 6);
|
||||
|
||||
$qualimat = $this->seedQualimatCarrier($name);
|
||||
|
||||
$carrier = new Carrier();
|
||||
$carrier->setName(mb_strtoupper($name.' '.$suffix, 'UTF-8'));
|
||||
$carrier->setQualimatCarrier($qualimat);
|
||||
$carrier->setCertificationType('QUALIMAT');
|
||||
$em->persist($carrier);
|
||||
|
||||
$address = new CarrierAddress();
|
||||
$address->setCarrier($carrier);
|
||||
$address->setPostalCode('86000');
|
||||
$address->setCity('Poitiers');
|
||||
$address->setStreet('12 rue des Acacias');
|
||||
$carrier->addAddress($address);
|
||||
$em->persist($address);
|
||||
|
||||
$contact = new CarrierContact();
|
||||
$contact->setCarrier($carrier);
|
||||
$contact->setFirstName('Marie');
|
||||
$contact->setLastName('Martin');
|
||||
$contact->setPhonePrimary('0612345678');
|
||||
$contact->setEmail('marie.martin@seed.test');
|
||||
$carrier->addContact($contact);
|
||||
$em->persist($contact);
|
||||
|
||||
// Refs cross-module : seedees localement (en test, les fixtures M1/M2 ne
|
||||
// sont pas chargees — seuls les sites le sont). Prouve l'embed via les
|
||||
// contrats Shared + resolve_target_entities (regle n°1).
|
||||
$site = $em->getRepository(Site::class)->findOneBy([]);
|
||||
self::assertNotNull($site, 'Un site fixture est requis (SitesFixtures).');
|
||||
|
||||
$clientAddress = $this->seedClientWithAddress($name.' '.$suffix);
|
||||
$supplierAddress = $this->seedSupplierWithAddress($name.' '.$suffix);
|
||||
|
||||
// Branche CLIENT (RG-4.10).
|
||||
$clientPrice = new CarrierPrice();
|
||||
$clientPrice->setCarrier($carrier);
|
||||
$clientPrice->setDirection('CLIENT');
|
||||
$clientPrice->setClient($clientAddress->getClient());
|
||||
$clientPrice->setClientDeliveryAddress($clientAddress);
|
||||
$clientPrice->setDepartureSite($site);
|
||||
$clientPrice->setContainerType('BENNE');
|
||||
$clientPrice->setPricingUnit('TONNE');
|
||||
$clientPrice->setPrice('42.50');
|
||||
$clientPrice->setPriceState('VALIDE');
|
||||
$carrier->addPrice($clientPrice);
|
||||
$em->persist($clientPrice);
|
||||
|
||||
// Branche FOURNISSEUR (RG-4.11).
|
||||
$supplierPrice = new CarrierPrice();
|
||||
$supplierPrice->setCarrier($carrier);
|
||||
$supplierPrice->setDirection('FOURNISSEUR');
|
||||
$supplierPrice->setSupplier($supplierAddress->getSupplier());
|
||||
$supplierPrice->setSupplierSupplyAddress($supplierAddress);
|
||||
$supplierPrice->setDeliverySite($site);
|
||||
$supplierPrice->setContainerType('FOND_MOUVANT');
|
||||
$supplierPrice->setPricingUnit('FORFAIT');
|
||||
$supplierPrice->setPrice('320.00');
|
||||
$supplierPrice->setPriceState('EN_COURS');
|
||||
$carrier->addPrice($supplierPrice);
|
||||
$em->persist($supplierPrice);
|
||||
|
||||
$em->flush();
|
||||
|
||||
return $carrier;
|
||||
}
|
||||
|
||||
/**
|
||||
* Seede un Client minimal (companyName prefixe pour la purge) + une adresse
|
||||
* de livraison valide (CHECKs client_address respectes). Retourne l'adresse.
|
||||
*/
|
||||
protected function seedClientWithAddress(string $label): ClientAddress
|
||||
{
|
||||
$em = $this->getEm();
|
||||
$suffix = substr(bin2hex(random_bytes(3)), 0, 6);
|
||||
|
||||
$client = new ClientEntity();
|
||||
$client->setCompanyName(mb_strtoupper(self::TEST_REF_PREFIX.' CLI '.$label.' '.$suffix, 'UTF-8'));
|
||||
$em->persist($client);
|
||||
|
||||
$address = new ClientAddress();
|
||||
$address->setClient($client);
|
||||
// Adresse de livraison : is_delivery=true, is_prospect=false, is_billing=false
|
||||
// -> satisfait chk_client_address_prospect_exclusive + chk_client_address_billing_email.
|
||||
$address->setIsDelivery(true);
|
||||
$address->setPostalCode('86000');
|
||||
$address->setCity('Poitiers');
|
||||
$address->setStreet('1 rue de la Livraison');
|
||||
$em->persist($address);
|
||||
|
||||
return $address;
|
||||
}
|
||||
|
||||
/**
|
||||
* Seede un Supplier minimal (companyName prefixe pour la purge) + une adresse
|
||||
* d'approvisionnement valide (address_type DEPART). Retourne l'adresse.
|
||||
*/
|
||||
protected function seedSupplierWithAddress(string $label): SupplierAddress
|
||||
{
|
||||
$em = $this->getEm();
|
||||
$suffix = substr(bin2hex(random_bytes(3)), 0, 6);
|
||||
|
||||
$supplier = new Supplier();
|
||||
$supplier->setCompanyName(mb_strtoupper(self::TEST_REF_PREFIX.' FRN '.$label.' '.$suffix, 'UTF-8'));
|
||||
$em->persist($supplier);
|
||||
|
||||
$address = new SupplierAddress();
|
||||
$address->setSupplier($supplier);
|
||||
$address->setAddressType('DEPART');
|
||||
$address->setPostalCode('17000');
|
||||
$address->setCity('La Rochelle');
|
||||
$address->setStreet('2 quai de l Appro');
|
||||
$em->persist($address);
|
||||
|
||||
return $address;
|
||||
}
|
||||
|
||||
/**
|
||||
* Insere une ligne qualimat_carrier de test en DBAL brut (l'entite mappee est
|
||||
* en lecture seule) et retourne l'entite rechargee. SIRET prefixe pour la purge.
|
||||
*/
|
||||
protected function seedQualimatCarrier(string $name): QualimatCarrier
|
||||
{
|
||||
$em = $this->getEm();
|
||||
$siret = self::TEST_SIRET_PREFIX.substr(bin2hex(random_bytes(6)), 0, 9);
|
||||
|
||||
$em->getConnection()->insert('qualimat_carrier', [
|
||||
'siret' => $siret,
|
||||
'name' => mb_strtoupper($name, 'UTF-8'),
|
||||
'address' => '12 rue des Acacias',
|
||||
'postal_code' => '86000',
|
||||
'city' => 'Poitiers',
|
||||
'status' => 'Valide',
|
||||
'validity_date' => '2027-12-31',
|
||||
'is_active' => 'true',
|
||||
'last_synced_at' => (new DateTimeImmutable())->format('Y-m-d H:i:s'),
|
||||
]);
|
||||
|
||||
$qualimat = $em->getRepository(QualimatCarrier::class)->findOneBy(['siret' => $siret]);
|
||||
self::assertNotNull($qualimat, 'La ligne qualimat_carrier de test doit etre rechargeable.');
|
||||
|
||||
return $qualimat;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,198 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Tests\Module\Transport\Api;
|
||||
|
||||
/**
|
||||
* Tests du CONTRAT DE SERIALISATION du repertoire transporteurs (M4, spec-back
|
||||
* § 4.0 / § 4.0.bis). Jumeau de {@see \App\Tests\Module\Commercial\Api\SupplierSerializationContractTest}.
|
||||
* Reverifie sur le JSON REEL les pieges silencieux du M1 transposes au M4 :
|
||||
* - #1/#2 : relations embarquees en OBJET (pas IRI nu) — qualimatCarrier, et au
|
||||
* detail prices[].client / .supplier / .departureSite / .deliverySite.
|
||||
* - #3 : booleens isArchived / isChartered presents dans le JSON (Groups +
|
||||
* SerializedName sur le getter).
|
||||
* - enveloppe AP4 (member/totalItems/view sans prefixe hydra:) + exclusion des
|
||||
* archives par defaut, ?includeArchived=true les reintegre.
|
||||
*
|
||||
* REGLE D'OR : on asserte sur le CORPS JSON reel, jamais sur les annotations.
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
final class CarrierSerializationContractTest extends AbstractCarrierApiTestCase
|
||||
{
|
||||
// === Enveloppe AP4 + exclusion des archives (§ 4.1) ===
|
||||
|
||||
public function testCollectionEnvelopeShapeAndArchivedExcluded(): void
|
||||
{
|
||||
$http = $this->createAdminClient();
|
||||
$token = 'EnvCheck'.substr(bin2hex(random_bytes(3)), 0, 6);
|
||||
|
||||
$this->seedCarrier($token.' Active');
|
||||
$this->seedCarrier($token.' Archived', true);
|
||||
|
||||
$default = $http->request('GET', '/api/carriers?search='.$token, ['headers' => ['Accept' => self::LD]])->toArray();
|
||||
|
||||
self::assertArrayHasKey('member', $default);
|
||||
self::assertArrayHasKey('totalItems', $default);
|
||||
self::assertArrayNotHasKey('hydra:member', $default);
|
||||
self::assertArrayNotHasKey('hydra:totalItems', $default);
|
||||
self::assertSame(1, $default['totalItems'], 'Archive exclu du totalItems par defaut.');
|
||||
|
||||
$all = $http->request('GET', '/api/carriers?search='.$token.'&includeArchived=true', ['headers' => ['Accept' => self::LD]])->toArray();
|
||||
self::assertSame(2, $all['totalItems']);
|
||||
|
||||
$paged = $http->request('GET', '/api/carriers?search='.$token.'&includeArchived=true&itemsPerPage=1', ['headers' => ['Accept' => self::LD]])->toArray();
|
||||
self::assertArrayHasKey('view', $paged);
|
||||
self::assertArrayNotHasKey('hydra:view', $paged);
|
||||
}
|
||||
|
||||
// === #3 — Booleens presents (isArchived) + embed qualimatCarrier en LISTE ===
|
||||
|
||||
public function testListExposesIsArchivedAndEmbeddedQualimat(): void
|
||||
{
|
||||
$token = 'List'.substr(bin2hex(random_bytes(3)), 0, 6);
|
||||
$carrier = $this->seedCompleteCarrier($token);
|
||||
|
||||
$http = $this->createAdminClient();
|
||||
$list = $http->request('GET', '/api/carriers?search='.$token, ['headers' => ['Accept' => self::LD]])->toArray();
|
||||
|
||||
$row = $this->memberById($list, (int) $carrier->getId());
|
||||
self::assertNotNull($row, 'Le transporteur seede doit apparaitre dans la liste filtree.');
|
||||
|
||||
// Boolean trap (#3) : cle presente et typee bool.
|
||||
self::assertArrayHasKey('isArchived', $row);
|
||||
self::assertFalse($row['isArchived']);
|
||||
|
||||
// qualimatCarrier embarque en OBJET (statut + date de validite — RG-4.04),
|
||||
// pas un IRI nu (#1/#2).
|
||||
self::assertArrayHasKey('qualimatCarrier', $row);
|
||||
self::assertIsArray($row['qualimatCarrier'], 'qualimatCarrier doit etre un objet embarque, pas un IRI nu.');
|
||||
self::assertArrayHasKey('status', $row['qualimatCarrier']);
|
||||
self::assertArrayHasKey('validityDate', $row['qualimatCarrier']);
|
||||
|
||||
// updatedAt (default:read) expose pour la colonne « Derniere activite ».
|
||||
self::assertArrayHasKey('updatedAt', $row);
|
||||
}
|
||||
|
||||
// === Detail : sous-collections embarquees + booleens ===
|
||||
|
||||
public function testDetailEmbedsSubCollectionsAndBooleans(): void
|
||||
{
|
||||
$carrier = $this->seedCompleteCarrier('Detail Co');
|
||||
|
||||
$http = $this->createAdminClient();
|
||||
$data = $http->request('GET', '/api/carriers/'.$carrier->getId(), ['headers' => ['Accept' => self::LD]])->toArray();
|
||||
|
||||
self::assertArrayHasKey('isArchived', $data);
|
||||
self::assertArrayHasKey('isChartered', $data);
|
||||
self::assertFalse($data['isArchived']);
|
||||
|
||||
self::assertNotEmpty($data['addresses']);
|
||||
self::assertSame('Poitiers', $data['addresses'][0]['city']);
|
||||
|
||||
self::assertNotEmpty($data['contacts']);
|
||||
self::assertSame('Marie', $data['contacts'][0]['firstName']);
|
||||
|
||||
self::assertNotEmpty($data['prices']);
|
||||
self::assertGreaterThanOrEqual(2, count($data['prices']));
|
||||
}
|
||||
|
||||
// === #1/#2 — prices[] : client / supplier / sites embarques en OBJET ===
|
||||
|
||||
public function testPriceCrossModuleRelationsAreEmbeddedObjects(): void
|
||||
{
|
||||
$carrier = $this->seedCompleteCarrier('Price Embed Co');
|
||||
|
||||
$http = $this->createAdminClient();
|
||||
$data = $http->request('GET', '/api/carriers/'.$carrier->getId(), ['headers' => ['Accept' => self::LD]])->toArray();
|
||||
|
||||
$byDirection = [];
|
||||
foreach ($data['prices'] as $price) {
|
||||
$byDirection[$price['direction']] = $price;
|
||||
}
|
||||
|
||||
self::assertArrayHasKey('CLIENT', $byDirection);
|
||||
self::assertArrayHasKey('FOURNISSEUR', $byDirection);
|
||||
|
||||
// Branche CLIENT : client + adresse + site de depart en OBJET (pas IRI).
|
||||
$clientPrice = $byDirection['CLIENT'];
|
||||
self::assertIsArray($clientPrice['client'], 'prices[].client doit etre un objet embarque (client:read), pas un IRI nu.');
|
||||
self::assertArrayHasKey('companyName', $clientPrice['client']);
|
||||
self::assertIsArray($clientPrice['clientDeliveryAddress']);
|
||||
self::assertArrayHasKey('city', $clientPrice['clientDeliveryAddress'], 'L\'adresse client doit embarquer ses champs (client_address:read).');
|
||||
self::assertIsArray($clientPrice['departureSite']);
|
||||
self::assertArrayHasKey('name', $clientPrice['departureSite']);
|
||||
|
||||
// Branche FOURNISSEUR : supplier + adresse + site de livraison en OBJET.
|
||||
$supplierPrice = $byDirection['FOURNISSEUR'];
|
||||
self::assertIsArray($supplierPrice['supplier'], 'prices[].supplier doit etre un objet embarque (supplier:read), pas un IRI nu.');
|
||||
self::assertArrayHasKey('companyName', $supplierPrice['supplier']);
|
||||
self::assertIsArray($supplierPrice['supplierSupplyAddress']);
|
||||
self::assertArrayHasKey('city', $supplierPrice['supplierSupplyAddress'], 'L\'adresse fournisseur doit embarquer ses champs (supplier_address:read).');
|
||||
self::assertIsArray($supplierPrice['deliverySite']);
|
||||
}
|
||||
|
||||
// === RBAC : 403 sans la permission view ===
|
||||
|
||||
public function testForbiddenWithoutViewPermission(): void
|
||||
{
|
||||
$carrier = $this->seedCarrier('Rbac Co');
|
||||
|
||||
// user-nothing : aucune permission transport.carriers.*.
|
||||
$creds = $this->createUserWithPermission('core.users.view');
|
||||
$http = $this->authenticatedClient($creds['username'], $creds['password']);
|
||||
|
||||
$http->request('GET', '/api/carriers', ['headers' => ['Accept' => self::LD]]);
|
||||
self::assertSame(403, $http->getResponse()->getStatusCode());
|
||||
|
||||
$http->request('GET', '/api/carriers/'.$carrier->getId(), ['headers' => ['Accept' => self::LD]]);
|
||||
self::assertSame(403, $http->getResponse()->getStatusCode());
|
||||
}
|
||||
|
||||
/**
|
||||
* DoD (§ 4.0.bis) : capture des reponses JSON REELLES (liste + detail) pour
|
||||
* les coller dans la spec avant de lancer les tickets front. Le test asserte
|
||||
* la forme ; si CARRIER_DOD_DUMP est positionnee, ecrit les corps sous /tmp.
|
||||
*/
|
||||
public function testDodReferenceJsonShape(): void
|
||||
{
|
||||
$token = 'DoD'.substr(bin2hex(random_bytes(3)), 0, 6);
|
||||
$carrier = $this->seedCompleteCarrier($token);
|
||||
$id = (int) $carrier->getId();
|
||||
|
||||
$admin = $this->createAdminClient();
|
||||
$list = $admin->request('GET', '/api/carriers?search='.$token, ['headers' => ['Accept' => self::LD]])->toArray();
|
||||
$detail = $admin->request('GET', '/api/carriers/'.$id, ['headers' => ['Accept' => self::LD]])->toArray();
|
||||
|
||||
self::assertArrayHasKey('member', $list);
|
||||
self::assertArrayHasKey('qualimatCarrier', $detail);
|
||||
self::assertArrayHasKey('addresses', $detail);
|
||||
self::assertArrayHasKey('contacts', $detail);
|
||||
self::assertArrayHasKey('prices', $detail);
|
||||
|
||||
if (false !== getenv('CARRIER_DOD_DUMP')) {
|
||||
$flags = JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES;
|
||||
file_put_contents('/tmp/carrier-dod-list.json', json_encode($list, $flags));
|
||||
file_put_contents('/tmp/carrier-dod-detail.json', json_encode($detail, $flags));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrouve un membre de la collection par son id (liste filtree).
|
||||
*
|
||||
* @param array<string, mixed> $collection
|
||||
*
|
||||
* @return array<string, mixed>|null
|
||||
*/
|
||||
private function memberById(array $collection, int $id): ?array
|
||||
{
|
||||
foreach ($collection['member'] ?? [] as $member) {
|
||||
if (($member['id'] ?? null) === $id) {
|
||||
return $member;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user