Files
unshackle/tests/core/test_monalisa_decrypt.py
imSp4rky 1a7192380d fix(drm): keep the monalisa ciphertext when the decrypt tool fails
The tool writes to a temporary file through run_step, which gains a timeout that hides the command line. A missing tool now raises instead of skipping.
2026-11-01 21:26:36 -07:00

96 lines
3.1 KiB
Python

"""MonaLisa's external decrypt tool: a missing tool raises, and a failed run leaves the ciphertext untouched."""
import subprocess
from pathlib import Path
from uuid import UUID
import pytest
from unshackle.core.drm.monalisa import MonaLisa
from unshackle.core.utils import subprocess as tool_runner
pytestmark = pytest.mark.unit
@pytest.fixture
def track(tmp_path: Path) -> Path:
path = tmp_path / "track.mp4"
path.write_bytes(b"cipher")
return path
@pytest.fixture
def make_drm(monkeypatch: pytest.MonkeyPatch):
"""Build a MonaLisa with a known key; extract_keys needs a real device file, so it is skipped."""
monkeypatch.setattr(MonaLisa, "extract_keys", lambda self: None)
def build(tool: Path) -> MonaLisa:
drm = MonaLisa("ticket", Path("unused.mld"), tool)
drm._kid = UUID(int=1)
drm._key = "00" * 16
return drm
return build
@pytest.fixture
def tool(tmp_path: Path) -> Path:
path = tmp_path / "decrypt-tool"
path.write_bytes(b"")
return path
def fake_tool(monkeypatch: pytest.MonkeyPatch, output: bytes, returncode: int = 0, stderr: bytes = b"") -> None:
"""Replace the tool run with one that writes `output` to the `-o` path."""
def run(cmd, **kwargs):
Path(cmd[cmd.index("-o") + 1]).write_bytes(output)
return subprocess.CompletedProcess(cmd, returncode, b"", stderr)
monkeypatch.setattr(tool_runner.subprocess, "run", run)
def test_a_missing_tool_raises_and_keeps_the_file(make_drm, track: Path, tmp_path: Path) -> None:
with pytest.raises(RuntimeError, match="tool"):
make_drm(tmp_path / "absent").decrypt(track)
assert track.read_bytes() == b"cipher"
@pytest.mark.parametrize(
("output", "returncode", "stderr"),
[(b"partial", 1, b"bad key"), (b"", 0, b"")],
ids=["non-zero exit", "empty output"],
)
def test_a_failed_run_keeps_the_ciphertext(
make_drm, track: Path, tool: Path, monkeypatch: pytest.MonkeyPatch, output: bytes, returncode: int, stderr: bytes
) -> None:
fake_tool(monkeypatch, output, returncode, stderr)
with pytest.raises(RuntimeError, match=f"failed: {stderr.decode()}"):
make_drm(tool).decrypt(track)
assert track.read_bytes() == b"cipher"
assert not track.with_name("track_decrypted.mp4").exists()
def test_a_timeout_keeps_the_ciphertext_and_hides_the_key(
make_drm, track: Path, tool: Path, monkeypatch: pytest.MonkeyPatch
) -> None:
def hang(cmd, **kwargs):
raise subprocess.TimeoutExpired(cmd, kwargs.get("timeout"))
monkeypatch.setattr(tool_runner.subprocess, "run", hang)
drm = make_drm(tool)
with pytest.raises(RuntimeError, match="timed out") as raised:
drm.decrypt(track)
assert drm.key_pair not in str(raised.value)
assert raised.value.__suppress_context__
assert track.read_bytes() == b"cipher"
def test_a_good_run_replaces_the_file_and_drops_the_temp_output(
make_drm, track: Path, tool: Path, monkeypatch: pytest.MonkeyPatch
) -> None:
fake_tool(monkeypatch, b"plain")
make_drm(tool).decrypt(track)
assert track.read_bytes() == b"plain"
assert not track.with_name("track_decrypted.mp4").exists()