feat: Permettre l'import d'élèves via fichier CSV ou XLSX

L'import manuel élève par élève est fastidieux pour les établissements
qui gèrent des centaines d'élèves. Un wizard d'import en 4 étapes
(upload → mapping → preview → confirmation) permet de traiter un
fichier complet en une seule opération, avec détection automatique
du format (Pronote, École Directe) et validation avant import.

L'import est traité de manière asynchrone via Messenger pour ne pas
bloquer l'interface, avec suivi de progression en temps réel et
réutilisation des mappings entre imports successifs.
This commit is contained in:
2026-02-25 16:51:13 +01:00
parent 560b941821
commit 2420e35492
62 changed files with 7510 additions and 86 deletions

View File

@@ -0,0 +1,293 @@
<?php
declare(strict_types=1);
namespace App\Tests\Unit\Administration\Application\Command\ImportStudents;
use App\Administration\Application\Command\ImportStudents\ImportStudentsCommand;
use App\Administration\Application\Command\ImportStudents\ImportStudentsHandler;
use App\Administration\Domain\Model\Import\ColumnMapping;
use App\Administration\Domain\Model\Import\ImportRow;
use App\Administration\Domain\Model\Import\ImportStatus;
use App\Administration\Domain\Model\Import\KnownImportFormat;
use App\Administration\Domain\Model\Import\StudentImportBatch;
use App\Administration\Domain\Model\Import\StudentImportField;
use App\Administration\Domain\Model\SchoolClass\AcademicYearId;
use App\Administration\Domain\Model\SchoolClass\ClassName;
use App\Administration\Domain\Model\SchoolClass\SchoolClass;
use App\Administration\Domain\Model\SchoolClass\SchoolId;
use App\Administration\Infrastructure\Persistence\InMemory\InMemoryClassAssignmentRepository;
use App\Administration\Infrastructure\Persistence\InMemory\InMemoryClassRepository;
use App\Administration\Infrastructure\Persistence\InMemory\InMemoryImportBatchRepository;
use App\Administration\Infrastructure\Persistence\InMemory\InMemoryUserRepository;
use App\Administration\Infrastructure\School\SchoolIdResolver;
use App\Shared\Domain\Clock;
use App\Shared\Domain\Tenant\TenantId;
use function count;
use DateTimeImmutable;
use Doctrine\DBAL\Connection;
use PHPUnit\Framework\Attributes\Test;
use PHPUnit\Framework\TestCase;
use Psr\Log\NullLogger;
final class ImportStudentsHandlerTest extends TestCase
{
private const string TENANT_ID = '550e8400-e29b-41d4-a716-446655440001';
private const string ACADEMIC_YEAR_ID = '550e8400-e29b-41d4-a716-446655440003';
private InMemoryImportBatchRepository $importBatchRepository;
private InMemoryUserRepository $userRepository;
private InMemoryClassRepository $classRepository;
private InMemoryClassAssignmentRepository $classAssignmentRepository;
private ImportStudentsHandler $handler;
private TenantId $tenantId;
private AcademicYearId $academicYearId;
protected function setUp(): void
{
$clock = new class implements Clock {
public function now(): DateTimeImmutable
{
return new DateTimeImmutable('2026-02-24 10:00:00');
}
};
$this->tenantId = TenantId::fromString(self::TENANT_ID);
$this->academicYearId = AcademicYearId::fromString(self::ACADEMIC_YEAR_ID);
$this->importBatchRepository = new InMemoryImportBatchRepository();
$this->userRepository = new InMemoryUserRepository();
$this->classRepository = new InMemoryClassRepository();
$this->classAssignmentRepository = new InMemoryClassAssignmentRepository();
$connection = $this->createMock(Connection::class);
$this->handler = new ImportStudentsHandler(
$this->importBatchRepository,
$this->userRepository,
$this->classRepository,
$this->classAssignmentRepository,
new SchoolIdResolver(),
$connection,
$clock,
new NullLogger(),
);
}
#[Test]
public function importsStudentsWithExistingClasses(): void
{
$class = $this->createClass('6ème A');
$batch = $this->createBatchWithRows([
$this->createMappedRow(1, 'Dupont', 'Jean', '6ème A'),
$this->createMappedRow(2, 'Martin', 'Marie', '6ème A'),
]);
($this->handler)(new ImportStudentsCommand(
batchId: (string) $batch->id,
tenantId: self::TENANT_ID,
schoolName: 'École Test',
academicYearId: self::ACADEMIC_YEAR_ID,
));
$updatedBatch = $this->importBatchRepository->get($batch->id);
self::assertSame(ImportStatus::COMPLETED, $updatedBatch->status);
self::assertSame(2, $updatedBatch->importedCount);
self::assertSame(0, $updatedBatch->errorCount);
}
#[Test]
public function importsStudentsWithEmail(): void
{
$this->createClass('6ème A');
$batch = $this->createBatchWithRows([
$this->createMappedRow(1, 'Dupont', 'Jean', '6ème A', 'jean@test.com'),
]);
($this->handler)(new ImportStudentsCommand(
batchId: (string) $batch->id,
tenantId: self::TENANT_ID,
schoolName: 'École Test',
academicYearId: self::ACADEMIC_YEAR_ID,
));
$students = $this->userRepository->findStudentsByTenant($this->tenantId);
self::assertCount(1, $students);
self::assertNotNull($students[0]->email);
}
#[Test]
public function importsStudentsWithoutEmail(): void
{
$this->createClass('6ème A');
$batch = $this->createBatchWithRows([
$this->createMappedRow(1, 'Dupont', 'Jean', '6ème A'),
]);
($this->handler)(new ImportStudentsCommand(
batchId: (string) $batch->id,
tenantId: self::TENANT_ID,
schoolName: 'École Test',
academicYearId: self::ACADEMIC_YEAR_ID,
));
$students = $this->userRepository->findStudentsByTenant($this->tenantId);
self::assertCount(1, $students);
self::assertNull($students[0]->email);
}
#[Test]
public function createsMissingClassesWhenEnabled(): void
{
$batch = $this->createBatchWithRows([
$this->createMappedRow(1, 'Dupont', 'Jean', '6ème A'),
]);
($this->handler)(new ImportStudentsCommand(
batchId: (string) $batch->id,
tenantId: self::TENANT_ID,
schoolName: 'École Test',
academicYearId: self::ACADEMIC_YEAR_ID,
createMissingClasses: true,
));
$updatedBatch = $this->importBatchRepository->get($batch->id);
self::assertSame(ImportStatus::COMPLETED, $updatedBatch->status);
self::assertSame(1, $updatedBatch->importedCount);
$createdClass = $this->classRepository->findByName(
new ClassName('6ème A'),
$this->tenantId,
$this->academicYearId,
);
self::assertNotNull($createdClass);
}
#[Test]
public function countsErrorsForMissingClassesWhenNotEnabled(): void
{
$batch = $this->createBatchWithRows([
$this->createMappedRow(1, 'Dupont', 'Jean', '6ème A'),
$this->createMappedRow(2, 'Martin', 'Marie', '5ème B'),
]);
($this->handler)(new ImportStudentsCommand(
batchId: (string) $batch->id,
tenantId: self::TENANT_ID,
schoolName: 'École Test',
academicYearId: self::ACADEMIC_YEAR_ID,
createMissingClasses: false,
));
$updatedBatch = $this->importBatchRepository->get($batch->id);
self::assertSame(ImportStatus::COMPLETED, $updatedBatch->status);
self::assertSame(0, $updatedBatch->importedCount);
self::assertSame(2, $updatedBatch->errorCount);
}
#[Test]
public function createsClassAssignments(): void
{
$class = $this->createClass('6ème A');
$batch = $this->createBatchWithRows([
$this->createMappedRow(1, 'Dupont', 'Jean', '6ème A'),
]);
($this->handler)(new ImportStudentsCommand(
batchId: (string) $batch->id,
tenantId: self::TENANT_ID,
schoolName: 'École Test',
academicYearId: self::ACADEMIC_YEAR_ID,
));
$students = $this->userRepository->findStudentsByTenant($this->tenantId);
self::assertCount(1, $students);
$assignment = $this->classAssignmentRepository->findByStudent(
$students[0]->id,
$this->academicYearId,
$this->tenantId,
);
self::assertNotNull($assignment);
self::assertTrue($assignment->classId->equals($class->id));
}
private function createClass(string $name): SchoolClass
{
$class = SchoolClass::creer(
tenantId: $this->tenantId,
schoolId: SchoolId::generate(),
academicYearId: $this->academicYearId,
name: new ClassName($name),
level: null,
capacity: null,
createdAt: new DateTimeImmutable('2026-01-01'),
);
$this->classRepository->save($class);
return $class;
}
/**
* @param list<ImportRow> $rows
*/
private function createBatchWithRows(array $rows): StudentImportBatch
{
$batch = StudentImportBatch::creer(
tenantId: $this->tenantId,
originalFilename: 'test.csv',
totalRows: count($rows),
detectedColumns: ['Nom', 'Prénom', 'Classe'],
detectedFormat: KnownImportFormat::CUSTOM,
createdAt: new DateTimeImmutable('2026-02-24 09:00:00'),
);
$mapping = ColumnMapping::creer(
[
'Nom' => StudentImportField::LAST_NAME,
'Prénom' => StudentImportField::FIRST_NAME,
'Classe' => StudentImportField::CLASS_NAME,
'Email' => StudentImportField::EMAIL,
],
KnownImportFormat::CUSTOM,
);
$batch->appliquerMapping($mapping);
$batch->enregistrerLignes($rows);
$this->importBatchRepository->save($batch);
return $batch;
}
private function createMappedRow(
int $line,
string $lastName,
string $firstName,
string $className,
?string $email = null,
): ImportRow {
$mappedData = [
'lastName' => $lastName,
'firstName' => $firstName,
'className' => $className,
];
if ($email !== null) {
$mappedData['email'] = $email;
}
return new ImportRow(
lineNumber: $line,
rawData: $mappedData,
mappedData: $mappedData,
);
}
}

