feat: Permettre aux administrateurs de configurer les règles de devoirs
Les établissements ont besoin de protéger les élèves et familles des devoirs de dernière minute. Cette configuration au niveau tenant permet de définir des règles de timing (délai minimum, pas de devoir pour lundi après une heure limite) et un mode d'application (avertissement, blocage ou désactivé). Le service de validation est prêt pour être branché dans le flux de création de devoirs (Stories 5.4/5.5). L'historique des changements assure la traçabilité des modifications de configuration.
This commit is contained in:
@@ -0,0 +1,163 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Tests\Unit\Administration\Application\Command\UpdateHomeworkRules;
|
||||
|
||||
use App\Administration\Application\Command\UpdateHomeworkRules\UpdateHomeworkRulesCommand;
|
||||
use App\Administration\Application\Command\UpdateHomeworkRules\UpdateHomeworkRulesHandler;
|
||||
use App\Administration\Domain\Model\HomeworkRules\EnforcementMode;
|
||||
use App\Administration\Domain\Model\HomeworkRules\HomeworkRule;
|
||||
use App\Administration\Domain\Model\HomeworkRules\HomeworkRules;
|
||||
use App\Administration\Domain\Model\HomeworkRules\RuleType;
|
||||
use App\Administration\Infrastructure\Persistence\InMemory\InMemoryHomeworkRulesHistoryRepository;
|
||||
use App\Administration\Infrastructure\Persistence\InMemory\InMemoryHomeworkRulesRepository;
|
||||
use App\Shared\Domain\Clock;
|
||||
use App\Shared\Domain\Tenant\TenantId;
|
||||
use DateTimeImmutable;
|
||||
use PHPUnit\Framework\Attributes\Test;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
final class UpdateHomeworkRulesHandlerTest extends TestCase
|
||||
{
|
||||
private const string TENANT_ID = '550e8400-e29b-41d4-a716-446655440001';
|
||||
private const string USER_ID = '550e8400-e29b-41d4-a716-446655440099';
|
||||
|
||||
private InMemoryHomeworkRulesRepository $rulesRepository;
|
||||
private InMemoryHomeworkRulesHistoryRepository $historyRepository;
|
||||
private Clock $clock;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
$this->rulesRepository = new InMemoryHomeworkRulesRepository();
|
||||
$this->historyRepository = new InMemoryHomeworkRulesHistoryRepository();
|
||||
$this->clock = new class implements Clock {
|
||||
public function now(): DateTimeImmutable
|
||||
{
|
||||
return new DateTimeImmutable('2026-03-17 10:00:00');
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function itCreatesRulesWhenNoneExist(): void
|
||||
{
|
||||
$handler = $this->handler();
|
||||
|
||||
$result = $handler($this->commandAvecDelai(3));
|
||||
|
||||
self::assertCount(1, $result->rules);
|
||||
self::assertSame(RuleType::MINIMUM_DELAY, $result->rules[0]->type);
|
||||
self::assertSame(EnforcementMode::SOFT, $result->enforcementMode);
|
||||
self::assertTrue($result->enabled);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function itUpdatesExistingRules(): void
|
||||
{
|
||||
$this->seedRules();
|
||||
|
||||
$handler = $this->handler();
|
||||
$result = $handler($this->commandAvecDelai(5, 'hard'));
|
||||
|
||||
self::assertCount(1, $result->rules);
|
||||
self::assertSame(5, $result->rules[0]->params['days']);
|
||||
self::assertSame(EnforcementMode::HARD, $result->enforcementMode);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function itRecordsHistory(): void
|
||||
{
|
||||
$handler = $this->handler();
|
||||
$handler($this->commandAvecDelai(3));
|
||||
|
||||
$history = $this->historyRepository->findByTenant(TenantId::fromString(self::TENANT_ID));
|
||||
|
||||
self::assertCount(1, $history);
|
||||
self::assertSame('soft', $history[0]['enforcement_mode']);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function itSavesToRepository(): void
|
||||
{
|
||||
$handler = $this->handler();
|
||||
$handler($this->commandAvecDelai(3));
|
||||
|
||||
$saved = $this->rulesRepository->findByTenantId(TenantId::fromString(self::TENANT_ID));
|
||||
self::assertNotNull($saved);
|
||||
self::assertCount(1, $saved->rules);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function itDisablesRules(): void
|
||||
{
|
||||
$this->seedRules();
|
||||
|
||||
$handler = $this->handler();
|
||||
$result = $handler(new UpdateHomeworkRulesCommand(
|
||||
tenantId: self::TENANT_ID,
|
||||
rules: [],
|
||||
enforcementMode: 'disabled',
|
||||
enabled: false,
|
||||
changedBy: self::USER_ID,
|
||||
));
|
||||
|
||||
self::assertFalse($result->enabled);
|
||||
self::assertSame(EnforcementMode::DISABLED, $result->enforcementMode);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function itHandlesMultipleRules(): void
|
||||
{
|
||||
$handler = $this->handler();
|
||||
$result = $handler(new UpdateHomeworkRulesCommand(
|
||||
tenantId: self::TENANT_ID,
|
||||
rules: [
|
||||
['type' => 'minimum_delay', 'params' => ['days' => 3]],
|
||||
['type' => 'no_monday_after', 'params' => ['day' => 'friday', 'time' => '12:00']],
|
||||
],
|
||||
enforcementMode: 'soft',
|
||||
enabled: true,
|
||||
changedBy: self::USER_ID,
|
||||
));
|
||||
|
||||
self::assertCount(2, $result->rules);
|
||||
}
|
||||
|
||||
private function handler(): UpdateHomeworkRulesHandler
|
||||
{
|
||||
return new UpdateHomeworkRulesHandler(
|
||||
$this->rulesRepository,
|
||||
$this->historyRepository,
|
||||
$this->clock,
|
||||
);
|
||||
}
|
||||
|
||||
private function commandAvecDelai(int $days, string $mode = 'soft'): UpdateHomeworkRulesCommand
|
||||
{
|
||||
return new UpdateHomeworkRulesCommand(
|
||||
tenantId: self::TENANT_ID,
|
||||
rules: [['type' => 'minimum_delay', 'params' => ['days' => $days]]],
|
||||
enforcementMode: $mode,
|
||||
enabled: true,
|
||||
changedBy: self::USER_ID,
|
||||
);
|
||||
}
|
||||
|
||||
private function seedRules(): void
|
||||
{
|
||||
$rules = HomeworkRules::creer(
|
||||
tenantId: TenantId::fromString(self::TENANT_ID),
|
||||
now: new DateTimeImmutable('2026-03-01'),
|
||||
);
|
||||
|
||||
$rules->mettreAJour(
|
||||
rules: [new HomeworkRule(RuleType::MINIMUM_DELAY, ['days' => 3])],
|
||||
enforcementMode: EnforcementMode::SOFT,
|
||||
enabled: true,
|
||||
now: new DateTimeImmutable('2026-03-01'),
|
||||
);
|
||||
|
||||
$this->rulesRepository->save($rules);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,264 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Tests\Unit\Administration\Application\Service;
|
||||
|
||||
use App\Administration\Application\Service\HomeworkRulesValidator;
|
||||
use App\Administration\Domain\Model\HomeworkRules\EnforcementMode;
|
||||
use App\Administration\Domain\Model\HomeworkRules\HomeworkRule;
|
||||
use App\Administration\Domain\Model\HomeworkRules\HomeworkRules;
|
||||
use App\Administration\Domain\Model\HomeworkRules\RuleType;
|
||||
use App\Shared\Domain\Tenant\TenantId;
|
||||
use DateTimeImmutable;
|
||||
use PHPUnit\Framework\Attributes\Test;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
final class HomeworkRulesValidatorTest extends TestCase
|
||||
{
|
||||
private HomeworkRulesValidator $validator;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
$this->validator = new HomeworkRulesValidator();
|
||||
}
|
||||
|
||||
// -- Règles désactivées --
|
||||
|
||||
#[Test]
|
||||
public function disabledRulesAlwaysValid(): void
|
||||
{
|
||||
$rules = $this->creerRulesAvecDelai(3, EnforcementMode::DISABLED, false);
|
||||
|
||||
$result = $this->validator->valider(
|
||||
$rules,
|
||||
new DateTimeImmutable('2026-03-20'), // vendredi
|
||||
new DateTimeImmutable('2026-03-19 15:00'), // jeudi (1 jour, < 3)
|
||||
);
|
||||
|
||||
self::assertTrue($result->estValide());
|
||||
}
|
||||
|
||||
// -- Délai minimum (AC3) --
|
||||
|
||||
#[Test]
|
||||
public function delaiMinimumOkQuandRespected(): void
|
||||
{
|
||||
$rules = $this->creerRulesAvecDelai(3);
|
||||
|
||||
// Devoir vendredi 20 mars, créé mardi 17 mars = 3 jours = OK
|
||||
$result = $this->validator->valider(
|
||||
$rules,
|
||||
new DateTimeImmutable('2026-03-20'),
|
||||
new DateTimeImmutable('2026-03-17 10:00'),
|
||||
);
|
||||
|
||||
self::assertTrue($result->estValide());
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function delaiMinimumViolationQuandTropTard(): void
|
||||
{
|
||||
$rules = $this->creerRulesAvecDelai(3);
|
||||
|
||||
// Devoir vendredi 20 mars, créé mercredi 18 mars = 2 jours < 3
|
||||
$result = $this->validator->valider(
|
||||
$rules,
|
||||
new DateTimeImmutable('2026-03-20'),
|
||||
new DateTimeImmutable('2026-03-18 10:00'),
|
||||
);
|
||||
|
||||
self::assertFalse($result->estValide());
|
||||
self::assertCount(1, $result->violations);
|
||||
self::assertSame(RuleType::MINIMUM_DELAY, $result->violations[0]->ruleType);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function delaiMinimumSoftModeRetourneAvertissement(): void
|
||||
{
|
||||
$rules = $this->creerRulesAvecDelai(3, EnforcementMode::SOFT);
|
||||
|
||||
$result = $this->validator->valider(
|
||||
$rules,
|
||||
new DateTimeImmutable('2026-03-20'),
|
||||
new DateTimeImmutable('2026-03-18 10:00'),
|
||||
);
|
||||
|
||||
self::assertTrue($result->estAvertissement());
|
||||
self::assertFalse($result->estBloquant());
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function delaiMinimumHardModeRetourneErreur(): void
|
||||
{
|
||||
$rules = $this->creerRulesAvecDelai(3, EnforcementMode::HARD);
|
||||
|
||||
$result = $this->validator->valider(
|
||||
$rules,
|
||||
new DateTimeImmutable('2026-03-20'),
|
||||
new DateTimeImmutable('2026-03-18 10:00'),
|
||||
);
|
||||
|
||||
self::assertTrue($result->estBloquant());
|
||||
self::assertFalse($result->estAvertissement());
|
||||
}
|
||||
|
||||
// -- Règle pas de devoir pour lundi (AC4) --
|
||||
|
||||
#[Test]
|
||||
public function pasLundiApresOkQuandCreationAvantCutoff(): void
|
||||
{
|
||||
$rules = $this->creerRulesAvecLundi();
|
||||
|
||||
// Devoir pour lundi 23 mars, créé vendredi 20 mars à 11h (avant 12h)
|
||||
$result = $this->validator->valider(
|
||||
$rules,
|
||||
new DateTimeImmutable('2026-03-23'), // lundi
|
||||
new DateTimeImmutable('2026-03-20 11:00'), // vendredi 11h
|
||||
);
|
||||
|
||||
self::assertTrue($result->estValide());
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function pasLundiApresViolationQuandCreationApresCutoff(): void
|
||||
{
|
||||
$rules = $this->creerRulesAvecLundi();
|
||||
|
||||
// Devoir pour lundi 23 mars, créé vendredi 20 mars à 14h (après 12h)
|
||||
$result = $this->validator->valider(
|
||||
$rules,
|
||||
new DateTimeImmutable('2026-03-23'), // lundi
|
||||
new DateTimeImmutable('2026-03-20 14:00'), // vendredi 14h
|
||||
);
|
||||
|
||||
self::assertFalse($result->estValide());
|
||||
self::assertSame(RuleType::NO_MONDAY_AFTER, $result->violations[0]->ruleType);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function pasLundiApresIgnorePourJoursNonLundi(): void
|
||||
{
|
||||
$rules = $this->creerRulesAvecLundi();
|
||||
|
||||
// Devoir pour mardi 24 mars, créé n'importe quand
|
||||
$result = $this->validator->valider(
|
||||
$rules,
|
||||
new DateTimeImmutable('2026-03-24'), // mardi
|
||||
new DateTimeImmutable('2026-03-23 20:00'), // lundi soir
|
||||
);
|
||||
|
||||
self::assertTrue($result->estValide());
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function pasLundiApresViolationSamediCreation(): void
|
||||
{
|
||||
$rules = $this->creerRulesAvecLundi();
|
||||
|
||||
// Devoir pour lundi 23 mars, créé samedi 21 mars (après vendredi 12h)
|
||||
$result = $this->validator->valider(
|
||||
$rules,
|
||||
new DateTimeImmutable('2026-03-23'), // lundi
|
||||
new DateTimeImmutable('2026-03-21 10:00'), // samedi
|
||||
);
|
||||
|
||||
self::assertFalse($result->estValide());
|
||||
}
|
||||
|
||||
// -- Règles combinées --
|
||||
|
||||
#[Test]
|
||||
public function multipleViolationsQuandPlusieursReglesEnfreintes(): void
|
||||
{
|
||||
$rules = $this->creerRulesComplet();
|
||||
|
||||
// Devoir pour lundi 23 mars, créé samedi 21 mars 10h
|
||||
// Violation délai minimum (3 jours: 23-3 = 20, samedi 21 > 20)
|
||||
// Violation lundi (après vendredi 12h)
|
||||
$result = $this->validator->valider(
|
||||
$rules,
|
||||
new DateTimeImmutable('2026-03-23'), // lundi
|
||||
new DateTimeImmutable('2026-03-21 10:00'), // samedi
|
||||
);
|
||||
|
||||
self::assertFalse($result->estValide());
|
||||
self::assertCount(2, $result->violations);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function messagesRetourneListeMessages(): void
|
||||
{
|
||||
$rules = $this->creerRulesAvecDelai(3);
|
||||
|
||||
$result = $this->validator->valider(
|
||||
$rules,
|
||||
new DateTimeImmutable('2026-03-20'),
|
||||
new DateTimeImmutable('2026-03-19 10:00'),
|
||||
);
|
||||
|
||||
self::assertCount(1, $result->messages());
|
||||
self::assertStringContainsString('3 jours', $result->messages()[0]);
|
||||
}
|
||||
|
||||
// -- Helpers --
|
||||
|
||||
private function creerRulesAvecDelai(
|
||||
int $days,
|
||||
EnforcementMode $mode = EnforcementMode::SOFT,
|
||||
bool $enabled = true,
|
||||
): HomeworkRules {
|
||||
$rules = HomeworkRules::creer(
|
||||
tenantId: TenantId::fromString('550e8400-e29b-41d4-a716-446655440001'),
|
||||
now: new DateTimeImmutable('2026-03-01'),
|
||||
);
|
||||
|
||||
$rules->mettreAJour(
|
||||
rules: [new HomeworkRule(RuleType::MINIMUM_DELAY, ['days' => $days])],
|
||||
enforcementMode: $mode,
|
||||
enabled: $enabled,
|
||||
now: new DateTimeImmutable('2026-03-01'),
|
||||
);
|
||||
|
||||
return $rules;
|
||||
}
|
||||
|
||||
private function creerRulesAvecLundi(
|
||||
EnforcementMode $mode = EnforcementMode::SOFT,
|
||||
): HomeworkRules {
|
||||
$rules = HomeworkRules::creer(
|
||||
tenantId: TenantId::fromString('550e8400-e29b-41d4-a716-446655440001'),
|
||||
now: new DateTimeImmutable('2026-03-01'),
|
||||
);
|
||||
|
||||
$rules->mettreAJour(
|
||||
rules: [new HomeworkRule(RuleType::NO_MONDAY_AFTER, ['day' => 'friday', 'time' => '12:00'])],
|
||||
enforcementMode: $mode,
|
||||
enabled: true,
|
||||
now: new DateTimeImmutable('2026-03-01'),
|
||||
);
|
||||
|
||||
return $rules;
|
||||
}
|
||||
|
||||
private function creerRulesComplet(
|
||||
EnforcementMode $mode = EnforcementMode::SOFT,
|
||||
): HomeworkRules {
|
||||
$rules = HomeworkRules::creer(
|
||||
tenantId: TenantId::fromString('550e8400-e29b-41d4-a716-446655440001'),
|
||||
now: new DateTimeImmutable('2026-03-01'),
|
||||
);
|
||||
|
||||
$rules->mettreAJour(
|
||||
rules: [
|
||||
new HomeworkRule(RuleType::MINIMUM_DELAY, ['days' => 3]),
|
||||
new HomeworkRule(RuleType::NO_MONDAY_AFTER, ['day' => 'friday', 'time' => '12:00']),
|
||||
],
|
||||
enforcementMode: $mode,
|
||||
enabled: true,
|
||||
now: new DateTimeImmutable('2026-03-01'),
|
||||
);
|
||||
|
||||
return $rules;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Tests\Unit\Administration\Domain\Model\HomeworkRules;
|
||||
|
||||
use App\Administration\Domain\Model\HomeworkRules\EnforcementMode;
|
||||
use PHPUnit\Framework\Attributes\Test;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
final class EnforcementModeTest extends TestCase
|
||||
{
|
||||
#[Test]
|
||||
public function softEstActif(): void
|
||||
{
|
||||
self::assertTrue(EnforcementMode::SOFT->estActif());
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function hardEstActif(): void
|
||||
{
|
||||
self::assertTrue(EnforcementMode::HARD->estActif());
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function disabledEstPasActif(): void
|
||||
{
|
||||
self::assertFalse(EnforcementMode::DISABLED->estActif());
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function softEstPasBloquant(): void
|
||||
{
|
||||
self::assertFalse(EnforcementMode::SOFT->estBloquant());
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function hardEstBloquant(): void
|
||||
{
|
||||
self::assertTrue(EnforcementMode::HARD->estBloquant());
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function disabledEstPasBloquant(): void
|
||||
{
|
||||
self::assertFalse(EnforcementMode::DISABLED->estBloquant());
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function labelsAreFrench(): void
|
||||
{
|
||||
self::assertSame('Avertissement', EnforcementMode::SOFT->label());
|
||||
self::assertSame('Blocage', EnforcementMode::HARD->label());
|
||||
self::assertSame('Désactivé', EnforcementMode::DISABLED->label());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,163 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Tests\Unit\Administration\Domain\Model\HomeworkRules;
|
||||
|
||||
use App\Administration\Domain\Exception\HomeworkRuleParamsInvalidException;
|
||||
use App\Administration\Domain\Model\HomeworkRules\HomeworkRule;
|
||||
use App\Administration\Domain\Model\HomeworkRules\RuleType;
|
||||
use PHPUnit\Framework\Attributes\Test;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
final class HomeworkRuleTest extends TestCase
|
||||
{
|
||||
#[Test]
|
||||
public function createMinimumDelayRule(): void
|
||||
{
|
||||
$rule = new HomeworkRule(RuleType::MINIMUM_DELAY, ['days' => 3]);
|
||||
|
||||
self::assertSame(RuleType::MINIMUM_DELAY, $rule->type);
|
||||
self::assertSame(3, $rule->params['days']);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function createNoMondayAfterRule(): void
|
||||
{
|
||||
$rule = new HomeworkRule(RuleType::NO_MONDAY_AFTER, ['day' => 'friday', 'time' => '12:00']);
|
||||
|
||||
self::assertSame(RuleType::NO_MONDAY_AFTER, $rule->type);
|
||||
self::assertSame('friday', $rule->params['day']);
|
||||
self::assertSame('12:00', $rule->params['time']);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function throwsOnMissingParamsForMinimumDelay(): void
|
||||
{
|
||||
$this->expectException(HomeworkRuleParamsInvalidException::class);
|
||||
|
||||
new HomeworkRule(RuleType::MINIMUM_DELAY, []);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function throwsOnMissingParamsForNoMondayAfter(): void
|
||||
{
|
||||
$this->expectException(HomeworkRuleParamsInvalidException::class);
|
||||
|
||||
new HomeworkRule(RuleType::NO_MONDAY_AFTER, ['day' => 'friday']);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function equalsReturnsTrueForIdenticalRules(): void
|
||||
{
|
||||
$rule1 = new HomeworkRule(RuleType::MINIMUM_DELAY, ['days' => 3]);
|
||||
$rule2 = new HomeworkRule(RuleType::MINIMUM_DELAY, ['days' => 3]);
|
||||
|
||||
self::assertTrue($rule1->equals($rule2));
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function equalsReturnsFalseForDifferentParams(): void
|
||||
{
|
||||
$rule1 = new HomeworkRule(RuleType::MINIMUM_DELAY, ['days' => 3]);
|
||||
$rule2 = new HomeworkRule(RuleType::MINIMUM_DELAY, ['days' => 5]);
|
||||
|
||||
self::assertFalse($rule1->equals($rule2));
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function equalsReturnsFalseForDifferentTypes(): void
|
||||
{
|
||||
$rule1 = new HomeworkRule(RuleType::MINIMUM_DELAY, ['days' => 3]);
|
||||
$rule2 = new HomeworkRule(RuleType::NO_MONDAY_AFTER, ['day' => 'friday', 'time' => '12:00']);
|
||||
|
||||
self::assertFalse($rule1->equals($rule2));
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function toArrayReturnsCorrectFormat(): void
|
||||
{
|
||||
$rule = new HomeworkRule(RuleType::MINIMUM_DELAY, ['days' => 3]);
|
||||
$array = $rule->toArray();
|
||||
|
||||
self::assertSame('minimum_delay', $array['type']);
|
||||
self::assertSame(['days' => 3], $array['params']);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function fromArrayCreatesRule(): void
|
||||
{
|
||||
$data = ['type' => 'minimum_delay', 'params' => ['days' => 3]];
|
||||
$rule = HomeworkRule::fromArray($data);
|
||||
|
||||
self::assertSame(RuleType::MINIMUM_DELAY, $rule->type);
|
||||
self::assertSame(3, $rule->params['days']);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function roundTripArrayConversion(): void
|
||||
{
|
||||
$original = new HomeworkRule(RuleType::NO_MONDAY_AFTER, ['day' => 'friday', 'time' => '12:00']);
|
||||
$restored = HomeworkRule::fromArray($original->toArray());
|
||||
|
||||
self::assertTrue($original->equals($restored));
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function throwsOnZeroDays(): void
|
||||
{
|
||||
$this->expectException(HomeworkRuleParamsInvalidException::class);
|
||||
|
||||
new HomeworkRule(RuleType::MINIMUM_DELAY, ['days' => 0]);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function throwsOnNegativeDays(): void
|
||||
{
|
||||
$this->expectException(HomeworkRuleParamsInvalidException::class);
|
||||
|
||||
new HomeworkRule(RuleType::MINIMUM_DELAY, ['days' => -1]);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function throwsOnExcessiveDays(): void
|
||||
{
|
||||
$this->expectException(HomeworkRuleParamsInvalidException::class);
|
||||
|
||||
new HomeworkRule(RuleType::MINIMUM_DELAY, ['days' => 31]);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function throwsOnStringDays(): void
|
||||
{
|
||||
$this->expectException(HomeworkRuleParamsInvalidException::class);
|
||||
|
||||
new HomeworkRule(RuleType::MINIMUM_DELAY, ['days' => 'three']);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function throwsOnInvalidDayName(): void
|
||||
{
|
||||
$this->expectException(HomeworkRuleParamsInvalidException::class);
|
||||
|
||||
new HomeworkRule(RuleType::NO_MONDAY_AFTER, ['day' => 'firday', 'time' => '12:00']);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function throwsOnInvalidTimeFormat(): void
|
||||
{
|
||||
$this->expectException(HomeworkRuleParamsInvalidException::class);
|
||||
|
||||
new HomeworkRule(RuleType::NO_MONDAY_AFTER, ['day' => 'friday', 'time' => 'banana']);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function acceptsBoundaryDays(): void
|
||||
{
|
||||
$rule1 = new HomeworkRule(RuleType::MINIMUM_DELAY, ['days' => 1]);
|
||||
$rule30 = new HomeworkRule(RuleType::MINIMUM_DELAY, ['days' => 30]);
|
||||
|
||||
self::assertSame(1, $rule1->params['days']);
|
||||
self::assertSame(30, $rule30->params['days']);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,244 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Tests\Unit\Administration\Domain\Model\HomeworkRules;
|
||||
|
||||
use App\Administration\Domain\Event\ReglesDevoirsModifiees;
|
||||
use App\Administration\Domain\Model\HomeworkRules\EnforcementMode;
|
||||
use App\Administration\Domain\Model\HomeworkRules\HomeworkRule;
|
||||
use App\Administration\Domain\Model\HomeworkRules\HomeworkRules;
|
||||
use App\Administration\Domain\Model\HomeworkRules\RuleType;
|
||||
use App\Shared\Domain\Tenant\TenantId;
|
||||
use DateTimeImmutable;
|
||||
use PHPUnit\Framework\Attributes\Test;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
final class HomeworkRulesTest extends TestCase
|
||||
{
|
||||
private const string TENANT_ID = '550e8400-e29b-41d4-a716-446655440001';
|
||||
|
||||
#[Test]
|
||||
public function creerWithDefaultValues(): void
|
||||
{
|
||||
$rules = $this->creerHomeworkRules();
|
||||
|
||||
self::assertTrue($rules->tenantId->equals(TenantId::fromString(self::TENANT_ID)));
|
||||
self::assertSame([], $rules->rules);
|
||||
self::assertSame(EnforcementMode::SOFT, $rules->enforcementMode);
|
||||
self::assertTrue($rules->enabled);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function creerDoesNotRecordEvent(): void
|
||||
{
|
||||
$rules = $this->creerHomeworkRules();
|
||||
|
||||
self::assertCount(0, $rules->pullDomainEvents());
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function mettreAJourChangesRulesAndRecordsEvent(): void
|
||||
{
|
||||
$rules = $this->creerHomeworkRules();
|
||||
$now = new DateTimeImmutable('2026-03-17 10:00:00');
|
||||
$newRules = [
|
||||
new HomeworkRule(RuleType::MINIMUM_DELAY, ['days' => 3]),
|
||||
];
|
||||
|
||||
$rules->mettreAJour(
|
||||
rules: $newRules,
|
||||
enforcementMode: EnforcementMode::HARD,
|
||||
enabled: true,
|
||||
now: $now,
|
||||
);
|
||||
|
||||
self::assertCount(1, $rules->rules);
|
||||
self::assertSame(EnforcementMode::HARD, $rules->enforcementMode);
|
||||
self::assertSame($now, $rules->updatedAt);
|
||||
|
||||
$events = $rules->pullDomainEvents();
|
||||
self::assertCount(1, $events);
|
||||
self::assertInstanceOf(ReglesDevoirsModifiees::class, $events[0]);
|
||||
self::assertSame([], $events[0]->previousRules);
|
||||
self::assertCount(1, $events[0]->newRules);
|
||||
self::assertSame(EnforcementMode::HARD, $events[0]->enforcementMode);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function mettreAJourDoesNotRecordEventWhenNothingChanges(): void
|
||||
{
|
||||
$rules = $this->creerHomeworkRules();
|
||||
$now = new DateTimeImmutable('2026-03-17 10:00:00');
|
||||
$newRules = [
|
||||
new HomeworkRule(RuleType::MINIMUM_DELAY, ['days' => 3]),
|
||||
];
|
||||
|
||||
$rules->mettreAJour(
|
||||
rules: $newRules,
|
||||
enforcementMode: EnforcementMode::SOFT,
|
||||
enabled: true,
|
||||
now: $now,
|
||||
);
|
||||
$rules->pullDomainEvents();
|
||||
|
||||
$rules->mettreAJour(
|
||||
rules: $newRules,
|
||||
enforcementMode: EnforcementMode::SOFT,
|
||||
enabled: true,
|
||||
now: new DateTimeImmutable('2026-03-17 11:00:00'),
|
||||
);
|
||||
|
||||
self::assertCount(0, $rules->pullDomainEvents());
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function mettreAJourWithMultipleRules(): void
|
||||
{
|
||||
$rules = $this->creerHomeworkRules();
|
||||
$now = new DateTimeImmutable('2026-03-17 10:00:00');
|
||||
$newRules = [
|
||||
new HomeworkRule(RuleType::MINIMUM_DELAY, ['days' => 3]),
|
||||
new HomeworkRule(RuleType::NO_MONDAY_AFTER, ['day' => 'friday', 'time' => '12:00']),
|
||||
];
|
||||
|
||||
$rules->mettreAJour(
|
||||
rules: $newRules,
|
||||
enforcementMode: EnforcementMode::SOFT,
|
||||
enabled: true,
|
||||
now: $now,
|
||||
);
|
||||
|
||||
self::assertCount(2, $rules->rules);
|
||||
self::assertSame(RuleType::MINIMUM_DELAY, $rules->rules[0]->type);
|
||||
self::assertSame(RuleType::NO_MONDAY_AFTER, $rules->rules[1]->type);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function desactiverDisablesRulesAndRecordsEvent(): void
|
||||
{
|
||||
$rules = $this->creerHomeworkRules();
|
||||
$now = new DateTimeImmutable('2026-03-17 10:00:00');
|
||||
|
||||
$rules->desactiver($now);
|
||||
|
||||
self::assertFalse($rules->enabled);
|
||||
self::assertSame(EnforcementMode::DISABLED, $rules->enforcementMode);
|
||||
|
||||
$events = $rules->pullDomainEvents();
|
||||
self::assertCount(1, $events);
|
||||
self::assertInstanceOf(ReglesDevoirsModifiees::class, $events[0]);
|
||||
self::assertFalse($events[0]->enabled);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function activerReenablesRulesWithGivenMode(): void
|
||||
{
|
||||
$rules = $this->creerHomeworkRules();
|
||||
$rules->desactiver(new DateTimeImmutable('2026-03-17 09:00:00'));
|
||||
$rules->pullDomainEvents();
|
||||
|
||||
$rules->activer(EnforcementMode::HARD, new DateTimeImmutable('2026-03-17 10:00:00'));
|
||||
|
||||
self::assertTrue($rules->enabled);
|
||||
self::assertSame(EnforcementMode::HARD, $rules->enforcementMode);
|
||||
|
||||
$events = $rules->pullDomainEvents();
|
||||
self::assertCount(1, $events);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function estActifReturnsTrueWhenEnabledAndNotDisabled(): void
|
||||
{
|
||||
$rules = $this->creerHomeworkRules();
|
||||
|
||||
self::assertTrue($rules->estActif());
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function estActifReturnsFalseWhenDisabled(): void
|
||||
{
|
||||
$rules = $this->creerHomeworkRules();
|
||||
$rules->desactiver(new DateTimeImmutable());
|
||||
|
||||
self::assertFalse($rules->estActif());
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function reconstituteFromStorage(): void
|
||||
{
|
||||
$tenantId = TenantId::fromString(self::TENANT_ID);
|
||||
$rulesList = [
|
||||
new HomeworkRule(RuleType::MINIMUM_DELAY, ['days' => 3]),
|
||||
];
|
||||
$createdAt = new DateTimeImmutable('2026-03-01 10:00:00');
|
||||
$updatedAt = new DateTimeImmutable('2026-03-15 14:30:00');
|
||||
$id = \App\Administration\Domain\Model\HomeworkRules\HomeworkRulesId::generate();
|
||||
|
||||
$rules = HomeworkRules::reconstitute(
|
||||
id: $id,
|
||||
tenantId: $tenantId,
|
||||
rules: $rulesList,
|
||||
enforcementMode: EnforcementMode::HARD,
|
||||
enabled: true,
|
||||
createdAt: $createdAt,
|
||||
updatedAt: $updatedAt,
|
||||
);
|
||||
|
||||
self::assertTrue($rules->id->equals($id));
|
||||
self::assertTrue($rules->tenantId->equals($tenantId));
|
||||
self::assertCount(1, $rules->rules);
|
||||
self::assertSame(EnforcementMode::HARD, $rules->enforcementMode);
|
||||
self::assertTrue($rules->enabled);
|
||||
self::assertSame($createdAt, $rules->createdAt);
|
||||
self::assertSame($updatedAt, $rules->updatedAt);
|
||||
self::assertCount(0, $rules->pullDomainEvents());
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function mettreAJourPreservesPreviousRulesInEvent(): void
|
||||
{
|
||||
$rules = $this->creerHomeworkRules();
|
||||
$now1 = new DateTimeImmutable('2026-03-17 10:00:00');
|
||||
$firstRules = [
|
||||
new HomeworkRule(RuleType::MINIMUM_DELAY, ['days' => 3]),
|
||||
];
|
||||
|
||||
$rules->mettreAJour(
|
||||
rules: $firstRules,
|
||||
enforcementMode: EnforcementMode::SOFT,
|
||||
enabled: true,
|
||||
now: $now1,
|
||||
);
|
||||
$rules->pullDomainEvents();
|
||||
|
||||
$now2 = new DateTimeImmutable('2026-03-17 11:00:00');
|
||||
$secondRules = [
|
||||
new HomeworkRule(RuleType::MINIMUM_DELAY, ['days' => 5]),
|
||||
];
|
||||
|
||||
$rules->mettreAJour(
|
||||
rules: $secondRules,
|
||||
enforcementMode: EnforcementMode::HARD,
|
||||
enabled: true,
|
||||
now: $now2,
|
||||
);
|
||||
|
||||
$events = $rules->pullDomainEvents();
|
||||
self::assertCount(1, $events);
|
||||
/** @var ReglesDevoirsModifiees $event */
|
||||
$event = $events[0];
|
||||
self::assertSame('minimum_delay', $event->previousRules[0]['type']);
|
||||
self::assertSame(3, $event->previousRules[0]['params']['days']);
|
||||
self::assertSame('minimum_delay', $event->newRules[0]['type']);
|
||||
self::assertSame(5, $event->newRules[0]['params']['days']);
|
||||
}
|
||||
|
||||
private function creerHomeworkRules(): HomeworkRules
|
||||
{
|
||||
return HomeworkRules::creer(
|
||||
tenantId: TenantId::fromString(self::TENANT_ID),
|
||||
now: new DateTimeImmutable('2026-03-01 10:00:00'),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Tests\Unit\Administration\Domain\Model\HomeworkRules;
|
||||
|
||||
use App\Administration\Domain\Model\HomeworkRules\RuleType;
|
||||
use PHPUnit\Framework\Attributes\Test;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
final class RuleTypeTest extends TestCase
|
||||
{
|
||||
#[Test]
|
||||
public function minimumDelayRequiresParams(): void
|
||||
{
|
||||
self::assertSame(['days'], RuleType::MINIMUM_DELAY->requiredParams());
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function noMondayAfterRequiresParams(): void
|
||||
{
|
||||
self::assertSame(['day', 'time'], RuleType::NO_MONDAY_AFTER->requiredParams());
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function labelsAreFrench(): void
|
||||
{
|
||||
self::assertSame('Délai minimum avant échéance', RuleType::MINIMUM_DELAY->label());
|
||||
self::assertSame('Pas de devoir pour lundi après un horaire', RuleType::NO_MONDAY_AFTER->label());
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user