get(Connection::class); $connection->executeStatement('UPDATE users SET image_rights_updated_by = NULL WHERE image_rights_updated_by = :id', ['id' => self::USER_ID]); $connection->executeStatement('DELETE FROM users WHERE id = :id', ['id' => self::STUDENT_1_ID]); $connection->executeStatement('DELETE FROM users WHERE id = :id', ['id' => self::STUDENT_2_ID]); $connection->executeStatement('DELETE FROM users WHERE id = :id', ['id' => self::USER_ID]); parent::tearDown(); } // ========================================================================= // Security - Without tenant // ========================================================================= #[Test] public function getImageRightsReturns404WithoutTenant(): void { $client = static::createClient(); $client->request('GET', '/api/students/image-rights', [ 'headers' => [ 'Host' => 'localhost', 'Accept' => 'application/json', ], ]); self::assertResponseStatusCodeSame(404); } #[Test] public function updateImageRightsReturns404WithoutTenant(): void { $client = static::createClient(); $client->request('PATCH', '/api/students/' . self::STUDENT_1_ID . '/image-rights', [ 'headers' => [ 'Host' => 'localhost', 'Accept' => 'application/json', 'Content-Type' => 'application/merge-patch+json', ], 'json' => ['imageRightsStatus' => 'authorized'], ]); self::assertResponseStatusCodeSame(404); } #[Test] public function exportImageRightsReturns404WithoutTenant(): void { $client = static::createClient(); $client->request('GET', '/api/students/image-rights/export', [ 'headers' => [ 'Host' => 'localhost', 'Accept' => 'application/json', ], ]); self::assertResponseStatusCodeSame(404); } // ========================================================================= // Security - Without authentication // ========================================================================= #[Test] public function getImageRightsReturns401WithoutAuthentication(): void { $client = static::createClient(); $client->request('GET', self::BASE_URL . '/students/image-rights', [ 'headers' => ['Accept' => 'application/json'], ]); self::assertResponseStatusCodeSame(401); } #[Test] public function updateImageRightsReturns401WithoutAuthentication(): void { $client = static::createClient(); $client->request('PATCH', self::BASE_URL . '/students/' . self::STUDENT_1_ID . '/image-rights', [ 'headers' => [ 'Accept' => 'application/json', 'Content-Type' => 'application/merge-patch+json', ], 'json' => ['imageRightsStatus' => 'authorized'], ]); self::assertResponseStatusCodeSame(401); } #[Test] public function exportImageRightsReturns401WithoutAuthentication(): void { $client = static::createClient(); $client->request('GET', self::BASE_URL . '/students/image-rights/export', [ 'headers' => ['Accept' => 'application/json'], ]); self::assertResponseStatusCodeSame(401); } // ========================================================================= // Security - Forbidden roles // ========================================================================= #[Test] public function getImageRightsReturns403ForStudent(): void { $client = $this->createAuthenticatedClient(['ROLE_ELEVE']); $client->request('GET', self::BASE_URL . '/students/image-rights', [ 'headers' => ['Accept' => 'application/json'], ]); self::assertResponseStatusCodeSame(403); } #[Test] public function updateImageRightsReturns403ForStudent(): void { $client = $this->createAuthenticatedClient(['ROLE_ELEVE']); $client->request('PATCH', self::BASE_URL . '/students/' . self::STUDENT_1_ID . '/image-rights', [ 'headers' => [ 'Accept' => 'application/json', 'Content-Type' => 'application/merge-patch+json', ], 'json' => ['imageRightsStatus' => 'authorized'], ]); self::assertResponseStatusCodeSame(403); } #[Test] public function exportImageRightsReturns403ForStudent(): void { $client = $this->createAuthenticatedClient(['ROLE_ELEVE']); $client->request('GET', self::BASE_URL . '/students/image-rights/export', [ 'headers' => ['Accept' => 'application/json'], ]); self::assertResponseStatusCodeSame(403); } #[Test] public function getImageRightsReturns200ForTeacher(): void { $client = $this->createAuthenticatedClient(['ROLE_PROF']); $client->request('GET', self::BASE_URL . '/students/image-rights', [ 'headers' => ['Accept' => 'application/json'], ]); self::assertResponseStatusCodeSame(200); } #[Test] public function getImageRightsReturns403ForParent(): void { $client = $this->createAuthenticatedClient(['ROLE_PARENT']); $client->request('GET', self::BASE_URL . '/students/image-rights', [ 'headers' => ['Accept' => 'application/json'], ]); self::assertResponseStatusCodeSame(403); } // ========================================================================= // AC1 (P1) - GET image rights collection // ========================================================================= #[Test] public function getImageRightsReturnsStudentsForAdmin(): void { $this->persistStudent(self::STUDENT_1_ID, 'Alice', 'Dupont', 'alice-ir@test.classeo.local', ImageRightsStatus::AUTHORIZED); $this->persistStudent(self::STUDENT_2_ID, 'Bob', 'Martin', 'bob-ir@test.classeo.local', ImageRightsStatus::REFUSED); $client = $this->createAuthenticatedClient(['ROLE_ADMIN']); $response = $client->request('GET', self::BASE_URL . '/students/image-rights', [ 'headers' => ['Accept' => 'application/json'], ]); self::assertResponseIsSuccessful(); $data = $response->toArray(); self::assertNotEmpty($data); self::assertArrayHasKey('id', $data[0]); self::assertArrayHasKey('imageRightsStatus', $data[0]); } #[Test] public function getImageRightsReturnsStudentsForSecretariat(): void { $this->persistStudent(self::STUDENT_1_ID, 'Alice', 'Dupont', 'alice-ir@test.classeo.local', ImageRightsStatus::AUTHORIZED); $client = $this->createAuthenticatedClient(['ROLE_SECRETARIAT']); $response = $client->request('GET', self::BASE_URL . '/students/image-rights', [ 'headers' => ['Accept' => 'application/json'], ]); self::assertResponseIsSuccessful(); $data = $response->toArray(); self::assertNotEmpty($data); self::assertArrayHasKey('id', $data[0]); self::assertArrayHasKey('imageRightsStatus', $data[0]); } // ========================================================================= // AC2 (P1) - Filter by status // ========================================================================= #[Test] public function getImageRightsFiltersByStatus(): void { $this->persistStudent(self::STUDENT_1_ID, 'Alice', 'Dupont', 'alice-ir@test.classeo.local', ImageRightsStatus::AUTHORIZED); $this->persistStudent(self::STUDENT_2_ID, 'Bob', 'Martin', 'bob-ir@test.classeo.local', ImageRightsStatus::REFUSED); $client = $this->createAuthenticatedClient(['ROLE_ADMIN']); $response = $client->request('GET', self::BASE_URL . '/students/image-rights?status=authorized', [ 'headers' => ['Accept' => 'application/json'], ]); self::assertResponseIsSuccessful(); $data = $response->toArray(); self::assertNotEmpty($data); foreach ($data as $member) { self::assertSame('authorized', $member['imageRightsStatus']); } } #[Test] public function getImageRightsFiltersByStatusNotSpecified(): void { $this->persistStudent(self::STUDENT_1_ID, 'Alice', 'Dupont', 'alice-ir@test.classeo.local', ImageRightsStatus::NOT_SPECIFIED); $this->persistStudent(self::STUDENT_2_ID, 'Bob', 'Martin', 'bob-ir@test.classeo.local', ImageRightsStatus::AUTHORIZED); $client = $this->createAuthenticatedClient(['ROLE_ADMIN']); $response = $client->request('GET', self::BASE_URL . '/students/image-rights?status=not_specified', [ 'headers' => ['Accept' => 'application/json'], ]); self::assertResponseIsSuccessful(); $data = $response->toArray(); self::assertNotEmpty($data); foreach ($data as $member) { self::assertSame('not_specified', $member['imageRightsStatus']); } } // ========================================================================= // AC3 (P0) - PATCH update image rights // ========================================================================= #[Test] public function updateImageRightsReturns200ForAdmin(): void { $this->persistAdmin(); $this->persistStudent(self::STUDENT_1_ID, 'Alice', 'Dupont', 'alice-ir@test.classeo.local', ImageRightsStatus::NOT_SPECIFIED); $client = $this->createAuthenticatedClient(['ROLE_ADMIN']); $response = $client->request('PATCH', self::BASE_URL . '/students/' . self::STUDENT_1_ID . '/image-rights', [ 'headers' => [ 'Accept' => 'application/json', 'Content-Type' => 'application/merge-patch+json', ], 'json' => ['imageRightsStatus' => 'authorized'], ]); self::assertResponseIsSuccessful(); $data = $response->toArray(); self::assertSame('authorized', $data['imageRightsStatus']); self::assertSame(self::STUDENT_1_ID, $data['id']); } #[Test] public function updateImageRightsReturns404ForUnknownStudent(): void { $client = $this->createAuthenticatedClient(['ROLE_ADMIN']); $client->request('PATCH', self::BASE_URL . '/students/00000000-0000-0000-0000-000000000000/image-rights', [ 'headers' => [ 'Accept' => 'application/json', 'Content-Type' => 'application/merge-patch+json', ], 'json' => ['imageRightsStatus' => 'authorized'], ]); self::assertResponseStatusCodeSame(404); } #[Test] public function updateImageRightsReturns422ForInvalidStatus(): void { $this->persistStudent(self::STUDENT_1_ID, 'Alice', 'Dupont', 'alice-ir@test.classeo.local', ImageRightsStatus::NOT_SPECIFIED); $client = $this->createAuthenticatedClient(['ROLE_ADMIN']); $client->request('PATCH', self::BASE_URL . '/students/' . self::STUDENT_1_ID . '/image-rights', [ 'headers' => [ 'Accept' => 'application/json', 'Content-Type' => 'application/merge-patch+json', ], 'json' => ['imageRightsStatus' => 'invalid_status'], ]); self::assertResponseStatusCodeSame(422); } // ========================================================================= // AC4 (P1) - Export CSV // ========================================================================= #[Test] public function exportImageRightsReturnsCsvForAdmin(): void { $this->persistStudent(self::STUDENT_1_ID, 'Alice', 'Dupont', 'alice-ir@test.classeo.local', ImageRightsStatus::AUTHORIZED); $client = $this->createAuthenticatedClient(['ROLE_ADMIN']); $response = $client->request('GET', self::BASE_URL . '/students/image-rights/export', [ 'headers' => ['Accept' => 'application/json'], ]); self::assertResponseIsSuccessful(); self::assertResponseHeaderSame('content-type', 'text/csv; charset=UTF-8'); self::assertResponseHeaderSame('content-disposition', 'attachment; filename="droits-image.csv"'); $content = $response->getContent(); self::assertStringContainsString('Nom', $content); self::assertStringContainsString('Dupont', $content); } // ========================================================================= // Helpers // ========================================================================= private function createAuthenticatedClient(array $roles): \ApiPlatform\Symfony\Bundle\Test\Client { $client = static::createClient(); $user = new SecurityUser( userId: UserId::fromString(self::USER_ID), email: 'admin@classeo.local', hashedPassword: '', tenantId: TenantId::fromString(self::TENANT_ID), roles: $roles, ); $client->loginUser($user, 'api'); return $client; } private function persistAdmin(): void { $admin = User::reconstitute( id: UserId::fromString(self::USER_ID), email: new Email('admin@classeo.local'), roles: [Role::ADMIN], tenantId: TenantId::fromString(self::TENANT_ID), schoolName: 'École Alpha', statut: StatutCompte::ACTIF, dateNaissance: null, createdAt: new DateTimeImmutable('2026-01-01'), hashedPassword: 'dummy', activatedAt: new DateTimeImmutable('2026-01-01'), consentementParental: null, ); /** @var UserRepository $repository */ $repository = static::getContainer()->get(UserRepository::class); $repository->save($admin); } private function persistStudent( string $id, string $firstName, string $lastName, string $email, ImageRightsStatus $imageRightsStatus, ): void { $user = User::reconstitute( id: UserId::fromString($id), email: new Email($email), roles: [Role::ELEVE], tenantId: TenantId::fromString(self::TENANT_ID), schoolName: 'École Alpha', statut: StatutCompte::EN_ATTENTE, dateNaissance: new DateTimeImmutable('2012-06-15'), createdAt: new DateTimeImmutable('2026-01-15'), hashedPassword: null, activatedAt: null, consentementParental: null, firstName: $firstName, lastName: $lastName, imageRightsStatus: $imageRightsStatus, ); /** @var UserRepository $repository */ $repository = static::getContainer()->get(UserRepository::class); $repository->save($user); } }