fix(catalog) : M7 — durcissement stockages (états JSONB séquentiels + Assert\Unique, neutralisation injection formules XLSX partagée, parité listing/export via StorageListFilters, streaming export)
- Storage.setStates() renormalise en liste séquentielle (array_values) : un states posté en objet JSON ne peut plus être persisté en JSONB objet (jsonb_array_length → 500). Doublons rejetés en 422 via Assert\Unique. - PhpSpreadsheetExporter écrit les cellules chaîne en TYPE_STRING explicite : neutralise l'injection de formules/DDE sur toutes les valeurs saisies (corrige aussi Produit/Client/Logistique/Supplier/Provider/Carrier). - StorageListFilters : source unique de parsing des filtres (?search, ?siteId[], ?storageTypeId, ?state), consommée par le provider ET l'export → fin des divergences (numéro « 0 » coercé à null, param tableau en 400, id non positif). - Export en streaming (toIterable + clear par lot) au lieu de getResult() : mémoire bornée. - Tests : doublon/objet states, normalisation trim RG-7.06, 422 relations nulles, absence de deletedAt, soft-delete liste discriminant, neutralisation formule, parité ?search=0, robustesse param tableau ; garde-fou Assert\Unique enregistré.
This commit is contained in:
@@ -80,6 +80,15 @@ final class StorageSerializationContractTest extends AbstractStorageApiTestCase
|
||||
self::assertArrayHasKey('displayName', $row);
|
||||
self::assertSame('Cellule '.$numero, $row['displayName']);
|
||||
|
||||
// === Piege #5 : le soft-delete n'est JAMAIS expose (§ 2.8) ===
|
||||
// `deletedAt` n'appartient a aucun groupe de lecture : un test « contrat » doit
|
||||
// garantir son ABSENCE, pas seulement la presence des champs attendus — sinon
|
||||
// un ajout accidentel a storage:read passerait au vert. (createdBy/updatedBy
|
||||
// sont, eux, exposes a dessein via la convention `default:read` du Trait
|
||||
// Timestampable/Blamable — au meme titre que createdAt/updatedAt.)
|
||||
self::assertArrayNotHasKey('deletedAt', $row, 'deletedAt ne doit pas etre expose en liste (§ 2.8).');
|
||||
self::assertArrayNotHasKey('deletedAt', $detail, 'deletedAt ne doit pas etre expose en detail (§ 2.8).');
|
||||
|
||||
// === DETAIL : memes garanties d'embarquement ===
|
||||
self::assertIsArray($detail['site']);
|
||||
self::assertArrayHasKey('name', $detail['site']);
|
||||
@@ -93,10 +102,14 @@ final class StorageSerializationContractTest extends AbstractStorageApiTestCase
|
||||
|
||||
/**
|
||||
* RG-7.07 : la liste (et le detail) n'exposent JAMAIS un stockage soft-deleted.
|
||||
* On seede 1 actif + 1 supprime pour que l'assertion de liste soit discriminante
|
||||
* (sinon, avec une collection vide, « absent » ne distingue pas l'exclusion du
|
||||
* soft-delete d'une page vide).
|
||||
*/
|
||||
public function testSoftDeletedIsNotExposed(): void
|
||||
{
|
||||
$deleted = $this->seedStorageEntity('SD', deletedAt: new DateTimeImmutable());
|
||||
$active = $this->seedStorageEntity('SD-ACTIVE');
|
||||
$deleted = $this->seedStorageEntity('SD-DELETED', deletedAt: new DateTimeImmutable());
|
||||
|
||||
$client = $this->createAdminClient();
|
||||
|
||||
@@ -104,9 +117,10 @@ final class StorageSerializationContractTest extends AbstractStorageApiTestCase
|
||||
$client->request('GET', '/api/storages/'.$deleted->getId(), ['headers' => ['Accept' => self::LD]]);
|
||||
self::assertResponseStatusCodeSame(404);
|
||||
|
||||
// … et absent de la collection (RG-7.07).
|
||||
// Collection : l'actif est present, le supprime est absent (RG-7.07).
|
||||
$list = $client->request('GET', '/api/storages', ['headers' => ['Accept' => self::LD]])->toArray();
|
||||
self::assertNull($this->memberById($list, (int) $deleted->getId()));
|
||||
self::assertNotNull($this->memberById($list, (int) $active->getId()), 'Le stockage actif doit etre liste.');
|
||||
self::assertNull($this->memberById($list, (int) $deleted->getId()), 'Le stockage soft-deleted ne doit pas etre liste.');
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user