diff --git a/server/packages/tavolo-platform/src/tavolo/platform/auth.py b/server/packages/tavolo-platform/src/tavolo/platform/auth.py index 7c85b20..c33d3ec 100644 --- a/server/packages/tavolo-platform/src/tavolo/platform/auth.py +++ b/server/packages/tavolo-platform/src/tavolo/platform/auth.py @@ -47,8 +47,13 @@ def get_ws_user(ws: WebSocket) -> Optional[OIDCUser]: def display_name(user: OIDCUser) -> str: - """Best-effort human-readable name for a user.""" - for key in ("name", "preferred_username", "email"): + """Best-effort human-readable name for a user. + + ``preferred_username`` is preferred: it is the account's stable handle, + while ``name`` is a free-form display name a user may change (and two + accounts can share). ``email`` is a last resort before the ``sub``. + """ + for key in ("preferred_username", "name", "email"): value = user.get(key) if isinstance(value, str) and value: return value diff --git a/server/packages/tavolo-platform/tests/test_auth.py b/server/packages/tavolo-platform/tests/test_auth.py index daf3ffe..382186d 100644 --- a/server/packages/tavolo-platform/tests/test_auth.py +++ b/server/packages/tavolo-platform/tests/test_auth.py @@ -11,11 +11,17 @@ import unittest from types import SimpleNamespace from typing import Any -from kaya.oidc import OIDCConfig, OIDCProviders +from kaya.oidc import OIDCConfig, OIDCProviders, OIDCUser from kaya.session import InMemorySessionStore, Session, SessionMixin from helpers import async_test -from tavolo.platform.auth import current_user, get_ws_user, require_auth, session_user +from tavolo.platform.auth import ( + current_user, + display_name, + get_ws_user, + require_auth, + session_user, +) def _providers(*names: str) -> OIDCProviders: @@ -87,6 +93,32 @@ class GetWsUserTest(unittest.TestCase): self.assertIsNone(get_ws_user(SimpleNamespace())) +class DisplayNameTest(unittest.TestCase): + """The claim priority used for the name shown at the table.""" + + def test_prefers_preferred_username_over_name(self) -> None: + user = OIDCUser( + {"sub": "alice", "preferred_username": "alice", "name": "Alice"} + ) + self.assertEqual("alice", display_name(user)) + + def test_falls_back_to_name(self) -> None: + user = OIDCUser({"sub": "alice", "name": "Alice"}) + self.assertEqual("Alice", display_name(user)) + + def test_falls_back_to_email(self) -> None: + user = OIDCUser({"sub": "alice", "email": "alice@example.com"}) + self.assertEqual("alice@example.com", display_name(user)) + + def test_falls_back_to_sub(self) -> None: + user = OIDCUser({"sub": "alice"}) + self.assertEqual("alice", display_name(user)) + + def test_ignores_empty_and_non_string_claims(self) -> None: + user = OIDCUser({"sub": "alice", "preferred_username": "", "name": 42}) + self.assertEqual("alice", display_name(user)) + + class CurrentUserTest(unittest.TestCase): def test_anonymous_session_has_no_user(self) -> None: self.assertIsNone(current_user(_providers("default"), _ctx({})))