"""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 `