test(helpdesk): add tests for assign, submitForReview, resendNotification and HandoverNotificationService

HandoverServiceTest: 30 new cases covering:
- assign(): happy path, draft→in_progress transition, permission check,
  zero assignedTo, not found, email sent, due date passed to repo
- submitForReview(): assignee and manager paths, non-assignee denied,
  wrong status (under_review/completed), revision created, email sent
- resendNotification(): happy path, permission denied, no assignee
- statusLabel/Variant for under_review

HandoverNotificationServiceTest: 9 new cases covering:
- notifyAssigned: sends correct template+vars, skips on empty/null email,
  fallback dash for empty debitor and due_date
- notifyReviewRequested: sends to manager, skips on no email,
  fallback dash for empty assignee name

48 tests total, all passing.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
aminovfariz
2026-06-01 14:59:10 +02:00
parent 88bd7caae5
commit 47a26d0ca6
2 changed files with 456 additions and 1 deletions

View File

@@ -0,0 +1,153 @@
<?php
namespace MintyPHP\Tests\Module\Helpdesk\Service;
use MintyPHP\Module\Helpdesk\Service\HandoverNotificationService;
use MintyPHP\Service\Mail\MailService;
use PHPUnit\Framework\TestCase;
class HandoverNotificationServiceTest extends TestCase
{
private function createNotificationService(?MailService $mailService = null): HandoverNotificationService
{
return new HandoverNotificationService(
$mailService ?? $this->createMock(MailService::class)
);
}
private function assignedHandover(array $overrides = []): array
{
return array_merge([
'id' => 1,
'debitor_name' => 'Acme Corp',
'domain_url' => 'example.com',
'due_date' => '2026-12-31',
'assigned_to_email' => 'employee@example.com',
'assigned_to_label' => 'John Doe',
'assigned_by_label' => 'Manager',
'assigned_by_email' => 'manager@example.com',
], $overrides);
}
// ── notifyAssigned ────────────────────────────────────────────────
public function testNotifyAssignedSendsEmailToAssignee(): void
{
$mailService = $this->createMock(MailService::class);
$mailService->expects($this->once())
->method('sendTemplate')
->with(
'handover_assigned',
$this->callback(fn (array $vars): bool => $vars['assignee_name'] === 'John Doe'
&& $vars['assigned_by_name'] === 'Manager'
&& $vars['debitor_name'] === 'Acme Corp'
&& $vars['due_date'] === '2026-12-31'
),
'employee@example.com',
$this->isType('string')
);
$service = $this->createNotificationService($mailService);
$service->notifyAssigned($this->assignedHandover());
}
public function testNotifyAssignedSkipsWhenNoEmail(): void
{
$mailService = $this->createMock(MailService::class);
$mailService->expects($this->never())->method('sendTemplate');
$service = $this->createNotificationService($mailService);
$service->notifyAssigned($this->assignedHandover(['assigned_to_email' => '']));
}
public function testNotifyAssignedSkipsWhenEmailIsNull(): void
{
$mailService = $this->createMock(MailService::class);
$mailService->expects($this->never())->method('sendTemplate');
$service = $this->createNotificationService($mailService);
$service->notifyAssigned($this->assignedHandover(['assigned_to_email' => null]));
}
public function testNotifyAssignedFallsBackToDashForEmptyDebitor(): void
{
$mailService = $this->createMock(MailService::class);
$mailService->expects($this->once())
->method('sendTemplate')
->with(
'handover_assigned',
$this->callback(fn (array $vars): bool => $vars['debitor_name'] === '—'),
$this->anything(),
$this->anything()
);
$service = $this->createNotificationService($mailService);
$service->notifyAssigned($this->assignedHandover(['debitor_name' => '']));
}
public function testNotifyAssignedFallsBackToDashForEmptyDueDate(): void
{
$mailService = $this->createMock(MailService::class);
$mailService->expects($this->once())
->method('sendTemplate')
->with(
'handover_assigned',
$this->callback(fn (array $vars): bool => $vars['due_date'] === '—'),
$this->anything(),
$this->anything()
);
$service = $this->createNotificationService($mailService);
$service->notifyAssigned($this->assignedHandover(['due_date' => '']));
}
// ── notifyReviewRequested ─────────────────────────────────────────
public function testNotifyReviewRequestedSendsEmailToManager(): void
{
$mailService = $this->createMock(MailService::class);
$mailService->expects($this->once())
->method('sendTemplate')
->with(
'handover_review_requested',
$this->callback(fn (array $vars): bool => $vars['manager_name'] === 'Manager'
&& $vars['assignee_name'] === 'John Doe'
&& $vars['debitor_name'] === 'Acme Corp'
),
'manager@example.com',
$this->isType('string')
);
$service = $this->createNotificationService($mailService);
$service->notifyReviewRequested($this->assignedHandover([
'assigned_to_label' => 'John Doe',
'assigned_by_label' => 'Manager',
'assigned_by_email' => 'manager@example.com',
]));
}
public function testNotifyReviewRequestedSkipsWhenNoManagerEmail(): void
{
$mailService = $this->createMock(MailService::class);
$mailService->expects($this->never())->method('sendTemplate');
$service = $this->createNotificationService($mailService);
$service->notifyReviewRequested($this->assignedHandover(['assigned_by_email' => '']));
}
public function testNotifyReviewRequestedFallsBackToDashForEmptyAssigneeName(): void
{
$mailService = $this->createMock(MailService::class);
$mailService->expects($this->once())
->method('sendTemplate')
->with(
'handover_review_requested',
$this->callback(fn (array $vars): bool => $vars['assignee_name'] === '—'),
$this->anything(),
$this->anything()
);
$service = $this->createNotificationService($mailService);
$service->notifyReviewRequested($this->assignedHandover(['assigned_to_label' => '']));
}
}

