getEm(); // Produits d'abord : ils referencent category / site / storage_type en FK // RESTRICT, donc le parent ne pourrait pas purger les categories tant // qu'un produit les pointe. Les jonctions product_site / // product_storage_type cascadent au niveau base (ON DELETE CASCADE). $em->createQuery('DELETE FROM '.Product::class)->execute(); // Types de stockage de test (prefixe code) — libere storage_type_site. $em->createQuery('DELETE FROM '.StorageType::class.' s WHERE s.code LIKE :prefix') ->setParameter('prefix', self::TEST_STORAGE_TYPE_PREFIX.'%') ->execute() ; parent::tearDown(); } /** * Recupere le CategoryType `PRODUIT` (find-or-create). Le cleanup parent * purge tous les category_type entre tests : on le recree au besoin pour que * les POST produit satisfassent RG-6.05. */ protected function productType(): CategoryType { $em = $this->getEm(); $existing = $em->getRepository(CategoryType::class)->findOneBy(['code' => self::PRODUCT_TYPE_CODE]); if ($existing instanceof CategoryType) { return $existing; } $type = new CategoryType(); $type->setCode(self::PRODUCT_TYPE_CODE); $type->setLabel('Produit'); $em->persist($type); $em->flush(); return $type; } /** * Categorie de test rattachee au type PRODUIT (satisfait RG-6.05). */ protected function productCategory(?string $name = null): Category { // Nom laisse a null par defaut -> createCategory genere un nom aleatoire // unique (uq_category_name_active impose LOWER(name) unique parmi les // actives : deux categories de meme nom dans un test collisionneraient). return $this->createCategory($name, $this->productType()); } /** * Categorie de test rattachee a un type NON-PRODUIT (viole RG-6.05). */ protected function nonProductCategory(): Category { return $this->createCategory(null, $this->createCategoryType()); } /** * Cree un type de stockage de test (code prefixe TESTPRD pour le cleanup), * rattache aux sites passes (disponibilite — RG-6.06). */ protected function seedStorageType(string $label = 'Tas de test', Site ...$sites): StorageType { $em = $this->getEm(); $storageType = new StorageType(); $storageType->setCode($this->uniqueCode(self::TEST_STORAGE_TYPE_PREFIX)); $storageType->setLabel($label); foreach ($sites as $site) { $storageType->addSite($em->getReference(Site::class, (int) $site->getId())); } $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.products.view`. */ protected function authView(): Client { $creds = $this->createUserWithPermission('catalog.products.view'); return $this->authenticatedClient($creds['username'], $creds['password']); } /** * Payload POST de reference : un produit valide (categorie PRODUIT, 1 site, * 1 type de stockage disponible sur ce site). Surchargeable par cle via * $overrides (ex: ['states' => ['SALE'], 'code' => 'X']). * * @param array $overrides * * @return array */ protected function validProductPayload(array $overrides = []): array { $site = $this->firstSite(); $storageType = $this->seedStorageType('Tas test', $site); $category = $this->productCategory(); $base = [ 'code' => $this->uniqueCode('TESTPRD'), 'name' => 'Produit test', 'states' => [Product::STATE_PURCHASE], 'manufactured' => false, 'containsMolasses' => false, 'category' => $this->iri('categories', (int) $category->getId()), 'sites' => [$this->iri('sites', (int) $site->getId())], 'storageTypes' => [$this->iri('storage_types', (int) $storageType->getId())], ]; return array_replace($base, $overrides); } /** * Seede un produit directement via l'EM (bypass Processor/Validator). Utile * pour disposer d'un id existant (RBAC item, PATCH) ou d'un produit * soft-deleted (reutilisation de code — RG-6.01). La categorie / le site / le * type de stockage manquants sont crees a la volee. * * @param list $states */ protected function seedProductEntity( ?string $code = null, array $states = [Product::STATE_PURCHASE], ?DateTimeImmutable $deletedAt = null, ?Site $site = null, ?StorageType $storageType = null, ?Category $category = null, ): Product { $em = $this->getEm(); $site ??= $this->firstSite(); $product = new Product(); $product->setCode($code ?? $this->uniqueCode(self::TEST_STORAGE_TYPE_PREFIX)); $product->setName('Produit seed'); $product->setStates($states); $product->setManufactured(false); $product->setContainsMolasses(false); $product->setCategory($category ?? $this->productCategory()); $product->addSite($em->getReference(Site::class, (int) $site->getId())); $product->addStorageType($storageType ?? $this->seedStorageType('Seed', $site)); $product->setDeletedAt($deletedAt); $em->persist($product); $em->flush(); return $product; } /** * Construit un IRI API Platform (`/api/{resource}/{id}`). */ protected function iri(string $resource, int $id): string { return sprintf('/api/%s/%d', $resource, $id); } /** * Code unique de test (prefixe + nonce). Deja en MAJUSCULE : stable apres la * normalisation serveur (trim + UPPER, RG-6.07). */ 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 (sans lever sur * le statut non-2xx). Sert a verifier que le back identifie bien le champ * fautif (contrat consomme par useFormErrors cote front). * * @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; } }