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.
429 lines
15 KiB
PHP
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);
|
|
}
|
|
}
|