import io import subprocess from unittest.mock import MagicMock import pytest from repeater import service_utils as su def test_is_buildroot_via_metadata_file(monkeypatch): monkeypatch.setattr(su.os.path, "exists", lambda p: p == su.BUILDROOT_METADATA_PATH) assert su.is_buildroot() is True def test_is_buildroot_via_os_release(monkeypatch): monkeypatch.setattr( su.os.path, "exists", lambda p: p == "/etc/os-release", ) monkeypatch.setattr( "builtins.open", lambda *args, **kwargs: io.StringIO("NAME=x\nID=buildroot\n"), ) assert su.is_buildroot() is True def test_get_buildroot_image_info_parse_and_error(monkeypatch): monkeypatch.setattr( "builtins.open", lambda *args, **kwargs: io.StringIO("\nfoo=bar\ninvalid\nimage_version=1.2.3\n"), ) info = su.get_buildroot_image_info() assert info["foo"] == "bar" assert info["image_version"] == "1.2.3" assert su.get_buildroot_image_version() == "1.2.3" def _raise(*_args, **_kwargs): raise OSError("nope") monkeypatch.setattr("builtins.open", _raise) assert su.get_buildroot_image_info() == {} def test_is_container_detection_paths(monkeypatch): # /.dockerenv path monkeypatch.setattr(su.os.path, "exists", lambda p: p == "/.dockerenv") monkeypatch.delenv("container", raising=False) assert su.is_container() is True # env var path monkeypatch.setattr(su.os.path, "exists", lambda _p: False) monkeypatch.setenv("container", "docker") assert su.is_container() is True @pytest.mark.parametrize( "environ_bytes,cgroup_text,host_path,expected", [ (b"abc\x00container=docker\x00", "", False, True), (b"abc", "1:name=systemd:/docker/abc", False, True), (b"abc", "1:name=systemd:/", True, True), (b"abc", "1:name=systemd:/", False, False), ], ) def test_is_container_proc_and_host_paths( monkeypatch, environ_bytes, cgroup_text, host_path, expected ): monkeypatch.setattr( su.os.path, "exists", lambda p: p == "/run/host/container-manager" and host_path ) monkeypatch.delenv("container", raising=False) def _open(path, mode="r", encoding=None): if path == "/proc/1/environ": return io.BytesIO(environ_bytes) if path == "/proc/1/cgroup": return io.StringIO(cgroup_text) raise OSError("unexpected") monkeypatch.setattr("builtins.open", _open) assert su.is_container() is expected def test_get_container_restart_message(): msg = su.get_container_restart_message() assert "Container restart initiated" in msg assert "Docker or Home Assistant" in msg def test_restart_service_container_path(monkeypatch): monkeypatch.setattr(su, "is_container", lambda: True) sched = MagicMock() monkeypatch.setattr(su, "_schedule_container_exit", sched) ok, msg = su.restart_service() assert ok is True assert "Container restart initiated" in msg sched.assert_called_once() def test_restart_service_buildroot_paths(monkeypatch): monkeypatch.setattr(su, "is_container", lambda: False) monkeypatch.setattr(su, "is_buildroot", lambda: True) # missing init script monkeypatch.setattr(su.os.path, "exists", lambda _p: False) ok, msg = su.restart_service() assert ok is False assert "init script not found" in msg # popen success monkeypatch.setattr(su.os.path, "exists", lambda p: p == su.INIT_SCRIPT) monkeypatch.setattr(su.subprocess, "Popen", MagicMock()) ok, msg = su.restart_service() assert ok is True assert "Service restart initiated" in msg # popen failure def _raise(*_args, **_kwargs): raise RuntimeError("boom") monkeypatch.setattr(su.subprocess, "Popen", _raise) ok, msg = su.restart_service() assert ok is False assert "Restart failed" in msg def test_restart_service_systemctl_and_sudo_paths(monkeypatch): monkeypatch.setattr(su, "is_container", lambda: False) monkeypatch.setattr(su, "is_buildroot", lambda: False) def _result(code=0, err=""): return subprocess.CompletedProcess(args=[], returncode=code, stdout="", stderr=err) # systemctl success monkeypatch.setattr(su.subprocess, "run", lambda *args, **kwargs: _result(0)) ok, msg = su.restart_service() assert ok is True assert "Service restart initiated" in msg # systemctl timeout def _timeout(*_args, **_kwargs): raise subprocess.TimeoutExpired(cmd="x", timeout=5) monkeypatch.setattr(su.subprocess, "run", _timeout) ok, msg = su.restart_service() assert ok is True assert "timeout" in msg # systemctl missing binary def _missing(*_args, **_kwargs): raise FileNotFoundError("systemctl") monkeypatch.setattr(su.subprocess, "run", _missing) ok, msg = su.restart_service() assert ok is False assert "systemctl not available" in msg # systemctl denied then sudo success calls = {"n": 0} def _denied_then_sudo(*args, **kwargs): calls["n"] += 1 if calls["n"] == 1: return _result(1, "Access denied") return _result(0) monkeypatch.setattr(su.subprocess, "run", _denied_then_sudo) ok, msg = su.restart_service() assert ok is True assert "Service restart initiated" in msg # systemctl generic fail then sudo fail calls = {"n": 0} def _fail_then_fail(*args, **kwargs): calls["n"] += 1 if calls["n"] == 1: return _result(1, "broken") return _result(2, "sudo denied") monkeypatch.setattr(su.subprocess, "run", _fail_then_fail) ok, msg = su.restart_service() assert ok is False assert "Restart failed" in msg # systemctl generic fail then sudo timeout calls = {"n": 0} def _fail_then_timeout(*args, **kwargs): calls["n"] += 1 if calls["n"] == 1: return _result(1, "broken") raise subprocess.TimeoutExpired(cmd="sudo", timeout=5) monkeypatch.setattr(su.subprocess, "run", _fail_then_timeout) ok, msg = su.restart_service() assert ok is True assert "timeout" in msg # systemctl generic fail then sudo missing calls = {"n": 0} def _fail_then_sudo_missing(*args, **kwargs): calls["n"] += 1 if calls["n"] == 1: return _result(1, "broken") raise FileNotFoundError("sudo") monkeypatch.setattr(su.subprocess, "run", _fail_then_sudo_missing) ok, msg = su.restart_service() assert ok is False assert "Neither polkit nor sudo" in msg # systemctl generic fail then sudo unexpected exception calls = {"n": 0} def _fail_then_exception(*args, **kwargs): calls["n"] += 1 if calls["n"] == 1: return _result(1, "broken") raise RuntimeError("bad") monkeypatch.setattr(su.subprocess, "run", _fail_then_exception) ok, msg = su.restart_service() assert ok is False assert "Restart command failed" in msg