View File

@@ -3,6 +3,7 @@
namespace MintyPHP\Tests\Module\Helpdesk\Service;
use MintyPHP\Module\Helpdesk\Repository\HandoverRepository;
use MintyPHP\Module\Helpdesk\Service\HandoverNotificationService;
use MintyPHP\Module\Helpdesk\Service\HandoverRevisionService;
use MintyPHP\Module\Helpdesk\Service\HandoverService;
use MintyPHP\Module\Helpdesk\Service\SoftwareProductService;
@@ -19,11 +20,30 @@ class HandoverServiceTest extends TestCase
?HandoverRepository $repository = null,
?SoftwareProductService $productService = null,
?HandoverRevisionService $revisionService = null,
?HandoverNotificationService $notificationService = null,
): HandoverService {
$repository = $repository ?? $this->createMock(HandoverRepository::class);
$productService = $productService ?? $this->createMock(SoftwareProductService::class);
return new HandoverService($repository, $productService, $revisionService);
return new HandoverService($repository, $productService, $revisionService, $notificationService);
}
private function handoverRow(array $overrides = []): array
{
return array_merge([
'id' => 1,
'tenant_id' => self::TENANT_ID,
'status' => HandoverService::STATUS_IN_PROGRESS,
'assigned_to' => 0,
'assigned_by' => 0,
'debitor_name' => 'Acme Corp',
'domain_url' => 'example.com',
'due_date' => null,
'assigned_to_label' => null,
'assigned_to_email' => null,
'assigned_by_label' => null,
'assigned_by_email' => null,
], $overrides);
}
private function mockProductService(bool $exists = true, bool $hasSchema = true): SoftwareProductService
@@ -332,6 +352,288 @@ class HandoverServiceTest extends TestCase
$service->createRevisionAfterSave(self::TENANT_ID, 1, ['name' => 'Test'], 'draft', false, false, 1, self::USER_ID);
}
// ── Assign tests ─────────────────────────────────────────────────
public function testAssignHappyPath(): void
{
$repo = $this->createMock(HandoverRepository::class);
$repo->method('findById')->willReturn($this->handoverRow(['status' => HandoverService::STATUS_IN_PROGRESS]));
$repo->expects($this->once())->method('assign')->willReturn(true);
$repo->expects($this->never())->method('updateStatus');
$service = $this->createService($repo);
$result = $service->assign(self::TENANT_ID, 1, 99, self::USER_ID, null, HandoverService::PERMISSION_MANAGE);
$this->assertTrue($result['ok']);
$this->assertEmpty($result['errors']);
}
public function testAssignDraftHandoverTransitionsToInProgress(): void
{
$repo = $this->createMock(HandoverRepository::class);
$repo->method('findById')->willReturn($this->handoverRow(['status' => HandoverService::STATUS_DRAFT]));
$repo->method('assign')->willReturn(true);
$repo->expects($this->once())
->method('updateStatus')
->with(self::TENANT_ID, 1, HandoverService::STATUS_IN_PROGRESS, self::USER_ID);
$service = $this->createService($repo);
$result = $service->assign(self::TENANT_ID, 1, 99, self::USER_ID, null, HandoverService::PERMISSION_MANAGE);
$this->assertTrue($result['ok']);
}
public function testAssignDeniedForCreatePermission(): void
{
$repo = $this->createMock(HandoverRepository::class);
$repo->expects($this->never())->method('assign');
$service = $this->createService($repo);
$result = $service->assign(self::TENANT_ID, 1, 99, self::USER_ID, null, HandoverService::PERMISSION_CREATE);
$this->assertFalse($result['ok']);
$this->assertArrayHasKey('general', $result['errors']);
}
public function testAssignFailsWhenAssignedToIsZero(): void
{
$repo = $this->createMock(HandoverRepository::class);
$repo->method('findById')->willReturn($this->handoverRow());
$repo->expects($this->never())->method('assign');
$service = $this->createService($repo);
$result = $service->assign(self::TENANT_ID, 1, 0, self::USER_ID, null, HandoverService::PERMISSION_MANAGE);
$this->assertFalse($result['ok']);
$this->assertArrayHasKey('assigned_to', $result['errors']);
}
public function testAssignFailsWhenHandoverNotFound(): void
{
$repo = $this->createMock(HandoverRepository::class);
$repo->method('findById')->willReturn(null);
$service = $this->createService($repo);
$result = $service->assign(self::TENANT_ID, 1, 99, self::USER_ID, null, HandoverService::PERMISSION_MANAGE);
$this->assertFalse($result['ok']);
$this->assertArrayHasKey('general', $result['errors']);
}
public function testAssignSendsNotificationEmail(): void
{
$repo = $this->createMock(HandoverRepository::class);
$repo->method('findById')->willReturn($this->handoverRow([
'assigned_to_email' => 'employee@example.com',
'assigned_to_label' => 'John Doe',
]));
$repo->method('assign')->willReturn(true);
$notificationService = $this->createMock(HandoverNotificationService::class);
$notificationService->expects($this->once())->method('notifyAssigned');
$service = $this->createService($repo, notificationService: $notificationService);
$service->assign(self::TENANT_ID, 1, 99, self::USER_ID, null, HandoverService::PERMISSION_MANAGE);
}
public function testAssignWithDueDatePassesItToRepository(): void
{
$repo = $this->createMock(HandoverRepository::class);
$repo->method('findById')->willReturn($this->handoverRow());
$repo->expects($this->once())
->method('assign')
->with(self::TENANT_ID, 1, 99, self::USER_ID, '2026-12-31', self::USER_ID)
->willReturn(true);
$service = $this->createService($repo);
$result = $service->assign(self::TENANT_ID, 1, 99, self::USER_ID, '2026-12-31', HandoverService::PERMISSION_MANAGE);
$this->assertTrue($result['ok']);
}
// ── Submit for review tests ───────────────────────────────────────
public function testSubmitForReviewHappyPathByAssignee(): void
{
$assigneeId = 99;
$repo = $this->createMock(HandoverRepository::class);
$repo->method('findById')->willReturn($this->handoverRow([
'assigned_to' => $assigneeId,
'status' => HandoverService::STATUS_IN_PROGRESS,
]));
$repo->expects($this->once())
->method('updateStatus')
->with(self::TENANT_ID, 1, HandoverService::STATUS_UNDER_REVIEW, $assigneeId)
->willReturn(true);
$service = $this->createService($repo);
$result = $service->submitForReview(self::TENANT_ID, 1, $assigneeId, HandoverService::PERMISSION_CREATE);
$this->assertTrue($result['ok']);
}
public function testSubmitForReviewAllowedForManager(): void
{
$repo = $this->createMock(HandoverRepository::class);
$repo->method('findById')->willReturn($this->handoverRow([
'assigned_to' => 99,
'status' => HandoverService::STATUS_DRAFT,
]));
$repo->method('updateStatus')->willReturn(true);
$service = $this->createService($repo);
$result = $service->submitForReview(self::TENANT_ID, 1, self::USER_ID, HandoverService::PERMISSION_MANAGE);
$this->assertTrue($result['ok']);
}
public function testSubmitForReviewDeniedForNonAssignee(): void
{
$repo = $this->createMock(HandoverRepository::class);
$repo->method('findById')->willReturn($this->handoverRow([
'assigned_to' => 99,
'status' => HandoverService::STATUS_IN_PROGRESS,
]));
$service = $this->createService($repo);
$result = $service->submitForReview(self::TENANT_ID, 1, self::USER_ID, HandoverService::PERMISSION_CREATE);
$this->assertFalse($result['ok']);
$this->assertArrayHasKey('general', $result['errors']);
}
public function testSubmitForReviewDeniedWhenStatusIsUnderReview(): void
{
$assigneeId = 99;
$repo = $this->createMock(HandoverRepository::class);
$repo->method('findById')->willReturn($this->handoverRow([
'assigned_to' => $assigneeId,
'status' => HandoverService::STATUS_UNDER_REVIEW,
]));
$service = $this->createService($repo);
$result = $service->submitForReview(self::TENANT_ID, 1, $assigneeId, HandoverService::PERMISSION_CREATE);
$this->assertFalse($result['ok']);
$this->assertArrayHasKey('status', $result['errors']);
}
public function testSubmitForReviewDeniedWhenStatusIsCompleted(): void
{
$assigneeId = 99;
$repo = $this->createMock(HandoverRepository::class);
$repo->method('findById')->willReturn($this->handoverRow([
'assigned_to' => $assigneeId,
'status' => HandoverService::STATUS_COMPLETED,
]));
$service = $this->createService($repo);
$result = $service->submitForReview(self::TENANT_ID, 1, $assigneeId, HandoverService::PERMISSION_CREATE);
$this->assertFalse($result['ok']);
$this->assertArrayHasKey('status', $result['errors']);
}
public function testSubmitForReviewSendsReviewRequestedEmail(): void
{
$assigneeId = 99;
$repo = $this->createMock(HandoverRepository::class);
$repo->method('findById')->willReturn($this->handoverRow([
'assigned_to' => $assigneeId,
'status' => HandoverService::STATUS_IN_PROGRESS,
'assigned_by_email' => 'manager@example.com',
]));
$repo->method('updateStatus')->willReturn(true);
$notificationService = $this->createMock(HandoverNotificationService::class);
$notificationService->expects($this->once())->method('notifyReviewRequested');
$service = $this->createService($repo, notificationService: $notificationService);
$service->submitForReview(self::TENANT_ID, 1, $assigneeId, HandoverService::PERMISSION_CREATE);
}
public function testSubmitForReviewCreatesRevision(): void
{
$assigneeId = 99;
$repo = $this->createMock(HandoverRepository::class);
$repo->method('findById')->willReturn($this->handoverRow([
'assigned_to' => $assigneeId,
'status' => HandoverService::STATUS_IN_PROGRESS,
]));
$repo->method('updateStatus')->willReturn(true);
$revisionService = $this->createMock(HandoverRevisionService::class);
$revisionService->expects($this->once())
->method('createRevision')
->with(
self::TENANT_ID,
1,
[],
HandoverService::STATUS_UNDER_REVIEW,
1,
$assigneeId,
HandoverRevisionService::CHANGE_TYPE_STATUS
);
$service = $this->createService($repo, revisionService: $revisionService);
$service->submitForReview(self::TENANT_ID, 1, $assigneeId, HandoverService::PERMISSION_CREATE);
}
// ── Resend notification tests ─────────────────────────────────────
public function testResendNotificationHappyPath(): void
{
$repo = $this->createMock(HandoverRepository::class);
$repo->method('findById')->willReturn($this->handoverRow(['assigned_to' => 99]));
$notificationService = $this->createMock(HandoverNotificationService::class);
$notificationService->expects($this->once())->method('notifyAssigned');
$service = $this->createService($repo, notificationService: $notificationService);
$result = $service->resendNotification(self::TENANT_ID, 1, HandoverService::PERMISSION_MANAGE);
$this->assertTrue($result['ok']);
}
public function testResendNotificationDeniedForCreatePermission(): void
{
$notificationService = $this->createMock(HandoverNotificationService::class);
$notificationService->expects($this->never())->method('notifyAssigned');
$service = $this->createService(notificationService: $notificationService);
$result = $service->resendNotification(self::TENANT_ID, 1, HandoverService::PERMISSION_CREATE);
$this->assertFalse($result['ok']);
$this->assertArrayHasKey('general', $result['errors']);
}
public function testResendNotificationFailsWhenNoAssignee(): void
{
$repo = $this->createMock(HandoverRepository::class);
$repo->method('findById')->willReturn($this->handoverRow(['assigned_to' => 0]));
$notificationService = $this->createMock(HandoverNotificationService::class);
$notificationService->expects($this->never())->method('notifyAssigned');
$service = $this->createService($repo, notificationService: $notificationService);
$result = $service->resendNotification(self::TENANT_ID, 1, HandoverService::PERMISSION_MANAGE);
$this->assertFalse($result['ok']);
$this->assertArrayHasKey('general', $result['errors']);
}
// ── Status label / variant tests ─────────────────────────────────
public function testStatusLabelUnderReview(): void
{
$this->assertNotEmpty(HandoverService::statusLabel(HandoverService::STATUS_UNDER_REVIEW));
}
public function testStatusVariantUnderReview(): void
{
$this->assertSame('info', HandoverService::statusVariant(HandoverService::STATUS_UNDER_REVIEW));
}
// ── Delete tests ─────────────────────────────────────────────────
public function testDeleteByIdsHappyPath(): void