View File

@@ -0,0 +1,106 @@
<?php
declare(strict_types=1);
namespace App\Tests\Unit\Administration\Application\Service\Import;
use App\Administration\Application\Service\Import\ColumnMappingSuggester;
use App\Administration\Domain\Model\Import\KnownImportFormat;
use App\Administration\Domain\Model\Import\StudentImportField;
use function count;
use PHPUnit\Framework\Attributes\Test;
use PHPUnit\Framework\TestCase;
use const SORT_REGULAR;
final class ColumnMappingSuggesterTest extends TestCase
{
private ColumnMappingSuggester $suggester;
protected function setUp(): void
{
$this->suggester = new ColumnMappingSuggester();
}
#[Test]
public function suggestPronoteMapping(): void
{
$columns = ['Élèves', 'Né(e) le', 'Sexe', 'Adresse E-mail', 'Classe de rattachement'];
$mapping = $this->suggester->suggerer($columns, KnownImportFormat::PRONOTE);
self::assertSame(StudentImportField::FULL_NAME, $mapping['Élèves']);
self::assertSame(StudentImportField::BIRTH_DATE, $mapping['Né(e) le']);
self::assertSame(StudentImportField::GENDER, $mapping['Sexe']);
self::assertSame(StudentImportField::EMAIL, $mapping['Adresse E-mail']);
self::assertSame(StudentImportField::CLASS_NAME, $mapping['Classe de rattachement']);
}
#[Test]
public function suggestEcoleDirecteMapping(): void
{
$columns = ['NOM', 'PRENOM', 'CLASSE', 'DATE_NAISSANCE', 'SEXE'];
$mapping = $this->suggester->suggerer($columns, KnownImportFormat::ECOLE_DIRECTE);
self::assertSame(StudentImportField::LAST_NAME, $mapping['NOM']);
self::assertSame(StudentImportField::FIRST_NAME, $mapping['PRENOM']);
self::assertSame(StudentImportField::CLASS_NAME, $mapping['CLASSE']);
self::assertSame(StudentImportField::BIRTH_DATE, $mapping['DATE_NAISSANCE']);
self::assertSame(StudentImportField::GENDER, $mapping['SEXE']);
}
#[Test]
public function suggestGenericMappingByKeywords(): void
{
$columns = ['Nom', 'Prénom', 'Classe', 'Email'];
$mapping = $this->suggester->suggerer($columns, KnownImportFormat::CUSTOM);
self::assertSame(StudentImportField::LAST_NAME, $mapping['Nom']);
self::assertSame(StudentImportField::FIRST_NAME, $mapping['Prénom']);
self::assertSame(StudentImportField::CLASS_NAME, $mapping['Classe']);
self::assertSame(StudentImportField::EMAIL, $mapping['Email']);
}
#[Test]
public function suggestDoesNotDuplicateFields(): void
{
$columns = ['Nom', 'Nom de famille', 'Prénom', 'Classe'];
$mapping = $this->suggester->suggerer($columns, KnownImportFormat::CUSTOM);
$mappedFields = array_values($mapping);
$uniqueFields = array_unique($mappedFields, SORT_REGULAR);
self::assertCount(count($uniqueFields), $mappedFields);
}
#[Test]
public function suggestHandlesUnknownColumns(): void
{
$columns = ['ColonneInconnue', 'AutreColonne', 'Nom', 'Classe'];
$mapping = $this->suggester->suggerer($columns, KnownImportFormat::CUSTOM);
self::assertArrayNotHasKey('ColonneInconnue', $mapping);
self::assertArrayNotHasKey('AutreColonne', $mapping);
self::assertArrayHasKey('Nom', $mapping);
self::assertArrayHasKey('Classe', $mapping);
}
#[Test]
public function suggestHandlesEnglishColumnNames(): void
{
$columns = ['Last Name', 'First Name', 'Class', 'Email'];
$mapping = $this->suggester->suggerer($columns, KnownImportFormat::CUSTOM);
self::assertSame(StudentImportField::LAST_NAME, $mapping['Last Name']);
self::assertSame(StudentImportField::FIRST_NAME, $mapping['First Name']);
self::assertSame(StudentImportField::CLASS_NAME, $mapping['Class']);
self::assertSame(StudentImportField::EMAIL, $mapping['Email']);
}
}

