Files
Inventory/tests/AbstractApiTestCase.php
Matthieu add3a9a21f fix(mcp) : return CallToolResult to prevent structuredContent serialization issue
Tools now return CallToolResult directly instead of Content arrays,
preventing the MCP SDK from auto-generating structuredContent as a
JSON array (which Claude Code rejects — expects a JSON object/record).
Also adds Accept header to test helpers and SSE response parsing.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-16 17:24:04 +01:00

557 lines
16 KiB
PHP

<?php
declare(strict_types=1);
namespace App\Tests;
use ApiPlatform\Symfony\Bundle\Test\ApiTestCase;
use ApiPlatform\Symfony\Bundle\Test\Client;
use App\Entity\Composant;
use App\Entity\ComposantPieceSlot;
use App\Entity\ComposantProductSlot;
use App\Entity\ComposantSubcomponentSlot;
use App\Entity\Constructeur;
use App\Entity\CustomField;
use App\Entity\CustomFieldValue;
use App\Entity\Machine;
use App\Entity\MachineComponentLink;
use App\Entity\MachinePieceLink;
use App\Entity\MachineProductLink;
use App\Entity\ModelType;
use App\Entity\Piece;
use App\Entity\PieceProductSlot;
use App\Entity\Product;
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
{
private const DEFAULT_PASSWORD = 'test1234';
protected function tearDown(): void
{
$this->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();
}
// ── 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',
'Accept' => 'application/json, text/event-stream',
'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',
'Accept' => 'application/json, text/event-stream',
'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<string, mixed>
*/
protected function callMcpTool(array $session, string $toolName, array $arguments = []): array
{
$response = $session['client']->request('POST', '/_mcp', [
'headers' => [
'Content-Type' => 'application/json',
'Accept' => 'application/json, text/event-stream',
'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),
]),
]);
$raw = $response->getContent(false);
$data = json_decode($raw, true);
if (null === $data) {
// SSE format: parse "data: {...}" lines
foreach (explode("\n", $raw) as $line) {
if (str_starts_with($line, 'data: ')) {
$parsed = json_decode(substr($line, 6), true);
if ($parsed && (isset($parsed['result']) || isset($parsed['error']))) {
$data = $parsed;
break;
}
}
}
}
$data ??= [];
if (isset($data['result']['content'][0]['text'])) {
$data['_parsed'] = json_decode($data['result']['content'][0]['text'], true);
}
return $data;
}
// ── 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,
?ModelType $typeComposant = null,
?ModelType $typePiece = null,
?ModelType $typeProduct = null,
int $orderIndex = 0,
): CustomField {
$cf = new CustomField();
$cf->setName($name);
$cf->setType($type);
$cf->setOrderIndex($orderIndex);
if (null !== $machine) {
$cf->setMachine($machine);
}
if (null !== $typeComposant) {
$cf->setTypeComposant($typeComposant);
}
if (null !== $typePiece) {
$cf->setTypePiece($typePiece);
}
if (null !== $typeProduct) {
$cf->setTypeProduct($typeProduct);
}
$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, int $quantity = 1): MachinePieceLink
{
$link = new MachinePieceLink();
$link->setMachine($machine);
$link->setPiece($piece);
$link->setQuantity($quantity);
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;
}
protected function createComposantPieceSlot(
Composant $composant,
?ModelType $typePiece = null,
?Piece $selectedPiece = null,
int $quantity = 1,
int $position = 0,
): ComposantPieceSlot {
$slot = new ComposantPieceSlot();
$slot->setComposant($composant);
$slot->setQuantity($quantity);
$slot->setPosition($position);
if (null !== $typePiece) {
$slot->setTypePiece($typePiece);
}
if (null !== $selectedPiece) {
$slot->setSelectedPiece($selectedPiece);
}
$em = $this->getEntityManager();
$em->persist($slot);
$em->flush();
return $slot;
}
protected function createComposantSubcomponentSlot(
Composant $composant,
?string $alias = null,
?string $familyCode = null,
?ModelType $typeComposant = null,
?Composant $selectedComposant = null,
int $position = 0,
): ComposantSubcomponentSlot {
$slot = new ComposantSubcomponentSlot();
$slot->setComposant($composant);
$slot->setAlias($alias);
$slot->setFamilyCode($familyCode);
$slot->setPosition($position);
if (null !== $typeComposant) {
$slot->setTypeComposant($typeComposant);
}
if (null !== $selectedComposant) {
$slot->setSelectedComposant($selectedComposant);
}
$em = $this->getEntityManager();
$em->persist($slot);
$em->flush();
return $slot;
}
protected function createComposantProductSlot(
Composant $composant,
?ModelType $typeProduct = null,
?Product $selectedProduct = null,
?string $familyCode = null,
int $position = 0,
): ComposantProductSlot {
$slot = new ComposantProductSlot();
$slot->setComposant($composant);
$slot->setFamilyCode($familyCode);
$slot->setPosition($position);
if (null !== $typeProduct) {
$slot->setTypeProduct($typeProduct);
}
if (null !== $selectedProduct) {
$slot->setSelectedProduct($selectedProduct);
}
$em = $this->getEntityManager();
$em->persist($slot);
$em->flush();
return $slot;
}
protected function createPieceProductSlot(
Piece $piece,
?ModelType $typeProduct = null,
?Product $selectedProduct = null,
?string $familyCode = null,
int $position = 0,
): PieceProductSlot {
$slot = new PieceProductSlot();
$slot->setPiece($piece);
$slot->setFamilyCode($familyCode);
$slot->setPosition($position);
if (null !== $typeProduct) {
$slot->setTypeProduct($typeProduct);
}
if (null !== $selectedProduct) {
$slot->setSelectedProduct($selectedProduct);
}
$em = $this->getEntityManager();
$em->persist($slot);
$em->flush();
return $slot;
}
// ── 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);
}
}