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.
This commit is contained in:
@@ -0,0 +1,229 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Tests\Functional\Mcp\Directory;
|
||||
|
||||
use App\Module\Core\Domain\Entity\User;
|
||||
use App\Module\Directory\Domain\Entity\Client;
|
||||
use App\Module\Directory\Domain\Entity\Prestataire;
|
||||
use App\Module\Directory\Domain\Entity\Prospect;
|
||||
use App\Module\Directory\Domain\Repository\AddressRepositoryInterface;
|
||||
use App\Module\Directory\Domain\Repository\ClientRepositoryInterface;
|
||||
use App\Module\Directory\Domain\Repository\PrestataireRepositoryInterface;
|
||||
use App\Module\Directory\Domain\Repository\ProspectRepositoryInterface;
|
||||
use App\Module\Directory\Infrastructure\Mcp\Tool\CreateAddressTool;
|
||||
use App\Module\Directory\Infrastructure\Mcp\Tool\DeleteAddressTool;
|
||||
use App\Module\Directory\Infrastructure\Mcp\Tool\GetAddressTool;
|
||||
use App\Module\Directory\Infrastructure\Mcp\Tool\ListAddressesTool;
|
||||
use App\Module\Directory\Infrastructure\Mcp\Tool\UpdateAddressTool;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use InvalidArgumentException;
|
||||
use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase;
|
||||
use Symfony\Bundle\SecurityBundle\Security;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
class AddressLifecycleTest extends KernelTestCase
|
||||
{
|
||||
private EntityManagerInterface $em;
|
||||
private User $admin;
|
||||
private Client $client;
|
||||
private Prospect $prospect;
|
||||
private Prestataire $prestataire;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
self::bootKernel();
|
||||
$this->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(),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,265 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Tests\Functional\Mcp\Directory;
|
||||
|
||||
use App\Module\Core\Domain\Entity\User;
|
||||
use App\Module\Directory\Domain\Entity\Client;
|
||||
use App\Module\Directory\Domain\Entity\Prestataire;
|
||||
use App\Module\Directory\Domain\Entity\Prospect;
|
||||
use App\Module\Directory\Domain\Enum\ReportType;
|
||||
use App\Module\Directory\Domain\Repository\ClientRepositoryInterface;
|
||||
use App\Module\Directory\Domain\Repository\CommercialReportRepositoryInterface;
|
||||
use App\Module\Directory\Domain\Repository\PrestataireRepositoryInterface;
|
||||
use App\Module\Directory\Domain\Repository\ProspectRepositoryInterface;
|
||||
use App\Module\Directory\Infrastructure\Mcp\Tool\CreateCommercialReportTool;
|
||||
use App\Module\Directory\Infrastructure\Mcp\Tool\DeleteCommercialReportTool;
|
||||
use App\Module\Directory\Infrastructure\Mcp\Tool\GetCommercialReportTool;
|
||||
use App\Module\Directory\Infrastructure\Mcp\Tool\ListCommercialReportsTool;
|
||||
use App\Module\Directory\Infrastructure\Mcp\Tool\UpdateCommercialReportTool;
|
||||
use DateTimeImmutable;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use InvalidArgumentException;
|
||||
use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase;
|
||||
use Symfony\Bundle\SecurityBundle\Security;
|
||||
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface;
|
||||
use Symfony\Component\Security\Core\Authentication\Token\UsernamePasswordToken;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
class CommercialReportLifecycleTest extends KernelTestCase
|
||||
{
|
||||
private EntityManagerInterface $em;
|
||||
private User $admin;
|
||||
private Client $client;
|
||||
private Prospect $prospect;
|
||||
private Prestataire $prestataire;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
self::bootKernel();
|
||||
$this->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(),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,216 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Tests\Functional\Mcp\Directory;
|
||||
|
||||
use App\Module\Core\Domain\Entity\User;
|
||||
use App\Module\Directory\Domain\Entity\Client;
|
||||
use App\Module\Directory\Domain\Entity\Prestataire;
|
||||
use App\Module\Directory\Domain\Entity\Prospect;
|
||||
use App\Module\Directory\Domain\Repository\ClientRepositoryInterface;
|
||||
use App\Module\Directory\Domain\Repository\ContactRepositoryInterface;
|
||||
use App\Module\Directory\Domain\Repository\PrestataireRepositoryInterface;
|
||||
use App\Module\Directory\Domain\Repository\ProspectRepositoryInterface;
|
||||
use App\Module\Directory\Infrastructure\Mcp\Tool\CreateContactTool;
|
||||
use App\Module\Directory\Infrastructure\Mcp\Tool\DeleteContactTool;
|
||||
use App\Module\Directory\Infrastructure\Mcp\Tool\GetContactTool;
|
||||
use App\Module\Directory\Infrastructure\Mcp\Tool\ListContactsTool;
|
||||
use App\Module\Directory\Infrastructure\Mcp\Tool\UpdateContactTool;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use InvalidArgumentException;
|
||||
use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase;
|
||||
use Symfony\Bundle\SecurityBundle\Security;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
class ContactLifecycleTest extends KernelTestCase
|
||||
{
|
||||
private EntityManagerInterface $em;
|
||||
private User $admin;
|
||||
private Client $client;
|
||||
private Prospect $prospect;
|
||||
private Prestataire $prestataire;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
self::bootKernel();
|
||||
$this->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(),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,183 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Tests\Functional\Mcp\Directory;
|
||||
|
||||
use App\Module\Core\Domain\Entity\User;
|
||||
use App\Module\Directory\Domain\Repository\AddressRepositoryInterface;
|
||||
use App\Module\Directory\Domain\Repository\CommercialReportRepositoryInterface;
|
||||
use App\Module\Directory\Domain\Repository\ContactRepositoryInterface;
|
||||
use App\Module\Directory\Domain\Repository\PrestataireRepositoryInterface;
|
||||
use App\Module\Directory\Infrastructure\Mcp\Tool\CreatePrestataireTool;
|
||||
use App\Module\Directory\Infrastructure\Mcp\Tool\DeletePrestataireTool;
|
||||
use App\Module\Directory\Infrastructure\Mcp\Tool\GetPrestataireTool;
|
||||
use App\Module\Directory\Infrastructure\Mcp\Tool\ListPrestatairesTool;
|
||||
use App\Module\Directory\Infrastructure\Mcp\Tool\UpdatePrestataireTool;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use InvalidArgumentException;
|
||||
use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase;
|
||||
use Symfony\Bundle\SecurityBundle\Security;
|
||||
use Symfony\Component\Security\Core\Exception\AccessDeniedException;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
class PrestataireLifecycleTest extends KernelTestCase
|
||||
{
|
||||
private EntityManagerInterface $em;
|
||||
private User $admin;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
self::bootKernel();
|
||||
$this->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),
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user