test(logistique) : tests PHPUnit RG-5.01→5.10 + capture contrat JSON (ERP-187)
Couverture des règles de gestion du M5 (tickets de pesée) et capture de la
réponse JSON réelle (DoD § 4.0.bis) avant les écrans front.
Tests unitaires (Processor/Normalizer/Callback, sans BDD ni HTTP) :
- NetWeightTest (RG-5.05) : net = plein − vide, null si pesée manquante, recalcul PATCH.
- CounterpartyValidationTest (RG-5.03) : présence par branche (propertyPath) + exclusivité.
- ImmatriculationNormalizationTest (RG-5.01/5.10) : masque XX-000-XX, « Tout format », 422.
Tests fonctionnels (API réelle) :
- WeighingTicketNumberingTest (RG-5.02/5.09) : format {siteCode}-TP-{NNNN}, séquence
par site, isolation inter-sites, immuabilité numéro/site au PATCH.
- WeighingTicketSerializationContractTest (DoD § 4.0.bis) : 4 pièges (client embarqué,
plateFreeFormat présent, number formaté, netWeight = full − empty) + dump JSON.
- WeighingTicketRBACMatrixTest (§ 5.2) : admin/bureau/usine OK, compta/commerciale 403,
anonyme 401.
DSD/stub/reading déjà couverts (ERP-184/185). spec-back.md § 4.0.bis : JSON réel collé.
This commit is contained in:
@@ -0,0 +1,162 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Tests\Module\Logistique\Application\Service;
|
||||
|
||||
use ApiPlatform\Metadata\Post;
|
||||
use ApiPlatform\State\ProcessorInterface;
|
||||
use ApiPlatform\Validator\Exception\ValidationException;
|
||||
use App\Module\Logistique\Application\Service\DsdAllocatorInterface;
|
||||
use App\Module\Logistique\Application\Service\WeighingTicketFieldNormalizer;
|
||||
use App\Module\Logistique\Application\Service\WeighingTicketNumberAllocatorInterface;
|
||||
use App\Module\Logistique\Domain\Entity\WeighingTicket;
|
||||
use App\Module\Logistique\Domain\Exception\InvalidImmatriculationException;
|
||||
use App\Module\Logistique\Infrastructure\ApiPlatform\State\Processor\WeighingTicketProcessor;
|
||||
use App\Module\Sites\Application\Service\CurrentSiteProviderInterface;
|
||||
use App\Module\Sites\Domain\Entity\Site;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use PHPUnit\Framework\Attributes\DataProvider;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
/**
|
||||
* Normalisation de l'immatriculation (RG-5.01 / RG-5.10 / § 6 / § 2.10) — test
|
||||
* unitaire en deux volets, sans BDD ni HTTP :
|
||||
*
|
||||
* 1. Le WeighingTicketFieldNormalizer applique trim + UPPER ; hors « Tout format »
|
||||
* il ramene la saisie au masque SIV canonique XX-000-XX (separateurs/espaces
|
||||
* ignores puis re-poses) et leve InvalidImmatriculationException si le squelette
|
||||
* 2-3-2 n'est pas respecte. En « Tout format », seul trim + UPPER s'applique.
|
||||
* 2. Le WeighingTicketProcessor traduit cette exception de domaine en 422 portant
|
||||
* un propertyPath « immatriculation » (mapping inline useFormErrors, ERP-101).
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
final class ImmatriculationNormalizationTest extends TestCase
|
||||
{
|
||||
private WeighingTicketFieldNormalizer $normalizer;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
$this->normalizer = new WeighingTicketFieldNormalizer();
|
||||
}
|
||||
|
||||
// === Volet 1 : normalisation pure (masque + « Tout format ») ===
|
||||
|
||||
#[DataProvider('provideMaskedPlates')]
|
||||
public function testMaskedPlateIsReformattedToCanonicalSiv(string $input): void
|
||||
{
|
||||
self::assertSame('AB-123-CD', $this->normalizer->normalizeImmatriculation($input, false));
|
||||
}
|
||||
|
||||
/**
|
||||
* @return iterable<string, array{string}>
|
||||
*/
|
||||
public static function provideMaskedPlates(): iterable
|
||||
{
|
||||
yield 'deja canonique' => ['AB-123-CD'];
|
||||
yield 'minuscules nues' => ['ab123cd'];
|
||||
yield 'espaces' => ['AB 123 CD'];
|
||||
yield 'minuscules tirets'=> ['ab-123-cd'];
|
||||
yield 'espaces de garde' => [' ab-123-cd '];
|
||||
}
|
||||
|
||||
public function testInvalidPlateWithoutFreeFormatThrows(): void
|
||||
{
|
||||
$this->expectException(InvalidImmatriculationException::class);
|
||||
$this->normalizer->normalizeImmatriculation('ABC-12-D', false);
|
||||
}
|
||||
|
||||
public function testFreeFormatBypassesTheMask(): void
|
||||
{
|
||||
// Ancienne plaque / engin : aucune contrainte de masque, juste trim + UPPER.
|
||||
self::assertSame('1234 WW 75', $this->normalizer->normalizeImmatriculation(' 1234 ww 75 ', true));
|
||||
self::assertSame('ENGIN-XYZ', $this->normalizer->normalizeImmatriculation('engin-xyz', true));
|
||||
}
|
||||
|
||||
public function testNullAndBlankAreNormalizedToNull(): void
|
||||
{
|
||||
self::assertNull($this->normalizer->normalizeImmatriculation(null, false));
|
||||
self::assertNull($this->normalizer->normalizeImmatriculation(' ', false));
|
||||
self::assertNull($this->normalizer->normalizeImmatriculation(' ', true));
|
||||
}
|
||||
|
||||
public function testOtherLabelIsTrimmedAndBlankBecomesNull(): void
|
||||
{
|
||||
self::assertSame('Reprise interne', $this->normalizer->normalizeOtherLabel(' Reprise interne '));
|
||||
self::assertNull($this->normalizer->normalizeOtherLabel(' '));
|
||||
self::assertNull($this->normalizer->normalizeOtherLabel(null));
|
||||
}
|
||||
|
||||
// === Volet 2 : mapping 422 par le Processor (RG-5.01, ERP-101) ===
|
||||
|
||||
public function testProcessorMapsInvalidPlateTo422OnImmatriculationPath(): void
|
||||
{
|
||||
$ticket = (new WeighingTicket())
|
||||
->setCounterpartyType('AUTRE')
|
||||
->setOtherLabel('Reprise')
|
||||
->setImmatriculation('PLAQUE INVALIDE')
|
||||
->setPlateFreeFormat(false)
|
||||
;
|
||||
|
||||
try {
|
||||
$this->makeProcessor()->process($ticket, new Post());
|
||||
self::fail('Une ValidationException (422) etait attendue sur une immatriculation invalide.');
|
||||
} catch (ValidationException $e) {
|
||||
$paths = [];
|
||||
foreach ($e->getConstraintViolationList() as $violation) {
|
||||
$paths[] = $violation->getPropertyPath();
|
||||
}
|
||||
self::assertContains('immatriculation', $paths);
|
||||
}
|
||||
}
|
||||
|
||||
public function testProcessorReformatsValidPlateAndHonorsFreeFormat(): void
|
||||
{
|
||||
// Masque applique a la persistance (saisie nue -> canonique).
|
||||
$masked = (new WeighingTicket())
|
||||
->setCounterpartyType('AUTRE')
|
||||
->setOtherLabel('Reprise')
|
||||
->setImmatriculation('ab123cd')
|
||||
->setPlateFreeFormat(false)
|
||||
;
|
||||
$this->makeProcessor()->process($masked, new Post());
|
||||
self::assertSame('AB-123-CD', $masked->getImmatriculation());
|
||||
|
||||
// « Tout format » : la plaque libre passe (UPPER seulement), aucune 422.
|
||||
$free = (new WeighingTicket())
|
||||
->setCounterpartyType('AUTRE')
|
||||
->setOtherLabel('Reprise')
|
||||
->setImmatriculation('vieux 4321 zz')
|
||||
->setPlateFreeFormat(true)
|
||||
;
|
||||
$this->makeProcessor()->process($free, new Post());
|
||||
self::assertSame('VIEUX 4321 ZZ', $free->getImmatriculation());
|
||||
}
|
||||
|
||||
private function makeProcessor(): WeighingTicketProcessor
|
||||
{
|
||||
$persist = $this->createStub(ProcessorInterface::class);
|
||||
$persist->method('process')->willReturnArgument(0);
|
||||
|
||||
$siteProvider = $this->createStub(CurrentSiteProviderInterface::class);
|
||||
$siteProvider->method('get')->willReturn(
|
||||
(new Site('Châtellerault', 'Rue du Pont', null, '86000', 'Châtellerault', '#112233'))->setCode('86'),
|
||||
);
|
||||
|
||||
$numberAllocator = $this->createStub(WeighingTicketNumberAllocatorInterface::class);
|
||||
$numberAllocator->method('allocate')->willReturn('86-TP-0001');
|
||||
|
||||
$em = $this->createStub(EntityManagerInterface::class);
|
||||
$em->method('contains')->willReturn(false);
|
||||
|
||||
return new WeighingTicketProcessor(
|
||||
$persist,
|
||||
$siteProvider,
|
||||
$numberAllocator,
|
||||
$this->createStub(DsdAllocatorInterface::class),
|
||||
new WeighingTicketFieldNormalizer(),
|
||||
$em,
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user