Files
Classeo/backend/tests/Unit/Administration/Application/Command/UpdateSubject/UpdateSubjectHandlerTest.php
Mathias STRASSER 0d5a097c4c feat: Gestion des matières scolaires
Les établissements ont besoin de définir leur référentiel de matières
pour pouvoir ensuite les associer aux enseignants et aux classes.
Cette fonctionnalité permet aux administrateurs de créer, modifier et
archiver les matières avec leurs propriétés (nom, code court, couleur).

L'architecture suit le pattern DDD avec des Value Objects utilisant
les property hooks PHP 8.5 pour garantir l'immutabilité et la validation.
L'isolation multi-tenant est assurée par vérification dans les handlers.
2026-02-05 20:42:31 +01:00

264 lines
8.8 KiB
PHP

<?php
declare(strict_types=1);
namespace App\Tests\Unit\Administration\Application\Command\UpdateSubject;
use App\Administration\Application\Command\UpdateSubject\UpdateSubjectCommand;
use App\Administration\Application\Command\UpdateSubject\UpdateSubjectHandler;
use App\Administration\Domain\Exception\SubjectDejaExistanteException;
use App\Administration\Domain\Exception\SubjectNotFoundException;
use App\Administration\Domain\Model\SchoolClass\SchoolId;
use App\Administration\Domain\Model\Subject\Subject;
use App\Administration\Domain\Model\Subject\SubjectCode;
use App\Administration\Domain\Model\Subject\SubjectColor;
use App\Administration\Domain\Model\Subject\SubjectName;
use App\Administration\Infrastructure\Persistence\InMemory\InMemorySubjectRepository;
use App\Shared\Domain\Clock;
use App\Shared\Domain\Tenant\TenantId;
use DateTimeImmutable;
use PHPUnit\Framework\Attributes\Test;
use PHPUnit\Framework\TestCase;
final class UpdateSubjectHandlerTest extends TestCase
{
private const string TENANT_ID = '550e8400-e29b-41d4-a716-446655440001';
private const string OTHER_TENANT_ID = '550e8400-e29b-41d4-a716-446655440099';
private const string SCHOOL_ID = '550e8400-e29b-41d4-a716-446655440002';
private InMemorySubjectRepository $subjectRepository;
private Clock $clock;
protected function setUp(): void
{
$this->subjectRepository = new InMemorySubjectRepository();
$this->clock = new class implements Clock {
public function now(): DateTimeImmutable
{
return new DateTimeImmutable('2026-02-01 10:00:00');
}
};
}
#[Test]
public function itUpdatesSubjectName(): void
{
$subject = $this->createAndSaveSubject();
$handler = new UpdateSubjectHandler($this->subjectRepository, $this->clock);
$command = new UpdateSubjectCommand(
subjectId: (string) $subject->id,
tenantId: self::TENANT_ID,
schoolId: self::SCHOOL_ID,
name: 'Mathématiques avancées',
);
$updatedSubject = $handler($command);
self::assertSame('Mathématiques avancées', (string) $updatedSubject->name);
}
#[Test]
public function itUpdatesSubjectCode(): void
{
$subject = $this->createAndSaveSubject();
$handler = new UpdateSubjectHandler($this->subjectRepository, $this->clock);
$command = new UpdateSubjectCommand(
subjectId: (string) $subject->id,
tenantId: self::TENANT_ID,
schoolId: self::SCHOOL_ID,
code: 'MATHS',
);
$updatedSubject = $handler($command);
self::assertSame('MATHS', (string) $updatedSubject->code);
}
#[Test]
public function itUpdatesSubjectColor(): void
{
$subject = $this->createAndSaveSubject();
$handler = new UpdateSubjectHandler($this->subjectRepository, $this->clock);
$command = new UpdateSubjectCommand(
subjectId: (string) $subject->id,
tenantId: self::TENANT_ID,
schoolId: self::SCHOOL_ID,
color: '#EF4444',
);
$updatedSubject = $handler($command);
self::assertNotNull($updatedSubject->color);
self::assertSame('#EF4444', (string) $updatedSubject->color);
}
#[Test]
public function itClearsSubjectColor(): void
{
$subject = $this->createAndSaveSubject();
$handler = new UpdateSubjectHandler($this->subjectRepository, $this->clock);
$command = new UpdateSubjectCommand(
subjectId: (string) $subject->id,
tenantId: self::TENANT_ID,
schoolId: self::SCHOOL_ID,
clearColor: true,
);
$updatedSubject = $handler($command);
self::assertNull($updatedSubject->color);
}
#[Test]
public function itUpdatesSubjectDescription(): void
{
$subject = $this->createAndSaveSubject();
$handler = new UpdateSubjectHandler($this->subjectRepository, $this->clock);
$command = new UpdateSubjectCommand(
subjectId: (string) $subject->id,
tenantId: self::TENANT_ID,
schoolId: self::SCHOOL_ID,
description: 'Cours de mathématiques pour tous les niveaux',
);
$updatedSubject = $handler($command);
self::assertSame('Cours de mathématiques pour tous les niveaux', $updatedSubject->description);
}
#[Test]
public function itClearsSubjectDescription(): void
{
$subject = $this->createAndSaveSubject();
// First add a description
$subject->decrire('Une description', new DateTimeImmutable());
$this->subjectRepository->save($subject);
$handler = new UpdateSubjectHandler($this->subjectRepository, $this->clock);
$command = new UpdateSubjectCommand(
subjectId: (string) $subject->id,
tenantId: self::TENANT_ID,
schoolId: self::SCHOOL_ID,
clearDescription: true,
);
$updatedSubject = $handler($command);
self::assertNull($updatedSubject->description);
}
#[Test]
public function itThrowsExceptionWhenChangingToExistingCode(): void
{
// Create first subject with code MATH
$subject1 = $this->createAndSaveSubject();
// Create second subject with code FR
$subject2 = Subject::creer(
tenantId: TenantId::fromString(self::TENANT_ID),
schoolId: SchoolId::fromString(self::SCHOOL_ID),
name: new SubjectName('Français'),
code: new SubjectCode('FR'),
color: new SubjectColor('#EF4444'),
createdAt: new DateTimeImmutable('2026-01-15 10:00:00'),
);
$this->subjectRepository->save($subject2);
$handler = new UpdateSubjectHandler($this->subjectRepository, $this->clock);
// Try to change subject2's code to MATH (which already exists)
$this->expectException(SubjectDejaExistanteException::class);
$command = new UpdateSubjectCommand(
subjectId: (string) $subject2->id,
tenantId: self::TENANT_ID,
schoolId: self::SCHOOL_ID,
code: 'MATH',
);
$handler($command);
}
#[Test]
public function itAllowsKeepingSameCode(): void
{
$subject = $this->createAndSaveSubject();
$handler = new UpdateSubjectHandler($this->subjectRepository, $this->clock);
// Update name but keep same code
$command = new UpdateSubjectCommand(
subjectId: (string) $subject->id,
tenantId: self::TENANT_ID,
schoolId: self::SCHOOL_ID,
name: 'Maths avancées',
code: 'MATH', // Same code
);
$updatedSubject = $handler($command);
self::assertSame('Maths avancées', (string) $updatedSubject->name);
self::assertSame('MATH', (string) $updatedSubject->code);
}
#[Test]
public function itUpdatesMultipleFieldsAtOnce(): void
{
$subject = $this->createAndSaveSubject();
$handler = new UpdateSubjectHandler($this->subjectRepository, $this->clock);
$command = new UpdateSubjectCommand(
subjectId: (string) $subject->id,
tenantId: self::TENANT_ID,
schoolId: self::SCHOOL_ID,
name: 'Maths avancées',
color: '#10B981',
description: 'Niveau supérieur',
);
$updatedSubject = $handler($command);
self::assertSame('Maths avancées', (string) $updatedSubject->name);
self::assertNotNull($updatedSubject->color);
self::assertSame('#10B981', (string) $updatedSubject->color);
self::assertSame('Niveau supérieur', $updatedSubject->description);
}
#[Test]
public function itThrowsExceptionWhenSubjectBelongsToDifferentTenant(): void
{
$subject = $this->createAndSaveSubject();
$handler = new UpdateSubjectHandler($this->subjectRepository, $this->clock);
$this->expectException(SubjectNotFoundException::class);
// Try to update with a different tenant ID (tenant isolation violation)
$command = new UpdateSubjectCommand(
subjectId: (string) $subject->id,
tenantId: self::OTHER_TENANT_ID,
schoolId: self::SCHOOL_ID,
name: 'Tentative de modification',
);
$handler($command);
}
private function createAndSaveSubject(): Subject
{
$subject = Subject::creer(
tenantId: TenantId::fromString(self::TENANT_ID),
schoolId: SchoolId::fromString(self::SCHOOL_ID),
name: new SubjectName('Mathématiques'),
code: new SubjectCode('MATH'),
color: new SubjectColor('#3B82F6'),
createdAt: new DateTimeImmutable('2026-01-15 10:00:00'),
);
$this->subjectRepository->save($subject);
return $subject;
}
}