getEm(); $em->createQuery('DELETE FROM '.Provider::class)->execute(); $em->createQuery('DELETE FROM '.Category::class.' c WHERE c.name LIKE :prefix') ->setParameter('prefix', self::TEST_CATEGORY_PREFIX.'%')->execute() ; $em->createQuery('DELETE FROM '.User::class.' u WHERE u.username LIKE :prefix') ->setParameter('prefix', 'test_%')->execute() ; $em->createQuery('DELETE FROM '.Role::class.' r WHERE r.code LIKE :prefix') ->setParameter('prefix', 'test_%')->execute() ; parent::tearDown(); } protected function createAdminClient(): Client { return $this->authenticatedClient('admin', 'admin'); } /** * Recupere (ou cree) le type PRESTATAIRE. Idempotent (unicite category_type.code). */ protected function providerCategoryType(): CategoryType { $em = $this->getEm(); $existing = $em->getRepository(CategoryType::class)->findOneBy(['code' => 'PRESTATAIRE']); if (null !== $existing) { return $existing; } $type = new CategoryType(); $type->setCode('PRESTATAIRE'); $type->setLabel('Prestataire'); $em->persist($type); $em->flush(); return $type; } /** * Fetch-or-create une categorie de type PRESTATAIRE par code (defaut NETTOYAGE). * Idempotent (lookup par code, aligne sur l'index unique partiel uq_category_code) * et auto-suffisant. Nom prefixe -> purge par tearDown. */ protected function providerCategory(string $code = 'NETTOYAGE'): Category { $em = $this->getEm(); $existing = $em->getRepository(Category::class)->findOneBy(['code' => $code, 'deletedAt' => null]); if (null !== $existing) { return $existing; } $category = new Category(); $category->setName(self::TEST_CATEGORY_PREFIX.strtolower($code)); $category->setCode($code); $category->addCategoryType($this->providerCategoryType()); $em->persist($category); $em->flush(); return $category; } /** * Cree une categorie d'un type DIFFERENT de PRESTATAIRE (pour tester le rejet * RG-3.09). Code unique pour ne pas collisionner avec une categorie existante. */ protected function foreignCategory(): Category { $em = $this->getEm(); $suffix = substr(bin2hex(random_bytes(4)), 0, 8); $type = $em->getRepository(CategoryType::class)->findOneBy(['code' => 'CLIENT']); if (null === $type) { $type = new CategoryType(); $type->setCode('CLIENT'); $type->setLabel('Client'); $em->persist($type); } $category = new Category(); $category->setName(self::TEST_CATEGORY_PREFIX.'foreign_'.$suffix); $category->setCode('FOREIGN_'.strtoupper($suffix)); $category->addCategoryType($type); $em->persist($category); $em->flush(); return $category; } /** * Recupere un site fixture par code postal (cf. SitesFixtures). Echoue * explicitement si absent (fixtures non chargees / module Sites off). */ protected function site(string $postalCode): Site { $site = $this->getEm()->getRepository(Site::class)->findOneBy(['postalCode' => $postalCode]); self::assertNotNull( $site, sprintf('Site fixture "%s" introuvable : SitesFixtures charge (make test-db-setup) ?', $postalCode), ); return $site; } /** * Seede directement un Provider minimal (sans passer par l'API), pour les tests * de liste / archivage / cloisonnement. Nom stocke en MAJUSCULES pour refleter * l'etat normalise (RG-3.11) qu'aurait produit le ProviderProcessor. Porte une * categorie PRESTATAIRE + les sites donnes (par code postal). * * @param list $sitePostalCodes codes postaux des sites a rattacher */ protected function seedProvider( string $companyName, array $sitePostalCodes = [self::SITE_86], bool $isArchived = false, string $categoryCode = 'NETTOYAGE', ?string $siren = null, ): Provider { $em = $this->getEm(); $provider = new Provider(); $provider->setCompanyName(mb_strtoupper($companyName, 'UTF-8')); $provider->addCategory($this->providerCategory($categoryCode)); foreach ($sitePostalCodes as $postalCode) { $provider->addSite($this->site($postalCode)); } if (null !== $siren) { $provider->setSiren($siren); } $provider->setIsArchived($isArchived); if ($isArchived) { $provider->setArchivedAt(new DateTimeImmutable()); } $em->persist($provider); $em->flush(); return $provider; } /** * Payload minimal valide du formulaire principal (companyName + 1 categorie * PRESTATAIRE + sites donnes). Categorie NETTOYAGE par defaut. * * @param list $sitePostalCodes * * @return array */ protected function validMainPayload(string $companyName, array $sitePostalCodes = [self::SITE_86]): array { $siteIris = array_map(fn (string $pc): string => '/api/sites/'.$this->site($pc)->getId(), $sitePostalCodes); return [ 'companyName' => $companyName, 'categories' => ['/api/categories/'.$this->providerCategory()->getId()], 'sites' => $siteIris, ]; } /** * Cree un utilisateur non-admin CLOISONNE : porte les permissions donnees via * un role jetable, rattache aux seuls sites donnes (par code postal), avec un * currentSite positionne. N'a PAS `sites.bypass_scope` (sauf si fourni dans * $permissionCodes) -> sujet ideal des tests de cloisonnement (RG-3.17). * * Contrairement a createUserWithPermissions() (parent, qui attache TOUS les * sites et ne pose pas de currentSite), ce helper controle finement le * perimetre site de l'user. * * @param list $permissionCodes * @param list $sitePostalCodes sites a rattacher (user_site) * * @return array{username: string, password: string} */ protected function createScopedUser( array $permissionCodes, array $sitePostalCodes, ?string $currentSitePostalCode = null, ): array { $em = $this->getEm(); $suffix = substr(bin2hex(random_bytes(4)), 0, 8); $username = 'test_scoped_'.$suffix; $password = 'testpass'; /** @var UserPasswordHasherInterface $hasher */ $hasher = self::getContainer()->get(UserPasswordHasherInterface::class); $role = new Role('test_'.$suffix, 'Test Role '.$suffix, false); foreach ($permissionCodes as $code) { $permission = $em->getRepository(Permission::class)->findOneBy(['code' => $code]); self::assertNotNull($permission, sprintf('Permission "%s" introuvable (app:sync-permissions ?).', $code)); $role->addPermission($permission); } $em->persist($role); $user = new User(); $user->setUsername($username); $user->setIsAdmin(false); $user->setPassword($hasher->hashPassword($user, $password)); $user->addRbacRole($role); foreach ($sitePostalCodes as $postalCode) { $user->addSite($this->site($postalCode)); } if (null !== $currentSitePostalCode) { $user->setCurrentSite($this->site($currentSitePostalCode)); } $em->persist($user); $em->flush(); $em->clear(); return ['username' => $username, 'password' => $password]; } /** * Ajoute un contact a un prestataire deja persiste (seed direct). */ protected function addContact( Provider $provider, ?string $firstName = 'Marie', ?string $lastName = 'Martin', ?string $phonePrimary = null, ?string $email = null, int $position = 0, ): ProviderContact { $contact = new ProviderContact(); $contact->setProvider($provider); $contact->setFirstName($firstName); $contact->setLastName($lastName); $contact->setPhonePrimary($phonePrimary); $contact->setEmail($email); $contact->setPosition($position); $provider->addContact($contact); $this->getEm()->persist($contact); $this->getEm()->flush(); return $contact; } /** * Ajoute un RIB a un prestataire deja persiste (seed direct). */ protected function addRib(Provider $provider, string $label = 'Compte principal'): ProviderRib { $rib = new ProviderRib(); $rib->setProvider($provider); $rib->setLabel($label); $rib->setBic(self::VALID_BIC); $rib->setIban(self::VALID_IBAN); $provider->addRib($rib); $this->getEm()->persist($rib); $this->getEm()->flush(); return $rib; } /** * Recupere un type de reglement seede (CommercialReferentialFixtures) par code * (ex. LCR, VIREMENT). Echoue explicitement si absent (fixtures non chargees). */ protected function paymentType(string $code): PaymentType { $paymentType = $this->getEm()->getRepository(PaymentType::class)->findOneBy(['code' => $code]); self::assertNotNull( $paymentType, sprintf('Type de reglement "%s" introuvable : fixtures comptables chargees (make test-db-setup) ?', $code), ); return $paymentType; } /** * Recupere une banque seedee (CommercialReferentialFixtures) par code (ex. SG). * Echoue explicitement si absente (fixtures non chargees). */ protected function bank(string $code): Bank { $bank = $this->getEm()->getRepository(Bank::class)->findOneBy(['code' => $code]); self::assertNotNull( $bank, sprintf('Banque "%s" introuvable : fixtures comptables chargees (make test-db-setup) ?', $code), ); return $bank; } /** * Indexe les violations d'un corps 422 par propertyPath (assert ciblee). * * @param array $body corps decode (toArray(false)) * * @return array propertyPath => message */ protected function violationsByPath(array $body): array { $byPath = []; foreach ($body['violations'] ?? [] as $v) { $byPath[$v['propertyPath']] = $v['message']; } return $byPath; } }