from __future__ import annotations import hashlib import os import re import subprocess from pathlib import Path import pytest import yaml ROOT = Path(__file__).parents[1] REPOSITORY_URL = "https://git.radixadm.dk/dennis/homeassistant-stelloauth-addon.git" def load_yaml(path: str) -> dict: with (ROOT / path).open(encoding="utf-8") as handle: value = yaml.safe_load(handle) assert isinstance(value, dict) return value def test_repository_metadata() -> None: metadata = load_yaml("repository.yaml") assert metadata["url"] == REPOSITORY_URL assert metadata["name"] == "Stelloauth for Home Assistant" assert metadata["maintainer"] == "Dennis / Radix ApS" def test_addon_contract() -> None: config = load_yaml("stelloauth/config.yaml") assert config["slug"] == "stelloauth" assert config["version"] == "0.1.0" assert config["arch"] == ["amd64", "aarch64"] assert config["startup"] == "application" assert config["boot"] == "auto" assert config["init"] is True assert config["watchdog"] == "http://[HOST]:[PORT:8080]/" assert config["ports"] == {"8080/tcp": None} for key in ("ingress", "host_network", "privileged", "full_access", "docker_api", "devices", "map"): assert key not in config def test_defaults_and_schema_are_aligned() -> None: config = load_yaml("stelloauth/config.yaml") assert config["options"] == { "queue_timeout": "60s", "rate_limit_count": 5, "rate_limit_duration": "1h", } assert set(config["schema"]) == set(config["options"]) assert config["schema"]["rate_limit_count"] == "int(1,20)" def test_repository_hostname_derivation() -> None: repository_id = hashlib.sha1(REPOSITORY_URL.lower().encode()).hexdigest()[:8] assert repository_id == "0031621f" assert f"{repository_id}-stelloauth" == "0031621f-stelloauth" def test_translations_cover_every_option() -> None: keys = set(load_yaml("stelloauth/config.yaml")["options"]) for language in ("da", "en"): translation = load_yaml(f"stelloauth/translations/{language}.yaml") assert set(translation["configuration"]) == keys for entry in translation["configuration"].values(): assert set(entry) == {"name", "description"} assert all(isinstance(value, str) and value.strip() for value in entry.values()) def test_dockerfile_uses_approved_pins_and_builds_patched_stelloauth() -> None: dockerfile = (ROOT / "stelloauth/Dockerfile").read_text(encoding="utf-8") assert ( "golang:1.27.1-bookworm@sha256:" "69a7b9788769bec032d238959b61854e9ae87f57be9029ec04e9885fabf99195" ) in dockerfile assert ( "cloakhq/cloakbrowser:0.5.10@sha256:" "2ed5b2d047cbdde22cde7ef1a796526c716aadaa5bccbe1db5ade49282b64a76" ) in dockerfile assert "367d4f8c02a3b072c59142c49dffc129edc8548b" in dockerfile assert "go test ./..." in dockerfile assert not re.search(r"^FROM\s+\S+:latest(?:\s|$)", dockerfile, re.MULTILINE) def test_dockerfile_declares_home_assistant_runtime_contract() -> None: dockerfile = (ROOT / "stelloauth/Dockerfile").read_text(encoding="utf-8") for label in ( "io.hass.name", "io.hass.description", "io.hass.arch", "io.hass.type", "io.hass.version", ): assert label in dockerfile assert re.search(r"^EXPOSE 8080$", dockerfile, re.MULTILINE) assert not re.search(r"^EXPOSE .*\b9222\b", dockerfile, re.MULTILINE) assert "ENTRYPOINT []" in dockerfile assert 'CMD ["/usr/local/bin/addon-supervisor"]' in dockerfile def test_dockerfile_patches_parent_cloakserve_instead_of_copying_a_binary() -> None: dockerfile = (ROOT / "stelloauth/Dockerfile").read_text(encoding="utf-8") assert "COPY patches/cloakserve-loopback.patch" in dockerfile for line in dockerfile.splitlines(): if line.lstrip().startswith("COPY "): source = line.split()[1] assert Path(source).name != "cloakserve" def test_runtime_accepts_docker_port_unpublished_status() -> None: runtime_test = (ROOT / "tests/test_runtime.sh").read_text(encoding="utf-8") assert 'docker port "$container" 9222/tcp 2>/dev/null || true' in runtime_test def test_runtime_requires_process_baseline_after_cdp_close_and_zero_stopped_pid() -> None: runtime_test = (ROOT / "tests/test_runtime.sh").read_text(encoding="utf-8") assert 'docker top "$container" -eo pid,args' in runtime_test for prefix in ("first", "second"): container = f"${prefix}_container" baseline = ( f'{prefix}_baseline="$(capture_process_baseline "{container}")"' ) ready_index = runtime_test.index( f'wait_ready "{container}" "${prefix}_port"' ) baseline_index = runtime_test.index(baseline) close_index = runtime_test.index( f'probe_and_close_cdp "{container}"', baseline_index ) return_index = runtime_test.index( f'assert_processes_return_to_baseline "{container}" "${prefix}_baseline"', close_index, ) assert ready_index < baseline_index < close_index < return_index assert "{{.State.Pid}}" in runtime_test assert '[ "$state" = "exited 0 0" ]' in runtime_test def test_runtime_process_normalization_retains_every_unknown_wrapped_child( tmp_path: Path, ) -> None: runtime_test = ROOT / "tests/test_runtime.sh" top_file = tmp_path / "docker-top.txt" environment = os.environ.copy() environment.update( { "DOCKER_HOST": "unix:///nonexistent-runtime-normalization.sock", "SKIP_BUILD": "1", } ) service_lines = [ "101 /run/rosetta/rosetta /usr/local/bin/python3 python3 /usr/local/bin/addon-supervisor", "102 /run/rosetta/rosetta /usr/local/bin/python3 python3 /usr/local/bin/cloakserve --headless=true --idle-timeout=30 --data-dir=/tmp/cloakserve", "103 /usr/bin/qemu-x86_64-static /usr/local/bin/stelloauth", ] def normalize(extra_line: str | None = None) -> list[str]: lines = ["PID COMMAND", *service_lines] if extra_line is not None: lines.append(extra_line) top_file.write_text("\n".join(lines) + "\n", encoding="utf-8") completed = subprocess.run( [str(runtime_test), "--normalize-processes", str(top_file)], cwd=ROOT, env=environment, text=True, capture_output=True, check=False, ) assert completed.returncode == 0, completed.stderr return completed.stdout.splitlines() baseline = ["addon-supervisor", "cloakserve", "stelloauth"] assert normalize() == baseline wrapped_children = [ "/run/rosetta/rosetta /opt/vendor/headless-shell --user-data-dir=/tmp/profile", "/run/rosetta/rosetta /opt/vendor/browser --profile runtime-readiness", "/run/rosetta/rosetta /opt/vendor/crashpad_handler --database=/tmp/profile", "/run/rosetta/rosetta /opt/vendor/opaque-child --flag", "/run/rosetta/rosetta /opt/vendor/opaque-child --parent=/usr/local/bin/cloakserve", "/usr/bin/qemu-x86_64-static /opt/vendor/opaque-qemu-child", ] for offset, child in enumerate(wrapped_children, start=104): normalized = normalize(f"{offset} {child}") assert normalized == sorted([*baseline, f"unexpected:{child}"]) assert normalized != baseline