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