feat: Permettre aux enseignants de dupliquer un devoir vers plusieurs classes
Un enseignant qui donne le même travail à plusieurs classes devait jusqu'ici recréer manuellement chaque devoir. La duplication permet de sélectionner les classes cibles, d'ajuster les dates d'échéance par classe, et de créer tous les devoirs en une seule opération atomique (transaction). La validation s'effectue par classe (affectation enseignant, date d'échéance) avec un rapport d'erreurs détaillé. L'infrastructure de warnings est prête pour les règles de timing de la Story 5.3. Le filtrage par classe dans la liste des devoirs passe côté serveur pour rester compatible avec la pagination.
This commit is contained in:
@@ -39,6 +39,7 @@ final class HomeworkEndpointsTest extends ApiTestCase
|
||||
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
|
||||
{
|
||||
@@ -52,7 +53,7 @@ final class HomeworkEndpointsTest extends ApiTestCase
|
||||
/** @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 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]);
|
||||
|
||||
@@ -92,6 +93,12 @@ final class HomeworkEndpointsTest extends ApiTestCase
|
||||
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],
|
||||
);
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
@@ -382,6 +389,123 @@ final class HomeworkEndpointsTest extends ApiTestCase
|
||||
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);
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// Helpers
|
||||
// =========================================================================
|
||||
|
||||
@@ -0,0 +1,348 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Tests\Unit\Scolarite\Application\Command\DuplicateHomework;
|
||||
|
||||
use App\Administration\Domain\Model\SchoolCalendar\SchoolCalendar;
|
||||
use App\Administration\Domain\Model\SchoolClass\AcademicYearId;
|
||||
use App\Administration\Domain\Model\SchoolClass\ClassId;
|
||||
use App\Administration\Domain\Model\Subject\SubjectId;
|
||||
use App\Administration\Domain\Model\User\UserId;
|
||||
use App\Scolarite\Application\Command\DuplicateHomework\DuplicateHomeworkCommand;
|
||||
use App\Scolarite\Application\Command\DuplicateHomework\DuplicateHomeworkHandler;
|
||||
use App\Scolarite\Application\Port\CurrentCalendarProvider;
|
||||
use App\Scolarite\Application\Port\EnseignantAffectationChecker;
|
||||
use App\Scolarite\Domain\Exception\DuplicationValidationException;
|
||||
use App\Scolarite\Domain\Exception\HomeworkNotFoundException;
|
||||
use App\Scolarite\Domain\Exception\NonProprietaireDuDevoirException;
|
||||
use App\Scolarite\Domain\Model\Homework\Homework;
|
||||
use App\Scolarite\Domain\Model\Homework\HomeworkAttachment;
|
||||
use App\Scolarite\Domain\Model\Homework\HomeworkAttachmentId;
|
||||
use App\Scolarite\Domain\Model\Homework\HomeworkStatus;
|
||||
use App\Scolarite\Domain\Service\DueDateValidator;
|
||||
use App\Scolarite\Domain\Service\HomeworkDuplicator;
|
||||
use App\Scolarite\Infrastructure\Persistence\InMemory\InMemoryHomeworkAttachmentRepository;
|
||||
use App\Scolarite\Infrastructure\Persistence\InMemory\InMemoryHomeworkRepository;
|
||||
use App\Shared\Domain\Clock;
|
||||
use App\Shared\Domain\Tenant\TenantId;
|
||||
use DateTimeImmutable;
|
||||
use Doctrine\DBAL\Connection;
|
||||
use PHPUnit\Framework\Attributes\Test;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
final class DuplicateHomeworkHandlerTest extends TestCase
|
||||
{
|
||||
private const string TENANT_ID = '550e8400-e29b-41d4-a716-446655440001';
|
||||
private const string CLASS_ID = '550e8400-e29b-41d4-a716-446655440020';
|
||||
private const string SUBJECT_ID = '550e8400-e29b-41d4-a716-446655440030';
|
||||
private const string TEACHER_ID = '550e8400-e29b-41d4-a716-446655440010';
|
||||
private const string OTHER_TEACHER_ID = '550e8400-e29b-41d4-a716-446655440099';
|
||||
private const string TARGET_CLASS_1 = '550e8400-e29b-41d4-a716-446655440021';
|
||||
private const string TARGET_CLASS_2 = '550e8400-e29b-41d4-a716-446655440022';
|
||||
|
||||
private InMemoryHomeworkRepository $homeworkRepository;
|
||||
private InMemoryHomeworkAttachmentRepository $attachmentRepository;
|
||||
private Clock $clock;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
$this->homeworkRepository = new InMemoryHomeworkRepository();
|
||||
$this->attachmentRepository = new InMemoryHomeworkAttachmentRepository();
|
||||
$this->clock = new class implements Clock {
|
||||
public function now(): DateTimeImmutable
|
||||
{
|
||||
return new DateTimeImmutable('2026-03-12 10:00:00');
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function itDuplicatesHomeworkForOneClass(): void
|
||||
{
|
||||
$source = $this->createAndSaveHomework();
|
||||
$handler = $this->createHandler(affecte: true);
|
||||
|
||||
$command = new DuplicateHomeworkCommand(
|
||||
tenantId: self::TENANT_ID,
|
||||
homeworkId: (string) $source->id,
|
||||
teacherId: self::TEACHER_ID,
|
||||
targetClassIds: [self::TARGET_CLASS_1],
|
||||
);
|
||||
|
||||
$result = $handler($command);
|
||||
|
||||
self::assertCount(1, $result->homeworks);
|
||||
self::assertSame($source->title, $result->homeworks[0]->title);
|
||||
self::assertSame($source->description, $result->homeworks[0]->description);
|
||||
self::assertTrue($result->homeworks[0]->classId->equals(ClassId::fromString(self::TARGET_CLASS_1)));
|
||||
self::assertSame(HomeworkStatus::PUBLISHED, $result->homeworks[0]->status);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function itDuplicatesHomeworkForMultipleClasses(): void
|
||||
{
|
||||
$source = $this->createAndSaveHomework();
|
||||
$handler = $this->createHandler(affecte: true);
|
||||
|
||||
$command = new DuplicateHomeworkCommand(
|
||||
tenantId: self::TENANT_ID,
|
||||
homeworkId: (string) $source->id,
|
||||
teacherId: self::TEACHER_ID,
|
||||
targetClassIds: [self::TARGET_CLASS_1, self::TARGET_CLASS_2],
|
||||
);
|
||||
|
||||
$result = $handler($command);
|
||||
|
||||
self::assertCount(2, $result->homeworks);
|
||||
self::assertTrue($result->homeworks[0]->classId->equals(ClassId::fromString(self::TARGET_CLASS_1)));
|
||||
self::assertTrue($result->homeworks[1]->classId->equals(ClassId::fromString(self::TARGET_CLASS_2)));
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function itPersistsDuplicatesInRepository(): void
|
||||
{
|
||||
$source = $this->createAndSaveHomework();
|
||||
$handler = $this->createHandler(affecte: true);
|
||||
|
||||
$command = new DuplicateHomeworkCommand(
|
||||
tenantId: self::TENANT_ID,
|
||||
homeworkId: (string) $source->id,
|
||||
teacherId: self::TEACHER_ID,
|
||||
targetClassIds: [self::TARGET_CLASS_1],
|
||||
);
|
||||
|
||||
$result = $handler($command);
|
||||
|
||||
$found = $this->homeworkRepository->get(
|
||||
$result->homeworks[0]->id,
|
||||
TenantId::fromString(self::TENANT_ID),
|
||||
);
|
||||
|
||||
self::assertSame($source->title, $found->title);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function itAllowsCustomDueDatePerClass(): void
|
||||
{
|
||||
$source = $this->createAndSaveHomework();
|
||||
$handler = $this->createHandler(affecte: true);
|
||||
|
||||
$command = new DuplicateHomeworkCommand(
|
||||
tenantId: self::TENANT_ID,
|
||||
homeworkId: (string) $source->id,
|
||||
teacherId: self::TEACHER_ID,
|
||||
targetClassIds: [self::TARGET_CLASS_1, self::TARGET_CLASS_2],
|
||||
dueDates: [
|
||||
self::TARGET_CLASS_1 => '2026-04-20',
|
||||
self::TARGET_CLASS_2 => '2026-04-21',
|
||||
],
|
||||
);
|
||||
|
||||
$result = $handler($command);
|
||||
|
||||
self::assertSame('2026-04-20', $result->homeworks[0]->dueDate->format('Y-m-d'));
|
||||
self::assertSame('2026-04-21', $result->homeworks[1]->dueDate->format('Y-m-d'));
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function itUsesSourceDueDateWhenNotCustomized(): void
|
||||
{
|
||||
$source = $this->createAndSaveHomework();
|
||||
$handler = $this->createHandler(affecte: true);
|
||||
|
||||
$command = new DuplicateHomeworkCommand(
|
||||
tenantId: self::TENANT_ID,
|
||||
homeworkId: (string) $source->id,
|
||||
teacherId: self::TEACHER_ID,
|
||||
targetClassIds: [self::TARGET_CLASS_1],
|
||||
);
|
||||
|
||||
$result = $handler($command);
|
||||
|
||||
self::assertSame($source->dueDate->format('Y-m-d'), $result->homeworks[0]->dueDate->format('Y-m-d'));
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function itThrowsWhenHomeworkNotFound(): void
|
||||
{
|
||||
$handler = $this->createHandler(affecte: true);
|
||||
|
||||
$this->expectException(HomeworkNotFoundException::class);
|
||||
|
||||
$handler(new DuplicateHomeworkCommand(
|
||||
tenantId: self::TENANT_ID,
|
||||
homeworkId: '550e8400-e29b-41d4-a716-446655440099',
|
||||
teacherId: self::TEACHER_ID,
|
||||
targetClassIds: [self::TARGET_CLASS_1],
|
||||
));
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function itThrowsWhenTeacherIsNotOwner(): void
|
||||
{
|
||||
$source = $this->createAndSaveHomework();
|
||||
$handler = $this->createHandler(affecte: true);
|
||||
|
||||
$this->expectException(NonProprietaireDuDevoirException::class);
|
||||
|
||||
$handler(new DuplicateHomeworkCommand(
|
||||
tenantId: self::TENANT_ID,
|
||||
homeworkId: (string) $source->id,
|
||||
teacherId: self::OTHER_TEACHER_ID,
|
||||
targetClassIds: [self::TARGET_CLASS_1],
|
||||
));
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function itThrowsWhenTeacherNotAffectedToTargetClass(): void
|
||||
{
|
||||
$source = $this->createAndSaveHomework();
|
||||
$handler = $this->createHandler(affecte: false);
|
||||
|
||||
$this->expectException(DuplicationValidationException::class);
|
||||
|
||||
$handler(new DuplicateHomeworkCommand(
|
||||
tenantId: self::TENANT_ID,
|
||||
homeworkId: (string) $source->id,
|
||||
teacherId: self::TEACHER_ID,
|
||||
targetClassIds: [self::TARGET_CLASS_1],
|
||||
));
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function itThrowsWithPerClassValidationResults(): void
|
||||
{
|
||||
$source = $this->createAndSaveHomework();
|
||||
$handler = $this->createHandler(affecte: false);
|
||||
|
||||
try {
|
||||
$handler(new DuplicateHomeworkCommand(
|
||||
tenantId: self::TENANT_ID,
|
||||
homeworkId: (string) $source->id,
|
||||
teacherId: self::TEACHER_ID,
|
||||
targetClassIds: [self::TARGET_CLASS_1, self::TARGET_CLASS_2],
|
||||
));
|
||||
self::fail('Expected DuplicationValidationException');
|
||||
} catch (DuplicationValidationException $e) {
|
||||
self::assertCount(2, $e->results);
|
||||
self::assertFalse($e->results[0]->valid);
|
||||
self::assertFalse($e->results[1]->valid);
|
||||
}
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function itDuplicatesAttachmentsForEachCopy(): void
|
||||
{
|
||||
$source = $this->createAndSaveHomework();
|
||||
$handler = $this->createHandler(affecte: true);
|
||||
|
||||
$attachment = new HomeworkAttachment(
|
||||
id: HomeworkAttachmentId::generate(),
|
||||
filename: 'exercice.pdf',
|
||||
filePath: 'homework/tenant/source/exercice.pdf',
|
||||
fileSize: 1024,
|
||||
mimeType: 'application/pdf',
|
||||
uploadedAt: new DateTimeImmutable('2026-03-12'),
|
||||
);
|
||||
$this->attachmentRepository->save($source->id, $attachment);
|
||||
|
||||
$command = new DuplicateHomeworkCommand(
|
||||
tenantId: self::TENANT_ID,
|
||||
homeworkId: (string) $source->id,
|
||||
teacherId: self::TEACHER_ID,
|
||||
targetClassIds: [self::TARGET_CLASS_1, self::TARGET_CLASS_2],
|
||||
);
|
||||
|
||||
$result = $handler($command);
|
||||
|
||||
foreach ($result->homeworks as $duplicate) {
|
||||
$attachments = $this->attachmentRepository->findByHomeworkId($duplicate->id);
|
||||
self::assertCount(1, $attachments);
|
||||
self::assertSame('exercice.pdf', $attachments[0]->filename);
|
||||
self::assertSame('homework/tenant/source/exercice.pdf', $attachments[0]->filePath);
|
||||
self::assertFalse($attachments[0]->id->equals($attachment->id));
|
||||
}
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function itCreatesIndependentDuplicates(): void
|
||||
{
|
||||
$source = $this->createAndSaveHomework();
|
||||
$handler = $this->createHandler(affecte: true);
|
||||
|
||||
$command = new DuplicateHomeworkCommand(
|
||||
tenantId: self::TENANT_ID,
|
||||
homeworkId: (string) $source->id,
|
||||
teacherId: self::TEACHER_ID,
|
||||
targetClassIds: [self::TARGET_CLASS_1, self::TARGET_CLASS_2],
|
||||
);
|
||||
|
||||
$result = $handler($command);
|
||||
|
||||
self::assertFalse($result->homeworks[0]->id->equals($source->id));
|
||||
self::assertFalse($result->homeworks[1]->id->equals($source->id));
|
||||
self::assertFalse($result->homeworks[0]->id->equals($result->homeworks[1]->id));
|
||||
}
|
||||
|
||||
private function createAndSaveHomework(): Homework
|
||||
{
|
||||
$homework = Homework::creer(
|
||||
tenantId: TenantId::fromString(self::TENANT_ID),
|
||||
classId: ClassId::fromString(self::CLASS_ID),
|
||||
subjectId: SubjectId::fromString(self::SUBJECT_ID),
|
||||
teacherId: UserId::fromString(self::TEACHER_ID),
|
||||
title: 'Exercices chapitre 5',
|
||||
description: 'Faire les exercices 1 à 10',
|
||||
dueDate: new DateTimeImmutable('2026-04-15'),
|
||||
now: new DateTimeImmutable('2026-03-10 10:00:00'),
|
||||
);
|
||||
|
||||
$homework->pullDomainEvents();
|
||||
$this->homeworkRepository->save($homework);
|
||||
|
||||
return $homework;
|
||||
}
|
||||
|
||||
private function createHandler(bool $affecte): DuplicateHomeworkHandler
|
||||
{
|
||||
$affectationChecker = new class($affecte) implements EnseignantAffectationChecker {
|
||||
public function __construct(private readonly bool $affecte)
|
||||
{
|
||||
}
|
||||
|
||||
public function estAffecte(UserId $teacherId, ClassId $classId, SubjectId $subjectId, TenantId $tenantId): bool
|
||||
{
|
||||
return $this->affecte;
|
||||
}
|
||||
};
|
||||
|
||||
$calendarProvider = new class implements CurrentCalendarProvider {
|
||||
public function forCurrentYear(TenantId $tenantId): SchoolCalendar
|
||||
{
|
||||
return SchoolCalendar::reconstitute(
|
||||
tenantId: $tenantId,
|
||||
academicYearId: AcademicYearId::fromString('550e8400-e29b-41d4-a716-446655440002'),
|
||||
zone: null,
|
||||
entries: [],
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
$connection = $this->createMock(Connection::class);
|
||||
$connection->method('beginTransaction');
|
||||
$connection->method('commit');
|
||||
$connection->method('rollBack');
|
||||
|
||||
return new DuplicateHomeworkHandler(
|
||||
$this->homeworkRepository,
|
||||
$this->attachmentRepository,
|
||||
$affectationChecker,
|
||||
$calendarProvider,
|
||||
new DueDateValidator(),
|
||||
new HomeworkDuplicator(),
|
||||
$this->clock,
|
||||
$connection,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -121,6 +121,57 @@ final class UpdateHomeworkHandlerTest extends TestCase
|
||||
));
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function itUpdatesTitleOnly(): void
|
||||
{
|
||||
$handler = $this->createHandler();
|
||||
$command = new UpdateHomeworkCommand(
|
||||
tenantId: self::TENANT_ID,
|
||||
homeworkId: (string) $this->existingHomeworkId,
|
||||
teacherId: '550e8400-e29b-41d4-a716-446655440010',
|
||||
title: 'Nouveau titre',
|
||||
description: null,
|
||||
dueDate: '2026-04-15',
|
||||
);
|
||||
|
||||
$homework = $handler($command);
|
||||
|
||||
self::assertSame('Nouveau titre', $homework->title);
|
||||
self::assertNull($homework->description);
|
||||
self::assertSame('2026-04-15', $homework->dueDate->format('Y-m-d'));
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function itUpdatesDescriptionFromNullToText(): void
|
||||
{
|
||||
$homeworkWithoutDescription = Homework::creer(
|
||||
tenantId: TenantId::fromString(self::TENANT_ID),
|
||||
classId: ClassId::fromString('550e8400-e29b-41d4-a716-446655440020'),
|
||||
subjectId: SubjectId::fromString('550e8400-e29b-41d4-a716-446655440030'),
|
||||
teacherId: UserId::fromString('550e8400-e29b-41d4-a716-446655440010'),
|
||||
title: 'Exercices sans description',
|
||||
description: null,
|
||||
dueDate: new DateTimeImmutable('2026-04-15'),
|
||||
now: new DateTimeImmutable('2026-03-10 10:00:00'),
|
||||
);
|
||||
$this->homeworkRepository->save($homeworkWithoutDescription);
|
||||
|
||||
$handler = $this->createHandler();
|
||||
$command = new UpdateHomeworkCommand(
|
||||
tenantId: self::TENANT_ID,
|
||||
homeworkId: (string) $homeworkWithoutDescription->id,
|
||||
teacherId: '550e8400-e29b-41d4-a716-446655440010',
|
||||
title: 'Exercices sans description',
|
||||
description: 'Nouvelle description ajoutée',
|
||||
dueDate: '2026-04-15',
|
||||
);
|
||||
|
||||
$homework = $handler($command);
|
||||
|
||||
self::assertSame('Nouvelle description ajoutée', $homework->description);
|
||||
self::assertSame('Exercices sans description', $homework->title);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function itThrowsWhenNotOwner(): void
|
||||
{
|
||||
|
||||
@@ -0,0 +1,157 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Tests\Unit\Scolarite\Domain\Service;
|
||||
|
||||
use App\Administration\Domain\Model\SchoolClass\ClassId;
|
||||
use App\Administration\Domain\Model\Subject\SubjectId;
|
||||
use App\Administration\Domain\Model\User\UserId;
|
||||
use App\Scolarite\Domain\Model\Homework\Homework;
|
||||
use App\Scolarite\Domain\Service\HomeworkDuplicator;
|
||||
use App\Shared\Domain\Tenant\TenantId;
|
||||
use DateTimeImmutable;
|
||||
use PHPUnit\Framework\Attributes\Test;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
final class HomeworkDuplicatorTest extends TestCase
|
||||
{
|
||||
private const string TENANT_ID = '550e8400-e29b-41d4-a716-446655440001';
|
||||
private const string CLASS_ID = '550e8400-e29b-41d4-a716-446655440020';
|
||||
private const string SUBJECT_ID = '550e8400-e29b-41d4-a716-446655440030';
|
||||
private const string TEACHER_ID = '550e8400-e29b-41d4-a716-446655440010';
|
||||
private const string TARGET_CLASS_1 = '550e8400-e29b-41d4-a716-446655440021';
|
||||
private const string TARGET_CLASS_2 = '550e8400-e29b-41d4-a716-446655440022';
|
||||
|
||||
private HomeworkDuplicator $duplicator;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
$this->duplicator = new HomeworkDuplicator();
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function itDuplicatesHomeworkForOneTargetClass(): void
|
||||
{
|
||||
$source = $this->createSourceHomework();
|
||||
$now = new DateTimeImmutable('2026-03-14 10:00:00');
|
||||
$targetClassId = ClassId::fromString(self::TARGET_CLASS_1);
|
||||
$dueDate = new DateTimeImmutable('2026-04-20');
|
||||
|
||||
$duplicates = $this->duplicator->dupliquer($source, [$targetClassId], [$dueDate], $now);
|
||||
|
||||
self::assertCount(1, $duplicates);
|
||||
$duplicate = $duplicates[0];
|
||||
self::assertSame($source->title, $duplicate->title);
|
||||
self::assertSame($source->description, $duplicate->description);
|
||||
self::assertTrue($duplicate->classId->equals($targetClassId));
|
||||
self::assertTrue($duplicate->subjectId->equals($source->subjectId));
|
||||
self::assertTrue($duplicate->teacherId->equals($source->teacherId));
|
||||
self::assertTrue($duplicate->tenantId->equals($source->tenantId));
|
||||
self::assertSame('2026-04-20', $duplicate->dueDate->format('Y-m-d'));
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function itDuplicatesHomeworkForMultipleTargetClasses(): void
|
||||
{
|
||||
$source = $this->createSourceHomework();
|
||||
$now = new DateTimeImmutable('2026-03-14 10:00:00');
|
||||
$targetClasses = [
|
||||
ClassId::fromString(self::TARGET_CLASS_1),
|
||||
ClassId::fromString(self::TARGET_CLASS_2),
|
||||
];
|
||||
$dueDates = [
|
||||
new DateTimeImmutable('2026-04-20'),
|
||||
new DateTimeImmutable('2026-04-21'),
|
||||
];
|
||||
|
||||
$duplicates = $this->duplicator->dupliquer($source, $targetClasses, $dueDates, $now);
|
||||
|
||||
self::assertCount(2, $duplicates);
|
||||
self::assertTrue($duplicates[0]->classId->equals($targetClasses[0]));
|
||||
self::assertTrue($duplicates[1]->classId->equals($targetClasses[1]));
|
||||
self::assertSame('2026-04-20', $duplicates[0]->dueDate->format('Y-m-d'));
|
||||
self::assertSame('2026-04-21', $duplicates[1]->dueDate->format('Y-m-d'));
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function itCreatesIndependentCopiesWithUniqueIds(): void
|
||||
{
|
||||
$source = $this->createSourceHomework();
|
||||
$now = new DateTimeImmutable('2026-03-14 10:00:00');
|
||||
$targetClasses = [
|
||||
ClassId::fromString(self::TARGET_CLASS_1),
|
||||
ClassId::fromString(self::TARGET_CLASS_2),
|
||||
];
|
||||
$dueDates = [
|
||||
new DateTimeImmutable('2026-04-20'),
|
||||
new DateTimeImmutable('2026-04-20'),
|
||||
];
|
||||
|
||||
$duplicates = $this->duplicator->dupliquer($source, $targetClasses, $dueDates, $now);
|
||||
|
||||
self::assertFalse($duplicates[0]->id->equals($source->id));
|
||||
self::assertFalse($duplicates[1]->id->equals($source->id));
|
||||
self::assertFalse($duplicates[0]->id->equals($duplicates[1]->id));
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function itRecordsDevoirCreeEventForEachDuplicate(): void
|
||||
{
|
||||
$source = $this->createSourceHomework();
|
||||
$now = new DateTimeImmutable('2026-03-14 10:00:00');
|
||||
$targetClasses = [
|
||||
ClassId::fromString(self::TARGET_CLASS_1),
|
||||
ClassId::fromString(self::TARGET_CLASS_2),
|
||||
];
|
||||
$dueDates = [
|
||||
new DateTimeImmutable('2026-04-20'),
|
||||
new DateTimeImmutable('2026-04-20'),
|
||||
];
|
||||
|
||||
$duplicates = $this->duplicator->dupliquer($source, $targetClasses, $dueDates, $now);
|
||||
|
||||
foreach ($duplicates as $duplicate) {
|
||||
$events = $duplicate->pullDomainEvents();
|
||||
self::assertCount(1, $events);
|
||||
}
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function itUsesSourceDueDateWhenNoneProvided(): void
|
||||
{
|
||||
$source = $this->createSourceHomework();
|
||||
$now = new DateTimeImmutable('2026-03-14 10:00:00');
|
||||
$targetClassId = ClassId::fromString(self::TARGET_CLASS_1);
|
||||
|
||||
$duplicates = $this->duplicator->dupliquer($source, [$targetClassId], [], $now);
|
||||
|
||||
self::assertSame($source->dueDate->format('Y-m-d'), $duplicates[0]->dueDate->format('Y-m-d'));
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function itPreservesNullDescription(): void
|
||||
{
|
||||
$source = $this->createSourceHomework(description: null);
|
||||
$now = new DateTimeImmutable('2026-03-14 10:00:00');
|
||||
$targetClassId = ClassId::fromString(self::TARGET_CLASS_1);
|
||||
|
||||
$duplicates = $this->duplicator->dupliquer($source, [$targetClassId], [], $now);
|
||||
|
||||
self::assertNull($duplicates[0]->description);
|
||||
}
|
||||
|
||||
private function createSourceHomework(?string $description = 'Faire les exercices 1 à 10'): Homework
|
||||
{
|
||||
return Homework::creer(
|
||||
tenantId: TenantId::fromString(self::TENANT_ID),
|
||||
classId: ClassId::fromString(self::CLASS_ID),
|
||||
subjectId: SubjectId::fromString(self::SUBJECT_ID),
|
||||
teacherId: UserId::fromString(self::TEACHER_ID),
|
||||
title: 'Exercices chapitre 5',
|
||||
description: $description,
|
||||
dueDate: new DateTimeImmutable('2026-04-15'),
|
||||
now: new DateTimeImmutable('2026-03-12 10:00:00'),
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user