- 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.
333 lines
11 KiB
PHP
333 lines
11 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
use App\Application\LiveComponents\Dashboard\SchedulerTimelineComponent;
|
|
use App\Application\LiveComponents\Dashboard\SchedulerState;
|
|
use App\Framework\LiveComponents\ValueObjects\ComponentId;
|
|
use App\Framework\Scheduler\Services\SchedulerService;
|
|
use App\Framework\Scheduler\ValueObjects\ScheduledTask;
|
|
use App\Framework\Scheduler\Schedules\CronSchedule;
|
|
use App\Framework\Scheduler\Schedules\IntervalSchedule;
|
|
use App\Framework\Core\ValueObjects\Timestamp;
|
|
use App\Framework\Core\ValueObjects\Duration;
|
|
|
|
describe('SchedulerTimelineComponent', function () {
|
|
beforeEach(function () {
|
|
$this->scheduler = Mockery::mock(SchedulerService::class);
|
|
$this->componentId = ComponentId::create('scheduler-timeline', 'test');
|
|
$this->initialState = SchedulerState::empty();
|
|
});
|
|
|
|
afterEach(function () {
|
|
Mockery::close();
|
|
});
|
|
|
|
it('polls and updates state with scheduler data', function () {
|
|
$now = Timestamp::now();
|
|
$in30Minutes = $now->add(Duration::fromMinutes(30));
|
|
$in2Hours = $now->add(Duration::fromHours(2));
|
|
|
|
$task1 = new ScheduledTask(
|
|
id: 'backup-task',
|
|
schedule: CronSchedule::fromExpression('0 2 * * *'),
|
|
callback: fn() => true,
|
|
nextRun: $in30Minutes
|
|
);
|
|
|
|
$task2 = new ScheduledTask(
|
|
id: 'cleanup-task',
|
|
schedule: IntervalSchedule::every(Duration::fromHours(1)),
|
|
callback: fn() => true,
|
|
nextRun: $in2Hours
|
|
);
|
|
|
|
$this->scheduler->shouldReceive('getScheduledTasks')
|
|
->once()
|
|
->andReturn([$task1, $task2]);
|
|
|
|
$this->scheduler->shouldReceive('getDueTasks')
|
|
->once()
|
|
->with(Mockery::type(Timestamp::class))
|
|
->andReturn([]);
|
|
|
|
$component = new SchedulerTimelineComponent(
|
|
id: $this->componentId,
|
|
state: $this->initialState,
|
|
scheduler: $this->scheduler
|
|
);
|
|
|
|
$newState = $component->poll();
|
|
|
|
expect($newState)->toBeInstanceOf(SchedulerState::class);
|
|
expect($newState->totalScheduledTasks)->toBe(2);
|
|
expect($newState->dueTasks)->toBe(0);
|
|
expect($newState->upcomingTasks)->toHaveCount(2);
|
|
expect($newState->nextExecution)->not->toBeNull();
|
|
});
|
|
|
|
it('identifies due tasks correctly', function () {
|
|
$now = Timestamp::now();
|
|
$past = $now->sub(Duration::fromMinutes(5));
|
|
|
|
$dueTask = new ScheduledTask(
|
|
id: 'due-task',
|
|
schedule: IntervalSchedule::every(Duration::fromMinutes(10)),
|
|
callback: fn() => true,
|
|
nextRun: $past
|
|
);
|
|
|
|
$this->scheduler->shouldReceive('getScheduledTasks')
|
|
->once()
|
|
->andReturn([$dueTask]);
|
|
|
|
$this->scheduler->shouldReceive('getDueTasks')
|
|
->once()
|
|
->andReturn([$dueTask]);
|
|
|
|
$component = new SchedulerTimelineComponent(
|
|
id: $this->componentId,
|
|
state: $this->initialState,
|
|
scheduler: $this->scheduler
|
|
);
|
|
|
|
$newState = $component->poll();
|
|
|
|
expect($newState->dueTasks)->toBe(1);
|
|
expect($newState->upcomingTasks[0]['is_due'])->toBeTrue();
|
|
});
|
|
|
|
it('formats time until execution correctly for less than 1 minute', function () {
|
|
$now = Timestamp::now();
|
|
$in30Seconds = $now->add(Duration::fromSeconds(30));
|
|
|
|
$task = new ScheduledTask(
|
|
id: 'imminent-task',
|
|
schedule: IntervalSchedule::every(Duration::fromMinutes(1)),
|
|
callback: fn() => true,
|
|
nextRun: $in30Seconds
|
|
);
|
|
|
|
$this->scheduler->shouldReceive('getScheduledTasks')
|
|
->once()
|
|
->andReturn([$task]);
|
|
|
|
$this->scheduler->shouldReceive('getDueTasks')
|
|
->once()
|
|
->andReturn([]);
|
|
|
|
$component = new SchedulerTimelineComponent(
|
|
id: $this->componentId,
|
|
state: $this->initialState,
|
|
scheduler: $this->scheduler
|
|
);
|
|
|
|
$newState = $component->poll();
|
|
|
|
expect($newState->upcomingTasks[0]['next_run_relative'])->toBe('Less than 1 minute');
|
|
});
|
|
|
|
it('formats time until execution correctly for hours and minutes', function () {
|
|
$now = Timestamp::now();
|
|
$in5Hours30Min = $now->add(Duration::fromMinutes(330)); // 5.5 hours
|
|
|
|
$task = new ScheduledTask(
|
|
id: 'future-task',
|
|
schedule: CronSchedule::fromExpression('30 17 * * *'),
|
|
callback: fn() => true,
|
|
nextRun: $in5Hours30Min
|
|
);
|
|
|
|
$this->scheduler->shouldReceive('getScheduledTasks')
|
|
->once()
|
|
->andReturn([$task]);
|
|
|
|
$this->scheduler->shouldReceive('getDueTasks')
|
|
->once()
|
|
->andReturn([]);
|
|
|
|
$component = new SchedulerTimelineComponent(
|
|
id: $this->componentId,
|
|
state: $this->initialState,
|
|
scheduler: $this->scheduler
|
|
);
|
|
|
|
$newState = $component->poll();
|
|
|
|
$relativeTime = $newState->upcomingTasks[0]['next_run_relative'];
|
|
expect($relativeTime)->toContain('5 hours');
|
|
expect($relativeTime)->toContain('30 min');
|
|
});
|
|
|
|
it('formats time until execution correctly for days and hours', function () {
|
|
$now = Timestamp::now();
|
|
$in2Days4Hours = $now->add(Duration::fromHours(52)); // 2 days, 4 hours
|
|
|
|
$task = new ScheduledTask(
|
|
id: 'distant-task',
|
|
schedule: CronSchedule::fromExpression('0 0 * * 0'), // Weekly
|
|
callback: fn() => true,
|
|
nextRun: $in2Days4Hours
|
|
);
|
|
|
|
$this->scheduler->shouldReceive('getScheduledTasks')
|
|
->once()
|
|
->andReturn([$task]);
|
|
|
|
$this->scheduler->shouldReceive('getDueTasks')
|
|
->once()
|
|
->andReturn([]);
|
|
|
|
$component = new SchedulerTimelineComponent(
|
|
id: $this->componentId,
|
|
state: $this->initialState,
|
|
scheduler: $this->scheduler
|
|
);
|
|
|
|
$newState = $component->poll();
|
|
|
|
$relativeTime = $newState->upcomingTasks[0]['next_run_relative'];
|
|
expect($relativeTime)->toContain('2 days');
|
|
expect($relativeTime)->toContain('4 hours');
|
|
});
|
|
|
|
it('has correct poll interval', function () {
|
|
$component = new SchedulerTimelineComponent(
|
|
id: $this->componentId,
|
|
state: $this->initialState,
|
|
scheduler: $this->scheduler
|
|
);
|
|
|
|
expect($component->getPollInterval())->toBe(30000);
|
|
});
|
|
|
|
it('returns correct render data', function () {
|
|
$upcomingTasks = [
|
|
[
|
|
'id' => 'task-1',
|
|
'schedule_type' => 'cron',
|
|
'next_run' => '2024-01-15 13:00:00',
|
|
'next_run_relative' => '30 min',
|
|
'is_due' => false,
|
|
],
|
|
];
|
|
|
|
$state = new SchedulerState(
|
|
totalScheduledTasks: 5,
|
|
dueTasks: 1,
|
|
upcomingTasks: $upcomingTasks,
|
|
nextExecution: '2024-01-15 13:00:00',
|
|
statistics: ['total_executions' => 100],
|
|
lastUpdated: '2024-01-15 12:00:00'
|
|
);
|
|
|
|
$component = new SchedulerTimelineComponent(
|
|
id: $this->componentId,
|
|
state: $state,
|
|
scheduler: $this->scheduler
|
|
);
|
|
|
|
$renderData = $component->getRenderData();
|
|
|
|
expect($renderData->templatePath)->toBe('livecomponent-scheduler-timeline');
|
|
expect($renderData->data)->toHaveKey('componentId');
|
|
expect($renderData->data)->toHaveKey('pollInterval');
|
|
expect($renderData->data['pollInterval'])->toBe(30000);
|
|
expect($renderData->data['totalScheduledTasks'])->toBe(5);
|
|
expect($renderData->data['dueTasks'])->toBe(1);
|
|
});
|
|
|
|
it('handles no scheduled tasks', function () {
|
|
$this->scheduler->shouldReceive('getScheduledTasks')
|
|
->once()
|
|
->andReturn([]);
|
|
|
|
$this->scheduler->shouldReceive('getDueTasks')
|
|
->once()
|
|
->andReturn([]);
|
|
|
|
$component = new SchedulerTimelineComponent(
|
|
id: $this->componentId,
|
|
state: $this->initialState,
|
|
scheduler: $this->scheduler
|
|
);
|
|
|
|
$newState = $component->poll();
|
|
|
|
expect($newState->totalScheduledTasks)->toBe(0);
|
|
expect($newState->dueTasks)->toBe(0);
|
|
expect($newState->upcomingTasks)->toBe([]);
|
|
expect($newState->nextExecution)->toBeNull();
|
|
});
|
|
|
|
it('limits upcoming tasks to 10 items', function () {
|
|
$tasks = [];
|
|
$now = Timestamp::now();
|
|
|
|
for ($i = 0; $i < 20; $i++) {
|
|
$tasks[] = new ScheduledTask(
|
|
id: "task-{$i}",
|
|
schedule: IntervalSchedule::every(Duration::fromMinutes($i + 1)),
|
|
callback: fn() => true,
|
|
nextRun: $now->add(Duration::fromMinutes($i + 1))
|
|
);
|
|
}
|
|
|
|
$this->scheduler->shouldReceive('getScheduledTasks')
|
|
->once()
|
|
->andReturn($tasks);
|
|
|
|
$this->scheduler->shouldReceive('getDueTasks')
|
|
->once()
|
|
->andReturn([]);
|
|
|
|
$component = new SchedulerTimelineComponent(
|
|
id: $this->componentId,
|
|
state: $this->initialState,
|
|
scheduler: $this->scheduler
|
|
);
|
|
|
|
$newState = $component->poll();
|
|
|
|
expect($newState->totalScheduledTasks)->toBe(20);
|
|
expect($newState->upcomingTasks)->toHaveCount(10); // Limited to 10
|
|
});
|
|
|
|
it('detects schedule type correctly', function () {
|
|
$now = Timestamp::now();
|
|
|
|
$cronTask = new ScheduledTask(
|
|
id: 'cron-task',
|
|
schedule: CronSchedule::fromExpression('0 * * * *'),
|
|
callback: fn() => true,
|
|
nextRun: $now->add(Duration::fromHours(1))
|
|
);
|
|
|
|
$intervalTask = new ScheduledTask(
|
|
id: 'interval-task',
|
|
schedule: IntervalSchedule::every(Duration::fromMinutes(30)),
|
|
callback: fn() => true,
|
|
nextRun: $now->add(Duration::fromMinutes(30))
|
|
);
|
|
|
|
$this->scheduler->shouldReceive('getScheduledTasks')
|
|
->once()
|
|
->andReturn([$cronTask, $intervalTask]);
|
|
|
|
$this->scheduler->shouldReceive('getDueTasks')
|
|
->once()
|
|
->andReturn([]);
|
|
|
|
$component = new SchedulerTimelineComponent(
|
|
id: $this->componentId,
|
|
state: $this->initialState,
|
|
scheduler: $this->scheduler
|
|
);
|
|
|
|
$newState = $component->poll();
|
|
|
|
expect($newState->upcomingTasks[0]['schedule_type'])->toBe('cron');
|
|
expect($newState->upcomingTasks[1]['schedule_type'])->toBe('interval');
|
|
});
|
|
});
|