feat: Permettre à l'élève de consulter ses devoirs
L'élève n'avait aucun moyen de voir les devoirs assignés à sa classe. Cette fonctionnalité ajoute la consultation complète : liste triée par échéance, détail avec pièces jointes, filtrage par matière, et marquage personnel « fait » en localStorage. Le dashboard élève affiche désormais les devoirs à venir avec ouverture du détail en modale, et un lien vers la page complète. L'accès API est sécurisé par vérification de la classe de l'élève (pas d'IDOR) et validation du chemin des pièces jointes (pas de path traversal).
This commit is contained in:
@@ -0,0 +1,259 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Tests\Unit\Scolarite\Application\Query\GetStudentHomework;
|
||||
|
||||
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\Port\ScheduleDisplayReader;
|
||||
use App\Scolarite\Application\Port\StudentClassReader;
|
||||
use App\Scolarite\Application\Query\GetStudentHomework\GetStudentHomeworkHandler;
|
||||
use App\Scolarite\Application\Query\GetStudentHomework\GetStudentHomeworkQuery;
|
||||
use App\Scolarite\Domain\Model\Homework\Homework;
|
||||
use App\Scolarite\Domain\Model\Homework\HomeworkAttachment;
|
||||
use App\Scolarite\Domain\Model\Homework\HomeworkAttachmentId;
|
||||
use App\Scolarite\Infrastructure\Persistence\InMemory\InMemoryHomeworkAttachmentRepository;
|
||||
use App\Scolarite\Infrastructure\Persistence\InMemory\InMemoryHomeworkRepository;
|
||||
use App\Shared\Domain\Tenant\TenantId;
|
||||
use DateTimeImmutable;
|
||||
use PHPUnit\Framework\Attributes\Test;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
final class GetStudentHomeworkHandlerTest extends TestCase
|
||||
{
|
||||
private const string TENANT_ID = '550e8400-e29b-41d4-a716-446655440001';
|
||||
private const string STUDENT_ID = '550e8400-e29b-41d4-a716-446655440050';
|
||||
private const string CLASS_ID = '550e8400-e29b-41d4-a716-446655440020';
|
||||
private const string SUBJECT_MATH_ID = '550e8400-e29b-41d4-a716-446655440030';
|
||||
private const string SUBJECT_FRENCH_ID = '550e8400-e29b-41d4-a716-446655440031';
|
||||
private const string TEACHER_ID = '550e8400-e29b-41d4-a716-446655440010';
|
||||
|
||||
private InMemoryHomeworkRepository $homeworkRepository;
|
||||
private InMemoryHomeworkAttachmentRepository $attachmentRepository;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
$this->homeworkRepository = new InMemoryHomeworkRepository();
|
||||
$this->attachmentRepository = new InMemoryHomeworkAttachmentRepository();
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function itReturnsEmptyWhenStudentHasNoClass(): void
|
||||
{
|
||||
$handler = $this->createHandler(classId: null);
|
||||
|
||||
$result = $handler(new GetStudentHomeworkQuery(
|
||||
studentId: self::STUDENT_ID,
|
||||
tenantId: self::TENANT_ID,
|
||||
));
|
||||
|
||||
self::assertSame([], $result);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function itReturnsHomeworkForStudentClass(): void
|
||||
{
|
||||
$this->givenHomework(title: 'Exercices chapitre 5', dueDate: '2026-04-15');
|
||||
$handler = $this->createHandler();
|
||||
|
||||
$result = $handler(new GetStudentHomeworkQuery(
|
||||
studentId: self::STUDENT_ID,
|
||||
tenantId: self::TENANT_ID,
|
||||
));
|
||||
|
||||
self::assertCount(1, $result);
|
||||
self::assertSame('Exercices chapitre 5', $result[0]->title);
|
||||
self::assertSame('2026-04-15', $result[0]->dueDate);
|
||||
self::assertSame('Mathématiques', $result[0]->subjectName);
|
||||
self::assertSame('#3b82f6', $result[0]->subjectColor);
|
||||
self::assertSame('Jean Dupont', $result[0]->teacherName);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function itSortsByDueDateAscending(): void
|
||||
{
|
||||
$this->givenHomework(title: 'Devoir lointain', dueDate: '2026-05-20');
|
||||
$this->givenHomework(title: 'Devoir proche', dueDate: '2026-04-10');
|
||||
$this->givenHomework(title: 'Devoir milieu', dueDate: '2026-04-25');
|
||||
|
||||
$handler = $this->createHandler();
|
||||
|
||||
$result = $handler(new GetStudentHomeworkQuery(
|
||||
studentId: self::STUDENT_ID,
|
||||
tenantId: self::TENANT_ID,
|
||||
));
|
||||
|
||||
self::assertCount(3, $result);
|
||||
self::assertSame('Devoir proche', $result[0]->title);
|
||||
self::assertSame('Devoir milieu', $result[1]->title);
|
||||
self::assertSame('Devoir lointain', $result[2]->title);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function itFiltersDeletedHomework(): void
|
||||
{
|
||||
$homework = $this->givenHomework(title: 'Devoir supprimé', dueDate: '2026-04-15');
|
||||
$homework->supprimer(new DateTimeImmutable('2026-03-20'));
|
||||
$this->homeworkRepository->save($homework);
|
||||
|
||||
$this->givenHomework(title: 'Devoir actif', dueDate: '2026-04-20');
|
||||
|
||||
$handler = $this->createHandler();
|
||||
|
||||
$result = $handler(new GetStudentHomeworkQuery(
|
||||
studentId: self::STUDENT_ID,
|
||||
tenantId: self::TENANT_ID,
|
||||
));
|
||||
|
||||
self::assertCount(1, $result);
|
||||
self::assertSame('Devoir actif', $result[0]->title);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function itFiltersBySubjectWhenProvided(): void
|
||||
{
|
||||
$this->givenHomework(title: 'Maths', dueDate: '2026-04-15', subjectId: self::SUBJECT_MATH_ID);
|
||||
$this->givenHomework(title: 'Français', dueDate: '2026-04-16', subjectId: self::SUBJECT_FRENCH_ID);
|
||||
|
||||
$handler = $this->createHandler();
|
||||
|
||||
$result = $handler(new GetStudentHomeworkQuery(
|
||||
studentId: self::STUDENT_ID,
|
||||
tenantId: self::TENANT_ID,
|
||||
subjectId: self::SUBJECT_MATH_ID,
|
||||
));
|
||||
|
||||
self::assertCount(1, $result);
|
||||
self::assertSame('Maths', $result[0]->title);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function itExcludesHomeworkFromOtherClasses(): void
|
||||
{
|
||||
$this->givenHomework(title: 'Mon devoir', dueDate: '2026-04-15', classId: self::CLASS_ID);
|
||||
|
||||
$otherClassId = '550e8400-e29b-41d4-a716-446655440099';
|
||||
$this->givenHomework(title: 'Autre classe', dueDate: '2026-04-16', classId: $otherClassId);
|
||||
|
||||
$handler = $this->createHandler();
|
||||
|
||||
$result = $handler(new GetStudentHomeworkQuery(
|
||||
studentId: self::STUDENT_ID,
|
||||
tenantId: self::TENANT_ID,
|
||||
));
|
||||
|
||||
self::assertCount(1, $result);
|
||||
self::assertSame('Mon devoir', $result[0]->title);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function itIndicatesWhenHomeworkHasAttachments(): void
|
||||
{
|
||||
$homework = $this->givenHomework(title: 'Avec pièce jointe', dueDate: '2026-04-15');
|
||||
|
||||
$attachment = new HomeworkAttachment(
|
||||
id: HomeworkAttachmentId::generate(),
|
||||
filename: 'exercice.pdf',
|
||||
filePath: '/uploads/exercice.pdf',
|
||||
fileSize: 1024,
|
||||
mimeType: 'application/pdf',
|
||||
uploadedAt: new DateTimeImmutable('2026-03-12'),
|
||||
);
|
||||
$this->attachmentRepository->save($homework->id, $attachment);
|
||||
|
||||
$handler = $this->createHandler();
|
||||
|
||||
$result = $handler(new GetStudentHomeworkQuery(
|
||||
studentId: self::STUDENT_ID,
|
||||
tenantId: self::TENANT_ID,
|
||||
));
|
||||
|
||||
self::assertCount(1, $result);
|
||||
self::assertTrue($result[0]->hasAttachments);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function itIndicatesWhenHomeworkHasNoAttachments(): void
|
||||
{
|
||||
$this->givenHomework(title: 'Sans pièce jointe', dueDate: '2026-04-15');
|
||||
|
||||
$handler = $this->createHandler();
|
||||
|
||||
$result = $handler(new GetStudentHomeworkQuery(
|
||||
studentId: self::STUDENT_ID,
|
||||
tenantId: self::TENANT_ID,
|
||||
));
|
||||
|
||||
self::assertCount(1, $result);
|
||||
self::assertFalse($result[0]->hasAttachments);
|
||||
}
|
||||
|
||||
private function createHandler(?string $classId = self::CLASS_ID): GetStudentHomeworkHandler
|
||||
{
|
||||
$studentClassReader = new class($classId) implements StudentClassReader {
|
||||
public function __construct(private readonly ?string $classId)
|
||||
{
|
||||
}
|
||||
|
||||
public function currentClassId(string $studentId, TenantId $tenantId): ?string
|
||||
{
|
||||
return $this->classId;
|
||||
}
|
||||
};
|
||||
|
||||
$displayReader = new class implements ScheduleDisplayReader {
|
||||
public function subjectDisplay(string $tenantId, string ...$subjectIds): array
|
||||
{
|
||||
$map = [];
|
||||
|
||||
foreach ($subjectIds as $id) {
|
||||
$map[$id] = ['name' => 'Mathématiques', 'color' => '#3b82f6'];
|
||||
}
|
||||
|
||||
return $map;
|
||||
}
|
||||
|
||||
public function teacherNames(string $tenantId, string ...$teacherIds): array
|
||||
{
|
||||
$map = [];
|
||||
|
||||
foreach ($teacherIds as $id) {
|
||||
$map[$id] = 'Jean Dupont';
|
||||
}
|
||||
|
||||
return $map;
|
||||
}
|
||||
};
|
||||
|
||||
return new GetStudentHomeworkHandler(
|
||||
$studentClassReader,
|
||||
$this->homeworkRepository,
|
||||
$this->attachmentRepository,
|
||||
$displayReader,
|
||||
);
|
||||
}
|
||||
|
||||
private function givenHomework(
|
||||
string $title,
|
||||
string $dueDate,
|
||||
string $classId = self::CLASS_ID,
|
||||
string $subjectId = self::SUBJECT_MATH_ID,
|
||||
): Homework {
|
||||
$homework = Homework::creer(
|
||||
tenantId: TenantId::fromString(self::TENANT_ID),
|
||||
classId: ClassId::fromString($classId),
|
||||
subjectId: SubjectId::fromString($subjectId),
|
||||
teacherId: UserId::fromString(self::TEACHER_ID),
|
||||
title: $title,
|
||||
description: null,
|
||||
dueDate: new DateTimeImmutable($dueDate),
|
||||
now: new DateTimeImmutable('2026-03-12 10:00:00'),
|
||||
);
|
||||
|
||||
$this->homeworkRepository->save($homework);
|
||||
|
||||
return $homework;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user