"""In-memory identity provider for tests (AD-1).""" from __future__ import annotations import secrets from collections.abc import Mapping from .base import Flow, Identity, IdpUnavailable, LoginRejected class FakeIdp: """`begin` hands out a flow; `complete` accepts the one-time code from `callback_params`.""" def __init__(self) -> None: self.unavailable = False self.identity: Identity | None = Identity("alice", "sub-alice") self._codes: dict[str, Identity | None] = {} def sign_in_as(self, username: str | None) -> None: self.identity = Identity(username, f"sub-{username}") if username else None def begin(self) -> tuple[str, Flow]: if self.unavailable: raise IdpUnavailable("The identity provider is not available") flow = Flow(secrets.token_urlsafe(8), secrets.token_urlsafe(8), secrets.token_urlsafe(8)) return f"https://idp.example.test/authorize?state={flow.state}", flow def callback_params(self, flow: Flow) -> dict[str, str]: code = secrets.token_urlsafe(8) self._codes[code] = self.identity return {"state": flow.state, "code": code} def complete(self, params: Mapping[str, str], flow: Flow) -> Identity: if self.unavailable: raise IdpUnavailable("The identity provider is not available") if "error" in params or params.get("state") != flow.state: raise LoginRejected("The sign-in response does not match the request") identity = self._codes.pop(params.get("code", ""), None) # a code works once if identity is None: raise LoginRejected("The identity provider refused the code") return identity