Compare commits

..

4 Commits

Author SHA1 Message Date
gitea-actions
c88333b052 chore : bump version to v1.9.29
All checks were successful
Auto Tag Develop / tag (push) Successful in 6s
Build & Push Docker Image / build (push) Successful in 2m6s
2026-05-03 18:05:16 +00:00
8f5cd98b82 fix(machine-clone) : preserve context field values when cloning a machine
All checks were successful
Auto Tag Develop / tag (push) Successful in 35s
Context CustomFieldValues attached to component/piece links were
silently dropped from the clone response (and from any subsequent
read in the same request) because the controller persisted the new
CFVs without adding them to the inverse-side collection of the new
link. Doctrine does not auto-sync inverse OneToMany associations,
so getContextFieldValues() returned an empty collection on the
freshly persisted link.

Also synchronise the inverse collection in the test factory so
identity-mapped entities reflect newly-created CFVs when reused
by request handlers within the same test.

Co-Authored-By: RuFlo <ruv@ruv.net>
2026-05-03 19:59:03 +02:00
48f7e4c6ac test(session) : align expectations with hardened auth from WIP 476060c
Generic 'Identifiants invalides.' is now returned for both wrong
password and missing-password-set cases (security obscurity, prevents
account enumeration). Tests still asserted the granular 'Mot de passe
incorrect.' message and a 403 status that the controller no longer
emits.

Co-Authored-By: RuFlo <ruv@ruv.net>
2026-05-03 19:56:53 +02:00
c46769a67d fix(model-types) : nullify weak references on ModelType delete
Belt-and-suspenders against orphan refs when a ModelType is deleted:
applicatively nullifies typeComposantId / typePieceId / typeProductId
on every "ON DELETE SET NULL" relationship before the row is removed,
in case the database FK cascade fails to fire.

Observed in prod 2026-04-28: deletion of ModelType "Paliers" left an
orphan in skeleton_subcomponent_requirements, surfacing as a 500 when
API Platform tried to lazy-load the missing proxy.

Co-Authored-By: RuFlo <ruv@ruv.net>
2026-05-03 19:29:36 +02:00
5 changed files with 71 additions and 4 deletions

View File

@@ -1,2 +1,2 @@
parameters:
app.version: '1.9.28'
app.version: '1.9.29'

View File

@@ -337,6 +337,7 @@ class MachineStructureController extends AbstractController
$newValue->setMachineComponentLink($newLink);
$newValue->setComposant($newLink->getComposant());
$this->entityManager->persist($newValue);
$newLink->getContextFieldValues()->add($newValue);
}
}
@@ -352,6 +353,7 @@ class MachineStructureController extends AbstractController
$newValue->setMachinePieceLink($newLink);
$newValue->setPiece($newLink->getPiece());
$this->entityManager->persist($newValue);
$newLink->getContextFieldValues()->add($newValue);
}
}
}

View File

@@ -0,0 +1,57 @@
<?php
declare(strict_types=1);
namespace App\EventSubscriber;
use App\Entity\ModelType;
use Doctrine\Bundle\DoctrineBundle\Attribute\AsDoctrineListener;
use Doctrine\ORM\Event\PreRemoveEventArgs;
use Doctrine\ORM\Events;
use function sprintf;
/**
* Belt-and-suspenders cleanup of weak references to a ModelType before deletion:
* runs the equivalent of every "ON DELETE SET NULL" cascade applicatively, in case
* the database FK fails to fire (observed on prod in 2026-04 — the deletion of
* ModelType "Paliers" left an orphan in skeleton_subcomponent_requirements).
*/
#[AsDoctrineListener(event: Events::preRemove)]
final class ModelTypeReferenceCleanupSubscriber
{
/** @var list<array{0: string, 1: string}> */
private const NULLABLE_REFERENCES = [
['skeleton_subcomponent_requirements', 'typecomposantid'],
['skeleton_piece_requirements', 'typepieceid'],
['skeleton_product_requirements', 'typeproductid'],
['composant_piece_slots', 'typepieceid'],
['composant_product_slots', 'typeproductid'],
['composant_subcomponent_slots', 'typecomposantid'],
['piece_product_slots', 'typeproductid'],
['machine_component_links', 'modeltypeid'],
['machine_piece_links', 'modeltypeid'],
['machine_product_links', 'modeltypeid'],
];
public function preRemove(PreRemoveEventArgs $args): void
{
$entity = $args->getObject();
if (!$entity instanceof ModelType) {
return;
}
$id = $entity->getId();
if (!$id) {
return;
}
$conn = $args->getObjectManager()->getConnection();
foreach (self::NULLABLE_REFERENCES as [$table, $column]) {
$conn->executeStatement(
sprintf('UPDATE %s SET %s = NULL WHERE %s = ?', $table, $column, $column),
[$id],
);
}
}
}

View File

@@ -7,10 +7,10 @@ namespace App\Tests;
use ApiPlatform\Symfony\Bundle\Test\ApiTestCase;
use ApiPlatform\Symfony\Bundle\Test\Client;
use App\Entity\Composant;
use App\Entity\ComposantConstructeurLink;
use App\Entity\ComposantPieceSlot;
use App\Entity\ComposantProductSlot;
use App\Entity\ComposantSubcomponentSlot;
use App\Entity\ComposantConstructeurLink;
use App\Entity\Constructeur;
use App\Entity\CustomField;
use App\Entity\CustomFieldValue;
@@ -467,6 +467,14 @@ abstract class AbstractApiTestCase extends ApiTestCase
$em->persist($cfv);
$em->flush();
// Keep inverse-side collections in sync so identity-mapped entities reflect the new CFV.
if (null !== $machineComponentLink && !$machineComponentLink->getContextFieldValues()->contains($cfv)) {
$machineComponentLink->getContextFieldValues()->add($cfv);
}
if (null !== $machinePieceLink && !$machinePieceLink->getContextFieldValues()->contains($cfv)) {
$machinePieceLink->getContextFieldValues()->add($cfv);
}
return $cfv;
}

View File

@@ -47,7 +47,7 @@ class SessionProfileTest extends AbstractApiTestCase
]);
$this->assertResponseStatusCodeSame(401);
$this->assertJsonContains(['message' => 'Mot de passe incorrect.']);
$this->assertJsonContains(['message' => 'Identifiants invalides.']);
}
public function testLoginMissingPassword(): void
@@ -103,7 +103,7 @@ class SessionProfileTest extends AbstractApiTestCase
],
]);
$this->assertResponseStatusCodeSame(403);
$this->assertResponseStatusCodeSame(401);
}
public function testGetActiveProfileAuthenticated(): void