Files
unshackle/tests/remote/unit/test_serve_reload.py
T
imSp4rky f86359de5d feat(serve): reload unshackle.yaml on SIGHUP without a restart
Extract the serve validation and app config builder so startup and reload share them. A bad file or a restart-only change keeps the running config; open CDM sessions survive.
2026-10-06 17:56:47 -06:00

79 lines
2.8 KiB
Python

"""Pins the SIGHUP config reload: a valid file swaps the users in, a bad or restart-only file leaves the old one."""
from __future__ import annotations
import logging
from pathlib import Path
import pytest
import yaml
from aiohttp import web
from unshackle.commands import serve as serve_cmd
from unshackle.core import config as config_module
from unshackle.core.config import config
pytestmark = pytest.mark.unit
LOG = logging.getLogger("serve")
@pytest.fixture
def running(monkeypatch: pytest.MonkeyPatch, tmp_path: Path) -> tuple[web.Application, Path]:
"""An app serving the key ``old`` with one open CDM session, and a writable config path."""
wvds = tmp_path / "wvds"
prds = tmp_path / "prds"
wvds.mkdir()
prds.mkdir()
(wvds / "dev.wvd").write_bytes(b"x")
monkeypatch.setattr(config.directories, "wvds", wvds)
monkeypatch.setattr(config.directories, "prds", prds)
monkeypatch.setattr(config, "serve", {"api_secret": "api", "users": {"old": {"devices": ["dev"]}}})
path = tmp_path / "unshackle.yaml"
monkeypatch.setattr(config_module, "config_path", path)
app = web.Application()
app["config"] = {"users": {"old": {"devices": ["dev"]}, "api": {"devices": ["dev"]}}, "devices": {}}
app["cdms"] = {("old", "dev"): object()}
return app, path
def _write(path: Path, **serve: object) -> None:
path.write_text(yaml.safe_dump({"serve": {"api_secret": "api", **serve}}), encoding="utf8")
def _reload(app: web.Application) -> bool:
return serve_cmd.reload_config(app, no_key=False, serve_widevine=True, serve_playready=True)
def test_valid_reload_swaps_the_accepted_keys(running: tuple[web.Application, Path]) -> None:
app, path = running
_write(path, users={"new": {"devices": ["dev"]}})
assert _reload(app) is True
assert set(app["config"]["users"]) == {"new", "api"}
assert app["config"]["devices"] == {"dev": path.parent / "wvds" / "dev.wvd"}
assert config.serve["users"] == {"new": {"devices": ["dev"]}}
assert ("old", "dev") in app["cdms"]
def test_invalid_file_keeps_the_running_config(running: tuple[web.Application, Path]) -> None:
app, path = running
_write(path, users={"new": {"devices": ["dev"], "tier": "gold"}})
assert _reload(app) is False
assert set(app["config"]["users"]) == {"old", "api"}
assert config.serve["users"] == {"old": {"devices": ["dev"]}}
def test_restart_only_change_is_reported_not_applied(
running: tuple[web.Application, Path], caplog: pytest.LogCaptureFixture
) -> None:
app, path = running
_write(path, users={"new": {"devices": ["dev"]}}, remote_only=True)
with caplog.at_level(logging.WARNING, logger=LOG.name):
assert _reload(app) is False
assert "restart" in caplog.text and "remote_only" in caplog.text
assert set(app["config"]["users"]) == {"old", "api"}