cleanupCatalogTestData(); parent::tearDown(); } /** * Cree un CategoryType de test. Le code est prefixe `TEST_` pour le * cleanup, suffixe par un nonce aleatoire pour eviter les collisions * inter-tests. */ protected function createCategoryType(?string $code = null, ?string $label = null): CategoryType { $em = $this->getEm(); $suffix = substr(bin2hex(random_bytes(4)), 0, 8); $type = new CategoryType(); $type->setCode($code ?? self::TEST_CATEGORY_TYPE_PREFIX.strtoupper($suffix)); $type->setLabel($label ?? 'Test Type '.$suffix); $em->persist($type); $em->flush(); return $type; } /** * Cree une Category de test. Le nom est prefixe `test_cat_` pour le * cleanup. Si aucun type n'est fourni, un nouveau CategoryType est cree. * Le flag $deletedAt permet de seeder directement une categorie * soft-deleted (pour les tests RG-1.08 / RG-1.11). */ protected function createCategory( ?string $name = null, ?CategoryType $type = null, ?DateTimeImmutable $deletedAt = null, ): Category { $em = $this->getEm(); $type ??= $this->createCategoryType(); $suffix = substr(bin2hex(random_bytes(4)), 0, 8); $category = new Category(); $category->setName($name ?? self::TEST_CATEGORY_PREFIX.$suffix); $category->setCategoryType($type); if (null !== $deletedAt) { $category->setDeletedAt($deletedAt); } $em->persist($category); $em->flush(); return $category; } /** * Client authentifie en tant qu'admin fixture (bypass via isAdmin). */ protected function createAdminClient(): Client { return $this->authenticatedClient('admin', 'admin'); } /** * Client non-admin portant la permission `catalog.categories.manage`. * Utilise pour prouver qu'un non-admin avec la permission obtient 200 / * 201 / 204 sur POST / PATCH / DELETE. * * @return array{client: Client, credentials: array{username: string, password: string}} */ protected function createManageClient(): array { $credentials = $this->createUserWithPermission('catalog.categories.manage'); $client = $this->authenticatedClient($credentials['username'], $credentials['password']); return ['client' => $client, 'credentials' => $credentials]; } /** * Client non-admin portant la permission `catalog.categories.view`. */ protected function createViewClient(): Client { $credentials = $this->createUserWithPermission('catalog.categories.view'); return $this->authenticatedClient($credentials['username'], $credentials['password']); } /** * Client authentifie en tant qu'un des 4 personas metier MALIO sans * permission catalog. Les 4 roles (Bureau / Compta / Commerciale / Usine) * sont seules creees a la volee dans le test, sans aucune permission * catalog.categories.* attachee. Le user obtient donc systematiquement * 403 sur tous les endpoints `/api/categories*` et `/api/category_types*`. * * Note : ces roles ne sont pas seedes dans AppFixtures (cf. HP-8 de la * spec M0). Les tests les materialisent juste pour prouver que porter * un role metier sans la permission catalog donne bien 403. */ protected function createPersonaClient(string $personaLabel): Client { if (!self::$kernel) { self::bootKernel(); } $em = $this->getEm(); $suffix = substr(bin2hex(random_bytes(4)), 0, 8); $username = self::TEST_USER_PREFIX.strtolower($personaLabel).'_'.$suffix; $password = 'testpass'; /** @var UserPasswordHasherInterface $hasher */ $hasher = self::getContainer()->get(UserPasswordHasherInterface::class); // Role nomme d'apres le persona MALIO, ZERO permission catalog. $role = new Role( self::TEST_ROLE_PREFIX.strtolower($personaLabel).'_'.$suffix, $personaLabel.' (test)', false, ); $em->persist($role); $user = new User(); $user->setUsername($username); $user->setIsAdmin(false); $user->setPassword($hasher->hashPassword($user, $password)); $user->addRbacRole($role); // Rattachement aux sites pour rester aligne sur createUserWithPermission. foreach ($em->getRepository(Site::class)->findAll() as $site) { $user->addSite($site); } $em->persist($user); $em->flush(); $em->clear(); return $this->authenticatedClient($username, $password); } /** * Purge des donnees Catalog crees par les tests. * * Strategie : purge complete des tables `category` et `category_type` * (aucune fixture ne les remplit au M0 — la migration cree les tables * vides, cf. spec-back § 1 + HP-1). On evite ainsi les pieges de * cleanup par prefixe quand un test valide le mauvais payload (ex: * name="" persiste sans matcher le LIKE) et laisse des orphelins * bloquant le DELETE category_type par FK violation. * * Ordre : * 1. Categories d'abord (FK ON DELETE RESTRICT vers category_type) ; * 2. CategoryTypes ensuite ; * 3. Users / Roles `test_*` enfin (FK created_by/updated_by sur * category est ON DELETE SET NULL, mais on a deja purge category). */ private function cleanupCatalogTestData(): void { $em = $this->getEm(); $em->createQuery('DELETE FROM '.Category::class)->execute(); $em->createQuery('DELETE FROM '.CategoryType::class)->execute(); $em->createQuery( 'DELETE FROM '.User::class.' u WHERE u.username LIKE :prefix' )->setParameter('prefix', self::TEST_USER_PREFIX.'%')->execute(); $em->createQuery( 'DELETE FROM '.Role::class.' r WHERE r.code LIKE :prefix' )->setParameter('prefix', self::TEST_ROLE_PREFIX.'%')->execute(); } }