feat(scheduler): dispatch event on job failure, notify admins via notifications module
Add ?ModuleEventDispatcher to SchedulerRunService (fail-open, nullable). Dispatch scheduler.job_failed event when a job fails with payload: job_id, job_key, label, error_code, error_message, trigger_type. New SchedulerJobFailedNotificationListener in notifications module creates in-app admin notifications for users with JOBS_MANAGE permission, scoped per tenant, with 30-min dedup per job_key. Also fixes audit module manifest: system_audit_purge job_key was mismatched (audit_system_purge vs system_audit_purge in DB). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,130 @@
|
||||
<?php
|
||||
|
||||
namespace MintyPHP\Tests\Module\Notifications\Listeners;
|
||||
|
||||
use MintyPHP\Module\Notifications\Listeners\SchedulerJobFailedNotificationListener;
|
||||
use MintyPHP\Module\Notifications\Service\NotificationMessage;
|
||||
use MintyPHP\Module\Notifications\Service\NotificationService;
|
||||
use MintyPHP\Repository\Tenant\UserTenantRepositoryInterface;
|
||||
use MintyPHP\Repository\User\UserReadRepositoryInterface;
|
||||
use MintyPHP\Service\Access\PermissionService;
|
||||
use PHPUnit\Framework\MockObject\MockObject;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
class SchedulerJobFailedNotificationListenerTest extends TestCase
|
||||
{
|
||||
private NotificationService&MockObject $notifService;
|
||||
private UserReadRepositoryInterface&MockObject $userRepo;
|
||||
private UserTenantRepositoryInterface&MockObject $utRepo;
|
||||
private SchedulerJobFailedNotificationListener $listener;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
$this->notifService = $this->createMock(NotificationService::class);
|
||||
$this->userRepo = $this->createMock(UserReadRepositoryInterface::class);
|
||||
$this->utRepo = $this->createMock(UserTenantRepositoryInterface::class);
|
||||
$this->listener = new SchedulerJobFailedNotificationListener($this->notifService, $this->userRepo, $this->utRepo);
|
||||
}
|
||||
|
||||
public function testHandleCreatesNotificationsForAdminsAcrossTenants(): void
|
||||
{
|
||||
$this->userRepo->expects($this->any())->method('listPrivilegedUserIdsByPermissionKeys')
|
||||
->with([PermissionService::JOBS_MANAGE])
|
||||
->willReturn([20, 30]);
|
||||
|
||||
$this->utRepo->expects($this->exactly(2))
|
||||
->method('listTenantIdsByUserId')
|
||||
->willReturnMap([
|
||||
[20, [1, 2]],
|
||||
[30, [2, 3]],
|
||||
]);
|
||||
|
||||
$this->notifService->expects($this->exactly(3))
|
||||
->method('createForTenantAdminUsersFromMessage')
|
||||
->willReturnCallback(function (int $tenantId, NotificationMessage $message, ?int $excludeUserId, array $adminSet): int {
|
||||
$this->assertContains($tenantId, [1, 2, 3]);
|
||||
$this->assertSame('scheduler.job_failed', $message->type());
|
||||
$this->assertStringContainsString('System audit purge', $message->title());
|
||||
$this->assertSame('admin/scheduled-jobs', $message->link());
|
||||
$this->assertNull($excludeUserId);
|
||||
$this->assertArrayHasKey(20, $adminSet);
|
||||
$this->assertArrayHasKey(30, $adminSet);
|
||||
$this->assertSame('Scheduled job failed: %s', $message->titleKey());
|
||||
$data = $message->data();
|
||||
$this->assertSame('job:system_audit_purge', $data['dedupe_target']);
|
||||
$this->assertSame('job_not_registered', $data['error_code']);
|
||||
return 1;
|
||||
});
|
||||
|
||||
$this->listener->handle('scheduler.job_failed', [
|
||||
'job_id' => 5,
|
||||
'job_key' => 'system_audit_purge',
|
||||
'label' => 'System audit purge',
|
||||
'error_code' => 'job_not_registered',
|
||||
'error_message' => null,
|
||||
'trigger_type' => 'scheduler',
|
||||
]);
|
||||
}
|
||||
|
||||
public function testHandleUsesJobKeyAsLabelWhenLabelEmpty(): void
|
||||
{
|
||||
$this->userRepo->expects($this->any())->method('listPrivilegedUserIdsByPermissionKeys')->willReturn([10]);
|
||||
$this->utRepo->expects($this->any())->method('listTenantIdsByUserId')->willReturn([1]);
|
||||
|
||||
$this->notifService->expects($this->once())
|
||||
->method('createForTenantAdminUsersFromMessage')
|
||||
->willReturnCallback(function (int $tenantId, NotificationMessage $message): int {
|
||||
$this->assertStringContainsString('my_job_key', $message->title());
|
||||
return 1;
|
||||
});
|
||||
|
||||
$this->listener->handle('scheduler.job_failed', [
|
||||
'job_id' => 1,
|
||||
'job_key' => 'my_job_key',
|
||||
'label' => '',
|
||||
'error_code' => 'unexpected_error',
|
||||
]);
|
||||
}
|
||||
|
||||
public function testHandleSkipsEmptyPayload(): void
|
||||
{
|
||||
$this->notifService->expects($this->never())->method('createForTenantAdminUsersFromMessage');
|
||||
|
||||
$this->listener->handle('scheduler.job_failed', []);
|
||||
$this->listener->handle('scheduler.job_failed', ['job_key' => '', 'job_id' => 0]);
|
||||
$this->listener->handle('scheduler.job_failed', ['job_key' => 'test', 'job_id' => 0]);
|
||||
}
|
||||
|
||||
public function testHandleSkipsWhenNoAdmins(): void
|
||||
{
|
||||
$this->userRepo->expects($this->any())->method('listPrivilegedUserIdsByPermissionKeys')->willReturn([]);
|
||||
$this->notifService->expects($this->never())->method('createForTenantAdminUsersFromMessage');
|
||||
|
||||
$this->listener->handle('scheduler.job_failed', [
|
||||
'job_id' => 5,
|
||||
'job_key' => 'some_job',
|
||||
'label' => 'Some Job',
|
||||
'error_code' => 'unexpected_error',
|
||||
]);
|
||||
}
|
||||
|
||||
public function testHandleOmitsBodyWhenNoErrorCode(): void
|
||||
{
|
||||
$this->userRepo->expects($this->any())->method('listPrivilegedUserIdsByPermissionKeys')->willReturn([10]);
|
||||
$this->utRepo->expects($this->any())->method('listTenantIdsByUserId')->willReturn([1]);
|
||||
|
||||
$this->notifService->expects($this->once())
|
||||
->method('createForTenantAdminUsersFromMessage')
|
||||
->willReturnCallback(function (int $tenantId, NotificationMessage $message): int {
|
||||
$this->assertNull($message->body());
|
||||
return 1;
|
||||
});
|
||||
|
||||
$this->listener->handle('scheduler.job_failed', [
|
||||
'job_id' => 1,
|
||||
'job_key' => 'test_job',
|
||||
'label' => 'Test',
|
||||
'error_code' => '',
|
||||
]);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user