From aad949c10cb26abc63445c374def64b44d84ffec Mon Sep 17 00:00:00 2001 From: matthieu Date: Wed, 24 Jun 2026 21:08:06 +0200 Subject: [PATCH] test(directory) : tests fonctionnels MCP pour Prestataire/Contact/Address/CommercialReport Couvre les 20 nouveaux outils MCP Directory (5 par entite : create/get/list/ update/delete) avec un focus sur les guards et invariants : - exactly-one-parent (Contact/Address/CommercialReport) - ROLE_ADMIN - ISO 3166 alpha-2 + normalisation uppercase (Address) - enum ReportType + defaults note/today + parsing date (CommercialReport) - author auto-rempli par CommercialReportAuthorListener (token storage) - collections vides dans get-prestataire enrichi - ordre DESC sur occurredAt pour list-commercial-reports - delete renvoie null apres em.clear() 38 tests / 105 assertions. Suite complete passe a 217/217. --- .../Mcp/Directory/AddressLifecycleTest.php | 229 +++++++++++++++ .../CommercialReportLifecycleTest.php | 265 ++++++++++++++++++ .../Mcp/Directory/ContactLifecycleTest.php | 216 ++++++++++++++ .../Directory/PrestataireLifecycleTest.php | 183 ++++++++++++ 4 files changed, 893 insertions(+) create mode 100644 tests/Functional/Mcp/Directory/AddressLifecycleTest.php create mode 100644 tests/Functional/Mcp/Directory/CommercialReportLifecycleTest.php create mode 100644 tests/Functional/Mcp/Directory/ContactLifecycleTest.php create mode 100644 tests/Functional/Mcp/Directory/PrestataireLifecycleTest.php diff --git a/tests/Functional/Mcp/Directory/AddressLifecycleTest.php b/tests/Functional/Mcp/Directory/AddressLifecycleTest.php new file mode 100644 index 0000000..bcd0222 --- /dev/null +++ b/tests/Functional/Mcp/Directory/AddressLifecycleTest.php @@ -0,0 +1,229 @@ +em = self::getContainer()->get(EntityManagerInterface::class); + + $this->admin = new User(); + $this->admin->setUsername('mcp-address-admin-'.uniqid()); + $this->admin->setPassword('x'); + $this->admin->setRoles(['ROLE_ADMIN']); + $this->em->persist($this->admin); + + $this->client = new Client(); + $this->client->setName('Test Client '.uniqid()); + $this->em->persist($this->client); + + $this->prospect = new Prospect(); + $this->prospect->setCompany('Test Prospect '.uniqid()); + $this->em->persist($this->prospect); + + $this->prestataire = new Prestataire(); + $this->prestataire->setName('Test Prestataire '.uniqid()); + $this->em->persist($this->prestataire); + + $this->em->flush(); + } + + public function testCreateRequiresExactlyOneParent(): void + { + try { + ($this->createTool())(null, null, null, 'Home'); + self::fail('Expected error when no parent provided.'); + } catch (InvalidArgumentException $e) { + self::assertStringContainsString('Exactly one of clientId, prospectId or prestataireId', $e->getMessage()); + } + + try { + ($this->createTool())($this->client->getId(), null, $this->prestataire->getId(), 'Dup'); + self::fail('Expected error when two parents provided.'); + } catch (InvalidArgumentException $e) { + self::assertStringContainsString('Exactly one of clientId, prospectId or prestataireId', $e->getMessage()); + } + } + + public function testCreateCountryDefaultsToFRWhenOmitted(): void + { + $data = json_decode(($this->createTool())($this->client->getId(), null, null, 'HQ'), true); + + self::assertSame('FR', $data['country']); + } + + public function testCreateRejectsNonIso3166Country(): void + { + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage('country must be a 2-letter ISO 3166 alpha-2 code'); + ($this->createTool())($this->client->getId(), null, null, 'HQ', null, null, null, null, 'France'); + } + + public function testCreateNormalizesCountryToUppercase(): void + { + $data = json_decode(($this->createTool())($this->client->getId(), null, null, 'HQ', null, null, null, null, 'be'), true); + + self::assertSame('BE', $data['country']); + } + + public function testCreateOnEachParentWorks(): void + { + $clientAddr = json_decode(($this->createTool())($this->client->getId(), null, null, 'CHQ'), true); + self::assertSame($this->client->getId(), $clientAddr['clientId']); + self::assertNull($clientAddr['prospectId']); + + $prospectAddr = json_decode(($this->createTool())(null, $this->prospect->getId(), null, 'PHQ'), true); + self::assertSame($this->prospect->getId(), $prospectAddr['prospectId']); + + $prestAddr = json_decode(($this->createTool())(null, null, $this->prestataire->getId(), 'XHQ'), true); + self::assertSame($this->prestataire->getId(), $prestAddr['prestataireId']); + } + + public function testGetReturnsAddress(): void + { + $created = json_decode(($this->createTool())($this->client->getId(), null, null, 'Office', '1 rue X', null, '75001', 'Paris', 'FR'), true); + + $data = json_decode(($this->getTool())((int) $created['id']), true); + + self::assertSame('Office', $data['label']); + self::assertSame('1 rue X', $data['street']); + self::assertSame('75001', $data['postalCode']); + self::assertSame('Paris', $data['city']); + self::assertSame('FR', $data['country']); + } + + public function testListFilteredByClient(): void + { + ($this->createTool())($this->client->getId(), null, null, 'A'); + ($this->createTool())($this->client->getId(), null, null, 'B'); + ($this->createTool())(null, null, $this->prestataire->getId(), 'Z'); + + $data = json_decode(($this->listTool())($this->client->getId(), null, null), true); + + self::assertCount(2, $data); + self::assertSame('A', $data[0]['label']); + self::assertSame('B', $data[1]['label']); + } + + public function testUpdateRejectsNonIso3166Country(): void + { + $created = json_decode(($this->createTool())($this->client->getId(), null, null, 'X'), true); + + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage('country must be a 2-letter ISO 3166 alpha-2 code'); + ($this->updateTool())((int) $created['id'], null, null, null, null, null, 'Belgium'); + } + + public function testUpdateOnlyTouchesProvidedFields(): void + { + $created = json_decode(($this->createTool())($this->client->getId(), null, null, 'Old', '1 rue X', null, '75001', 'Paris', 'FR'), true); + + $data = json_decode(($this->updateTool())((int) $created['id'], 'New', null, null, '75002', null, 'be'), true); + + self::assertSame('New', $data['label']); // changed + self::assertSame('1 rue X', $data['street']); // unchanged + self::assertSame('75002', $data['postalCode']); // changed + self::assertSame('Paris', $data['city']); // unchanged + self::assertSame('BE', $data['country']); // changed + uppercased + } + + public function testDeleteRemovesAddress(): void + { + $created = json_decode(($this->createTool())($this->client->getId(), null, null, 'Bye'), true); + $id = (int) $created['id']; + + $data = json_decode(($this->deleteTool())($id), true); + + self::assertTrue($data['success']); + + $this->em->clear(); + self::assertNull(self::getContainer()->get(AddressRepositoryInterface::class)->findById($id)); + } + + private function securityFor(bool $admin = true): Security + { + $security = $this->createMock(Security::class); + $security->method('isGranted')->willReturn($admin); + $security->method('getUser')->willReturn($admin ? $this->admin : null); + + return $security; + } + + private function createTool(): CreateAddressTool + { + $c = self::getContainer(); + + return new CreateAddressTool( + $this->em, + $c->get(ClientRepositoryInterface::class), + $c->get(ProspectRepositoryInterface::class), + $c->get(PrestataireRepositoryInterface::class), + $this->securityFor(), + ); + } + + private function getTool(): GetAddressTool + { + return new GetAddressTool( + self::getContainer()->get(AddressRepositoryInterface::class), + $this->securityFor(), + ); + } + + private function listTool(): ListAddressesTool + { + return new ListAddressesTool( + self::getContainer()->get(AddressRepositoryInterface::class), + $this->securityFor(), + ); + } + + private function updateTool(): UpdateAddressTool + { + return new UpdateAddressTool( + self::getContainer()->get(AddressRepositoryInterface::class), + $this->em, + $this->securityFor(), + ); + } + + private function deleteTool(): DeleteAddressTool + { + return new DeleteAddressTool( + self::getContainer()->get(AddressRepositoryInterface::class), + $this->em, + $this->securityFor(), + ); + } +} diff --git a/tests/Functional/Mcp/Directory/CommercialReportLifecycleTest.php b/tests/Functional/Mcp/Directory/CommercialReportLifecycleTest.php new file mode 100644 index 0000000..74c6282 --- /dev/null +++ b/tests/Functional/Mcp/Directory/CommercialReportLifecycleTest.php @@ -0,0 +1,265 @@ +em = self::getContainer()->get(EntityManagerInterface::class); + + $this->admin = new User(); + $this->admin->setUsername('mcp-report-admin-'.uniqid()); + $this->admin->setPassword('x'); + $this->admin->setRoles(['ROLE_ADMIN']); + $this->em->persist($this->admin); + + $this->client = new Client(); + $this->client->setName('Test Client '.uniqid()); + $this->em->persist($this->client); + + $this->prospect = new Prospect(); + $this->prospect->setCompany('Test Prospect '.uniqid()); + $this->em->persist($this->prospect); + + $this->prestataire = new Prestataire(); + $this->prestataire->setName('Test Prestataire '.uniqid()); + $this->em->persist($this->prestataire); + + $this->em->flush(); + } + + public function testCreateRequiresExactlyOneParent(): void + { + try { + ($this->createTool())('subject', null, null, null); + self::fail('Expected error when no parent provided.'); + } catch (InvalidArgumentException $e) { + self::assertStringContainsString('Exactly one of clientId, prospectId or prestataireId', $e->getMessage()); + } + + try { + ($this->createTool())('subject', $this->client->getId(), null, $this->prestataire->getId()); + self::fail('Expected error when two parents provided.'); + } catch (InvalidArgumentException $e) { + self::assertStringContainsString('Exactly one of clientId, prospectId or prestataireId', $e->getMessage()); + } + } + + public function testCreateRejectsInvalidType(): void + { + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage('Invalid type "lunch". Allowed: note, call, meeting, email.'); + ($this->createTool())('Lunch at noon', $this->client->getId(), null, null, null, null, 'lunch'); + } + + public function testCreateAcceptsAllValidTypes(): void + { + foreach (['note', 'call', 'meeting', 'email'] as $type) { + $data = json_decode( + ($this->createTool())('subject', $this->client->getId(), null, null, null, '2026-01-15', $type), + true, + ); + self::assertSame($type, $data['type']); + } + } + + public function testCreateDefaultsTypeToNote(): void + { + $data = json_decode(($this->createTool())('subject', $this->client->getId(), null, null), true); + + self::assertSame(ReportType::Note->value, $data['type']); + } + + public function testCreateRejectsInvalidDate(): void + { + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage('Invalid occurredAt "not-a-date"'); + ($this->createTool())('subject', $this->client->getId(), null, null, null, 'not-a-date'); + } + + public function testCreateDefaultsOccurredAtToToday(): void + { + $data = json_decode(($this->createTool())('subject', $this->client->getId(), null, null), true); + + self::assertSame(new DateTimeImmutable('today')->format('Y-m-d'), $data['occurredAt']); + } + + public function testCreateAutoFillsAuthorFromCurrentUser(): void + { + $this->loginAdmin(); + + $data = json_decode(($this->createTool())('subject', $this->client->getId(), null, null), true); + + self::assertNotNull($data['author']); + self::assertSame($this->admin->getId(), $data['author']['id']); + self::assertSame($this->admin->getUsername(), $data['author']['username']); + } + + public function testGetReturnsReport(): void + { + $created = json_decode( + ($this->createTool())('My subject', $this->prestataire->getId() ? null : null, null, $this->prestataire->getId(), 'body text', '2026-03-01', 'meeting'), + true, + ); + + $data = json_decode(($this->getTool())((int) $created['id']), true); + + self::assertSame('My subject', $data['subject']); + self::assertSame('body text', $data['body']); + self::assertSame('2026-03-01', $data['occurredAt']); + self::assertSame('meeting', $data['type']); + self::assertSame($this->prestataire->getId(), $data['prestataireId']); + self::assertSame([], $data['documents']); + } + + public function testListOrderedByOccurredAtDesc(): void + { + ($this->createTool())('oldest', $this->client->getId(), null, null, null, '2026-01-01'); + ($this->createTool())('newest', $this->client->getId(), null, null, null, '2026-12-01'); + ($this->createTool())('middle', $this->client->getId(), null, null, null, '2026-06-15'); + + $data = json_decode(($this->listTool())($this->client->getId(), null, null), true); + + self::assertCount(3, $data); + self::assertSame('newest', $data[0]['subject']); + self::assertSame('middle', $data[1]['subject']); + self::assertSame('oldest', $data[2]['subject']); + } + + public function testListRejectsMultipleFilters(): void + { + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage('At most one of clientId, prospectId or prestataireId'); + ($this->listTool())($this->client->getId(), $this->prospect->getId(), null); + } + + public function testUpdateChangesTypeAndDate(): void + { + $created = json_decode(($this->createTool())('s', $this->client->getId(), null, null, null, '2026-01-01', 'note'), true); + + $data = json_decode(($this->updateTool())((int) $created['id'], 'new subject', null, '2026-02-02', 'call'), true); + + self::assertSame('new subject', $data['subject']); + self::assertSame('2026-02-02', $data['occurredAt']); + self::assertSame('call', $data['type']); + } + + public function testUpdateRejectsInvalidType(): void + { + $created = json_decode(($this->createTool())('s', $this->client->getId(), null, null), true); + + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage('Invalid type "lunch"'); + ($this->updateTool())((int) $created['id'], null, null, null, 'lunch'); + } + + public function testDeleteRemovesReport(): void + { + $created = json_decode(($this->createTool())('Bye', $this->client->getId(), null, null), true); + $id = (int) $created['id']; + + $data = json_decode(($this->deleteTool())($id), true); + + self::assertTrue($data['success']); + + $this->em->clear(); + self::assertNull(self::getContainer()->get(CommercialReportRepositoryInterface::class)->findById($id)); + } + + private function loginAdmin(): void + { + $token = new UsernamePasswordToken($this->admin, 'main', $this->admin->getRoles()); + self::getContainer()->get(TokenStorageInterface::class)->setToken($token); + } + + private function securityFor(bool $admin = true): Security + { + $security = $this->createMock(Security::class); + $security->method('isGranted')->willReturn($admin); + $security->method('getUser')->willReturn($admin ? $this->admin : null); + + return $security; + } + + private function createTool(): CreateCommercialReportTool + { + $c = self::getContainer(); + + return new CreateCommercialReportTool( + $this->em, + $c->get(ClientRepositoryInterface::class), + $c->get(ProspectRepositoryInterface::class), + $c->get(PrestataireRepositoryInterface::class), + $this->securityFor(), + ); + } + + private function getTool(): GetCommercialReportTool + { + return new GetCommercialReportTool( + self::getContainer()->get(CommercialReportRepositoryInterface::class), + $this->securityFor(), + ); + } + + private function listTool(): ListCommercialReportsTool + { + return new ListCommercialReportsTool( + self::getContainer()->get(CommercialReportRepositoryInterface::class), + $this->securityFor(), + ); + } + + private function updateTool(): UpdateCommercialReportTool + { + return new UpdateCommercialReportTool( + self::getContainer()->get(CommercialReportRepositoryInterface::class), + $this->em, + $this->securityFor(), + ); + } + + private function deleteTool(): DeleteCommercialReportTool + { + return new DeleteCommercialReportTool( + self::getContainer()->get(CommercialReportRepositoryInterface::class), + $this->em, + $this->securityFor(), + ); + } +} diff --git a/tests/Functional/Mcp/Directory/ContactLifecycleTest.php b/tests/Functional/Mcp/Directory/ContactLifecycleTest.php new file mode 100644 index 0000000..5099aeb --- /dev/null +++ b/tests/Functional/Mcp/Directory/ContactLifecycleTest.php @@ -0,0 +1,216 @@ +em = self::getContainer()->get(EntityManagerInterface::class); + + $this->admin = new User(); + $this->admin->setUsername('mcp-contact-admin-'.uniqid()); + $this->admin->setPassword('x'); + $this->admin->setRoles(['ROLE_ADMIN']); + $this->em->persist($this->admin); + + $this->client = new Client(); + $this->client->setName('Test Client '.uniqid()); + $this->em->persist($this->client); + + $this->prospect = new Prospect(); + $this->prospect->setCompany('Test Prospect '.uniqid()); + $this->em->persist($this->prospect); + + $this->prestataire = new Prestataire(); + $this->prestataire->setName('Test Prestataire '.uniqid()); + $this->em->persist($this->prestataire); + + $this->em->flush(); + } + + public function testCreateRequiresExactlyOneParent(): void + { + try { + ($this->createTool())(null, null, null, 'Anon'); + self::fail('Expected error when no parent provided.'); + } catch (InvalidArgumentException $e) { + self::assertStringContainsString('Exactly one of clientId, prospectId or prestataireId', $e->getMessage()); + } + + try { + ($this->createTool())($this->client->getId(), $this->prospect->getId(), null, 'Dup'); + self::fail('Expected error when two parents provided.'); + } catch (InvalidArgumentException $e) { + self::assertStringContainsString('Exactly one of clientId, prospectId or prestataireId', $e->getMessage()); + } + } + + public function testCreateWithUnknownClientThrows(): void + { + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage('Client with ID 999999 not found.'); + ($this->createTool())(999999, null, null, 'Anon'); + } + + public function testCreateOnEachParentWorks(): void + { + foreach ( + [ + ['clientId', $this->client->getId()], + ['prospectId', $this->prospect->getId()], + ['prestataireId', $this->prestataire->getId()], + ] as [$field, $id] + ) { + $args = [null, null, null, 'John', 'Doe-'.$field, 'CTO', 'john@x.test']; + $idx = ['clientId' => 0, 'prospectId' => 1, 'prestataireId' => 2][$field]; + $args[$idx] = $id; + + $data = json_decode(($this->createTool())(...$args), true); + self::assertSame('Doe-'.$field, $data['lastName']); + self::assertSame($id, $data[$field]); + } + } + + public function testGetReturnsContact(): void + { + $created = json_decode(($this->createTool())($this->client->getId(), null, null, 'Jane', 'Smith'), true); + + $data = json_decode(($this->getTool())((int) $created['id']), true); + + self::assertSame('Jane', $data['firstName']); + self::assertSame('Smith', $data['lastName']); + self::assertSame($this->client->getId(), $data['clientId']); + } + + public function testListFilteredByPrestataire(): void + { + ($this->createTool())(null, null, $this->prestataire->getId(), 'A', 'A-Last'); + ($this->createTool())(null, null, $this->prestataire->getId(), 'B', 'B-Last'); + ($this->createTool())($this->client->getId(), null, null, 'Z', 'Z-Last'); + + $data = json_decode(($this->listTool())(null, null, $this->prestataire->getId()), true); + + self::assertCount(2, $data); + self::assertSame('A-Last', $data[0]['lastName']); + self::assertSame('B-Last', $data[1]['lastName']); + } + + public function testListRejectsMultipleFilters(): void + { + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage('At most one of clientId, prospectId or prestataireId'); + ($this->listTool())($this->client->getId(), $this->prospect->getId(), null); + } + + public function testUpdateOnlyTouchesProvidedFields(): void + { + $created = json_decode(($this->createTool())(null, null, $this->prestataire->getId(), 'Old', 'Last', 'CTO', 'old@x.test'), true); + + $data = json_decode(($this->updateTool())((int) $created['id'], 'New', null, null, 'new@x.test'), true); + + self::assertSame('New', $data['firstName']); // changed + self::assertSame('Last', $data['lastName']); // unchanged + self::assertSame('CTO', $data['jobTitle']); // unchanged + self::assertSame('new@x.test', $data['email']); // changed + } + + public function testDeleteRemovesContact(): void + { + $created = json_decode(($this->createTool())($this->client->getId(), null, null, 'Bye'), true); + $id = (int) $created['id']; + + $data = json_decode(($this->deleteTool())($id), true); + + self::assertTrue($data['success']); + + $this->em->clear(); + self::assertNull(self::getContainer()->get(ContactRepositoryInterface::class)->findById($id)); + } + + private function securityFor(bool $admin = true): Security + { + $security = $this->createMock(Security::class); + $security->method('isGranted')->willReturn($admin); + $security->method('getUser')->willReturn($admin ? $this->admin : null); + + return $security; + } + + private function createTool(): CreateContactTool + { + $c = self::getContainer(); + + return new CreateContactTool( + $this->em, + $c->get(ClientRepositoryInterface::class), + $c->get(ProspectRepositoryInterface::class), + $c->get(PrestataireRepositoryInterface::class), + $this->securityFor(), + ); + } + + private function getTool(): GetContactTool + { + return new GetContactTool( + self::getContainer()->get(ContactRepositoryInterface::class), + $this->securityFor(), + ); + } + + private function listTool(): ListContactsTool + { + return new ListContactsTool( + self::getContainer()->get(ContactRepositoryInterface::class), + $this->securityFor(), + ); + } + + private function updateTool(): UpdateContactTool + { + return new UpdateContactTool( + self::getContainer()->get(ContactRepositoryInterface::class), + $this->em, + $this->securityFor(), + ); + } + + private function deleteTool(): DeleteContactTool + { + return new DeleteContactTool( + self::getContainer()->get(ContactRepositoryInterface::class), + $this->em, + $this->securityFor(), + ); + } +} diff --git a/tests/Functional/Mcp/Directory/PrestataireLifecycleTest.php b/tests/Functional/Mcp/Directory/PrestataireLifecycleTest.php new file mode 100644 index 0000000..7219afd --- /dev/null +++ b/tests/Functional/Mcp/Directory/PrestataireLifecycleTest.php @@ -0,0 +1,183 @@ +em = self::getContainer()->get(EntityManagerInterface::class); + + $this->admin = new User(); + $this->admin->setUsername('mcp-prest-admin-'.uniqid()); + $this->admin->setPassword('x'); + $this->admin->setRoles(['ROLE_ADMIN']); + $this->em->persist($this->admin); + $this->em->flush(); + } + + public function testCreatePersistsAllFields(): void + { + $json = ($this->createTool(admin: true))('ACME Cleaning', 'contact@acme.example', '+33100000000', 'https://acme.example'); + $data = json_decode($json, true); + + self::assertIsInt($data['id']); + self::assertSame('ACME Cleaning', $data['name']); + self::assertSame('contact@acme.example', $data['email']); + self::assertSame('+33100000000', $data['phone']); + self::assertSame('https://acme.example', $data['website']); + } + + public function testCreateRequiresAdmin(): void + { + $this->expectException(AccessDeniedException::class); + ($this->createTool(admin: false))('Should not pass'); + } + + public function testGetReturnsEmptyCollectionsWhenNoChildren(): void + { + $created = json_decode(($this->createTool(admin: true))('Lonely Prest'), true); + + $json = ($this->getTool(admin: true))((int) $created['id']); + $data = json_decode($json, true); + + self::assertSame($created['id'], $data['id']); + self::assertSame([], $data['contacts']); + self::assertSame([], $data['addresses']); + self::assertSame([], $data['reports']); + } + + public function testGetUnknownIdThrows(): void + { + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage('Prestataire with ID 999999 not found.'); + ($this->getTool(admin: true))(999999); + } + + public function testUpdateOnlyTouchesProvidedFields(): void + { + $created = json_decode(($this->createTool(admin: true))('Before', 'before@x.test', '+33000000000', 'https://before.test'), true); + + $json = ($this->updateTool(admin: true))((int) $created['id'], null, 'after@x.test', null, null); + $data = json_decode($json, true); + + self::assertSame('Before', $data['name']); // unchanged + self::assertSame('after@x.test', $data['email']); // changed + self::assertSame('+33000000000', $data['phone']); // unchanged + self::assertSame('https://before.test', $data['website']); // unchanged + } + + public function testListReturnsAllPrestatairesOrderedByName(): void + { + // Unique prefix isolates this test from data leaked by prior PHPUnit + // runs (DAMA rollback is not active in this project). + $prefix = 'list-test-'.uniqid().'-'; + ($this->createTool(admin: true))($prefix.'Zeta'); + ($this->createTool(admin: true))($prefix.'Alpha'); + ($this->createTool(admin: true))($prefix.'Mu'); + + $data = json_decode(($this->listTool(admin: true))(), true); + $names = array_values(array_filter( + array_column($data, 'name'), + fn ($n) => str_starts_with((string) $n, $prefix), + )); + + self::assertSame([$prefix.'Alpha', $prefix.'Mu', $prefix.'Zeta'], $names); + } + + public function testDeleteRemovesPrestataire(): void + { + $created = json_decode(($this->createTool(admin: true))('To be removed'), true); + $id = (int) $created['id']; + + $json = ($this->deleteTool(admin: true))($id); + $data = json_decode($json, true); + + self::assertTrue($data['success']); + self::assertStringContainsString('"To be removed"', $data['message']); + + $this->em->clear(); + self::assertNull(self::getContainer()->get(PrestataireRepositoryInterface::class)->findById($id)); + } + + private function securityFor(bool $admin): Security + { + $security = $this->createMock(Security::class); + $security->method('isGranted')->willReturn($admin); + $security->method('getUser')->willReturn($admin ? $this->admin : null); + + return $security; + } + + private function createTool(bool $admin): CreatePrestataireTool + { + return new CreatePrestataireTool( + $this->em, + $this->securityFor($admin), + ); + } + + private function getTool(bool $admin): GetPrestataireTool + { + $c = self::getContainer(); + + return new GetPrestataireTool( + $c->get(PrestataireRepositoryInterface::class), + $c->get(ContactRepositoryInterface::class), + $c->get(AddressRepositoryInterface::class), + $c->get(CommercialReportRepositoryInterface::class), + $this->securityFor($admin), + ); + } + + private function updateTool(bool $admin): UpdatePrestataireTool + { + return new UpdatePrestataireTool( + self::getContainer()->get(PrestataireRepositoryInterface::class), + $this->em, + $this->securityFor($admin), + ); + } + + private function listTool(bool $admin): ListPrestatairesTool + { + return new ListPrestatairesTool( + self::getContainer()->get(PrestataireRepositoryInterface::class), + $this->securityFor($admin), + ); + } + + private function deleteTool(bool $admin): DeletePrestataireTool + { + return new DeletePrestataireTool( + self::getContainer()->get(PrestataireRepositoryInterface::class), + $this->em, + $this->securityFor($admin), + ); + } +}