From efc6ec5691723d056bd895d328dcf16c4017b7ac Mon Sep 17 00:00:00 2001 From: r-dev Date: Sun, 8 Mar 2026 13:42:56 +0100 Subject: [PATCH] test(api) : add comprehensive API test suite (161 tests) - Add AbstractApiTestCase with auth helpers and entity factories - Add tests for all entities: Machine, Piece, Composant, Product, Site, ModelType, Constructeur, CustomField, CustomFieldValue, Document, MachineComponentLink, MachinePieceLink, MachineProductLink, Profile - Add controller tests: CommentController, EntityHistory - Add HealthCheck, Filter, Pagination, Validation, Session tests - Test auth (401), authorization (403), CRUD, and edge cases Co-Authored-By: Claude Opus 4.6 --- tests/AbstractApiTestCase.php | 327 ++++++++++++++++++ .../Api/Controller/CommentControllerTest.php | 150 ++++++++ tests/Api/Controller/EntityHistoryTest.php | 94 +++++ tests/Api/Entity/ComposantTest.php | 138 ++++++++ tests/Api/Entity/ConstructeurTest.php | 129 +++++++ tests/Api/Entity/CustomFieldTest.php | 108 ++++++ tests/Api/Entity/CustomFieldValueTest.php | 112 ++++++ tests/Api/Entity/DocumentTest.php | 146 ++++++++ tests/Api/Entity/MachineComponentLinkTest.php | 134 +++++++ tests/Api/Entity/MachinePieceLinkTest.php | 124 +++++++ tests/Api/Entity/MachineProductLinkTest.php | 121 +++++++ tests/Api/Entity/MachineTest.php | 135 ++++++++ tests/Api/Entity/ModelTypeTest.php | 155 +++++++++ tests/Api/Entity/PieceTest.php | 145 ++++++++ tests/Api/Entity/ProductTest.php | 142 ++++++++ tests/Api/Entity/ProfileTest.php | 125 +++++++ tests/Api/Entity/SiteTest.php | 124 +++++++ tests/Api/FilterTest.php | 112 ++++++ tests/Api/HealthCheckTest.php | 43 +++ tests/Api/PaginationTest.php | 71 ++++ tests/Api/Session/SessionProfileTest.php | 138 ++++++++ tests/Api/ValidationTest.php | 70 ++++ 22 files changed, 2843 insertions(+) create mode 100644 tests/AbstractApiTestCase.php create mode 100644 tests/Api/Controller/CommentControllerTest.php create mode 100644 tests/Api/Controller/EntityHistoryTest.php create mode 100644 tests/Api/Entity/ComposantTest.php create mode 100644 tests/Api/Entity/ConstructeurTest.php create mode 100644 tests/Api/Entity/CustomFieldTest.php create mode 100644 tests/Api/Entity/CustomFieldValueTest.php create mode 100644 tests/Api/Entity/DocumentTest.php create mode 100644 tests/Api/Entity/MachineComponentLinkTest.php create mode 100644 tests/Api/Entity/MachinePieceLinkTest.php create mode 100644 tests/Api/Entity/MachineProductLinkTest.php create mode 100644 tests/Api/Entity/MachineTest.php create mode 100644 tests/Api/Entity/ModelTypeTest.php create mode 100644 tests/Api/Entity/PieceTest.php create mode 100644 tests/Api/Entity/ProductTest.php create mode 100644 tests/Api/Entity/ProfileTest.php create mode 100644 tests/Api/Entity/SiteTest.php create mode 100644 tests/Api/FilterTest.php create mode 100644 tests/Api/HealthCheckTest.php create mode 100644 tests/Api/PaginationTest.php create mode 100644 tests/Api/Session/SessionProfileTest.php create mode 100644 tests/Api/ValidationTest.php diff --git a/tests/AbstractApiTestCase.php b/tests/AbstractApiTestCase.php new file mode 100644 index 0000000..8b6a44f --- /dev/null +++ b/tests/AbstractApiTestCase.php @@ -0,0 +1,327 @@ +getEntityManager()->clear(); + + parent::tearDown(); + } + + protected function getEntityManager(): EntityManagerInterface + { + return static::getContainer()->get('doctrine')->getManager(); + } + + protected function getPasswordHasher(): UserPasswordHasherInterface + { + return static::getContainer()->get(UserPasswordHasherInterface::class); + } + + // ── Auth helpers ──────────────────────────────────────────────── + + protected function createAuthenticatedClient(string $role): Client + { + $profile = $this->createProfile(roles: [$role], password: self::DEFAULT_PASSWORD); + + $client = static::createClient(); + $client->request('POST', '/api/session/profile', [ + 'json' => [ + 'profileId' => $profile->getId(), + 'password' => self::DEFAULT_PASSWORD, + ], + ]); + + return $client; + } + + protected function createViewerClient(): Client + { + return $this->createAuthenticatedClient('ROLE_VIEWER'); + } + + protected function createGestionnaireClient(): Client + { + return $this->createAuthenticatedClient('ROLE_GESTIONNAIRE'); + } + + protected function createAdminClient(): Client + { + return $this->createAuthenticatedClient('ROLE_ADMIN'); + } + + protected function createUnauthenticatedClient(): Client + { + return static::createClient(); + } + + // ── Factory helpers ───────────────────────────────────────────── + + protected function createProfile( + string $firstName = 'Test', + string $lastName = 'User', + ?string $email = null, + array $roles = ['ROLE_USER'], + ?string $password = null, + bool $isActive = true, + ): Profile { + $profile = new Profile(); + $profile->setFirstName($firstName); + $profile->setLastName($lastName); + $profile->setEmail($email ?? uniqid('test-', true).'@test.local'); + $profile->setRoles($roles); + $profile->setIsActive($isActive); + + if (null !== $password) { + $hashed = $this->getPasswordHasher()->hashPassword($profile, $password); + $profile->setPassword($hashed); + } + + $em = $this->getEntityManager(); + $em->persist($profile); + $em->flush(); + + return $profile; + } + + protected function createSite(string $name = 'Site Test', array $extra = []): Site + { + $site = new Site(); + $site->setName($name); + + if (isset($extra['contactName'])) { + $site->setContactName($extra['contactName']); + } + if (isset($extra['contactCity'])) { + $site->setContactCity($extra['contactCity']); + } + + $em = $this->getEntityManager(); + $em->persist($site); + $em->flush(); + + return $site; + } + + protected function createConstructeur(string $name = 'Constructeur Test', ?string $email = null, ?string $phone = null): Constructeur + { + $c = new Constructeur(); + $c->setName($name); + $c->setEmail($email); + $c->setPhone($phone); + + $em = $this->getEntityManager(); + $em->persist($c); + $em->flush(); + + return $c; + } + + protected function createModelType( + string $name = 'ModelType Test', + string $code = 'MT-001', + ModelCategory $category = ModelCategory::COMPONENT, + ): ModelType { + $mt = new ModelType(); + $mt->setName($name); + $mt->setCode($code); + $mt->setCategory($category); + + $em = $this->getEntityManager(); + $em->persist($mt); + $em->flush(); + + return $mt; + } + + protected function createMachine(string $name = 'Machine Test', ?Site $site = null, ?string $reference = null): Machine + { + $site ??= $this->createSite(); + + $machine = new Machine(); + $machine->setName($name); + $machine->setSite($site); + if (null !== $reference) { + $machine->setReference($reference); + } + + $em = $this->getEntityManager(); + $em->persist($machine); + $em->flush(); + + return $machine; + } + + protected function createComposant(string $name = 'Composant Test', ?ModelType $type = null): Composant + { + $c = new Composant(); + $c->setName($name); + if (null !== $type) { + $c->setTypeComposant($type); + } + + $em = $this->getEntityManager(); + $em->persist($c); + $em->flush(); + + return $c; + } + + protected function createPiece(string $name = 'Piece Test', ?string $reference = null, ?ModelType $type = null): Piece + { + $p = new Piece(); + $p->setName($name); + if (null !== $reference) { + $p->setReference($reference); + } + if (null !== $type) { + $p->setTypePiece($type); + } + + $em = $this->getEntityManager(); + $em->persist($p); + $em->flush(); + + return $p; + } + + protected function createProduct(string $name = 'Product Test', ?string $reference = null, ?ModelType $type = null): Product + { + $p = new Product(); + $p->setName($name); + if (null !== $reference) { + $p->setReference($reference); + } + if (null !== $type) { + $p->setTypeProduct($type); + } + + $em = $this->getEntityManager(); + $em->persist($p); + $em->flush(); + + return $p; + } + + protected function createCustomField( + string $name = 'Custom Field', + string $type = 'text', + ?Machine $machine = null, + ): CustomField { + $cf = new CustomField(); + $cf->setName($name); + $cf->setType($type); + if (null !== $machine) { + $cf->setMachine($machine); + } + + $em = $this->getEntityManager(); + $em->persist($cf); + $em->flush(); + + return $cf; + } + + protected function createCustomFieldValue( + CustomField $customField, + string $value = 'test value', + ?Machine $machine = null, + ?Composant $composant = null, + ): CustomFieldValue { + $cfv = new CustomFieldValue(); + $cfv->setValue($value); + $cfv->setCustomField($customField); + if (null !== $machine) { + $cfv->setMachine($machine); + } + if (null !== $composant) { + $cfv->setComposant($composant); + } + + $em = $this->getEntityManager(); + $em->persist($cfv); + $em->flush(); + + return $cfv; + } + + protected function createMachineComponentLink(Machine $machine, Composant $composant): MachineComponentLink + { + $link = new MachineComponentLink(); + $link->setMachine($machine); + $link->setComposant($composant); + + $em = $this->getEntityManager(); + $em->persist($link); + $em->flush(); + + return $link; + } + + protected function createMachinePieceLink(Machine $machine, Piece $piece, ?MachineComponentLink $parentLink = null): MachinePieceLink + { + $link = new MachinePieceLink(); + $link->setMachine($machine); + $link->setPiece($piece); + if (null !== $parentLink) { + $link->setParentLink($parentLink); + } + + $em = $this->getEntityManager(); + $em->persist($link); + $em->flush(); + + return $link; + } + + protected function createMachineProductLink(Machine $machine, Product $product): MachineProductLink + { + $link = new MachineProductLink(); + $link->setMachine($machine); + $link->setProduct($product); + + $em = $this->getEntityManager(); + $em->persist($link); + $em->flush(); + + return $link; + } + + // ── Assertion helpers ─────────────────────────────────────────── + + protected function assertJsonContainsHydraCollection(): void + { + $this->assertJsonContains(['@type' => 'Collection']); + } + + protected static function iri(string $resource, string $id): string + { + return sprintf('/api/%s/%s', $resource, $id); + } +} diff --git a/tests/Api/Controller/CommentControllerTest.php b/tests/Api/Controller/CommentControllerTest.php new file mode 100644 index 0000000..d86c981 --- /dev/null +++ b/tests/Api/Controller/CommentControllerTest.php @@ -0,0 +1,150 @@ +createMachine('Machine A'); + + $client = $this->createViewerClient(); + $client->request('POST', '/api/comments', [ + 'json' => [ + 'content' => 'Test comment', + 'entityType' => 'machine', + 'entityId' => $machine->getId(), + 'entityName' => 'Machine A', + ], + ]); + + $this->assertResponseStatusCodeSame(201); + $this->assertJsonContains([ + 'content' => 'Test comment', + 'entityType' => 'machine', + 'status' => 'open', + ]); + } + + public function testCreateCommentUnauthenticated(): void + { + $client = $this->createUnauthenticatedClient(); + $client->request('POST', '/api/comments', [ + 'json' => [ + 'content' => 'No auth', + 'entityType' => 'machine', + 'entityId' => 'fake-id', + ], + ]); + + $this->assertResponseStatusCodeSame(401); + } + + public function testCreateCommentEmptyContent(): void + { + $client = $this->createViewerClient(); + $client->request('POST', '/api/comments', [ + 'json' => [ + 'content' => '', + 'entityType' => 'machine', + 'entityId' => 'some-id', + ], + ]); + + $this->assertResponseStatusCodeSame(400); + $this->assertJsonContains(['message' => 'Le contenu est requis.']); + } + + public function testCreateCommentInvalidEntityType(): void + { + $client = $this->createViewerClient(); + $client->request('POST', '/api/comments', [ + 'json' => [ + 'content' => 'Invalid type', + 'entityType' => 'invalid', + 'entityId' => 'some-id', + ], + ]); + + $this->assertResponseStatusCodeSame(400); + $this->assertJsonContains(['message' => "Type d'entité invalide."]); + } + + public function testCreateCommentMissingEntityId(): void + { + $client = $this->createViewerClient(); + $client->request('POST', '/api/comments', [ + 'json' => [ + 'content' => 'Missing ID', + 'entityType' => 'machine', + 'entityId' => '', + ], + ]); + + $this->assertResponseStatusCodeSame(400); + } + + public function testResolveComment(): void + { + $machine = $this->createMachine('Machine A'); + + $viewerClient = $this->createViewerClient(); + $response = $viewerClient->request('POST', '/api/comments', [ + 'json' => [ + 'content' => 'To resolve', + 'entityType' => 'machine', + 'entityId' => $machine->getId(), + ], + ]); + + $data = $response->toArray(); + $commentId = $data['id']; + + $gestionnaireClient = $this->createGestionnaireClient(); + $gestionnaireClient->request('PATCH', sprintf('/api/comments/%s/resolve', $commentId)); + + $this->assertResponseIsSuccessful(); + $this->assertJsonContains(['status' => 'resolved']); + } + + public function testResolveCommentNotFound(): void + { + $client = $this->createGestionnaireClient(); + $client->request('PATCH', '/api/comments/nonexistent-id/resolve'); + + $this->assertResponseStatusCodeSame(404); + } + + public function testUnresolvedCount(): void + { + $machine = $this->createMachine('Machine A'); + + $client = $this->createViewerClient(); + $client->request('POST', '/api/comments', [ + 'json' => [ + 'content' => 'Open 1', + 'entityType' => 'machine', + 'entityId' => $machine->getId(), + ], + ]); + $client->request('POST', '/api/comments', [ + 'json' => [ + 'content' => 'Open 2', + 'entityType' => 'machine', + 'entityId' => $machine->getId(), + ], + ]); + + $client->request('GET', '/api/comments/stats/unresolved-count'); + + $this->assertResponseIsSuccessful(); + $this->assertJsonContains(['count' => 2]); + } +} diff --git a/tests/Api/Controller/EntityHistoryTest.php b/tests/Api/Controller/EntityHistoryTest.php new file mode 100644 index 0000000..a28bc7a --- /dev/null +++ b/tests/Api/Controller/EntityHistoryTest.php @@ -0,0 +1,94 @@ +createMachine('Machine A'); + + $client = $this->createViewerClient(); + $client->request('GET', sprintf('/api/machines/%s/history', $machine->getId())); + + $this->assertResponseIsSuccessful(); + $data = $client->getResponse()->toArray(); + $this->assertArrayHasKey('items', $data); + $this->assertArrayHasKey('total', $data); + } + + public function testMachineHistoryAfterUpdate(): void + { + $machine = $this->createMachine('Machine A'); + + $gClient = $this->createGestionnaireClient(); + $gClient->request('PATCH', self::iri('machines', $machine->getId()), [ + 'headers' => ['Content-Type' => 'application/merge-patch+json'], + 'json' => ['name' => 'Machine A Updated'], + ]); + $this->assertResponseIsSuccessful(); + + $vClient = $this->createViewerClient(); + $vClient->request('GET', sprintf('/api/machines/%s/history', $machine->getId())); + + $this->assertResponseIsSuccessful(); + $data = $vClient->getResponse()->toArray(); + $this->assertGreaterThanOrEqual(1, $data['total']); + } + + public function testMachineHistoryNotFound(): void + { + $client = $this->createViewerClient(); + $client->request('GET', '/api/machines/nonexistent-id/history'); + + $this->assertResponseStatusCodeSame(404); + $this->assertJsonContains(['message' => 'Machine introuvable.']); + } + + public function testPieceHistory(): void + { + $piece = $this->createPiece('Joint A'); + + $client = $this->createViewerClient(); + $client->request('GET', sprintf('/api/pieces/%s/history', $piece->getId())); + + $this->assertResponseIsSuccessful(); + } + + public function testComposantHistory(): void + { + $composant = $this->createComposant('Pompe A'); + + $client = $this->createViewerClient(); + $client->request('GET', sprintf('/api/composants/%s/history', $composant->getId())); + + $this->assertResponseIsSuccessful(); + } + + public function testProductHistory(): void + { + $product = $this->createProduct('Produit A'); + + $client = $this->createViewerClient(); + $client->request('GET', sprintf('/api/products/%s/history', $product->getId())); + + $this->assertResponseIsSuccessful(); + } + + public function testHistoryUnauthenticated(): void + { + $machine = $this->createMachine('Machine A'); + + $client = $this->createUnauthenticatedClient(); + $client->request('GET', sprintf('/api/machines/%s/history', $machine->getId())); + + $this->assertResponseStatusCodeSame(401); + } +} diff --git a/tests/Api/Entity/ComposantTest.php b/tests/Api/Entity/ComposantTest.php new file mode 100644 index 0000000..3d9fb45 --- /dev/null +++ b/tests/Api/Entity/ComposantTest.php @@ -0,0 +1,138 @@ +createComposant('Pompe A'); + $this->createComposant('Pompe B'); + + $client = $this->createViewerClient(); + $client->request('GET', '/api/composants'); + + $this->assertResponseIsSuccessful(); + $this->assertJsonContainsHydraCollection(); + $this->assertJsonContains(['totalItems' => 2]); + } + + public function testGetItem(): void + { + $c = $this->createComposant('Pompe A'); + + $client = $this->createViewerClient(); + $client->request('GET', self::iri('composants', $c->getId())); + + $this->assertResponseIsSuccessful(); + $this->assertJsonContains(['name' => 'Pompe A']); + } + + public function testPost(): void + { + $client = $this->createGestionnaireClient(); + $client->request('POST', '/api/composants', [ + 'headers' => ['Content-Type' => 'application/ld+json'], + 'json' => [ + 'name' => 'Nouveau composant', + 'reference' => 'REF-COMP-001', + ], + ]); + + $this->assertResponseStatusCodeSame(201); + $this->assertJsonContains(['name' => 'Nouveau composant']); + } + + public function testPostWithType(): void + { + $mt = $this->createModelType('Pompe', 'POMPE-001', ModelCategory::COMPONENT); + + $client = $this->createGestionnaireClient(); + $client->request('POST', '/api/composants', [ + 'headers' => ['Content-Type' => 'application/ld+json'], + 'json' => [ + 'name' => 'Composant typé', + 'typeComposant' => self::iri('model_types', $mt->getId()), + ], + ]); + + $this->assertResponseStatusCodeSame(201); + } + + public function testPut(): void + { + $c = $this->createComposant('Pompe A'); + + $client = $this->createGestionnaireClient(); + $client->request('PUT', self::iri('composants', $c->getId()), [ + 'headers' => ['Content-Type' => 'application/ld+json'], + 'json' => ['name' => 'Pompe A Renommée'], + ]); + + $this->assertResponseIsSuccessful(); + $this->assertJsonContains(['name' => 'Pompe A Renommée']); + } + + public function testPatch(): void + { + $c = $this->createComposant('Pompe A'); + + $client = $this->createGestionnaireClient(); + $client->request('PATCH', self::iri('composants', $c->getId()), [ + 'headers' => ['Content-Type' => 'application/merge-patch+json'], + 'json' => ['prix' => '250.00'], + ]); + + $this->assertResponseIsSuccessful(); + $this->assertJsonContains(['prix' => '250.00']); + } + + public function testDelete(): void + { + $c = $this->createComposant('ToDelete'); + + $client = $this->createGestionnaireClient(); + $client->request('DELETE', self::iri('composants', $c->getId())); + + $this->assertResponseStatusCodeSame(204); + } + + public function testUnauthenticatedAccess(): void + { + $client = $this->createUnauthenticatedClient(); + $client->request('GET', '/api/composants'); + + $this->assertResponseStatusCodeSame(401); + } + + public function testViewerCannotWrite(): void + { + $client = $this->createViewerClient(); + $client->request('POST', '/api/composants', [ + 'headers' => ['Content-Type' => 'application/ld+json'], + 'json' => ['name' => 'Blocked'], + ]); + + $this->assertResponseStatusCodeSame(403); + } + + public function testSearchFilter(): void + { + $this->createComposant('Pompe hydraulique'); + $this->createComposant('Vanne de contrôle'); + + $client = $this->createViewerClient(); + $client->request('GET', '/api/composants?name=pompe'); + + $this->assertResponseIsSuccessful(); + $this->assertJsonContains(['totalItems' => 1]); + } +} diff --git a/tests/Api/Entity/ConstructeurTest.php b/tests/Api/Entity/ConstructeurTest.php new file mode 100644 index 0000000..dc2063b --- /dev/null +++ b/tests/Api/Entity/ConstructeurTest.php @@ -0,0 +1,129 @@ +createConstructeur('Siemens'); + $this->createConstructeur('ABB'); + + $client = $this->createViewerClient(); + $client->request('GET', '/api/constructeurs'); + + $this->assertResponseIsSuccessful(); + $this->assertJsonContainsHydraCollection(); + $this->assertJsonContains(['totalItems' => 2]); + } + + public function testGetItem(): void + { + $c = $this->createConstructeur('Siemens', 'contact@siemens.com', '+33123456789'); + + $client = $this->createViewerClient(); + $client->request('GET', self::iri('constructeurs', $c->getId())); + + $this->assertResponseIsSuccessful(); + $this->assertJsonContains([ + 'name' => 'Siemens', + 'email' => 'contact@siemens.com', + 'phone' => '+33123456789', + ]); + } + + public function testPost(): void + { + $client = $this->createGestionnaireClient(); + $client->request('POST', '/api/constructeurs', [ + 'headers' => ['Content-Type' => 'application/ld+json'], + 'json' => [ + 'name' => 'Schneider', + 'email' => 'info@schneider.com', + ], + ]); + + $this->assertResponseStatusCodeSame(201); + $this->assertJsonContains(['name' => 'Schneider']); + } + + public function testPut(): void + { + $c = $this->createConstructeur('Siemens'); + + $client = $this->createGestionnaireClient(); + $client->request('PUT', self::iri('constructeurs', $c->getId()), [ + 'headers' => ['Content-Type' => 'application/ld+json'], + 'json' => [ + 'name' => 'Siemens AG', + 'email' => 'updated@siemens.com', + ], + ]); + + $this->assertResponseIsSuccessful(); + $this->assertJsonContains(['name' => 'Siemens AG']); + } + + public function testPatch(): void + { + $c = $this->createConstructeur('Siemens'); + + $client = $this->createGestionnaireClient(); + $client->request('PATCH', self::iri('constructeurs', $c->getId()), [ + 'headers' => ['Content-Type' => 'application/merge-patch+json'], + 'json' => ['phone' => '+33987654321'], + ]); + + $this->assertResponseIsSuccessful(); + $this->assertJsonContains(['phone' => '+33987654321']); + } + + public function testDelete(): void + { + $c = $this->createConstructeur('ToDelete'); + + $client = $this->createGestionnaireClient(); + $client->request('DELETE', self::iri('constructeurs', $c->getId())); + + $this->assertResponseStatusCodeSame(204); + } + + public function testUnauthenticatedAccess(): void + { + $client = $this->createUnauthenticatedClient(); + $client->request('GET', '/api/constructeurs'); + + $this->assertResponseStatusCodeSame(401); + } + + public function testViewerCannotWrite(): void + { + $client = $this->createViewerClient(); + $client->request('POST', '/api/constructeurs', [ + 'headers' => ['Content-Type' => 'application/ld+json'], + 'json' => ['name' => 'Blocked'], + ]); + + $this->assertResponseStatusCodeSame(403); + } + + public function testUniqueNameConstraint(): void + { + $this->createConstructeur('Siemens'); + + $client = $this->createGestionnaireClient(); + $client->request('POST', '/api/constructeurs', [ + 'headers' => ['Content-Type' => 'application/ld+json'], + 'json' => ['name' => 'Siemens'], + ]); + + $this->assertResponseStatusCodeSame(422); + } +} diff --git a/tests/Api/Entity/CustomFieldTest.php b/tests/Api/Entity/CustomFieldTest.php new file mode 100644 index 0000000..29d7634 --- /dev/null +++ b/tests/Api/Entity/CustomFieldTest.php @@ -0,0 +1,108 @@ +createMachine('Machine A'); + $this->createCustomField('Tension', 'number', $machine); + $this->createCustomField('Couleur', 'text', $machine); + + $client = $this->createViewerClient(); + $client->request('GET', '/api/custom_fields'); + + $this->assertResponseIsSuccessful(); + $this->assertJsonContainsHydraCollection(); + $this->assertJsonContains(['totalItems' => 2]); + } + + public function testGetItem(): void + { + $machine = $this->createMachine('Machine A'); + $cf = $this->createCustomField('Tension', 'number', $machine); + + $client = $this->createViewerClient(); + $client->request('GET', self::iri('custom_fields', $cf->getId())); + + $this->assertResponseIsSuccessful(); + $this->assertJsonContains([ + 'name' => 'Tension', + 'type' => 'number', + ]); + } + + public function testPost(): void + { + $machine = $this->createMachine('Machine A'); + + $client = $this->createGestionnaireClient(); + $client->request('POST', '/api/custom_fields', [ + 'headers' => ['Content-Type' => 'application/ld+json'], + 'json' => [ + 'name' => 'Pression', + 'type' => 'number', + 'machine' => self::iri('machines', $machine->getId()), + ], + ]); + + $this->assertResponseStatusCodeSame(201); + $this->assertJsonContains(['name' => 'Pression']); + } + + public function testPatch(): void + { + $machine = $this->createMachine('Machine A'); + $cf = $this->createCustomField('Tension', 'number', $machine); + + $client = $this->createGestionnaireClient(); + $client->request('PATCH', self::iri('custom_fields', $cf->getId()), [ + 'headers' => ['Content-Type' => 'application/merge-patch+json'], + 'json' => ['required' => true], + ]); + + $this->assertResponseIsSuccessful(); + $this->assertJsonContains(['required' => true]); + } + + public function testDelete(): void + { + $machine = $this->createMachine('Machine A'); + $cf = $this->createCustomField('ToDelete', 'text', $machine); + + $client = $this->createGestionnaireClient(); + $client->request('DELETE', self::iri('custom_fields', $cf->getId())); + + $this->assertResponseStatusCodeSame(204); + } + + public function testUnauthenticatedAccess(): void + { + $client = $this->createUnauthenticatedClient(); + $client->request('GET', '/api/custom_fields'); + + $this->assertResponseStatusCodeSame(401); + } + + public function testViewerCannotWrite(): void + { + $client = $this->createViewerClient(); + $client->request('POST', '/api/custom_fields', [ + 'headers' => ['Content-Type' => 'application/ld+json'], + 'json' => [ + 'name' => 'Blocked', + 'type' => 'text', + ], + ]); + + $this->assertResponseStatusCodeSame(403); + } +} diff --git a/tests/Api/Entity/CustomFieldValueTest.php b/tests/Api/Entity/CustomFieldValueTest.php new file mode 100644 index 0000000..bf3f211 --- /dev/null +++ b/tests/Api/Entity/CustomFieldValueTest.php @@ -0,0 +1,112 @@ +createMachine('Machine A'); + $cf = $this->createCustomField('Tension', 'number', $machine); + $this->createCustomFieldValue($cf, '220', $machine); + + $client = $this->createViewerClient(); + $client->request('GET', '/api/custom_field_values'); + + $this->assertResponseIsSuccessful(); + $this->assertJsonContainsHydraCollection(); + $this->assertJsonContains(['totalItems' => 1]); + } + + public function testGetItem(): void + { + $machine = $this->createMachine('Machine A'); + $cf = $this->createCustomField('Tension', 'number', $machine); + $cfv = $this->createCustomFieldValue($cf, '220', $machine); + + $client = $this->createViewerClient(); + $client->request('GET', self::iri('custom_field_values', $cfv->getId())); + + $this->assertResponseIsSuccessful(); + $this->assertJsonContains(['value' => '220']); + } + + public function testPost(): void + { + $machine = $this->createMachine('Machine A'); + $cf = $this->createCustomField('Tension', 'number', $machine); + + $client = $this->createGestionnaireClient(); + $client->request('POST', '/api/custom_field_values', [ + 'headers' => ['Content-Type' => 'application/ld+json'], + 'json' => [ + 'value' => '380', + 'customField' => self::iri('custom_fields', $cf->getId()), + 'machine' => self::iri('machines', $machine->getId()), + ], + ]); + + $this->assertResponseStatusCodeSame(201); + $this->assertJsonContains(['value' => '380']); + } + + public function testPatch(): void + { + $machine = $this->createMachine('Machine A'); + $cf = $this->createCustomField('Tension', 'number', $machine); + $cfv = $this->createCustomFieldValue($cf, '220', $machine); + + $client = $this->createGestionnaireClient(); + $client->request('PATCH', self::iri('custom_field_values', $cfv->getId()), [ + 'headers' => ['Content-Type' => 'application/merge-patch+json'], + 'json' => ['value' => '400'], + ]); + + $this->assertResponseIsSuccessful(); + $this->assertJsonContains(['value' => '400']); + } + + public function testDelete(): void + { + $machine = $this->createMachine('Machine A'); + $cf = $this->createCustomField('ToDelete', 'text', $machine); + $cfv = $this->createCustomFieldValue($cf, 'val', $machine); + + $client = $this->createGestionnaireClient(); + $client->request('DELETE', self::iri('custom_field_values', $cfv->getId())); + + $this->assertResponseStatusCodeSame(204); + } + + public function testUnauthenticatedAccess(): void + { + $client = $this->createUnauthenticatedClient(); + $client->request('GET', '/api/custom_field_values'); + + $this->assertResponseStatusCodeSame(401); + } + + public function testViewerCannotWrite(): void + { + $machine = $this->createMachine('Machine A'); + $cf = $this->createCustomField('Blocked', 'text', $machine); + + $client = $this->createViewerClient(); + $client->request('POST', '/api/custom_field_values', [ + 'headers' => ['Content-Type' => 'application/ld+json'], + 'json' => [ + 'value' => 'nope', + 'customField' => self::iri('custom_fields', $cf->getId()), + ], + ]); + + $this->assertResponseStatusCodeSame(403); + } +} diff --git a/tests/Api/Entity/DocumentTest.php b/tests/Api/Entity/DocumentTest.php new file mode 100644 index 0000000..91b1a1c --- /dev/null +++ b/tests/Api/Entity/DocumentTest.php @@ -0,0 +1,146 @@ +createDocumentInDb(); + + $client = $this->createViewerClient(); + $client->request('GET', '/api/documents'); + + $this->assertResponseIsSuccessful(); + $this->assertJsonContainsHydraCollection(); + $this->assertJsonContains(['totalItems' => 1]); + } + + public function testGetItem(): void + { + $doc = $this->createDocumentInDb(); + + $client = $this->createViewerClient(); + $client->request('GET', self::iri('documents', $doc->getId())); + + $this->assertResponseIsSuccessful(); + $this->assertJsonContains([ + 'name' => 'test-doc', + 'filename' => 'test.pdf', + 'mimeType' => 'application/pdf', + 'size' => 1024, + ]); + } + + public function testPut(): void + { + $doc = $this->createDocumentInDb(); + + $client = $this->createGestionnaireClient(); + $client->request('PUT', self::iri('documents', $doc->getId()), [ + 'headers' => ['Content-Type' => 'application/ld+json'], + 'json' => [ + 'name' => 'renamed-doc', + 'filename' => 'test.pdf', + 'path' => '/uploads/test.pdf', + 'mimeType' => 'application/pdf', + 'size' => 1024, + ], + ]); + + $this->assertResponseIsSuccessful(); + $this->assertJsonContains(['name' => 'renamed-doc']); + } + + public function testDelete(): void + { + $doc = $this->createDocumentInDb(); + + $client = $this->createGestionnaireClient(); + $client->request('DELETE', self::iri('documents', $doc->getId())); + + $this->assertResponseStatusCodeSame(204); + } + + public function testUnauthenticatedAccess(): void + { + $client = $this->createUnauthenticatedClient(); + $client->request('GET', '/api/documents'); + + $this->assertResponseStatusCodeSame(401); + } + + public function testViewerCannotDelete(): void + { + $doc = $this->createDocumentInDb(); + + $client = $this->createViewerClient(); + $client->request('DELETE', self::iri('documents', $doc->getId())); + + $this->assertResponseStatusCodeSame(403); + } + + public function testDocumentLinkedToMachine(): void + { + $machine = $this->createMachine('Machine A'); + $doc = $this->createDocumentInDb($machine->getId()); + + $client = $this->createViewerClient(); + $client->request('GET', '/api/documents?machine[exists]=true'); + + $this->assertResponseIsSuccessful(); + $this->assertJsonContains(['totalItems' => 1]); + } + + public function testDocumentLinkedToSite(): void + { + $site = $this->createSite('Site A'); + $doc = $this->createDocumentInDb(siteId: $site->getId()); + + $client = $this->createViewerClient(); + $client->request('GET', '/api/documents?site[exists]=true'); + + $this->assertResponseIsSuccessful(); + $this->assertJsonContains(['totalItems' => 1]); + } + + private function createDocumentInDb(?string $machineId = null, ?string $siteId = null): Document + { + $doc = new Document(); + $doc->setName('test-doc'); + $doc->setFilename('test.pdf'); + $doc->setPath('/uploads/test.pdf'); + $doc->setMimeType('application/pdf'); + $doc->setSize(1024); + + if ($machineId) { + $machine = $this->getEntityManager()->getRepository(Machine::class)->find($machineId); + if ($machine) { + $doc->setMachine($machine); + } + } + + if ($siteId) { + $site = $this->getEntityManager()->getRepository(Site::class)->find($siteId); + if ($site) { + $doc->setSite($site); + } + } + + $em = $this->getEntityManager(); + $em->persist($doc); + $em->flush(); + + return $doc; + } +} diff --git a/tests/Api/Entity/MachineComponentLinkTest.php b/tests/Api/Entity/MachineComponentLinkTest.php new file mode 100644 index 0000000..f3dcb73 --- /dev/null +++ b/tests/Api/Entity/MachineComponentLinkTest.php @@ -0,0 +1,134 @@ +createMachine('Machine A'); + $composant = $this->createComposant('Pompe A'); + $this->createMachineComponentLink($machine, $composant); + + $client = $this->createViewerClient(); + $client->request('GET', '/api/machine_component_links'); + + $this->assertResponseIsSuccessful(); + $this->assertJsonContainsHydraCollection(); + $this->assertJsonContains(['totalItems' => 1]); + } + + public function testGetItem(): void + { + $machine = $this->createMachine('Machine A'); + $composant = $this->createComposant('Pompe A'); + $link = $this->createMachineComponentLink($machine, $composant); + + $client = $this->createViewerClient(); + $client->request('GET', self::iri('machine_component_links', $link->getId())); + + $this->assertResponseIsSuccessful(); + } + + public function testPost(): void + { + $machine = $this->createMachine('Machine A'); + $composant = $this->createComposant('Pompe A'); + + $client = $this->createGestionnaireClient(); + $client->request('POST', '/api/machine_component_links', [ + 'headers' => ['Content-Type' => 'application/ld+json'], + 'json' => [ + 'machine' => self::iri('machines', $machine->getId()), + 'composant' => self::iri('composants', $composant->getId()), + ], + ]); + + $this->assertResponseStatusCodeSame(201); + } + + public function testPostWithOverrides(): void + { + $machine = $this->createMachine('Machine A'); + $composant = $this->createComposant('Pompe A'); + + $client = $this->createGestionnaireClient(); + $client->request('POST', '/api/machine_component_links', [ + 'headers' => ['Content-Type' => 'application/ld+json'], + 'json' => [ + 'machine' => self::iri('machines', $machine->getId()), + 'composant' => self::iri('composants', $composant->getId()), + 'nameOverride' => 'Pompe spéciale', + 'referenceOverride' => 'REF-OVERRIDE', + 'prixOverride' => '500.00', + ], + ]); + + $this->assertResponseStatusCodeSame(201); + $this->assertJsonContains([ + 'nameOverride' => 'Pompe spéciale', + 'referenceOverride' => 'REF-OVERRIDE', + 'prixOverride' => '500.00', + ]); + } + + public function testPatch(): void + { + $machine = $this->createMachine('Machine A'); + $composant = $this->createComposant('Pompe A'); + $link = $this->createMachineComponentLink($machine, $composant); + + $client = $this->createGestionnaireClient(); + $client->request('PATCH', self::iri('machine_component_links', $link->getId()), [ + 'headers' => ['Content-Type' => 'application/merge-patch+json'], + 'json' => ['nameOverride' => 'Overridden'], + ]); + + $this->assertResponseIsSuccessful(); + $this->assertJsonContains(['nameOverride' => 'Overridden']); + } + + public function testDelete(): void + { + $machine = $this->createMachine('Machine A'); + $composant = $this->createComposant('Pompe A'); + $link = $this->createMachineComponentLink($machine, $composant); + + $client = $this->createGestionnaireClient(); + $client->request('DELETE', self::iri('machine_component_links', $link->getId())); + + $this->assertResponseStatusCodeSame(204); + } + + public function testUnauthenticatedAccess(): void + { + $client = $this->createUnauthenticatedClient(); + $client->request('GET', '/api/machine_component_links'); + + $this->assertResponseStatusCodeSame(401); + } + + public function testViewerCannotWrite(): void + { + $machine = $this->createMachine('Machine A'); + $composant = $this->createComposant('Pompe A'); + + $client = $this->createViewerClient(); + $client->request('POST', '/api/machine_component_links', [ + 'headers' => ['Content-Type' => 'application/ld+json'], + 'json' => [ + 'machine' => self::iri('machines', $machine->getId()), + 'composant' => self::iri('composants', $composant->getId()), + ], + ]); + + $this->assertResponseStatusCodeSame(403); + } +} diff --git a/tests/Api/Entity/MachinePieceLinkTest.php b/tests/Api/Entity/MachinePieceLinkTest.php new file mode 100644 index 0000000..994e8e4 --- /dev/null +++ b/tests/Api/Entity/MachinePieceLinkTest.php @@ -0,0 +1,124 @@ +createMachine('Machine A'); + $piece = $this->createPiece('Joint A'); + $this->createMachinePieceLink($machine, $piece); + + $client = $this->createViewerClient(); + $client->request('GET', '/api/machine_piece_links'); + + $this->assertResponseIsSuccessful(); + $this->assertJsonContainsHydraCollection(); + $this->assertJsonContains(['totalItems' => 1]); + } + + public function testPost(): void + { + $machine = $this->createMachine('Machine A'); + $piece = $this->createPiece('Joint A'); + + $client = $this->createGestionnaireClient(); + $client->request('POST', '/api/machine_piece_links', [ + 'headers' => ['Content-Type' => 'application/ld+json'], + 'json' => [ + 'machine' => self::iri('machines', $machine->getId()), + 'piece' => self::iri('pieces', $piece->getId()), + ], + ]); + + $this->assertResponseStatusCodeSame(201); + } + + public function testPostWithParentComponentLink(): void + { + $machine = $this->createMachine('Machine A'); + $composant = $this->createComposant('Pompe A'); + $piece = $this->createPiece('Joint A'); + $compLink = $this->createMachineComponentLink($machine, $composant); + + $client = $this->createGestionnaireClient(); + $client->request('POST', '/api/machine_piece_links', [ + 'headers' => ['Content-Type' => 'application/ld+json'], + 'json' => [ + 'machine' => self::iri('machines', $machine->getId()), + 'piece' => self::iri('pieces', $piece->getId()), + 'parentLink' => self::iri('machine_component_links', $compLink->getId()), + ], + ]); + + $this->assertResponseStatusCodeSame(201); + } + + public function testPostWithOverrides(): void + { + $machine = $this->createMachine('Machine A'); + $piece = $this->createPiece('Joint A'); + + $client = $this->createGestionnaireClient(); + $client->request('POST', '/api/machine_piece_links', [ + 'headers' => ['Content-Type' => 'application/ld+json'], + 'json' => [ + 'machine' => self::iri('machines', $machine->getId()), + 'piece' => self::iri('pieces', $piece->getId()), + 'nameOverride' => 'Joint spécial', + 'prixOverride' => '25.00', + ], + ]); + + $this->assertResponseStatusCodeSame(201); + $this->assertJsonContains([ + 'nameOverride' => 'Joint spécial', + 'prixOverride' => '25.00', + ]); + } + + public function testDelete(): void + { + $machine = $this->createMachine('Machine A'); + $piece = $this->createPiece('Joint A'); + $link = $this->createMachinePieceLink($machine, $piece); + + $client = $this->createGestionnaireClient(); + $client->request('DELETE', self::iri('machine_piece_links', $link->getId())); + + $this->assertResponseStatusCodeSame(204); + } + + public function testUnauthenticatedAccess(): void + { + $client = $this->createUnauthenticatedClient(); + $client->request('GET', '/api/machine_piece_links'); + + $this->assertResponseStatusCodeSame(401); + } + + public function testViewerCannotWrite(): void + { + $machine = $this->createMachine('Machine A'); + $piece = $this->createPiece('Joint A'); + + $client = $this->createViewerClient(); + $client->request('POST', '/api/machine_piece_links', [ + 'headers' => ['Content-Type' => 'application/ld+json'], + 'json' => [ + 'machine' => self::iri('machines', $machine->getId()), + 'piece' => self::iri('pieces', $piece->getId()), + ], + ]); + + $this->assertResponseStatusCodeSame(403); + } +} diff --git a/tests/Api/Entity/MachineProductLinkTest.php b/tests/Api/Entity/MachineProductLinkTest.php new file mode 100644 index 0000000..0d3f231 --- /dev/null +++ b/tests/Api/Entity/MachineProductLinkTest.php @@ -0,0 +1,121 @@ +createMachine('Machine A'); + $product = $this->createProduct('Produit A'); + $this->createMachineProductLink($machine, $product); + + $client = $this->createViewerClient(); + $client->request('GET', '/api/machine_product_links'); + + $this->assertResponseIsSuccessful(); + $this->assertJsonContainsHydraCollection(); + $this->assertJsonContains(['totalItems' => 1]); + } + + public function testPost(): void + { + $machine = $this->createMachine('Machine A'); + $product = $this->createProduct('Produit A'); + + $client = $this->createGestionnaireClient(); + $client->request('POST', '/api/machine_product_links', [ + 'headers' => ['Content-Type' => 'application/ld+json'], + 'json' => [ + 'machine' => self::iri('machines', $machine->getId()), + 'product' => self::iri('products', $product->getId()), + ], + ]); + + $this->assertResponseStatusCodeSame(201); + } + + public function testPostWithParentComponentLink(): void + { + $machine = $this->createMachine('Machine A'); + $composant = $this->createComposant('Pompe'); + $product = $this->createProduct('Produit A'); + $compLink = $this->createMachineComponentLink($machine, $composant); + + $client = $this->createGestionnaireClient(); + $client->request('POST', '/api/machine_product_links', [ + 'headers' => ['Content-Type' => 'application/ld+json'], + 'json' => [ + 'machine' => self::iri('machines', $machine->getId()), + 'product' => self::iri('products', $product->getId()), + 'parentComponentLink' => self::iri('machine_component_links', $compLink->getId()), + ], + ]); + + $this->assertResponseStatusCodeSame(201); + } + + public function testPostWithParentPieceLink(): void + { + $machine = $this->createMachine('Machine A'); + $piece = $this->createPiece('Joint'); + $product = $this->createProduct('Produit A'); + $plLink = $this->createMachinePieceLink($machine, $piece); + + $client = $this->createGestionnaireClient(); + $client->request('POST', '/api/machine_product_links', [ + 'headers' => ['Content-Type' => 'application/ld+json'], + 'json' => [ + 'machine' => self::iri('machines', $machine->getId()), + 'product' => self::iri('products', $product->getId()), + 'parentPieceLink' => self::iri('machine_piece_links', $plLink->getId()), + ], + ]); + + $this->assertResponseStatusCodeSame(201); + } + + public function testDelete(): void + { + $machine = $this->createMachine('Machine A'); + $product = $this->createProduct('Produit A'); + $link = $this->createMachineProductLink($machine, $product); + + $client = $this->createGestionnaireClient(); + $client->request('DELETE', self::iri('machine_product_links', $link->getId())); + + $this->assertResponseStatusCodeSame(204); + } + + public function testUnauthenticatedAccess(): void + { + $client = $this->createUnauthenticatedClient(); + $client->request('GET', '/api/machine_product_links'); + + $this->assertResponseStatusCodeSame(401); + } + + public function testViewerCannotWrite(): void + { + $machine = $this->createMachine('Machine A'); + $product = $this->createProduct('Produit A'); + + $client = $this->createViewerClient(); + $client->request('POST', '/api/machine_product_links', [ + 'headers' => ['Content-Type' => 'application/ld+json'], + 'json' => [ + 'machine' => self::iri('machines', $machine->getId()), + 'product' => self::iri('products', $product->getId()), + ], + ]); + + $this->assertResponseStatusCodeSame(403); + } +} diff --git a/tests/Api/Entity/MachineTest.php b/tests/Api/Entity/MachineTest.php new file mode 100644 index 0000000..1066779 --- /dev/null +++ b/tests/Api/Entity/MachineTest.php @@ -0,0 +1,135 @@ +createSite(); + $this->createMachine('Machine A', $site); + $this->createMachine('Machine B', $site); + + $client = $this->createViewerClient(); + $client->request('GET', '/api/machines'); + + $this->assertResponseIsSuccessful(); + $this->assertJsonContainsHydraCollection(); + $this->assertJsonContains(['totalItems' => 2]); + } + + public function testGetItem(): void + { + $m = $this->createMachine('Machine A', reference: 'REF-001'); + + $client = $this->createViewerClient(); + $client->request('GET', self::iri('machines', $m->getId())); + + $this->assertResponseIsSuccessful(); + $this->assertJsonContains([ + 'name' => 'Machine A', + 'reference' => 'REF-001', + ]); + } + + public function testPost(): void + { + $site = $this->createSite('Usine'); + + $client = $this->createGestionnaireClient(); + $client->request('POST', '/api/machines', [ + 'headers' => ['Content-Type' => 'application/ld+json'], + 'json' => [ + 'name' => 'Nouvelle Machine', + 'reference' => 'REF-NEW', + 'site' => self::iri('sites', $site->getId()), + ], + ]); + + $this->assertResponseStatusCodeSame(201); + $this->assertJsonContains(['name' => 'Nouvelle Machine']); + } + + public function testPut(): void + { + $m = $this->createMachine('Machine A'); + + $client = $this->createGestionnaireClient(); + $client->request('PUT', self::iri('machines', $m->getId()), [ + 'headers' => ['Content-Type' => 'application/ld+json'], + 'json' => [ + 'name' => 'Machine A Renommée', + 'site' => self::iri('sites', $m->getSite()->getId()), + ], + ]); + + $this->assertResponseIsSuccessful(); + $this->assertJsonContains(['name' => 'Machine A Renommée']); + } + + public function testPatch(): void + { + $m = $this->createMachine('Machine A'); + + $client = $this->createGestionnaireClient(); + $client->request('PATCH', self::iri('machines', $m->getId()), [ + 'headers' => ['Content-Type' => 'application/merge-patch+json'], + 'json' => ['prix' => '1500.00'], + ]); + + $this->assertResponseIsSuccessful(); + $this->assertJsonContains(['prix' => '1500.00']); + } + + public function testDelete(): void + { + $m = $this->createMachine('ToDelete'); + + $client = $this->createGestionnaireClient(); + $client->request('DELETE', self::iri('machines', $m->getId())); + + $this->assertResponseStatusCodeSame(204); + } + + public function testUnauthenticatedAccess(): void + { + $client = $this->createUnauthenticatedClient(); + $client->request('GET', '/api/machines'); + + $this->assertResponseStatusCodeSame(401); + } + + public function testViewerCannotWrite(): void + { + $site = $this->createSite(); + + $client = $this->createViewerClient(); + $client->request('POST', '/api/machines', [ + 'headers' => ['Content-Type' => 'application/ld+json'], + 'json' => [ + 'name' => 'Blocked', + 'site' => self::iri('sites', $site->getId()), + ], + ]); + + $this->assertResponseStatusCodeSame(403); + } + + public function testPostWithoutSiteFails(): void + { + $client = $this->createGestionnaireClient(); + $client->request('POST', '/api/machines', [ + 'headers' => ['Content-Type' => 'application/ld+json'], + 'json' => ['name' => 'No site'], + ]); + + $this->assertResponseStatusCodeSame(422); + } +} diff --git a/tests/Api/Entity/ModelTypeTest.php b/tests/Api/Entity/ModelTypeTest.php new file mode 100644 index 0000000..06faedf --- /dev/null +++ b/tests/Api/Entity/ModelTypeTest.php @@ -0,0 +1,155 @@ +createModelType('Pompe', 'POMPE-001', ModelCategory::COMPONENT); + $this->createModelType('Joint', 'JOINT-001', ModelCategory::PIECE); + + $client = $this->createViewerClient(); + $client->request('GET', '/api/model_types'); + + $this->assertResponseIsSuccessful(); + $this->assertJsonContainsHydraCollection(); + $this->assertJsonContains(['totalItems' => 2]); + } + + public function testGetItem(): void + { + $mt = $this->createModelType('Pompe', 'POMPE-001', ModelCategory::COMPONENT); + + $client = $this->createViewerClient(); + $client->request('GET', self::iri('model_types', $mt->getId())); + + $this->assertResponseIsSuccessful(); + $this->assertJsonContains([ + 'name' => 'Pompe', + 'code' => 'POMPE-001', + 'category' => 'COMPONENT', + ]); + } + + public function testPost(): void + { + $client = $this->createGestionnaireClient(); + $client->request('POST', '/api/model_types', [ + 'headers' => ['Content-Type' => 'application/ld+json'], + 'json' => [ + 'name' => 'Roulement', + 'code' => 'ROUL-001', + 'category' => 'PIECE', + ], + ]); + + $this->assertResponseStatusCodeSame(201); + $this->assertJsonContains([ + 'name' => 'Roulement', + 'category' => 'PIECE', + ]); + } + + public function testPut(): void + { + $mt = $this->createModelType('Pompe', 'POMPE-001', ModelCategory::COMPONENT); + + $client = $this->createGestionnaireClient(); + $client->request('PUT', self::iri('model_types', $mt->getId()), [ + 'headers' => ['Content-Type' => 'application/ld+json'], + 'json' => [ + 'name' => 'Pompe hydraulique', + 'code' => 'POMPE-001', + 'category' => 'COMPONENT', + ], + ]); + + $this->assertResponseIsSuccessful(); + $this->assertJsonContains(['name' => 'Pompe hydraulique']); + } + + public function testPatch(): void + { + $mt = $this->createModelType('Pompe', 'POMPE-001', ModelCategory::COMPONENT); + + $client = $this->createGestionnaireClient(); + $client->request('PATCH', self::iri('model_types', $mt->getId()), [ + 'headers' => ['Content-Type' => 'application/merge-patch+json'], + 'json' => ['notes' => 'Updated notes'], + ]); + + $this->assertResponseIsSuccessful(); + $this->assertJsonContains(['notes' => 'Updated notes']); + } + + public function testDelete(): void + { + $mt = $this->createModelType('ToDelete', 'DEL-001', ModelCategory::PRODUCT); + + $client = $this->createGestionnaireClient(); + $client->request('DELETE', self::iri('model_types', $mt->getId())); + + $this->assertResponseStatusCodeSame(204); + } + + public function testUnauthenticatedAccess(): void + { + $client = $this->createUnauthenticatedClient(); + $client->request('GET', '/api/model_types'); + + $this->assertResponseStatusCodeSame(401); + } + + public function testViewerCannotWrite(): void + { + $client = $this->createViewerClient(); + $client->request('POST', '/api/model_types', [ + 'headers' => ['Content-Type' => 'application/ld+json'], + 'json' => [ + 'name' => 'Blocked', + 'code' => 'BLK-001', + 'category' => 'COMPONENT', + ], + ]); + + $this->assertResponseStatusCodeSame(403); + } + + public function testFilterByCategory(): void + { + $this->createModelType('Pompe', 'POMPE-001', ModelCategory::COMPONENT); + $this->createModelType('Joint', 'JOINT-001', ModelCategory::PIECE); + + $client = $this->createViewerClient(); + $client->request('GET', '/api/model_types?category=COMPONENT'); + + $this->assertResponseIsSuccessful(); + $this->assertJsonContains(['totalItems' => 1]); + } + + public function testUniqueCodeConstraint(): void + { + $this->createModelType('Pompe', 'POMPE-001', ModelCategory::COMPONENT); + + $client = $this->createGestionnaireClient(); + $client->request('POST', '/api/model_types', [ + 'headers' => ['Content-Type' => 'application/ld+json'], + 'json' => [ + 'name' => 'Autre', + 'code' => 'POMPE-001', + 'category' => 'PIECE', + ], + ]); + + $this->assertResponseStatusCodeSame(409); + } +} diff --git a/tests/Api/Entity/PieceTest.php b/tests/Api/Entity/PieceTest.php new file mode 100644 index 0000000..6e4962b --- /dev/null +++ b/tests/Api/Entity/PieceTest.php @@ -0,0 +1,145 @@ +createPiece('Joint A'); + $this->createPiece('Joint B'); + + $client = $this->createViewerClient(); + $client->request('GET', '/api/pieces'); + + $this->assertResponseIsSuccessful(); + $this->assertJsonContainsHydraCollection(); + $this->assertJsonContains(['totalItems' => 2]); + } + + public function testGetItem(): void + { + $p = $this->createPiece('Joint A', 'REF-J001'); + + $client = $this->createViewerClient(); + $client->request('GET', self::iri('pieces', $p->getId())); + + $this->assertResponseIsSuccessful(); + $this->assertJsonContains([ + 'name' => 'Joint A', + 'reference' => 'REF-J001', + ]); + } + + public function testPost(): void + { + $client = $this->createGestionnaireClient(); + $client->request('POST', '/api/pieces', [ + 'headers' => ['Content-Type' => 'application/ld+json'], + 'json' => [ + 'name' => 'Nouvelle pièce', + 'reference' => 'REF-NEW', + ], + ]); + + $this->assertResponseStatusCodeSame(201); + $this->assertJsonContains(['name' => 'Nouvelle pièce']); + } + + public function testPostWithType(): void + { + $mt = $this->createModelType('Joint', 'JOINT-001', ModelCategory::PIECE); + + $client = $this->createGestionnaireClient(); + $client->request('POST', '/api/pieces', [ + 'headers' => ['Content-Type' => 'application/ld+json'], + 'json' => [ + 'name' => 'Pièce typée', + 'typePiece' => self::iri('model_types', $mt->getId()), + ], + ]); + + $this->assertResponseStatusCodeSame(201); + } + + public function testPut(): void + { + $p = $this->createPiece('Joint A'); + + $client = $this->createGestionnaireClient(); + $client->request('PUT', self::iri('pieces', $p->getId()), [ + 'headers' => ['Content-Type' => 'application/ld+json'], + 'json' => ['name' => 'Joint A Renommé'], + ]); + + $this->assertResponseIsSuccessful(); + $this->assertJsonContains(['name' => 'Joint A Renommé']); + } + + public function testPatch(): void + { + $p = $this->createPiece('Joint A'); + + $client = $this->createGestionnaireClient(); + $client->request('PATCH', self::iri('pieces', $p->getId()), [ + 'headers' => ['Content-Type' => 'application/merge-patch+json'], + 'json' => ['prix' => '15.50'], + ]); + + $this->assertResponseIsSuccessful(); + $this->assertJsonContains(['prix' => '15.50']); + } + + public function testDelete(): void + { + $p = $this->createPiece('ToDelete'); + + $client = $this->createGestionnaireClient(); + $client->request('DELETE', self::iri('pieces', $p->getId())); + + $this->assertResponseStatusCodeSame(204); + } + + public function testUnauthenticatedAccess(): void + { + $client = $this->createUnauthenticatedClient(); + $client->request('GET', '/api/pieces'); + + $this->assertResponseStatusCodeSame(401); + } + + public function testViewerCannotWrite(): void + { + $client = $this->createViewerClient(); + $client->request('POST', '/api/pieces', [ + 'headers' => ['Content-Type' => 'application/ld+json'], + 'json' => ['name' => 'Blocked'], + ]); + + $this->assertResponseStatusCodeSame(403); + } + + public function testUniqueReferenceConstraint(): void + { + $this->createPiece('Joint A', 'REF-UNIQUE'); + + $client = $this->createGestionnaireClient(); + $client->request('POST', '/api/pieces', [ + 'headers' => ['Content-Type' => 'application/ld+json'], + 'json' => [ + 'name' => 'Joint B', + 'reference' => 'REF-UNIQUE', + ], + ]); + + $this->assertResponseStatusCodeSame(422); + } +} diff --git a/tests/Api/Entity/ProductTest.php b/tests/Api/Entity/ProductTest.php new file mode 100644 index 0000000..a70d6b6 --- /dev/null +++ b/tests/Api/Entity/ProductTest.php @@ -0,0 +1,142 @@ +createProduct('Produit A'); + $this->createProduct('Produit B'); + + $client = $this->createViewerClient(); + $client->request('GET', '/api/products'); + + $this->assertResponseIsSuccessful(); + $this->assertJsonContainsHydraCollection(); + $this->assertJsonContains(['totalItems' => 2]); + } + + public function testGetItem(): void + { + $p = $this->createProduct('Produit A', 'REF-P001'); + + $client = $this->createViewerClient(); + $client->request('GET', self::iri('products', $p->getId())); + + $this->assertResponseIsSuccessful(); + $this->assertJsonContains([ + 'name' => 'Produit A', + 'reference' => 'REF-P001', + ]); + } + + public function testPost(): void + { + $client = $this->createGestionnaireClient(); + $client->request('POST', '/api/products', [ + 'headers' => ['Content-Type' => 'application/ld+json'], + 'json' => [ + 'name' => 'Nouveau produit', + 'reference' => 'REF-NEW', + 'supplierPrice' => '99.99', + ], + ]); + + $this->assertResponseStatusCodeSame(201); + $this->assertJsonContains(['name' => 'Nouveau produit']); + } + + public function testPostWithType(): void + { + $mt = $this->createModelType('Huile', 'HUILE-001', ModelCategory::PRODUCT); + + $client = $this->createGestionnaireClient(); + $client->request('POST', '/api/products', [ + 'headers' => ['Content-Type' => 'application/ld+json'], + 'json' => [ + 'name' => 'Produit typé', + 'typeProduct' => self::iri('model_types', $mt->getId()), + ], + ]); + + $this->assertResponseStatusCodeSame(201); + } + + public function testPut(): void + { + $p = $this->createProduct('Produit A'); + + $client = $this->createGestionnaireClient(); + $client->request('PUT', self::iri('products', $p->getId()), [ + 'headers' => ['Content-Type' => 'application/ld+json'], + 'json' => ['name' => 'Produit A Renommé'], + ]); + + $this->assertResponseIsSuccessful(); + $this->assertJsonContains(['name' => 'Produit A Renommé']); + } + + public function testPatch(): void + { + $p = $this->createProduct('Produit A'); + + $client = $this->createGestionnaireClient(); + $client->request('PATCH', self::iri('products', $p->getId()), [ + 'headers' => ['Content-Type' => 'application/merge-patch+json'], + 'json' => ['supplierPrice' => '150.00'], + ]); + + $this->assertResponseIsSuccessful(); + $this->assertJsonContains(['supplierPrice' => '150.00']); + } + + public function testDelete(): void + { + $p = $this->createProduct('ToDelete'); + + $client = $this->createGestionnaireClient(); + $client->request('DELETE', self::iri('products', $p->getId())); + + $this->assertResponseStatusCodeSame(204); + } + + public function testUnauthenticatedAccess(): void + { + $client = $this->createUnauthenticatedClient(); + $client->request('GET', '/api/products'); + + $this->assertResponseStatusCodeSame(401); + } + + public function testViewerCannotWrite(): void + { + $client = $this->createViewerClient(); + $client->request('POST', '/api/products', [ + 'headers' => ['Content-Type' => 'application/ld+json'], + 'json' => ['name' => 'Blocked'], + ]); + + $this->assertResponseStatusCodeSame(403); + } + + public function testSearchFilter(): void + { + $this->createProduct('Huile moteur'); + $this->createProduct('Filtre à air'); + + $client = $this->createViewerClient(); + $client->request('GET', '/api/products?name=huile'); + + $this->assertResponseIsSuccessful(); + $this->assertJsonContains(['totalItems' => 1]); + } +} diff --git a/tests/Api/Entity/ProfileTest.php b/tests/Api/Entity/ProfileTest.php new file mode 100644 index 0000000..9583972 --- /dev/null +++ b/tests/Api/Entity/ProfileTest.php @@ -0,0 +1,125 @@ +createProfile(firstName: 'Alice', lastName: 'Dupont'); + + $client = $this->createAdminClient(); + $client->request('GET', '/api/profiles'); + + $this->assertResponseIsSuccessful(); + $this->assertJsonContainsHydraCollection(); + } + + public function testGetCollectionForbiddenForViewer(): void + { + $client = $this->createViewerClient(); + $client->request('GET', '/api/profiles'); + + $this->assertResponseStatusCodeSame(403); + } + + public function testGetItemForbiddenForViewer(): void + { + $profile = $this->createProfile(firstName: 'Alice', lastName: 'Dupont'); + + $client = $this->createViewerClient(); + $client->request('GET', self::iri('profiles', $profile->getId())); + + $this->assertResponseStatusCodeSame(403); + } + + public function testGetItemAsAdmin(): void + { + $profile = $this->createProfile(firstName: 'Alice', lastName: 'Dupont'); + + $client = $this->createAdminClient(); + $client->request('GET', self::iri('profiles', $profile->getId())); + + $this->assertResponseIsSuccessful(); + $this->assertJsonContains([ + 'firstName' => 'Alice', + 'lastName' => 'Dupont', + ]); + } + + public function testPostAsAdmin(): void + { + $client = $this->createAdminClient(); + $client->request('POST', '/api/profiles', [ + 'headers' => ['Content-Type' => 'application/ld+json'], + 'json' => [ + 'firstName' => 'Nouveau', + 'lastName' => 'Profil', + 'email' => 'new@test.local', + 'plainPassword' => 'secret123', + 'roles' => ['ROLE_VIEWER'], + ], + ]); + + $this->assertResponseStatusCodeSame(201); + $this->assertJsonContains([ + 'firstName' => 'Nouveau', + 'lastName' => 'Profil', + ]); + } + + public function testPostForbiddenForGestionnaire(): void + { + $client = $this->createGestionnaireClient(); + $client->request('POST', '/api/profiles', [ + 'headers' => ['Content-Type' => 'application/ld+json'], + 'json' => [ + 'firstName' => 'Blocked', + 'lastName' => 'User', + ], + ]); + + $this->assertResponseStatusCodeSame(403); + } + + public function testPatchAsAdmin(): void + { + $profile = $this->createProfile(firstName: 'Alice', lastName: 'Dupont'); + + $client = $this->createAdminClient(); + $client->request('PATCH', self::iri('profiles', $profile->getId()), [ + 'headers' => ['Content-Type' => 'application/merge-patch+json'], + 'json' => ['firstName' => 'Alice modifiée'], + ]); + + $this->assertResponseIsSuccessful(); + $this->assertJsonContains(['firstName' => 'Alice modifiée']); + } + + public function testDeleteAsAdmin(): void + { + $profile = $this->createProfile(firstName: 'ToDelete', lastName: 'User'); + + $client = $this->createAdminClient(); + $client->request('DELETE', self::iri('profiles', $profile->getId())); + + $this->assertResponseStatusCodeSame(204); + } + + public function testUnauthenticatedAccess(): void + { + $profile = $this->createProfile(); + + $client = $this->createUnauthenticatedClient(); + $client->request('GET', self::iri('profiles', $profile->getId())); + + $this->assertResponseStatusCodeSame(401); + } +} diff --git a/tests/Api/Entity/SiteTest.php b/tests/Api/Entity/SiteTest.php new file mode 100644 index 0000000..aa48b1c --- /dev/null +++ b/tests/Api/Entity/SiteTest.php @@ -0,0 +1,124 @@ +createSite('Usine Nord'); + $this->createSite('Usine Sud'); + + $client = $this->createViewerClient(); + $client->request('GET', '/api/sites'); + + $this->assertResponseIsSuccessful(); + $this->assertJsonContainsHydraCollection(); + $this->assertJsonContains(['totalItems' => 2]); + } + + public function testGetItem(): void + { + $site = $this->createSite('Usine Nord', ['contactCity' => 'Lyon']); + + $client = $this->createViewerClient(); + $client->request('GET', self::iri('sites', $site->getId())); + + $this->assertResponseIsSuccessful(); + $this->assertJsonContains([ + 'name' => 'Usine Nord', + 'contactCity' => 'Lyon', + ]); + } + + public function testPost(): void + { + $client = $this->createGestionnaireClient(); + $client->request('POST', '/api/sites', [ + 'headers' => ['Content-Type' => 'application/ld+json'], + 'json' => [ + 'name' => 'Usine Est', + 'contactName' => 'Jean Dupont', + 'contactCity' => 'Strasbourg', + ], + ]); + + $this->assertResponseStatusCodeSame(201); + $this->assertJsonContains(['name' => 'Usine Est']); + } + + public function testPut(): void + { + $site = $this->createSite('Usine Nord'); + + $client = $this->createGestionnaireClient(); + $client->request('PUT', self::iri('sites', $site->getId()), [ + 'headers' => ['Content-Type' => 'application/ld+json'], + 'json' => ['name' => 'Usine Nord Renommée'], + ]); + + $this->assertResponseIsSuccessful(); + $this->assertJsonContains(['name' => 'Usine Nord Renommée']); + } + + public function testPatch(): void + { + $site = $this->createSite('Usine Nord'); + + $client = $this->createGestionnaireClient(); + $client->request('PATCH', self::iri('sites', $site->getId()), [ + 'headers' => ['Content-Type' => 'application/merge-patch+json'], + 'json' => ['contactPhone' => '0612345678'], + ]); + + $this->assertResponseIsSuccessful(); + $this->assertJsonContains(['contactPhone' => '0612345678']); + } + + public function testDelete(): void + { + $site = $this->createSite('ToDelete'); + + $client = $this->createGestionnaireClient(); + $client->request('DELETE', self::iri('sites', $site->getId())); + + $this->assertResponseStatusCodeSame(204); + } + + public function testUnauthenticatedAccess(): void + { + $client = $this->createUnauthenticatedClient(); + $client->request('GET', '/api/sites'); + + $this->assertResponseStatusCodeSame(401); + } + + public function testViewerCannotWrite(): void + { + $client = $this->createViewerClient(); + $client->request('POST', '/api/sites', [ + 'headers' => ['Content-Type' => 'application/ld+json'], + 'json' => ['name' => 'Blocked'], + ]); + + $this->assertResponseStatusCodeSame(403); + } + + public function testValidationNameRequired(): void + { + $client = $this->createGestionnaireClient(); + $client->request('POST', '/api/sites', [ + 'headers' => ['Content-Type' => 'application/ld+json'], + 'json' => ['contactCity' => 'Paris'], + ]); + + $this->assertResponseStatusCodeSame(422); + } +} diff --git a/tests/Api/FilterTest.php b/tests/Api/FilterTest.php new file mode 100644 index 0000000..bbb7be6 --- /dev/null +++ b/tests/Api/FilterTest.php @@ -0,0 +1,112 @@ +createComposant('Pompe hydraulique'); + $this->createComposant('Vanne de contrôle'); + $this->createComposant('Pompe centrifuge'); + + $client = $this->createViewerClient(); + $client->request('GET', '/api/composants?name=pompe'); + + $this->assertResponseIsSuccessful(); + $this->assertJsonContains(['totalItems' => 2]); + } + + public function testSearchFilterOnReference(): void + { + $this->createPiece('Joint A', 'REF-ALPHA'); + $this->createPiece('Joint B', 'REF-BETA'); + + $client = $this->createViewerClient(); + $client->request('GET', '/api/pieces?reference=alpha'); + + $this->assertResponseIsSuccessful(); + $this->assertJsonContains(['totalItems' => 1]); + } + + public function testOrderFilterAscending(): void + { + $this->createProduct('Zebra'); + $this->createProduct('Alpha'); + + $client = $this->createViewerClient(); + $response = $client->request('GET', '/api/products?order[name]=asc'); + + $this->assertResponseIsSuccessful(); + $data = $response->toArray(); + $this->assertSame('Alpha', $data['member'][0]['name']); + } + + public function testOrderFilterDescending(): void + { + $this->createProduct('Alpha'); + $this->createProduct('Zebra'); + + $client = $this->createViewerClient(); + $response = $client->request('GET', '/api/products?order[name]=desc'); + + $this->assertResponseIsSuccessful(); + $data = $response->toArray(); + $this->assertSame('Zebra', $data['member'][0]['name']); + } + + public function testExactFilterOnCategory(): void + { + $this->createModelType('Pompe', 'POMPE-001', ModelCategory::COMPONENT); + $this->createModelType('Joint', 'JOINT-001', ModelCategory::PIECE); + $this->createModelType('Huile', 'HUILE-001', ModelCategory::PRODUCT); + + $client = $this->createViewerClient(); + $client->request('GET', '/api/model_types?category=PIECE'); + + $this->assertResponseIsSuccessful(); + $this->assertJsonContains(['totalItems' => 1]); + } + + public function testExistsFilterOnDocuments(): void + { + $machine = $this->createMachine('Machine A'); + $site = $this->createSite('Site B'); + + $doc1 = new Document(); + $doc1->setName('doc1'); + $doc1->setFilename('f1.pdf'); + $doc1->setPath('/uploads/f1.pdf'); + $doc1->setMimeType('application/pdf'); + $doc1->setSize(100); + $doc1->setMachine($machine); + + $doc2 = new Document(); + $doc2->setName('doc2'); + $doc2->setFilename('f2.pdf'); + $doc2->setPath('/uploads/f2.pdf'); + $doc2->setMimeType('application/pdf'); + $doc2->setSize(200); + $doc2->setSite($site); + + $em = $this->getEntityManager(); + $em->persist($doc1); + $em->persist($doc2); + $em->flush(); + + $client = $this->createViewerClient(); + $client->request('GET', '/api/documents?exists[machine]=true'); + + $this->assertResponseIsSuccessful(); + $this->assertJsonContains(['totalItems' => 1]); + } +} diff --git a/tests/Api/HealthCheckTest.php b/tests/Api/HealthCheckTest.php new file mode 100644 index 0000000..015e9b4 --- /dev/null +++ b/tests/Api/HealthCheckTest.php @@ -0,0 +1,43 @@ +createUnauthenticatedClient(); + $client->request('GET', '/api/health'); + + $this->assertResponseIsSuccessful(); + $this->assertJsonContains(['status' => 'ok']); + + $data = $client->getResponse()->toArray(); + $this->assertArrayNotHasKey('php', $data); + $this->assertArrayNotHasKey('checks', $data); + $this->assertArrayNotHasKey('memory_mb', $data); + } + + public function testHealthEndpointAdminReturnsFullInfo(): void + { + $client = $this->createAdminClient(); + $client->request('GET', '/api/health'); + + $this->assertResponseIsSuccessful(); + $this->assertJsonContains([ + 'status' => 'ok', + 'checks' => [ + 'database' => [ + 'status' => 'ok', + ], + ], + ]); + } +} diff --git a/tests/Api/PaginationTest.php b/tests/Api/PaginationTest.php new file mode 100644 index 0000000..d503a93 --- /dev/null +++ b/tests/Api/PaginationTest.php @@ -0,0 +1,71 @@ +createConstructeur("Constructeur {$i}"); + } + + $client = $this->createViewerClient(); + $client->request('GET', '/api/constructeurs'); + + $this->assertResponseIsSuccessful(); + $data = $client->getResponse()->toArray(); + $this->assertSame(35, $data['totalItems']); + $this->assertCount(30, $data['member']); + } + + public function testCustomItemsPerPage(): void + { + for ($i = 1; $i <= 15; ++$i) { + $this->createConstructeur("Constructeur {$i}"); + } + + $client = $this->createViewerClient(); + $client->request('GET', '/api/constructeurs?itemsPerPage=5'); + + $this->assertResponseIsSuccessful(); + $data = $client->getResponse()->toArray(); + $this->assertCount(5, $data['member']); + $this->assertSame(15, $data['totalItems']); + } + + public function testPageNavigation(): void + { + for ($i = 1; $i <= 15; ++$i) { + $this->createConstructeur("Constructeur {$i}"); + } + + $client = $this->createViewerClient(); + $client->request('GET', '/api/constructeurs?itemsPerPage=5&page=2'); + + $this->assertResponseIsSuccessful(); + $data = $client->getResponse()->toArray(); + $this->assertCount(5, $data['member']); + } + + public function testMaxItemsPerPageIsCapped(): void + { + for ($i = 1; $i <= 5; ++$i) { + $this->createConstructeur("Constructeur {$i}"); + } + + $client = $this->createViewerClient(); + $client->request('GET', '/api/constructeurs?itemsPerPage=999'); + + $this->assertResponseIsSuccessful(); + $data = $client->getResponse()->toArray(); + $this->assertCount(5, $data['member']); + } +} diff --git a/tests/Api/Session/SessionProfileTest.php b/tests/Api/Session/SessionProfileTest.php new file mode 100644 index 0000000..7102945 --- /dev/null +++ b/tests/Api/Session/SessionProfileTest.php @@ -0,0 +1,138 @@ +createProfile(password: self::PASSWORD); + $client = static::createClient(); + + $client->request('POST', '/api/session/profile', [ + 'json' => [ + 'profileId' => $profile->getId(), + 'password' => self::PASSWORD, + ], + ]); + + $this->assertResponseStatusCodeSame(200); + $this->assertJsonContains([ + 'id' => $profile->getId(), + 'firstName' => 'Test', + 'lastName' => 'User', + 'isActive' => true, + ]); + } + + public function testLoginWrongPassword(): void + { + $profile = $this->createProfile(password: self::PASSWORD); + $client = static::createClient(); + + $client->request('POST', '/api/session/profile', [ + 'json' => [ + 'profileId' => $profile->getId(), + 'password' => 'wrong', + ], + ]); + + $this->assertResponseStatusCodeSame(401); + $this->assertJsonContains(['message' => 'Mot de passe incorrect.']); + } + + public function testLoginMissingPassword(): void + { + $profile = $this->createProfile(password: self::PASSWORD); + $client = static::createClient(); + + $client->request('POST', '/api/session/profile', [ + 'json' => [ + 'profileId' => $profile->getId(), + ], + ]); + + $this->assertResponseStatusCodeSame(400); + $this->assertJsonContains(['message' => 'Mot de passe requis.']); + } + + public function testLoginMissingProfileId(): void + { + $client = static::createClient(); + $client->request('POST', '/api/session/profile', [ + 'json' => [], + ]); + + $this->assertResponseStatusCodeSame(400); + $this->assertJsonContains(['message' => 'profileId est requis.']); + } + + public function testLoginInactiveProfile(): void + { + $profile = $this->createProfile(password: self::PASSWORD, isActive: false); + $client = static::createClient(); + + $client->request('POST', '/api/session/profile', [ + 'json' => [ + 'profileId' => $profile->getId(), + 'password' => self::PASSWORD, + ], + ]); + + $this->assertResponseStatusCodeSame(401); + } + + public function testLoginNoPasswordSet(): void + { + $profile = $this->createProfile(); + $client = static::createClient(); + + $client->request('POST', '/api/session/profile', [ + 'json' => [ + 'profileId' => $profile->getId(), + 'password' => 'anything', + ], + ]); + + $this->assertResponseStatusCodeSame(403); + } + + public function testGetActiveProfileAuthenticated(): void + { + $client = $this->createViewerClient(); + $client->request('GET', '/api/session/profile'); + + $this->assertResponseIsSuccessful(); + $this->assertJsonContains(['isActive' => true]); + } + + public function testGetActiveProfileUnauthenticated(): void + { + $client = $this->createUnauthenticatedClient(); + $client->request('GET', '/api/session/profile'); + + $this->assertResponseStatusCodeSame(401); + $this->assertJsonContains(['message' => 'Aucun profil actif.']); + } + + public function testLogout(): void + { + $client = $this->createViewerClient(); + + $client->request('DELETE', '/api/session/profile'); + $this->assertResponseIsSuccessful(); + $this->assertJsonContains(['success' => true]); + + $client->request('GET', '/api/session/profile'); + $this->assertResponseStatusCodeSame(401); + } +} diff --git a/tests/Api/ValidationTest.php b/tests/Api/ValidationTest.php new file mode 100644 index 0000000..58b486c --- /dev/null +++ b/tests/Api/ValidationTest.php @@ -0,0 +1,70 @@ +createGestionnaireClient(); + $client->request('POST', '/api/constructeurs', [ + 'headers' => ['Content-Type' => 'application/ld+json'], + 'json' => ['email' => 'no-name@test.com'], + ]); + + $this->assertResponseStatusCodeSame(422); + } + + public function testSiteRequiresName(): void + { + $client = $this->createGestionnaireClient(); + $client->request('POST', '/api/sites', [ + 'headers' => ['Content-Type' => 'application/ld+json'], + 'json' => ['contactCity' => 'Paris'], + ]); + + $this->assertResponseStatusCodeSame(422); + } + + public function testMachineRequiresSite(): void + { + $client = $this->createGestionnaireClient(); + $client->request('POST', '/api/machines', [ + 'headers' => ['Content-Type' => 'application/ld+json'], + 'json' => ['name' => 'Machine sans site'], + ]); + + $this->assertResponseStatusCodeSame(422); + } + + public function testProfileRequiresFirstAndLastName(): void + { + $client = $this->createAdminClient(); + $client->request('POST', '/api/profiles', [ + 'headers' => ['Content-Type' => 'application/ld+json'], + 'json' => ['email' => 'missing@names.com'], + ]); + + $this->assertResponseStatusCodeSame(422); + } + + public function testDuplicateConstructeurName(): void + { + $this->createConstructeur('Siemens'); + + $client = $this->createGestionnaireClient(); + $client->request('POST', '/api/constructeurs', [ + 'headers' => ['Content-Type' => 'application/ld+json'], + 'json' => ['name' => 'Siemens'], + ]); + + $this->assertResponseStatusCodeSame(422); + } +}