556 lines
17 KiB
PHP
556 lines
17 KiB
PHP
<?php
|
|
|
|
namespace Tests\Unit\Schedule\UseCases;
|
|
|
|
use App\Element\CreateElementDto;
|
|
use App\Exceptions\BadRequestException;
|
|
use App\Exceptions\NotFoundException;
|
|
use App\Schedule\EvenDistributionScheduler;
|
|
use App\Schedule\UseCases\CreateSchedule\CreateSchedule;
|
|
use App\Schedule\UseCases\CreateSchedule\CreateScheduleRequest;
|
|
use App\Set\CreateSetDto;
|
|
use App\Set\CreateSetLevelDto;
|
|
use App\Set\Set;
|
|
use App\Set\SetLevel;
|
|
use App\Shared\ValueObject\EmailAddress;
|
|
use App\User\User;
|
|
use PHPUnit\Framework\TestCase;
|
|
use Tests\Fakes\FakeElementRepository;
|
|
use Tests\Fakes\FakeScheduleRepository;
|
|
use Tests\Fakes\FakeSetRepository;
|
|
use Tests\Fakes\FakeSetLevelRepository;
|
|
|
|
class CreateScheduleTest extends TestCase
|
|
{
|
|
public function test_it_schedules_every_element_on_the_selected_level(): void
|
|
{
|
|
$user = $this->user();
|
|
$setRepository = new FakeSetRepository;
|
|
$setLevelRepository = new FakeSetLevelRepository;
|
|
$elementRepository = new FakeElementRepository;
|
|
$scheduleRepository = new FakeScheduleRepository;
|
|
$set = $setRepository->create(new CreateSetDto(
|
|
name: 'Bible',
|
|
creator: $user,
|
|
));
|
|
$bookLevel = $this->createLevel(
|
|
$setLevelRepository,
|
|
$set,
|
|
'book',
|
|
);
|
|
$portionLevel = $this->createLevel(
|
|
$setLevelRepository,
|
|
$set,
|
|
'portion',
|
|
);
|
|
$chapterLevel = $this->createLevel(
|
|
$setLevelRepository,
|
|
$set,
|
|
'chapter',
|
|
);
|
|
$genesis = $elementRepository->create(new CreateElementDto(
|
|
name: 'Genesis',
|
|
level: $bookLevel,
|
|
parentElement: null,
|
|
));
|
|
$exodus = $elementRepository->create(new CreateElementDto(
|
|
name: 'Exodus',
|
|
level: $bookLevel,
|
|
parentElement: null,
|
|
));
|
|
$creation = $elementRepository->create(new CreateElementDto(
|
|
name: 'Creation',
|
|
level: $portionLevel,
|
|
parentElement: $genesis,
|
|
));
|
|
$shemot = $elementRepository->create(new CreateElementDto(
|
|
name: 'Shemot',
|
|
level: $portionLevel,
|
|
parentElement: $exodus,
|
|
));
|
|
$genesisChapterOne = $elementRepository->create(
|
|
new CreateElementDto(
|
|
name: 'Genesis 1',
|
|
level: $chapterLevel,
|
|
parentElement: $creation,
|
|
),
|
|
);
|
|
$genesisChapterTwo = $elementRepository->create(
|
|
new CreateElementDto(
|
|
name: 'Genesis 2',
|
|
level: $chapterLevel,
|
|
parentElement: $creation,
|
|
),
|
|
);
|
|
$genesisChapterThree = $elementRepository->create(
|
|
new CreateElementDto(
|
|
name: 'Genesis 3',
|
|
level: $chapterLevel,
|
|
parentElement: $creation,
|
|
),
|
|
);
|
|
$exodusChapterOne = $elementRepository->create(
|
|
new CreateElementDto(
|
|
name: 'Exodus 1',
|
|
level: $chapterLevel,
|
|
parentElement: $shemot,
|
|
),
|
|
);
|
|
$exodusChapterTwo = $elementRepository->create(
|
|
new CreateElementDto(
|
|
name: 'Exodus 2',
|
|
level: $chapterLevel,
|
|
parentElement: $shemot,
|
|
),
|
|
);
|
|
|
|
$schedule = (new CreateSchedule(
|
|
$setRepository,
|
|
$setLevelRepository,
|
|
$elementRepository,
|
|
$scheduleRepository,
|
|
new EvenDistributionScheduler,
|
|
))->execute(new CreateScheduleRequest(
|
|
user: $user,
|
|
setId: $set->getId(),
|
|
levelId: $chapterLevel->getId(),
|
|
startDate: '2026-08-10',
|
|
targetDate: '2026-08-12',
|
|
workloadPlacement: 'middle',
|
|
));
|
|
|
|
$this->assertSame('chapter', $schedule->getElementKind());
|
|
$this->assertSame('2026-08-10', $schedule->getStartDate()->format(
|
|
'Y-m-d',
|
|
));
|
|
$this->assertSame('2026-08-12', $schedule->getTargetDate()->format(
|
|
'Y-m-d',
|
|
));
|
|
$this->assertSame(
|
|
[
|
|
$genesisChapterOne->getName(),
|
|
$genesisChapterTwo->getName(),
|
|
$genesisChapterThree->getName(),
|
|
$exodusChapterOne->getName(),
|
|
$exodusChapterTwo->getName(),
|
|
],
|
|
array_map(function ($assignment): string {
|
|
return $assignment->getName();
|
|
}, $schedule->getAssignments()),
|
|
);
|
|
$this->assertSame(
|
|
['Genesis', 'Creation', 'Genesis 1'],
|
|
$schedule->getAssignments()[0]->getPath(),
|
|
);
|
|
$this->assertSame(
|
|
[
|
|
'2026-08-10',
|
|
'2026-08-10',
|
|
'2026-08-11',
|
|
'2026-08-11',
|
|
'2026-08-12',
|
|
],
|
|
array_map(function ($assignment): string {
|
|
return $assignment->getScheduledDate()->format('Y-m-d');
|
|
}, $schedule->getAssignments()),
|
|
);
|
|
}
|
|
|
|
public function test_it_spreads_sparse_work_across_the_full_range(): void
|
|
{
|
|
$user = $this->user();
|
|
$setRepository = new FakeSetRepository;
|
|
$setLevelRepository = new FakeSetLevelRepository;
|
|
$elementRepository = new FakeElementRepository;
|
|
$set = $setRepository->create(new CreateSetDto(
|
|
name: 'Course',
|
|
creator: $user,
|
|
));
|
|
$lessonLevel = $this->createLevel(
|
|
$setLevelRepository,
|
|
$set,
|
|
'lesson',
|
|
);
|
|
|
|
foreach (['First', 'Second', 'Third'] as $name) {
|
|
$elementRepository->create(new CreateElementDto(
|
|
name: $name,
|
|
level: $lessonLevel,
|
|
parentElement: null,
|
|
));
|
|
}
|
|
|
|
$schedule = (new CreateSchedule(
|
|
$setRepository,
|
|
$setLevelRepository,
|
|
$elementRepository,
|
|
new FakeScheduleRepository,
|
|
new EvenDistributionScheduler,
|
|
))->execute(new CreateScheduleRequest(
|
|
user: $user,
|
|
setId: $set->getId(),
|
|
levelId: $lessonLevel->getId(),
|
|
startDate: '2026-08-10',
|
|
targetDate: '2026-08-16',
|
|
workloadPlacement: 'middle',
|
|
));
|
|
|
|
$this->assertSame(
|
|
['2026-08-10', '2026-08-13', '2026-08-16'],
|
|
array_map(function ($assignment): string {
|
|
return $assignment->getScheduledDate()->format('Y-m-d');
|
|
}, $schedule->getAssignments()),
|
|
);
|
|
}
|
|
|
|
public function test_it_places_one_element_on_the_start_date(): void
|
|
{
|
|
$user = $this->user();
|
|
$setRepository = new FakeSetRepository;
|
|
$setLevelRepository = new FakeSetLevelRepository;
|
|
$elementRepository = new FakeElementRepository;
|
|
$set = $setRepository->create(new CreateSetDto(
|
|
name: 'Project',
|
|
creator: $user,
|
|
));
|
|
$milestoneLevel = $this->createLevel(
|
|
$setLevelRepository,
|
|
$set,
|
|
'milestone',
|
|
);
|
|
$elementRepository->create(new CreateElementDto(
|
|
name: 'Ship it',
|
|
level: $milestoneLevel,
|
|
parentElement: null,
|
|
));
|
|
|
|
$schedule = (new CreateSchedule(
|
|
$setRepository,
|
|
$setLevelRepository,
|
|
$elementRepository,
|
|
new FakeScheduleRepository,
|
|
new EvenDistributionScheduler,
|
|
))->execute(new CreateScheduleRequest(
|
|
user: $user,
|
|
setId: $set->getId(),
|
|
levelId: $milestoneLevel->getId(),
|
|
startDate: '2020-01-01',
|
|
targetDate: '2030-01-01',
|
|
workloadPlacement: 'middle',
|
|
));
|
|
|
|
$this->assertSame(
|
|
'2020-01-01',
|
|
$schedule->getAssignments()[0]
|
|
->getScheduledDate()
|
|
->format('Y-m-d'),
|
|
);
|
|
}
|
|
|
|
public function test_it_defaults_heavier_days_to_the_middle(): void
|
|
{
|
|
$user = $this->user();
|
|
$setRepository = new FakeSetRepository;
|
|
$setLevelRepository = new FakeSetLevelRepository;
|
|
$elementRepository = new FakeElementRepository;
|
|
$set = $setRepository->create(new CreateSetDto(
|
|
name: 'Course',
|
|
creator: $user,
|
|
));
|
|
$lessonLevel = $this->createLevel(
|
|
$setLevelRepository,
|
|
$set,
|
|
'lesson',
|
|
);
|
|
|
|
foreach (range(1, 6) as $number) {
|
|
$elementRepository->create(new CreateElementDto(
|
|
name: "Lesson {$number}",
|
|
level: $lessonLevel,
|
|
parentElement: null,
|
|
));
|
|
}
|
|
|
|
$schedule = (new CreateSchedule(
|
|
$setRepository,
|
|
$setLevelRepository,
|
|
$elementRepository,
|
|
new FakeScheduleRepository,
|
|
new EvenDistributionScheduler,
|
|
))->execute(new CreateScheduleRequest(
|
|
user: $user,
|
|
setId: $set->getId(),
|
|
levelId: $lessonLevel->getId(),
|
|
startDate: '2026-08-10',
|
|
targetDate: '2026-08-13',
|
|
workloadPlacement: null,
|
|
));
|
|
|
|
$this->assertSame(
|
|
[
|
|
'2026-08-10',
|
|
'2026-08-11',
|
|
'2026-08-11',
|
|
'2026-08-12',
|
|
'2026-08-12',
|
|
'2026-08-13',
|
|
],
|
|
array_map(function ($assignment): string {
|
|
return $assignment->getScheduledDate()->format('Y-m-d');
|
|
}, $schedule->getAssignments()),
|
|
);
|
|
}
|
|
|
|
public function test_it_rejects_an_unknown_workload_placement(): void
|
|
{
|
|
$user = $this->user();
|
|
$setRepository = new FakeSetRepository;
|
|
$setLevelRepository = new FakeSetLevelRepository;
|
|
$elementRepository = new FakeElementRepository;
|
|
$set = $setRepository->create(new CreateSetDto(
|
|
name: 'Course',
|
|
creator: $user,
|
|
));
|
|
$lessonLevel = $this->createLevel(
|
|
$setLevelRepository,
|
|
$set,
|
|
'lesson',
|
|
);
|
|
$elementRepository->create(new CreateElementDto(
|
|
name: 'Lesson 1',
|
|
level: $lessonLevel,
|
|
parentElement: null,
|
|
));
|
|
|
|
$this->expectException(BadRequestException::class);
|
|
$this->expectExceptionMessage(
|
|
'workloadPlacement must be start, middle, or end',
|
|
);
|
|
|
|
(new CreateSchedule(
|
|
$setRepository,
|
|
$setLevelRepository,
|
|
$elementRepository,
|
|
new FakeScheduleRepository,
|
|
new EvenDistributionScheduler,
|
|
))->execute(new CreateScheduleRequest(
|
|
user: $user,
|
|
setId: $set->getId(),
|
|
levelId: $lessonLevel->getId(),
|
|
startDate: '2026-08-10',
|
|
targetDate: '2026-08-13',
|
|
workloadPlacement: 'sideways',
|
|
));
|
|
}
|
|
|
|
public function test_it_rejects_an_unknown_set(): void
|
|
{
|
|
$this->expectException(NotFoundException::class);
|
|
$this->expectExceptionMessage('set not found');
|
|
|
|
(new CreateSchedule(
|
|
new FakeSetRepository,
|
|
new FakeSetLevelRepository,
|
|
new FakeElementRepository,
|
|
new FakeScheduleRepository,
|
|
new EvenDistributionScheduler,
|
|
))->execute(new CreateScheduleRequest(
|
|
user: $this->user(),
|
|
setId: 999,
|
|
levelId: 1,
|
|
startDate: '2026-08-10',
|
|
targetDate: '2026-08-12',
|
|
workloadPlacement: 'middle',
|
|
));
|
|
}
|
|
|
|
public function test_it_rejects_a_missing_level(): void
|
|
{
|
|
$user = $this->user();
|
|
$setRepository = new FakeSetRepository;
|
|
$set = $setRepository->create(new CreateSetDto(
|
|
name: 'Bible',
|
|
creator: $user,
|
|
));
|
|
|
|
$this->expectException(BadRequestException::class);
|
|
$this->expectExceptionMessage('levelId is required');
|
|
|
|
(new CreateSchedule(
|
|
$setRepository,
|
|
new FakeSetLevelRepository,
|
|
new FakeElementRepository,
|
|
new FakeScheduleRepository,
|
|
new EvenDistributionScheduler,
|
|
))->execute(new CreateScheduleRequest(
|
|
user: $user,
|
|
setId: $set->getId(),
|
|
levelId: null,
|
|
startDate: '2026-08-10',
|
|
targetDate: '2026-08-12',
|
|
workloadPlacement: 'middle',
|
|
));
|
|
}
|
|
|
|
public function test_it_rejects_a_level_from_another_set(): void
|
|
{
|
|
$user = $this->user();
|
|
$setRepository = new FakeSetRepository;
|
|
$setLevelRepository = new FakeSetLevelRepository;
|
|
$bible = $setRepository->create(new CreateSetDto(
|
|
name: 'Bible',
|
|
creator: $user,
|
|
));
|
|
$course = $setRepository->create(new CreateSetDto(
|
|
name: 'Course',
|
|
creator: $user,
|
|
));
|
|
$moduleLevel = $this->createLevel(
|
|
$setLevelRepository,
|
|
$course,
|
|
'module',
|
|
);
|
|
|
|
$this->expectException(BadRequestException::class);
|
|
$this->expectExceptionMessage('level is not available for set');
|
|
|
|
(new CreateSchedule(
|
|
$setRepository,
|
|
$setLevelRepository,
|
|
new FakeElementRepository,
|
|
new FakeScheduleRepository,
|
|
new EvenDistributionScheduler,
|
|
))->execute(new CreateScheduleRequest(
|
|
user: $user,
|
|
setId: $bible->getId(),
|
|
levelId: $moduleLevel->getId(),
|
|
startDate: '2026-08-10',
|
|
targetDate: '2026-08-12',
|
|
workloadPlacement: 'middle',
|
|
));
|
|
}
|
|
|
|
public function test_it_rejects_a_level_without_elements(): void
|
|
{
|
|
$user = $this->user();
|
|
$setRepository = new FakeSetRepository;
|
|
$setLevelRepository = new FakeSetLevelRepository;
|
|
$set = $setRepository->create(new CreateSetDto(
|
|
name: 'Bible',
|
|
creator: $user,
|
|
));
|
|
$chapterLevel = $this->createLevel(
|
|
$setLevelRepository,
|
|
$set,
|
|
'chapter',
|
|
);
|
|
|
|
$this->expectException(BadRequestException::class);
|
|
$this->expectExceptionMessage('level has no elements');
|
|
|
|
(new CreateSchedule(
|
|
$setRepository,
|
|
$setLevelRepository,
|
|
new FakeElementRepository,
|
|
new FakeScheduleRepository,
|
|
new EvenDistributionScheduler,
|
|
))->execute(new CreateScheduleRequest(
|
|
user: $user,
|
|
setId: $set->getId(),
|
|
levelId: $chapterLevel->getId(),
|
|
startDate: '2026-08-10',
|
|
targetDate: '2026-08-12',
|
|
workloadPlacement: 'middle',
|
|
));
|
|
}
|
|
|
|
public function test_it_rejects_invalid_dates(): void
|
|
{
|
|
$user = $this->user();
|
|
$setRepository = new FakeSetRepository;
|
|
$setLevelRepository = new FakeSetLevelRepository;
|
|
$set = $setRepository->create(new CreateSetDto(
|
|
name: 'Bible',
|
|
creator: $user,
|
|
));
|
|
$chapterLevel = $this->createLevel(
|
|
$setLevelRepository,
|
|
$set,
|
|
'chapter',
|
|
);
|
|
|
|
$this->expectException(BadRequestException::class);
|
|
$this->expectExceptionMessage('startDate must be a valid date');
|
|
|
|
(new CreateSchedule(
|
|
$setRepository,
|
|
$setLevelRepository,
|
|
new FakeElementRepository,
|
|
new FakeScheduleRepository,
|
|
new EvenDistributionScheduler,
|
|
))->execute(new CreateScheduleRequest(
|
|
user: $user,
|
|
setId: $set->getId(),
|
|
levelId: $chapterLevel->getId(),
|
|
startDate: '2026-02-30',
|
|
targetDate: '2026-08-12',
|
|
workloadPlacement: 'middle',
|
|
));
|
|
}
|
|
|
|
public function test_it_rejects_a_target_before_the_start(): void
|
|
{
|
|
$user = $this->user();
|
|
$setRepository = new FakeSetRepository;
|
|
$setLevelRepository = new FakeSetLevelRepository;
|
|
$set = $setRepository->create(new CreateSetDto(
|
|
name: 'Bible',
|
|
creator: $user,
|
|
));
|
|
$chapterLevel = $this->createLevel(
|
|
$setLevelRepository,
|
|
$set,
|
|
'chapter',
|
|
);
|
|
|
|
$this->expectException(BadRequestException::class);
|
|
$this->expectExceptionMessage(
|
|
'targetDate must not be before startDate',
|
|
);
|
|
|
|
(new CreateSchedule(
|
|
$setRepository,
|
|
$setLevelRepository,
|
|
new FakeElementRepository,
|
|
new FakeScheduleRepository,
|
|
new EvenDistributionScheduler,
|
|
))->execute(new CreateScheduleRequest(
|
|
user: $user,
|
|
setId: $set->getId(),
|
|
levelId: $chapterLevel->getId(),
|
|
startDate: '2026-08-12',
|
|
targetDate: '2026-08-10',
|
|
workloadPlacement: 'middle',
|
|
));
|
|
}
|
|
|
|
private function createLevel(
|
|
FakeSetLevelRepository $repository,
|
|
Set $set,
|
|
string $kind,
|
|
): SetLevel {
|
|
return $repository->create(new CreateSetLevelDto(
|
|
set: $set,
|
|
kind: $kind,
|
|
));
|
|
}
|
|
|
|
private function user(): User
|
|
{
|
|
return new User(
|
|
id: 7,
|
|
email: new EmailAddress('reader@example.com'),
|
|
passwordHash: 'hashed-password',
|
|
);
|
|
}
|
|
}
|