View File

@@ -0,0 +1,121 @@
<?php
declare(strict_types=1);
namespace App\Tests\Unit\Administration\Application\Service\Import;
use App\Administration\Application\Service\Import\CsvParser;
use App\Administration\Domain\Exception\FichierImportInvalideException;
use PHPUnit\Framework\Attributes\Test;
use PHPUnit\Framework\TestCase;
final class CsvParserTest extends TestCase
{
private CsvParser $parser;
protected function setUp(): void
{
$this->parser = new CsvParser();
}
#[Test]
public function parseSemicolonSeparatedCsv(): void
{
$result = $this->parser->parse($this->fixture('eleves_simple.csv'));
self::assertSame(['Nom', 'Prénom', 'Classe', 'Email'], $result->columns);
self::assertSame(3, $result->totalRows());
self::assertSame('Dupont', $result->rows[0]['Nom']);
self::assertSame('Jean', $result->rows[0]['Prénom']);
self::assertSame('6ème A', $result->rows[0]['Classe']);
self::assertSame('jean.dupont@email.com', $result->rows[0]['Email']);
}
#[Test]
public function parseCommaSeparatedCsv(): void
{
$result = $this->parser->parse($this->fixture('eleves_comma.csv'));
self::assertSame(['Nom', 'Prénom', 'Classe'], $result->columns);
self::assertSame(2, $result->totalRows());
self::assertSame('Dupont', $result->rows[0]['Nom']);
}
#[Test]
public function parsePronoteFormatCsv(): void
{
$result = $this->parser->parse($this->fixture('eleves_pronote.csv'));
self::assertContains('Élèves', $result->columns);
self::assertContains('Né(e) le', $result->columns);
self::assertContains('Sexe', $result->columns);
self::assertSame(27, $result->totalRows());
self::assertSame('BERTHE Alexandre', $result->rows[0]['Élèves']);
self::assertSame('07/07/2011', $result->rows[0]['Né(e) le']);
self::assertSame('Masculin', $result->rows[0]['Sexe']);
self::assertSame('alexandre.berthe@fournisseur.fr', $result->rows[0]['Adresse E-mail']);
}
#[Test]
public function previewReturnsLimitedRows(): void
{
$result = $this->parser->parse($this->fixture('eleves_simple.csv'));
$preview = $result->preview(2);
self::assertCount(2, $preview);
self::assertSame('Dupont', $preview[0]['Nom']);
self::assertSame('Martin', $preview[1]['Nom']);
}
#[Test]
public function parseHandlesUtf8Bom(): void
{
$tempFile = tempnam(sys_get_temp_dir(), 'csv_');
file_put_contents($tempFile, "\xEF\xBB\xBFNom;Prénom\nDupont;Jean\n");
try {
$result = $this->parser->parse($tempFile);
self::assertSame(['Nom', 'Prénom'], $result->columns);
self::assertSame(1, $result->totalRows());
} finally {
unlink($tempFile);
}
}
#[Test]
public function parseEmptyFileThrowsException(): void
{
$tempFile = tempnam(sys_get_temp_dir(), 'csv_');
file_put_contents($tempFile, '');
try {
$this->expectException(FichierImportInvalideException::class);
$this->parser->parse($tempFile);
} finally {
unlink($tempFile);
}
}
#[Test]
public function parseHandlesEmptyRowsGracefully(): void
{
$tempFile = tempnam(sys_get_temp_dir(), 'csv_');
file_put_contents($tempFile, "Nom;Prénom\nDupont;Jean\n\n\nMartin;Marie\n");
try {
$result = $this->parser->parse($tempFile);
self::assertSame(2, $result->totalRows());
} finally {
unlink($tempFile);
}
}
private function fixture(string $filename): string
{
return __DIR__ . '/../../../../../fixtures/import/' . $filename;
}
}

View File

@@ -0,0 +1,70 @@
<?php
declare(strict_types=1);
namespace App\Tests\Unit\Administration\Application\Service\Import;
use App\Administration\Application\Service\Import\ImportFormatDetector;
use App\Administration\Domain\Model\Import\KnownImportFormat;
use PHPUnit\Framework\Attributes\Test;
use PHPUnit\Framework\TestCase;
final class ImportFormatDetectorTest extends TestCase
{
private ImportFormatDetector $detector;
protected function setUp(): void
{
$this->detector = new ImportFormatDetector();
}
#[Test]
public function detectsPronoteFormat(): void
{
$columns = ['Élèves', 'Encouragement/Valorisation', 'Né(e) le', 'Sexe', 'Adresse E-mail', 'Entrée', 'Sortie', 'Classe de rattachement'];
$format = $this->detector->detecter($columns);
self::assertSame(KnownImportFormat::PRONOTE, $format);
}
#[Test]
public function detectsEcoleDirecteFormat(): void
{
$columns = ['NOM', 'PRENOM', 'CLASSE', 'DATE_NAISSANCE', 'SEXE'];
$format = $this->detector->detecter($columns);
self::assertSame(KnownImportFormat::ECOLE_DIRECTE, $format);
}
#[Test]
public function detectsCustomFormatForUnknownColumns(): void
{
$columns = ['Nom', 'Prénom', 'Classe', 'Email'];
$format = $this->detector->detecter($columns);
self::assertSame(KnownImportFormat::CUSTOM, $format);
}
#[Test]
public function detectsPronoteWithPartialMatch(): void
{
$columns = ['Élèves', 'Né(e) le', 'Sexe', 'Autre colonne'];
$format = $this->detector->detecter($columns);
self::assertSame(KnownImportFormat::PRONOTE, $format);
}
#[Test]
public function detectsEcoleDirecteWithCaseVariations(): void
{
$columns = ['nom', 'prenom', 'classe', 'date_naissance'];
$format = $this->detector->detecter($columns);
self::assertSame(KnownImportFormat::ECOLE_DIRECTE, $format);
}
}

