getEm(); // Stockages d'abord : ils referencent site / storage_type en FK RESTRICT. $em->createQuery('DELETE FROM '.Storage::class)->execute(); // Types de stockage de test (prefixe code). $em->createQuery('DELETE FROM '.StorageType::class.' s WHERE s.code LIKE :prefix') ->setParameter('prefix', self::TEST_STORAGE_TYPE_PREFIX.'%') ->execute() ; parent::tearDown(); } /** * Cree un type de stockage de test (code prefixe TESTSTO pour le cleanup). */ protected function seedStorageType(string $label = 'Cellule test'): StorageType { $em = $this->getEm(); $storageType = new StorageType(); $storageType->setCode($this->uniqueCode(self::TEST_STORAGE_TYPE_PREFIX)); $storageType->setLabel($label); $em->persist($storageType); $em->flush(); return $storageType; } protected function siteByCode(string $code): Site { $site = $this->getEm()->getRepository(Site::class)->findOneBy(['code' => $code]); self::assertInstanceOf(Site::class, $site, sprintf('Le site de code "%s" doit etre seede.', $code)); return $site; } protected function firstSite(): Site { $site = $this->getEm()->getRepository(Site::class)->findOneBy([]); self::assertInstanceOf(Site::class, $site, 'Un site fixture est requis (SitesFixtures).'); return $site; } /** * Client non-admin portant seulement `catalog.storages.view`. */ protected function authView(): Client { $creds = $this->createUserWithPermission('catalog.storages.view'); return $this->authenticatedClient($creds['username'], $creds['password']); } /** * Payload POST de reference : un stockage valide (1 site, 1 type, 1 numero, * 1 etat). Surchargeable par cle via $overrides (ex: ['numero' => 'A1']). * * @param array $overrides * * @return array */ protected function validStoragePayload(array $overrides = []): array { $site = $this->firstSite(); $storageType = $this->seedStorageType(); $base = [ 'site' => $this->iri('sites', (int) $site->getId()), 'storageType' => $this->iri('storage_types', (int) $storageType->getId()), 'numero' => $this->uniqueCode('NUM'), 'states' => [Storage::STATE_RECEPTION], ]; return array_replace($base, $overrides); } /** * Seede un stockage directement via l'EM (bypass Processor/Validator). Utile pour * disposer d'un id existant (RBAC item, PATCH) ou d'un stockage soft-deleted * (reutilisation du triplet — RG-7.01). Le site / le type manquants sont crees * a la volee. * * @param list $states */ protected function seedStorageEntity( ?string $numero = null, array $states = [Storage::STATE_RECEPTION], ?DateTimeImmutable $deletedAt = null, ?Site $site = null, ?StorageType $storageType = null, ): Storage { $em = $this->getEm(); $site ??= $this->firstSite(); $storage = new Storage(); $storage->setSite($em->getReference(Site::class, (int) $site->getId())); $storage->setStorageType($storageType ?? $this->seedStorageType('Seed')); $storage->setNumero($numero ?? $this->uniqueCode('NUM')); $storage->setStates($states); $storage->setDeletedAt($deletedAt); $em->persist($storage); $em->flush(); return $storage; } /** * Construit un IRI API Platform (`/api/{resource}/{id}`). */ protected function iri(string $resource, int $id): string { return sprintf('/api/%s/%d', $resource, $id); } /** * Identifiant unique de test (prefixe + nonce), deja en MAJUSCULE. */ protected function uniqueCode(string $prefix): string { return $prefix.'_'.strtoupper(substr(bin2hex(random_bytes(5)), 0, 10)); } /** * Extrait les `propertyPath` des violations d'une reponse 422. * * @return list */ protected function violationPaths(ResponseInterface $response): array { $body = $response->toArray(false); return array_values(array_map( static fn (array $violation): string => (string) ($violation['propertyPath'] ?? ''), $body['violations'] ?? [], )); } /** * Retrouve un membre d'une collection Hydra par son id (ou null). * * @param array $list * * @return null|array */ protected function memberById(array $list, int $id): ?array { foreach ($list['member'] ?? [] as $member) { if (($member['id'] ?? null) === $id) { return $member; } } return null; } }