Files
michaelschiemer/tests/Feature/LiveComponents/SchedulerTimelineComponentTest.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

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