complete story 8.10 with qa test

This commit is contained in:
Naser Mansour
2026-01-02 23:14:53 +02:00
parent 78b3a01c4d
commit 97dca05562
13 changed files with 908 additions and 29 deletions
@@ -0,0 +1,120 @@
<?php
use App\Notifications\QueueFailureNotification;
use Illuminate\Contracts\Queue\Job;
use Illuminate\Notifications\AnonymousNotifiable;
use Illuminate\Queue\Events\JobFailed;
use Illuminate\Support\Facades\Notification;
beforeEach(function () {
config([
'libra.notifications.system_notifications_enabled' => true,
'libra.notifications.admin_email' => 'admin@libra.ps',
'libra.notifications.throttle_minutes' => 15,
]);
});
test('queue failure notification contains job information', function () {
$job = Mockery::mock(Job::class);
$job->shouldReceive('resolveName')->andReturn('App\\Jobs\\SendEmailJob');
$job->shouldReceive('getQueue')->andReturn('default');
$job->shouldReceive('attempts')->andReturn(3);
$exception = new RuntimeException('Job processing failed');
$event = new JobFailed('database', $job, $exception);
$notification = new QueueFailureNotification($event);
$mail = $notification->toMail(new AnonymousNotifiable);
expect($mail->subject)->toContain('[URGENT]');
expect($mail->subject)->toContain('Queue Job Failed');
expect($mail->subject)->toContain('SendEmailJob');
expect($mail->subject)->toContain('Libra Law Firm');
});
test('queue failure notification includes environment', function () {
$job = Mockery::mock(Job::class);
$job->shouldReceive('resolveName')->andReturn('TestJob');
$job->shouldReceive('getQueue')->andReturn('default');
$job->shouldReceive('attempts')->andReturn(1);
$exception = new RuntimeException('Failed');
$event = new JobFailed('database', $job, $exception);
$notification = new QueueFailureNotification($event);
$mail = $notification->toMail(new AnonymousNotifiable);
expect($mail->subject)->toContain('['.app()->environment().']');
});
test('queue failure notification array contains required data', function () {
$job = Mockery::mock(Job::class);
$job->shouldReceive('resolveName')->andReturn('App\\Jobs\\TestJob');
$job->shouldReceive('getQueue')->andReturn('emails');
$job->shouldReceive('attempts')->andReturn(2);
$exception = new RuntimeException('Test failure');
$event = new JobFailed('database', $job, $exception);
$notification = new QueueFailureNotification($event);
$array = $notification->toArray(new AnonymousNotifiable);
expect($array)->toHaveKey('type', 'queue_failure');
expect($array)->toHaveKey('job', 'App\\Jobs\\TestJob');
expect($array)->toHaveKey('queue', 'emails');
});
test('queue failure listener sends notification when enabled', function () {
Notification::fake();
$job = Mockery::mock(Job::class);
$job->shouldReceive('resolveName')->andReturn('TestJob');
$job->shouldReceive('getQueue')->andReturn('default');
$job->shouldReceive('attempts')->andReturn(1);
$exception = new RuntimeException('Failed');
$event = new JobFailed('database', $job, $exception);
event($event);
Notification::assertSentOnDemand(
QueueFailureNotification::class,
function ($notification, $channels, $notifiable) {
return $notifiable->routes['mail'] === config('libra.notifications.admin_email');
}
);
});
test('queue failure listener does not send notification when disabled', function () {
Notification::fake();
config(['libra.notifications.system_notifications_enabled' => false]);
$job = Mockery::mock(Job::class);
$job->shouldReceive('resolveName')->andReturn('TestJob');
$job->shouldReceive('getQueue')->andReturn('default');
$job->shouldReceive('attempts')->andReturn(1);
$exception = new RuntimeException('Failed');
$event = new JobFailed('database', $job, $exception);
event($event);
Notification::assertNothingSent();
});
test('queue failure listener does not send notification when admin email not configured', function () {
Notification::fake();
config(['libra.notifications.admin_email' => null]);
$job = Mockery::mock(Job::class);
$job->shouldReceive('resolveName')->andReturn('TestJob');
$job->shouldReceive('getQueue')->andReturn('default');
$job->shouldReceive('attempts')->andReturn(1);
$exception = new RuntimeException('Failed');
$event = new JobFailed('database', $job, $exception);
event($event);
Notification::assertNothingSent();
});
@@ -0,0 +1,191 @@
<?php
use App\Contracts\ShouldNotifyAdmin;
use App\Notifications\SystemErrorNotification;
use App\Services\AdminNotificationService;
use Illuminate\Notifications\AnonymousNotifiable;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\Notification;
beforeEach(function () {
config([
'libra.notifications.system_notifications_enabled' => true,
'libra.notifications.admin_email' => 'admin@libra.ps',
'libra.notifications.throttle_minutes' => 15,
]);
Cache::flush();
});
test('critical exception triggers admin notification via service', function () {
Notification::fake();
$exception = new class('Critical payment error') extends Exception implements ShouldNotifyAdmin {};
$service = app(AdminNotificationService::class);
if ($service->shouldNotifyAdmin($exception)) {
$service->notifyError($exception);
}
Notification::assertSentOnDemand(
SystemErrorNotification::class,
function ($notification, $channels, $notifiable) {
return $notifiable->routes['mail'] === config('libra.notifications.admin_email');
}
);
});
test('validation exception does not trigger admin notification', function () {
Notification::fake();
try {
validator([], ['email' => 'required'])->validate();
} catch (\Illuminate\Validation\ValidationException $e) {
$service = app(AdminNotificationService::class);
if ($service->shouldNotifyAdmin($e)) {
$service->notifyError($e);
}
}
Notification::assertNothingSent();
});
test('authentication exception does not trigger admin notification', function () {
Notification::fake();
$exception = new \Illuminate\Auth\AuthenticationException;
$service = app(AdminNotificationService::class);
if ($service->shouldNotifyAdmin($exception)) {
$service->notifyError($exception);
}
Notification::assertNothingSent();
});
test('authorization exception does not trigger admin notification', function () {
Notification::fake();
$exception = new \Illuminate\Auth\Access\AuthorizationException;
$service = app(AdminNotificationService::class);
if ($service->shouldNotifyAdmin($exception)) {
$service->notifyError($exception);
}
Notification::assertNothingSent();
});
test('model not found exception does not trigger admin notification', function () {
Notification::fake();
$exception = new \Illuminate\Database\Eloquent\ModelNotFoundException;
$service = app(AdminNotificationService::class);
if ($service->shouldNotifyAdmin($exception)) {
$service->notifyError($exception);
}
Notification::assertNothingSent();
});
test('http 404 exception does not trigger admin notification', function () {
Notification::fake();
$exception = new \Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
$service = app(AdminNotificationService::class);
if ($service->shouldNotifyAdmin($exception)) {
$service->notifyError($exception);
}
Notification::assertNothingSent();
});
test('system error notification contains required information', function () {
$exception = new RuntimeException('Test error message');
$notification = new SystemErrorNotification($exception);
$mail = $notification->toMail(new AnonymousNotifiable);
expect($mail->subject)->toContain('[URGENT]');
expect($mail->subject)->toContain('RuntimeException');
expect($mail->subject)->toContain('Libra Law Firm');
});
test('system error notification includes environment', function () {
$exception = new RuntimeException('Test error');
$notification = new SystemErrorNotification($exception);
$mail = $notification->toMail(new AnonymousNotifiable);
expect($mail->subject)->toContain('['.app()->environment().']');
});
test('system error notification sent immediately not queued', function () {
Notification::fake();
$exception = new class('Critical error') extends Exception implements ShouldNotifyAdmin {};
$service = app(AdminNotificationService::class);
$service->notifyError($exception);
Notification::assertSentOnDemand(SystemErrorNotification::class);
});
test('rate limiting prevents duplicate notifications within throttle period', function () {
Notification::fake();
$exception = new class('Critical error') extends Exception implements ShouldNotifyAdmin {};
$service = app(AdminNotificationService::class);
$service->notifyError($exception);
$service->notifyError($exception);
$service->notifyError($exception);
Notification::assertSentOnDemandTimes(SystemErrorNotification::class, 1);
});
test('different exceptions are not rate limited together', function () {
Notification::fake();
$exception1 = new class('Error 1') extends Exception implements ShouldNotifyAdmin {};
$exception2 = new class('Error 2') extends Exception implements ShouldNotifyAdmin {};
$service = app(AdminNotificationService::class);
$service->notifyError($exception1);
$service->notifyError($exception2);
Notification::assertSentOnDemandTimes(SystemErrorNotification::class, 2);
});
test('notification not sent when system notifications disabled', function () {
Notification::fake();
config(['libra.notifications.system_notifications_enabled' => false]);
$exception = new class('Critical error') extends Exception implements ShouldNotifyAdmin {};
$service = app(AdminNotificationService::class);
$service->notifyError($exception);
Notification::assertNothingSent();
});
test('notification not sent when admin email not configured', function () {
Notification::fake();
config(['libra.notifications.admin_email' => null]);
$exception = new class('Critical error') extends Exception implements ShouldNotifyAdmin {};
$service = app(AdminNotificationService::class);
$service->notifyError($exception);
Notification::assertNothingSent();
});
@@ -0,0 +1,187 @@
<?php
use App\Contracts\ShouldNotifyAdmin;
use App\Notifications\SystemErrorNotification;
use App\Services\AdminNotificationService;
use Illuminate\Auth\Access\AuthorizationException;
use Illuminate\Auth\AuthenticationException;
use Illuminate\Database\Eloquent\ModelNotFoundException;
use Illuminate\Database\QueryException;
use Illuminate\Session\TokenMismatchException;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\Notification;
use Illuminate\Validation\ValidationException;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
use Symfony\Component\Mailer\Exception\TransportException;
beforeEach(function () {
config([
'libra.notifications.system_notifications_enabled' => true,
'libra.notifications.admin_email' => 'admin@test.com',
'libra.notifications.throttle_minutes' => 15,
]);
});
describe('shouldNotifyAdmin', function () {
it('returns false for validation exceptions', function () {
$service = new AdminNotificationService;
$exception = ValidationException::withMessages(['field' => ['error']]);
expect($service->shouldNotifyAdmin($exception))->toBeFalse();
});
it('returns false for authentication exceptions', function () {
$service = new AdminNotificationService;
$exception = new AuthenticationException;
expect($service->shouldNotifyAdmin($exception))->toBeFalse();
});
it('returns false for authorization exceptions', function () {
$service = new AdminNotificationService;
$exception = new AuthorizationException;
expect($service->shouldNotifyAdmin($exception))->toBeFalse();
});
it('returns false for model not found exceptions', function () {
$service = new AdminNotificationService;
$exception = new ModelNotFoundException;
expect($service->shouldNotifyAdmin($exception))->toBeFalse();
});
it('returns false for not found http exceptions', function () {
$service = new AdminNotificationService;
$exception = new NotFoundHttpException;
expect($service->shouldNotifyAdmin($exception))->toBeFalse();
});
it('returns false for token mismatch exceptions', function () {
$service = new AdminNotificationService;
$exception = new TokenMismatchException;
expect($service->shouldNotifyAdmin($exception))->toBeFalse();
});
it('returns true for exceptions implementing ShouldNotifyAdmin', function () {
$service = new AdminNotificationService;
$exception = new class extends Exception implements ShouldNotifyAdmin {};
expect($service->shouldNotifyAdmin($exception))->toBeTrue();
});
it('returns true for mail transport exceptions', function () {
$service = new AdminNotificationService;
$exception = new TransportException('SMTP error');
expect($service->shouldNotifyAdmin($exception))->toBeTrue();
});
it('returns true for critical database connection errors', function () {
$service = new AdminNotificationService;
$pdo = new PDOException('Connection refused');
$exception = new QueryException('mysql', 'SELECT 1', [], $pdo);
// Mock the code to be a critical code
$reflection = new ReflectionClass($exception);
$property = $reflection->getProperty('code');
$property->setAccessible(true);
$property->setValue($exception, '2002');
expect($service->shouldNotifyAdmin($exception))->toBeTrue();
});
it('returns false for non-critical database errors', function () {
$service = new AdminNotificationService;
$pdo = new PDOException('Syntax error');
$exception = new QueryException('mysql', 'SELECT 1', [], $pdo);
expect($service->shouldNotifyAdmin($exception))->toBeFalse();
});
});
describe('notifyError', function () {
it('sends notification when enabled', function () {
Notification::fake();
Cache::flush();
$service = new AdminNotificationService;
$exception = new RuntimeException('Test error');
$service->notifyError($exception);
Notification::assertSentOnDemand(
SystemErrorNotification::class,
function ($notification, $channels, $notifiable) {
return $notifiable->routes['mail'] === 'admin@test.com';
}
);
});
it('does not send notification when disabled', function () {
Notification::fake();
config(['libra.notifications.system_notifications_enabled' => false]);
$service = new AdminNotificationService;
$exception = new RuntimeException('Test error');
$service->notifyError($exception);
Notification::assertNothingSent();
});
it('does not send notification when admin email is not configured', function () {
Notification::fake();
config(['libra.notifications.admin_email' => null]);
$service = new AdminNotificationService;
$exception = new RuntimeException('Test error');
$service->notifyError($exception);
Notification::assertNothingSent();
});
it('rate limits duplicate notifications', function () {
Notification::fake();
Cache::flush();
$service = new AdminNotificationService;
$exception = new RuntimeException('Test error');
$service->notifyError($exception);
$service->notifyError($exception);
Notification::assertSentOnDemandTimes(SystemErrorNotification::class, 1);
});
it('allows notifications for different error types', function () {
Notification::fake();
Cache::flush();
$service = new AdminNotificationService;
$exception1 = new RuntimeException('Error 1');
$exception2 = new InvalidArgumentException('Error 2');
$service->notifyError($exception1);
$service->notifyError($exception2);
Notification::assertSentOnDemandTimes(SystemErrorNotification::class, 2);
});
it('allows notifications for same error type with different messages', function () {
Notification::fake();
Cache::flush();
$service = new AdminNotificationService;
$exception1 = new RuntimeException('Error message 1');
$exception2 = new RuntimeException('Error message 2');
$service->notifyError($exception1);
$service->notifyError($exception2);
Notification::assertSentOnDemandTimes(SystemErrorNotification::class, 2);
});
});