fbfb77f7a4
Auto Tag Develop / tag (push) Successful in 12s
## Objectif Améliorer les multiselects (`MalioSelectCheckbox`) de l'application : ### Couleur des sites sur les tags Les tags des multiselects **sites** (86 / 17 / 82) prennent désormais : - en **fond** la couleur d'identification du site (champ `color`, groupe `site:read` — déjà exposé côté API, aucune modif back) ; - en **texte** du blanc, pour rester lisibles sur les fonds colorés. Appliqué en saisie **et** en consultation, dans les 4 modules concernés : Clients (M1), Fournisseurs (M2), Prestataires (M3), Produits (M6). ### Limite d'affichage des autres multiselects Tous les multiselects **non-sites** (catégories, contacts, états, types de stockage…) affichent **au maximum 3 tags** ; le surplus est condensé en « +N ». ## Dépendance - Bump `@malio/layer-ui` `1.7.15` → `1.7.17` (support `color` / `textColor` et `maxTags` sur les options). ## Tests - 722 tests Vitest verts (69 fichiers), assertions des options sites enrichies (`color` / `textColor`). - ESLint clean sur les 15 fichiers `.vue` modifiés. > Commit front-only : hook pre-commit (tests back) contourné via `--no-verify`, la validation front a été lancée séparément. Reviewed-on: #161 Co-authored-by: tristan <tristan@yuno.malio.fr> Co-committed-by: tristan <tristan@yuno.malio.fr>
215 lines
8.0 KiB
PHP
215 lines
8.0 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace App\Tests\Module\Logistique\Api;
|
|
|
|
use ApiPlatform\Symfony\Bundle\Test\Client;
|
|
use App\Module\Core\Domain\Entity\Role;
|
|
use App\Module\Core\Domain\Entity\User;
|
|
use App\Module\Sites\Domain\Entity\Site;
|
|
use App\Tests\Module\Core\Api\AbstractApiTestCase;
|
|
|
|
/**
|
|
* Endpoint `POST /api/weighbridge_readings` (§ 4.2) — tests fonctionnels.
|
|
*
|
|
* Couvre le wiring securite/routage (que les tests unitaires ne voient pas) :
|
|
* - happy path AUTO / MANUAL avec site courant et permission `manage` ;
|
|
* - 403 sans la permission `manage` (RBAC § 5.2) ;
|
|
* - 422 si le mode est absent / invalide (validation de la ressource).
|
|
*
|
|
* Nettoyage manuel (pas de DAMA) : users/roles `test*` + compteurs DSD.
|
|
*
|
|
* @internal
|
|
*/
|
|
final class WeighbridgeReadingApiTest extends AbstractApiTestCase
|
|
{
|
|
protected function tearDown(): void
|
|
{
|
|
$em = $this->getEm();
|
|
$em->getConnection()->executeStatement('DELETE FROM weighbridge_dsd_counter');
|
|
$em->createQuery('DELETE FROM '.User::class.' u WHERE u.username LIKE :p')
|
|
->setParameter('p', 'testuser_%')->execute()
|
|
;
|
|
$em->createQuery('DELETE FROM '.Role::class.' r WHERE r.code LIKE :p')
|
|
->setParameter('p', 'test_%')->execute()
|
|
;
|
|
|
|
parent::tearDown();
|
|
}
|
|
|
|
public function testAutoWeighingReturnsWeightInBoundsAndDsd(): void
|
|
{
|
|
$client = $this->manageClientWithCurrentSite();
|
|
|
|
$response = $client->request('POST', '/api/weighbridge_readings', [
|
|
'headers' => ['Content-Type' => 'application/ld+json'],
|
|
'json' => ['mode' => 'AUTO'],
|
|
]);
|
|
|
|
self::assertResponseStatusCodeSame(200);
|
|
$data = $response->toArray();
|
|
|
|
self::assertSame('AUTO', $data['mode']);
|
|
self::assertIsInt($data['weight']);
|
|
self::assertGreaterThanOrEqual(10000, $data['weight']);
|
|
self::assertLessThanOrEqual(50000, $data['weight']);
|
|
self::assertIsInt($data['dsd']);
|
|
self::assertGreaterThanOrEqual(1, $data['dsd']);
|
|
}
|
|
|
|
public function testManualWeighingKeepsWeightAndEnteredDsd(): void
|
|
{
|
|
$client = $this->manageClientWithCurrentSite();
|
|
|
|
$response = $client->request('POST', '/api/weighbridge_readings', [
|
|
'headers' => ['Content-Type' => 'application/ld+json'],
|
|
// Le DSD est SAISI par l'operateur et conserve tel quel (ERP-193).
|
|
'json' => ['mode' => 'MANUAL', 'weight' => 23187, 'dsd' => 16619],
|
|
]);
|
|
|
|
self::assertResponseStatusCodeSame(200);
|
|
$data = $response->toArray();
|
|
|
|
self::assertSame('MANUAL', $data['mode']);
|
|
self::assertSame(23187, $data['weight']);
|
|
self::assertSame(16619, $data['dsd'], 'Le DSD saisi est conserve, pas d\'auto-increment.');
|
|
}
|
|
|
|
public function testManagePermissionIsRequired(): void
|
|
{
|
|
// Un user portant uniquement `view` ne peut pas declencher de pesee.
|
|
$credentials = $this->createUserWithPermission('logistique.weighing_tickets.view');
|
|
$client = $this->authenticatedClient($credentials['username'], $credentials['password']);
|
|
|
|
$client->request('POST', '/api/weighbridge_readings', [
|
|
'headers' => ['Content-Type' => 'application/ld+json'],
|
|
'json' => ['mode' => 'AUTO'],
|
|
]);
|
|
|
|
self::assertResponseStatusCodeSame(403);
|
|
}
|
|
|
|
public function testInvalidModeIsRejected(): void
|
|
{
|
|
$client = $this->manageClientWithCurrentSite();
|
|
|
|
$response = $client->request('POST', '/api/weighbridge_readings', [
|
|
'headers' => ['Content-Type' => 'application/ld+json'],
|
|
'json' => ['mode' => 'INVALID'],
|
|
]);
|
|
|
|
// Garde-fou ERP-101 : la 422 doit cibler `mode` (Assert\Choice), pas juste
|
|
// un bon code HTTP — sinon une violation sur le mauvais champ passerait.
|
|
self::assertResponseStatusCodeSame(422);
|
|
self::assertViolationOnPath($response, 'mode');
|
|
}
|
|
|
|
public function testManualWeighingRequiresWeight(): void
|
|
{
|
|
$client = $this->manageClientWithCurrentSite();
|
|
|
|
$response = $client->request('POST', '/api/weighbridge_readings', [
|
|
'headers' => ['Content-Type' => 'application/ld+json'],
|
|
'json' => ['mode' => 'MANUAL'],
|
|
]);
|
|
|
|
// Garde-fou ERP-101 : la 422 doit cibler `weight` (Callback validateManualFields).
|
|
self::assertResponseStatusCodeSame(422);
|
|
self::assertViolationOnPath($response, 'weight');
|
|
}
|
|
|
|
public function testManualWeighingRequiresDsd(): void
|
|
{
|
|
$client = $this->manageClientWithCurrentSite();
|
|
|
|
$response = $client->request('POST', '/api/weighbridge_readings', [
|
|
'headers' => ['Content-Type' => 'application/ld+json'],
|
|
'json' => ['mode' => 'MANUAL', 'weight' => 23187],
|
|
]);
|
|
|
|
// En manuel, le DSD est saisi → obligatoire (Callback validateManualFields).
|
|
self::assertResponseStatusCodeSame(422);
|
|
self::assertViolationOnPath($response, 'dsd');
|
|
}
|
|
|
|
public function testManualWeighingRejectsWeightOverFiveDigits(): void
|
|
{
|
|
$client = $this->manageClientWithCurrentSite();
|
|
|
|
$response = $client->request('POST', '/api/weighbridge_readings', [
|
|
'headers' => ['Content-Type' => 'application/ld+json'],
|
|
// 100000 = 6 chiffres → au-dela du plafond 5 chiffres (99999).
|
|
'json' => ['mode' => 'MANUAL', 'weight' => 100000, 'dsd' => 16619],
|
|
]);
|
|
|
|
self::assertResponseStatusCodeSame(422);
|
|
self::assertViolationOnPath($response, 'weight');
|
|
}
|
|
|
|
public function testManualWeighingRejectsDsdOverFiveDigits(): void
|
|
{
|
|
$client = $this->manageClientWithCurrentSite();
|
|
|
|
$response = $client->request('POST', '/api/weighbridge_readings', [
|
|
'headers' => ['Content-Type' => 'application/ld+json'],
|
|
'json' => ['mode' => 'MANUAL', 'weight' => 23187, 'dsd' => 100000],
|
|
]);
|
|
|
|
self::assertResponseStatusCodeSame(422);
|
|
self::assertViolationOnPath($response, 'dsd');
|
|
}
|
|
|
|
public function testManualWeighingAcceptsFiveDigitBoundary(): void
|
|
{
|
|
$client = $this->manageClientWithCurrentSite();
|
|
|
|
$response = $client->request('POST', '/api/weighbridge_readings', [
|
|
'headers' => ['Content-Type' => 'application/ld+json'],
|
|
// 99999 = exactement 5 chiffres → derniere valeur acceptee.
|
|
'json' => ['mode' => 'MANUAL', 'weight' => 99999, 'dsd' => 99999],
|
|
]);
|
|
|
|
self::assertResponseStatusCodeSame(200);
|
|
$data = $response->toArray();
|
|
self::assertSame(99999, $data['weight']);
|
|
self::assertSame(99999, $data['dsd']);
|
|
}
|
|
|
|
/**
|
|
* Garde-fou ERP-101 (miroir AbstractWeighingTicketApiTestCase) : une 422 doit
|
|
* porter une violation sur le `propertyPath` attendu, consommable inline par
|
|
* useFormErrors cote front, pas seulement le bon statut HTTP.
|
|
*/
|
|
private static function assertViolationOnPath(object $response, string $path): void
|
|
{
|
|
$paths = array_column($response->toArray(false)['violations'] ?? [], 'propertyPath');
|
|
|
|
self::assertContains(
|
|
$path,
|
|
$paths,
|
|
sprintf('Aucune violation sur "%s" (paths: %s).', $path, implode(', ', $paths)),
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Cree un user non-admin portant `logistique.weighing_tickets.manage`, lui
|
|
* positionne un site courant (l'endpoint est cloisonne par site, § 2.3) et
|
|
* renvoie un client authentifie.
|
|
*/
|
|
private function manageClientWithCurrentSite(): Client
|
|
{
|
|
$credentials = $this->createUserWithPermission('logistique.weighing_tickets.manage');
|
|
|
|
$em = $this->getEm();
|
|
$user = $em->getRepository(User::class)->findOneBy(['username' => $credentials['username']]);
|
|
self::assertInstanceOf(User::class, $user);
|
|
|
|
$site = $em->getRepository(Site::class)->findAll()[0];
|
|
$user->setCurrentSite($site);
|
|
$em->flush();
|
|
|
|
return $this->authenticatedClient($credentials['username'], $credentials['password']);
|
|
}
|
|
}
|