From 5c55441e6cc3816d0fdbaa762157cde468025208 Mon Sep 17 00:00:00 2001 From: Matthieu Date: Wed, 6 May 2026 15:30:59 +0200 Subject: [PATCH] =?UTF-8?q?fix(audit)=20:=20visibilit=C3=A9=20protected=20?= =?UTF-8?q?pour=20ActorProfileResolver?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit AbstractAuditSubscriber déclarait $actorProfileResolver en private readonly via promoted property. MachineAuditSubscriber surcharge onFlush() et accède à $this->actorProfileResolver, mais private n'est pas hérité — PHP voyait null et levait "Call to a member function resolve() on null" sur chaque flush Doctrine touchant des link entities. Le passage à protected suit la convention déjà en place dans la classe (safeGet, normalizeValue, persistAuditLog, etc. sont protected). readonly préserve l'immutabilité de la dépendance DI. Ajoute aussi deux tests de régression pour le clone des contextFieldValues (symétrique au test composant existant) et nettoie deux lignes vides cosmétiques laissées par le refactor précédent. - testCloneMachineCopiesPieceContextFieldValues : vérifie que les CFV context d'un MachinePieceLink sont bien rattachées au nouveau lien après clone. - testCloneMachineLeavesSourceContextFieldValuesIntact : vérifie que la machine source garde ses CFV context après clone (invariant implicite). --- .../AbstractAuditSubscriber.php | 2 +- src/Service/EntityVersionService.php | 1 - .../ModelTypeCategoryConversionService.php | 1 - .../Entity/MachineContextCustomFieldTest.php | 105 ++++++++++++++++-- 4 files changed, 95 insertions(+), 14 deletions(-) diff --git a/src/EventSubscriber/AbstractAuditSubscriber.php b/src/EventSubscriber/AbstractAuditSubscriber.php index 91627d3..a064f1f 100644 --- a/src/EventSubscriber/AbstractAuditSubscriber.php +++ b/src/EventSubscriber/AbstractAuditSubscriber.php @@ -31,7 +31,7 @@ use function method_exists; abstract class AbstractAuditSubscriber implements EventSubscriber { public function __construct( - private readonly ActorProfileResolver $actorProfileResolver, + protected readonly ActorProfileResolver $actorProfileResolver, ) {} public function getSubscribedEvents(): array diff --git a/src/Service/EntityVersionService.php b/src/Service/EntityVersionService.php index 5e837d4..01da962 100644 --- a/src/Service/EntityVersionService.php +++ b/src/Service/EntityVersionService.php @@ -957,5 +957,4 @@ final class EntityVersionService return $serialized; } - } diff --git a/src/Service/ModelTypeCategoryConversionService.php b/src/Service/ModelTypeCategoryConversionService.php index 928f871..e30e941 100644 --- a/src/Service/ModelTypeCategoryConversionService.php +++ b/src/Service/ModelTypeCategoryConversionService.php @@ -451,5 +451,4 @@ final class ModelTypeCategoryConversionService ], ); } - } diff --git a/tests/Api/Entity/MachineContextCustomFieldTest.php b/tests/Api/Entity/MachineContextCustomFieldTest.php index 05bf451..68f634b 100644 --- a/tests/Api/Entity/MachineContextCustomFieldTest.php +++ b/tests/Api/Entity/MachineContextCustomFieldTest.php @@ -7,6 +7,9 @@ namespace App\Tests\Api\Entity; use App\Enum\ModelCategory; use App\Tests\AbstractApiTestCase; +/** + * @internal + */ class MachineContextCustomFieldTest extends AbstractApiTestCase { public function testStructureReturnsContextFieldsOnComponentLink(): void @@ -56,7 +59,7 @@ class MachineContextCustomFieldTest extends AbstractApiTestCase $normalFields = array_filter( $componentLink['composant']['customFields'], - fn (array $f) => $f['name'] === 'Serial', + fn (array $f) => 'Serial' === $f['name'], ); $this->assertCount(1, $normalFields); } @@ -65,8 +68,8 @@ class MachineContextCustomFieldTest extends AbstractApiTestCase { $client = $this->createGestionnaireClient(); - $site = $this->createSite('Site B'); - $modelType = $this->createModelType('Bearing', 'BRG', ModelCategory::PIECE); + $site = $this->createSite('Site B'); + $modelType = $this->createModelType('Bearing', 'BRG', ModelCategory::PIECE); $contextField = $this->createCustomField( name: 'Wear Level', type: 'select', @@ -101,8 +104,8 @@ class MachineContextCustomFieldTest extends AbstractApiTestCase { $client = $this->createGestionnaireClient(); - $site = $this->createSite('Site C'); - $modelType = $this->createModelType('Pump', 'PMP', ModelCategory::COMPONENT); + $site = $this->createSite('Site C'); + $modelType = $this->createModelType('Pump', 'PMP', ModelCategory::COMPONENT); $contextField = $this->createCustomField( name: 'Flow Rate', type: 'number', @@ -131,8 +134,8 @@ class MachineContextCustomFieldTest extends AbstractApiTestCase { $client = $this->createGestionnaireClient(); - $site = $this->createSite('Site D'); - $modelType = $this->createModelType('Valve', 'VLV', ModelCategory::COMPONENT); + $site = $this->createSite('Site D'); + $modelType = $this->createModelType('Valve', 'VLV', ModelCategory::COMPONENT); $contextField = $this->createCustomField( name: 'Pressure', type: 'number', @@ -171,8 +174,8 @@ class MachineContextCustomFieldTest extends AbstractApiTestCase { $client = $this->createGestionnaireClient(); - $site = $this->createSite('Site E'); - $modelType = $this->createModelType('Sensor', 'SNS', ModelCategory::COMPONENT); + $site = $this->createSite('Site E'); + $modelType = $this->createModelType('Sensor', 'SNS', ModelCategory::COMPONENT); $contextField = $this->createCustomField( name: 'Calibration Date', type: 'date', @@ -190,8 +193,8 @@ class MachineContextCustomFieldTest extends AbstractApiTestCase { $client = $this->createGestionnaireClient(); - $site = $this->createSite('Site F'); - $modelType = $this->createModelType('Motor Clone', 'MOTC', ModelCategory::COMPONENT); + $site = $this->createSite('Site F'); + $modelType = $this->createModelType('Motor Clone', 'MOTC', ModelCategory::COMPONENT); $contextField = $this->createCustomField( name: 'RPM Setting', type: 'number', @@ -225,4 +228,84 @@ class MachineContextCustomFieldTest extends AbstractApiTestCase $this->assertCount(1, $clonedLink['contextCustomFieldValues']); $this->assertSame('3000', $clonedLink['contextCustomFieldValues'][0]['value']); } + + public function testCloneMachineCopiesPieceContextFieldValues(): void + { + $client = $this->createGestionnaireClient(); + + $site = $this->createSite('Site G'); + $modelType = $this->createModelType('Bearing Clone', 'BRGC', ModelCategory::PIECE); + $contextField = $this->createCustomField( + name: 'Wear Level', + type: 'text', + typePiece: $modelType, + machineContextOnly: true, + ); + + $source = $this->createMachine('Source Piece Machine', $site); + $piece = $this->createPiece('Bearing C', 'BRGC-001', $modelType); + $link = $this->createMachinePieceLink($source, $piece); + + $this->createCustomFieldValue( + customField: $contextField, + value: 'Fair', + piece: $piece, + machinePieceLink: $link, + ); + + $response = $client->request('POST', '/api/machines/'.$source->getId().'/clone', [ + 'json' => [ + 'name' => 'Cloned Piece Machine', + 'siteId' => $site->getId(), + ], + ]); + + $this->assertResponseStatusCodeSame(201); + $data = $response->toArray(); + + $clonedLink = $data['pieceLinks'][0] ?? null; + $this->assertNotNull($clonedLink, 'Clone should expose at least one pieceLink'); + $this->assertCount(1, $clonedLink['contextCustomFieldValues']); + $this->assertSame('Fair', $clonedLink['contextCustomFieldValues'][0]['value']); + } + + public function testCloneMachineLeavesSourceContextFieldValuesIntact(): void + { + $client = $this->createGestionnaireClient(); + + $site = $this->createSite('Site H'); + $modelType = $this->createModelType('Motor Source', 'MOTS', ModelCategory::COMPONENT); + $contextField = $this->createCustomField( + name: 'RPM', + type: 'number', + typeComposant: $modelType, + machineContextOnly: true, + ); + + $source = $this->createMachine('Original Machine', $site); + $composant = $this->createComposant('Motor S', 'MOTS-001', $modelType); + $link = $this->createMachineComponentLink($source, $composant); + + $this->createCustomFieldValue( + customField: $contextField, + value: '1500', + composant: $composant, + machineComponentLink: $link, + ); + + $client->request('POST', '/api/machines/'.$source->getId().'/clone', [ + 'json' => [ + 'name' => 'Clone Machine', + 'siteId' => $site->getId(), + ], + ]); + $this->assertResponseStatusCodeSame(201); + + // Source must still expose its original context field value + $sourceData = $client->request('GET', '/api/machines/'.$source->getId().'/structure')->toArray(); + $sourceLink = $sourceData['componentLinks'][0] ?? null; + $this->assertNotNull($sourceLink, 'Source machine should still expose its component link'); + $this->assertCount(1, $sourceLink['contextCustomFieldValues']); + $this->assertSame('1500', $sourceLink['contextCustomFieldValues'][0]['value']); + } }