"""Making an instance somebody else's. Three things are worth pinning here and the rest follows from them: that a default is never stored, that the snapshot is dropped when it changes, and that a colour reaching a stylesheet is a colour and not whatever was typed. """ from __future__ import annotations import pytest from fastapi.testclient import TestClient from lembas.services import branding, settings_store @pytest.fixture(autouse=True) def clean(db): branding.forget() yield branding.forget() def _identity(client: TestClient, *, files=None, **fields): return client.post( "/admin/customization/identity", data={"instance_name": "", "tagline": "", **fields}, files=files, follow_redirects=False, ) # --- Defaults in code, overrides in the database -------------------------------- def test_the_shipped_wording_is_never_stored_as_an_override(db, client, registered): """The prompt-fragment rule, and the reason the page can render every flavour string as a box: leaving one alone must not freeze it, or a later release improving the wording would reach nobody who had ever pressed Save. Blanked rather than dropped, because `settings_store.update` merges — an omitted key leaves whatever was stored last time, so dropping would make "I typed the default back" and "I changed nothing" store different things. """ client.post( "/admin/customization/flavour", data={key: value for key, (_, _, value) in branding.FLAVOUR.items()} | {f"text_{key}": value for key, (_, _, value) in branding.FLAVOUR.items()}, follow_redirects=False, ) stored = settings_store.get_group(db, branding.BRANDING) written = {key for key, value in stored.items() if key.startswith("text_") and value} assert written == set() def test_an_override_is_written_and_read_back(db, client, registered): client.post( "/admin/customization/flavour", data={"text_error_404": "There is nothing here."}, follow_redirects=False, ) assert branding.snapshot().text["error_404"] == "There is nothing here." # And the others still say what they always did. assert branding.snapshot().text["error_403"] == branding.FLAVOUR["error_403"][2] def test_clearing_a_box_gives_the_shipped_wording_back(db, client, registered): client.post( "/admin/customization/flavour", data={"text_error_404": "Gone."}, follow_redirects=False ) client.post( "/admin/customization/flavour", data={"text_error_404": ""}, follow_redirects=False ) assert branding.snapshot().text["error_404"] == branding.FLAVOUR["error_404"][2] def test_a_string_not_submitted_is_left_alone(db, client, registered): """The page posts one card at a time, so a save of the identity must not wipe the wording. `save_flavour` only touches keys the form carried.""" client.post( "/admin/customization/flavour", data={"text_error_404": "Gone."}, follow_redirects=False ) _identity(client, instance_name="Rivendell") assert branding.snapshot().text["error_404"] == "Gone." assert branding.snapshot().name == "Rivendell" # --- The cache ------------------------------------------------------------------ def test_a_save_drops_the_snapshot(db, client, registered): """A process-level cache read by a Jinja global. A save that did not drop it would take effect at the next restart, which is a control that looks like it worked and did nothing -- the failure this codebase keeps cataloguing.""" assert branding.snapshot().name == "LLeMbas" _identity(client, instance_name="Rivendell") assert branding.snapshot().name == "Rivendell" assert "Rivendell" in client.get("/chat").text def test_the_name_reaches_the_title_and_the_model(db, client, registered): from lembas.services import harness _identity(client, instance_name="Rivendell") assert "New chat - Rivendell" in client.get("/chat").text assert harness.context_variables(db, None, [])["instance_name"] == "Rivendell" def test_a_name_stored_by_an_older_version_is_kept(db, registered): """`instance_name` lived in the general group before there was a branding one. An upgrade must not quietly rename somebody's instance back.""" settings_store.update(db, {"instance_name": "Rivendell"}) branding.forget() assert branding.snapshot().name == "Rivendell" def test_the_legacy_name_is_a_seed_and_not_a_fallback(db, client, registered): """Consulted only while the branding group has nothing of its own. A fallback read every time would resurrect the old name underneath a cleared one, which is exactly what a cleared reasoning effort documents.""" settings_store.update(db, {"instance_name": "Rivendell"}) branding.forget() _identity(client, instance_name="Bree") assert branding.snapshot().name == "Bree" _identity(client, instance_name="") assert branding.snapshot().name == "LLeMbas" # --- Themes --------------------------------------------------------------------- def _save_theme(client, **fields): return client.post( "/admin/customization/themes", data={"theme_0_id": "dusk", "theme_0_label": "Dusk", "theme_0_base": "moria", **fields}, follow_redirects=False, ) def test_a_custom_theme_becomes_a_rule(db, client, registered): _save_theme(client, theme_0_bg="#123456", theme_0_accent="#abcdef") css = client.get("/branding.css").text assert ':root[data-theme="dusk"]' in css assert "--bg: #123456;" in css assert "--accent: #abcdef;" in css def test_the_soft_variants_are_derived(db, client, registered): """The same colour at 14%. An administrator who set an accent without them would get focus rings in the old hue, which reads as the setting half-working rather than as a field they missed.""" _save_theme(client, theme_0_accent="#8FB3CC") assert "--accent-soft: rgba(143, 179, 204, 0.14);" in client.get("/branding.css").text def test_a_value_that_is_not_a_colour_is_dropped(db, client, registered): """It reaches a stylesheet, so a `}` in one would end the rule and silently break every rule after it, and `url(...)` in a colour slot is a request to a third party from every page.""" _save_theme( client, theme_0_bg="#123456} body { display: none } .x {", theme_0_accent="url(http://evil.test/x)", theme_0_ink="#fff", ) css = client.get("/branding.css").text assert "display: none" not in css assert "evil.test" not in css assert "--ink: #fff;" in css def test_a_token_nobody_offered_is_dropped(db): """Validated on **read**, so a theme written straight into the settings table -- or stored by an earlier version -- still has to produce a stylesheet that parses.""" brand = branding.build( {"themes": [{"id": "dusk", "tokens": {"bg": "#111", "position": "absolute"}}]} ) assert brand.theme("dusk").tokens == {"bg": "#111"} def test_a_theme_cannot_take_a_built_in_name(db): brand = branding.build({"themes": [{"id": "moria", "tokens": {"bg": "#111"}}]}) assert len(brand.themes) == 2 def test_a_custom_theme_is_offered_and_can_be_chosen(db, client, registered): _save_theme(client, theme_0_bg="#123456") page = client.get("/chat").text assert 'data-themes="moria:moria shire:shire dusk:moria"' in page assert client.post("/api/preferences/theme", json={"theme": "dusk"}).json()["ok"] is True # And the settings screen lists it by name. assert "Dusk" in client.get("/settings").text def test_an_unknown_theme_is_still_refused(db, client, registered): assert client.post("/api/preferences/theme", json={"theme": "dusk"}).json()["ok"] is False def test_a_light_theme_carries_its_base(db, client, registered): """`data-base` is what makes it inherit. Without it a custom light theme is four light colours on Moria's near-black surfaces.""" _save_theme(client, theme_0_base="shire", theme_0_bg="#fff") assert branding.snapshot().theme("dusk").base == "shire" assert 'dusk:shire' in client.get("/chat").text def test_tokens_css_matches_the_base_attribute(db): """The other half of that, and it lives in the stylesheet: `shire`'s block has to match `[data-base="shire"]` or the inheritance is a comment.""" from pathlib import Path import lembas css = (Path(lembas.__file__).parent / "web/static/css/tokens.css").read_text() assert ':root[data-base="shire"]' in css def test_clearing_an_id_removes_the_theme(db, client, registered): _save_theme(client, theme_0_bg="#123456") assert "dusk" in branding.snapshot().theme_ids _save_theme(client, theme_0_id="") assert "dusk" not in branding.snapshot().theme_ids # --- The stylesheet ------------------------------------------------------------- def test_custom_css_is_served_as_a_stylesheet(db, client, registered): """A route rather than an inline `