View File

@@ -0,0 +1,311 @@
<?php
declare(strict_types=1);
namespace App\Tests\Unit\Administration\Application\Service\Import;
use App\Administration\Application\Service\Import\ImportRowValidator;
use App\Administration\Domain\Model\Import\ImportRow;
use App\Administration\Domain\Model\Import\StudentImportField;
use PHPUnit\Framework\Attributes\Test;
use PHPUnit\Framework\TestCase;
final class ImportRowValidatorTest extends TestCase
{
#[Test]
public function validRowRemainsValid(): void
{
$validator = new ImportRowValidator();
$row = $this->createRow([
'lastName' => 'Dupont',
'firstName' => 'Jean',
'className' => '6ème A',
]);
$result = $validator->valider($row);
self::assertTrue($result->estValide());
}
#[Test]
public function missingLastNameAddsError(): void
{
$validator = new ImportRowValidator();
$row = $this->createRow([
'lastName' => '',
'firstName' => 'Jean',
'className' => '6ème A',
]);
$result = $validator->valider($row);
self::assertFalse($result->estValide());
self::assertCount(1, $result->errors);
self::assertSame('lastName', $result->errors[0]->column);
}
#[Test]
public function missingMultipleFieldsAddsMultipleErrors(): void
{
$validator = new ImportRowValidator();
$row = $this->createRow([
'lastName' => '',
'firstName' => '',
'className' => '',
]);
$result = $validator->valider($row);
self::assertFalse($result->estValide());
self::assertCount(3, $result->errors);
}
#[Test]
public function invalidEmailAddsError(): void
{
$validator = new ImportRowValidator();
$row = $this->createRow([
'lastName' => 'Dupont',
'firstName' => 'Jean',
'className' => '6ème A',
'email' => 'not-an-email',
]);
$result = $validator->valider($row);
self::assertFalse($result->estValide());
self::assertSame('email', $result->errors[0]->column);
}
#[Test]
public function validEmailPasses(): void
{
$validator = new ImportRowValidator();
$row = $this->createRow([
'lastName' => 'Dupont',
'firstName' => 'Jean',
'className' => '6ème A',
'email' => 'jean.dupont@email.com',
]);
$result = $validator->valider($row);
self::assertTrue($result->estValide());
}
#[Test]
public function emptyEmailIsAccepted(): void
{
$validator = new ImportRowValidator();
$row = $this->createRow([
'lastName' => 'Dupont',
'firstName' => 'Jean',
'className' => '6ème A',
'email' => '',
]);
$result = $validator->valider($row);
self::assertTrue($result->estValide());
}
#[Test]
public function invalidDateAddsError(): void
{
$validator = new ImportRowValidator();
$row = $this->createRow([
'lastName' => 'Dupont',
'firstName' => 'Jean',
'className' => '6ème A',
'birthDate' => 'not-a-date',
]);
$result = $validator->valider($row);
self::assertFalse($result->estValide());
self::assertSame('birthDate', $result->errors[0]->column);
}
#[Test]
public function frenchDateFormatAccepted(): void
{
$validator = new ImportRowValidator();
$row = $this->createRow([
'lastName' => 'Dupont',
'firstName' => 'Jean',
'className' => '6ème A',
'birthDate' => '15/03/2014',
]);
$result = $validator->valider($row);
self::assertTrue($result->estValide());
}
#[Test]
public function isoDateFormatAccepted(): void
{
$validator = new ImportRowValidator();
$row = $this->createRow([
'lastName' => 'Dupont',
'firstName' => 'Jean',
'className' => '6ème A',
'birthDate' => '2014-03-15',
]);
$result = $validator->valider($row);
self::assertTrue($result->estValide());
}
#[Test]
public function unknownClassAddsError(): void
{
$validator = new ImportRowValidator(['6ème A', '6ème B']);
$row = $this->createRow([
'lastName' => 'Dupont',
'firstName' => 'Jean',
'className' => '5ème C',
]);
$result = $validator->valider($row);
self::assertFalse($result->estValide());
self::assertSame('className', $result->errors[0]->column);
}
#[Test]
public function knownClassPasses(): void
{
$validator = new ImportRowValidator(['6ème A', '6ème B']);
$row = $this->createRow([
'lastName' => 'Dupont',
'firstName' => 'Jean',
'className' => '6ème A',
]);
$result = $validator->valider($row);
self::assertTrue($result->estValide());
}
#[Test]
public function classValidationSkippedWhenNoClassesProvided(): void
{
$validator = new ImportRowValidator();
$row = $this->createRow([
'lastName' => 'Dupont',
'firstName' => 'Jean',
'className' => 'ClasseInconnue',
]);
$result = $validator->valider($row);
self::assertTrue($result->estValide());
}
#[Test]
public function validerToutValidatesAllRows(): void
{
$validator = new ImportRowValidator();
$rows = [
$this->createRow(['lastName' => 'Dupont', 'firstName' => 'Jean', 'className' => '6A']),
$this->createRow(['lastName' => '', 'firstName' => 'Marie', 'className' => '6B']),
$this->createRow(['lastName' => 'Bernard', 'firstName' => 'Pierre', 'className' => '5A']),
];
$results = $validator->validerTout($rows);
self::assertCount(3, $results);
self::assertTrue($results[0]->estValide());
self::assertFalse($results[1]->estValide());
self::assertTrue($results[2]->estValide());
}
#[Test]
public function fullNameExpandsToLastNameAndFirstName(): void
{
$validator = new ImportRowValidator();
$row = $this->createRow([
'fullName' => 'BERTHE Alexandre',
'className' => '6ème A',
]);
$result = $validator->valider($row);
self::assertTrue($result->estValide());
self::assertSame('BERTHE', $result->valeurChamp(StudentImportField::LAST_NAME));
self::assertSame('Alexandre', $result->valeurChamp(StudentImportField::FIRST_NAME));
}
#[Test]
public function fullNameDoesNotOverrideExistingLastNameFirstName(): void
{
$validator = new ImportRowValidator();
$row = $this->createRow([
'fullName' => 'BERTHE Alexandre',
'lastName' => 'Dupont',
'firstName' => 'Jean',
'className' => '6ème A',
]);
$result = $validator->valider($row);
self::assertTrue($result->estValide());
self::assertSame('Dupont', $result->valeurChamp(StudentImportField::LAST_NAME));
self::assertSame('Jean', $result->valeurChamp(StudentImportField::FIRST_NAME));
}
#[Test]
public function splitFullNameHandlesCompoundLastName(): void
{
[$lastName, $firstName] = ImportRowValidator::splitFullName('DE LA FONTAINE Jean');
self::assertSame('DE LA FONTAINE', $lastName);
self::assertSame('Jean', $firstName);
}
#[Test]
public function splitFullNameHandlesSimpleName(): void
{
[$lastName, $firstName] = ImportRowValidator::splitFullName('DUPONT Marie');
self::assertSame('DUPONT', $lastName);
self::assertSame('Marie', $firstName);
}
#[Test]
public function splitFullNameHandlesCompoundFirstName(): void
{
[$lastName, $firstName] = ImportRowValidator::splitFullName('OLIVIER Jean-Philippe');
self::assertSame('OLIVIER', $lastName);
self::assertSame('Jean-Philippe', $firstName);
}
#[Test]
public function emptyFullNameDoesNotExpand(): void
{
$validator = new ImportRowValidator();
$row = $this->createRow([
'fullName' => '',
'className' => '6ème A',
]);
$result = $validator->valider($row);
self::assertFalse($result->estValide());
}
/**
* @param array<string, string> $mappedData
*/
private function createRow(array $mappedData, int $lineNumber = 1): ImportRow
{
return new ImportRow(
lineNumber: $lineNumber,
rawData: $mappedData,
mappedData: $mappedData,
);
}
}

