fix(api): validate quality and bitrate params like dl and pass cdm_only to services

Reject bad quality, bitrate/range conflicts and malformed ranges with a 400, convert quality in the worker, and pass cdm_only into ctx.params.
This commit is contained in:
imSp4rky
2026-10-20 19:59:38 -06:00
parent 639d51c41b
commit 3ffc7a9735
4 changed files with 79 additions and 13 deletions
@@ -0,0 +1,50 @@
"""The REST download path converts ``quality`` and passes ``cdm_only`` like the CLI."""
from __future__ import annotations
from pathlib import Path
from typing import Any
import click
import pytest
import unshackle.commands.dl as dl_module
from unshackle.core.api import download_manager, handlers
from unshackle.core.api.handlers import validate_download_parameters
from unshackle.core.services import Services
pytestmark = pytest.mark.unit
class _Captured(Exception):
pass
@pytest.mark.parametrize("quality", ["1080p", ["1080"], [1080, 720], "1080,720"])
def test_the_api_accepts_the_quality_forms_the_cli_accepts(quality: Any) -> None:
assert validate_download_parameters({"quality": quality}) is None
def test_a_rest_job_converts_quality_and_passes_cdm_only(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
class FakeDl:
cli = click.Command("dl")
def __init__(self, ctx: click.Context, **kwargs: object) -> None:
self.ctx = ctx
def result(self, **kwargs: object) -> None:
raise _Captured(self.ctx.params["quality"], kwargs["quality"], self.ctx.params["cdm_only"])
class FakeService:
def __init__(self, ctx: click.Context) -> None:
pass
monkeypatch.setattr(dl_module, "dl", FakeDl)
monkeypatch.setattr(Services, "get_path", staticmethod(lambda s: tmp_path))
monkeypatch.setattr(Services, "load", staticmethod(lambda s: FakeService))
monkeypatch.setattr(handlers, "load_full_cdm", lambda *a: None)
with pytest.raises(_Captured) as exc:
download_manager.perform_download("job-1", "EXAMPLE1", "t1", {"quality": "1080p", "cdm_only": True})
assert exc.value.args == ([1080], [1080], True)
+11 -5
View File
@@ -279,8 +279,12 @@ def test_validate_download_params_accepts_defaults() -> None:
({"sub_format": "doc"}, "Invalid sub_format"),
({"vbitrate": -1}, "vbitrate"),
({"abitrate": "no"}, "abitrate"),
({"vbitrate_range": "no-dash-but-letters"}, None),
({"vbitrate_range": "nope"}, "MIN-MAX"),
({"vbitrate_range": "no-dash-but-letters"}, "MIN-MAX"),
({"vbitrate": 5000, "vbitrate_range": "1-2"}, "vbitrate and vbitrate_range"),
({"abitrate": 128, "abitrate_range": "96-256"}, "abitrate and abitrate_range"),
({"abitrate_range": "1-2-3"}, "MIN-MAX"),
({"quality": "4k"}, "Invalid quality"),
({"quality": [0]}, "above 0"),
({"channels": -3}, "channels"),
({"workers": 0}, "workers"),
({"downloads": 0}, "downloads"),
@@ -295,7 +299,6 @@ def test_validate_download_params_accepts_defaults() -> None:
def test_validate_download_params_errors(data: dict, fragment) -> None:
result = validate_download_parameters(data)
if fragment is None:
# A dash-containing string is valid syntactically per current rule
assert result is None
else:
assert result is not None
@@ -311,8 +314,6 @@ def test_validate_download_params_accepts_valid_values() -> None:
"sub_format": "VTT",
"vbitrate": 6000,
"abitrate": 128,
"vbitrate_range": "6000-7000",
"abitrate_range": "96-192",
"channels": 5.1,
"workers": 8,
"downloads": 2,
@@ -323,6 +324,11 @@ def test_validate_download_params_accepts_valid_values() -> None:
)
@pytest.mark.parametrize("vrange", ["6000-7000", "7000-6000", " 6000 - 7000 "])
def test_validate_download_params_accepts_valid_bitrate_ranges(vrange: str) -> None:
assert validate_download_parameters({"vbitrate_range": vrange, "abitrate_range": "96-192"}) is None
async def test_search_missing_service_raises_invalid_input() -> None:
# MISSING_SERVICE was never an APIErrorCode member, so this raised AttributeError
# and routes.py surfaced a 500 instead of the intended 400.