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 */ 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); } }