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,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