Files
michaelschiemer/tests/Feature/LiveComponents/FailedJobsListComponentTest.php
Michael Schiemer fc3d7e6357 feat(Production): Complete production deployment infrastructure
- 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.
2025-10-25 19:18:37 +02:00

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 + '...'
});
});