Implémentation complète du flux de réinitialisation de mot de passe (Story 1.5): Backend: - Aggregate PasswordResetToken avec TTL 1h, UUID v7, usage unique - Endpoint POST /api/password/forgot avec rate limiting (3/h par email, 10/h par IP) - Endpoint POST /api/password/reset avec validation token - Templates email (demande + confirmation) - Repository Redis avec TTL 2h pour distinguer expiré/invalide Frontend: - Page /mot-de-passe-oublie avec message générique (anti-énumération) - Page /reset-password/[token] avec validation temps réel des critères - Gestion erreurs: token invalide, expiré, déjà utilisé Tests: - 14 tests unitaires PasswordResetToken - 7 tests unitaires RequestPasswordResetHandler - 7 tests unitaires ResetPasswordHandler - Tests E2E Playwright pour le flux complet
88 lines
2.2 KiB
PHP
88 lines
2.2 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace App\Tests\Unit\Shared\Domain;
|
|
|
|
use App\Shared\Domain\AggregateRoot;
|
|
use App\Shared\Domain\DomainEvent;
|
|
use DateTimeImmutable;
|
|
use PHPUnit\Framework\TestCase;
|
|
use Ramsey\Uuid\Uuid;
|
|
use Ramsey\Uuid\UuidInterface;
|
|
|
|
final class AggregateRootTest extends TestCase
|
|
{
|
|
public function testPullDomainEventsReturnsRecordedEvents(): void
|
|
{
|
|
$aggregate = new TestAggregate();
|
|
$event1 = new TestDomainEvent('test1');
|
|
$event2 = new TestDomainEvent('test2');
|
|
|
|
$aggregate->doSomething($event1);
|
|
$aggregate->doSomething($event2);
|
|
|
|
$events = $aggregate->pullDomainEvents();
|
|
|
|
$this->assertCount(2, $events);
|
|
$this->assertSame($event1, $events[0]);
|
|
$this->assertSame($event2, $events[1]);
|
|
}
|
|
|
|
public function testPullDomainEventsClearsEventsAfterPulling(): void
|
|
{
|
|
$aggregate = new TestAggregate();
|
|
$event = new TestDomainEvent('test');
|
|
|
|
$aggregate->doSomething($event);
|
|
$aggregate->pullDomainEvents();
|
|
|
|
$secondPull = $aggregate->pullDomainEvents();
|
|
|
|
$this->assertCount(0, $secondPull);
|
|
}
|
|
|
|
public function testRecordEventAddsEventToInternalList(): void
|
|
{
|
|
$aggregate = new TestAggregate();
|
|
$event = new TestDomainEvent('test');
|
|
|
|
$aggregate->doSomething($event);
|
|
$events = $aggregate->pullDomainEvents();
|
|
|
|
$this->assertCount(1, $events);
|
|
$this->assertInstanceOf(TestDomainEvent::class, $events[0]);
|
|
}
|
|
}
|
|
|
|
// Test implementations
|
|
final class TestAggregate extends AggregateRoot
|
|
{
|
|
public function doSomething(DomainEvent $event): void
|
|
{
|
|
$this->recordEvent($event);
|
|
}
|
|
}
|
|
|
|
final readonly class TestDomainEvent implements DomainEvent
|
|
{
|
|
private DateTimeImmutable $occurredOn;
|
|
|
|
public function __construct(
|
|
public string $data,
|
|
private ?UuidInterface $testAggregateId = null,
|
|
) {
|
|
$this->occurredOn = new DateTimeImmutable();
|
|
}
|
|
|
|
public function occurredOn(): DateTimeImmutable
|
|
{
|
|
return $this->occurredOn;
|
|
}
|
|
|
|
public function aggregateId(): UuidInterface
|
|
{
|
|
return $this->testAggregateId ?? Uuid::uuid7();
|
|
}
|
|
}
|