test(commercial) : fix CI anti-N+1 (profiling test) + durcissement 422/gating M2 fournisseurs (ERP-92)
Pull Request — Quality gate / Backend (PHP CS + PHPUnit) (pull_request) Successful in 2m1s
Pull Request — Quality gate / Frontend (lint + Vitest + build) (pull_request) Successful in 1m6s

- config/packages/test/doctrine.yaml : force dbal profiling en test pour que
  doctrine.debug_data_holder existe sous APP_DEBUG=0 (CI). Le test anti-N+1
  SupplierListTest passait en local (debug=1) mais cassait en CI.
- RBACMatrix/SupplierApi : les 422 RG-2.03 et RG-2.14 assertent desormais le
  propertyPath / message (plus seulement le code) — un 422 orthogonal ne peut
  plus faire passer le test.
- RBACMatrix : gating bureau/commerciale verifie l'ensemble des champs
  comptables (accountNumber/nTva/tvaMode/paymentType), plus seulement siren/ribs.
- violationsByPath() mutualise dans AbstractSupplierApiTestCase (dedup).
This commit is contained in:
Matthieu
2026-06-07 11:18:37 +02:00
parent 42cc9be4ae
commit 85963ec3ff
6 changed files with 57 additions and 33 deletions
+12
View File
@@ -0,0 +1,12 @@
doctrine:
dbal:
connections:
# Force le profiling DBAL en environnement de test independamment de
# APP_DEBUG. Sans cela, la CI tourne en APP_DEBUG=0 (prod-like) et le
# service `doctrine.debug_data_holder` n'est pas enregistre : le test
# anti-N+1 (SupplierListTest::testListQueryCountDoesNotGrowWithRowCount)
# qui compte les requetes via ce holder echoue alors en CI alors qu'il
# passe en local (APP_DEBUG=1). Activer le profiling ici garde le test
# actif precisement la ou il compte (CI), sans impacter la prod.
default:
profiling: true
@@ -316,4 +316,24 @@ abstract class AbstractSupplierApiTestCase extends AbstractCommercialApiTestCase
return $entity;
}
/**
* Indexe les violations d'un corps de reponse 422 par propertyPath. Permet
* d'asserter qu'un 422 porte bien sur le champ attendu (et n'est pas un 422
* orthogonal) : un test qui se contente du code 422 passerait meme si la RG
* visee etait cassee pour une autre raison.
*
* @param array<string, mixed> $body corps decode de la reponse (toArray(false))
*
* @return array<string, string> propertyPath => message
*/
protected function violationsByPath(array $body): array
{
$byPath = [];
foreach ($body['violations'] ?? [] as $v) {
$byPath[$v['propertyPath']] = $v['message'];
}
return $byPath;
}
}
@@ -77,18 +77,5 @@ final class SupplierAccountingApiTest extends AbstractSupplierApiTestCase
self::assertResponseStatusCodeSame(200);
}
/**
* @param array<string, mixed> $body
*
* @return array<string, string>
*/
private function violationsByPath(array $body): array
{
$byPath = [];
foreach ($body['violations'] ?? [] as $v) {
$byPath[$v['propertyPath']] = $v['message'];
}
return $byPath;
}
// violationsByPath() : helper mutualise dans AbstractSupplierApiTestCase.
}
@@ -147,12 +147,15 @@ final class SupplierApiTest extends AbstractSupplierApiTestCase
$seed = $this->seedSupplier('Archive Plus Field');
// RG-2.14 : une requete d'archivage ne modifie aucun autre champ.
$client->request('PATCH', '/api/suppliers/'.$seed->getId(), [
$response = $client->request('PATCH', '/api/suppliers/'.$seed->getId(), [
'headers' => ['Content-Type' => self::MERGE],
'json' => ['isArchived' => true, 'companyName' => 'Renamed While Archiving'],
]);
self::assertResponseStatusCodeSame(422);
// Le 422 doit etre celui de RG-2.14 (archivage exclusif) et non un 422
// orthogonal : on verifie le message porte par l'exception.
self::assertStringContainsString('archivage', $response->getContent(false));
}
public function testRestoreSetsArchivedAtNull(): void
@@ -131,7 +131,14 @@ final class SupplierRBACMatrixTest extends AbstractSupplierApiTestCase
$data = $client->request('GET', '/api/suppliers/'.$supplier->getId(), ['headers' => ['Accept' => self::LD]])->toArray();
// Gating par omission sur l'ensemble des champs comptables (pas seulement
// siren/ribs) : une regression reintroduisant accountNumber/nTva/tvaMode/
// paymentType dans le groupe bureau serait sinon invisible.
self::assertArrayNotHasKey('siren', $data);
self::assertArrayNotHasKey('accountNumber', $data);
self::assertArrayNotHasKey('nTva', $data);
self::assertArrayNotHasKey('tvaMode', $data);
self::assertArrayNotHasKey('paymentType', $data);
self::assertArrayNotHasKey('ribs', $data);
}
@@ -205,11 +212,14 @@ final class SupplierRBACMatrixTest extends AbstractSupplierApiTestCase
// manage : la creation passe la security d'operation (pas un 403 comme
// Compta) mais bute sur RG-2.03 (onglet Information incomplet) -> 422.
$client->request('POST', '/api/suppliers', [
$response = $client->request('POST', '/api/suppliers', [
'headers' => ['Content-Type' => self::LD],
'json' => $this->validMainPayload('Commerciale Post'),
]);
self::assertResponseStatusCodeSame(422);
// Le 422 doit bien etre celui de RG-2.03 (onglet Information) et non un
// 422 orthogonal : on exige une violation sur un champ de completude.
self::assertArrayHasKey('description', $this->violationsByPath($response->toArray(false)));
// PAS accounting : edition onglet Comptabilite refusee
$client->request('PATCH', '/api/suppliers/'.$seed->getId(), [
@@ -234,8 +244,11 @@ final class SupplierRBACMatrixTest extends AbstractSupplierApiTestCase
$data = $client->request('GET', '/api/suppliers/'.$supplier->getId(), ['headers' => ['Accept' => self::LD]])->toArray();
self::assertArrayNotHasKey('siren', $data);
self::assertArrayNotHasKey('ribs', $data);
self::assertArrayNotHasKey('accountNumber', $data);
self::assertArrayNotHasKey('nTva', $data);
self::assertArrayNotHasKey('tvaMode', $data);
self::assertArrayNotHasKey('paymentType', $data);
self::assertArrayNotHasKey('ribs', $data);
}
public function testRG203CommercialePostIncompleteIs422AdminIs201(): void
@@ -244,11 +257,12 @@ final class SupplierRBACMatrixTest extends AbstractSupplierApiTestCase
// RG-2.03 : Commerciale POST sans onglet Information complet -> 422.
$commerciale = $this->authAs('commerciale');
$commerciale->request('POST', '/api/suppliers', [
$response = $commerciale->request('POST', '/api/suppliers', [
'headers' => ['Content-Type' => self::LD],
'json' => $this->validMainPayload('RG203 Commerciale', $cat->getId()),
]);
self::assertResponseStatusCodeSame(422);
self::assertArrayHasKey('description', $this->violationsByPath($response->toArray(false)));
// Meme payload par un Admin (non gate par RG-2.03) -> 201.
$admin = $this->createAdminClient();
@@ -266,11 +280,12 @@ final class SupplierRBACMatrixTest extends AbstractSupplierApiTestCase
$seed = $this->seedSupplier('Commerciale Patch Incomplete');
$commerciale = $this->authAs('commerciale');
$commerciale->request('PATCH', '/api/suppliers/'.$seed->getId(), [
$response = $commerciale->request('PATCH', '/api/suppliers/'.$seed->getId(), [
'headers' => ['Content-Type' => self::MERGE],
'json' => ['companyName' => 'Commerciale Renamed'],
]);
self::assertResponseStatusCodeSame(422);
self::assertArrayHasKey('description', $this->violationsByPath($response->toArray(false)));
// Le meme PATCH par un Admin passe (non gate par RG-2.03) -> 200.
$admin = $this->createAdminClient();
@@ -345,20 +345,7 @@ final class SupplierSubResourceApiTest extends AbstractSupplierApiTestCase
// === Helpers ===
/**
* @param array<string, mixed> $body
*
* @return array<string, string> propertyPath => message
*/
private function violationsByPath(array $body): array
{
$byPath = [];
foreach ($body['violations'] ?? [] as $v) {
$byPath[$v['propertyPath']] = $v['message'];
}
return $byPath;
}
// violationsByPath() : helper mutualise dans AbstractSupplierApiTestCase.
private function firstSiteIri(): string
{