test(catalog) : M7 — tests RG-7.01→7.08 + contrat de sérialisation stockage (ERP-215)

This commit is contained in:
2026-06-29 17:10:59 +02:00
parent 0800ed99cf
commit caa558f582
7 changed files with 488 additions and 204 deletions
@@ -0,0 +1,129 @@
<?php
declare(strict_types=1);
namespace App\Tests\Module\Catalog\Api;
use App\Module\Catalog\Domain\Entity\Storage;
use DateTimeImmutable;
/**
* Contrat de serialisation du stockage (M7, spec-back § 4.0 / § 4.0.bis).
* Jumeau du ProductSerializationContractTest (M6).
*
* Capture le JSON REEL (liste + detail) via un stockage cree par l'API (POST reel,
* normalisation serveur reelle) et reverifie les pieges du RETEX M1 transposes au
* M7 :
* #1 : `site` sort en OBJET embarque (site:read), jamais en IRI nu.
* #2 : `storageType` sort en OBJET embarque (storage_type:read), jamais en IRI nu.
* #3 : `states` = tableau de chaines.
* #4 : `displayName` present et correct (RG-7.05 : « <label> <numero> »).
*
* REGLE D'OR : on asserte sur le CORPS JSON reel, jamais sur les annotations.
* DoD (§ 4.0.bis) : avec STORAGE_DOD_DUMP positionnee, ecrit les corps liste +
* detail sous /tmp pour les coller dans la spec avant les ecrans front.
*
* @internal
*/
final class StorageSerializationContractTest extends AbstractStorageApiTestCase
{
public function testListAndDetailSerializationContract(): void
{
$client = $this->createAdminClient();
$site = $this->firstSite();
$type = $this->seedStorageType('Cellule');
$numero = $this->uniqueCode('NUM');
// Stockage cree par un POST reel (2 etats pour exercer le tableau).
$created = $client->request('POST', '/api/storages', [
'headers' => ['Content-Type' => self::LD],
'json' => [
'site' => $this->iri('sites', (int) $site->getId()),
'storageType' => $this->iri('storage_types', (int) $type->getId()),
'numero' => $numero,
'states' => [Storage::STATE_RECEPTION, Storage::STATE_TRIAGE],
],
])->toArray();
self::assertResponseStatusCodeSame(201);
$id = (int) $created['id'];
$detail = $client->request('GET', '/api/storages/'.$id, [
'headers' => ['Accept' => self::LD],
])->toArray();
$list = $client->request('GET', '/api/storages?search='.$numero, [
'headers' => ['Accept' => self::LD],
])->toArray();
// Enveloppe Hydra AP4 (member/totalItems sans prefixe hydra:).
self::assertArrayHasKey('member', $list);
self::assertArrayNotHasKey('hydra:member', $list);
$row = $this->memberById($list, $id);
self::assertNotNull($row, 'Le stockage cree doit apparaitre dans la liste filtree.');
// === Piege #1 : site en OBJET embarque (pas IRI nu) ===
self::assertIsArray($row['site'], 'site doit etre un objet embarque (site:read), pas un IRI nu.');
self::assertArrayHasKey('name', $row['site']);
self::assertArrayHasKey('code', $row['site']);
// === Piege #2 : storageType en OBJET embarque (pas IRI nu) ===
self::assertIsArray($row['storageType'], 'storageType doit etre un objet embarque (storage_type:read), pas un IRI nu.');
self::assertArrayHasKey('label', $row['storageType']);
self::assertSame('Cellule', $row['storageType']['label']);
// === Piege #3 : states tableau de chaines ===
self::assertSame([Storage::STATE_RECEPTION, Storage::STATE_TRIAGE], $row['states']);
// === Piege #4 : displayName present + correct (RG-7.05) ===
self::assertArrayHasKey('displayName', $row);
self::assertSame('Cellule '.$numero, $row['displayName']);
// === DETAIL : memes garanties d'embarquement ===
self::assertIsArray($detail['site']);
self::assertArrayHasKey('name', $detail['site']);
self::assertIsArray($detail['storageType']);
self::assertArrayHasKey('label', $detail['storageType']);
self::assertSame([Storage::STATE_RECEPTION, Storage::STATE_TRIAGE], $detail['states']);
self::assertSame('Cellule '.$numero, $detail['displayName']);
$this->dumpDodIfRequested($list, $detail);
}
/**
* RG-7.07 : la liste (et le detail) n'exposent JAMAIS un stockage soft-deleted.
*/
public function testSoftDeletedIsNotExposed(): void
{
$deleted = $this->seedStorageEntity('SD', deletedAt: new DateTimeImmutable());
$client = $this->createAdminClient();
// Item soft-deleted -> 404 (§ 2.8).
$client->request('GET', '/api/storages/'.$deleted->getId(), ['headers' => ['Accept' => self::LD]]);
self::assertResponseStatusCodeSame(404);
// … et absent de la collection (RG-7.07).
$list = $client->request('GET', '/api/storages', ['headers' => ['Accept' => self::LD]])->toArray();
self::assertNull($this->memberById($list, (int) $deleted->getId()));
}
/**
* DoD (§ 4.0.bis) : ecrit les corps JSON reels sous /tmp si STORAGE_DOD_DUMP est
* positionnee (sinon no-op). A coller dans spec-back.md § 4.0.bis.
*
* @param array<string, mixed> $list
* @param array<string, mixed> $detail
*/
private function dumpDodIfRequested(array $list, array $detail): void
{
if (false === getenv('STORAGE_DOD_DUMP')) {
return;
}
$flags = JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES;
file_put_contents('/tmp/storage-dod-list.json', json_encode($list, $flags));
file_put_contents('/tmp/storage-dod-detail.json', json_encode($detail, $flags));
}
}