Backend: - Migration: version column on audit_logs and machines - AuditLog, Machine, Composant, Piece, Product: version + skipAudit properties - AbstractAuditSubscriber: auto-increment version, skip on restore, fix decimal diff - Enriched snapshots with slots, custom fields and version number - AuditLogRepository: findVersionHistory, findByVersion - EntityVersionService: list, preview, restore with skeleton/integrity checks - EntityVersionController: REST endpoints for all 4 entity types - 11 tests covering list, preview, restore, auth Frontend: update submodule pointer Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
175 lines
6.6 KiB
PHP
175 lines
6.6 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace App\Tests\Api\Controller;
|
|
|
|
use App\Tests\AbstractApiTestCase;
|
|
|
|
/**
|
|
* @internal
|
|
*/
|
|
class EntityVersionTest extends AbstractApiTestCase
|
|
{
|
|
// ── Versions list ───────────────────────────────────────────────
|
|
|
|
public function testMachineVersionsAfterCreateAndUpdate(): void
|
|
{
|
|
$machine = $this->createMachine('Machine V');
|
|
|
|
$gClient = $this->createGestionnaireClient();
|
|
$gClient->request('PATCH', self::iri('machines', $machine->getId()), [
|
|
'headers' => ['Content-Type' => 'application/merge-patch+json'],
|
|
'json' => ['name' => 'Machine V Updated'],
|
|
]);
|
|
$this->assertResponseIsSuccessful();
|
|
|
|
$vClient = $this->createViewerClient();
|
|
$vClient->request('GET', sprintf('/api/machines/%s/versions', $machine->getId()));
|
|
$this->assertResponseIsSuccessful();
|
|
|
|
$data = $vClient->getResponse()->toArray();
|
|
$this->assertArrayHasKey('items', $data);
|
|
$this->assertArrayHasKey('total', $data);
|
|
$this->assertGreaterThanOrEqual(1, $data['total']);
|
|
|
|
$firstItem = $data['items'][0];
|
|
$this->assertArrayHasKey('version', $firstItem);
|
|
$this->assertArrayHasKey('action', $firstItem);
|
|
$this->assertArrayHasKey('createdAt', $firstItem);
|
|
}
|
|
|
|
public function testComposantVersionsList(): void
|
|
{
|
|
$composant = $this->createComposant('Composant V');
|
|
|
|
$client = $this->createViewerClient();
|
|
$client->request('GET', sprintf('/api/composants/%s/versions', $composant->getId()));
|
|
$this->assertResponseIsSuccessful();
|
|
|
|
$data = $client->getResponse()->toArray();
|
|
$this->assertArrayHasKey('items', $data);
|
|
}
|
|
|
|
public function testPieceVersionsList(): void
|
|
{
|
|
$piece = $this->createPiece('Piece V');
|
|
|
|
$client = $this->createViewerClient();
|
|
$client->request('GET', sprintf('/api/pieces/%s/versions', $piece->getId()));
|
|
$this->assertResponseIsSuccessful();
|
|
}
|
|
|
|
public function testProductVersionsList(): void
|
|
{
|
|
$product = $this->createProduct('Product V');
|
|
|
|
$client = $this->createViewerClient();
|
|
$client->request('GET', sprintf('/api/products/%s/versions', $product->getId()));
|
|
$this->assertResponseIsSuccessful();
|
|
}
|
|
|
|
public function testVersionsNotFound(): void
|
|
{
|
|
$client = $this->createViewerClient();
|
|
$client->request('GET', '/api/machines/nonexistent-id/versions');
|
|
$this->assertResponseStatusCodeSame(404);
|
|
}
|
|
|
|
public function testVersionsUnauthenticated(): void
|
|
{
|
|
$machine = $this->createMachine('Machine V');
|
|
|
|
$client = $this->createUnauthenticatedClient();
|
|
$client->request('GET', sprintf('/api/machines/%s/versions', $machine->getId()));
|
|
$this->assertResponseStatusCodeSame(401);
|
|
}
|
|
|
|
// ── Preview ─────────────────────────────────────────────────────
|
|
|
|
public function testPreviewRequiresGestionnaire(): void
|
|
{
|
|
$machine = $this->createMachine('Machine P');
|
|
|
|
$client = $this->createViewerClient();
|
|
$client->request('GET', sprintf('/api/machines/%s/versions/1/preview', $machine->getId()));
|
|
$this->assertResponseStatusCodeSame(403);
|
|
}
|
|
|
|
public function testPreviewReturnsRestoreInfo(): void
|
|
{
|
|
$composant = $this->createComposant('Composant P');
|
|
|
|
// Update to create version 2
|
|
$gClient = $this->createGestionnaireClient();
|
|
$gClient->request('PATCH', self::iri('composants', $composant->getId()), [
|
|
'headers' => ['Content-Type' => 'application/merge-patch+json'],
|
|
'json' => ['name' => 'Composant P Updated'],
|
|
]);
|
|
$this->assertResponseIsSuccessful();
|
|
|
|
// Preview restore to version 1
|
|
$gClient2 = $this->createGestionnaireClient();
|
|
$gClient2->request('GET', sprintf('/api/composants/%s/versions/1/preview', $composant->getId()));
|
|
$this->assertResponseIsSuccessful();
|
|
|
|
$data = $gClient2->getResponse()->toArray();
|
|
$this->assertArrayHasKey('version', $data);
|
|
$this->assertArrayHasKey('restoreMode', $data);
|
|
$this->assertArrayHasKey('diff', $data);
|
|
$this->assertArrayHasKey('warnings', $data);
|
|
$this->assertEquals(1, $data['version']);
|
|
$this->assertEquals('full', $data['restoreMode']);
|
|
}
|
|
|
|
// ── Restore ─────────────────────────────────────────────────────
|
|
|
|
public function testRestoreRequiresGestionnaire(): void
|
|
{
|
|
$machine = $this->createMachine('Machine R');
|
|
|
|
$client = $this->createViewerClient();
|
|
$client->request('POST', sprintf('/api/machines/%s/versions/1/restore', $machine->getId()));
|
|
$this->assertResponseStatusCodeSame(403);
|
|
}
|
|
|
|
public function testRestoreCreatesNewVersion(): void
|
|
{
|
|
$composant = $this->createComposant('Original Name');
|
|
|
|
// Update
|
|
$gClient = $this->createGestionnaireClient();
|
|
$gClient->request('PATCH', self::iri('composants', $composant->getId()), [
|
|
'headers' => ['Content-Type' => 'application/merge-patch+json'],
|
|
'json' => ['name' => 'Updated Name'],
|
|
]);
|
|
$this->assertResponseIsSuccessful();
|
|
|
|
// Restore to version 1
|
|
$gClient2 = $this->createGestionnaireClient();
|
|
$gClient2->request('POST', sprintf('/api/composants/%s/versions/1/restore', $composant->getId()));
|
|
$this->assertResponseIsSuccessful();
|
|
|
|
$data = $gClient2->getResponse()->toArray();
|
|
$this->assertTrue($data['success']);
|
|
$this->assertEquals(1, $data['restoredFromVersion']);
|
|
$this->assertGreaterThan(2, $data['newVersion']);
|
|
|
|
// Verify the name was restored
|
|
$vClient = $this->createViewerClient();
|
|
$vClient->request('GET', self::iri('composants', $composant->getId()));
|
|
$this->assertResponseIsSuccessful();
|
|
$entityData = $vClient->getResponse()->toArray();
|
|
$this->assertEquals('Original Name', $entityData['name']);
|
|
}
|
|
|
|
public function testRestoreVersionNotFound(): void
|
|
{
|
|
$composant = $this->createComposant('Composant NF');
|
|
|
|
$client = $this->createGestionnaireClient();
|
|
$client->request('POST', sprintf('/api/composants/%s/versions/999/restore', $composant->getId()));
|
|
$this->assertResponseStatusCodeSame(404);
|
|
}
|
|
}
|