View File

@@ -0,0 +1,153 @@
<?php
declare(strict_types=1);
namespace App\Tests\Unit\Administration\Application\Service\Import;
use App\Administration\Application\Service\Import\ColumnMappingSuggester;
use App\Administration\Application\Service\Import\CsvParser;
use App\Administration\Application\Service\Import\ImportFormatDetector;
use App\Administration\Application\Service\Import\ImportReport;
use App\Administration\Application\Service\Import\ImportRowValidator;
use App\Administration\Domain\Model\Import\ImportRow;
use App\Administration\Domain\Model\Import\KnownImportFormat;
use App\Administration\Domain\Model\Import\StudentImportField;
use PHPUnit\Framework\Attributes\Test;
use PHPUnit\Framework\TestCase;
/**
* Test d'intégration de la chaîne complète d'import avec un vrai fichier Pronote.
*
* Parse → Détection format → Mapping → Validation → Rapport
*/
final class PronoteImportIntegrationTest extends TestCase
{
#[Test]
public function fullPronoteImportPipeline(): void
{
$filePath = __DIR__ . '/../../../../../fixtures/import/eleves_pronote.csv';
// 1. Parser le fichier
$parser = new CsvParser();
$parseResult = $parser->parse($filePath);
self::assertSame(27, $parseResult->totalRows());
self::assertContains('Élèves', $parseResult->columns);
// 2. Détecter le format
$detector = new ImportFormatDetector();
$format = $detector->detecter($parseResult->columns);
self::assertSame(KnownImportFormat::PRONOTE, $format);
// 3. Suggérer le mapping
$suggester = new ColumnMappingSuggester();
$suggestedMapping = $suggester->suggerer($parseResult->columns, $format);
self::assertSame(StudentImportField::FULL_NAME, $suggestedMapping['Élèves']);
self::assertSame(StudentImportField::BIRTH_DATE, $suggestedMapping['Né(e) le']);
self::assertSame(StudentImportField::GENDER, $suggestedMapping['Sexe']);
self::assertSame(StudentImportField::EMAIL, $suggestedMapping['Adresse E-mail']);
// 4. Appliquer le mapping sur les lignes
$rows = [];
$lineNumber = 1;
foreach ($parseResult->rows as $rawData) {
$mappedData = [];
foreach ($suggestedMapping as $column => $field) {
$mappedData[$field->value] = $rawData[$column] ?? '';
}
$rows[] = new ImportRow($lineNumber, $rawData, $mappedData);
++$lineNumber;
}
self::assertCount(27, $rows);
self::assertSame('BERTHE Alexandre', $rows[0]->valeurChamp(StudentImportField::FULL_NAME));
// 5. Valider les lignes (pas de vérification de classes existantes)
$validator = new ImportRowValidator();
$validatedRows = $validator->validerTout($rows);
// Toutes les lignes devraient être valides car FULL_NAME → LAST_NAME + FIRST_NAME
$validCount = 0;
$errorCount = 0;
foreach ($validatedRows as $row) {
if ($row->estValide()) {
++$validCount;
} else {
++$errorCount;
}
}
// Certaines lignes ont une classe vide → erreur sur className obligatoire
self::assertGreaterThan(0, $validCount + $errorCount);
self::assertSame(27, $validCount + $errorCount);
// Vérifions que le splitFullName a bien fonctionné sur la première ligne
$firstRow = $validatedRows[0];
self::assertSame('BERTHE', $firstRow->valeurChamp(StudentImportField::LAST_NAME));
self::assertSame('Alexandre', $firstRow->valeurChamp(StudentImportField::FIRST_NAME));
// Vérifions Jean-Philippe (prénom composé)
$jeanPhilippe = null;
foreach ($validatedRows as $row) {
if (str_contains($row->valeurChamp(StudentImportField::FULL_NAME) ?? '', 'OLIVIER')) {
$jeanPhilippe = $row;
break;
}
}
self::assertNotNull($jeanPhilippe);
self::assertSame('OLIVIER', $jeanPhilippe->valeurChamp(StudentImportField::LAST_NAME));
self::assertSame('Jean-Philippe', $jeanPhilippe->valeurChamp(StudentImportField::FIRST_NAME));
// 6. Générer le rapport
$report = ImportReport::fromValidatedRows($validatedRows);
self::assertSame(27, $report->totalRows);
self::assertSame($validCount, $report->importedCount);
self::assertSame($errorCount, $report->errorCount);
}
#[Test]
public function pronoteClasseDeRattachementIsEmpty(): void
{
$filePath = __DIR__ . '/../../../../../fixtures/import/eleves_pronote.csv';
$parser = new CsvParser();
$parseResult = $parser->parse($filePath);
// Dans le fichier Pronote de démo, "Classe de rattachement" est vide pour tous
foreach ($parseResult->rows as $row) {
self::assertSame('', $row['Classe de rattachement']);
}
}
#[Test]
public function pronoteRowsWithEmptyClassAreInvalid(): void
{
$validator = new ImportRowValidator();
$row = new ImportRow(
lineNumber: 1,
rawData: ['Élèves' => 'BERTHE Alexandre', 'Classe de rattachement' => ''],
mappedData: [
'fullName' => 'BERTHE Alexandre',
'className' => '',
'birthDate' => '07/07/2011',
'gender' => 'Masculin',
'email' => 'alexandre.berthe@fournisseur.fr',
],
);
$result = $validator->valider($row);
// className est obligatoire → erreur
self::assertFalse($result->estValide());
self::assertSame('className', $result->errors[0]->column);
// Mais le nom a bien été splitté
self::assertSame('BERTHE', $result->valeurChamp(StudentImportField::LAST_NAME));
self::assertSame('Alexandre', $result->valeurChamp(StudentImportField::FIRST_NAME));
}
}

