user(1, 'reader@example.com'); $repository = new FakeScheduleRepository; $schedule = $repository->create($this->schedule($user)); $now = $this->utc('2026-08-15T12:00:00'); $assignment = (new SetAssignmentCompletion( $repository, new FakeClock($now), ))->execute(new SetAssignmentCompletionRequest( assignmentId: $schedule->getAssignments()[0]->getId(), user: $user, completed: true, )); $this->assertSame($now, $assignment->getCompletedAt()); $this->assertSame( $now, $repository->findAssignmentForUser( $assignment->getId(), $user, )?->getCompletedAt(), ); } public function test_it_preserves_the_first_completion_time(): void { $user = $this->user(1, 'reader@example.com'); $repository = new FakeScheduleRepository; $schedule = $repository->create($this->schedule($user)); $assignmentId = $schedule->getAssignments()[0]->getId(); $firstCompletion = $this->utc('2026-08-15T12:00:00'); $laterCompletion = $this->utc('2026-08-15T13:00:00'); (new SetAssignmentCompletion( $repository, new FakeClock($firstCompletion), ))->execute(new SetAssignmentCompletionRequest( assignmentId: $assignmentId, user: $user, completed: true, )); $assignment = (new SetAssignmentCompletion( $repository, new FakeClock($laterCompletion), ))->execute(new SetAssignmentCompletionRequest( assignmentId: $assignmentId, user: $user, completed: true, )); $this->assertSame( $firstCompletion, $assignment->getCompletedAt(), ); } public function test_it_reopens_a_completed_assignment(): void { $user = $this->user(1, 'reader@example.com'); $repository = new FakeScheduleRepository; $schedule = $repository->create($this->schedule($user)); $assignmentId = $schedule->getAssignments()[0]->getId(); $useCase = new SetAssignmentCompletion( $repository, new FakeClock($this->utc('2026-08-15T12:00:00')), ); $useCase->execute(new SetAssignmentCompletionRequest( assignmentId: $assignmentId, user: $user, completed: true, )); $assignment = $useCase->execute( new SetAssignmentCompletionRequest( assignmentId: $assignmentId, user: $user, completed: false, ), ); $this->assertNull($assignment->getCompletedAt()); } public function test_it_hides_another_users_assignment(): void { $repository = new FakeScheduleRepository; $owner = $this->user(1, 'owner@example.com'); $schedule = $repository->create($this->schedule($owner)); $this->expectException(NotFoundException::class); $this->expectExceptionMessage('assignment not found'); (new SetAssignmentCompletion( $repository, new FakeClock($this->utc('2026-08-15T12:00:00')), ))->execute(new SetAssignmentCompletionRequest( assignmentId: $schedule->getAssignments()[0]->getId(), user: $this->user(2, 'viewer@example.com'), completed: true, )); } public function test_it_requires_a_boolean_completion_value(): void { $this->expectException(BadRequestException::class); $this->expectExceptionMessage('completed must be a boolean'); (new SetAssignmentCompletion( new FakeScheduleRepository, new FakeClock($this->utc('2026-08-15T12:00:00')), ))->execute(new SetAssignmentCompletionRequest( assignmentId: 1, user: $this->user(1, 'reader@example.com'), completed: null, )); } private function schedule(User $user): CreateScheduleDto { $scheduledDate = $this->utc('2026-08-20'); return new CreateScheduleDto( user: $user, setName: 'Course', elementKind: 'lesson', startDate: $scheduledDate, targetDate: $scheduledDate, assignments: [ new CreateScheduleAssignmentDto( name: 'Lesson', kind: 'lesson', path: ['Course', 'Lesson'], scheduledDate: $scheduledDate, position: 1, ), ], ); } 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')); } }