ab15452459
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é.
163 lines
6.4 KiB
PHP
163 lines
6.4 KiB
PHP
<?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,
|
|
);
|
|
}
|
|
}
|