createFixtures(); } protected function tearDown(): void { $container = static::getContainer(); /** @var Connection $connection */ $connection = $container->get(Connection::class); $connection->executeStatement('DELETE FROM homework WHERE tenant_id = :tid', ['tid' => self::TENANT_ID]); $connection->executeStatement('DELETE FROM school_classes WHERE id IN (:id1, :id2)', ['id1' => self::CLASS_ID, 'id2' => self::TARGET_CLASS_ID]); $connection->executeStatement('DELETE FROM subjects WHERE id = :id', ['id' => self::SUBJECT_ID]); $connection->executeStatement('DELETE FROM users WHERE id IN (:o, :t)', ['o' => self::OWNER_TEACHER_ID, 't' => self::OTHER_TEACHER_ID]); parent::tearDown(); } private function createFixtures(): void { $container = static::getContainer(); /** @var Connection $connection */ $connection = $container->get(Connection::class); $schoolId = '550e8400-e29b-41d4-a716-ff6655440001'; $academicYearId = '550e8400-e29b-41d4-a716-ff6655440002'; $connection->executeStatement( "INSERT INTO users (id, tenant_id, email, hashed_password, first_name, last_name, roles, statut, created_at, updated_at) VALUES (:id, :tid, 'owner-hw@test.local', '', 'Owner', 'Teacher', '[\"ROLE_PROF\"]', 'active', NOW(), NOW()) ON CONFLICT (id) DO NOTHING", ['id' => self::OWNER_TEACHER_ID, 'tid' => self::TENANT_ID], ); $connection->executeStatement( "INSERT INTO users (id, tenant_id, email, hashed_password, first_name, last_name, roles, statut, created_at, updated_at) VALUES (:id, :tid, 'other-hw@test.local', '', 'Other', 'Teacher', '[\"ROLE_PROF\"]', 'active', NOW(), NOW()) ON CONFLICT (id) DO NOTHING", ['id' => self::OTHER_TEACHER_ID, 'tid' => self::TENANT_ID], ); $connection->executeStatement( "INSERT INTO school_classes (id, tenant_id, school_id, academic_year_id, name, status, created_at, updated_at) VALUES (:id, :tid, :sid, :ayid, 'Test-HW-Class', 'active', NOW(), NOW()) ON CONFLICT (id) DO NOTHING", ['id' => self::CLASS_ID, 'tid' => self::TENANT_ID, 'sid' => $schoolId, 'ayid' => $academicYearId], ); $connection->executeStatement( "INSERT INTO subjects (id, tenant_id, school_id, name, code, status, created_at, updated_at) VALUES (:id, :tid, :sid, 'Test-HW-Subject', 'THW', 'active', NOW(), NOW()) ON CONFLICT (id) DO NOTHING", ['id' => self::SUBJECT_ID, 'tid' => self::TENANT_ID, 'sid' => $schoolId], ); $connection->executeStatement( "INSERT INTO school_classes (id, tenant_id, school_id, academic_year_id, name, status, created_at, updated_at) VALUES (:id, :tid, :sid, :ayid, 'Test-HW-Target-Class', 'active', NOW(), NOW()) ON CONFLICT (id) DO NOTHING", ['id' => self::TARGET_CLASS_ID, 'tid' => self::TENANT_ID, 'sid' => $schoolId, 'ayid' => $academicYearId], ); } // ========================================================================= // Security - Without tenant (404) // ========================================================================= #[Test] public function getHomeworkListReturns404WithoutTenant(): void { $client = static::createClient(); $client->request('GET', '/api/homework', [ 'headers' => [ 'Host' => 'localhost', 'Accept' => 'application/json', ], ]); self::assertResponseStatusCodeSame(404); } #[Test] public function createHomeworkReturns404WithoutTenant(): void { $client = static::createClient(); $client->request('POST', '/api/homework', [ 'headers' => [ 'Host' => 'localhost', 'Accept' => 'application/json', 'Content-Type' => 'application/json', ], 'json' => [ 'classId' => self::CLASS_ID, 'subjectId' => self::SUBJECT_ID, 'title' => 'Devoir de maths', 'dueDate' => '2026-06-15', ], ]); self::assertResponseStatusCodeSame(404); } #[Test] public function updateHomeworkReturns404WithoutTenant(): void { $client = static::createClient(); $client->request('PATCH', '/api/homework/' . self::HOMEWORK_ID, [ 'headers' => [ 'Host' => 'localhost', 'Accept' => 'application/json', 'Content-Type' => 'application/merge-patch+json', ], 'json' => [ 'title' => 'Titre modifié', 'dueDate' => '2026-06-20', ], ]); self::assertResponseStatusCodeSame(404); } #[Test] public function deleteHomeworkReturns404WithoutTenant(): void { $client = static::createClient(); $client->request('DELETE', '/api/homework/' . self::HOMEWORK_ID, [ 'headers' => [ 'Host' => 'localhost', 'Accept' => 'application/json', ], ]); self::assertResponseStatusCodeSame(404); } // ========================================================================= // Security - Without authentication (401) // ========================================================================= #[Test] public function getHomeworkListReturns401WithoutAuthentication(): void { $client = static::createClient(); $client->request('GET', 'http://ecole-alpha.classeo.local/api/homework', [ 'headers' => [ 'Accept' => 'application/json', ], ]); self::assertResponseStatusCodeSame(401); } #[Test] public function createHomeworkReturns401WithoutAuthentication(): void { $client = static::createClient(); $client->request('POST', 'http://ecole-alpha.classeo.local/api/homework', [ 'headers' => [ 'Accept' => 'application/json', 'Content-Type' => 'application/json', ], 'json' => [ 'classId' => self::CLASS_ID, 'subjectId' => self::SUBJECT_ID, 'title' => 'Devoir de maths', 'dueDate' => '2026-06-15', ], ]); self::assertResponseStatusCodeSame(401); } #[Test] public function updateHomeworkReturns401WithoutAuthentication(): void { $client = static::createClient(); $client->request('PATCH', 'http://ecole-alpha.classeo.local/api/homework/' . self::HOMEWORK_ID, [ 'headers' => [ 'Accept' => 'application/json', 'Content-Type' => 'application/merge-patch+json', ], 'json' => [ 'title' => 'Titre modifié', 'dueDate' => '2026-06-20', ], ]); self::assertResponseStatusCodeSame(401); } #[Test] public function deleteHomeworkReturns401WithoutAuthentication(): void { $client = static::createClient(); $client->request('DELETE', 'http://ecole-alpha.classeo.local/api/homework/' . self::HOMEWORK_ID, [ 'headers' => [ 'Accept' => 'application/json', ], ]); self::assertResponseStatusCodeSame(401); } // ========================================================================= // 5.1-FUNC-005 (P1) - POST /homework with empty title -> 422 // ========================================================================= #[Test] public function createHomeworkWithEmptyTitleReturns422(): void { $client = $this->createAuthenticatedClient(self::OWNER_TEACHER_ID, ['ROLE_PROF']); $client->request('POST', 'http://ecole-alpha.classeo.local/api/homework', [ 'headers' => [ 'Accept' => 'application/json', 'Content-Type' => 'application/json', ], 'json' => [ 'classId' => self::CLASS_ID, 'subjectId' => self::SUBJECT_ID, 'title' => '', 'dueDate' => '2026-06-15', ], ]); self::assertResponseStatusCodeSame(422); } // ========================================================================= // 5.1-FUNC-006 (P1) - POST /homework with past due date -> 400 // ========================================================================= #[Test] public function createHomeworkWithPastDueDateReturns400(): void { $client = $this->createAuthenticatedClient(self::OWNER_TEACHER_ID, ['ROLE_PROF']); $client->request('POST', 'http://ecole-alpha.classeo.local/api/homework', [ 'headers' => [ 'Accept' => 'application/json', 'Content-Type' => 'application/json', ], 'json' => [ 'classId' => self::CLASS_ID, 'subjectId' => self::SUBJECT_ID, 'title' => 'Devoir de maths', 'dueDate' => '2020-01-01', ], ]); self::assertResponseStatusCodeSame(400); } // ========================================================================= // 5.1-FUNC-007 (P0) - PATCH /homework/{id} by non-owner -> 403 // ========================================================================= #[Test] public function updateHomeworkByNonOwnerReturns403(): void { $this->persistHomework(self::HOMEWORK_ID, HomeworkStatus::PUBLISHED); $client = $this->createAuthenticatedClient(self::OTHER_TEACHER_ID, ['ROLE_PROF']); $client->request('PATCH', 'http://ecole-alpha.classeo.local/api/homework/' . self::HOMEWORK_ID, [ 'headers' => [ 'Accept' => 'application/json', 'Content-Type' => 'application/merge-patch+json', ], 'json' => [ 'title' => 'Titre piraté', 'dueDate' => '2026-06-20', ], ]); self::assertResponseStatusCodeSame(403); } // ========================================================================= // 5.1-FUNC-008 (P1) - PATCH /homework/{id} on deleted homework -> 400 // ========================================================================= #[Test] public function updateDeletedHomeworkReturns400(): void { $this->persistHomework(self::DELETED_HOMEWORK_ID, HomeworkStatus::DELETED); $client = $this->createAuthenticatedClient(self::OWNER_TEACHER_ID, ['ROLE_PROF']); $client->request('PATCH', 'http://ecole-alpha.classeo.local/api/homework/' . self::DELETED_HOMEWORK_ID, [ 'headers' => [ 'Accept' => 'application/json', 'Content-Type' => 'application/merge-patch+json', ], 'json' => [ 'title' => 'Titre modifié', 'dueDate' => '2026-06-20', ], ]); self::assertResponseStatusCodeSame(400); } // ========================================================================= // 5.1-FUNC-009 (P0) - DELETE /homework/{id} by non-owner -> 403 // ========================================================================= #[Test] public function deleteHomeworkByNonOwnerReturns403(): void { $this->persistHomework(self::HOMEWORK_ID, HomeworkStatus::PUBLISHED); $client = $this->createAuthenticatedClient(self::OTHER_TEACHER_ID, ['ROLE_PROF']); $client->request('DELETE', 'http://ecole-alpha.classeo.local/api/homework/' . self::HOMEWORK_ID, [ 'headers' => [ 'Accept' => 'application/json', ], ]); self::assertResponseStatusCodeSame(403); } // ========================================================================= // 5.1-FUNC-010 (P1) - DELETE /homework/{id} on already deleted -> 400 // ========================================================================= #[Test] public function deleteAlreadyDeletedHomeworkReturns400(): void { $this->persistHomework(self::DELETED_HOMEWORK_ID, HomeworkStatus::DELETED); $client = $this->createAuthenticatedClient(self::OWNER_TEACHER_ID, ['ROLE_PROF']); $client->request('DELETE', 'http://ecole-alpha.classeo.local/api/homework/' . self::DELETED_HOMEWORK_ID, [ 'headers' => [ 'Accept' => 'application/json', ], ]); self::assertResponseStatusCodeSame(400); } // ========================================================================= // 5.2-FUNC-001 (P1) - POST /homework/{id}/duplicate without tenant -> 404 // ========================================================================= #[Test] public function duplicateHomeworkReturns404WithoutTenant(): void { $client = static::createClient(); $client->request('POST', '/api/homework/' . self::HOMEWORK_ID . '/duplicate', [ 'headers' => [ 'Host' => 'localhost', 'Accept' => 'application/json', 'Content-Type' => 'application/json', ], 'json' => [ 'targetClassIds' => [self::TARGET_CLASS_ID], ], ]); self::assertResponseStatusCodeSame(404); } // ========================================================================= // 5.2-FUNC-002 (P1) - POST /homework/{id}/duplicate without auth -> 401 // ========================================================================= #[Test] public function duplicateHomeworkReturns401WithoutAuthentication(): void { $client = static::createClient(); $client->request('POST', 'http://ecole-alpha.classeo.local/api/homework/' . self::HOMEWORK_ID . '/duplicate', [ 'headers' => [ 'Accept' => 'application/json', 'Content-Type' => 'application/json', ], 'json' => [ 'targetClassIds' => [self::TARGET_CLASS_ID], ], ]); self::assertResponseStatusCodeSame(401); } // ========================================================================= // 5.2-FUNC-003 (P1) - POST /homework/{id}/duplicate empty classes -> 400 // ========================================================================= #[Test] public function duplicateHomeworkReturns400WithEmptyTargetClasses(): void { $this->persistHomework(self::HOMEWORK_ID, HomeworkStatus::PUBLISHED); $client = $this->createAuthenticatedClient(self::OWNER_TEACHER_ID, ['ROLE_PROF']); $client->request('POST', 'http://ecole-alpha.classeo.local/api/homework/' . self::HOMEWORK_ID . '/duplicate', [ 'headers' => [ 'Accept' => 'application/json', 'Content-Type' => 'application/json', ], 'json' => [ 'targetClassIds' => [], ], ]); self::assertResponseStatusCodeSame(400); } // ========================================================================= // 5.2-FUNC-004 (P1) - POST /homework/{id}/duplicate not found -> 404 // ========================================================================= #[Test] public function duplicateHomeworkReturns404WhenHomeworkNotFound(): void { $nonExistentId = '00000000-0000-0000-0000-000000000099'; $client = $this->createAuthenticatedClient(self::OWNER_TEACHER_ID, ['ROLE_PROF']); $client->request('POST', 'http://ecole-alpha.classeo.local/api/homework/' . $nonExistentId . '/duplicate', [ 'headers' => [ 'Accept' => 'application/json', 'Content-Type' => 'application/json', ], 'json' => [ 'targetClassIds' => [self::TARGET_CLASS_ID], ], ]); self::assertResponseStatusCodeSame(404); } // ========================================================================= // 5.2-FUNC-005 (P1) - POST /homework/{id}/duplicate non-owner -> 404 // ========================================================================= #[Test] public function duplicateHomeworkReturns404WhenTeacherNotOwner(): void { $this->persistHomework(self::HOMEWORK_ID, HomeworkStatus::PUBLISHED); $client = $this->createAuthenticatedClient(self::OTHER_TEACHER_ID, ['ROLE_PROF']); $client->request('POST', 'http://ecole-alpha.classeo.local/api/homework/' . self::HOMEWORK_ID . '/duplicate', [ 'headers' => [ 'Accept' => 'application/json', 'Content-Type' => 'application/json', ], 'json' => [ 'targetClassIds' => [self::TARGET_CLASS_ID], ], ]); self::assertResponseStatusCodeSame(404); } // ========================================================================= // Helpers // ========================================================================= private function createAuthenticatedClient(string $userId, array $roles): \ApiPlatform\Symfony\Bundle\Test\Client { $client = static::createClient(); $user = new SecurityUser( userId: UserId::fromString($userId), email: 'teacher@classeo.local', hashedPassword: '', tenantId: TenantId::fromString(self::TENANT_ID), roles: $roles, ); $client->loginUser($user, 'api'); return $client; } private function persistHomework(string $homeworkId, HomeworkStatus $status): void { $now = new DateTimeImmutable(); $homework = Homework::reconstitute( id: HomeworkId::fromString($homeworkId), tenantId: TenantId::fromString(self::TENANT_ID), classId: ClassId::fromString(self::CLASS_ID), subjectId: SubjectId::fromString(self::SUBJECT_ID), teacherId: UserId::fromString(self::OWNER_TEACHER_ID), title: 'Devoir existant', description: null, dueDate: new DateTimeImmutable('2026-06-15'), status: $status, createdAt: $now, updatedAt: $now, ); /** @var HomeworkRepository $repository */ $repository = static::getContainer()->get(HomeworkRepository::class); $repository->save($homework); } }