Files
Starseed/tests/Module/Transport/Api/CarrierContactApiTest.php
T
Matthieu c1fcd9a7c8
Pull Request — Quality gate / Backend (PHP CS + PHPUnit) (pull_request) Failing after 54s
Pull Request — Quality gate / Frontend (lint + Vitest + build) (pull_request) Successful in 1m33s
test(transport) : rigueur RG sous-ressources (propertyPath, 404 parent, 401, certif)
Repond aux retours de review (rigueur d'assertion transversale) :
- mutualise assertViolationOnPath dans AbstractCarrierApiTestCase (au lieu d'un
  duplicata local a CarrierWriteApiTest) ;
- asserte le propertyPath des 422 des sous-ressources (adresses city/street/postalCode,
  contacts firstName/phones/email, prix clientDeliveryAddress/supplierSupplyAddress/price)
  -> evite les faux-verts du mapping inline (ERP-101) ;
- 404 parent (POST sur /carriers/999999/{addresses,contacts,prices}) ;
- 401 anonyme + filtre ?certificationType= sur la collection (trous releves sur le
  contrat de lecture).
2026-06-16 15:13:11 +02:00

210 lines
7.8 KiB
PHP

<?php
declare(strict_types=1);
namespace App\Tests\Module\Transport\Api;
use App\Module\Core\Infrastructure\DataFixtures\RbacDemoFixtures;
use App\Module\Transport\Domain\Entity\CarrierContact;
use Symfony\Bundle\FrameworkBundle\Console\Application;
use Symfony\Component\Console\Input\ArrayInput;
use Symfony\Component\Console\Output\NullOutput;
/**
* Sous-ressource Contact d'un transporteur (spec-back M4 § 4.5, ERP-160).
* POST /api/carriers/{id}/contacts, PATCH/DELETE /api/carrier_contacts/{id}.
*
* Contrat verifie :
* - RG-4.08 : contact totalement vide -> 422 (au moins 1 champ requis) ;
* - RG-4.08 : 1 seul champ rempli -> 201 ;
* - RG-4.08 : 3 telephones (tableau `phones`) -> 422 (max 2) ;
* - mapping `phones[]` -> phonePrimary / phoneSecondary + normalisation (RG-4.13) ;
* - PATCH / DELETE OK avec transport.carriers.manage, 403 sans (view seul).
*
* @internal
*/
final class CarrierContactApiTest extends AbstractCarrierApiTestCase
{
private const string PWD = RbacDemoFixtures::DEMO_PASSWORD;
protected function setUp(): void
{
parent::setUp();
// Seed idempotent des roles + matrice § 5.2 + comptes demo (meme chemin
// qu'en recette), requis pour les tests de permission (bureau/commerciale).
self::bootKernel();
$application = new Application(self::$kernel);
$application->setAutoExit(false);
$exit = $application->run(
new ArrayInput([
'command' => 'app:seed-rbac',
'--with-demo-users' => true,
'--password' => self::PWD,
]),
new NullOutput(),
);
self::assertSame(0, $exit, 'app:seed-rbac a echoue (permissions transport.carriers.* synchronisees ?).');
self::ensureKernelShutdown();
}
public function testEmptyContactReturns422(): void
{
// RG-4.08 : aucun champ rempli -> 422 (garde Processor, double du CHECK BDD).
$carrier = $this->seedCarrier('Contact Vide');
$client = $this->createAdminClient();
$response = $client->request('POST', '/api/carriers/'.$carrier->getId().'/contacts', [
'headers' => ['Content-Type' => self::LD],
'json' => [],
]);
self::assertResponseStatusCodeSame(422);
// RG-4.08 : la violation est rattachee a `firstName` (mapping inline ERP-101).
self::assertViolationOnPath($response, 'firstName');
}
public function testSingleFieldContactIsCreated(): void
{
// RG-4.08 : un seul champ suffit a valider le bloc.
$carrier = $this->seedCarrier('Contact Mono');
$client = $this->createAdminClient();
$client->request('POST', '/api/carriers/'.$carrier->getId().'/contacts', [
'headers' => ['Content-Type' => self::LD],
'json' => ['lastName' => 'martin'],
]);
self::assertResponseStatusCodeSame(201);
// RG-4.13 : nom capitalise serveur.
self::assertJsonContains(['lastName' => 'Martin']);
}
public function testThirdPhoneReturns422(): void
{
// RG-4.08 : max 2 telephones. Le contrat d'ecriture accepte un tableau
// `phones` (liste dynamique cote front « x1, +1 possible, max 2 ») ; un 3e
// numero -> 422 rattachee au champ `phones`.
$carrier = $this->seedCarrier('Contact Trois Tel');
$client = $this->createAdminClient();
$response = $client->request('POST', '/api/carriers/'.$carrier->getId().'/contacts', [
'headers' => ['Content-Type' => self::LD],
'json' => [
'firstName' => 'Jean',
'phones' => ['0611111111', '0622222222', '0633333333'],
],
]);
self::assertResponseStatusCodeSame(422);
// Le max-2 cible le champ virtuel `phones` (mapping inline ERP-101).
self::assertViolationOnPath($response, 'phones');
}
public function testInvalidEmailReturns422(): void
{
// L'email du contact porte un Assert\Email (nouvelle contrainte M4) : une
// adresse mal formee -> 422 ciblee sur `email`.
$carrier = $this->seedCarrier('Contact Email Invalide');
$client = $this->createAdminClient();
$response = $client->request('POST', '/api/carriers/'.$carrier->getId().'/contacts', [
'headers' => ['Content-Type' => self::LD],
'json' => ['lastName' => 'Durand', 'email' => 'pas-un-email'],
]);
self::assertResponseStatusCodeSame(422);
self::assertViolationOnPath($response, 'email');
}
public function testPostContactOnUnknownCarrierReturns404(): void
{
// Parent introuvable (read:false) -> 404 explicite du processor.
$client = $this->createAdminClient();
$client->request('POST', '/api/carriers/999999/contacts', [
'headers' => ['Content-Type' => self::LD],
'json' => ['lastName' => 'Martin'],
]);
self::assertResponseStatusCodeSame(404);
}
public function testPhonesAreMappedAndNormalized(): void
{
// Mapping `phones[0]` -> phonePrimary, `phones[1]` -> phoneSecondary +
// normalisation RG-4.13 (chiffres uniquement).
$carrier = $this->seedCarrier('Contact Deux Tel');
$client = $this->createAdminClient();
$client->request('POST', '/api/carriers/'.$carrier->getId().'/contacts', [
'headers' => ['Content-Type' => self::LD],
'json' => [
'lastName' => 'Dupont',
'phones' => ['06.11.11.11.11', '06 22 22 22 22'],
],
]);
self::assertResponseStatusCodeSame(201);
self::assertJsonContains([
'phonePrimary' => '0611111111',
'phoneSecondary' => '0622222222',
]);
}
public function testPatchAndDeleteSucceedWithManage(): void
{
$contact = $this->seedContact('Patch Delete');
$client = $this->authenticatedClient('bureau', self::PWD); // manage (matrice § 5.2)
// PATCH (manage) -> 200
$client->request('PATCH', '/api/carrier_contacts/'.$contact->getId(), [
'headers' => ['Content-Type' => self::MERGE],
'json' => ['jobTitle' => 'Directeur'],
]);
self::assertResponseStatusCodeSame(200);
// DELETE (manage) -> 204
$client->request('DELETE', '/api/carrier_contacts/'.$contact->getId());
self::assertResponseStatusCodeSame(204);
}
public function testWriteForbiddenWithoutManage(): void
{
$contact = $this->seedContact('Forbidden');
$carrier = $contact->getCarrier();
self::assertNotNull($carrier);
$client = $this->authenticatedClient('commerciale', self::PWD); // view seul
$client->request('POST', '/api/carriers/'.$carrier->getId().'/contacts', [
'headers' => ['Content-Type' => self::LD],
'json' => ['lastName' => 'Bernard'],
]);
self::assertResponseStatusCodeSame(403);
$client->request('PATCH', '/api/carrier_contacts/'.$contact->getId(), [
'headers' => ['Content-Type' => self::MERGE],
'json' => ['jobTitle' => 'Chef'],
]);
self::assertResponseStatusCodeSame(403);
$client->request('DELETE', '/api/carrier_contacts/'.$contact->getId());
self::assertResponseStatusCodeSame(403);
}
/**
* Seede un transporteur + un contact rattache (pour les tests PATCH/DELETE).
*/
private function seedContact(string $name): CarrierContact
{
$em = $this->getEm();
$carrier = $this->seedCarrier($name);
$contact = new CarrierContact();
$contact->setCarrier($carrier);
$contact->setLastName('Martin');
$contact->setPhonePrimary('0612345678');
$carrier->addContact($contact);
$em->persist($contact);
$em->flush();
return $contact;
}
}