diff --git a/src/Mcp/Tool/Constructeur/CreateConstructeurTool.php b/src/Mcp/Tool/Constructeur/CreateConstructeurTool.php new file mode 100644 index 0000000..78ad2e3 --- /dev/null +++ b/src/Mcp/Tool/Constructeur/CreateConstructeurTool.php @@ -0,0 +1,46 @@ +requireRole($this->security, 'ROLE_GESTIONNAIRE'); + + $constructeur = new Constructeur(); + $constructeur->setName($name); + $constructeur->setEmail('' !== $email ? $email : null); + $constructeur->setPhone('' !== $phone ? $phone : null); + + $this->em->persist($constructeur); + $this->em->flush(); + + return $this->jsonResponse([ + 'id' => $constructeur->getId(), + 'name' => $constructeur->getName(), + ]); + } +} diff --git a/src/Mcp/Tool/Constructeur/DeleteConstructeurTool.php b/src/Mcp/Tool/Constructeur/DeleteConstructeurTool.php new file mode 100644 index 0000000..2aa07ce --- /dev/null +++ b/src/Mcp/Tool/Constructeur/DeleteConstructeurTool.php @@ -0,0 +1,42 @@ +requireRole($this->security, 'ROLE_GESTIONNAIRE'); + + $constructeur = $this->constructeurs->find($constructeurId); + + if (!$constructeur) { + $this->mcpError('not_found', "Constructeur not found: {$constructeurId}"); + } + + $this->em->remove($constructeur); + $this->em->flush(); + + return $this->jsonResponse(['deleted' => true, 'id' => $constructeurId]); + } +} diff --git a/src/Mcp/Tool/Constructeur/GetConstructeurTool.php b/src/Mcp/Tool/Constructeur/GetConstructeurTool.php new file mode 100644 index 0000000..69647f0 --- /dev/null +++ b/src/Mcp/Tool/Constructeur/GetConstructeurTool.php @@ -0,0 +1,40 @@ +constructeurs->find($constructeurId); + + if (!$constructeur) { + $this->mcpError('not_found', "Constructeur not found: {$constructeurId}"); + } + + return $this->jsonResponse([ + 'id' => $constructeur->getId(), + 'name' => $constructeur->getName(), + 'email' => $constructeur->getEmail(), + 'phone' => $constructeur->getPhone(), + 'createdAt' => $constructeur->getCreatedAt()->format('Y-m-d H:i:s'), + 'updatedAt' => $constructeur->getUpdatedAt()->format('Y-m-d H:i:s'), + ]); + } +} diff --git a/src/Mcp/Tool/Constructeur/ListConstructeursTool.php b/src/Mcp/Tool/Constructeur/ListConstructeursTool.php new file mode 100644 index 0000000..6ebe64d --- /dev/null +++ b/src/Mcp/Tool/Constructeur/ListConstructeursTool.php @@ -0,0 +1,55 @@ +paginationParams($page, $limit); + + $countQb = $this->constructeurs->createQueryBuilder('c') + ->select('COUNT(c.id)') + ; + + $qb = $this->constructeurs->createQueryBuilder('c') + ->select('c.id', 'c.name', 'c.email', 'c.phone') + ->orderBy('c.name', 'ASC') + ; + + if ('' !== $search) { + $countQb->andWhere('LOWER(c.name) LIKE LOWER(:search)') + ->setParameter('search', "%{$search}%") + ; + $qb->andWhere('LOWER(c.name) LIKE LOWER(:search)') + ->setParameter('search', "%{$search}%") + ; + } + + $total = (int) $countQb->getQuery()->getSingleScalarResult(); + + $items = $qb->setFirstResult($p['offset']) + ->setMaxResults($p['limit']) + ->getQuery() + ->getArrayResult() + ; + + return $this->paginatedResponse($items, $total, $p['page'], $p['limit']); + } +} diff --git a/src/Mcp/Tool/Constructeur/UpdateConstructeurTool.php b/src/Mcp/Tool/Constructeur/UpdateConstructeurTool.php new file mode 100644 index 0000000..d136493 --- /dev/null +++ b/src/Mcp/Tool/Constructeur/UpdateConstructeurTool.php @@ -0,0 +1,55 @@ +requireRole($this->security, 'ROLE_GESTIONNAIRE'); + + $constructeur = $this->constructeurs->find($constructeurId); + + if (!$constructeur) { + $this->mcpError('not_found', "Constructeur not found: {$constructeurId}"); + } + + if (null !== $name) { + $constructeur->setName($name); + } + if (null !== $email) { + $constructeur->setEmail($email); + } + if (null !== $phone) { + $constructeur->setPhone($phone); + } + + $this->em->flush(); + + return $this->jsonResponse(['id' => $constructeur->getId(), 'name' => $constructeur->getName()]); + } +} diff --git a/src/Mcp/Tool/Product/CreateProductTool.php b/src/Mcp/Tool/Product/CreateProductTool.php new file mode 100644 index 0000000..22fbcdb --- /dev/null +++ b/src/Mcp/Tool/Product/CreateProductTool.php @@ -0,0 +1,76 @@ +requireRole($this->security, 'ROLE_GESTIONNAIRE'); + + $product = new Product(); + $product->setName($name); + + if ('' !== $reference) { + $product->setReference($reference); + } + if ('' !== $supplierPrice) { + $product->setSupplierPrice($supplierPrice); + } + + if ('' !== $modelTypeId) { + $modelType = $this->modelTypes->find($modelTypeId); + if (!$modelType) { + $this->mcpError('not_found', "ModelType not found: {$modelTypeId}"); + } + $product->setTypeProduct($modelType); + } + + foreach ($constructeurIds as $cId) { + $c = $this->constructeurs->find($cId); + if (!$c) { + $this->mcpError('not_found', "Constructeur not found: {$cId}"); + } + $product->addConstructeur($c); + } + + $this->em->persist($product); + $this->em->flush(); + + return $this->jsonResponse([ + 'id' => $product->getId(), + 'name' => $product->getName(), + ]); + } +} diff --git a/src/Mcp/Tool/Product/DeleteProductTool.php b/src/Mcp/Tool/Product/DeleteProductTool.php new file mode 100644 index 0000000..76395bc --- /dev/null +++ b/src/Mcp/Tool/Product/DeleteProductTool.php @@ -0,0 +1,42 @@ +requireRole($this->security, 'ROLE_GESTIONNAIRE'); + + $product = $this->products->find($productId); + + if (!$product) { + $this->mcpError('not_found', "Product not found: {$productId}"); + } + + $this->em->remove($product); + $this->em->flush(); + + return $this->jsonResponse(['deleted' => true, 'id' => $productId]); + } +} diff --git a/src/Mcp/Tool/Product/GetProductTool.php b/src/Mcp/Tool/Product/GetProductTool.php new file mode 100644 index 0000000..f3cf6ba --- /dev/null +++ b/src/Mcp/Tool/Product/GetProductTool.php @@ -0,0 +1,58 @@ +products->find($productId); + + if (!$product) { + $this->mcpError('not_found', "Product not found: {$productId}"); + } + + $constructeurs = []; + foreach ($product->getConstructeurs() as $c) { + $constructeurs[] = [ + 'id' => $c->getId(), + 'name' => $c->getName(), + ]; + } + + $typeProduct = null; + if ($product->getTypeProduct()) { + $typeProduct = [ + 'id' => $product->getTypeProduct()->getId(), + 'name' => $product->getTypeProduct()->getName(), + ]; + } + + return $this->jsonResponse([ + 'id' => $product->getId(), + 'name' => $product->getName(), + 'reference' => $product->getReference(), + 'supplierPrice' => $product->getSupplierPrice(), + 'typeProduct' => $typeProduct, + 'constructeurs' => $constructeurs, + 'createdAt' => $product->getCreatedAt()->format('Y-m-d H:i:s'), + 'updatedAt' => $product->getUpdatedAt()->format('Y-m-d H:i:s'), + ]); + } +} diff --git a/src/Mcp/Tool/Product/ListProductsTool.php b/src/Mcp/Tool/Product/ListProductsTool.php new file mode 100644 index 0000000..baf68ca --- /dev/null +++ b/src/Mcp/Tool/Product/ListProductsTool.php @@ -0,0 +1,55 @@ +paginationParams($page, $limit); + + $countQb = $this->products->createQueryBuilder('pr') + ->select('COUNT(pr.id)') + ; + + $qb = $this->products->createQueryBuilder('pr') + ->select('pr.id', 'pr.name', 'pr.reference', 'pr.supplierPrice') + ->orderBy('pr.name', 'ASC') + ; + + if ('' !== $search) { + $countQb->andWhere('LOWER(pr.name) LIKE LOWER(:search) OR LOWER(pr.reference) LIKE LOWER(:search)') + ->setParameter('search', "%{$search}%") + ; + $qb->andWhere('LOWER(pr.name) LIKE LOWER(:search) OR LOWER(pr.reference) LIKE LOWER(:search)') + ->setParameter('search', "%{$search}%") + ; + } + + $total = (int) $countQb->getQuery()->getSingleScalarResult(); + + $items = $qb->setFirstResult($p['offset']) + ->setMaxResults($p['limit']) + ->getQuery() + ->getArrayResult() + ; + + return $this->paginatedResponse($items, $total, $p['page'], $p['limit']); + } +} diff --git a/src/Mcp/Tool/Product/UpdateProductTool.php b/src/Mcp/Tool/Product/UpdateProductTool.php new file mode 100644 index 0000000..c3fc9b9 --- /dev/null +++ b/src/Mcp/Tool/Product/UpdateProductTool.php @@ -0,0 +1,89 @@ +requireRole($this->security, 'ROLE_GESTIONNAIRE'); + + $product = $this->products->find($productId); + + if (!$product) { + $this->mcpError('not_found', "Product not found: {$productId}"); + } + + if (null !== $name) { + $product->setName($name); + } + if (null !== $reference) { + $product->setReference($reference); + } + if (null !== $supplierPrice) { + $product->setSupplierPrice($supplierPrice); + } + + if (null !== $modelTypeId) { + if ('' === $modelTypeId) { + $product->setTypeProduct(null); + } else { + $modelType = $this->modelTypes->find($modelTypeId); + if (!$modelType) { + $this->mcpError('not_found', "ModelType not found: {$modelTypeId}"); + } + $product->setTypeProduct($modelType); + } + } + + if (null !== $constructeurIds) { + foreach ($product->getConstructeurs()->toArray() as $existing) { + $product->removeConstructeur($existing); + } + foreach ($constructeurIds as $cId) { + $c = $this->constructeurs->find($cId); + if (!$c) { + $this->mcpError('not_found', "Constructeur not found: {$cId}"); + } + $product->addConstructeur($c); + } + } + + $this->em->flush(); + + return $this->jsonResponse(['id' => $product->getId(), 'name' => $product->getName()]); + } +} diff --git a/src/Mcp/Tool/Site/CreateSiteTool.php b/src/Mcp/Tool/Site/CreateSiteTool.php new file mode 100644 index 0000000..0272239 --- /dev/null +++ b/src/Mcp/Tool/Site/CreateSiteTool.php @@ -0,0 +1,54 @@ +requireRole($this->security, 'ROLE_GESTIONNAIRE'); + + $site = new Site(); + $site->setName($name); + $site->setContactName($contactName); + $site->setContactPhone($contactPhone); + $site->setContactAddress($contactAddress); + $site->setContactPostalCode($contactPostalCode); + $site->setContactCity($contactCity); + $site->setColor($color); + + $this->em->persist($site); + $this->em->flush(); + + return $this->jsonResponse([ + 'id' => $site->getId(), + 'name' => $site->getName(), + ]); + } +} diff --git a/src/Mcp/Tool/Site/DeleteSiteTool.php b/src/Mcp/Tool/Site/DeleteSiteTool.php new file mode 100644 index 0000000..c611684 --- /dev/null +++ b/src/Mcp/Tool/Site/DeleteSiteTool.php @@ -0,0 +1,42 @@ +requireRole($this->security, 'ROLE_GESTIONNAIRE'); + + $site = $this->sites->find($siteId); + + if (!$site) { + $this->mcpError('not_found', "Site not found: {$siteId}"); + } + + $this->em->remove($site); + $this->em->flush(); + + return $this->jsonResponse(['deleted' => true, 'id' => $siteId]); + } +} diff --git a/src/Mcp/Tool/Site/GetSiteTool.php b/src/Mcp/Tool/Site/GetSiteTool.php new file mode 100644 index 0000000..233a354 --- /dev/null +++ b/src/Mcp/Tool/Site/GetSiteTool.php @@ -0,0 +1,44 @@ +sites->find($siteId); + + if (!$site) { + $this->mcpError('not_found', "Site not found: {$siteId}"); + } + + return $this->jsonResponse([ + 'id' => $site->getId(), + 'name' => $site->getName(), + 'contactName' => $site->getContactName(), + 'contactPhone' => $site->getContactPhone(), + 'contactAddress' => $site->getContactAddress(), + 'contactPostalCode' => $site->getContactPostalCode(), + 'contactCity' => $site->getContactCity(), + 'color' => $site->getColor(), + 'createdAt' => $site->getCreatedAt()->format('Y-m-d H:i:s'), + 'updatedAt' => $site->getUpdatedAt()->format('Y-m-d H:i:s'), + ]); + } +} diff --git a/src/Mcp/Tool/Site/ListSitesTool.php b/src/Mcp/Tool/Site/ListSitesTool.php new file mode 100644 index 0000000..f99367f --- /dev/null +++ b/src/Mcp/Tool/Site/ListSitesTool.php @@ -0,0 +1,55 @@ +paginationParams($page, $limit); + + $countQb = $this->sites->createQueryBuilder('s') + ->select('COUNT(s.id)') + ; + + $qb = $this->sites->createQueryBuilder('s') + ->select('s.id', 's.name', 's.contactName', 's.contactCity', 's.contactPhone') + ->orderBy('s.name', 'ASC') + ; + + if ('' !== $search) { + $countQb->andWhere('LOWER(s.name) LIKE LOWER(:search)') + ->setParameter('search', "%{$search}%") + ; + $qb->andWhere('LOWER(s.name) LIKE LOWER(:search)') + ->setParameter('search', "%{$search}%") + ; + } + + $total = (int) $countQb->getQuery()->getSingleScalarResult(); + + $items = $qb->setFirstResult($p['offset']) + ->setMaxResults($p['limit']) + ->getQuery() + ->getArrayResult() + ; + + return $this->paginatedResponse($items, $total, $p['page'], $p['limit']); + } +} diff --git a/src/Mcp/Tool/Site/UpdateSiteTool.php b/src/Mcp/Tool/Site/UpdateSiteTool.php new file mode 100644 index 0000000..010881d --- /dev/null +++ b/src/Mcp/Tool/Site/UpdateSiteTool.php @@ -0,0 +1,71 @@ +requireRole($this->security, 'ROLE_GESTIONNAIRE'); + + $site = $this->sites->find($siteId); + + if (!$site) { + $this->mcpError('not_found', "Site not found: {$siteId}"); + } + + if (null !== $name) { + $site->setName($name); + } + if (null !== $contactName) { + $site->setContactName($contactName); + } + if (null !== $contactPhone) { + $site->setContactPhone($contactPhone); + } + if (null !== $contactAddress) { + $site->setContactAddress($contactAddress); + } + if (null !== $contactPostalCode) { + $site->setContactPostalCode($contactPostalCode); + } + if (null !== $contactCity) { + $site->setContactCity($contactCity); + } + if (null !== $color) { + $site->setColor($color); + } + + $this->em->flush(); + + return $this->jsonResponse(['id' => $site->getId(), 'name' => $site->getName()]); + } +} diff --git a/tests/AbstractApiTestCase.php b/tests/AbstractApiTestCase.php index 7b72f30..6e66eb4 100644 --- a/tests/AbstractApiTestCase.php +++ b/tests/AbstractApiTestCase.php @@ -25,6 +25,7 @@ use App\Entity\Profile; use App\Entity\Site; use App\Enum\ModelCategory; use Doctrine\ORM\EntityManagerInterface; +use stdClass; use Symfony\Component\PasswordHasher\Hasher\UserPasswordHasherInterface; abstract class AbstractApiTestCase extends ApiTestCase @@ -85,6 +86,93 @@ abstract class AbstractApiTestCase extends ApiTestCase return static::createClient(); } + // ── MCP helpers ────────────────────────────────────────────────── + + /** + * @return array{client: Client, sessionId: string} + */ + protected function createMcpClient(string $role = 'ROLE_VIEWER'): array + { + $profile = $this->createProfile(roles: [$role], password: self::DEFAULT_PASSWORD); + + return $this->initMcpSession($profile->getId(), self::DEFAULT_PASSWORD); + } + + /** + * @return array{client: Client, sessionId: string} + */ + protected function initMcpSession(string $profileId, string $password): array + { + $client = static::createClient(); + + $response = $client->request('POST', '/_mcp', [ + 'headers' => [ + 'Content-Type' => 'application/json', + 'X-Profile-Id' => $profileId, + 'X-Profile-Password' => $password, + ], + 'body' => json_encode([ + 'jsonrpc' => '2.0', + 'method' => 'initialize', + 'params' => [ + 'protocolVersion' => '2025-03-26', + 'capabilities' => new stdClass(), + 'clientInfo' => ['name' => 'test', 'version' => '1.0'], + ], + 'id' => 1, + ]), + ]); + + $sessionId = $response->getHeaders()['mcp-session-id'][0] ?? ''; + + $client->request('POST', '/_mcp', [ + 'headers' => [ + 'Content-Type' => 'application/json', + 'X-Profile-Id' => $profileId, + 'X-Profile-Password' => $password, + 'Mcp-Session-Id' => $sessionId, + ], + 'body' => json_encode([ + 'jsonrpc' => '2.0', + 'method' => 'notifications/initialized', + ]), + ]); + + return ['client' => $client, 'sessionId' => $sessionId, 'profileId' => $profileId, 'password' => $password]; + } + + /** + * @return array + */ + protected function callMcpTool(array $session, string $toolName, array $arguments = []): array + { + $response = $session['client']->request('POST', '/_mcp', [ + 'headers' => [ + 'Content-Type' => 'application/json', + 'X-Profile-Id' => $session['profileId'], + 'X-Profile-Password' => $session['password'], + 'Mcp-Session-Id' => $session['sessionId'], + ], + 'body' => json_encode([ + 'jsonrpc' => '2.0', + 'method' => 'tools/call', + 'params' => [ + 'name' => $toolName, + 'arguments' => empty($arguments) ? new stdClass() : $arguments, + ], + 'id' => random_int(10, 9999), + ]), + ]); + + $data = $response->toArray(false); + + if (isset($data['result']['content'][0]['text'])) { + $data['_parsed'] = json_decode($data['result']['content'][0]['text'], true); + } + + return $data; + } + // ── Factory helpers ───────────────────────────────────────────── protected function createProfile( diff --git a/tests/Mcp/Tool/Constructeur/ConstructeursCrudToolTest.php b/tests/Mcp/Tool/Constructeur/ConstructeursCrudToolTest.php new file mode 100644 index 0000000..efb22cf --- /dev/null +++ b/tests/Mcp/Tool/Constructeur/ConstructeursCrudToolTest.php @@ -0,0 +1,85 @@ +createConstructeur(name: 'Constructeur Alpha'); + $this->createConstructeur(name: 'Constructeur Beta'); + $session = $this->createMcpClient('ROLE_VIEWER'); + + $data = $this->callMcpTool($session, 'list_constructeurs'); + + $this->assertArrayHasKey('_parsed', $data, 'MCP response: '.json_encode($data)); + $this->assertGreaterThanOrEqual(2, $data['_parsed']['total']); + } + + public function testGetConstructeur(): void + { + $constructeur = $this->createConstructeur(name: 'Constructeur Gamma'); + $session = $this->createMcpClient('ROLE_VIEWER'); + + $data = $this->callMcpTool($session, 'get_constructeur', ['constructeurId' => $constructeur->getId()]); + + $this->assertArrayHasKey('_parsed', $data); + $this->assertSame('Constructeur Gamma', $data['_parsed']['name']); + } + + public function testCreateConstructeur(): void + { + $session = $this->createMcpClient('ROLE_GESTIONNAIRE'); + + $data = $this->callMcpTool($session, 'create_constructeur', [ + 'name' => 'Constructeur Nouveau', + 'email' => 'contact@nouveau.com', + 'phone' => '+33123456789', + ]); + + $this->assertArrayHasKey('_parsed', $data); + $this->assertSame('Constructeur Nouveau', $data['_parsed']['name']); + $this->assertNotEmpty($data['_parsed']['id']); + } + + public function testCreateConstructeurRequiresGestionnaire(): void + { + $session = $this->createMcpClient('ROLE_VIEWER'); + + $data = $this->callMcpTool($session, 'create_constructeur', ['name' => 'Forbidden']); + + $this->assertArrayHasKey('error', $data, 'Should fail with VIEWER role'); + } + + public function testUpdateConstructeur(): void + { + $constructeur = $this->createConstructeur(name: 'Old Name'); + $session = $this->createMcpClient('ROLE_GESTIONNAIRE'); + + $data = $this->callMcpTool($session, 'update_constructeur', [ + 'constructeurId' => $constructeur->getId(), + 'name' => 'New Name', + ]); + + $this->assertArrayHasKey('_parsed', $data); + $this->assertSame('New Name', $data['_parsed']['name']); + } + + public function testDeleteConstructeur(): void + { + $constructeur = $this->createConstructeur(name: 'To Delete'); + $session = $this->createMcpClient('ROLE_GESTIONNAIRE'); + + $data = $this->callMcpTool($session, 'delete_constructeur', ['constructeurId' => $constructeur->getId()]); + + $this->assertArrayHasKey('_parsed', $data); + $this->assertTrue($data['_parsed']['deleted']); + } +} diff --git a/tests/Mcp/Tool/DashboardStatsToolTest.php b/tests/Mcp/Tool/DashboardStatsToolTest.php index bcc5b56..64dbf6a 100644 --- a/tests/Mcp/Tool/DashboardStatsToolTest.php +++ b/tests/Mcp/Tool/DashboardStatsToolTest.php @@ -5,7 +5,6 @@ declare(strict_types=1); namespace App\Tests\Mcp\Tool; use App\Tests\AbstractApiTestCase; -use stdClass; /** * @internal @@ -18,98 +17,13 @@ class DashboardStatsToolTest extends AbstractApiTestCase $this->createMachine(name: 'Machine Stats 1', site: $site); $this->createMachine(name: 'Machine Stats 2', site: $site); - $profile = $this->createProfile(roles: ['ROLE_VIEWER'], password: 'test123'); - $session = $this->initMcpSession($profile->getId(), 'test123'); + $session = $this->createMcpClient('ROLE_VIEWER'); + $data = $this->callMcpTool($session, 'get_dashboard_stats'); - $response = $session['client']->request('POST', '/_mcp', [ - 'headers' => [ - 'Content-Type' => 'application/json', - 'X-Profile-Id' => $profile->getId(), - 'X-Profile-Password' => 'test123', - 'Mcp-Session-Id' => $session['sessionId'], - ], - 'body' => json_encode([ - 'jsonrpc' => '2.0', - 'method' => 'tools/call', - 'params' => [ - 'name' => 'get_dashboard_stats', - 'arguments' => new stdClass(), - ], - 'id' => 2, - ]), - ]); - - // First list tools to see what's registered - $listResponse = $session['client']->request('POST', '/_mcp', [ - 'headers' => [ - 'Content-Type' => 'application/json', - 'X-Profile-Id' => $profile->getId(), - 'X-Profile-Password' => 'test123', - 'Mcp-Session-Id' => $session['sessionId'], - ], - 'body' => json_encode([ - 'jsonrpc' => '2.0', - 'method' => 'tools/list', - 'params' => new stdClass(), - 'id' => 3, - ]), - ]); - $toolsList = $listResponse->toArray(false); - - $this->assertResponseIsSuccessful(); - $data = $response->toArray(false); - $this->assertArrayHasKey('result', $data, 'Tools list: '.json_encode($toolsList).' | Call response: '.json_encode($data)); - - // Parse the text content from the MCP response - $content = $data['result']['content'][0]['text'] ?? ''; - $stats = json_decode($content, true); - $this->assertIsArray($stats); + $this->assertArrayHasKey('_parsed', $data); + $stats = $data['_parsed']; $this->assertGreaterThanOrEqual(2, $stats['machines']); $this->assertArrayHasKey('sites', $stats); $this->assertArrayHasKey('unresolvedComments', $stats); } - - private function initMcpSession(string $profileId, string $password): array - { - $client = static::createClient(); - - // Step 1: Initialize MCP session - $response = $client->request('POST', '/_mcp', [ - 'headers' => [ - 'Content-Type' => 'application/json', - 'X-Profile-Id' => $profileId, - 'X-Profile-Password' => $password, - ], - 'body' => json_encode([ - 'jsonrpc' => '2.0', - 'method' => 'initialize', - 'params' => [ - 'protocolVersion' => '2025-03-26', - 'capabilities' => new stdClass(), - 'clientInfo' => ['name' => 'test', 'version' => '1.0'], - ], - 'id' => 1, - ]), - ]); - - $this->assertResponseIsSuccessful(); - $sessionId = $response->getHeaders()['mcp-session-id'][0] ?? ''; - $this->assertNotEmpty($sessionId, 'MCP session ID should be returned'); - - // Step 2: Send initialized notification - $client->request('POST', '/_mcp', [ - 'headers' => [ - 'Content-Type' => 'application/json', - 'X-Profile-Id' => $profileId, - 'X-Profile-Password' => $password, - 'Mcp-Session-Id' => $sessionId, - ], - 'body' => json_encode([ - 'jsonrpc' => '2.0', - 'method' => 'notifications/initialized', - ]), - ]); - - return ['client' => $client, 'sessionId' => $sessionId]; - } } diff --git a/tests/Mcp/Tool/Product/ProductsCrudToolTest.php b/tests/Mcp/Tool/Product/ProductsCrudToolTest.php new file mode 100644 index 0000000..a0593db --- /dev/null +++ b/tests/Mcp/Tool/Product/ProductsCrudToolTest.php @@ -0,0 +1,99 @@ +createProduct(name: 'Product Alpha'); + $this->createProduct(name: 'Product Beta'); + $session = $this->createMcpClient('ROLE_VIEWER'); + + $data = $this->callMcpTool($session, 'list_products'); + + $this->assertArrayHasKey('_parsed', $data); + $this->assertGreaterThanOrEqual(2, $data['_parsed']['total']); + } + + public function testGetProduct(): void + { + $constructeur = $this->createConstructeur(name: 'Fournisseur A'); + $modelType = $this->createModelType(name: 'Type Produit', code: 'TP-001', category: ModelCategory::PRODUCT); + $product = $this->createProduct(name: 'Product Gamma', reference: 'REF-001', type: $modelType); + + // Add constructeur to product + $product->addConstructeur($constructeur); + $this->getEntityManager()->flush(); + + $session = $this->createMcpClient('ROLE_VIEWER'); + + $data = $this->callMcpTool($session, 'get_product', ['productId' => $product->getId()]); + + $this->assertArrayHasKey('_parsed', $data); + $this->assertSame('Product Gamma', $data['_parsed']['name']); + $this->assertSame('REF-001', $data['_parsed']['reference']); + $this->assertNotNull($data['_parsed']['typeProduct']); + $this->assertSame('Type Produit', $data['_parsed']['typeProduct']['name']); + $this->assertCount(1, $data['_parsed']['constructeurs']); + $this->assertSame('Fournisseur A', $data['_parsed']['constructeurs'][0]['name']); + } + + public function testCreateProduct(): void + { + $session = $this->createMcpClient('ROLE_GESTIONNAIRE'); + + $data = $this->callMcpTool($session, 'create_product', [ + 'name' => 'Product Nouveau', + 'reference' => 'REF-NEW', + 'supplierPrice' => '42.99', + ]); + + $this->assertArrayHasKey('_parsed', $data); + $this->assertSame('Product Nouveau', $data['_parsed']['name']); + $this->assertNotEmpty($data['_parsed']['id']); + } + + public function testCreateProductRequiresGestionnaire(): void + { + $session = $this->createMcpClient('ROLE_VIEWER'); + + $data = $this->callMcpTool($session, 'create_product', ['name' => 'Forbidden']); + + $this->assertArrayHasKey('error', $data, 'Should fail with VIEWER role'); + } + + public function testUpdateProduct(): void + { + $product = $this->createProduct(name: 'Old Product'); + $session = $this->createMcpClient('ROLE_GESTIONNAIRE'); + + $data = $this->callMcpTool($session, 'update_product', [ + 'productId' => $product->getId(), + 'name' => 'Updated Product', + 'supplierPrice' => '99.00', + ]); + + $this->assertArrayHasKey('_parsed', $data); + $this->assertSame('Updated Product', $data['_parsed']['name']); + } + + public function testDeleteProduct(): void + { + $product = $this->createProduct(name: 'To Delete'); + $session = $this->createMcpClient('ROLE_GESTIONNAIRE'); + + $data = $this->callMcpTool($session, 'delete_product', ['productId' => $product->getId()]); + + $this->assertArrayHasKey('_parsed', $data); + $this->assertTrue($data['_parsed']['deleted']); + } +} diff --git a/tests/Mcp/Tool/Site/SitesCrudToolTest.php b/tests/Mcp/Tool/Site/SitesCrudToolTest.php new file mode 100644 index 0000000..0c70548 --- /dev/null +++ b/tests/Mcp/Tool/Site/SitesCrudToolTest.php @@ -0,0 +1,84 @@ +createSite(name: 'Site Alpha'); + $this->createSite(name: 'Site Beta'); + $session = $this->createMcpClient('ROLE_VIEWER'); + + $data = $this->callMcpTool($session, 'list_sites'); + + $this->assertArrayHasKey('_parsed', $data); + $this->assertGreaterThanOrEqual(2, $data['_parsed']['total']); + } + + public function testGetSite(): void + { + $site = $this->createSite(name: 'Site Gamma'); + $session = $this->createMcpClient('ROLE_VIEWER'); + + $data = $this->callMcpTool($session, 'get_site', ['siteId' => $site->getId()]); + + $this->assertArrayHasKey('_parsed', $data); + $this->assertSame('Site Gamma', $data['_parsed']['name']); + } + + public function testCreateSite(): void + { + $session = $this->createMcpClient('ROLE_GESTIONNAIRE'); + + $data = $this->callMcpTool($session, 'create_site', [ + 'name' => 'Site Nouveau', + 'contactCity' => 'Paris', + ]); + + $this->assertArrayHasKey('_parsed', $data); + $this->assertSame('Site Nouveau', $data['_parsed']['name']); + $this->assertNotEmpty($data['_parsed']['id']); + } + + public function testCreateSiteRequiresGestionnaire(): void + { + $session = $this->createMcpClient('ROLE_VIEWER'); + + $data = $this->callMcpTool($session, 'create_site', ['name' => 'Forbidden']); + + $this->assertArrayHasKey('error', $data); + } + + public function testUpdateSite(): void + { + $site = $this->createSite(name: 'Old Name'); + $session = $this->createMcpClient('ROLE_GESTIONNAIRE'); + + $data = $this->callMcpTool($session, 'update_site', [ + 'siteId' => $site->getId(), + 'name' => 'New Name', + ]); + + $this->assertArrayHasKey('_parsed', $data); + $this->assertSame('New Name', $data['_parsed']['name']); + } + + public function testDeleteSite(): void + { + $site = $this->createSite(name: 'To Delete'); + $session = $this->createMcpClient('ROLE_GESTIONNAIRE'); + + $data = $this->callMcpTool($session, 'delete_site', ['siteId' => $site->getId()]); + + $this->assertArrayHasKey('_parsed', $data); + $this->assertTrue($data['_parsed']['deleted']); + } +}