Attainly/backend/tests/Unit/Schedule/UseCases/RescheduleScheduleTest.php

218 lines
7.4 KiB
PHP

<?php
namespace Tests\Unit\Schedule\UseCases;
use App\Exceptions\BadRequestException;
use App\Exceptions\NotFoundException;
use App\Schedule\CreateScheduleAssignmentDto;
use App\Schedule\CreateScheduleDto;
use App\Schedule\EvenDistributionScheduler;
use App\Schedule\ScheduleAssignment;
use App\Schedule\UseCases\RescheduleSchedule\RescheduleSchedule;
use App\Schedule\UseCases\RescheduleSchedule\RescheduleScheduleRequest;
use App\Shared\ValueObject\EmailAddress;
use App\User\User;
use DateTimeImmutable;
use DateTimeZone;
use PHPUnit\Framework\TestCase;
use Tests\Fakes\FakeScheduleRepository;
class RescheduleScheduleTest extends TestCase
{
public function test_it_redistributes_only_unfinished_assignments(): void
{
$user = $this->user(1, 'reader@example.com');
$repository = new FakeScheduleRepository;
$schedule = $repository->create($this->schedule($user));
$assignments = $schedule->getAssignments();
$firstCompletion = $this->utc('2026-08-15T10:00:00');
$secondCompletion = $this->utc('2026-08-16T11:00:00');
$assignments[1]->complete($firstCompletion);
$assignments[3]->complete($secondCompletion);
$repository->updateAssignment($assignments[1]);
$repository->updateAssignment($assignments[3]);
$rescheduled = $this->useCase($repository)->execute(
new RescheduleScheduleRequest(
scheduleId: $schedule->getId(),
user: $user,
startDate: '2026-08-20',
targetDate: '2026-08-21',
workloadPlacement: 'end',
),
);
$this->assertSame('2026-08-20', $rescheduled->getStartDate()
->format('Y-m-d'));
$this->assertSame('2026-08-21', $rescheduled->getTargetDate()
->format('Y-m-d'));
$this->assertSame(
[
'2026-08-20',
'2026-08-12',
'2026-08-21',
'2026-08-14',
'2026-08-21',
],
array_map(function (ScheduleAssignment $assignment): string {
return $assignment->getScheduledDate()->format('Y-m-d');
}, $rescheduled->getAssignments()),
);
$this->assertSame(
[null, $firstCompletion, null, $secondCompletion, null],
array_map(function (
ScheduleAssignment $assignment,
): ?DateTimeImmutable {
return $assignment->getCompletedAt();
}, $rescheduled->getAssignments()),
);
$this->assertSame(
[1, 2, 3, 4, 5],
array_map(function (ScheduleAssignment $assignment): int {
return $assignment->getId();
}, $rescheduled->getAssignments()),
);
$this->assertSame(
['Lesson 1', 'Lesson 2', 'Lesson 3', 'Lesson 4', 'Lesson 5'],
array_map(function (ScheduleAssignment $assignment): string {
return $assignment->getName();
}, $rescheduled->getAssignments()),
);
}
public function test_it_rejects_a_target_before_the_start(): void
{
$user = $this->user(1, 'reader@example.com');
$repository = new FakeScheduleRepository;
$schedule = $repository->create($this->schedule($user));
$this->expectException(BadRequestException::class);
$this->expectExceptionMessage(
'targetDate must not be before startDate',
);
$this->useCase($repository)->execute(
new RescheduleScheduleRequest(
scheduleId: $schedule->getId(),
user: $user,
startDate: '2026-08-21',
targetDate: '2026-08-20',
workloadPlacement: 'middle',
),
);
}
public function test_it_rejects_an_unknown_workload_placement(): void
{
$user = $this->user(1, 'reader@example.com');
$repository = new FakeScheduleRepository;
$schedule = $repository->create($this->schedule($user));
$this->expectException(BadRequestException::class);
$this->expectExceptionMessage(
'workloadPlacement must be start, middle, or end',
);
$this->useCase($repository)->execute(
new RescheduleScheduleRequest(
scheduleId: $schedule->getId(),
user: $user,
startDate: '2026-08-20',
targetDate: '2026-08-21',
workloadPlacement: 'sideways',
),
);
}
public function test_it_rejects_a_schedule_without_unfinished_work(): void
{
$user = $this->user(1, 'reader@example.com');
$repository = new FakeScheduleRepository;
$schedule = $repository->create($this->schedule($user));
foreach ($schedule->getAssignments() as $assignment) {
$assignment->complete($this->utc('2026-08-15T10:00:00'));
$repository->updateAssignment($assignment);
}
$this->expectException(BadRequestException::class);
$this->expectExceptionMessage(
'schedule has no unfinished assignments',
);
$this->useCase($repository)->execute(
new RescheduleScheduleRequest(
scheduleId: $schedule->getId(),
user: $user,
startDate: '2026-08-20',
targetDate: '2026-08-21',
workloadPlacement: 'middle',
),
);
}
public function test_it_hides_another_users_schedule(): void
{
$repository = new FakeScheduleRepository;
$owner = $this->user(1, 'owner@example.com');
$schedule = $repository->create($this->schedule($owner));
$this->expectException(NotFoundException::class);
$this->expectExceptionMessage('schedule not found');
$this->useCase($repository)->execute(
new RescheduleScheduleRequest(
scheduleId: $schedule->getId(),
user: $this->user(2, 'viewer@example.com'),
startDate: '2026-08-20',
targetDate: '2026-08-21',
workloadPlacement: 'middle',
),
);
}
private function useCase(
FakeScheduleRepository $repository,
): RescheduleSchedule {
return new RescheduleSchedule(
$repository,
new EvenDistributionScheduler,
);
}
private function schedule(User $user): CreateScheduleDto
{
$assignments = [];
foreach (range(1, 5) as $number) {
$assignments[] = new CreateScheduleAssignmentDto(
name: "Lesson {$number}",
kind: 'lesson',
path: ['Course', "Lesson {$number}"],
scheduledDate: $this->utc("2026-08-1{$number}"),
position: $number,
);
}
return new CreateScheduleDto(
user: $user,
setName: 'Course',
elementKind: 'lesson',
startDate: $this->utc('2026-08-10'),
targetDate: $this->utc('2026-08-15'),
assignments: $assignments,
);
}
private function user(int $id, string $email): User
{
return new User(
id: $id,
email: new EmailAddress($email),
passwordHash: 'hashed-password',
);
}
private function utc(string $value): DateTimeImmutable
{
return new DateTimeImmutable($value, new DateTimeZone('UTC'));
}
}