createAdminClient(); $cat = $this->createCategory('SECTEUR'); $response = $client->request('POST', '/api/clients', [ 'headers' => ['Content-Type' => self::LD], 'json' => [ 'companyName' => 'acme sas', 'firstName' => 'JEAN', 'lastName' => 'dupont', 'phonePrimary' => '06.12.34.56.78', 'email' => 'Jean.DUPONT@ACME.FR', 'categories' => ['/api/categories/'.$cat->getId()], ], ]); self::assertResponseStatusCodeSame(201); $data = $response->toArray(); // RG-1.18 / 1.19 / 1.20 / 1.21 self::assertSame('ACME SAS', $data['companyName']); self::assertSame('Jean', $data['firstName']); self::assertSame('Dupont', $data['lastName']); self::assertSame('0612345678', $data['phonePrimary']); self::assertSame('jean.dupont@acme.fr', $data['email']); self::assertFalse($data['isArchived']); } public function testPostDuplicateCompanyNameReturns409(): void { $client = $this->createAdminClient(); $cat = $this->createCategory('SECTEUR'); $iri = '/api/categories/'.$cat->getId(); $payload = [ 'companyName' => 'Doublon SARL', 'firstName' => 'A', 'phonePrimary' => '0102030405', 'email' => 'dup@test.fr', 'categories' => [$iri], ]; $client->request('POST', '/api/clients', ['headers' => ['Content-Type' => self::LD], 'json' => $payload]); self::assertResponseStatusCodeSame(201); // Meme nom (insensible a la casse via l'index LOWER) -> 409 (RG-1.16). $payload['email'] = 'dup2@test.fr'; $client->request('POST', '/api/clients', ['headers' => ['Content-Type' => self::LD], 'json' => $payload]); self::assertResponseStatusCodeSame(409); } public function testPostWithoutFirstOrLastNameReturns422(): void { $client = $this->createAdminClient(); $cat = $this->createCategory('SECTEUR'); $client->request('POST', '/api/clients', [ 'headers' => ['Content-Type' => self::LD], 'json' => [ 'companyName' => 'No Contact Name', 'phonePrimary' => '0102030405', 'email' => 'nc@test.fr', 'categories' => ['/api/categories/'.$cat->getId()], ], ]); // RG-1.01 self::assertResponseStatusCodeSame(422); } public function testPostWithoutCategoryReturns422(): void { $client = $this->createAdminClient(); $client->request('POST', '/api/clients', [ 'headers' => ['Content-Type' => self::LD], 'json' => [ 'companyName' => 'No Category', 'firstName' => 'A', 'phonePrimary' => '0102030405', 'email' => 'nocat@test.fr', 'categories' => [], ], ]); // Assert\Count(min: 1) self::assertResponseStatusCodeSame(422); } public function testPostWithDistributorAndBrokerReturns422(): void { $client = $this->createAdminClient(); $cat = $this->createCategory('SECTEUR'); $distributor = $this->seedClient('Distrib Mutex', false, 'DISTRIBUTEUR'); $client->request('POST', '/api/clients', [ 'headers' => ['Content-Type' => self::LD], 'json' => [ 'companyName' => 'Mutex Client', 'firstName' => 'A', 'phonePrimary' => '0102030405', 'email' => 'mutex@test.fr', 'categories' => ['/api/categories/'.$cat->getId()], 'distributor' => '/api/clients/'.$distributor->getId(), 'broker' => '/api/clients/'.$distributor->getId(), ], ]); // RG-1.03 (exclusivite) self::assertResponseStatusCodeSame(422); } public function testPostDistributorReferencingNonDistributorReturns422(): void { $client = $this->createAdminClient(); $cat = $this->createCategory('SECTEUR'); $notDistro = $this->seedClient('Pas Un Distrib', false, 'SECTEUR'); $client->request('POST', '/api/clients', [ 'headers' => ['Content-Type' => self::LD], 'json' => [ 'companyName' => 'Bad Distrib Ref', 'firstName' => 'A', 'phonePrimary' => '0102030405', 'email' => 'baddistrib@test.fr', 'categories' => ['/api/categories/'.$cat->getId()], 'distributor' => '/api/clients/'.$notDistro->getId(), ], ]); // RG-1.03 (le distributor doit etre categorise DISTRIBUTEUR) self::assertResponseStatusCodeSame(422); } public function testPostValidDistributorReturns201(): void { $client = $this->createAdminClient(); $cat = $this->createCategory('SECTEUR'); $distributor = $this->seedClient('Vrai Distrib', false, 'DISTRIBUTEUR'); $client->request('POST', '/api/clients', [ 'headers' => ['Content-Type' => self::LD], 'json' => [ 'companyName' => 'Client Avec Distrib', 'firstName' => 'A', 'phonePrimary' => '0102030405', 'email' => 'okdistrib@test.fr', 'categories' => ['/api/categories/'.$cat->getId()], 'distributor' => '/api/clients/'.$distributor->getId(), ], ]); self::assertResponseStatusCodeSame(201); } public function testListSortedByCompanyNameAscAndExcludesArchived(): void { $client = $this->createAdminClient(); $this->seedClient('Zebra Co'); $this->seedClient('Alpha Co'); $this->seedClient('Archivé Co', true); $names = $client->request('GET', '/api/clients?pagination=false', [ 'headers' => ['Accept' => self::LD], ])->toArray()['member']; $companyNames = array_map(static fn (array $c): string => $c['companyName'], $names); // RG-1.24 : l'archive est exclue par defaut. self::assertNotContains('ARCHIVÉ CO', $companyNames); // RG-1.26 : tri companyName ASC (Alpha avant Zebra). $alpha = array_search('ALPHA CO', $companyNames, true); $zebra = array_search('ZEBRA CO', $companyNames, true); self::assertNotFalse($alpha); self::assertNotFalse($zebra); self::assertLessThan($zebra, $alpha); } public function testListIncludeArchivedReturnsArchived(): void { $client = $this->createAdminClient(); $this->seedClient('Hidden Archived', true); $members = $client->request('GET', '/api/clients?includeArchived=true&pagination=false', [ 'headers' => ['Accept' => self::LD], ])->toArray()['member']; $names = array_map(static fn (array $c): string => $c['companyName'], $members); // RG-1.25 self::assertContains('HIDDEN ARCHIVED', $names); } public function testCollectionIsPaginated(): void { $client = $this->createAdminClient(); $this->seedClient('Paginated One'); // Collection Hydra avec total (la cle `view` n'apparait qu'a partir de // 2 pages cote API Platform 4, donc non assertable sur page unique). $page1 = $client->request('GET', '/api/clients', ['headers' => ['Accept' => self::LD]])->toArray(); self::assertArrayHasKey('totalItems', $page1); self::assertNotEmpty($page1['member']); // Preuve que la pagination serveur est bien engagee : la page 2 d'un jeu // tenant sur une page est vide (un provider non pagine ignorerait `page` // et renverrait quand meme les items). $page2 = $client->request('GET', '/api/clients?page=2', ['headers' => ['Accept' => self::LD]])->toArray(); self::assertSame([], $page2['member']); } public function testPatchArchiveSetsArchivedAtThenRestore(): void { $client = $this->createAdminClient(); $seed = $this->seedClient('To Archive'); $iri = '/api/clients/'.$seed->getId(); // Archive (RG-1.22) : admin a la permission archive via bypass isAdmin. $archived = $client->request('PATCH', $iri, [ 'headers' => ['Content-Type' => 'application/merge-patch+json'], 'json' => ['isArchived' => true], ])->toArray(); self::assertResponseStatusCodeSame(200); self::assertTrue($archived['isArchived']); self::assertNotNull($archived['archivedAt']); // Restauration (RG-1.23) : archivedAt repasse a null. $restored = $client->request('PATCH', $iri, [ 'headers' => ['Content-Type' => 'application/merge-patch+json'], 'json' => ['isArchived' => false], ])->toArray(); self::assertResponseStatusCodeSame(200); self::assertFalse($restored['isArchived']); self::assertNull($restored['archivedAt']); } public function testPatchArchiveWithOtherFieldReturns422(): void { $client = $this->createAdminClient(); $seed = $this->seedClient('Archive Plus Field'); $client->request('PATCH', '/api/clients/'.$seed->getId(), [ 'headers' => ['Content-Type' => 'application/merge-patch+json'], 'json' => ['isArchived' => true, 'companyName' => 'Renamed'], ]); // RG-1.22 : une requete d'archivage ne modifie aucun autre champ. self::assertResponseStatusCodeSame(422); } public function testGetDetailEmbedsSubCollections(): void { $client = $this->createAdminClient(); $seed = $this->seedClient('Detail Embed'); $data = $client->request('GET', '/api/clients/'.$seed->getId(), [ 'headers' => ['Accept' => self::LD], ])->toArray(); // § 4.2 : le detail embarque contacts / adresses / ribs. self::assertArrayHasKey('contacts', $data); self::assertArrayHasKey('addresses', $data); self::assertArrayHasKey('ribs', $data); } }