Quand un établissement configure des règles de devoirs en mode "soft", l'enseignant est maintenant averti avant la création si la date d'échéance ne respecte pas les contraintes (délai minimum, pas de lundi après un certain créneau). Il peut alors choisir de continuer (avec traçabilité) ou de modifier la date vers une date conforme. Le mode "hard" (blocage) reste protégé : acknowledgeWarning ne permet pas de contourner les règles bloquantes, préparant la story 5.5.
735 lines
28 KiB
PHP
735 lines
28 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 const JSON_THROW_ON_ERROR;
|
|
|
|
use PHPUnit\Framework\Attributes\Test;
|
|
use Ramsey\Uuid\Uuid;
|
|
|
|
/**
|
|
* 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';
|
|
private const string TARGET_CLASS_ID = '550e8400-e29b-41d4-a716-776655440002';
|
|
|
|
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 homework_rules 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);
|
|
}
|
|
|
|
// =========================================================================
|
|
// 5.4-FUNC-001 (P2) - GET /homework/{id} with rule override -> hasRuleOverride = true
|
|
// =========================================================================
|
|
|
|
#[Test]
|
|
public function getHomeworkShowsHasRuleOverrideTrue(): void
|
|
{
|
|
$this->persistHomeworkWithRuleOverride(self::HOMEWORK_ID);
|
|
|
|
$client = $this->createAuthenticatedClient(self::OWNER_TEACHER_ID, ['ROLE_PROF']);
|
|
|
|
$client->request('GET', 'http://ecole-alpha.classeo.local/api/homework/' . self::HOMEWORK_ID, [
|
|
'headers' => [
|
|
'Accept' => 'application/json',
|
|
],
|
|
]);
|
|
|
|
self::assertResponseStatusCodeSame(200);
|
|
self::assertJsonContains(['hasRuleOverride' => true]);
|
|
}
|
|
|
|
// =========================================================================
|
|
// 5.4-FUNC-002 (P2) - GET /homework/{id} without rule override -> hasRuleOverride = false
|
|
// =========================================================================
|
|
|
|
#[Test]
|
|
public function getHomeworkShowsHasRuleOverrideFalse(): void
|
|
{
|
|
$this->persistHomework(self::HOMEWORK_ID, HomeworkStatus::PUBLISHED);
|
|
|
|
$client = $this->createAuthenticatedClient(self::OWNER_TEACHER_ID, ['ROLE_PROF']);
|
|
|
|
$client->request('GET', 'http://ecole-alpha.classeo.local/api/homework/' . self::HOMEWORK_ID, [
|
|
'headers' => [
|
|
'Accept' => 'application/json',
|
|
],
|
|
]);
|
|
|
|
self::assertResponseStatusCodeSame(200);
|
|
self::assertJsonContains(['hasRuleOverride' => false]);
|
|
}
|
|
|
|
// =========================================================================
|
|
// 5.4-FUNC-003 (P1) - POST /homework with soft rules violated → 409 with warnings
|
|
// =========================================================================
|
|
|
|
#[Test]
|
|
public function createHomeworkReturns409WhenSoftRulesViolated(): void
|
|
{
|
|
$this->persistSoftRulesWithMinimumDelay(7);
|
|
$this->seedTeacherAssignment();
|
|
|
|
$client = $this->createAuthenticatedClient(self::OWNER_TEACHER_ID, ['ROLE_PROF']);
|
|
|
|
// Due date tomorrow = violates 7-day minimum_delay
|
|
$tomorrow = (new DateTimeImmutable('+1 weekday'))->format('Y-m-d');
|
|
|
|
$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 test 409',
|
|
'dueDate' => $tomorrow,
|
|
],
|
|
]);
|
|
|
|
self::assertResponseStatusCodeSame(409);
|
|
$json = $client->getResponse()->toArray(false);
|
|
self::assertSame('homework_rules_warning', $json['type']);
|
|
self::assertNotEmpty($json['warnings']);
|
|
self::assertSame('minimum_delay', $json['warnings'][0]['ruleType']);
|
|
}
|
|
|
|
// =========================================================================
|
|
// 5.4-FUNC-004 (P1) - POST /homework with acknowledgeWarning → 201 created
|
|
// =========================================================================
|
|
|
|
#[Test]
|
|
public function createHomeworkReturns201WhenSoftRulesAcknowledged(): void
|
|
{
|
|
$this->persistSoftRulesWithMinimumDelay(7);
|
|
$this->seedTeacherAssignment();
|
|
|
|
$client = $this->createAuthenticatedClient(self::OWNER_TEACHER_ID, ['ROLE_PROF']);
|
|
|
|
$tomorrow = (new DateTimeImmutable('+1 weekday'))->format('Y-m-d');
|
|
|
|
$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 acknowledge',
|
|
'dueDate' => $tomorrow,
|
|
'acknowledgeWarning' => true,
|
|
],
|
|
]);
|
|
|
|
self::assertResponseStatusCodeSame(201);
|
|
self::assertJsonContains([
|
|
'title' => 'Devoir acknowledge',
|
|
'hasRuleOverride' => true,
|
|
]);
|
|
}
|
|
|
|
// =========================================================================
|
|
// 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 persistSoftRulesWithMinimumDelay(int $days): void
|
|
{
|
|
/** @var Connection $connection */
|
|
$connection = static::getContainer()->get(Connection::class);
|
|
$rulesJson = json_encode([['type' => 'minimum_delay', 'params' => ['days' => $days]]], JSON_THROW_ON_ERROR);
|
|
$connection->executeStatement(
|
|
"INSERT INTO homework_rules (id, tenant_id, rules, enforcement_mode, enabled, created_at, updated_at)
|
|
VALUES (gen_random_uuid(), :tid, :rules::jsonb, 'soft', true, NOW(), NOW())
|
|
ON CONFLICT (tenant_id) DO UPDATE SET rules = :rules::jsonb, enforcement_mode = 'soft', enabled = true, updated_at = NOW()",
|
|
['tid' => self::TENANT_ID, 'rules' => $rulesJson],
|
|
);
|
|
}
|
|
|
|
private function seedTeacherAssignment(): void
|
|
{
|
|
/** @var Connection $connection */
|
|
$connection = static::getContainer()->get(Connection::class);
|
|
|
|
// Compute the current academic year UUID the same way CurrentAcademicYearResolver does
|
|
$month = (int) date('n');
|
|
$year = (int) date('Y');
|
|
$startYear = $month >= 9 ? $year : $year - 1;
|
|
$academicYearId = Uuid::uuid5(
|
|
'6ba7b814-9dad-11d1-80b4-00c04fd430c8',
|
|
self::TENANT_ID . ':' . $startYear . '-' . ($startYear + 1),
|
|
)->toString();
|
|
|
|
$connection->executeStatement(
|
|
"INSERT INTO teacher_assignments (id, tenant_id, teacher_id, school_class_id, subject_id, academic_year_id, status, start_date, created_at, updated_at)
|
|
VALUES (gen_random_uuid(), :tid, :teacher, :class, :subject, :ayid, 'active', NOW(), NOW(), NOW())
|
|
ON CONFLICT DO NOTHING",
|
|
[
|
|
'tid' => self::TENANT_ID,
|
|
'teacher' => self::OWNER_TEACHER_ID,
|
|
'class' => self::CLASS_ID,
|
|
'subject' => self::SUBJECT_ID,
|
|
'ayid' => $academicYearId,
|
|
],
|
|
);
|
|
}
|
|
|
|
private function persistHomeworkWithRuleOverride(string $homeworkId): 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: HomeworkStatus::PUBLISHED,
|
|
createdAt: $now,
|
|
updatedAt: $now,
|
|
ruleOverride: ['warnings' => ['minimum_delay'], 'acknowledgedAt' => '2026-03-18T10:00:00+00:00'],
|
|
);
|
|
|
|
/** @var HomeworkRepository $repository */
|
|
$repository = static::getContainer()->get(HomeworkRepository::class);
|
|
$repository->save($homework);
|
|
}
|
|
|
|
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);
|
|
}
|
|
}
|