music-assistant-server

8.7 KBPY
test_fetch_airplay_cli.py
8.7 KB238 lines • python
1"""Tests for the local cliairplay download helper."""
2
3from __future__ import annotations
4
5import hashlib
6import stat
7from pathlib import Path
8
9import pytest
10
11from scripts import fetch_airplay_cli
12
13
14def test_fetch_airplay_cli_installs_verified_platform_binary(
15    tmp_path: Path, monkeypatch: pytest.MonkeyPatch
16) -> None:
17    """
18    Install the matching binary after verifying both release checksums.
19
20    The Dockerfile remains the single source for the development and container pins.
21    """
22    binary = b"cliairplay test binary"
23    asset_name = "cliairplay-macos-arm64"
24    manifest = f"{hashlib.sha256(binary).hexdigest()}  {asset_name}\n".encode()
25    _write_dockerfile(tmp_path, manifest)
26    downloads: list[str] = []
27
28    def download(url: str) -> bytes:
29        downloads.append(url)
30        return manifest if url.endswith("/SHA256SUMS") else binary
31
32    monkeypatch.setattr("scripts.fetch_airplay_cli.platform.system", lambda: "Darwin")
33    monkeypatch.setattr("scripts.fetch_airplay_cli.platform.machine", lambda: "arm64")
34    monkeypatch.setattr(fetch_airplay_cli, "_download", download)
35
36    result = fetch_airplay_cli.fetch_airplay_cli(tmp_path)
37
38    expected = tmp_path / fetch_airplay_cli.BINARY_DIR / asset_name
39    assert result == expected
40    assert expected.read_bytes() == binary
41    assert expected.stat().st_mode & stat.S_IXUSR
42    assert downloads == [
43        f"{fetch_airplay_cli.RELEASE_BASE_URL}/v0.1.0/SHA256SUMS",
44        f"{fetch_airplay_cli.RELEASE_BASE_URL}/v0.1.0/{asset_name}",
45    ]
46
47
48def test_fetch_airplay_cli_preserves_existing_binary(
49    tmp_path: Path, monkeypatch: pytest.MonkeyPatch
50) -> None:
51    """
52    Preserve an existing local binary without contacting the release service.
53
54    Developers may intentionally replace the pinned executable with a local build.
55    """
56    destination = tmp_path / fetch_airplay_cli.BINARY_DIR / "cliairplay-linux-x86_64"
57    destination.parent.mkdir(parents=True)
58    destination.write_bytes(b"local build")
59    _write_dockerfile(tmp_path, b"unused manifest")
60    monkeypatch.setattr("scripts.fetch_airplay_cli.platform.system", lambda: "Linux")
61    monkeypatch.setattr("scripts.fetch_airplay_cli.platform.machine", lambda: "x86_64")
62    monkeypatch.setattr(fetch_airplay_cli, "_read_binary_version", lambda _path: None)
63    monkeypatch.setattr(
64        fetch_airplay_cli,
65        "_download",
66        lambda _url: pytest.fail("existing binaries must not trigger a download"),
67    )
68
69    assert fetch_airplay_cli.fetch_airplay_cli(tmp_path) == destination
70    assert destination.read_bytes() == b"local build"
71
72
73def test_fetch_airplay_cli_replaces_older_release_binary(
74    tmp_path: Path, monkeypatch: pytest.MonkeyPatch
75) -> None:
76    """Replace a recognized older release binary with the pinned release."""
77    binary = b"new release"
78    asset_name = "cliairplay-macos-arm64"
79    manifest = f"{hashlib.sha256(binary).hexdigest()}  {asset_name}\n".encode()
80    _write_dockerfile(tmp_path, manifest)
81    destination = tmp_path / fetch_airplay_cli.BINARY_DIR / asset_name
82    destination.parent.mkdir(parents=True)
83    destination.write_bytes(b"old release")
84    downloads: list[str] = []
85
86    def download(url: str) -> bytes:
87        downloads.append(url)
88        return manifest if url.endswith("/SHA256SUMS") else binary
89
90    monkeypatch.setattr("scripts.fetch_airplay_cli.platform.system", lambda: "Darwin")
91    monkeypatch.setattr("scripts.fetch_airplay_cli.platform.machine", lambda: "arm64")
92    monkeypatch.setattr(fetch_airplay_cli, "_read_binary_version", lambda _path: (0, 0, 9))
93    monkeypatch.setattr(fetch_airplay_cli, "_download", download)
94
95    assert fetch_airplay_cli.fetch_airplay_cli(tmp_path) == destination
96    assert destination.read_bytes() == binary
97    assert downloads == [
98        f"{fetch_airplay_cli.RELEASE_BASE_URL}/v0.1.0/SHA256SUMS",
99        f"{fetch_airplay_cli.RELEASE_BASE_URL}/v0.1.0/{asset_name}",
100    ]
101
102
103def test_fetch_airplay_cli_rejects_checksum_mismatch(
104    tmp_path: Path, monkeypatch: pytest.MonkeyPatch
105) -> None:
106    """
107    Reject a binary that differs from the pinned release checksum.
108
109    A failed verification must not leave an executable in the provider directory.
110    """
111    asset_name = "cliairplay-linux-aarch64"
112    manifest = f"{hashlib.sha256(b'expected').hexdigest()}  {asset_name}\n".encode()
113    _write_dockerfile(tmp_path, manifest)
114    monkeypatch.setattr("scripts.fetch_airplay_cli.platform.system", lambda: "Linux")
115    monkeypatch.setattr("scripts.fetch_airplay_cli.platform.machine", lambda: "arm64")
116    monkeypatch.setattr(
117        fetch_airplay_cli,
118        "_download",
119        lambda url: manifest if url.endswith("/SHA256SUMS") else b"unexpected",
120    )
121
122    with pytest.raises(RuntimeError, match="Checksum mismatch"):
123        fetch_airplay_cli.fetch_airplay_cli(tmp_path)
124
125    assert not (tmp_path / fetch_airplay_cli.BINARY_DIR / asset_name).exists()
126
127
128def test_fetch_airplay_cli_rejects_manifest_digest_mismatch(
129    tmp_path: Path, monkeypatch: pytest.MonkeyPatch
130) -> None:
131    """
132    Reject a checksum manifest that differs from the Dockerfile pin.
133
134    A modified manifest must fail before the executable asset is downloaded.
135    """
136    asset_name = "cliairplay-linux-x86_64"
137    manifest = f"{hashlib.sha256(b'binary').hexdigest()}  {asset_name}\n".encode()
138    _write_dockerfile(tmp_path, manifest, manifest_digest="0" * 64)
139    monkeypatch.setattr("scripts.fetch_airplay_cli.platform.system", lambda: "Linux")
140    monkeypatch.setattr("scripts.fetch_airplay_cli.platform.machine", lambda: "x86_64")
141    downloads: list[str] = []
142
143    def download(url: str) -> bytes:
144        downloads.append(url)
145        return manifest
146
147    monkeypatch.setattr(fetch_airplay_cli, "_download", download)
148
149    with pytest.raises(RuntimeError, match="SHA256SUMS digest mismatch"):
150        fetch_airplay_cli.fetch_airplay_cli(tmp_path)
151
152    assert downloads == [f"{fetch_airplay_cli.RELEASE_BASE_URL}/v0.1.0/SHA256SUMS"]
153
154
155def test_read_docker_arg_reports_missing_file(tmp_path: Path) -> None:
156    """Report an unreadable Dockerfile as a clean setup error."""
157    dockerfile = tmp_path / "Dockerfile"
158
159    with pytest.raises(RuntimeError, match=f"Unable to read {dockerfile}"):
160        fetch_airplay_cli._read_docker_arg(dockerfile, "CLIAIRPLAY_VERSION")
161
162
163def test_main_reports_filesystem_error(
164    monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str]
165) -> None:
166    """Return a clean failure instead of a traceback for filesystem errors."""
167    monkeypatch.setattr(
168        fetch_airplay_cli,
169        "fetch_airplay_cli",
170        lambda _root: (_ for _ in ()).throw(PermissionError("read-only filesystem")),
171    )
172
173    assert fetch_airplay_cli.main() == 1
174    assert capsys.readouterr().err == "ERROR: read-only filesystem\n"
175
176
177@pytest.mark.parametrize(
178    ("system", "machine", "expected"),
179    [
180        ("Darwin", "aarch64", "cliairplay-macos-arm64"),
181        ("Darwin", "x86_64", "cliairplay-macos-x86_64"),
182        ("Linux", "arm64", "cliairplay-linux-aarch64"),
183        ("Linux", "amd64", "cliairplay-linux-x86_64"),
184        ("Windows", "AMD64", None),
185        ("Linux", "riscv64", None),
186    ],
187)
188def test_asset_name(system: str, machine: str, expected: str | None) -> None:
189    """Map supported development platforms to their release asset."""
190    assert fetch_airplay_cli._asset_name(system, machine) == expected
191
192
193@pytest.mark.parametrize(
194    ("value", "expected"),
195    [
196        ("v0.3.0", (0, 3, 0)),
197        ("1.2.3", (1, 2, 3)),
198        ("0.3", None),
199        ("development", None),
200    ],
201)
202def test_parse_release_version(value: str, expected: tuple[int, int, int] | None) -> None:
203    """Parse supported release versions without treating local labels as releases."""
204    assert fetch_airplay_cli._parse_release_version(value) == expected
205
206
207@pytest.mark.parametrize(
208    ("current", "pinned", "expected"),
209    [
210        ((0, 2, 0), "v0.3.0", True),
211        ((0, 3, 0), "v0.3.0", False),
212        ((0, 3, 1), "v0.3.0", False),
213        (None, "v0.3.0", False),
214    ],
215)
216def test_binary_is_outdated(
217    tmp_path: Path,
218    monkeypatch: pytest.MonkeyPatch,
219    current: tuple[int, int, int] | None,
220    pinned: str,
221    expected: bool,
222) -> None:
223    """Only older recognized releases are replaced."""
224    binary_path = tmp_path / "cliairplay"
225    monkeypatch.setattr(fetch_airplay_cli, "_read_binary_version", lambda _path: current)
226
227    assert fetch_airplay_cli._binary_is_outdated(binary_path, pinned) is expected
228
229
230def _write_dockerfile(root: Path, manifest: bytes, *, manifest_digest: str | None = None) -> None:
231    """Write the release pins consumed by the helper."""
232    if manifest_digest is None:
233        manifest_digest = hashlib.sha256(manifest).hexdigest()
234    root.joinpath("Dockerfile").write_text(
235        f"ARG CLIAIRPLAY_VERSION=v0.1.0\nARG CLIAIRPLAY_CHECKSUMS_SHA256={manifest_digest}\n",
236        encoding="utf-8",
237    )
238