From d2dae476f038b9caa6fa2a336c29f4bceedd7f68 Mon Sep 17 00:00:00 2001 From: Walter Oggioni Date: Tue, 6 Oct 2026 09:52:24 +0000 Subject: [PATCH] Prefer preferred_username for the displayed player name The free-form "name" claim can change and two accounts can share it; preferred_username is the account's stable handle. display_name() now falls back preferred_username -> name -> email -> sub, with tests pinning the claim priority. --- .../src/tavolo/platform/auth.py | 9 +++-- .../tavolo-platform/tests/test_auth.py | 36 +++++++++++++++++-- 2 files changed, 41 insertions(+), 4 deletions(-) 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({})))