Files
Classeo/backend/tests/Unit/Administration/Infrastructure/Persistence/Cache/CacheUserRepositoryTest.php
Mathias STRASSER c5e6c1d810 feat: Activation de compte utilisateur avec validation token
L'inscription Classeo se fait via invitation : un admin crée un compte,
l'utilisateur reçoit un lien d'activation par email pour définir son
mot de passe. Ce flow sécurisé évite les inscriptions non autorisées
et garantit que seuls les utilisateurs légitimes accèdent au système.

Points clés de l'implémentation :
- Tokens d'activation à usage unique stockés en cache (Redis/filesystem)
- Validation du consentement parental pour les mineurs < 15 ans (RGPD)
- L'échec d'activation ne consume pas le token (retry possible)
- Users dans un cache séparé sans TTL (pas d'expiration)
- Hot reload en dev (FrankenPHP sans mode worker)

Story: 1.3 - Inscription et activation de compte
2026-01-31 19:34:03 +01:00

161 lines
5.5 KiB
PHP

<?php
declare(strict_types=1);
namespace App\Tests\Unit\Administration\Infrastructure\Persistence\Cache;
use App\Administration\Domain\Model\User\Email;
use App\Administration\Domain\Model\User\Role;
use App\Administration\Domain\Model\User\User;
use App\Administration\Infrastructure\Persistence\Cache\CacheUserRepository;
use App\Shared\Infrastructure\Tenant\TenantId;
use DateTimeImmutable;
use PHPUnit\Framework\Attributes\Test;
use PHPUnit\Framework\TestCase;
use Psr\Cache\CacheItemInterface;
use Psr\Cache\CacheItemPoolInterface;
/**
* Tests for CacheUserRepository.
*
* Key invariant: Users must not expire from cache (unlike activation tokens which have 7-day TTL).
* This was a bug where users were stored in the activation_tokens.cache pool with TTL,
* causing activated accounts to become inaccessible after 7 days.
*/
final class CacheUserRepositoryTest extends TestCase
{
#[Test]
public function userIsSavedWithoutExpiration(): void
{
// Arrange: Create a mock cache that tracks expiration settings
$expirationSet = null;
$cacheItem = $this->createMock(CacheItemInterface::class);
$cacheItem->method('set')->willReturnSelf();
$cacheItem->method('expiresAfter')
->willReturnCallback(static function ($ttl) use (&$expirationSet, $cacheItem) {
$expirationSet = $ttl;
return $cacheItem;
});
$cachePool = $this->createMock(CacheItemPoolInterface::class);
$cachePool->method('getItem')->willReturn($cacheItem);
$cachePool->method('save')->willReturn(true);
$repository = new CacheUserRepository($cachePool);
$user = User::creer(
email: new Email('test@example.com'),
role: Role::PARENT,
tenantId: TenantId::fromString('550e8400-e29b-41d4-a716-446655440001'),
schoolName: 'École Test',
dateNaissance: null,
createdAt: new DateTimeImmutable(),
);
// Act
$repository->save($user);
// Assert: No expiration should be set (expiresAfter should not be called with a TTL)
// The users.cache pool is configured with default_lifetime: 0 (no expiration)
// But CacheUserRepository should NOT explicitly set any TTL
self::assertNull(
$expirationSet,
'User cache entries should not have explicit expiration set by the repository'
);
}
#[Test]
public function userCanBeRetrievedById(): void
{
// Arrange
$userId = '550e8400-e29b-41d4-a716-446655440001';
$email = 'test@example.com';
$tenantId = '550e8400-e29b-41d4-a716-446655440002';
$userData = [
'id' => $userId,
'email' => $email,
'role' => 'ROLE_PARENT',
'tenant_id' => $tenantId,
'school_name' => 'École Test',
'statut' => 'pending',
'hashed_password' => null,
'date_naissance' => null,
'created_at' => '2026-01-15T10:00:00+00:00',
'activated_at' => null,
'consentement_parental' => null,
];
$cacheItem = $this->createMock(CacheItemInterface::class);
$cacheItem->method('isHit')->willReturn(true);
$cacheItem->method('get')->willReturn($userData);
$cachePool = $this->createMock(CacheItemPoolInterface::class);
$cachePool->method('getItem')->willReturn($cacheItem);
$repository = new CacheUserRepository($cachePool);
// Act
$user = $repository->findById(\App\Administration\Domain\Model\User\UserId::fromString($userId));
// Assert
self::assertNotNull($user);
self::assertSame($userId, (string) $user->id);
self::assertSame($email, (string) $user->email);
self::assertSame(Role::PARENT, $user->role);
}
#[Test]
public function userCanBeRetrievedByEmail(): void
{
// Arrange
$userId = '550e8400-e29b-41d4-a716-446655440001';
$email = 'test@example.com';
$tenantId = '550e8400-e29b-41d4-a716-446655440002';
$userData = [
'id' => $userId,
'email' => $email,
'role' => 'ROLE_PARENT',
'tenant_id' => $tenantId,
'school_name' => 'École Test',
'statut' => 'pending',
'hashed_password' => null,
'date_naissance' => null,
'created_at' => '2026-01-15T10:00:00+00:00',
'activated_at' => null,
'consentement_parental' => null,
];
$emailIndexItem = $this->createMock(CacheItemInterface::class);
$emailIndexItem->method('isHit')->willReturn(true);
$emailIndexItem->method('get')->willReturn($userId);
$userItem = $this->createMock(CacheItemInterface::class);
$userItem->method('isHit')->willReturn(true);
$userItem->method('get')->willReturn($userData);
$cachePool = $this->createMock(CacheItemPoolInterface::class);
$cachePool->method('getItem')
->willReturnCallback(static function ($key) use ($emailIndexItem, $userItem) {
if (str_starts_with($key, 'user_email:')) {
return $emailIndexItem;
}
return $userItem;
});
$repository = new CacheUserRepository($cachePool);
// Act
$user = $repository->findByEmail(new Email($email));
// Assert
self::assertNotNull($user);
self::assertSame($userId, (string) $user->id);
self::assertSame($email, (string) $user->email);
}
}