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(self::TENANT_ALPHA_ID), 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'; $userData = [ 'id' => $userId, 'email' => $email, 'role' => 'ROLE_PARENT', 'tenant_id' => self::TENANT_ALPHA_ID, '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 userCanBeRetrievedByEmailWithinSameTenant(): void { // Arrange $userId = '550e8400-e29b-41d4-a716-446655440001'; $email = 'test@example.com'; $tenantId = TenantId::fromString(self::TENANT_ALPHA_ID); $userData = [ 'id' => $userId, 'email' => $email, 'role' => 'ROLE_PARENT', 'tenant_id' => self::TENANT_ALPHA_ID, '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, $tenantId) { // Email index key should include tenant ID $expectedEmailKey = 'user_email:' . $tenantId . ':test_at_example_dot_com'; if ($key === $expectedEmailKey) { return $emailIndexItem; } return $userItem; }); $repository = new CacheUserRepository($cachePool); // Act $user = $repository->findByEmail(new Email($email), $tenantId); // Assert self::assertNotNull($user); self::assertSame($userId, (string) $user->id); self::assertSame($email, (string) $user->email); } #[Test] public function userCannotBeFoundByEmailInDifferentTenant(): void { // Arrange: User exists in tenant Alpha $tenantAlpha = TenantId::fromString(self::TENANT_ALPHA_ID); $tenantBeta = TenantId::fromString(self::TENANT_BETA_ID); $email = new Email('test@example.com'); // Cache miss for tenant Beta's email index $missItem = $this->createMock(CacheItemInterface::class); $missItem->method('isHit')->willReturn(false); $cachePool = $this->createMock(CacheItemPoolInterface::class); $cachePool->method('getItem') ->willReturnCallback(static function ($key) use ($missItem, $tenantBeta) { // When looking up with tenant Beta, return cache miss $betaEmailKey = 'user_email:' . $tenantBeta . ':test_at_example_dot_com'; if ($key === $betaEmailKey) { return $missItem; } // For any other key, also return miss return $missItem; }); $repository = new CacheUserRepository($cachePool); // Act: Try to find user in tenant Beta (where they don't exist) $user = $repository->findByEmail($email, $tenantBeta); // Assert: User should not be found self::assertNull($user, 'User from tenant Alpha should not be found when searching in tenant Beta'); } #[Test] public function emailIndexKeyIncludesTenantId(): void { // Arrange: Track what cache keys are used $savedKeys = []; $cacheItem = $this->createMock(CacheItemInterface::class); $cacheItem->method('set')->willReturnSelf(); $cachePool = $this->createMock(CacheItemPoolInterface::class); $cachePool->method('getItem') ->willReturnCallback(static function ($key) use (&$savedKeys, $cacheItem) { $savedKeys[] = $key; return $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(self::TENANT_ALPHA_ID), schoolName: 'École Test', dateNaissance: null, createdAt: new DateTimeImmutable(), ); // Act $repository->save($user); // Assert: Email index key should include tenant ID $emailIndexKey = 'user_email:' . self::TENANT_ALPHA_ID . ':test_at_example_dot_com'; self::assertContains( $emailIndexKey, $savedKeys, 'Email index cache key should include tenant ID for multi-tenant isolation' ); } }