e265a008bc
Auto Tag Develop / tag (push) Successful in 7s
Étape 4/7 du M2 fournisseurs — stackée sur #67 (ERP-88). ## Périmètre (RG-2.03 / 2.07 / 2.08 / 2.10) Décision figée ERP-89 : les RG inter-champs passent par `Assert\Callback` + `->atPath()` sur l'entité Supplier (et non dans le Processor), pour que chaque 422 porte un `propertyPath` consommable par `extractApiViolations` (mapping inline, pas un toast — ERP-101). - **RG-2.10** — `Supplier::validateCategoryType()` → `atPath('categories')` : catégories de type FOURNISSEUR uniquement sur `supplier.categories` (miroir d'ERP-88 côté adresse). - **RG-2.07** — `Supplier::validatePaymentTypeConsistency()` → `atPath('bank')` : VIREMENT impose une banque. - **RG-2.08** — même Callback → `atPath('ribs')` : LCR impose ≥ 1 RIB (le 409 sur DELETE du dernier RIB en LCR reste porté par ERP-88). - **RG-2.03** — `SupplierInformationCompletenessValidator` (8 champs Information dont `volumeForecast`), invoqué par le `SupplierProcessor` après détection back du rôle Commerciale via `BusinessRoleAwareInterface`. Le Processor ne porte que rôle / mode strict / gating. ## Tests (16, verts) - `SupplierValidationTest` — Callbacks RG-2.07/2.08/2.10, assertion par propertyPath. - `SupplierInformationCompletenessValidatorTest` — complétude / champs manquants / zéros valides. - `SupplierProcessorTest` — détection rôle RG-2.03 (POST + PATCH main-only + non-Commerciale). `make test` : 499 tests OK. `php-cs-fixer` : clean. --------- Co-authored-by: admin malio <malio@yuno.malio.fr> Co-authored-by: Matthieu <contact@malio.fr> Reviewed-on: #68 Co-authored-by: THOLOT DECHENE Matthieu <matthieu@yuno.malio.fr> Co-committed-by: THOLOT DECHENE Matthieu <matthieu@yuno.malio.fr>
200 lines
7.2 KiB
PHP
200 lines
7.2 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace App\Tests\Module\Commercial\Unit;
|
|
|
|
use ApiPlatform\Metadata\Operation;
|
|
use ApiPlatform\State\ProcessorInterface;
|
|
use ApiPlatform\Validator\Exception\ValidationException;
|
|
use App\Module\Commercial\Application\Service\SupplierFieldNormalizer;
|
|
use App\Module\Commercial\Application\Validator\SupplierInformationCompletenessValidator;
|
|
use App\Module\Commercial\Domain\Entity\Supplier;
|
|
use App\Module\Commercial\Infrastructure\ApiPlatform\State\Processor\SupplierProcessor;
|
|
use App\Shared\Domain\Contract\BusinessRoleAwareInterface;
|
|
use App\Shared\Domain\Security\BusinessRoles;
|
|
use DateTimeImmutable;
|
|
use Doctrine\ORM\EntityManagerInterface;
|
|
use Doctrine\ORM\UnitOfWork;
|
|
use PHPUnit\Framework\TestCase;
|
|
use Symfony\Bundle\SecurityBundle\Security;
|
|
use Symfony\Component\HttpFoundation\Request;
|
|
use Symfony\Component\HttpFoundation\RequestStack;
|
|
use Symfony\Component\Security\Core\User\UserInterface;
|
|
|
|
/**
|
|
* Tests unitaires du SupplierProcessor — perimetre ERP-89 : detection du role
|
|
* Commerciale cote back (RG-2.03). Les autres responsabilites du processor
|
|
* (gating accounting / archive / mode strict) sont heritees d'ERP-87 et testees
|
|
* a leur niveau ; les RG inter-champs (RG-2.07/2.08/2.10) sont des contraintes
|
|
* d'entite (cf. SupplierValidationTest), non portees ici.
|
|
*
|
|
* @internal
|
|
*/
|
|
final class SupplierProcessorTest extends TestCase
|
|
{
|
|
public function testCommercialeIncompleteInformationIsUnprocessable(): void
|
|
{
|
|
// RG-2.03 : role Commerciale + onglet Information incomplet -> 422, meme
|
|
// sur un POST (les champs Information n'y sont pas renseignables).
|
|
$supplier = $this->minimalSupplier();
|
|
$supplier->setDescription('Une description'); // les autres champs Information restent null
|
|
|
|
$processor = $this->makeProcessor(
|
|
payload: ['description' => 'Une description'],
|
|
user: $this->commercialeUser(),
|
|
);
|
|
|
|
$this->expectException(ValidationException::class);
|
|
$processor->process($supplier, $this->operation());
|
|
}
|
|
|
|
public function testCommercialeIncompleteInformationOnMainOnlyPatchIsUnprocessable(): void
|
|
{
|
|
// RG-2.03 : pour une Commerciale, la completude Information est exigee
|
|
// meme quand le payload ne touche PAS l'onglet Information (ici
|
|
// companyName seul) -> 422.
|
|
$supplier = $this->minimalSupplier();
|
|
$supplier->setCompanyName('Renamed Co');
|
|
|
|
$processor = $this->makeProcessor(
|
|
granted: ['commercial.suppliers.manage'],
|
|
payload: ['companyName' => 'Renamed Co'],
|
|
user: $this->commercialeUser(),
|
|
managed: true,
|
|
originalData: [
|
|
'companyName' => 'TEST CO',
|
|
'isArchived' => false,
|
|
],
|
|
);
|
|
|
|
$this->expectException(ValidationException::class);
|
|
$processor->process($supplier, $this->operation());
|
|
}
|
|
|
|
public function testCommercialeCompleteInformationPasses(): void
|
|
{
|
|
// RG-2.03 satisfaite : tous les champs Information renseignes -> 200.
|
|
$supplier = $this->completeInformationSupplier();
|
|
|
|
$processor = $this->makeProcessor(
|
|
granted: ['commercial.suppliers.manage'],
|
|
payload: ['description' => 'desc'],
|
|
user: $this->commercialeUser(),
|
|
);
|
|
|
|
self::assertInstanceOf(Supplier::class, $processor->process($supplier, $this->operation()));
|
|
}
|
|
|
|
public function testNonCommercialeSkipsInformationCompleteness(): void
|
|
{
|
|
// Meme onglet Information incomplet, mais user non-Commerciale -> aucun
|
|
// blocage (la completude est specifique a la Commerciale).
|
|
$supplier = $this->minimalSupplier();
|
|
$supplier->setDescription('Une description');
|
|
|
|
$processor = $this->makeProcessor(
|
|
payload: ['description' => 'Une description'],
|
|
user: null,
|
|
);
|
|
|
|
self::assertInstanceOf(Supplier::class, $processor->process($supplier, $this->operation()));
|
|
}
|
|
|
|
/**
|
|
* @param list<string> $granted Permissions accordees a l'utilisateur courant
|
|
* @param array<string, mixed> $payload Corps JSON simule de la requete
|
|
* @param bool $managed true = entite geree par l'ORM (PATCH), false = creation (POST)
|
|
* @param array<string, mixed> $originalData Etat persiste simule (getOriginalEntityData)
|
|
*/
|
|
private function makeProcessor(
|
|
array $granted = [],
|
|
array $payload = [],
|
|
?UserInterface $user = null,
|
|
bool $managed = false,
|
|
array $originalData = [],
|
|
): SupplierProcessor {
|
|
$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)));
|
|
|
|
$uow = $this->createMock(UnitOfWork::class);
|
|
$uow->method('getOriginalEntityData')->willReturn($originalData);
|
|
|
|
$em = $this->createMock(EntityManagerInterface::class);
|
|
$em->method('contains')->willReturn($managed);
|
|
$em->method('getUnitOfWork')->willReturn($uow);
|
|
|
|
return new SupplierProcessor(
|
|
$persist,
|
|
new SupplierFieldNormalizer(),
|
|
new SupplierInformationCompletenessValidator(),
|
|
$security,
|
|
$requestStack,
|
|
$em,
|
|
);
|
|
}
|
|
|
|
private function minimalSupplier(): Supplier
|
|
{
|
|
$supplier = new Supplier();
|
|
$supplier->setCompanyName('Test Co');
|
|
|
|
return $supplier;
|
|
}
|
|
|
|
private function completeInformationSupplier(): Supplier
|
|
{
|
|
$supplier = $this->minimalSupplier();
|
|
$supplier->setDescription('desc');
|
|
$supplier->setCompetitors('concurrents');
|
|
$supplier->setFoundedAt(new DateTimeImmutable('2010-01-01'));
|
|
$supplier->setEmployeesCount(10);
|
|
$supplier->setRevenueAmount('1000.00');
|
|
$supplier->setDirectorName('Marie Durand');
|
|
$supplier->setProfitAmount('100.00');
|
|
$supplier->setVolumeForecast(500);
|
|
|
|
return $supplier;
|
|
}
|
|
|
|
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';
|
|
}
|
|
};
|
|
}
|
|
}
|