View File

@@ -0,0 +1,129 @@
<?php
declare(strict_types=1);
namespace App\Tests\Unit\Administration\Domain\Model\Import;
use App\Administration\Domain\Exception\MappingIncompletException;
use App\Administration\Domain\Model\Import\ColumnMapping;
use App\Administration\Domain\Model\Import\KnownImportFormat;
use App\Administration\Domain\Model\Import\StudentImportField;
use PHPUnit\Framework\Attributes\Test;
use PHPUnit\Framework\TestCase;
final class ColumnMappingTest extends TestCase
{
#[Test]
public function creerWithAllRequiredFieldsSucceeds(): void
{
$mapping = ColumnMapping::creer(
[
'Nom' => StudentImportField::LAST_NAME,
'Prénom' => StudentImportField::FIRST_NAME,
'Classe' => StudentImportField::CLASS_NAME,
],
KnownImportFormat::CUSTOM,
);
self::assertCount(3, $mapping->colonnesSources());
self::assertSame(KnownImportFormat::CUSTOM, $mapping->format);
}
#[Test]
public function creerWithOptionalFieldsSucceeds(): void
{
$mapping = ColumnMapping::creer(
[
'Nom' => StudentImportField::LAST_NAME,
'Prénom' => StudentImportField::FIRST_NAME,
'Classe' => StudentImportField::CLASS_NAME,
'Email' => StudentImportField::EMAIL,
'Naissance' => StudentImportField::BIRTH_DATE,
],
KnownImportFormat::PRONOTE,
);
self::assertCount(5, $mapping->colonnesSources());
}
#[Test]
public function creerSansNomLeveException(): void
{
$this->expectException(MappingIncompletException::class);
ColumnMapping::creer(
[
'Prénom' => StudentImportField::FIRST_NAME,
'Classe' => StudentImportField::CLASS_NAME,
],
KnownImportFormat::CUSTOM,
);
}
#[Test]
public function creerSansPrenomLeveException(): void
{
$this->expectException(MappingIncompletException::class);
ColumnMapping::creer(
[
'Nom' => StudentImportField::LAST_NAME,
'Classe' => StudentImportField::CLASS_NAME,
],
KnownImportFormat::CUSTOM,
);
}
#[Test]
public function creerSansClasseLeveException(): void
{
$this->expectException(MappingIncompletException::class);
ColumnMapping::creer(
[
'Nom' => StudentImportField::LAST_NAME,
'Prénom' => StudentImportField::FIRST_NAME,
],
KnownImportFormat::CUSTOM,
);
}
#[Test]
public function champPourReturnsMappedField(): void
{
$mapping = ColumnMapping::creer(
[
'Nom' => StudentImportField::LAST_NAME,
'Prénom' => StudentImportField::FIRST_NAME,
'Classe' => StudentImportField::CLASS_NAME,
],
KnownImportFormat::CUSTOM,
);
self::assertSame(StudentImportField::LAST_NAME, $mapping->champPour('Nom'));
self::assertSame(StudentImportField::FIRST_NAME, $mapping->champPour('Prénom'));
self::assertNull($mapping->champPour('Inconnu'));
}
#[Test]
public function equalsComparesCorrectly(): void
{
$mapping1 = ColumnMapping::creer(
['Nom' => StudentImportField::LAST_NAME, 'Prénom' => StudentImportField::FIRST_NAME, 'Classe' => StudentImportField::CLASS_NAME],
KnownImportFormat::CUSTOM,
);
$mapping2 = ColumnMapping::creer(
['Nom' => StudentImportField::LAST_NAME, 'Prénom' => StudentImportField::FIRST_NAME, 'Classe' => StudentImportField::CLASS_NAME],
KnownImportFormat::CUSTOM,
);
$mapping3 = ColumnMapping::creer(
['Nom' => StudentImportField::LAST_NAME, 'Prénom' => StudentImportField::FIRST_NAME, 'Classe' => StudentImportField::CLASS_NAME],
KnownImportFormat::PRONOTE,
);
self::assertTrue($mapping1->equals($mapping2));
self::assertFalse($mapping1->equals($mapping3));
}
}

View File

@@ -0,0 +1,74 @@
<?php
declare(strict_types=1);
namespace App\Tests\Unit\Administration\Domain\Model\Import;
use App\Administration\Domain\Model\Import\ImportRow;
use App\Administration\Domain\Model\Import\ImportRowError;
use App\Administration\Domain\Model\Import\StudentImportField;
use PHPUnit\Framework\Attributes\Test;
use PHPUnit\Framework\TestCase;
final class ImportRowTest extends TestCase
{
#[Test]
public function rowSansErreurEstValide(): void
{
$row = new ImportRow(
lineNumber: 1,
rawData: ['Nom' => 'Dupont', 'Prénom' => 'Jean'],
mappedData: ['lastName' => 'Dupont', 'firstName' => 'Jean'],
);
self::assertTrue($row->estValide());
self::assertSame(1, $row->lineNumber);
}
#[Test]
public function rowAvecErreursEstInvalide(): void
{
$row = new ImportRow(
lineNumber: 3,
rawData: ['Nom' => '', 'Prénom' => 'Jean'],
mappedData: ['lastName' => '', 'firstName' => 'Jean'],
errors: [new ImportRowError('lastName', 'Le nom est obligatoire')],
);
self::assertFalse($row->estValide());
self::assertCount(1, $row->errors);
}
#[Test]
public function valeurChampReturnsMappedValue(): void
{
$row = new ImportRow(
lineNumber: 1,
rawData: [],
mappedData: ['lastName' => 'Dupont', 'firstName' => 'Jean'],
);
self::assertSame('Dupont', $row->valeurChamp(StudentImportField::LAST_NAME));
self::assertSame('Jean', $row->valeurChamp(StudentImportField::FIRST_NAME));
self::assertNull($row->valeurChamp(StudentImportField::EMAIL));
}
#[Test]
public function avecErreursCreatesNewRowWithAdditionalErrors(): void
{
$row = new ImportRow(
lineNumber: 1,
rawData: [],
mappedData: ['lastName' => ''],
errors: [new ImportRowError('lastName', 'Le nom est obligatoire')],
);
$newRow = $row->avecErreurs(
new ImportRowError('email', 'Email invalide'),
);
self::assertCount(1, $row->errors);
self::assertCount(2, $newRow->errors);
self::assertSame($row->lineNumber, $newRow->lineNumber);
}
}

