"""In-memory port implementations for fast, DB-free unit tests.""" import datetime as dt import uuid from dataclasses import dataclass, replace from app.domain.entities import Credentials, SubsonicCredentials, User @dataclass class _Stored: user: User password_hash: str subsonic_password_enc: str | None = None class InMemoryUserRepository: def __init__(self) -> None: self._by_id: dict[uuid.UUID, _Stored] = {} async def get_by_id(self, user_id: uuid.UUID) -> User | None: stored = self._by_id.get(user_id) return stored.user if stored else None async def get_credentials_by_username(self, username: str) -> Credentials | None: for stored in self._by_id.values(): if stored.user.username == username: return Credentials(user=stored.user, password_hash=stored.password_hash) return None async def add(self, *, username: str, password_hash: str, is_superuser: bool) -> User: now = dt.datetime.now(dt.UTC) user = User( id=uuid.uuid4(), username=username, is_superuser=is_superuser, is_active=True, created_at=now, updated_at=now, ) self._by_id[user.id] = _Stored(user=user, password_hash=password_hash) return user async def list(self, *, limit: int, offset: int) -> list[User]: users = [s.user for s in self._by_id.values()] users.sort(key=lambda u: u.created_at) return users[offset : offset + limit] async def set_password_hash(self, user_id: uuid.UUID, password_hash: str) -> None: self._by_id[user_id].password_hash = password_hash async def set_superuser(self, user_id: uuid.UUID, is_superuser: bool) -> User: stored = self._by_id[user_id] stored.user = replace(stored.user, is_superuser=is_superuser) return stored.user async def set_active(self, user_id: uuid.UUID, is_active: bool) -> User: stored = self._by_id[user_id] stored.user = replace(stored.user, is_active=is_active) return stored.user async def count(self) -> int: return len(self._by_id) async def get_subsonic_credentials_by_username( self, username: str ) -> SubsonicCredentials | None: for stored in self._by_id.values(): if stored.user.username == username: return SubsonicCredentials( user=stored.user, password_enc=stored.subsonic_password_enc ) return None async def get_subsonic_password_enc(self, user_id: uuid.UUID) -> str | None: return self._by_id[user_id].subsonic_password_enc async def set_subsonic_password_enc(self, user_id: uuid.UUID, password_enc: str) -> None: self._by_id[user_id].subsonic_password_enc = password_enc @dataclass class _Token: user_id: uuid.UUID token_hash: str expires_at: dt.datetime revoked_at: dt.datetime | None = None class InMemoryRefreshTokenRepository: def __init__(self) -> None: self._by_jti: dict[uuid.UUID, _Token] = {} async def add( self, *, jti: uuid.UUID, user_id: uuid.UUID, token_hash: str, expires_at: dt.datetime, ) -> None: self._by_jti[jti] = _Token(user_id=user_id, token_hash=token_hash, expires_at=expires_at) async def is_valid(self, jti: uuid.UUID) -> bool: token = self._by_jti.get(jti) if token is None or token.revoked_at is not None: return False return token.expires_at > dt.datetime.now(dt.UTC) async def revoke(self, jti: uuid.UUID) -> None: token = self._by_jti.get(jti) if token and token.revoked_at is None: token.revoked_at = dt.datetime.now(dt.UTC) async def revoke_all_for_user(self, user_id: uuid.UUID) -> None: now = dt.datetime.now(dt.UTC) for token in self._by_jti.values(): if token.user_id == user_id and token.revoked_at is None: token.revoked_at = now