"""Asset sourcing tests for webinstaller. Slice 1a of the self-update refactor moved every inline HTML/CSS/script/unit file payload out of webinstaller/app.py and into furtka/assets/. These tests lock the new contract: _post_install_commands() and _resource_manager_commands() must write files whose content is byte-equal to the on-disk asset. If the asset tree drifts or a constant sneaks back in, a test breaks loudly. """ import base64 import re import sys from pathlib import Path import pytest sys.path.insert(0, str(Path(__file__).resolve().parent.parent / "webinstaller")) import app # noqa: E402 REPO_ROOT = Path(__file__).resolve().parent.parent ASSETS = REPO_ROOT / "furtka" / "assets" # (install target path, asset path under furtka/assets/) ASSET_TARGETS = [ ("/etc/caddy/Caddyfile", "Caddyfile"), ("/srv/furtka/www/index.html", "www/index.html"), ("/srv/furtka/www/settings/index.html", "www/settings/index.html"), ("/srv/furtka/www/style.css", "www/style.css"), ("/srv/furtka/www/status.json", "www/status.json"), ("/usr/local/bin/furtka-status", "bin/furtka-status"), ("/usr/local/bin/furtka-welcome", "bin/furtka-welcome"), ("/etc/systemd/system/furtka-status.service", "systemd/furtka-status.service"), ("/etc/systemd/system/furtka-status.timer", "systemd/furtka-status.timer"), ("/etc/systemd/system/furtka-welcome.service", "systemd/furtka-welcome.service"), ] def _extract_written_content(cmd, target): """Pull the base64 payload back out of a _write_file_cmd() shell string. Shape: `mkdir -p && printf %s | base64 -d > [ && chmod ...]`. """ match = re.search(r"printf %s (\S+) \| base64 -d > " + re.escape(target), cmd) assert match, f"cmd didn't look like a write of {target}: {cmd[:100]}" return base64.b64decode(match.group(1)).decode("utf-8") @pytest.fixture def install_cmds(): return app._post_install_commands("testhost") @pytest.mark.parametrize("target,asset_relpath", ASSET_TARGETS) def test_post_install_writes_asset_from_disk(install_cmds, target, asset_relpath): expected = (ASSETS / asset_relpath).read_text(encoding="utf-8") # /srv/furtka/www/index.html carries a one-off hostname sed *after* the # write; the bytes written match the asset verbatim. if target == "/srv/furtka/www/index.html": assert "__HOSTNAME__" in expected # slice-1a invariant: sed still applies matching = [c for c in install_cmds if f" > {target}" in c] assert matching, f"no command writes {target}" assert _extract_written_content(matching[0], target) == expected def test_read_asset_raises_for_missing_file(): with pytest.raises(FileNotFoundError): app._read_asset("does/not/exist.html") def test_assets_dir_resolves_to_repo_tree(): # The resolved path must point into the repo's furtka/assets — sanity # check that the "two levels up from webinstaller/app.py" math is correct. assert app._ASSETS_DIR == ASSETS def test_resource_manager_commands_use_asset_units(monkeypatch, tmp_path): # Create a fake payload so _resource_manager_commands doesn't bail out. fake = tmp_path / "payload.tar.gz" fake.write_bytes(b"not a real tarball") monkeypatch.setattr(app, "RESOURCE_MANAGER_PAYLOAD", fake) cmds = app._resource_manager_commands() # Expect reconcile + api unit files to be sourced from furtka/assets/systemd/. for unit in ("furtka-reconcile.service", "furtka-api.service"): expected = (ASSETS / "systemd" / unit).read_text(encoding="utf-8") matching = [c for c in cmds if f"/etc/systemd/system/{unit}" in c] assert matching, f"no command writes {unit}" assert _extract_written_content(matching[0], f"/etc/systemd/system/{unit}") == expected def test_version_asset_matches_pyproject(): # Slice 1a ships a VERSION file alongside the assets; it should match the # single source of truth in pyproject.toml. import tomllib with open(REPO_ROOT / "pyproject.toml", "rb") as f: version = tomllib.load(f)["project"]["version"] assert (ASSETS / "VERSION").read_text().strip() == version