View File

@@ -0,0 +1,39 @@
<?php
declare(strict_types=1);
namespace App\Tests\Unit\Administration\Domain\Model\Import;
use App\Administration\Domain\Model\Import\ImportStatus;
use PHPUnit\Framework\Attributes\Test;
use PHPUnit\Framework\TestCase;
final class ImportStatusTest extends TestCase
{
#[Test]
public function peutDemarrerOnlyForPending(): void
{
self::assertTrue(ImportStatus::PENDING->peutDemarrer());
self::assertFalse(ImportStatus::PROCESSING->peutDemarrer());
self::assertFalse(ImportStatus::COMPLETED->peutDemarrer());
self::assertFalse(ImportStatus::FAILED->peutDemarrer());
}
#[Test]
public function estTermineForCompletedAndFailed(): void
{
self::assertFalse(ImportStatus::PENDING->estTermine());
self::assertFalse(ImportStatus::PROCESSING->estTermine());
self::assertTrue(ImportStatus::COMPLETED->estTermine());
self::assertTrue(ImportStatus::FAILED->estTermine());
}
#[Test]
public function labelReturnsReadableText(): void
{
self::assertSame('En attente', ImportStatus::PENDING->label());
self::assertSame('En cours', ImportStatus::PROCESSING->label());
self::assertSame('Terminé', ImportStatus::COMPLETED->label());
self::assertSame('Échoué', ImportStatus::FAILED->label());
}
}

View File

