mirror of
https://github.com/kuhyx/screen-locker.git
synced 2026-11-10 20:47:14 +01:00
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018RHwVPyiiQMPh4M3JQwQku
187 lines
6.6 KiB
Python
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
|