403. $processor = $this->makeProcessor(granted: [], payload: ['siren' => '123456789']); $this->expectException(AccessDeniedHttpException::class); $processor->process($this->minimalClient(), $this->operation()); } public function testStrictMixWithAccountingFieldIsForbidden(): void { // RG-1.28 : payload mixant main + accounting sans la permission -> 403 // sur l'ensemble (pas de filtrage silencieux). $processor = $this->makeProcessor( granted: [], payload: ['companyName' => 'X', 'siren' => '123456789'], ); $this->expectException(AccessDeniedHttpException::class); $processor->process($this->minimalClient(), $this->operation()); } public function testArchiveWithoutPermissionIsForbidden(): void { // RG-1.22 : isArchived sans la permission archive -> 403. $processor = $this->makeProcessor(granted: [], payload: ['isArchived' => true]); $this->expectException(AccessDeniedHttpException::class); $processor->process($this->minimalClient(), $this->operation()); } public function testArchiveWithOtherFieldIsUnprocessable(): void { // RG-1.22 : une requete d'archivage ne modifie aucun autre champ. $processor = $this->makeProcessor( granted: ['commercial.clients.archive'], payload: ['isArchived' => true, 'companyName' => 'X'], ); $this->expectException(UnprocessableEntityHttpException::class); $processor->process($this->minimalClient(), $this->operation()); } public function testVirementWithoutBankIsUnprocessable(): void { // RG-1.12 $client = $this->minimalClient(); $client->setPaymentType($this->paymentType('VIREMENT')); $processor = $this->makeProcessor( granted: ['commercial.clients.accounting.manage'], payload: ['paymentType' => '/api/payment_types/1'], ); $this->expectException(ValidationException::class); $processor->process($client, $this->operation()); } public function testVirementWithBankPasses(): void { // RG-1.12 satisfait : Virement + banque. $client = $this->minimalClient(); $client->setPaymentType($this->paymentType('VIREMENT')); $client->setBank(new Bank()); $processor = $this->makeProcessor( granted: ['commercial.clients.accounting.manage'], payload: ['paymentType' => '/api/payment_types/1', 'bank' => '/api/banks/1'], ); $result = $processor->process($client, $this->operation()); self::assertInstanceOf(Client::class, $result); } public function testLcrWithoutRibIsUnprocessable(): void { // RG-1.13 $client = $this->minimalClient(); $client->setPaymentType($this->paymentType('LCR')); $processor = $this->makeProcessor( granted: ['commercial.clients.accounting.manage'], payload: ['paymentType' => '/api/payment_types/2'], ); $this->expectException(ValidationException::class); $processor->process($client, $this->operation()); } public function testLcrWithRibPasses(): void { // RG-1.13 satisfait : LCR + au moins un RIB. $client = $this->minimalClient(); $client->setPaymentType($this->paymentType('LCR')); $client->addRib(new ClientRib()); $processor = $this->makeProcessor( granted: ['commercial.clients.accounting.manage'], payload: ['paymentType' => '/api/payment_types/2'], ); self::assertInstanceOf(Client::class, $processor->process($client, $this->operation())); } public function testCommercialeIncompleteInformationIsUnprocessable(): void { // RG-1.04 : role Commerciale + onglet Information incomplet -> 422. $client = $this->minimalClient(); $client->setDescription('Une description'); // les autres champs Information restent null $processor = $this->makeProcessor( granted: [], payload: ['description' => 'Une description'], user: $this->commercialeUser(), ); $this->expectException(ValidationException::class); $processor->process($client, $this->operation()); } public function testNonCommercialeSkipsInformationCompleteness(): void { // Meme payload incomplet, mais user non-Commerciale -> aucun blocage. $client = $this->minimalClient(); $client->setDescription('Une description'); $processor = $this->makeProcessor( granted: [], payload: ['description' => 'Une description'], user: null, ); self::assertInstanceOf(Client::class, $processor->process($client, $this->operation())); } /** * @param list $granted Permissions accordees a l'utilisateur courant * @param array $payload Corps JSON simule de la requete */ private function makeProcessor(array $granted, array $payload, ?UserInterface $user = null): ClientProcessor { $persist = new class implements ProcessorInterface { public function process(mixed $data, Operation $operation, array $uriVariables = [], array $context = []): mixed { return $data; } }; $security = $this->createStub(Security::class); $security->method('isGranted')->willReturnCallback( static fn (mixed $attribute): bool => is_string($attribute) && in_array($attribute, $granted, true), ); $security->method('getUser')->willReturn($user); $requestStack = new RequestStack(); $requestStack->push(new Request([], [], [], [], [], [], json_encode($payload, JSON_THROW_ON_ERROR))); return new ClientProcessor( $persist, new ClientFieldNormalizer(), new ClientInformationCompletenessValidator(), $security, $requestStack, ); } /** * Client minimal valide vis-a-vis de RG-1.01 (un nom de contact) — suffisant * pour atteindre les validations testees. */ private function minimalClient(): Client { $client = new Client(); $client->setCompanyName('Test Co'); $client->setLastName('Dupont'); $client->setPhonePrimary('0102030405'); $client->setEmail('t@test.fr'); return $client; } private function paymentType(string $code): PaymentType { $type = new PaymentType(); $type->setCode($code); $type->setLabel($code); return $type; } private function operation(): Operation { return $this->createStub(Operation::class); } private function commercialeUser(): UserInterface { return new class implements UserInterface, BusinessRoleAwareInterface { public function hasBusinessRole(string $roleCode): bool { return BusinessRoles::COMMERCIALE === $roleCode; } public function getRoles(): array { return ['ROLE_USER']; } public function eraseCredentials(): void {} public function getUserIdentifier(): string { return 'commerciale-test'; } }; } }