"""Fake Keycloak client for E2E testing. This module provides a mock implementation of KeycloakAuthClient that doesn't require a real Keycloak server. Stores users and tokens in memory for fast, isolated testing. """ import secrets import time import uuid from dataclasses import dataclass, field from typing import Any from app.infrastructure.auth.models import KeycloakUser, TokenInfo @dataclass class TestUser: """Test user data for fake Keycloak. Stores user credentials and profile information. Attributes: id: Unique user identifier. username: User login name. email: User email address. password: User password (plaintext for testing). roles: List of user roles. first_name: User first name. last_name: User last name. """ id: str username: str email: str password: str roles: list[str] = field(default_factory=list) first_name: str = "" last_name: str = "" class FakeKeycloakClient: """In-memory Keycloak client for E2E testing. Mimics KeycloakAuthClient interface without external dependencies. Stores users and tokens in memory. Tokens are simple strings that can be validated locally. Attributes: _users: Dictionary of users by username. _tokens: Dictionary of active tokens to user IDs. _token_ttl: Token time-to-live in seconds. Example: >>> client = FakeKeycloakClient() >>> user = client.create_user("john", "pass", ["user"]) >>> token = client.login("john", "pass") >>> info = await client.introspect_token(token) >>> assert info.active """ def __init__(self, token_ttl: int = 3600) -> None: """Initialize fake Keycloak client. Args: token_ttl: Token lifetime in seconds (default: 1 hour). """ self._users: dict[str, TestUser] = {} self._tokens: dict[str, tuple[str, float]] = {} # token -> (user_id, issued_at) self._token_ttl = token_ttl def create_user( self, username: str, password: str, roles: list[str] | None = None, email: str | None = None, first_name: str = "", last_name: str = "", ) -> TestUser: """Create a new test user. Args: username: Unique username. password: User password. roles: List of roles (default: ["user"]). email: User email (default: username@test.com). first_name: First name. last_name: Last name. Returns: Created TestUser instance. Raises: ValueError: If username already exists. """ if username in self._users: raise ValueError(f"User {username} already exists") user = TestUser( id=str(uuid.uuid4()), username=username, email=email or f"{username}@test.com", password=password, roles=roles or ["user"], first_name=first_name, last_name=last_name, ) self._users[username] = user return user def login(self, username: str, password: str) -> str: """Authenticate user and return access token. Args: username: User login name. password: User password. Returns: Access token string. Raises: ValueError: If credentials are invalid. """ user = self._users.get(username) if not user or user.password != password: raise ValueError("Invalid credentials") token = secrets.token_urlsafe(32) self._tokens[token] = (user.id, time.time()) return token def logout(self, token: str) -> None: """Invalidate access token. Args: token: Token to invalidate. """ self._tokens.pop(token, None) def _get_token_user(self, token: str) -> TestUser | None: """Get user associated with token if valid. Args: token: Access token to validate. Returns: User if token is valid and not expired, None otherwise. """ if token not in self._tokens: return None user_id, issued_at = self._tokens[token] if time.time() - issued_at > self._token_ttl: del self._tokens[token] return None for user in self._users.values(): if user.id == user_id: return user return None async def introspect_token(self, token: str) -> TokenInfo: """Validate token and return token info. Mimics Keycloak token introspection endpoint. Args: token: Access token to validate. Returns: TokenInfo with validation result. """ user = self._get_token_user(token) if not user: return TokenInfo(active=False, raw_claims={"error": "invalid_token"}) raw_claims: dict[str, Any] = { "sub": user.id, "preferred_username": user.username, "email": user.email, "realm_access": {"roles": user.roles}, } return TokenInfo( active=True, user_id=user.id, username=user.username, email=user.email, roles=user.roles, raw_claims=raw_claims, ) async def get_userinfo(self, token: str) -> KeycloakUser | None: """Get user info from token. Mimics Keycloak userinfo endpoint. Args: token: Valid access token. Returns: KeycloakUser if token is valid, None otherwise. """ user = self._get_token_user(token) if not user: return None return KeycloakUser( id=user.id, username=user.username, email=user.email, first_name=user.first_name, last_name=user.last_name, roles=user.roles, ) def clear(self) -> None: """Clear all users and tokens. Useful for cleanup between tests. """ self._users.clear() self._tokens.clear()