GET /api/me now carries the profile picture URL when the provider supplies an absolute http(s) one; the SPA shows it in the lobby navbar and draws a deterministic identicon from the sub otherwise, falling back to the identicon when the image fails to load. The identicon is a 5x5 mirrored grid seeded by an inline FNV-1a hash so it stays stable across Rust versions and is unit-testable on the host. The dev mock IdP hands alice and bob a picture claim.
82 lines
2.4 KiB
Python
82 lines
2.4 KiB
Python
"""Helpers for faking the OIDC authenticated user during tests.
|
|
|
|
HTTP handlers funnel through ``OIDCProviders.get_users`` (wrapped by
|
|
``tavolo.platform.auth.current_user``); websocket handlers through
|
|
:func:`tavolo.platform.auth.get_ws_user`. Patching those two entry points
|
|
lets route and websocket tests run entirely in-process with no IdP.
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
import contextlib
|
|
import unittest.mock as _mock
|
|
from typing import Iterator, Optional, Sequence
|
|
|
|
from kaya.oidc import OIDCUser
|
|
|
|
# Import the app first: it pulls in the route modules, which import
|
|
# ``tavolo.platform.auth`` themselves.
|
|
from tavolo.app import oidc
|
|
from tavolo.platform import auth
|
|
|
|
# The app's first configured provider (the test environment configures one
|
|
# named "default").
|
|
_PROVIDER = next(iter(oidc.providers))
|
|
|
|
|
|
def make_user(
|
|
sub: str,
|
|
name: Optional[str] = None,
|
|
email: Optional[str] = None,
|
|
picture: Optional[str] = None,
|
|
) -> OIDCUser:
|
|
claims = {"sub": sub, "preferred_username": name or sub}
|
|
if email:
|
|
claims["email"] = email
|
|
if picture:
|
|
claims["picture"] = picture
|
|
return OIDCUser(claims)
|
|
|
|
|
|
@contextlib.contextmanager
|
|
def oidc_user(
|
|
sub: str,
|
|
name: Optional[str] = None,
|
|
email: Optional[str] = None,
|
|
picture: Optional[str] = None,
|
|
) -> Iterator[OIDCUser]:
|
|
"""Context manager: make ``oidc.get_users`` return this user.
|
|
|
|
``email`` is optional so tests can simulate an IdP that still returns
|
|
the claim: the app requests only the ``openid`` and ``profile`` scopes
|
|
and ignores email where present. ``picture`` simulates the profile
|
|
scope's avatar claim.
|
|
"""
|
|
user = make_user(sub, name, email, picture)
|
|
patcher = _mock.patch.object(oidc, "get_users", return_value={_PROVIDER: user})
|
|
patcher.start()
|
|
try:
|
|
yield user
|
|
finally:
|
|
patcher.stop()
|
|
|
|
|
|
@contextlib.contextmanager
|
|
def ws_users(users: Sequence[OIDCUser]) -> Iterator[None]:
|
|
"""Context manager: patch ``auth.get_ws_user`` to hand out ``users``
|
|
one per websocket connection, in order. Once exhausted it keeps
|
|
returning the last user."""
|
|
remaining = list(users)
|
|
last = remaining[-1] if remaining else None
|
|
|
|
def _next(_ws):
|
|
if remaining:
|
|
return remaining.pop(0)
|
|
return last
|
|
|
|
patcher = _mock.patch.object(auth, "get_ws_user", side_effect=_next)
|
|
patcher.start()
|
|
try:
|
|
yield
|
|
finally:
|
|
patcher.stop()
|