Files
kernalix7--winpodx/tests/test_reverse_open_config.py
wehub-resource-sync 3e779be6f3
CI / lint (push) Failing after 13m4s
CI / test (3.11, ubuntu-latest) (push) Failing after 2m4s
CI / test (3.13, ubuntu-latest) (push) Successful in 13m30s
CI / test (3.14, ubuntu-latest) (push) Successful in 17m21s
CI / test (3.12, ubuntu-latest) (push) Successful in 17m55s
CI / discover-apps-ps (push) Successful in 1m56s
CI / test (3.9, ubuntu-latest) (push) Successful in 13m17s
CI / test (3.10, ubuntu-latest) (push) Successful in 26m21s
CI / audit (push) Successful in 13m38s
Deploy site / deploy (push) Has been cancelled
CI / test (3.14, ubuntu-24.04-arm) (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 12:32:37 +08:00

261 lines
9.1 KiB
Python

# SPDX-License-Identifier: MIT
"""Tests for :class:`winpodx.reverse_open.config.ReverseOpenConfig`.
Phase 1, PR 1: pure host-side schema. These tests pin the contract
described in ``docs/design/REVERSE_OPEN_DESIGN.md`` § "Component
contracts → config.py" — defaults stay opt-in, hand-edited TOML is
defended against, and the round-trip through ``Config.save`` /
``Config.load`` preserves user values.
"""
from __future__ import annotations
from winpodx.core.config import Config
from winpodx.reverse_open.config import ReverseOpenConfig
# ---- defaults --------------------------------------------------------
def test_defaults_enabled_and_lists_empty():
"""Fresh instance ships default-on: ``enabled=True``, no user lists."""
cfg = ReverseOpenConfig()
assert cfg.enabled is True
assert cfg.allowlist == []
assert cfg.last_synced_at == ""
assert cfg.deny_dangerous is True
def test_defaults_fold_dangerous_into_denylist():
"""``deny_dangerous=True`` (default) seeds the denylist on init."""
cfg = ReverseOpenConfig()
# Every dangerous slug must appear in the effective denylist.
for slug in ReverseOpenConfig.DANGEROUS_DEFAULTS:
assert slug in cfg.denylist
# No duplicates introduced.
assert len(cfg.denylist) == len(set(cfg.denylist))
def test_dangerous_defaults_set_contents():
"""Pin the exact membership of the DANGEROUS_DEFAULTS set.
Adding to this list is a deliberate security decision — this test
ensures any future edit shows up as an explicit diff.
"""
assert ReverseOpenConfig.DANGEROUS_DEFAULTS == frozenset(
{
"code",
"vscodium",
"atom",
"gnome-terminal",
"konsole",
"xfce4-terminal",
"alacritty",
"kitty",
"wezterm",
"foot",
"tilix",
}
)
# ---- deny_dangerous toggle -------------------------------------------
def test_deny_dangerous_false_does_not_fold():
"""Opting out of the default-deny disables the fold step."""
cfg = ReverseOpenConfig(deny_dangerous=False)
for slug in ReverseOpenConfig.DANGEROUS_DEFAULTS:
assert slug not in cfg.denylist
assert cfg.denylist == []
def test_deny_dangerous_false_preserves_user_denies():
"""User-supplied denylist survives the no-fold path intact."""
cfg = ReverseOpenConfig(deny_dangerous=False, denylist=["my-app", "other-app"])
assert cfg.denylist == ["my-app", "other-app"]
def test_deny_dangerous_true_unions_with_user_denies():
"""User denies + dangerous defaults coexist; no duplication."""
cfg = ReverseOpenConfig(denylist=["my-app", "code"])
# User entry preserved.
assert "my-app" in cfg.denylist
# Dangerous default folded in.
assert "code" in cfg.denylist
# No duplicate of "code" — fold is union, not append.
assert cfg.denylist.count("code") == 1
# ---- slug regex defence ----------------------------------------------
def test_bad_slug_filtered_from_allowlist():
"""Slugs with whitespace / uppercase / punctuation are dropped.
A hand-edited TOML can put anything here. The slug regex is the
same one the listener uses to validate guest input, so an entry
that wouldn't survive guest validation must not survive load
either.
"""
cfg = ReverseOpenConfig(allowlist=["good-slug", "foo bar baz", "Bad", "ok2"])
assert cfg.allowlist == ["good-slug", "ok2"]
def test_bad_slug_filtered_from_denylist():
cfg = ReverseOpenConfig(deny_dangerous=False, denylist=["good-slug", "BAD!", "ok"])
assert cfg.denylist == ["good-slug", "ok"]
def test_non_string_slugs_dropped():
"""Non-string elements (int, None, dict) get binned silently."""
cfg = ReverseOpenConfig(allowlist=["a", 42, None, "b", {"x": 1}]) # type: ignore[list-item]
assert cfg.allowlist == ["a", "b"]
# ---- list-type coercion ----------------------------------------------
def test_non_list_allowlist_coerced_to_empty():
cfg = ReverseOpenConfig(allowlist="not-a-list") # type: ignore[arg-type]
assert cfg.allowlist == []
def test_non_list_denylist_coerced_to_empty_then_dangerous_folded():
"""Coercion happens BEFORE the dangerous fold, so the user gets
the safe defaults rather than a broken/empty denylist."""
cfg = ReverseOpenConfig(denylist=42) # type: ignore[arg-type]
# Coerced to [] then folded.
for slug in ReverseOpenConfig.DANGEROUS_DEFAULTS:
assert slug in cfg.denylist
# ---- ISO-8601 round-trip ---------------------------------------------
def test_valid_iso8601_preserved():
cfg = ReverseOpenConfig(last_synced_at="2026-05-07T09:41:00+00:00")
assert cfg.last_synced_at == "2026-05-07T09:41:00+00:00"
def test_z_suffix_preserved():
"""``Z`` is normalised only for *parsing*; the original is kept."""
cfg = ReverseOpenConfig(last_synced_at="2026-05-07T09:41:00Z")
assert cfg.last_synced_at == "2026-05-07T09:41:00Z"
def test_bad_iso8601_cleared():
cfg = ReverseOpenConfig(last_synced_at="not-a-timestamp")
assert cfg.last_synced_at == ""
def test_non_string_iso8601_cleared():
cfg = ReverseOpenConfig(last_synced_at=12345) # type: ignore[arg-type]
assert cfg.last_synced_at == ""
def test_empty_iso8601_stays_empty():
cfg = ReverseOpenConfig(last_synced_at="")
assert cfg.last_synced_at == ""
# ---- post_init idempotence -------------------------------------------
def test_post_init_idempotent():
"""Re-running ``__post_init__`` (as Config.load does) doesn't
double-fold the dangerous defaults."""
cfg = ReverseOpenConfig()
before = list(cfg.denylist)
cfg.__post_init__()
cfg.__post_init__()
assert cfg.denylist == before
# ---- Config.save / Config.load round-trip ----------------------------
def test_config_roundtrip_preserves_reverse_open_values(tmp_path, monkeypatch):
"""A user's choices survive disk round-trip through Config.save."""
monkeypatch.setenv("XDG_CONFIG_HOME", str(tmp_path))
cfg = Config()
cfg.reverse_open.enabled = True
cfg.reverse_open.allowlist = ["org-kde-kate", "org-gimp-gimp"]
cfg.reverse_open.last_synced_at = "2026-05-07T09:41:00+00:00"
cfg.reverse_open.deny_dangerous = False
# Strip the dangerous defaults that were folded in by the
# default-True deny_dangerous path, then re-set the user's denies.
cfg.reverse_open.denylist = ["custom-bad-app"]
cfg.save()
loaded = Config.load()
assert loaded.reverse_open.enabled is True
assert loaded.reverse_open.allowlist == ["org-kde-kate", "org-gimp-gimp"]
assert loaded.reverse_open.last_synced_at == "2026-05-07T09:41:00+00:00"
# deny_dangerous=False so the dangerous set is NOT re-folded on load.
assert loaded.reverse_open.deny_dangerous is False
assert loaded.reverse_open.denylist == ["custom-bad-app"]
def test_config_roundtrip_default_denylist_stable(tmp_path, monkeypatch):
"""A default config saved + loaded yields the same denylist
(no double-fold via the round-trip)."""
monkeypatch.setenv("XDG_CONFIG_HOME", str(tmp_path))
cfg = Config()
cfg.save()
loaded = Config.load()
assert sorted(loaded.reverse_open.denylist) == sorted(cfg.reverse_open.denylist)
# And every dangerous default is in there exactly once.
for slug in ReverseOpenConfig.DANGEROUS_DEFAULTS:
assert loaded.reverse_open.denylist.count(slug) == 1
def test_config_load_revalidates_reverse_open(tmp_path, monkeypatch):
"""A hand-edited TOML with malformed slugs is sanitised on load."""
monkeypatch.setenv("XDG_CONFIG_HOME", str(tmp_path))
path = Config.path()
path.parent.mkdir(parents=True, exist_ok=True)
path.write_text(
"[reverse_open]\n"
"enabled = true\n"
'allowlist = ["good-slug", "Bad Slug", "AlsoBad!"]\n'
'denylist = ["mine"]\n'
'last_synced_at = "garbage"\n'
"deny_dangerous = false\n",
encoding="utf-8",
)
loaded = Config.load()
assert loaded.reverse_open.enabled is True
# Bad slugs filtered out; good one preserved.
assert loaded.reverse_open.allowlist == ["good-slug"]
# User-supplied denylist preserved (no fold because deny_dangerous=False).
assert loaded.reverse_open.denylist == ["mine"]
# Garbage timestamp cleared.
assert loaded.reverse_open.last_synced_at == ""
def test_config_load_missing_section_yields_defaults(tmp_path, monkeypatch):
"""Old configs (pre-reverse_open) load with default values."""
monkeypatch.setenv("XDG_CONFIG_HOME", str(tmp_path))
path = Config.path()
path.parent.mkdir(parents=True, exist_ok=True)
path.write_text(
'[rdp]\nuser = "old-user"\n[pod]\nbackend = "podman"\n',
encoding="utf-8",
)
loaded = Config.load()
assert loaded.rdp.user == "old-user"
# reverse_open defaults applied even though the section wasn't on disk.
assert loaded.reverse_open.enabled is True
assert loaded.reverse_open.allowlist == []
assert loaded.reverse_open.deny_dangerous is True
# Dangerous defaults still folded (because deny_dangerous=True default).
for slug in ReverseOpenConfig.DANGEROUS_DEFAULTS:
assert slug in loaded.reverse_open.denylist