Files
breadcrumb-the-shire/modules/helpdesk/tests/Module/Helpdesk/Service/HandoverRevisionServiceTest.php
fs 03e15eaf08 feat(helpdesk): add handover revision history with timeline, diff, and restore
Every save creates an immutable revision snapshot. The aside shows a
commit-graph-style timeline (newest first) with vertical line, circle
nodes, version numbers, change type, user, and date. Clicking a past
revision renders it readonly with inline git-diff-style highlights
(changed/added/removed with tinted backgrounds). Compare mode allows
diffing any two arbitrary revisions. MANAGE users can restore old
versions, which creates a new revision preserving full history.

New: HandoverRevisionService, HandoverRevisionRepository,
migration 006, 14 PHPUnit tests, DE/EN translations.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-15 22:14:40 +02:00

193 lines
7.9 KiB
PHP

<?php
namespace MintyPHP\Tests\Module\Helpdesk\Service;
use MintyPHP\Module\Helpdesk\Repository\HandoverRepository;
use MintyPHP\Module\Helpdesk\Repository\HandoverRevisionRepository;
use MintyPHP\Module\Helpdesk\Service\HandoverRevisionService;
use MintyPHP\Module\Helpdesk\Service\HandoverService;
use PHPUnit\Framework\TestCase;
class HandoverRevisionServiceTest extends TestCase
{
private const TENANT_ID = 1;
private const USER_ID = 42;
private const HANDOVER_ID = 10;
private function createService(
?HandoverRevisionRepository $revisionRepo = null,
?HandoverRepository $handoverRepo = null,
): HandoverRevisionService {
$revisionRepo = $revisionRepo ?? $this->createMock(HandoverRevisionRepository::class);
$handoverRepo = $handoverRepo ?? $this->createMock(HandoverRepository::class);
return new HandoverRevisionService($revisionRepo, $handoverRepo);
}
// ── createRevision ───────────────────────────────────────────────
public function testCreateRevisionIncrementsNumber(): void
{
$revisionRepo = $this->createMock(HandoverRevisionRepository::class);
$revisionRepo->method('getLatestRevisionNumber')->willReturn(3);
$revisionRepo->expects($this->once())
->method('insert')
->with($this->callback(function (array $data): bool {
return $data['revision'] === 4;
}))
->willReturn(100);
$service = $this->createService($revisionRepo);
$result = $service->createRevision(self::TENANT_ID, self::HANDOVER_ID, ['name' => 'Test'], 'draft', 1, self::USER_ID);
$this->assertSame(100, $result);
}
public function testCreateRevisionStoresSnapshot(): void
{
$revisionRepo = $this->createMock(HandoverRevisionRepository::class);
$revisionRepo->method('getLatestRevisionNumber')->willReturn(0);
$revisionRepo->expects($this->once())
->method('insert')
->with($this->callback(function (array $data): bool {
return $data['status'] === 'in_progress'
&& $data['schema_version'] === 2
&& $data['change_type'] === 'fields'
&& json_decode($data['field_values'], true) === ['name' => 'Acme'];
}))
->willReturn(1);
$service = $this->createService($revisionRepo);
$service->createRevision(self::TENANT_ID, self::HANDOVER_ID, ['name' => 'Acme'], 'in_progress', 2, self::USER_ID);
}
// ── computeDiff ──────────────────────────────────────────────────
public function testComputeDiffDetectsAddedFields(): void
{
$service = $this->createService();
$diff = $service->computeDiff([], ['name' => 'New']);
$this->assertArrayHasKey('name', $diff);
$this->assertSame('added', $diff['name']['type']);
$this->assertNull($diff['name']['old']);
$this->assertSame('New', $diff['name']['new']);
}
public function testComputeDiffDetectsRemovedFields(): void
{
$service = $this->createService();
$diff = $service->computeDiff(['name' => 'Old'], []);
$this->assertArrayHasKey('name', $diff);
$this->assertSame('removed', $diff['name']['type']);
$this->assertSame('Old', $diff['name']['old']);
$this->assertNull($diff['name']['new']);
}
public function testComputeDiffDetectsChangedFields(): void
{
$service = $this->createService();
$diff = $service->computeDiff(['name' => 'Old', 'priority' => 'low'], ['name' => 'New', 'priority' => 'low']);
$this->assertArrayHasKey('name', $diff);
$this->assertSame('changed', $diff['name']['type']);
$this->assertSame('Old', $diff['name']['old']);
$this->assertSame('New', $diff['name']['new']);
$this->assertArrayNotHasKey('priority', $diff);
}
public function testComputeDiffHandlesEmptyArrays(): void
{
$service = $this->createService();
$diff = $service->computeDiff([], []);
$this->assertSame([], $diff);
}
// ── restoreRevision ──────────────────────────────────────────────
public function testRestoreRevisionRequiresManagePermission(): void
{
$service = $this->createService();
$result = $service->restoreRevision(self::TENANT_ID, self::HANDOVER_ID, 1, self::USER_ID, HandoverService::PERMISSION_CREATE);
$this->assertFalse($result['ok']);
$this->assertArrayHasKey('general', $result['errors']);
}
public function testRestoreRevisionRejectsInvalidRevision(): void
{
$revisionRepo = $this->createMock(HandoverRevisionRepository::class);
$revisionRepo->method('findByRevision')->willReturn(null);
$service = $this->createService($revisionRepo);
$result = $service->restoreRevision(self::TENANT_ID, self::HANDOVER_ID, 999, self::USER_ID, HandoverService::PERMISSION_MANAGE);
$this->assertFalse($result['ok']);
}
public function testRestoreRevisionCreatesNewRevision(): void
{
$revisionRepo = $this->createMock(HandoverRevisionRepository::class);
$revisionRepo->method('findByRevision')->willReturn([
'id' => 1,
'revision' => 2,
'field_values' => '{"name":"Old value"}',
'status' => 'draft',
]);
$revisionRepo->method('getLatestRevisionNumber')->willReturn(5);
$revisionRepo->expects($this->once())
->method('insert')
->with($this->callback(function (array $data): bool {
return $data['revision'] === 6
&& $data['change_type'] === 'restore'
&& json_decode($data['field_values'], true) === ['name' => 'Old value'];
}))
->willReturn(200);
$handoverRepo = $this->createMock(HandoverRepository::class);
$handoverRepo->method('findById')->willReturn([
'id' => self::HANDOVER_ID,
'status' => 'in_progress',
'field_values' => '{"name":"Current value"}',
'schema_snapshot' => '{"version":1,"fields":[]}',
]);
$handoverRepo->expects($this->once())->method('updateFieldValues');
$handoverRepo->expects($this->once())->method('updateStatus');
$service = $this->createService($revisionRepo, $handoverRepo);
$result = $service->restoreRevision(self::TENANT_ID, self::HANDOVER_ID, 2, self::USER_ID, HandoverService::PERMISSION_MANAGE);
$this->assertTrue($result['ok']);
}
public function testRestoreRevisionSkipsStatusUpdateWhenSame(): void
{
$revisionRepo = $this->createMock(HandoverRevisionRepository::class);
$revisionRepo->method('findByRevision')->willReturn([
'id' => 1,
'revision' => 1,
'field_values' => '{"name":"Old"}',
'status' => 'draft',
]);
$revisionRepo->method('getLatestRevisionNumber')->willReturn(3);
$revisionRepo->method('insert')->willReturn(100);
$handoverRepo = $this->createMock(HandoverRepository::class);
$handoverRepo->method('findById')->willReturn([
'id' => self::HANDOVER_ID,
'status' => 'draft',
'field_values' => '{"name":"Current"}',
'schema_snapshot' => '{"version":1,"fields":[]}',
]);
$handoverRepo->expects($this->once())->method('updateFieldValues');
$handoverRepo->expects($this->never())->method('updateStatus');
$service = $this->createService($revisionRepo, $handoverRepo);
$result = $service->restoreRevision(self::TENANT_ID, self::HANDOVER_ID, 1, self::USER_ID, HandoverService::PERMISSION_MANAGE);
$this->assertTrue($result['ok']);
}
}