@@ -0,0 +1,281 @@
<?php
declare(strict_types=1);
namespace App\Tests\Unit\Administration\Domain\Model\Import;
use App\Administration\Domain\Event\ImportElevesLance;
use App\Administration\Domain\Event\ImportElevesTermine;
use App\Administration\Domain\Exception\ImportNonDemarrableException;
use App\Administration\Domain\Model\Import\ColumnMapping;
use App\Administration\Domain\Model\Import\ImportRow;
use App\Administration\Domain\Model\Import\ImportRowError;
use App\Administration\Domain\Model\Import\ImportStatus;
use App\Administration\Domain\Model\Import\KnownImportFormat;
use App\Administration\Domain\Model\Import\StudentImportBatch;
use App\Administration\Domain\Model\Import\StudentImportField;
use App\Shared\Domain\Tenant\TenantId;
use DateTimeImmutable;
use PHPUnit\Framework\Attributes\Test;
use PHPUnit\Framework\TestCase;
final class StudentImportBatchTest extends TestCase
{
private const string TENANT_ID = '550e8400-e29b-41d4-a716-446655440001';
#[Test]
public function creerCreatesBatchWithPendingStatus(): void
{
$batch = $this->createBatch();
self::assertSame(ImportStatus::PENDING, $batch->status);
self::assertSame(0, $batch->importedCount);
self::assertSame(0, $batch->errorCount);
self::assertNull($batch->completedAt);
self::assertNull($batch->mapping);
}
#[Test]
public function creerSetsAllProperties(): void
{
$tenantId = TenantId::fromString(self::TENANT_ID);
$createdAt = new DateTimeImmutable('2026-02-24 10:00:00');
$columns = ['Nom', 'Prénom', 'Classe'];
$batch = StudentImportBatch::creer(
tenantId: $tenantId,
originalFilename: 'eleves.csv',
totalRows: 50,
detectedColumns: $columns,
detectedFormat: KnownImportFormat::PRONOTE,
createdAt: $createdAt,
);
self::assertTrue($batch->tenantId->equals($tenantId));
self::assertSame('eleves.csv', $batch->originalFilename);
self::assertSame(50, $batch->totalRows);
self::assertSame($columns, $batch->detectedColumns);
self::assertSame(KnownImportFormat::PRONOTE, $batch->detectedFormat);
self::assertEquals($createdAt, $batch->createdAt);
}
#[Test]
public function creerDoesNotRecordAnyEvent(): void
{
$batch = $this->createBatch();
self::assertEmpty($batch->pullDomainEvents());
}
#[Test]
public function appliquerMappingSetsMapping(): void
{
$batch = $this->createBatch();
$mapping = $this->createValidMapping();
$batch->appliquerMapping($mapping);
self::assertNotNull($batch->mapping);
self::assertTrue($batch->mapping->equals($mapping));
}
#[Test]
public function enregistrerLignesStoresRows(): void
{
$batch = $this->createBatch();
$rows = [
new ImportRow(1, ['Nom' => 'Dupont'], ['lastName' => 'Dupont']),
new ImportRow(2, ['Nom' => 'Martin'], ['lastName' => 'Martin']),
];
$batch->enregistrerLignes($rows);
self::assertCount(2, $batch->lignes());
self::assertSame(50, $batch->totalRows);
}
#[Test]
public function demarrerTransitionsToProcessingAndRecordsEvent(): void
{
$batch = $this->createBatch();
$batch->appliquerMapping($this->createValidMapping());
$at = new DateTimeImmutable('2026-02-24 11:00:00');
$batch->demarrer($at);
self::assertSame(ImportStatus::PROCESSING, $batch->status);
$events = $batch->pullDomainEvents();
self::assertCount(1, $events);
self::assertInstanceOf(ImportElevesLance::class, $events[0]);
self::assertTrue($events[0]->batchId->equals($batch->id));
self::assertTrue($events[0]->tenantId->equals($batch->tenantId));
}
#[Test]
public function demarrerSansMappingLeveException(): void
{
$batch = $this->createBatch();
$this->expectException(ImportNonDemarrableException::class);
$batch->demarrer(new DateTimeImmutable());
}
#[Test]
public function demarrerDepuisStatutNonPendingLeveException(): void
{
$batch = $this->createBatch();
$batch->appliquerMapping($this->createValidMapping());
$batch->demarrer(new DateTimeImmutable());
$this->expectException(ImportNonDemarrableException::class);
$batch->demarrer(new DateTimeImmutable());
}
#[Test]
public function terminerSetsCompletedStatusAndRecordsEvent(): void
{
$batch = $this->createBatch();
$batch->appliquerMapping($this->createValidMapping());
$batch->demarrer(new DateTimeImmutable());
$batch->pullDomainEvents();
$at = new DateTimeImmutable('2026-02-24 12:00:00');
$batch->terminer(45, 5, $at);
self::assertSame(ImportStatus::COMPLETED, $batch->status);
self::assertSame(45, $batch->importedCount);
self::assertSame(5, $batch->errorCount);
self::assertEquals($at, $batch->completedAt);
self::assertTrue($batch->estTermine());
$events = $batch->pullDomainEvents();
self::assertCount(1, $events);
self::assertInstanceOf(ImportElevesTermine::class, $events[0]);
self::assertSame(45, $events[0]->importedCount);
self::assertSame(5, $events[0]->errorCount);
}
#[Test]
public function echouerSetsFailedStatus(): void
{
$batch = $this->createBatch();
$batch->appliquerMapping($this->createValidMapping());
$batch->demarrer(new DateTimeImmutable());
$batch->pullDomainEvents();
$at = new DateTimeImmutable('2026-02-24 12:00:00');
$batch->echouer(50, $at);
self::assertSame(ImportStatus::FAILED, $batch->status);
self::assertSame(50, $batch->errorCount);
self::assertEquals($at, $batch->completedAt);
self::assertTrue($batch->estTermine());
}
#[Test]
public function lignesValidesFiltersCorrectly(): void
{
$batch = $this->createBatch();
$rows = [
new ImportRow(1, [], ['lastName' => 'Dupont']),
new ImportRow(2, [], ['lastName' => ''], [new ImportRowError('lastName', 'Nom vide')]),
new ImportRow(3, [], ['lastName' => 'Martin']),
];
$batch->enregistrerLignes($rows);
self::assertCount(2, $batch->lignesValides());
self::assertCount(1, $batch->lignesEnErreur());
}
#[Test]
public function progressionCalculatesCorrectly(): void
{
$batch = $this->createBatch();
$batch->appliquerMapping($this->createValidMapping());
$batch->demarrer(new DateTimeImmutable());
$batch->terminer(40, 10, new DateTimeImmutable());
self::assertEqualsWithDelta(100.0, $batch->progression(), 0.01);
}
#[Test]
public function progressionReturnsZeroForEmptyBatch(): void
{
$batch = StudentImportBatch::creer(
tenantId: TenantId::fromString(self::TENANT_ID),
originalFilename: 'empty.csv',
totalRows: 0,
detectedColumns: [],
detectedFormat: null,
createdAt: new DateTimeImmutable(),
);
self::assertEqualsWithDelta(0.0, $batch->progression(), 0.01);
}
#[Test]
public function reconstituteRestoresAllProperties(): void
{
$id = \App\Administration\Domain\Model\Import\ImportBatchId::generate();
$tenantId = TenantId::fromString(self::TENANT_ID);
$mapping = $this->createValidMapping();
$createdAt = new DateTimeImmutable('2026-02-24 10:00:00');
$completedAt = new DateTimeImmutable('2026-02-24 12:00:00');
$batch = StudentImportBatch::reconstitute(
id: $id,
tenantId: $tenantId,
originalFilename: 'eleves.csv',
totalRows: 50,
detectedColumns: ['Nom', 'Prénom', 'Classe'],
detectedFormat: KnownImportFormat::PRONOTE,
status: ImportStatus::COMPLETED,
mapping: $mapping,
importedCount: 45,
errorCount: 5,
createdAt: $createdAt,
completedAt: $completedAt,
);
self::assertTrue($batch->id->equals($id));
self::assertTrue($batch->tenantId->equals($tenantId));
self::assertSame('eleves.csv', $batch->originalFilename);
self::assertSame(50, $batch->totalRows);
self::assertSame(ImportStatus::COMPLETED, $batch->status);
self::assertNotNull($batch->mapping);
self::assertSame(45, $batch->importedCount);
self::assertSame(5, $batch->errorCount);
self::assertEquals($createdAt, $batch->createdAt);
self::assertEquals($completedAt, $batch->completedAt);
self::assertEmpty($batch->pullDomainEvents());
}
private function createBatch(): StudentImportBatch
{
return StudentImportBatch::creer(
tenantId: TenantId::fromString(self::TENANT_ID),
originalFilename: 'eleves.csv',
totalRows: 50,
detectedColumns: ['Nom', 'Prénom', 'Classe', 'Email'],
detectedFormat: KnownImportFormat::CUSTOM,
createdAt: new DateTimeImmutable('2026-02-24 10:00:00'),
);
}
private function createValidMapping(): ColumnMapping
{
return ColumnMapping::creer(
[
'Nom' => StudentImportField::LAST_NAME,
'Prénom' => StudentImportField::FIRST_NAME,
'Classe' => StudentImportField::CLASS_NAME,
],
KnownImportFormat::CUSTOM,
);
}
}

View File

@@ -0,0 +1,49 @@
<?php
declare(strict_types=1);
namespace App\Tests\Unit\Administration\Domain\Model\Import;
use App\Administration\Domain\Model\Import\StudentImportField;
use PHPUnit\Framework\Attributes\Test;
use PHPUnit\Framework\TestCase;
final class StudentImportFieldTest extends TestCase
{
#[Test]
public function champsObligatoiresReturnsRequiredFields(): void
{
$required = StudentImportField::champsObligatoires();
self::assertCount(3, $required);
self::assertContains(StudentImportField::LAST_NAME, $required);
self::assertContains(StudentImportField::FIRST_NAME, $required);
self::assertContains(StudentImportField::CLASS_NAME, $required);
}
#[Test]
public function estObligatoireForRequiredFields(): void
{
self::assertTrue(StudentImportField::LAST_NAME->estObligatoire());
self::assertTrue(StudentImportField::FIRST_NAME->estObligatoire());
self::assertTrue(StudentImportField::CLASS_NAME->estObligatoire());
}
#[Test]
public function estObligatoireFalseForOptionalFields(): void
{
self::assertFalse(StudentImportField::EMAIL->estObligatoire());
self::assertFalse(StudentImportField::BIRTH_DATE->estObligatoire());
self::assertFalse(StudentImportField::GENDER->estObligatoire());
self::assertFalse(StudentImportField::STUDENT_NUMBER->estObligatoire());
}
#[Test]
public function labelReturnsReadableText(): void
{
self::assertSame('Nom', StudentImportField::LAST_NAME->label());
self::assertSame('Prénom', StudentImportField::FIRST_NAME->label());
self::assertSame('Classe', StudentImportField::CLASS_NAME->label());
self::assertSame('Email', StudentImportField::EMAIL->label());
}
}