L'admin doit pouvoir voir en un coup d'œil quelles matières sont actives (notes saisies) pour décider lesquelles peuvent être supprimées sans perte de données. Auparavant, la suppression d'une matière était silencieuse : elle cascade-deletait évaluations et notes sans avertir. La liste des matières affiche désormais les compteurs d'enseignants, classes, évaluations et notes. La suppression déclenche une confirmation explicite quand la matière contient des notes, avec récapitulatif des volumes impactés, pour rendre l'action irréversible consciente. Côté tests, un endpoint de seeding HTTP remplace les appels docker exec dans les E2E (gain ~30-60s → 5-10s par test), et un trait partagé factorise le SQL de seeding entre les deux suites fonctionnelles.
137 lines
5.1 KiB
PHP
137 lines
5.1 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace App\Tests\Functional\Administration\Infrastructure\ReadModel;
|
|
|
|
use App\Administration\Application\Port\PaginatedSubjectsReader;
|
|
use App\Tests\Functional\Helpers\SubjectStatsSeedingTrait;
|
|
use Doctrine\DBAL\Connection;
|
|
use PHPUnit\Framework\Attributes\Test;
|
|
use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase;
|
|
|
|
final class DbalPaginatedSubjectsReaderTest extends KernelTestCase
|
|
{
|
|
use SubjectStatsSeedingTrait;
|
|
|
|
// Plage UUID 0020-0029 réservée à cette suite pour éviter les collisions
|
|
// avec d'autres tests fonctionnels qui seed les mêmes tables.
|
|
private const string TENANT_A = 'aaaaaaaa-bbbb-cccc-dddd-eeeeeeee0020';
|
|
private const string TENANT_B = 'aaaaaaaa-bbbb-cccc-dddd-eeeeeeee0021';
|
|
private const string SCHOOL_ID = 'aaaaaaaa-bbbb-cccc-dddd-eeeeeeee0022';
|
|
|
|
private Connection $sharedConnection;
|
|
private PaginatedSubjectsReader $reader;
|
|
|
|
protected function setUp(): void
|
|
{
|
|
self::bootKernel();
|
|
|
|
/** @var Connection $connection */
|
|
$connection = static::getContainer()->get(Connection::class);
|
|
$this->sharedConnection = $connection;
|
|
|
|
/** @var PaginatedSubjectsReader $reader */
|
|
$reader = static::getContainer()->get(PaginatedSubjectsReader::class);
|
|
$this->reader = $reader;
|
|
}
|
|
|
|
protected function tearDown(): void
|
|
{
|
|
$this->cleanupSubjectStatsData([self::TENANT_A, self::TENANT_B]);
|
|
|
|
parent::tearDown();
|
|
}
|
|
|
|
protected function connection(): Connection
|
|
{
|
|
return $this->sharedConnection;
|
|
}
|
|
|
|
#[Test]
|
|
public function paginatedResultExposesZeroStatsWhenSubjectHasNoData(): void
|
|
{
|
|
$this->insertSubject(self::TENANT_A, self::SCHOOL_ID, 'Matière vide', 'EMPTY');
|
|
|
|
$result = $this->reader->findPaginated(
|
|
tenantId: self::TENANT_A,
|
|
schoolId: self::SCHOOL_ID,
|
|
search: 'Matière vide',
|
|
page: 1,
|
|
limit: 30,
|
|
);
|
|
|
|
self::assertCount(1, $result->items);
|
|
$dto = $result->items[0];
|
|
self::assertSame('Matière vide', $dto->name);
|
|
self::assertSame(0, $dto->teacherCount);
|
|
self::assertSame(0, $dto->classCount);
|
|
self::assertSame(0, $dto->evaluationCount);
|
|
self::assertSame(0, $dto->gradeCount);
|
|
self::assertFalse($dto->hasGrades());
|
|
}
|
|
|
|
#[Test]
|
|
public function paginatedResultCountsEvaluationsGradesTeachersAndClassesPerSubject(): void
|
|
{
|
|
$subjectId = $this->insertSubject(self::TENANT_A, self::SCHOOL_ID, 'Mathématiques', 'MATH2');
|
|
$classId = $this->insertClass(self::TENANT_A, self::SCHOOL_ID);
|
|
$teacherId = $this->insertUser(self::TENANT_A, 'paginated-teacher@test.local');
|
|
$studentId = $this->insertUser(self::TENANT_A, 'paginated-student@test.local');
|
|
|
|
$this->insertTeacherAssignment(self::TENANT_A, $teacherId, $classId, $subjectId, self::SCHOOL_ID);
|
|
|
|
$eval1 = $this->insertEvaluation(self::TENANT_A, $classId, $subjectId, $teacherId, 'Eval 1');
|
|
$eval2 = $this->insertEvaluation(self::TENANT_A, $classId, $subjectId, $teacherId, 'Eval 2');
|
|
$this->insertGrade(self::TENANT_A, $eval1, $studentId, 15.0);
|
|
$this->insertGrade(self::TENANT_A, $eval2, $studentId, 18.0);
|
|
|
|
$result = $this->reader->findPaginated(
|
|
tenantId: self::TENANT_A,
|
|
schoolId: self::SCHOOL_ID,
|
|
search: 'Mathématiques',
|
|
page: 1,
|
|
limit: 30,
|
|
);
|
|
|
|
self::assertCount(1, $result->items);
|
|
$dto = $result->items[0];
|
|
self::assertSame(1, $dto->teacherCount);
|
|
self::assertSame(1, $dto->classCount);
|
|
self::assertSame(2, $dto->evaluationCount);
|
|
self::assertSame(2, $dto->gradeCount);
|
|
self::assertTrue($dto->hasGrades());
|
|
}
|
|
|
|
#[Test]
|
|
public function paginatedResultDoesNotLeakStatsFromOtherTenants(): void
|
|
{
|
|
$subjectA = $this->insertSubject(self::TENANT_A, self::SCHOOL_ID, 'Isolation', 'ISO');
|
|
|
|
// Données tenant B avec le même nom de matière
|
|
$subjectB = $this->insertSubject(self::TENANT_B, self::SCHOOL_ID, 'Isolation', 'ISO');
|
|
$classB = $this->insertClass(self::TENANT_B, self::SCHOOL_ID);
|
|
$teacherB = $this->insertUser(self::TENANT_B, 'isolation-teacher@test.local');
|
|
$studentB = $this->insertUser(self::TENANT_B, 'isolation-student@test.local');
|
|
$this->insertTeacherAssignment(self::TENANT_B, $teacherB, $classB, $subjectB, self::SCHOOL_ID);
|
|
$evalB = $this->insertEvaluation(self::TENANT_B, $classB, $subjectB, $teacherB, 'Eval B');
|
|
$this->insertGrade(self::TENANT_B, $evalB, $studentB, 10.0);
|
|
|
|
$resultA = $this->reader->findPaginated(
|
|
tenantId: self::TENANT_A,
|
|
schoolId: self::SCHOOL_ID,
|
|
search: 'Isolation',
|
|
page: 1,
|
|
limit: 30,
|
|
);
|
|
|
|
self::assertCount(1, $resultA->items);
|
|
$dto = $resultA->items[0];
|
|
self::assertSame($subjectA, $dto->id);
|
|
self::assertSame(0, $dto->teacherCount);
|
|
self::assertSame(0, $dto->classCount);
|
|
self::assertSame(0, $dto->evaluationCount);
|
|
self::assertSame(0, $dto->gradeCount);
|
|
}
|
|
}
|