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 $collection * * @return array|null */ private function memberById(array $collection, int $id): ?array { foreach ($collection['member'] ?? [] as $member) { if (($member['id'] ?? null) === $id) { return $member; } } return null; } }