Prefer preferred_username for the displayed player name
CI / Build and push docker image (push) Successful in 1m8s

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.
This commit is contained in:
2026-10-06 17:53:27 +08:00
parent 18877b8109
commit d2dae476f0
2 changed files with 41 additions and 4 deletions
@@ -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
@@ -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({})))