Files
Classeo/backend/tests/Functional/Scolarite/Api/HomeworkEndpointsTest.php
Mathias STRASSER e9efb90f59
Some checks failed
CI / Backend Tests (push) Has been cancelled
CI / Frontend Tests (push) Has been cancelled
CI / E2E Tests (push) Has been cancelled
CI / Naming Conventions (push) Has been cancelled
CI / Build Check (push) Has been cancelled
feat: Permettre aux enseignants de créer et gérer les devoirs
Les enseignants avaient besoin d'un outil pour créer des devoirs assignés
à leurs classes, avec filtrage automatique par matière selon la classe
sélectionnée. Le système valide que la date d'échéance tombe un jour
ouvrable (lundi-vendredi) et empêche les dates dans le passé.

Le domaine modélise le devoir comme un agrégat avec pièces jointes,
statut brouillon/publié, et événements métier (création, modification,
suppression). Les handlers de notification écoutent ces événements pour
les futurs envois aux parents et élèves.
2026-03-14 00:33:49 +01:00

429 lines
15 KiB
PHP

<?php
declare(strict_types=1);
namespace App\Tests\Functional\Scolarite\Api;
use ApiPlatform\Symfony\Bundle\Test\ApiTestCase;
use App\Administration\Domain\Model\SchoolClass\ClassId;
use App\Administration\Domain\Model\Subject\SubjectId;
use App\Administration\Domain\Model\User\UserId;
use App\Administration\Infrastructure\Security\SecurityUser;
use App\Scolarite\Domain\Model\Homework\Homework;
use App\Scolarite\Domain\Model\Homework\HomeworkId;
use App\Scolarite\Domain\Model\Homework\HomeworkStatus;
use App\Scolarite\Domain\Repository\HomeworkRepository;
use App\Shared\Domain\Tenant\TenantId;
use DateTimeImmutable;
use Doctrine\DBAL\Connection;
use PHPUnit\Framework\Attributes\Test;
/**
* Tests for homework API endpoints.
*
* @see Story 5.1 - Création de Devoirs
*/
final class HomeworkEndpointsTest extends ApiTestCase
{
/**
* Opt-in for API Platform 5.0 behavior where kernel boot is explicit.
*
* @see https://github.com/api-platform/core/issues/6971
*/
protected static ?bool $alwaysBootKernel = true;
private const string TENANT_ID = 'a1b2c3d4-e5f6-7890-abcd-ef1234567890';
private const string OWNER_TEACHER_ID = '550e8400-e29b-41d4-a716-446655440000';
private const string OTHER_TEACHER_ID = '660e8400-e29b-41d4-a716-446655440001';
private const string CLASS_ID = '550e8400-e29b-41d4-a716-776655440001';
private const string SUBJECT_ID = '550e8400-e29b-41d4-a716-886655440001';
private const string HOMEWORK_ID = '550e8400-e29b-41d4-a716-996655440001';
private const string DELETED_HOMEWORK_ID = '550e8400-e29b-41d4-a716-aa6655440001';
protected function setUp(): void
{
parent::setUp();
$this->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 = :id', ['id' => self::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],
);
}
// =========================================================================
// 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);
}
// =========================================================================
// 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);
}
}