From 2543d44860fa30ba4538103afdd717db1a0ad7e2 Mon Sep 17 00:00:00 2001 From: npeter83 Date: Thu, 1 Oct 2026 21:17:09 +0200 Subject: [PATCH] fix(messages): the welcome reads in the reader's language, not the one it was created in E31 S1: the welcome is rendered once, at account creation, so an account that switched language kept it in the old one (the owner's, English under a Hungarian UI). A stored copy is recognised by its opening line (derived from WELCOME) and served in the reader's language; other system messages stay as stored. --- backend/app/routes/messages.py | 27 ++++++++++--- backend/tests/test_messages_welcome.py | 53 ++++++++++++++++++++++++++ 2 files changed, 75 insertions(+), 5 deletions(-) create mode 100644 backend/tests/test_messages_welcome.py diff --git a/backend/app/routes/messages.py b/backend/app/routes/messages.py index 342bda78..d0e9a7b0 100644 --- a/backend/app/routes/messages.py +++ b/backend/app/routes/messages.py @@ -94,13 +94,30 @@ def _system_partner() -> dict: return {"id": SYSTEM_PARTNER_ID, "name": SYSTEM_NAME, "avatar_url": None} -def _serialize_msg(m: Message) -> dict: +# The welcome's opening line in every language — how a stored copy is recognised. It was rendered +# once, in the language of the day it was created, so an account that later switched language kept +# reading it in the old one (E31 S1: the owner's, in English). Recognised here, it is served in the +# reader's language instead. Derived from WELCOME itself, so only a change to an OPENING line would +# orphan the copies already stored. +_WELCOME_OPENINGS = {text.partition("\n")[0] for text in WELCOME.values()} + + +def _system_body(body: str | None, lang: str | None) -> str | None: + """A system message's text for a reader of `lang`: the welcome in their language, any other as + stored.""" + if body is None or lang is None or body.partition("\n")[0] not in _WELCOME_OPENINGS: + return body + return WELCOME.get(lang, WELCOME["en"]) + + +def _serialize_msg(m: Message, lang: str | None = None) -> dict: + """`lang`: the reader's language, for a system message's text (a user message is ciphertext).""" return { "id": m.id, "kind": m.kind, "sender_id": m.sender_id, "recipient_id": m.recipient_id, - "body": m.body, # only set for system messages + "body": _system_body(m.body, lang) if m.kind == "system" else m.body, "ciphertext": m.ciphertext, "iv": m.iv, "read_at": iso(m.read_at), @@ -124,7 +141,7 @@ def ensure_welcome(db: Session, user: User) -> None: db.add(m) db.commit() db.refresh(m) - manager.push(user.id, {"type": "message", "message": _serialize_msg(m)}) + manager.push(user.id, {"type": "message", "message": _serialize_msg(m, lang_for_user(user))}) # --- E2EE key directory / blob store ----------------------------------------- @@ -313,7 +330,7 @@ def list_conversations(user: User = Depends(require_human), db: Session = Depend items.append( { "partner": partner, - "last_message": _serialize_msg(m), + "last_message": _serialize_msg(m, lang_for_user(user)), "unread": int(unread.get(pid, 0)), } ) @@ -366,7 +383,7 @@ def get_thread( # refreshes live instead of waiting for the next 20s poll. The client re-fetches the # count on this signal, so no number is carried here. manager.push(user.id, {"type": "unread"}) - return {"partner": partner, "items": [_serialize_msg(m) for m in msgs]} + return {"partner": partner, "items": [_serialize_msg(m, lang_for_user(user)) for m in msgs]} @router.post("") diff --git a/backend/tests/test_messages_welcome.py b/backend/tests/test_messages_welcome.py new file mode 100644 index 00000000..8d279450 --- /dev/null +++ b/backend/tests/test_messages_welcome.py @@ -0,0 +1,53 @@ +"""E31 S1: the welcome message is rendered once, when the account is created, in the language of that day. +An account that switched language afterwards kept reading it in the old one (the owner's: English under a +Hungarian UI). It is served in the reader's language now; any other system message stays as stored. +Needs the DB.""" + +from fastapi.testclient import TestClient + +import app.state as state +from app.auth import current_user +from app.db import get_db +from app.main import app +from app.models import Message, User +from app.routes.messages import WELCOME + + +def _client(db, user, monkeypatch, override_dep) -> TestClient: + monkeypatch.setattr(state, "_configured_cache", True) + override_dep(get_db, db.info["request"]) + override_dep(current_user, lambda: user) + return TestClient(app) + + +def _user_with_english_welcome(db) -> User: + """An account whose welcome was written in English, then switched to Hungarian.""" + user = User(email="welcome-hu@example.com", preferences={"language": "hu"}) + db.add(user) + db.flush() + db.add(Message(kind="system", sender_id=None, recipient_id=user.id, body=WELCOME["en"])) + db.commit() + return user + + +def test_the_welcome_is_read_in_the_readers_language(db, monkeypatch, override_dep): + user = _user_with_english_welcome(db) + client = _client(db, user, monkeypatch, override_dep) + + thread = client.get("/api/messages/conversations/0").json()["items"] + assert [m["body"] for m in thread] == [WELCOME["hu"]] + + convs = client.get("/api/messages/conversations").json()["items"] + assert [c["last_message"]["body"] for c in convs if c["partner"]["id"] == 0] == [WELCOME["hu"]] + + +def test_another_system_message_is_served_as_stored(db, monkeypatch, override_dep): + user = _user_with_english_welcome(db) + db.add( + Message(kind="system", sender_id=None, recipient_id=user.id, body="Maintenance tonight.") + ) + db.commit() + client = _client(db, user, monkeypatch, override_dep) + + bodies = [m["body"] for m in client.get("/api/messages/conversations/0").json()["items"]] + assert bodies == [WELCOME["hu"], "Maintenance tonight."]