Les établissements utilisant le mode "Hard" des règles de devoirs empêchent désormais les enseignants de créer des devoirs hors règles. Contrairement au mode "Soft" (avertissement avec possibilité de passer outre), le mode "Hard" est un blocage strict : même acknowledgeWarning ne permet pas de contourner. L'API retourne 422 (au lieu de 409 pour le soft) avec des dates conformes suggérées calculées via le calendrier scolaire (weekends, fériés, vacances exclus). Le frontend affiche un modal de blocage avec les raisons, des dates cliquables, et une validation client inline qui empêche la soumission de dates non conformes.
817 lines
31 KiB
PHP
817 lines
31 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,
|
|
]);
|
|
}
|
|
|
|
// =========================================================================
|
|
// 5.5-FUNC-001 (P0) - POST /homework with hard rules violated → 422 with blocked response
|
|
// =========================================================================
|
|
|
|
#[Test]
|
|
public function createHomeworkReturns422WhenHardRulesViolated(): void
|
|
{
|
|
$this->persistHardRulesWithMinimumDelay(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 test 422',
|
|
'dueDate' => $tomorrow,
|
|
],
|
|
]);
|
|
|
|
self::assertResponseStatusCodeSame(422);
|
|
$json = $client->getResponse()->toArray(false);
|
|
self::assertSame('homework_rules_blocked', $json['type']);
|
|
self::assertNotEmpty($json['warnings']);
|
|
self::assertSame('minimum_delay', $json['warnings'][0]['ruleType']);
|
|
self::assertArrayHasKey('suggestedDates', $json);
|
|
self::assertArrayHasKey('exceptionRequestPath', $json);
|
|
}
|
|
|
|
// =========================================================================
|
|
// 5.5-FUNC-002 (P0) - POST /homework with hard rules violated + acknowledgeWarning → still 422
|
|
// =========================================================================
|
|
|
|
#[Test]
|
|
public function createHomeworkReturns422EvenWhenHardRulesAcknowledged(): void
|
|
{
|
|
$this->persistHardRulesWithMinimumDelay(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 bypass hard',
|
|
'dueDate' => $tomorrow,
|
|
'acknowledgeWarning' => true,
|
|
],
|
|
]);
|
|
|
|
self::assertResponseStatusCodeSame(422);
|
|
$json = $client->getResponse()->toArray(false);
|
|
self::assertSame('homework_rules_blocked', $json['type']);
|
|
}
|
|
|
|
// =========================================================================
|
|
// 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 persistHardRulesWithMinimumDelay(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, 'hard', true, NOW(), NOW())
|
|
ON CONFLICT (tenant_id) DO UPDATE SET rules = :rules::jsonb, enforcement_mode = 'hard', 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);
|
|
}
|
|
}
|