Files
breadcrumb-the-shire/modules/helpdesk/tests/Module/Helpdesk/Service/HandoverServiceTest.php
fs e7c60468c9 feat(helpdesk): add domain linking, bulk actions, and revision polish to handovers
Adds domain selection (cascaded from debitor) to handover creation and edit,
bulk delete with confirmation dialog, and various UI improvements to the
handover wizard and list page. Includes migration for domain columns,
domain-select AJAX endpoint, and updated tests.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-16 13:21:12 +02:00

375 lines
15 KiB
PHP

<?php
namespace MintyPHP\Tests\Module\Helpdesk\Service;
use MintyPHP\Module\Helpdesk\Repository\HandoverRepository;
use MintyPHP\Module\Helpdesk\Service\HandoverRevisionService;
use MintyPHP\Module\Helpdesk\Service\HandoverService;
use MintyPHP\Module\Helpdesk\Service\SoftwareProductService;
use PHPUnit\Framework\TestCase;
class HandoverServiceTest extends TestCase
{
private const TENANT_ID = 1;
private const USER_ID = 42;
private const VALID_SCHEMA_JSON = '{"version":1,"fields":[{"type":"text","label":"Name","key":"name","required":true},{"type":"select","label":"Priority","key":"priority","required":false,"options":[{"value":"high","label":"High"},{"value":"low","label":"Low"}]}]}';
private function createService(
?HandoverRepository $repository = null,
?SoftwareProductService $productService = null,
?HandoverRevisionService $revisionService = null,
): HandoverService {
$repository = $repository ?? $this->createMock(HandoverRepository::class);
$productService = $productService ?? $this->createMock(SoftwareProductService::class);
return new HandoverService($repository, $productService, $revisionService);
}
private function mockProductService(bool $exists = true, bool $hasSchema = true): SoftwareProductService
{
$mock = $this->createMock(SoftwareProductService::class);
if (!$exists) {
$mock->method('findByCode')->willReturn(null);
} else {
$mock->method('findByCode')->willReturn([
'id' => 1,
'code' => 'PROD-A',
'name' => 'Product A',
'active' => 1,
'handover_protocol_schema' => $hasSchema ? self::VALID_SCHEMA_JSON : null,
]);
}
return $mock;
}
// ── Create tests ─────────────────────────────────────────────────
public function testCreateHappyPath(): void
{
$repo = $this->createMock(HandoverRepository::class);
$repo->expects($this->once())->method('insert')->willReturn(99);
$service = $this->createService($repo, $this->mockProductService());
$result = $service->create(self::TENANT_ID, 'D10001', 'Acme Corp', 'PROD-A', 'DNS00001', 'example.com', self::USER_ID);
$this->assertTrue($result['ok']);
$this->assertSame(99, $result['id']);
$this->assertEmpty($result['errors']);
}
public function testCreateWithInvalidProductCode(): void
{
$service = $this->createService(productService: $this->mockProductService(exists: false));
$result = $service->create(self::TENANT_ID, 'D10001', 'Acme Corp', 'INVALID', 'DNS00001', 'example.com', self::USER_ID);
$this->assertFalse($result['ok']);
$this->assertNull($result['id']);
$this->assertArrayHasKey('product_code', $result['errors']);
}
public function testCreateWithProductWithoutSchema(): void
{
$service = $this->createService(productService: $this->mockProductService(hasSchema: false));
$result = $service->create(self::TENANT_ID, 'D10001', 'Acme Corp', 'PROD-A', 'DNS00001', 'example.com', self::USER_ID);
$this->assertFalse($result['ok']);
$this->assertNull($result['id']);
$this->assertArrayHasKey('product_code', $result['errors']);
}
public function testCreateWithEmptyDebitorNo(): void
{
$service = $this->createService();
$result = $service->create(self::TENANT_ID, '', 'Acme Corp', 'PROD-A', 'DNS00001', 'example.com', self::USER_ID);
$this->assertFalse($result['ok']);
$this->assertArrayHasKey('debitor_no', $result['errors']);
}
// ── Update fields tests ──────────────────────────────────────────
public function testUpdateFieldsHappyPath(): void
{
$repo = $this->createMock(HandoverRepository::class);
$repo->method('findById')->willReturn([
'id' => 1,
'status' => 'draft',
'schema_snapshot' => self::VALID_SCHEMA_JSON,
]);
$repo->expects($this->once())->method('updateFieldValues')->willReturn(true);
$service = $this->createService($repo);
$result = $service->updateFields(self::TENANT_ID, 1, ['name' => 'Test', 'priority' => 'high'], self::USER_ID, HandoverService::PERMISSION_CREATE);
$this->assertTrue($result['ok']);
}
public function testUpdateFieldsRejectsUnknownKeys(): void
{
$repo = $this->createMock(HandoverRepository::class);
$repo->method('findById')->willReturn([
'id' => 1,
'status' => 'draft',
'schema_snapshot' => self::VALID_SCHEMA_JSON,
]);
$service = $this->createService($repo);
$result = $service->updateFields(self::TENANT_ID, 1, ['unknown_field' => 'value'], self::USER_ID, HandoverService::PERMISSION_CREATE);
$this->assertFalse($result['ok']);
$this->assertArrayHasKey('unknown_field', $result['errors']);
}
// ── Status transition tests ──────────────────────────────────────
public function testChangeStatusByCreateUser(): void
{
$repo = $this->createMock(HandoverRepository::class);
$repo->method('findById')->willReturn(['id' => 1, 'status' => 'draft']);
$repo->method('updateStatus')->willReturn(true);
$service = $this->createService($repo);
// Can set in_progress
$result = $service->changeStatus(self::TENANT_ID, 1, 'in_progress', self::USER_ID, HandoverService::PERMISSION_CREATE);
$this->assertTrue($result['ok']);
}
public function testChangeStatusCreateUserCannotArchive(): void
{
$repo = $this->createMock(HandoverRepository::class);
$repo->method('findById')->willReturn(['id' => 1, 'status' => 'completed']);
$service = $this->createService($repo);
$result = $service->changeStatus(self::TENANT_ID, 1, 'archived', self::USER_ID, HandoverService::PERMISSION_CREATE);
$this->assertFalse($result['ok']);
$this->assertArrayHasKey('status', $result['errors']);
}
public function testChangeStatusManageUserCanArchive(): void
{
$repo = $this->createMock(HandoverRepository::class);
$repo->method('findById')->willReturn(['id' => 1, 'status' => 'completed']);
$repo->method('updateStatus')->willReturn(true);
$service = $this->createService($repo);
$result = $service->changeStatus(self::TENANT_ID, 1, 'archived', self::USER_ID, HandoverService::PERMISSION_MANAGE);
$this->assertTrue($result['ok']);
}
// ── Edit permission tests ────────────────────────────────────────
public function testEditDeniedForCreateUserOnCompletedHandover(): void
{
$repo = $this->createMock(HandoverRepository::class);
$repo->method('findById')->willReturn([
'id' => 1,
'status' => 'completed',
'schema_snapshot' => self::VALID_SCHEMA_JSON,
]);
$service = $this->createService($repo);
$result = $service->updateFields(self::TENANT_ID, 1, ['name' => 'Test'], self::USER_ID, HandoverService::PERMISSION_CREATE);
$this->assertFalse($result['ok']);
$this->assertArrayHasKey('general', $result['errors']);
}
public function testEditAllowedForManageUserOnCompletedHandover(): void
{
$repo = $this->createMock(HandoverRepository::class);
$repo->method('findById')->willReturn([
'id' => 1,
'status' => 'completed',
'schema_snapshot' => self::VALID_SCHEMA_JSON,
]);
$repo->expects($this->once())->method('updateFieldValues')->willReturn(true);
$service = $this->createService($repo);
$result = $service->updateFields(self::TENANT_ID, 1, ['name' => 'Test'], self::USER_ID, HandoverService::PERMISSION_MANAGE);
$this->assertTrue($result['ok']);
}
// ── Revision integration tests ──────────────────────────────────
public function testCreateCreatesInitialRevision(): void
{
$repo = $this->createMock(HandoverRepository::class);
$repo->expects($this->once())->method('insert')->willReturn(99);
$revisionService = $this->createMock(HandoverRevisionService::class);
$revisionService->expects($this->once())
->method('createRevision')
->with(
self::TENANT_ID,
99,
[],
HandoverService::STATUS_DRAFT,
1,
self::USER_ID,
HandoverRevisionService::CHANGE_TYPE_INITIAL
);
$service = $this->createService($repo, $this->mockProductService(), $revisionService);
$result = $service->create(self::TENANT_ID, 'D10001', 'Acme Corp', 'PROD-A', 'DNS00001', 'example.com', self::USER_ID);
$this->assertTrue($result['ok']);
}
public function testCreateWithEmptyDomainNo(): void
{
$service = $this->createService(productService: $this->mockProductService());
$result = $service->create(self::TENANT_ID, 'D10001', 'Acme Corp', 'PROD-A', '', 'example.com', self::USER_ID);
$this->assertFalse($result['ok']);
$this->assertArrayHasKey('domain_no', $result['errors']);
}
public function testCreatePassesDomainToRepository(): void
{
$repo = $this->createMock(HandoverRepository::class);
$repo->expects($this->once())
->method('insert')
->with($this->callback(function (array $data): bool {
return ($data['domain_no'] ?? '') === 'DNS00001'
&& ($data['domain_url'] ?? '') === 'example.com';
}))
->willReturn(99);
$service = $this->createService($repo, $this->mockProductService());
$result = $service->create(self::TENANT_ID, 'D10001', 'Acme Corp', 'PROD-A', 'DNS00001', 'example.com', self::USER_ID);
$this->assertTrue($result['ok']);
}
public function testCreateRevisionAfterSaveSetsBothWhenFieldsAndStatusChanged(): void
{
$repo = $this->createMock(HandoverRepository::class);
$revisionService = $this->createMock(HandoverRevisionService::class);
$revisionService->expects($this->once())
->method('createRevision')
->with(
self::TENANT_ID,
1,
['name' => 'Test'],
'in_progress',
1,
self::USER_ID,
HandoverRevisionService::CHANGE_TYPE_BOTH
);
$service = $this->createService($repo, null, $revisionService);
$service->createRevisionAfterSave(self::TENANT_ID, 1, ['name' => 'Test'], 'in_progress', true, true, 1, self::USER_ID);
}
public function testCreateRevisionAfterSaveSetsFieldsWhenOnlyFieldsChanged(): void
{
$repo = $this->createMock(HandoverRepository::class);
$revisionService = $this->createMock(HandoverRevisionService::class);
$revisionService->expects($this->once())
->method('createRevision')
->with(
$this->anything(),
$this->anything(),
$this->anything(),
$this->anything(),
$this->anything(),
$this->anything(),
HandoverRevisionService::CHANGE_TYPE_FIELDS
);
$service = $this->createService($repo, null, $revisionService);
$service->createRevisionAfterSave(self::TENANT_ID, 1, ['name' => 'Test'], 'draft', true, false, 1, self::USER_ID);
}
public function testCreateRevisionAfterSaveSetsStatusWhenOnlyStatusChanged(): void
{
$repo = $this->createMock(HandoverRepository::class);
$revisionService = $this->createMock(HandoverRevisionService::class);
$revisionService->expects($this->once())
->method('createRevision')
->with(
self::TENANT_ID,
1,
['name' => 'Test'],
'completed',
1,
self::USER_ID,
HandoverRevisionService::CHANGE_TYPE_STATUS
);
$service = $this->createService($repo, null, $revisionService);
$service->createRevisionAfterSave(self::TENANT_ID, 1, ['name' => 'Test'], 'completed', false, true, 1, self::USER_ID);
}
public function testCreateRevisionAfterSaveDefaultsToFieldsWhenNoContentChanged(): void
{
$repo = $this->createMock(HandoverRepository::class);
$revisionService = $this->createMock(HandoverRevisionService::class);
$revisionService->expects($this->once())
->method('createRevision')
->with(
self::TENANT_ID,
1,
['name' => 'Test'],
'draft',
1,
self::USER_ID,
HandoverRevisionService::CHANGE_TYPE_FIELDS
);
$service = $this->createService($repo, null, $revisionService);
$service->createRevisionAfterSave(self::TENANT_ID, 1, ['name' => 'Test'], 'draft', false, false, 1, self::USER_ID);
}
// ── Delete tests ─────────────────────────────────────────────────
public function testDeleteByIdsHappyPath(): void
{
$repo = $this->createMock(HandoverRepository::class);
$repo->expects($this->once())
->method('deleteByIds')
->with(self::TENANT_ID, [1, 2, 3])
->willReturn(3);
$service = $this->createService($repo);
$result = $service->deleteByIds(self::TENANT_ID, [1, 2, 3], HandoverService::PERMISSION_MANAGE);
$this->assertTrue($result['ok']);
$this->assertSame(3, $result['count']);
}
public function testDeleteByIdsDeniedForCreatePermission(): void
{
$repo = $this->createMock(HandoverRepository::class);
$repo->expects($this->never())->method('deleteByIds');
$service = $this->createService($repo);
$result = $service->deleteByIds(self::TENANT_ID, [1], HandoverService::PERMISSION_CREATE);
$this->assertFalse($result['ok']);
$this->assertSame(0, $result['count']);
}
public function testDeleteByIdsWithEmptyArray(): void
{
$repo = $this->createMock(HandoverRepository::class);
$repo->expects($this->never())->method('deleteByIds');
$service = $this->createService($repo);
$result = $service->deleteByIds(self::TENANT_ID, [], HandoverService::PERMISSION_MANAGE);
$this->assertFalse($result['ok']);
}
}