"""The sysconfig registry: a group's keys stay together, every group has its section, and a closed-set knob is a CHOICE. The admin Configuration page used to keep its own hand-written group order, which had already drifted (`explore` was missing, so its tab sorted last). The page now follows the registry: SECTIONS orders the groups under their headings, and a group missing there would fall off the page — so each group of SPECS sits in exactly one section. A closed-set admin knob is a CHOICE, not free text. `log_level`, `search_source`, `download_layout` and the transcode `hw`/`preset` used to be free-text fields: the admin could type anything and every consumer carried its own `or "auto").lower()` guess at what a typo meant. Now the value set is a `Literal` in config.py, pydantic holds the env to it at boot, sysconfig holds a stored override to it, and the admin page renders a picker from the same list. The DB lane (`db` fixture) covers the store; the rest is pure logic. """ import pytest from pydantic import ValidationError from app import sysconfig from app.config import Settings, choices_of CHOICE_KEYS = {s.key for s in sysconfig.SPECS if s.type == "choice"} def test_each_group_is_one_run_of_keys(): runs = [ s.group for i, s in enumerate(sysconfig.SPECS) if i == 0 or sysconfig.SPECS[i - 1].group != s.group ] assert len(runs) == len(set(runs)), runs assert runs[:2] == ["access", "email"] def test_every_group_sits_in_exactly_one_section(): listed = [g for _, members in sysconfig.SECTIONS for g in members] assert len(listed) == len(set(listed)), listed assert set(listed) == {s.group for s in sysconfig.SPECS} assert [sid for sid, _ in sysconfig.SECTIONS] == ["access", "youtube", "media", "system"] def test_every_closed_set_knob_is_a_choice_whose_options_come_from_config(): assert CHOICE_KEYS == { "search_source", "download_layout", "log_level", "plex_transcode_hw", "plex_transcode_preset", } for s in sysconfig.SPECS: field = Settings.model_fields[s.default_attr] if s.type == "choice": assert s.choices, s.key assert field.default in s.choices, s.key else: # A Literal-typed setting exposed as "str" would be a free-text field again. assert s.choices == () and choices_of(field.annotation) == (), s.key def test_the_env_accepts_any_letter_case_and_rejects_an_unknown_value(): assert Settings(log_level="debug").log_level == "DEBUG" assert Settings(search_source=" API ").search_source == "api" with pytest.raises(ValidationError): Settings(log_level="verbose") class _Row: def __init__(self, key, value): self.key, self.value, self.encrypted = key, value, False class _FakeDb: """Just enough of a Session for sysconfig's reads: one stored override row per key.""" def __init__(self, **rows): self.rows = {k: _Row(k, v) for k, v in rows.items()} def get(self, _model, key): return self.rows.get(key) def scalars(self, _stmt): # describe() reads every override at once. return list(self.rows.values()) def test_a_stored_value_reads_back_canonical_and_a_stale_one_falls_back_to_the_default(): assert sysconfig.get_str(_FakeDb(plex_transcode_hw="OFF"), "plex_transcode_hw") == "off" # A value that is no longer (or never was) a choice is not passed through to the consumer. assert sysconfig.get_str(_FakeDb(plex_transcode_hw="maybe"), "plex_transcode_hw") == "auto" def test_describe_hands_the_picker_its_options(): items = {i["key"]: i for g in sysconfig.describe(_FakeDb())["groups"].values() for i in g} assert items["log_level"]["choices"] == ["TRACE", "DEBUG", "INFO", "WARNING", "ERROR"] assert items["log_level"]["value"] == "INFO" assert items["quota_daily_budget"]["choices"] == [] def test_describe_lists_every_group_under_its_section_in_order(): d = sysconfig.describe(_FakeDb()) assert [s["id"] for s in d["sections"]] == ["access", "youtube", "media", "system"] assert d["sections"][0]["groups"] == ["access", "google", "email"] listed = [g for s in d["sections"] for g in s["groups"]] assert sorted(listed) == sorted(d["groups"]) def test_set_value_stores_the_canonical_spelling_and_refuses_anything_else(db): sysconfig.set_value(db, "search_source", "API") assert sysconfig.get_str(db, "search_source") == "api" with pytest.raises(ValueError, match="scrape, api"): sysconfig.set_value(db, "search_source", "bing") db.rollback()