- Add comprehensive health check system with multiple endpoints - Add Prometheus metrics endpoint - Add production logging configurations (5 strategies) - Add complete deployment documentation suite: * QUICKSTART.md - 30-minute deployment guide * DEPLOYMENT_CHECKLIST.md - Printable verification checklist * DEPLOYMENT_WORKFLOW.md - Complete deployment lifecycle * PRODUCTION_DEPLOYMENT.md - Comprehensive technical reference * production-logging.md - Logging configuration guide * ANSIBLE_DEPLOYMENT.md - Infrastructure as Code automation * README.md - Navigation hub * DEPLOYMENT_SUMMARY.md - Executive summary - Add deployment scripts and automation - Add DEPLOYMENT_PLAN.md - Concrete plan for immediate deployment - Update README with production-ready features All production infrastructure is now complete and ready for deployment.
261 lines
8.4 KiB
PHP
261 lines
8.4 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
use App\Application\LiveComponents\Dashboard\FailedJobsListComponent;
|
|
use App\Application\LiveComponents\Dashboard\FailedJobsState;
|
|
use App\Framework\LiveComponents\ValueObjects\ComponentId;
|
|
use App\Framework\Queue\Services\DeadLetterManager;
|
|
use App\Framework\Queue\Entities\DeadLetterJob;
|
|
use App\Framework\Queue\ValueObjects\JobId;
|
|
use App\Framework\Queue\ValueObjects\QueueName;
|
|
use App\Framework\Core\ValueObjects\Timestamp;
|
|
|
|
describe('FailedJobsListComponent', function () {
|
|
beforeEach(function () {
|
|
$this->deadLetterManager = Mockery::mock(DeadLetterManager::class);
|
|
$this->componentId = ComponentId::create('failed-jobs', 'test');
|
|
$this->initialState = FailedJobsState::empty();
|
|
});
|
|
|
|
afterEach(function () {
|
|
Mockery::close();
|
|
});
|
|
|
|
it('polls and updates state with failed jobs', function () {
|
|
$failedJob1 = new DeadLetterJob(
|
|
id: JobId::generate(),
|
|
queueName: new QueueName('default'),
|
|
jobType: 'SendEmailJob',
|
|
payload: ['email' => 'test@example.com'],
|
|
exception: 'Connection timeout',
|
|
failedAt: Timestamp::now(),
|
|
attempts: 3
|
|
);
|
|
|
|
$failedJob2 = new DeadLetterJob(
|
|
id: JobId::generate(),
|
|
queueName: new QueueName('high-priority'),
|
|
jobType: 'ProcessPaymentJob',
|
|
payload: ['amount' => 100],
|
|
exception: 'Payment gateway error',
|
|
failedAt: Timestamp::now(),
|
|
attempts: 5
|
|
);
|
|
|
|
$this->deadLetterManager->shouldReceive('getFailedJobs')
|
|
->once()
|
|
->with(50)
|
|
->andReturn([$failedJob1, $failedJob2]);
|
|
|
|
$component = new FailedJobsListComponent(
|
|
id: $this->componentId,
|
|
state: $this->initialState,
|
|
deadLetterManager: $this->deadLetterManager
|
|
);
|
|
|
|
$newState = $component->poll();
|
|
|
|
expect($newState)->toBeInstanceOf(FailedJobsState::class);
|
|
expect($newState->totalFailedJobs)->toBe(2);
|
|
expect($newState->failedJobs)->toHaveCount(2);
|
|
expect($newState->failedJobs[0]['job_type'])->toBe('SendEmailJob');
|
|
expect($newState->failedJobs[0]['queue'])->toBe('default');
|
|
expect($newState->failedJobs[0]['attempts'])->toBe(3);
|
|
});
|
|
|
|
it('handles retry job action successfully', function () {
|
|
$jobId = JobId::generate();
|
|
|
|
$this->deadLetterManager->shouldReceive('retryJob')
|
|
->once()
|
|
->with($jobId->toString())
|
|
->andReturn(true);
|
|
|
|
// After retry, poll returns updated state
|
|
$this->deadLetterManager->shouldReceive('getFailedJobs')
|
|
->once()
|
|
->with(50)
|
|
->andReturn([]);
|
|
|
|
$component = new FailedJobsListComponent(
|
|
id: $this->componentId,
|
|
state: $this->initialState,
|
|
deadLetterManager: $this->deadLetterManager
|
|
);
|
|
|
|
$newState = $component->retryJob($jobId->toString());
|
|
|
|
expect($newState)->toBeInstanceOf(FailedJobsState::class);
|
|
expect($newState->totalFailedJobs)->toBe(0); // Job was retried
|
|
});
|
|
|
|
it('handles retry job action failure', function () {
|
|
$jobId = JobId::generate();
|
|
|
|
$this->deadLetterManager->shouldReceive('retryJob')
|
|
->once()
|
|
->with($jobId->toString())
|
|
->andReturn(false);
|
|
|
|
// Still poll for current state
|
|
$this->deadLetterManager->shouldReceive('getFailedJobs')
|
|
->once()
|
|
->andReturn([]);
|
|
|
|
$component = new FailedJobsListComponent(
|
|
id: $this->componentId,
|
|
state: $this->initialState,
|
|
deadLetterManager: $this->deadLetterManager
|
|
);
|
|
|
|
$newState = $component->retryJob($jobId->toString());
|
|
|
|
expect($newState)->toBeInstanceOf(FailedJobsState::class);
|
|
});
|
|
|
|
it('handles delete job action successfully', function () {
|
|
$jobId = JobId::generate();
|
|
|
|
$this->deadLetterManager->shouldReceive('deleteJob')
|
|
->once()
|
|
->with($jobId->toString())
|
|
->andReturn(true);
|
|
|
|
// After delete, poll returns updated state
|
|
$this->deadLetterManager->shouldReceive('getFailedJobs')
|
|
->once()
|
|
->with(50)
|
|
->andReturn([]);
|
|
|
|
$component = new FailedJobsListComponent(
|
|
id: $this->componentId,
|
|
state: $this->initialState,
|
|
deadLetterManager: $this->deadLetterManager
|
|
);
|
|
|
|
$newState = $component->deleteJob($jobId->toString());
|
|
|
|
expect($newState)->toBeInstanceOf(FailedJobsState::class);
|
|
expect($newState->totalFailedJobs)->toBe(0);
|
|
});
|
|
|
|
it('dispatches events on retry success', function () {
|
|
$jobId = JobId::generate();
|
|
$eventDispatcher = Mockery::mock(ComponentEventDispatcher::class);
|
|
|
|
$this->deadLetterManager->shouldReceive('retryJob')
|
|
->once()
|
|
->andReturn(true);
|
|
|
|
$this->deadLetterManager->shouldReceive('getFailedJobs')
|
|
->once()
|
|
->andReturn([]);
|
|
|
|
$eventDispatcher->shouldReceive('dispatch')
|
|
->once()
|
|
->with('failed-jobs:retry-success', ['jobId' => $jobId->toString()]);
|
|
|
|
$component = new FailedJobsListComponent(
|
|
id: $this->componentId,
|
|
state: $this->initialState,
|
|
deadLetterManager: $this->deadLetterManager
|
|
);
|
|
|
|
$component->retryJob($jobId->toString(), $eventDispatcher);
|
|
});
|
|
|
|
it('has correct poll interval', function () {
|
|
$component = new FailedJobsListComponent(
|
|
id: $this->componentId,
|
|
state: $this->initialState,
|
|
deadLetterManager: $this->deadLetterManager
|
|
);
|
|
|
|
expect($component->getPollInterval())->toBe(10000);
|
|
});
|
|
|
|
it('returns correct render data', function () {
|
|
$failedJobs = [
|
|
[
|
|
'id' => 'job-1',
|
|
'queue' => 'default',
|
|
'job_type' => 'EmailJob',
|
|
'error' => 'Connection failed',
|
|
'failed_at' => '2024-01-15 12:00:00',
|
|
'attempts' => 3,
|
|
],
|
|
];
|
|
|
|
$state = new FailedJobsState(
|
|
totalFailedJobs: 1,
|
|
failedJobs: $failedJobs,
|
|
statistics: ['total_retries' => 5],
|
|
lastUpdated: '2024-01-15 12:00:00'
|
|
);
|
|
|
|
$component = new FailedJobsListComponent(
|
|
id: $this->componentId,
|
|
state: $state,
|
|
deadLetterManager: $this->deadLetterManager
|
|
);
|
|
|
|
$renderData = $component->getRenderData();
|
|
|
|
expect($renderData->templatePath)->toBe('livecomponent-failed-jobs-list');
|
|
expect($renderData->data)->toHaveKey('componentId');
|
|
expect($renderData->data)->toHaveKey('pollInterval');
|
|
expect($renderData->data['pollInterval'])->toBe(10000);
|
|
expect($renderData->data['totalFailedJobs'])->toBe(1);
|
|
expect($renderData->data['failedJobs'])->toHaveCount(1);
|
|
});
|
|
|
|
it('handles empty failed jobs list', function () {
|
|
$this->deadLetterManager->shouldReceive('getFailedJobs')
|
|
->once()
|
|
->with(50)
|
|
->andReturn([]);
|
|
|
|
$component = new FailedJobsListComponent(
|
|
id: $this->componentId,
|
|
state: $this->initialState,
|
|
deadLetterManager: $this->deadLetterManager
|
|
);
|
|
|
|
$newState = $component->poll();
|
|
|
|
expect($newState->totalFailedJobs)->toBe(0);
|
|
expect($newState->failedJobs)->toBe([]);
|
|
});
|
|
|
|
it('truncates payload preview to 100 characters', function () {
|
|
$longPayload = str_repeat('a', 200);
|
|
|
|
$failedJob = new DeadLetterJob(
|
|
id: JobId::generate(),
|
|
queueName: new QueueName('default'),
|
|
jobType: 'LongJob',
|
|
payload: ['data' => $longPayload],
|
|
exception: 'Error',
|
|
failedAt: Timestamp::now(),
|
|
attempts: 1
|
|
);
|
|
|
|
$this->deadLetterManager->shouldReceive('getFailedJobs')
|
|
->once()
|
|
->with(50)
|
|
->andReturn([$failedJob]);
|
|
|
|
$component = new FailedJobsListComponent(
|
|
id: $this->componentId,
|
|
state: $this->initialState,
|
|
deadLetterManager: $this->deadLetterManager
|
|
);
|
|
|
|
$newState = $component->poll();
|
|
|
|
$payloadPreview = $newState->failedJobs[0]['payload_preview'];
|
|
expect(strlen($payloadPreview))->toBeLessThanOrEqual(103); // 100 + '...'
|
|
});
|
|
});
|