createMachine('Source Machine'); $cf = $this->createCustomField('Puissance', 'text', $source); $this->createCustomFieldValue($cf, '15 kW', $source); $client = $this->createGestionnaireClient(); $client->request('POST', '/api/machines/'.$source->getId().'/clone', [ 'headers' => ['Content-Type' => 'application/json'], 'json' => [ 'name' => 'Cloned Machine', 'siteId' => $source->getSite()->getId(), ], ]); $this->assertResponseIsSuccessful(); $data = $client->getResponse()->toArray(); $clonedMachineId = $data['machine']['id'] ?? null; $this->assertNotNull($clonedMachineId, 'Clone should return new machine ID'); $this->assertNotSame($source->getId(), $clonedMachineId); // Verify cloned CustomFieldValues reference CustomFields owned by the clone $em = $this->getEntityManager(); $clonedValues = $em->getRepository(CustomFieldValue::class)->findBy(['machine' => $clonedMachineId]); $clonedFields = $em->getRepository(CustomField::class)->findBy(['machine' => $clonedMachineId]); $this->assertNotEmpty($clonedValues, 'Clone should have custom field values'); $this->assertNotEmpty($clonedFields, 'Clone should have custom field definitions'); $clonedFieldIds = array_map(static fn (CustomField $cf) => $cf->getId(), $clonedFields); foreach ($clonedValues as $cfv) { $this->assertContains( $cfv->getCustomField()->getId(), $clonedFieldIds, 'Cloned CustomFieldValue must reference a CustomField owned by the cloned machine, not the source' ); } } public function testCloneCustomFieldValuesPreserveValues(): void { $source = $this->createMachine('Source'); $cf1 = $this->createCustomField('Tension', 'text', $source); $cf2 = $this->createCustomField('Vitesse', 'text', $source); $this->createCustomFieldValue($cf1, '400 V', $source); $this->createCustomFieldValue($cf2, '1500 tr/min', $source); $client = $this->createGestionnaireClient(); $client->request('POST', '/api/machines/'.$source->getId().'/clone', [ 'headers' => ['Content-Type' => 'application/json'], 'json' => [ 'name' => 'Clone with values', 'siteId' => $source->getSite()->getId(), ], ]); $this->assertResponseIsSuccessful(); $data = $client->getResponse()->toArray(); $clonedMachineId = $data['machine']['id']; $em = $this->getEntityManager(); $clonedValues = $em->getRepository(CustomFieldValue::class)->findBy(['machine' => $clonedMachineId]); $valueMap = []; foreach ($clonedValues as $cfv) { $valueMap[$cfv->getCustomField()->getName()] = $cfv->getValue(); } $this->assertSame('400 V', $valueMap['Tension'] ?? null); $this->assertSame('1500 tr/min', $valueMap['Vitesse'] ?? null); } // ----------------------------------------------------------------------- // T2 — Slot PATCH: type validation + 404 on missing piece // ----------------------------------------------------------------------- public function testSlotPatchWithNonExistentPieceReturns404(): void { $composant = $this->createComposant('Comp slot 404'); $pieceType = $this->createModelType('Roulement', 'ROUL-404', ModelCategory::PIECE); $slot = $this->createComposantPieceSlot($composant, $pieceType, null, 1, 0); $client = $this->createGestionnaireClient(); $client->request('PATCH', '/api/composant-piece-slots/'.$slot->getId(), [ 'headers' => ['Content-Type' => 'application/json'], 'json' => ['selectedPieceId' => 'cl_nonexistent_id_123'], ]); $this->assertResponseStatusCodeSame(404); } public function testSlotPatchWithWrongPieceTypeReturns422(): void { $typeA = $this->createModelType('Roulement', 'ROUL-A', ModelCategory::PIECE); $typeB = $this->createModelType('Joint', 'JOINT-B', ModelCategory::PIECE); $composant = $this->createComposant('Comp slot type'); $slot = $this->createComposantPieceSlot($composant, $typeA, null, 1, 0); $wrongPiece = $this->createPiece('Joint XY', 'REF-JXY', $typeB); $client = $this->createGestionnaireClient(); $client->request('PATCH', '/api/composant-piece-slots/'.$slot->getId(), [ 'headers' => ['Content-Type' => 'application/json'], 'json' => ['selectedPieceId' => $wrongPiece->getId()], ]); $this->assertResponseStatusCodeSame(422); $data = $client->getResponse()->toArray(false); $this->assertStringContainsString('Roulement', $data['error']); } public function testSlotPatchWithCorrectPieceTypeSucceeds(): void { $type = $this->createModelType('Filtre', 'FILT-OK', ModelCategory::PIECE); $composant = $this->createComposant('Comp slot OK'); $slot = $this->createComposantPieceSlot($composant, $type, null, 1, 0); $piece = $this->createPiece('Filtre Parker', 'REF-FP', $type); $client = $this->createGestionnaireClient(); $client->request('PATCH', '/api/composant-piece-slots/'.$slot->getId(), [ 'headers' => ['Content-Type' => 'application/json'], 'json' => ['selectedPieceId' => $piece->getId()], ]); $this->assertResponseIsSuccessful(); $data = $client->getResponse()->toArray(); $this->assertSame($piece->getId(), $data['selectedPieceId']); } public function testSlotPatchClearPieceSucceeds(): void { $type = $this->createModelType('Joint', 'JOINT-CLR', ModelCategory::PIECE); $composant = $this->createComposant('Comp slot clear'); $piece = $this->createPiece('Joint A', 'REF-JA', $type); $slot = $this->createComposantPieceSlot($composant, $type, $piece, 1, 0); $client = $this->createGestionnaireClient(); $client->request('PATCH', '/api/composant-piece-slots/'.$slot->getId(), [ 'headers' => ['Content-Type' => 'application/json'], 'json' => ['selectedPieceId' => null], ]); $this->assertResponseIsSuccessful(); $data = $client->getResponse()->toArray(); $this->assertNull($data['selectedPieceId']); } // ----------------------------------------------------------------------- // T3 — Conversion: blocks when slots have data // ----------------------------------------------------------------------- public function testConversionBlockedWhenSlotsHaveData(): void { $compType = $this->createModelType('Pompe', 'POMPE-CONV', ModelCategory::COMPONENT); $pieceType = $this->createModelType('Joint', 'JOINT-CONV', ModelCategory::PIECE); $composant = $this->createComposant('Pompe A', null, $compType); $piece = $this->createPiece('Joint réel', 'REF-JR', $pieceType); $this->createComposantPieceSlot($composant, $pieceType, $piece, 1, 0); $client = $this->createGestionnaireClient(); $client->request('GET', '/api/model_types/'.$compType->getId().'/conversion-check'); $this->assertResponseIsSuccessful(); $data = $client->getResponse()->toArray(); $this->assertFalse($data['canConvert'], 'Conversion should be blocked when slots have data'); $this->assertNotEmpty($data['blockers']); } public function testConversionAllowedWhenSlotsEmpty(): void { $compType = $this->createModelType('Vanne', 'VANNE-CONV', ModelCategory::COMPONENT); $pieceType = $this->createModelType('Joint', 'JOINT-EMPTY', ModelCategory::PIECE); $composant = $this->createComposant('Vanne A', null, $compType); // Slot without selected piece (empty) $this->createComposantPieceSlot($composant, $pieceType, null, 1, 0); $client = $this->createGestionnaireClient(); $client->request('GET', '/api/model_types/'.$compType->getId().'/conversion-check'); $this->assertResponseIsSuccessful(); $data = $client->getResponse()->toArray(); // May still be blocked by machine links, but NOT by slots $slotBlocker = array_filter( $data['blockers'] ?? [], static fn (string $b) => str_contains($b, 'slot') ); $this->assertEmpty($slotBlocker, 'Empty slots should not block conversion'); } // ----------------------------------------------------------------------- // T4 — CustomFieldValue: no orphan CustomField creation // ----------------------------------------------------------------------- public function testCustomFieldValueRejectsOrphanCreation(): void { $machine = $this->createMachine('Machine CF'); $client = $this->createGestionnaireClient(); $client->request('POST', '/api/custom-fields/values', [ 'headers' => ['Content-Type' => 'application/json'], 'json' => [ 'customFieldName' => 'NonExistentField', 'value' => 'some value', 'entityType' => 'machine', 'entityId' => $machine->getId(), ], ]); $this->assertResponseStatusCodeSame(404); } public function testCustomFieldValueAcceptsExistingFieldById(): void { $machine = $this->createMachine('Machine CF ok'); $cf = $this->createCustomField('Existing Field', 'text', $machine); $client = $this->createGestionnaireClient(); $client->request('POST', '/api/custom-fields/values', [ 'headers' => ['Content-Type' => 'application/json'], 'json' => [ 'customFieldId' => $cf->getId(), 'value' => 'test value', 'entityType' => 'machine', 'entityId' => $machine->getId(), ], ]); $this->assertResponseIsSuccessful(); } }