screen-locker/screen_locker/tests/test_poke_main.py
2026-10-09 19:05:51 +02:00

187 lines
6.6 KiB
Python

"""The poke daemon CLI: key loading, startup refusals, stream wiring."""
from __future__ import annotations
import logging
import threading
from typing import TYPE_CHECKING, Any, ClassVar
from unittest.mock import MagicMock
import pytest
from screen_locker import _poke_credit, poke_server
from screen_locker._poke_server import POKE_PATH
if TYPE_CHECKING:
from pathlib import Path
_KEY_HEX = "ab" * 32
def _key_file(tmp_path: Path, text: str = _KEY_HEX, mode: int = 0o600) -> Path:
path = tmp_path / "key"
path.write_text(text, encoding="ascii")
path.chmod(mode)
return path
class FakeServer:
"""Stands in for PokeServer: records how the daemon drove it."""
instances: ClassVar[list[FakeServer]] = []
def __init__(self, address: tuple[str, int], key: bytes) -> None:
self.address = address
self.key = key
self.gate = threading.Lock()
self.closed = False
self.instances.append(self)
def serve_forever(self) -> None:
return
def server_close(self) -> None:
self.closed = True
@pytest.fixture(autouse=True)
def _daemon_stubs(monkeypatch: pytest.MonkeyPatch, tmp_path: Path) -> None:
"""No real budget sum or server socket in any test here."""
FakeServer.instances.clear()
locker = MagicMock()
locker.log_file = tmp_path / "log.json"
monkeypatch.setattr(_poke_credit, "fresh_locker", lambda: locker)
monkeypatch.setattr(_poke_credit, "current_shutdown", MagicMock())
monkeypatch.setattr(_poke_credit, "current_gaming_minutes", MagicMock())
monkeypatch.setattr(poke_server, "PokeServer", FakeServer)
@pytest.fixture(autouse=True)
def stream(monkeypatch: pytest.MonkeyPatch) -> MagicMock:
"""The Firebase stream class; never a real connection."""
stub = MagicMock()
monkeypatch.setattr(poke_server, "SessionStream", stub)
return stub
class TestLoadKey:
def test_reads_a_600_key(self, tmp_path: Path) -> None:
assert poke_server.load_key(_key_file(tmp_path)) == bytes.fromhex(_KEY_HEX)
@pytest.mark.parametrize("mode", [0o640, 0o604, 0o660, 0o644])
def test_group_or_world_access_is_refused_with_the_fix(
self, tmp_path: Path, mode: int
) -> None:
with pytest.raises(ValueError, match="chmod 600"):
poke_server.load_key(_key_file(tmp_path, mode=mode))
def test_malformed_key_is_refused(self, tmp_path: Path) -> None:
with pytest.raises(ValueError, match="hex"):
poke_server.load_key(_key_file(tmp_path, text="short"))
def test_missing_file_raises_oserror(self, tmp_path: Path) -> None:
with pytest.raises(OSError, match="key"):
poke_server.load_key(tmp_path / "key")
class TestMain:
def test_missing_key_refuses_to_start_at_critical(
self, tmp_path: Path, caplog: pytest.LogCaptureFixture
) -> None:
with caplog.at_level(logging.CRITICAL):
code = poke_server.main(["--key-file", str(tmp_path / "nope")])
assert code == 1
assert "bad poke key" in caplog.text
assert "workout_poke_keygen.sh" in caplog.text
assert not FakeServer.instances
def test_loose_key_mode_refuses_to_start(self, tmp_path: Path) -> None:
key = _key_file(tmp_path, mode=0o644)
assert poke_server.main(["--key-file", str(key)]) == 1
assert not FakeServer.instances
def test_bind_failure_refuses_to_start_at_critical(
self,
tmp_path: Path,
monkeypatch: pytest.MonkeyPatch,
caplog: pytest.LogCaptureFixture,
) -> None:
def busy(_address: tuple[str, int], _key: bytes) -> Any:
msg = "address in use"
raise OSError(msg)
monkeypatch.setattr(poke_server, "PokeServer", busy)
with caplog.at_level(logging.CRITICAL):
code = poke_server.main(
["--key-file", str(_key_file(tmp_path)), "--port", "9"]
)
assert code == 1
assert "cannot bind :9 (address in use)" in caplog.text
def test_serves_with_the_key_and_starts_the_stream(
self,
tmp_path: Path,
stream: MagicMock,
caplog: pytest.LogCaptureFixture,
) -> None:
argv = ["--key-file", str(_key_file(tmp_path)), "--port", "1234"]
with caplog.at_level(logging.INFO):
code = poke_server.main([*argv, "--bind", "127.0.0.1"])
(server,) = FakeServer.instances
assert code == 0
assert server.address == ("127.0.0.1", 1234)
assert server.key == bytes.fromhex(_KEY_HEX)
assert server.closed
stream.assert_called_once_with(server.gate)
stream.return_value.start.assert_called_once_with()
assert f"127.0.0.1:1234{POKE_PATH}" in caplog.text
assert isinstance(_poke_credit.current_shutdown, MagicMock)
_poke_credit.current_shutdown.assert_called_once_with()
def test_default_bind_is_all_interfaces_on_the_default_port(
self, tmp_path: Path, caplog: pytest.LogCaptureFixture
) -> None:
with caplog.at_level(logging.INFO):
poke_server.main(["--key-file", str(_key_file(tmp_path))])
assert FakeServer.instances[0].address == ("", poke_server.DEFAULT_PORT)
assert f"*:{poke_server.DEFAULT_PORT}" in caplog.text
def test_no_stream_skips_firebase_and_says_so(
self,
tmp_path: Path,
stream: MagicMock,
caplog: pytest.LogCaptureFixture,
) -> None:
with caplog.at_level(logging.WARNING):
code = poke_server.main(
["--key-file", str(_key_file(tmp_path)), "--no-stream"]
)
assert code == 0
stream.assert_not_called()
assert "disabled by --no-stream" in caplog.text
assert FakeServer.instances[0].closed
def test_stream_failure_never_stops_the_lan_poke(
self,
tmp_path: Path,
stream: MagicMock,
caplog: pytest.LogCaptureFixture,
) -> None:
stream.return_value.start.side_effect = RuntimeError("no network")
with caplog.at_level(logging.ERROR):
code = poke_server.main(["--key-file", str(_key_file(tmp_path))])
assert code == 0
assert "Could not start the Firebase session stream" in caplog.text
assert FakeServer.instances[0].closed
def test_server_is_closed_even_when_serving_raises(
self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch
) -> None:
def explode(_self: FakeServer) -> None:
raise KeyboardInterrupt
monkeypatch.setattr(FakeServer, "serve_forever", explode)
with pytest.raises(KeyboardInterrupt):
poke_server.main(["--key-file", str(_key_file(tmp_path))])
assert FakeServer.instances[0].closed