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.
This commit is contained in:
@@ -0,0 +1,142 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Tests\Unit\Administration\Application\Command\ArchiveSubject;
|
||||
|
||||
use App\Administration\Application\Command\ArchiveSubject\ArchiveSubjectCommand;
|
||||
use App\Administration\Application\Command\ArchiveSubject\ArchiveSubjectHandler;
|
||||
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\Domain\Model\Subject\SubjectStatus;
|
||||
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 ArchiveSubjectHandlerTest 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 itArchivesSubjectSuccessfully(): void
|
||||
{
|
||||
$subject = $this->createAndSaveSubject();
|
||||
$handler = new ArchiveSubjectHandler($this->subjectRepository, $this->clock);
|
||||
|
||||
$command = new ArchiveSubjectCommand(
|
||||
subjectId: (string) $subject->id,
|
||||
tenantId: self::TENANT_ID,
|
||||
);
|
||||
|
||||
$archivedSubject = $handler($command);
|
||||
|
||||
self::assertSame(SubjectStatus::ARCHIVED, $archivedSubject->status);
|
||||
self::assertFalse($archivedSubject->estActive());
|
||||
self::assertNotNull($archivedSubject->deletedAt);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function itPersistsArchivedSubjectInRepository(): void
|
||||
{
|
||||
$subject = $this->createAndSaveSubject();
|
||||
$handler = new ArchiveSubjectHandler($this->subjectRepository, $this->clock);
|
||||
|
||||
$command = new ArchiveSubjectCommand(
|
||||
subjectId: (string) $subject->id,
|
||||
tenantId: self::TENANT_ID,
|
||||
);
|
||||
|
||||
$handler($command);
|
||||
|
||||
// Retrieve from repository
|
||||
$retrievedSubject = $this->subjectRepository->get($subject->id);
|
||||
|
||||
self::assertSame(SubjectStatus::ARCHIVED, $retrievedSubject->status);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function itThrowsExceptionWhenSubjectNotFound(): void
|
||||
{
|
||||
$handler = new ArchiveSubjectHandler($this->subjectRepository, $this->clock);
|
||||
|
||||
$this->expectException(SubjectNotFoundException::class);
|
||||
|
||||
$command = new ArchiveSubjectCommand(
|
||||
subjectId: '550e8400-e29b-41d4-a716-446655440099',
|
||||
tenantId: self::TENANT_ID,
|
||||
);
|
||||
$handler($command);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function itThrowsExceptionWhenSubjectBelongsToDifferentTenant(): void
|
||||
{
|
||||
$subject = $this->createAndSaveSubject();
|
||||
$handler = new ArchiveSubjectHandler($this->subjectRepository, $this->clock);
|
||||
|
||||
$this->expectException(SubjectNotFoundException::class);
|
||||
|
||||
// Try to archive with a different tenant ID
|
||||
$command = new ArchiveSubjectCommand(
|
||||
subjectId: (string) $subject->id,
|
||||
tenantId: self::OTHER_TENANT_ID,
|
||||
);
|
||||
$handler($command);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function itIsIdempotentWhenArchivingAlreadyArchivedSubject(): void
|
||||
{
|
||||
$subject = $this->createAndSaveSubject();
|
||||
$handler = new ArchiveSubjectHandler($this->subjectRepository, $this->clock);
|
||||
|
||||
$command = new ArchiveSubjectCommand(
|
||||
subjectId: (string) $subject->id,
|
||||
tenantId: self::TENANT_ID,
|
||||
);
|
||||
|
||||
// Archive twice
|
||||
$handler($command);
|
||||
$archivedAgain = $handler($command);
|
||||
|
||||
self::assertSame(SubjectStatus::ARCHIVED, $archivedAgain->status);
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,207 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Tests\Unit\Administration\Application\Command\CreateSubject;
|
||||
|
||||
use App\Administration\Application\Command\CreateSubject\CreateSubjectCommand;
|
||||
use App\Administration\Application\Command\CreateSubject\CreateSubjectHandler;
|
||||
use App\Administration\Domain\Exception\SubjectDejaExistanteException;
|
||||
use App\Administration\Domain\Model\Subject\SubjectId;
|
||||
use App\Administration\Domain\Model\Subject\SubjectStatus;
|
||||
use App\Administration\Infrastructure\Persistence\InMemory\InMemorySubjectRepository;
|
||||
use App\Shared\Domain\Clock;
|
||||
use DateTimeImmutable;
|
||||
use PHPUnit\Framework\Attributes\Test;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
final class CreateSubjectHandlerTest extends TestCase
|
||||
{
|
||||
private const string TENANT_ID = '550e8400-e29b-41d4-a716-446655440001';
|
||||
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-01-31 10:00:00');
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function itCreatesSubjectSuccessfully(): void
|
||||
{
|
||||
$handler = new CreateSubjectHandler($this->subjectRepository, $this->clock);
|
||||
$command = new CreateSubjectCommand(
|
||||
tenantId: self::TENANT_ID,
|
||||
schoolId: self::SCHOOL_ID,
|
||||
name: 'Mathématiques',
|
||||
code: 'MATH',
|
||||
color: '#3B82F6',
|
||||
);
|
||||
|
||||
$subject = $handler($command);
|
||||
|
||||
self::assertNotEmpty((string) $subject->id);
|
||||
self::assertSame('Mathématiques', (string) $subject->name);
|
||||
self::assertSame('MATH', (string) $subject->code);
|
||||
self::assertNotNull($subject->color);
|
||||
self::assertSame('#3B82F6', (string) $subject->color);
|
||||
self::assertSame(SubjectStatus::ACTIVE, $subject->status);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function itCreatesSubjectWithNullColor(): void
|
||||
{
|
||||
$handler = new CreateSubjectHandler($this->subjectRepository, $this->clock);
|
||||
$command = new CreateSubjectCommand(
|
||||
tenantId: self::TENANT_ID,
|
||||
schoolId: self::SCHOOL_ID,
|
||||
name: 'Arts plastiques',
|
||||
code: 'ART',
|
||||
color: null,
|
||||
);
|
||||
|
||||
$subject = $handler($command);
|
||||
|
||||
self::assertNotEmpty((string) $subject->id);
|
||||
self::assertSame('Arts plastiques', (string) $subject->name);
|
||||
self::assertSame('ART', (string) $subject->code);
|
||||
self::assertNull($subject->color);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function itPersistsSubjectInRepository(): void
|
||||
{
|
||||
$handler = new CreateSubjectHandler($this->subjectRepository, $this->clock);
|
||||
$command = new CreateSubjectCommand(
|
||||
tenantId: self::TENANT_ID,
|
||||
schoolId: self::SCHOOL_ID,
|
||||
name: 'Mathématiques',
|
||||
code: 'MATH',
|
||||
color: '#3B82F6',
|
||||
);
|
||||
|
||||
$createdSubject = $handler($command);
|
||||
|
||||
$subject = $this->subjectRepository->get(
|
||||
SubjectId::fromString((string) $createdSubject->id),
|
||||
);
|
||||
|
||||
self::assertSame('Mathématiques', (string) $subject->name);
|
||||
self::assertSame('MATH', (string) $subject->code);
|
||||
self::assertSame(SubjectStatus::ACTIVE, $subject->status);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function itThrowsExceptionWhenSubjectCodeAlreadyExists(): void
|
||||
{
|
||||
$handler = new CreateSubjectHandler($this->subjectRepository, $this->clock);
|
||||
$command = new CreateSubjectCommand(
|
||||
tenantId: self::TENANT_ID,
|
||||
schoolId: self::SCHOOL_ID,
|
||||
name: 'Mathématiques',
|
||||
code: 'MATH',
|
||||
color: '#3B82F6',
|
||||
);
|
||||
|
||||
// First creation should succeed
|
||||
$handler($command);
|
||||
|
||||
// Second creation with same code should throw
|
||||
$this->expectException(SubjectDejaExistanteException::class);
|
||||
$this->expectExceptionMessage('Une matière avec le code "MATH" existe déjà dans cet établissement.');
|
||||
|
||||
$commandDuplicate = new CreateSubjectCommand(
|
||||
tenantId: self::TENANT_ID,
|
||||
schoolId: self::SCHOOL_ID,
|
||||
name: 'Maths avancées', // Different name, same code
|
||||
code: 'MATH',
|
||||
color: '#EF4444',
|
||||
);
|
||||
$handler($commandDuplicate);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function itAllowsSameCodeInDifferentTenant(): void
|
||||
{
|
||||
$handler = new CreateSubjectHandler($this->subjectRepository, $this->clock);
|
||||
|
||||
// Create in tenant 1
|
||||
$command1 = new CreateSubjectCommand(
|
||||
tenantId: self::TENANT_ID,
|
||||
schoolId: self::SCHOOL_ID,
|
||||
name: 'Mathématiques',
|
||||
code: 'MATH',
|
||||
color: '#3B82F6',
|
||||
);
|
||||
$subject1 = $handler($command1);
|
||||
|
||||
// Create same code in tenant 2 should succeed
|
||||
$command2 = new CreateSubjectCommand(
|
||||
tenantId: '550e8400-e29b-41d4-a716-446655440099',
|
||||
schoolId: self::SCHOOL_ID,
|
||||
name: 'Mathématiques',
|
||||
code: 'MATH',
|
||||
color: '#3B82F6',
|
||||
);
|
||||
$subject2 = $handler($command2);
|
||||
|
||||
self::assertFalse($subject1->id->equals($subject2->id));
|
||||
self::assertSame('MATH', (string) $subject1->code);
|
||||
self::assertSame('MATH', (string) $subject2->code);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function itAllowsSameCodeInDifferentSchool(): void
|
||||
{
|
||||
$handler = new CreateSubjectHandler($this->subjectRepository, $this->clock);
|
||||
|
||||
// Create in school 1
|
||||
$command1 = new CreateSubjectCommand(
|
||||
tenantId: self::TENANT_ID,
|
||||
schoolId: self::SCHOOL_ID,
|
||||
name: 'Mathématiques',
|
||||
code: 'MATH',
|
||||
color: '#3B82F6',
|
||||
);
|
||||
$subject1 = $handler($command1);
|
||||
|
||||
// Create same code in school 2 should succeed
|
||||
$command2 = new CreateSubjectCommand(
|
||||
tenantId: self::TENANT_ID,
|
||||
schoolId: '550e8400-e29b-41d4-a716-446655440099',
|
||||
name: 'Mathématiques',
|
||||
code: 'MATH',
|
||||
color: '#3B82F6',
|
||||
);
|
||||
$subject2 = $handler($command2);
|
||||
|
||||
self::assertFalse($subject1->id->equals($subject2->id));
|
||||
self::assertSame('MATH', (string) $subject1->code);
|
||||
self::assertSame('MATH', (string) $subject2->code);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function itNormalizesCodeToUppercase(): void
|
||||
{
|
||||
$handler = new CreateSubjectHandler($this->subjectRepository, $this->clock);
|
||||
$command = new CreateSubjectCommand(
|
||||
tenantId: self::TENANT_ID,
|
||||
schoolId: self::SCHOOL_ID,
|
||||
name: 'Mathématiques',
|
||||
code: 'math',
|
||||
color: '#3B82F6',
|
||||
);
|
||||
|
||||
$subject = $handler($command);
|
||||
|
||||
self::assertSame('MATH', (string) $subject->code);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,263 @@
|
||||
<?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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,120 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Tests\Unit\Administration\Domain\Model\Subject;
|
||||
|
||||
use App\Administration\Domain\Exception\SubjectCodeInvalideException;
|
||||
use App\Administration\Domain\Model\Subject\SubjectCode;
|
||||
use PHPUnit\Framework\Attributes\DataProvider;
|
||||
use PHPUnit\Framework\Attributes\Test;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
final class SubjectCodeTest extends TestCase
|
||||
{
|
||||
#[Test]
|
||||
public function constructWithValidCode(): void
|
||||
{
|
||||
$code = new SubjectCode('MATH');
|
||||
|
||||
self::assertSame('MATH', $code->value);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function constructNormalizesToUppercase(): void
|
||||
{
|
||||
$code = new SubjectCode('math');
|
||||
|
||||
self::assertSame('MATH', $code->value);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function constructTrimsWhitespace(): void
|
||||
{
|
||||
$code = new SubjectCode(' MATH ');
|
||||
|
||||
self::assertSame('MATH', $code->value);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function constructWithMinimumLength(): void
|
||||
{
|
||||
$code = new SubjectCode('FR');
|
||||
|
||||
self::assertSame('FR', $code->value);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function constructWithMaximumLength(): void
|
||||
{
|
||||
$code = new SubjectCode('HISTGEO123');
|
||||
|
||||
self::assertSame('HISTGEO123', $code->value);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function constructWithDigits(): void
|
||||
{
|
||||
$code = new SubjectCode('EPS1');
|
||||
|
||||
self::assertSame('EPS1', $code->value);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
#[DataProvider('invalidCodesProvider')]
|
||||
public function constructThrowsExceptionForInvalidCode(string $invalidCode): void
|
||||
{
|
||||
$this->expectException(SubjectCodeInvalideException::class);
|
||||
|
||||
new SubjectCode($invalidCode);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return iterable<string, array{string}>
|
||||
*/
|
||||
public static function invalidCodesProvider(): iterable
|
||||
{
|
||||
yield 'empty string' => [''];
|
||||
yield 'single character' => ['M'];
|
||||
yield 'whitespace only' => [' '];
|
||||
yield 'too long' => ['MATHEMATICS']; // 11 chars
|
||||
yield 'with special characters' => ['MATH-1'];
|
||||
yield 'with spaces' => ['MA TH'];
|
||||
yield 'lowercase with special' => ['math!'];
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function equalsReturnsTrueForSameValue(): void
|
||||
{
|
||||
$code1 = new SubjectCode('MATH');
|
||||
$code2 = new SubjectCode('MATH');
|
||||
|
||||
self::assertTrue($code1->equals($code2));
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function equalsReturnsTrueForDifferentCase(): void
|
||||
{
|
||||
$code1 = new SubjectCode('MATH');
|
||||
$code2 = new SubjectCode('math');
|
||||
|
||||
self::assertTrue($code1->equals($code2));
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function equalsReturnsFalseForDifferentValue(): void
|
||||
{
|
||||
$code1 = new SubjectCode('MATH');
|
||||
$code2 = new SubjectCode('FR');
|
||||
|
||||
self::assertFalse($code1->equals($code2));
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function toStringReturnsValue(): void
|
||||
{
|
||||
$code = new SubjectCode('MATH');
|
||||
|
||||
self::assertSame('MATH', (string) $code);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,120 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Tests\Unit\Administration\Domain\Model\Subject;
|
||||
|
||||
use App\Administration\Domain\Exception\SubjectColorInvalideException;
|
||||
use App\Administration\Domain\Model\Subject\SubjectColor;
|
||||
use PHPUnit\Framework\Attributes\DataProvider;
|
||||
use PHPUnit\Framework\Attributes\Test;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
final class SubjectColorTest extends TestCase
|
||||
{
|
||||
#[Test]
|
||||
public function constructWithValidColor(): void
|
||||
{
|
||||
$color = new SubjectColor('#3B82F6');
|
||||
|
||||
self::assertSame('#3B82F6', $color->value);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function constructNormalizesToUppercase(): void
|
||||
{
|
||||
$color = new SubjectColor('#3b82f6');
|
||||
|
||||
self::assertSame('#3B82F6', $color->value);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function constructTrimsWhitespace(): void
|
||||
{
|
||||
$color = new SubjectColor(' #3B82F6 ');
|
||||
|
||||
self::assertSame('#3B82F6', $color->value);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
#[DataProvider('validColorsProvider')]
|
||||
public function constructWithValidColors(string $input, string $expected): void
|
||||
{
|
||||
$color = new SubjectColor($input);
|
||||
|
||||
self::assertSame($expected, $color->value);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return iterable<string, array{string, string}>
|
||||
*/
|
||||
public static function validColorsProvider(): iterable
|
||||
{
|
||||
yield 'blue' => ['#3B82F6', '#3B82F6'];
|
||||
yield 'red' => ['#EF4444', '#EF4444'];
|
||||
yield 'green' => ['#10B981', '#10B981'];
|
||||
yield 'orange' => ['#F59E0B', '#F59E0B'];
|
||||
yield 'black' => ['#000000', '#000000'];
|
||||
yield 'white' => ['#FFFFFF', '#FFFFFF'];
|
||||
yield 'lowercase' => ['#aabbcc', '#AABBCC'];
|
||||
}
|
||||
|
||||
#[Test]
|
||||
#[DataProvider('invalidColorsProvider')]
|
||||
public function constructThrowsExceptionForInvalidColor(string $invalidColor): void
|
||||
{
|
||||
$this->expectException(SubjectColorInvalideException::class);
|
||||
|
||||
new SubjectColor($invalidColor);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return iterable<string, array{string}>
|
||||
*/
|
||||
public static function invalidColorsProvider(): iterable
|
||||
{
|
||||
yield 'empty string' => [''];
|
||||
yield 'no hash' => ['3B82F6'];
|
||||
yield 'short format' => ['#FFF'];
|
||||
yield 'too short' => ['#3B82F'];
|
||||
yield 'too long' => ['#3B82F6F'];
|
||||
yield 'invalid characters' => ['#GGGGGG'];
|
||||
yield 'rgb format' => ['rgb(59,130,246)'];
|
||||
yield 'named color' => ['blue'];
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function equalsReturnsTrueForSameValue(): void
|
||||
{
|
||||
$color1 = new SubjectColor('#3B82F6');
|
||||
$color2 = new SubjectColor('#3B82F6');
|
||||
|
||||
self::assertTrue($color1->equals($color2));
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function equalsReturnsTrueForDifferentCase(): void
|
||||
{
|
||||
$color1 = new SubjectColor('#3B82F6');
|
||||
$color2 = new SubjectColor('#3b82f6');
|
||||
|
||||
self::assertTrue($color1->equals($color2));
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function equalsReturnsFalseForDifferentValue(): void
|
||||
{
|
||||
$color1 = new SubjectColor('#3B82F6');
|
||||
$color2 = new SubjectColor('#EF4444');
|
||||
|
||||
self::assertFalse($color1->equals($color2));
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function toStringReturnsValue(): void
|
||||
{
|
||||
$color = new SubjectColor('#3B82F6');
|
||||
|
||||
self::assertSame('#3B82F6', (string) $color);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Tests\Unit\Administration\Domain\Model\Subject;
|
||||
|
||||
use App\Administration\Domain\Exception\SubjectNameInvalideException;
|
||||
use App\Administration\Domain\Model\Subject\SubjectName;
|
||||
use PHPUnit\Framework\Attributes\DataProvider;
|
||||
use PHPUnit\Framework\Attributes\Test;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
final class SubjectNameTest extends TestCase
|
||||
{
|
||||
#[Test]
|
||||
public function constructWithValidName(): void
|
||||
{
|
||||
$name = new SubjectName('Mathématiques');
|
||||
|
||||
self::assertSame('Mathématiques', $name->value);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function constructTrimsWhitespace(): void
|
||||
{
|
||||
$name = new SubjectName(' Mathématiques ');
|
||||
|
||||
self::assertSame('Mathématiques', $name->value);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function constructWithMinimumLength(): void
|
||||
{
|
||||
$name = new SubjectName('FR');
|
||||
|
||||
self::assertSame('FR', $name->value);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function constructWithMaximumLength(): void
|
||||
{
|
||||
$longName = str_repeat('M', 100);
|
||||
$name = new SubjectName($longName);
|
||||
|
||||
self::assertSame($longName, $name->value);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
#[DataProvider('invalidNamesProvider')]
|
||||
public function constructThrowsExceptionForInvalidName(string $invalidName): void
|
||||
{
|
||||
$this->expectException(SubjectNameInvalideException::class);
|
||||
|
||||
new SubjectName($invalidName);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return iterable<string, array{string}>
|
||||
*/
|
||||
public static function invalidNamesProvider(): iterable
|
||||
{
|
||||
yield 'empty string' => [''];
|
||||
yield 'single character' => ['M'];
|
||||
yield 'whitespace only' => [' '];
|
||||
yield 'too long' => [str_repeat('M', 101)];
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function equalsReturnsTrueForSameValue(): void
|
||||
{
|
||||
$name1 = new SubjectName('Mathématiques');
|
||||
$name2 = new SubjectName('Mathématiques');
|
||||
|
||||
self::assertTrue($name1->equals($name2));
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function equalsReturnsFalseForDifferentValue(): void
|
||||
{
|
||||
$name1 = new SubjectName('Mathématiques');
|
||||
$name2 = new SubjectName('Français');
|
||||
|
||||
self::assertFalse($name1->equals($name2));
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function toStringReturnsValue(): void
|
||||
{
|
||||
$name = new SubjectName('Mathématiques');
|
||||
|
||||
self::assertSame('Mathématiques', (string) $name);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,294 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Tests\Unit\Administration\Domain\Model\Subject;
|
||||
|
||||
use App\Administration\Domain\Event\MatiereCreee;
|
||||
use App\Administration\Domain\Event\MatiereModifiee;
|
||||
use App\Administration\Domain\Event\MatiereSupprimee;
|
||||
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\SubjectId;
|
||||
use App\Administration\Domain\Model\Subject\SubjectName;
|
||||
use App\Administration\Domain\Model\Subject\SubjectStatus;
|
||||
use App\Shared\Domain\Tenant\TenantId;
|
||||
use DateTimeImmutable;
|
||||
use PHPUnit\Framework\Attributes\Test;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
final class SubjectTest extends TestCase
|
||||
{
|
||||
private const string TENANT_ID = '550e8400-e29b-41d4-a716-446655440001';
|
||||
private const string SCHOOL_ID = '550e8400-e29b-41d4-a716-446655440002';
|
||||
|
||||
#[Test]
|
||||
public function creerCreatesSubjectWithActiveStatus(): void
|
||||
{
|
||||
$subject = $this->createSubject();
|
||||
|
||||
self::assertSame(SubjectStatus::ACTIVE, $subject->status);
|
||||
self::assertTrue($subject->estActive());
|
||||
self::assertTrue($subject->peutEtreUtilisee());
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function creerRecordsMatiereCreeeEvent(): void
|
||||
{
|
||||
$subject = $this->createSubject();
|
||||
|
||||
$events = $subject->pullDomainEvents();
|
||||
|
||||
self::assertCount(1, $events);
|
||||
self::assertInstanceOf(MatiereCreee::class, $events[0]);
|
||||
self::assertSame($subject->id, $events[0]->subjectId);
|
||||
self::assertSame($subject->tenantId, $events[0]->tenantId);
|
||||
self::assertSame($subject->name, $events[0]->name);
|
||||
self::assertSame($subject->code, $events[0]->code);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function creerSetsAllProperties(): void
|
||||
{
|
||||
$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');
|
||||
|
||||
$subject = Subject::creer(
|
||||
tenantId: $tenantId,
|
||||
schoolId: $schoolId,
|
||||
name: $name,
|
||||
code: $code,
|
||||
color: $color,
|
||||
createdAt: $createdAt,
|
||||
);
|
||||
|
||||
self::assertTrue($subject->tenantId->equals($tenantId));
|
||||
self::assertTrue($subject->schoolId->equals($schoolId));
|
||||
self::assertTrue($subject->name->equals($name));
|
||||
self::assertTrue($subject->code->equals($code));
|
||||
self::assertNotNull($subject->color);
|
||||
self::assertTrue($subject->color->equals($color));
|
||||
self::assertEquals($createdAt, $subject->createdAt);
|
||||
self::assertEquals($createdAt, $subject->updatedAt);
|
||||
self::assertNull($subject->deletedAt);
|
||||
self::assertNull($subject->description);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function creerWithNullColor(): void
|
||||
{
|
||||
$subject = Subject::creer(
|
||||
tenantId: TenantId::fromString(self::TENANT_ID),
|
||||
schoolId: SchoolId::fromString(self::SCHOOL_ID),
|
||||
name: new SubjectName('Arts plastiques'),
|
||||
code: new SubjectCode('ART'),
|
||||
color: null,
|
||||
createdAt: new DateTimeImmutable(),
|
||||
);
|
||||
|
||||
self::assertNull($subject->color);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function renommerChangesNameAndRecordsEvent(): void
|
||||
{
|
||||
$subject = $this->createSubject();
|
||||
$subject->pullDomainEvents();
|
||||
$ancienNom = $subject->name;
|
||||
$nouveauNom = new SubjectName('Mathématiques avancées');
|
||||
$at = new DateTimeImmutable('2026-02-01 10:00:00');
|
||||
|
||||
$subject->renommer($nouveauNom, $at);
|
||||
|
||||
self::assertTrue($subject->name->equals($nouveauNom));
|
||||
self::assertEquals($at, $subject->updatedAt);
|
||||
|
||||
$events = $subject->pullDomainEvents();
|
||||
self::assertCount(1, $events);
|
||||
self::assertInstanceOf(MatiereModifiee::class, $events[0]);
|
||||
self::assertTrue($events[0]->ancienNom->equals($ancienNom));
|
||||
self::assertTrue($events[0]->nouveauNom->equals($nouveauNom));
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function renommerWithSameNameDoesNothing(): void
|
||||
{
|
||||
$subject = $this->createSubject();
|
||||
$subject->pullDomainEvents();
|
||||
$originalUpdatedAt = $subject->updatedAt;
|
||||
|
||||
$subject->renommer(new SubjectName('Mathématiques'), new DateTimeImmutable('2026-02-01 10:00:00'));
|
||||
|
||||
self::assertEquals($originalUpdatedAt, $subject->updatedAt);
|
||||
self::assertEmpty($subject->pullDomainEvents());
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function changerCodeUpdatesCode(): void
|
||||
{
|
||||
$subject = $this->createSubject();
|
||||
$at = new DateTimeImmutable('2026-02-01 10:00:00');
|
||||
$nouveauCode = new SubjectCode('MATHS');
|
||||
|
||||
$subject->changerCode($nouveauCode, $at);
|
||||
|
||||
self::assertTrue($subject->code->equals($nouveauCode));
|
||||
self::assertEquals($at, $subject->updatedAt);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function changerCodeWithSameCodeDoesNothing(): void
|
||||
{
|
||||
$subject = $this->createSubject();
|
||||
$originalUpdatedAt = $subject->updatedAt;
|
||||
|
||||
$subject->changerCode(new SubjectCode('MATH'), new DateTimeImmutable('2026-02-01 10:00:00'));
|
||||
|
||||
self::assertEquals($originalUpdatedAt, $subject->updatedAt);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function changerCouleurUpdatesColor(): void
|
||||
{
|
||||
$subject = $this->createSubject();
|
||||
$at = new DateTimeImmutable('2026-02-01 10:00:00');
|
||||
$nouvelleCouleur = new SubjectColor('#EF4444');
|
||||
|
||||
$subject->changerCouleur($nouvelleCouleur, $at);
|
||||
|
||||
self::assertNotNull($subject->color);
|
||||
self::assertTrue($subject->color->equals($nouvelleCouleur));
|
||||
self::assertEquals($at, $subject->updatedAt);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function changerCouleurToNullRemovesColor(): void
|
||||
{
|
||||
$subject = $this->createSubject();
|
||||
$at = new DateTimeImmutable('2026-02-01 10:00:00');
|
||||
|
||||
$subject->changerCouleur(null, $at);
|
||||
|
||||
self::assertNull($subject->color);
|
||||
self::assertEquals($at, $subject->updatedAt);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function changerCouleurWithSameColorDoesNothing(): void
|
||||
{
|
||||
$subject = $this->createSubject();
|
||||
$originalUpdatedAt = $subject->updatedAt;
|
||||
|
||||
$subject->changerCouleur(new SubjectColor('#3B82F6'), new DateTimeImmutable('2026-02-01 10:00:00'));
|
||||
|
||||
self::assertEquals($originalUpdatedAt, $subject->updatedAt);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function decrireUpdatesDescription(): void
|
||||
{
|
||||
$subject = $this->createSubject();
|
||||
$at = new DateTimeImmutable('2026-02-01 10:00:00');
|
||||
|
||||
$subject->decrire('Cours de mathématiques généralistes', $at);
|
||||
|
||||
self::assertSame('Cours de mathématiques généralistes', $subject->description);
|
||||
self::assertEquals($at, $subject->updatedAt);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function archiverChangesStatusAndRecordsEvent(): void
|
||||
{
|
||||
$subject = $this->createSubject();
|
||||
$subject->pullDomainEvents();
|
||||
$at = new DateTimeImmutable('2026-02-01 10:00:00');
|
||||
|
||||
$subject->archiver($at);
|
||||
|
||||
self::assertSame(SubjectStatus::ARCHIVED, $subject->status);
|
||||
self::assertFalse($subject->estActive());
|
||||
self::assertFalse($subject->peutEtreUtilisee());
|
||||
self::assertEquals($at, $subject->deletedAt);
|
||||
self::assertEquals($at, $subject->updatedAt);
|
||||
|
||||
$events = $subject->pullDomainEvents();
|
||||
self::assertCount(1, $events);
|
||||
self::assertInstanceOf(MatiereSupprimee::class, $events[0]);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function archiverAlreadyArchivedSubjectDoesNothing(): void
|
||||
{
|
||||
$subject = $this->createSubject();
|
||||
$subject->archiver(new DateTimeImmutable('2026-02-01 10:00:00'));
|
||||
$subject->pullDomainEvents();
|
||||
$originalDeletedAt = $subject->deletedAt;
|
||||
|
||||
$subject->archiver(new DateTimeImmutable('2026-02-02 10:00:00'));
|
||||
|
||||
self::assertEquals($originalDeletedAt, $subject->deletedAt);
|
||||
self::assertEmpty($subject->pullDomainEvents());
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function reconstituteRestoresAllProperties(): void
|
||||
{
|
||||
$id = SubjectId::generate();
|
||||
$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');
|
||||
$status = SubjectStatus::ARCHIVED;
|
||||
$description = 'Test description';
|
||||
$createdAt = new DateTimeImmutable('2026-01-15 10:00:00');
|
||||
$updatedAt = new DateTimeImmutable('2026-02-01 10:00:00');
|
||||
$deletedAt = new DateTimeImmutable('2026-02-01 10:00:00');
|
||||
|
||||
$subject = Subject::reconstitute(
|
||||
id: $id,
|
||||
tenantId: $tenantId,
|
||||
schoolId: $schoolId,
|
||||
name: $name,
|
||||
code: $code,
|
||||
color: $color,
|
||||
status: $status,
|
||||
description: $description,
|
||||
createdAt: $createdAt,
|
||||
updatedAt: $updatedAt,
|
||||
deletedAt: $deletedAt,
|
||||
);
|
||||
|
||||
self::assertTrue($subject->id->equals($id));
|
||||
self::assertTrue($subject->tenantId->equals($tenantId));
|
||||
self::assertTrue($subject->schoolId->equals($schoolId));
|
||||
self::assertTrue($subject->name->equals($name));
|
||||
self::assertTrue($subject->code->equals($code));
|
||||
self::assertNotNull($subject->color);
|
||||
self::assertTrue($subject->color->equals($color));
|
||||
self::assertSame($status, $subject->status);
|
||||
self::assertSame($description, $subject->description);
|
||||
self::assertEquals($createdAt, $subject->createdAt);
|
||||
self::assertEquals($updatedAt, $subject->updatedAt);
|
||||
self::assertEquals($deletedAt, $subject->deletedAt);
|
||||
self::assertEmpty($subject->pullDomainEvents());
|
||||
}
|
||||
|
||||
private function createSubject(): Subject
|
||||
{
|
||||
return 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'),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,220 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Tests\Unit\Administration\Infrastructure\Persistence\InMemory;
|
||||
|
||||
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\SubjectId;
|
||||
use App\Administration\Domain\Model\Subject\SubjectName;
|
||||
use App\Administration\Infrastructure\Persistence\InMemory\InMemorySubjectRepository;
|
||||
use App\Shared\Domain\Tenant\TenantId;
|
||||
use DateTimeImmutable;
|
||||
use PHPUnit\Framework\Attributes\Test;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
final class InMemorySubjectRepositoryTest extends TestCase
|
||||
{
|
||||
private const string TENANT_ID = '550e8400-e29b-41d4-a716-446655440001';
|
||||
private const string SCHOOL_ID = '550e8400-e29b-41d4-a716-446655440002';
|
||||
|
||||
private InMemorySubjectRepository $repository;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
$this->repository = new InMemorySubjectRepository();
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function saveAndFindById(): void
|
||||
{
|
||||
$subject = $this->createSubject();
|
||||
|
||||
$this->repository->save($subject);
|
||||
$found = $this->repository->findById($subject->id);
|
||||
|
||||
self::assertNotNull($found);
|
||||
self::assertTrue($subject->id->equals($found->id));
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function getReturnsSubject(): void
|
||||
{
|
||||
$subject = $this->createSubject();
|
||||
$this->repository->save($subject);
|
||||
|
||||
$found = $this->repository->get($subject->id);
|
||||
|
||||
self::assertTrue($subject->id->equals($found->id));
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function getThrowsExceptionWhenNotFound(): void
|
||||
{
|
||||
$this->expectException(SubjectNotFoundException::class);
|
||||
|
||||
$this->repository->get(SubjectId::fromString('550e8400-e29b-41d4-a716-446655440099'));
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function findByIdReturnsNullWhenNotFound(): void
|
||||
{
|
||||
$found = $this->repository->findById(
|
||||
SubjectId::fromString('550e8400-e29b-41d4-a716-446655440099'),
|
||||
);
|
||||
|
||||
self::assertNull($found);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function findByCode(): void
|
||||
{
|
||||
$subject = $this->createSubject();
|
||||
$this->repository->save($subject);
|
||||
|
||||
$found = $this->repository->findByCode(
|
||||
$subject->code,
|
||||
$subject->tenantId,
|
||||
$subject->schoolId,
|
||||
);
|
||||
|
||||
self::assertNotNull($found);
|
||||
self::assertTrue($subject->id->equals($found->id));
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function findByCodeReturnsNullWhenNotFound(): void
|
||||
{
|
||||
$found = $this->repository->findByCode(
|
||||
new SubjectCode('NOTEXIST'),
|
||||
TenantId::fromString(self::TENANT_ID),
|
||||
SchoolId::fromString(self::SCHOOL_ID),
|
||||
);
|
||||
|
||||
self::assertNull($found);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function findByCodeIsTenantScoped(): void
|
||||
{
|
||||
$subject = $this->createSubject();
|
||||
$this->repository->save($subject);
|
||||
|
||||
// Same code, different tenant
|
||||
$found = $this->repository->findByCode(
|
||||
$subject->code,
|
||||
TenantId::fromString('550e8400-e29b-41d4-a716-446655440099'),
|
||||
$subject->schoolId,
|
||||
);
|
||||
|
||||
self::assertNull($found);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function findByCodeIsSchoolScoped(): void
|
||||
{
|
||||
$subject = $this->createSubject();
|
||||
$this->repository->save($subject);
|
||||
|
||||
// Same code, different school
|
||||
$found = $this->repository->findByCode(
|
||||
$subject->code,
|
||||
$subject->tenantId,
|
||||
SchoolId::fromString('550e8400-e29b-41d4-a716-446655440099'),
|
||||
);
|
||||
|
||||
self::assertNull($found);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function findActiveByTenantAndSchool(): void
|
||||
{
|
||||
// Create 2 active subjects
|
||||
$subject1 = $this->createSubject();
|
||||
$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'),
|
||||
);
|
||||
|
||||
// Create 1 archived subject
|
||||
$subject3 = Subject::creer(
|
||||
tenantId: TenantId::fromString(self::TENANT_ID),
|
||||
schoolId: SchoolId::fromString(self::SCHOOL_ID),
|
||||
name: new SubjectName('Latin'),
|
||||
code: new SubjectCode('LAT'),
|
||||
color: null,
|
||||
createdAt: new DateTimeImmutable('2026-01-15 10:00:00'),
|
||||
);
|
||||
$subject3->archiver(new DateTimeImmutable());
|
||||
|
||||
$this->repository->save($subject1);
|
||||
$this->repository->save($subject2);
|
||||
$this->repository->save($subject3);
|
||||
|
||||
$activeSubjects = $this->repository->findActiveByTenantAndSchool(
|
||||
TenantId::fromString(self::TENANT_ID),
|
||||
SchoolId::fromString(self::SCHOOL_ID),
|
||||
);
|
||||
|
||||
self::assertCount(2, $activeSubjects);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function delete(): void
|
||||
{
|
||||
$subject = $this->createSubject();
|
||||
$this->repository->save($subject);
|
||||
|
||||
$this->repository->delete($subject->id);
|
||||
|
||||
self::assertNull($this->repository->findById($subject->id));
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function saveUpdatesCodeIndex(): void
|
||||
{
|
||||
$subject = $this->createSubject();
|
||||
$this->repository->save($subject);
|
||||
|
||||
// Change code
|
||||
$subject->changerCode(new SubjectCode('MATHS'), new DateTimeImmutable());
|
||||
$this->repository->save($subject);
|
||||
|
||||
// Old code should not find anything
|
||||
$foundOld = $this->repository->findByCode(
|
||||
new SubjectCode('MATH'),
|
||||
$subject->tenantId,
|
||||
$subject->schoolId,
|
||||
);
|
||||
self::assertNull($foundOld);
|
||||
|
||||
// New code should find it
|
||||
$foundNew = $this->repository->findByCode(
|
||||
new SubjectCode('MATHS'),
|
||||
$subject->tenantId,
|
||||
$subject->schoolId,
|
||||
);
|
||||
self::assertNotNull($foundNew);
|
||||
self::assertTrue($subject->id->equals($foundNew->id));
|
||||
}
|
||||
|
||||
private function createSubject(): Subject
|
||||
{
|
||||
return 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'),
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user