test set scheduling flow
This commit is contained in:
parent
0049248231
commit
bdb266746d
3 changed files with 724 additions and 0 deletions
97
backend/tests/Fakes/FakeScheduleRepository.php
Normal file
97
backend/tests/Fakes/FakeScheduleRepository.php
Normal file
|
|
@ -0,0 +1,97 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Tests\Fakes;
|
||||||
|
|
||||||
|
use App\Schedule\CreateScheduleDto;
|
||||||
|
use App\Schedule\Schedule;
|
||||||
|
use App\Schedule\ScheduleAssignment;
|
||||||
|
use App\Schedule\ScheduleRepository;
|
||||||
|
use App\User\User;
|
||||||
|
|
||||||
|
class FakeScheduleRepository implements ScheduleRepository
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @var array<int, Schedule>
|
||||||
|
*/
|
||||||
|
private array $schedules = [];
|
||||||
|
|
||||||
|
public function create(CreateScheduleDto $dto): Schedule
|
||||||
|
{
|
||||||
|
$id = count($this->schedules) + 1;
|
||||||
|
$assignments = [];
|
||||||
|
|
||||||
|
foreach ($dto->assignments as $assignmentDto) {
|
||||||
|
$assignments[] = new ScheduleAssignment(
|
||||||
|
id: count($assignments) + 1,
|
||||||
|
element: $assignmentDto->element,
|
||||||
|
scheduledDate: $assignmentDto->scheduledDate,
|
||||||
|
position: $assignmentDto->position,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
$schedule = new Schedule(
|
||||||
|
id: $id,
|
||||||
|
user: $dto->user,
|
||||||
|
set: $dto->set,
|
||||||
|
elementKind: $dto->elementKind,
|
||||||
|
startDate: $dto->startDate,
|
||||||
|
targetDate: $dto->targetDate,
|
||||||
|
assignments: $assignments,
|
||||||
|
);
|
||||||
|
$this->schedules[$id] = $schedule;
|
||||||
|
|
||||||
|
return $this->copy($schedule);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function findForUser(int $id, User $user): ?Schedule
|
||||||
|
{
|
||||||
|
$schedule = $this->schedules[$id] ?? null;
|
||||||
|
if ($schedule === null || $schedule->getUser()->getId()
|
||||||
|
!== $user->getId()
|
||||||
|
) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->copy($schedule);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function findAllForUser(User $user): array
|
||||||
|
{
|
||||||
|
$schedules = array_filter(
|
||||||
|
$this->schedules,
|
||||||
|
function (Schedule $schedule) use ($user): bool {
|
||||||
|
return $schedule->getUser()->getId() === $user->getId();
|
||||||
|
},
|
||||||
|
);
|
||||||
|
krsort($schedules);
|
||||||
|
|
||||||
|
return array_map(function (Schedule $schedule): Schedule {
|
||||||
|
return $this->copy($schedule);
|
||||||
|
}, array_values($schedules));
|
||||||
|
}
|
||||||
|
|
||||||
|
private function copy(Schedule $schedule): Schedule
|
||||||
|
{
|
||||||
|
$assignments = array_map(
|
||||||
|
function (ScheduleAssignment $assignment): ScheduleAssignment {
|
||||||
|
return new ScheduleAssignment(
|
||||||
|
id: $assignment->getId(),
|
||||||
|
element: $assignment->getElement(),
|
||||||
|
scheduledDate: $assignment->getScheduledDate(),
|
||||||
|
position: $assignment->getPosition(),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
$schedule->getAssignments(),
|
||||||
|
);
|
||||||
|
|
||||||
|
return new Schedule(
|
||||||
|
id: $schedule->getId(),
|
||||||
|
user: $schedule->getUser(),
|
||||||
|
set: $schedule->getSet(),
|
||||||
|
elementKind: $schedule->getElementKind(),
|
||||||
|
startDate: $schedule->getStartDate(),
|
||||||
|
targetDate: $schedule->getTargetDate(),
|
||||||
|
assignments: $assignments,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
313
backend/tests/Feature/Schedule/ScheduleEndpointTest.php
Normal file
313
backend/tests/Feature/Schedule/ScheduleEndpointTest.php
Normal file
|
|
@ -0,0 +1,313 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Tests\Feature\Schedule;
|
||||||
|
|
||||||
|
use App\Auth\CreateSessionDto;
|
||||||
|
use App\Auth\SessionRepository;
|
||||||
|
use App\Element\CreateElementDto;
|
||||||
|
use App\Element\ElementRepository;
|
||||||
|
use App\Http\Middleware\AuthMiddleware;
|
||||||
|
use App\Set\CreateSetDto;
|
||||||
|
use App\Set\Set;
|
||||||
|
use App\Set\SetRepository;
|
||||||
|
use App\Shared\ValueObject\EmailAddress;
|
||||||
|
use App\User\CreateUserDto;
|
||||||
|
use App\User\User;
|
||||||
|
use App\User\UserRepository;
|
||||||
|
use DateTimeImmutable;
|
||||||
|
use DateTimeZone;
|
||||||
|
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||||
|
use Illuminate\Testing\TestResponse;
|
||||||
|
use Tests\TestCase;
|
||||||
|
|
||||||
|
class ScheduleEndpointTest extends TestCase
|
||||||
|
{
|
||||||
|
use RefreshDatabase;
|
||||||
|
|
||||||
|
public function test_it_creates_and_returns_a_schedule(): void
|
||||||
|
{
|
||||||
|
$user = $this->createUser('reader@example.com');
|
||||||
|
$creator = $this->createUser('creator@example.com');
|
||||||
|
$set = $this->createSet($creator, 'Bible');
|
||||||
|
$repository = app(ElementRepository::class);
|
||||||
|
$genesis = $repository->create(new CreateElementDto(
|
||||||
|
set: $set,
|
||||||
|
name: 'Genesis',
|
||||||
|
kind: 'book',
|
||||||
|
parentElement: null,
|
||||||
|
));
|
||||||
|
$creation = $repository->create(new CreateElementDto(
|
||||||
|
set: $set,
|
||||||
|
name: 'Creation',
|
||||||
|
kind: 'portion',
|
||||||
|
parentElement: $genesis,
|
||||||
|
));
|
||||||
|
$chapterOne = $repository->create(new CreateElementDto(
|
||||||
|
set: $set,
|
||||||
|
name: 'Chapter 1',
|
||||||
|
kind: 'chapter',
|
||||||
|
parentElement: $creation,
|
||||||
|
));
|
||||||
|
$exodus = $repository->create(new CreateElementDto(
|
||||||
|
set: $set,
|
||||||
|
name: 'Exodus',
|
||||||
|
kind: 'book',
|
||||||
|
parentElement: null,
|
||||||
|
));
|
||||||
|
$chapterTwo = $repository->create(new CreateElementDto(
|
||||||
|
set: $set,
|
||||||
|
name: 'Chapter 1',
|
||||||
|
kind: 'chapter',
|
||||||
|
parentElement: $exodus,
|
||||||
|
));
|
||||||
|
$this->createSession($user, 'valid-token');
|
||||||
|
|
||||||
|
$response = $this->credentialedPost('/api/schedules', [
|
||||||
|
'setId' => $set->getId(),
|
||||||
|
'elementKind' => 'chapter',
|
||||||
|
'startDate' => '2026-08-10',
|
||||||
|
'targetDate' => '2026-08-12',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$response->assertCreated()->assertExactJson([
|
||||||
|
'schedule' => [
|
||||||
|
'id' => 1,
|
||||||
|
'set' => [
|
||||||
|
'id' => $set->getId(),
|
||||||
|
'name' => 'Bible',
|
||||||
|
],
|
||||||
|
'elementKind' => 'chapter',
|
||||||
|
'startDate' => '2026-08-10',
|
||||||
|
'targetDate' => '2026-08-12',
|
||||||
|
'assignmentCount' => 2,
|
||||||
|
'days' => [
|
||||||
|
[
|
||||||
|
'date' => '2026-08-10',
|
||||||
|
'assignments' => [
|
||||||
|
[
|
||||||
|
'element' => [
|
||||||
|
'id' => $chapterOne->getId(),
|
||||||
|
'name' => 'Chapter 1',
|
||||||
|
'kind' => 'chapter',
|
||||||
|
'path' => [
|
||||||
|
'Genesis',
|
||||||
|
'Creation',
|
||||||
|
'Chapter 1',
|
||||||
|
],
|
||||||
|
],
|
||||||
|
],
|
||||||
|
],
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'date' => '2026-08-11',
|
||||||
|
'assignments' => [],
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'date' => '2026-08-12',
|
||||||
|
'assignments' => [
|
||||||
|
[
|
||||||
|
'element' => [
|
||||||
|
'id' => $chapterTwo->getId(),
|
||||||
|
'name' => 'Chapter 1',
|
||||||
|
'kind' => 'chapter',
|
||||||
|
'path' => [
|
||||||
|
'Exodus',
|
||||||
|
'Chapter 1',
|
||||||
|
],
|
||||||
|
],
|
||||||
|
],
|
||||||
|
],
|
||||||
|
],
|
||||||
|
],
|
||||||
|
],
|
||||||
|
]);
|
||||||
|
$this->assertDatabaseHas('schedules', [
|
||||||
|
'user_id' => $user->getId(),
|
||||||
|
'set_id' => $set->getId(),
|
||||||
|
'element_kind' => 'chapter',
|
||||||
|
'start_date' => '2026-08-10',
|
||||||
|
'target_date' => '2026-08-12',
|
||||||
|
]);
|
||||||
|
$this->assertDatabaseCount('schedule_assignments', 2);
|
||||||
|
|
||||||
|
$this->credentialedGet('/api/schedules/1')
|
||||||
|
->assertOk()
|
||||||
|
->assertExactJson($response->json());
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_it_lists_only_the_users_schedules_newest_first(): void
|
||||||
|
{
|
||||||
|
$user = $this->createUser('reader@example.com');
|
||||||
|
$otherUser = $this->createUser('other@example.com');
|
||||||
|
$set = $this->createSet($user, 'Course');
|
||||||
|
app(ElementRepository::class)->create(new CreateElementDto(
|
||||||
|
set: $set,
|
||||||
|
name: 'Welcome',
|
||||||
|
kind: 'lesson',
|
||||||
|
parentElement: null,
|
||||||
|
));
|
||||||
|
$this->createSession($user, 'valid-token');
|
||||||
|
$this->createSession($otherUser, 'other-token');
|
||||||
|
|
||||||
|
$this->credentialedPost('/api/schedules', [
|
||||||
|
'setId' => $set->getId(),
|
||||||
|
'elementKind' => 'lesson',
|
||||||
|
'startDate' => '2026-08-01',
|
||||||
|
'targetDate' => '2026-08-01',
|
||||||
|
])->assertCreated();
|
||||||
|
$this->credentialedPost('/api/schedules', [
|
||||||
|
'setId' => $set->getId(),
|
||||||
|
'elementKind' => 'lesson',
|
||||||
|
'startDate' => '2026-09-01',
|
||||||
|
'targetDate' => '2026-09-01',
|
||||||
|
])->assertCreated();
|
||||||
|
$this->withCredentials()
|
||||||
|
->withUnencryptedCookie(
|
||||||
|
AuthMiddleware::COOKIE_NAME,
|
||||||
|
'other-token',
|
||||||
|
)->postJson('/api/schedules', [
|
||||||
|
'setId' => $set->getId(),
|
||||||
|
'elementKind' => 'lesson',
|
||||||
|
'startDate' => '2026-10-01',
|
||||||
|
'targetDate' => '2026-10-01',
|
||||||
|
])->assertCreated();
|
||||||
|
|
||||||
|
$this->credentialedGet('/api/schedules')
|
||||||
|
->assertOk()
|
||||||
|
->assertExactJson([
|
||||||
|
'schedules' => [
|
||||||
|
[
|
||||||
|
'id' => 2,
|
||||||
|
'set' => [
|
||||||
|
'id' => $set->getId(),
|
||||||
|
'name' => 'Course',
|
||||||
|
],
|
||||||
|
'elementKind' => 'lesson',
|
||||||
|
'startDate' => '2026-09-01',
|
||||||
|
'targetDate' => '2026-09-01',
|
||||||
|
'assignmentCount' => 1,
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'id' => 1,
|
||||||
|
'set' => [
|
||||||
|
'id' => $set->getId(),
|
||||||
|
'name' => 'Course',
|
||||||
|
],
|
||||||
|
'elementKind' => 'lesson',
|
||||||
|
'startDate' => '2026-08-01',
|
||||||
|
'targetDate' => '2026-08-01',
|
||||||
|
'assignmentCount' => 1,
|
||||||
|
],
|
||||||
|
],
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_it_hides_another_users_schedule(): void
|
||||||
|
{
|
||||||
|
$owner = $this->createUser('owner@example.com');
|
||||||
|
$viewer = $this->createUser('viewer@example.com');
|
||||||
|
$set = $this->createSet($owner, 'Course');
|
||||||
|
app(ElementRepository::class)->create(new CreateElementDto(
|
||||||
|
set: $set,
|
||||||
|
name: 'Welcome',
|
||||||
|
kind: 'lesson',
|
||||||
|
parentElement: null,
|
||||||
|
));
|
||||||
|
$this->createSession($owner, 'owner-token');
|
||||||
|
$this->createSession($viewer, 'viewer-token');
|
||||||
|
|
||||||
|
$this->withCredentials()
|
||||||
|
->withUnencryptedCookie(
|
||||||
|
AuthMiddleware::COOKIE_NAME,
|
||||||
|
'owner-token',
|
||||||
|
)->postJson('/api/schedules', [
|
||||||
|
'setId' => $set->getId(),
|
||||||
|
'elementKind' => 'lesson',
|
||||||
|
'startDate' => '2026-08-01',
|
||||||
|
'targetDate' => '2026-08-01',
|
||||||
|
])->assertCreated();
|
||||||
|
|
||||||
|
$this->withCredentials()
|
||||||
|
->withUnencryptedCookie(
|
||||||
|
AuthMiddleware::COOKIE_NAME,
|
||||||
|
'viewer-token',
|
||||||
|
)->getJson('/api/schedules/1')
|
||||||
|
->assertNotFound()
|
||||||
|
->assertExactJson(['error' => 'schedule not found']);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_it_rejects_invalid_creation_input(): void
|
||||||
|
{
|
||||||
|
$user = $this->createUser('reader@example.com');
|
||||||
|
$this->createSession($user, 'valid-token');
|
||||||
|
|
||||||
|
$this->credentialedPost('/api/schedules', [
|
||||||
|
'setId' => 999,
|
||||||
|
'elementKind' => 'chapter',
|
||||||
|
'startDate' => '2026-08-12',
|
||||||
|
'targetDate' => '2026-08-10',
|
||||||
|
])->assertNotFound()->assertExactJson([
|
||||||
|
'error' => 'set not found',
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_schedule_endpoints_require_authentication(): void
|
||||||
|
{
|
||||||
|
$this->getJson('/api/schedules')->assertStatus(401);
|
||||||
|
$this->getJson('/api/schedules/1')->assertStatus(401);
|
||||||
|
$this->postJson('/api/schedules', [])->assertStatus(401);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function createUser(string $email): User
|
||||||
|
{
|
||||||
|
return app(UserRepository::class)->create(new CreateUserDto(
|
||||||
|
email: new EmailAddress($email),
|
||||||
|
passwordHash: 'hashed-password',
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
private function createSet(User $user, string $name): Set
|
||||||
|
{
|
||||||
|
return app(SetRepository::class)->create(new CreateSetDto(
|
||||||
|
name: $name,
|
||||||
|
creator: $user,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
private function createSession(User $user, string $token): void
|
||||||
|
{
|
||||||
|
$createdAt = new DateTimeImmutable(
|
||||||
|
'2026-08-03T12:00:00',
|
||||||
|
new DateTimeZone('UTC'),
|
||||||
|
);
|
||||||
|
app(SessionRepository::class)->create(new CreateSessionDto(
|
||||||
|
token: $token,
|
||||||
|
user: $user,
|
||||||
|
createdAt: $createdAt,
|
||||||
|
expiresAt: $createdAt->modify('+7 days'),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param array<string, mixed> $payload
|
||||||
|
*/
|
||||||
|
private function credentialedPost(
|
||||||
|
string $uri,
|
||||||
|
array $payload,
|
||||||
|
): TestResponse {
|
||||||
|
return $this->withCredentials()
|
||||||
|
->withUnencryptedCookie(
|
||||||
|
AuthMiddleware::COOKIE_NAME,
|
||||||
|
'valid-token',
|
||||||
|
)->postJson($uri, $payload);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function credentialedGet(string $uri): TestResponse
|
||||||
|
{
|
||||||
|
return $this->withCredentials()
|
||||||
|
->withUnencryptedCookie(
|
||||||
|
AuthMiddleware::COOKIE_NAME,
|
||||||
|
'valid-token',
|
||||||
|
)->getJson($uri);
|
||||||
|
}
|
||||||
|
}
|
||||||
314
backend/tests/Unit/Schedule/UseCases/CreateScheduleTest.php
Normal file
314
backend/tests/Unit/Schedule/UseCases/CreateScheduleTest.php
Normal file
|
|
@ -0,0 +1,314 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Tests\Unit\Schedule\UseCases;
|
||||||
|
|
||||||
|
use App\Element\CreateElementDto;
|
||||||
|
use App\Exceptions\BadRequestException;
|
||||||
|
use App\Exceptions\NotFoundException;
|
||||||
|
use App\Schedule\UseCases\CreateSchedule\CreateSchedule;
|
||||||
|
use App\Schedule\UseCases\CreateSchedule\CreateScheduleRequest;
|
||||||
|
use App\Set\CreateSetDto;
|
||||||
|
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;
|
||||||
|
|
||||||
|
class CreateScheduleTest extends TestCase
|
||||||
|
{
|
||||||
|
public function test_it_schedules_every_matching_element_in_tree_order(): void
|
||||||
|
{
|
||||||
|
$user = $this->user();
|
||||||
|
$setRepository = new FakeSetRepository;
|
||||||
|
$elementRepository = new FakeElementRepository;
|
||||||
|
$scheduleRepository = new FakeScheduleRepository;
|
||||||
|
$set = $setRepository->create(new CreateSetDto(
|
||||||
|
name: 'Bible',
|
||||||
|
creator: $user,
|
||||||
|
));
|
||||||
|
$genesis = $elementRepository->create(new CreateElementDto(
|
||||||
|
set: $set,
|
||||||
|
name: 'Genesis',
|
||||||
|
kind: 'book',
|
||||||
|
parentElement: null,
|
||||||
|
));
|
||||||
|
$exodus = $elementRepository->create(new CreateElementDto(
|
||||||
|
set: $set,
|
||||||
|
name: 'Exodus',
|
||||||
|
kind: 'book',
|
||||||
|
parentElement: null,
|
||||||
|
));
|
||||||
|
$exodusChapterOne = $elementRepository->create(
|
||||||
|
new CreateElementDto(
|
||||||
|
set: $set,
|
||||||
|
name: 'Exodus 1',
|
||||||
|
kind: 'chapter',
|
||||||
|
parentElement: $exodus,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
$creation = $elementRepository->create(new CreateElementDto(
|
||||||
|
set: $set,
|
||||||
|
name: 'Creation',
|
||||||
|
kind: 'portion',
|
||||||
|
parentElement: $genesis,
|
||||||
|
));
|
||||||
|
$genesisChapterOne = $elementRepository->create(
|
||||||
|
new CreateElementDto(
|
||||||
|
set: $set,
|
||||||
|
name: 'Genesis 1',
|
||||||
|
kind: 'chapter',
|
||||||
|
parentElement: $creation,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
$genesisChapterTwo = $elementRepository->create(
|
||||||
|
new CreateElementDto(
|
||||||
|
set: $set,
|
||||||
|
name: 'Genesis 2',
|
||||||
|
kind: 'chapter',
|
||||||
|
parentElement: $creation,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
$genesisChapterThree = $elementRepository->create(
|
||||||
|
new CreateElementDto(
|
||||||
|
set: $set,
|
||||||
|
name: 'Genesis 3',
|
||||||
|
kind: 'chapter',
|
||||||
|
parentElement: $creation,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
$exodusChapterTwo = $elementRepository->create(
|
||||||
|
new CreateElementDto(
|
||||||
|
set: $set,
|
||||||
|
name: 'Exodus 2',
|
||||||
|
kind: 'chapter',
|
||||||
|
parentElement: $exodus,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
$schedule = (new CreateSchedule(
|
||||||
|
$setRepository,
|
||||||
|
$elementRepository,
|
||||||
|
$scheduleRepository,
|
||||||
|
))->execute(new CreateScheduleRequest(
|
||||||
|
user: $user,
|
||||||
|
setId: $set->getId(),
|
||||||
|
elementKind: 'chapter',
|
||||||
|
startDate: '2026-08-10',
|
||||||
|
targetDate: '2026-08-12',
|
||||||
|
));
|
||||||
|
|
||||||
|
$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->getId(),
|
||||||
|
$genesisChapterTwo->getId(),
|
||||||
|
$genesisChapterThree->getId(),
|
||||||
|
$exodusChapterOne->getId(),
|
||||||
|
$exodusChapterTwo->getId(),
|
||||||
|
],
|
||||||
|
array_map(function ($assignment): int {
|
||||||
|
return $assignment->getElement()->getId();
|
||||||
|
}, $schedule->getAssignments()),
|
||||||
|
);
|
||||||
|
$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;
|
||||||
|
$elementRepository = new FakeElementRepository;
|
||||||
|
$set = $setRepository->create(new CreateSetDto(
|
||||||
|
name: 'Course',
|
||||||
|
creator: $user,
|
||||||
|
));
|
||||||
|
|
||||||
|
foreach (['First', 'Second', 'Third'] as $name) {
|
||||||
|
$elementRepository->create(new CreateElementDto(
|
||||||
|
set: $set,
|
||||||
|
name: $name,
|
||||||
|
kind: 'lesson',
|
||||||
|
parentElement: null,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
$schedule = (new CreateSchedule(
|
||||||
|
$setRepository,
|
||||||
|
$elementRepository,
|
||||||
|
new FakeScheduleRepository,
|
||||||
|
))->execute(new CreateScheduleRequest(
|
||||||
|
user: $user,
|
||||||
|
setId: $set->getId(),
|
||||||
|
elementKind: 'lesson',
|
||||||
|
startDate: '2026-08-10',
|
||||||
|
targetDate: '2026-08-16',
|
||||||
|
));
|
||||||
|
|
||||||
|
$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;
|
||||||
|
$elementRepository = new FakeElementRepository;
|
||||||
|
$set = $setRepository->create(new CreateSetDto(
|
||||||
|
name: 'Project',
|
||||||
|
creator: $user,
|
||||||
|
));
|
||||||
|
$elementRepository->create(new CreateElementDto(
|
||||||
|
set: $set,
|
||||||
|
name: 'Ship it',
|
||||||
|
kind: 'milestone',
|
||||||
|
parentElement: null,
|
||||||
|
));
|
||||||
|
|
||||||
|
$schedule = (new CreateSchedule(
|
||||||
|
$setRepository,
|
||||||
|
$elementRepository,
|
||||||
|
new FakeScheduleRepository,
|
||||||
|
))->execute(new CreateScheduleRequest(
|
||||||
|
user: $user,
|
||||||
|
setId: $set->getId(),
|
||||||
|
elementKind: 'milestone',
|
||||||
|
startDate: '2020-01-01',
|
||||||
|
targetDate: '2030-01-01',
|
||||||
|
));
|
||||||
|
|
||||||
|
$this->assertSame(
|
||||||
|
'2020-01-01',
|
||||||
|
$schedule->getAssignments()[0]
|
||||||
|
->getScheduledDate()
|
||||||
|
->format('Y-m-d'),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_it_rejects_an_unknown_set(): void
|
||||||
|
{
|
||||||
|
$this->expectException(NotFoundException::class);
|
||||||
|
$this->expectExceptionMessage('set not found');
|
||||||
|
|
||||||
|
(new CreateSchedule(
|
||||||
|
new FakeSetRepository,
|
||||||
|
new FakeElementRepository,
|
||||||
|
new FakeScheduleRepository,
|
||||||
|
))->execute(new CreateScheduleRequest(
|
||||||
|
user: $this->user(),
|
||||||
|
setId: 999,
|
||||||
|
elementKind: 'chapter',
|
||||||
|
startDate: '2026-08-10',
|
||||||
|
targetDate: '2026-08-12',
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_it_rejects_an_unavailable_element_kind(): void
|
||||||
|
{
|
||||||
|
$user = $this->user();
|
||||||
|
$setRepository = new FakeSetRepository;
|
||||||
|
$set = $setRepository->create(new CreateSetDto(
|
||||||
|
name: 'Bible',
|
||||||
|
creator: $user,
|
||||||
|
));
|
||||||
|
|
||||||
|
$this->expectException(BadRequestException::class);
|
||||||
|
$this->expectExceptionMessage(
|
||||||
|
'elementKind is not available for set',
|
||||||
|
);
|
||||||
|
|
||||||
|
(new CreateSchedule(
|
||||||
|
$setRepository,
|
||||||
|
new FakeElementRepository,
|
||||||
|
new FakeScheduleRepository,
|
||||||
|
))->execute(new CreateScheduleRequest(
|
||||||
|
user: $user,
|
||||||
|
setId: $set->getId(),
|
||||||
|
elementKind: 'Chapter',
|
||||||
|
startDate: '2026-08-10',
|
||||||
|
targetDate: '2026-08-12',
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_it_rejects_invalid_dates(): void
|
||||||
|
{
|
||||||
|
$user = $this->user();
|
||||||
|
$setRepository = new FakeSetRepository;
|
||||||
|
$set = $setRepository->create(new CreateSetDto(
|
||||||
|
name: 'Bible',
|
||||||
|
creator: $user,
|
||||||
|
));
|
||||||
|
|
||||||
|
$this->expectException(BadRequestException::class);
|
||||||
|
$this->expectExceptionMessage('startDate must be a valid date');
|
||||||
|
|
||||||
|
(new CreateSchedule(
|
||||||
|
$setRepository,
|
||||||
|
new FakeElementRepository,
|
||||||
|
new FakeScheduleRepository,
|
||||||
|
))->execute(new CreateScheduleRequest(
|
||||||
|
user: $user,
|
||||||
|
setId: $set->getId(),
|
||||||
|
elementKind: 'chapter',
|
||||||
|
startDate: '2026-02-30',
|
||||||
|
targetDate: '2026-08-12',
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_it_rejects_a_target_before_the_start(): void
|
||||||
|
{
|
||||||
|
$user = $this->user();
|
||||||
|
$setRepository = new FakeSetRepository;
|
||||||
|
$set = $setRepository->create(new CreateSetDto(
|
||||||
|
name: 'Bible',
|
||||||
|
creator: $user,
|
||||||
|
));
|
||||||
|
|
||||||
|
$this->expectException(BadRequestException::class);
|
||||||
|
$this->expectExceptionMessage(
|
||||||
|
'targetDate must not be before startDate',
|
||||||
|
);
|
||||||
|
|
||||||
|
(new CreateSchedule(
|
||||||
|
$setRepository,
|
||||||
|
new FakeElementRepository,
|
||||||
|
new FakeScheduleRepository,
|
||||||
|
))->execute(new CreateScheduleRequest(
|
||||||
|
user: $user,
|
||||||
|
setId: $set->getId(),
|
||||||
|
elementKind: 'chapter',
|
||||||
|
startDate: '2026-08-12',
|
||||||
|
targetDate: '2026-08-10',
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
private function user(): User
|
||||||
|
{
|
||||||
|
return new User(
|
||||||
|
id: 7,
|
||||||
|
email: new EmailAddress('reader@example.com'),
|
||||||
|
passwordHash: 'hashed-password',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue