mirror of
https://github.com/unshackle-dl/unshackle.git
synced 2026-11-12 20:15:10 +00:00
test: replace real service tags and brands with generic placeholders
This commit is contained in:
@@ -12,7 +12,7 @@ from unshackle.core.api.handlers import load_client_cookies
|
||||
COOKIE_FILE = "\n".join(
|
||||
[
|
||||
"# Netscape HTTP Cookie File",
|
||||
"\t".join([".primevideo.com", "TRUE", "/", "TRUE", "2000000000", "session-id", "123-456"]),
|
||||
"\t".join([".example.com", "TRUE", "/", "TRUE", "2000000000", "session-id", "123-456"]),
|
||||
"",
|
||||
]
|
||||
)
|
||||
|
||||
@@ -15,7 +15,7 @@ dl:
|
||||
a_lang: [yes]
|
||||
require_subs: no
|
||||
require_audio: [no]
|
||||
AMZN:
|
||||
EXAMPLE:
|
||||
lang: no
|
||||
muxing:
|
||||
default_language:
|
||||
@@ -43,7 +43,7 @@ def test_bool_shaped_tags_survive_every_language_key(tmp_path: Path) -> None:
|
||||
assert config.dl["a_lang"] == ["yes"]
|
||||
assert config.dl["require_subs"] == "no"
|
||||
assert config.dl["require_audio"] == ["no"]
|
||||
assert config.dl["AMZN"]["lang"] == "no"
|
||||
assert config.dl["EXAMPLE"]["lang"] == "no"
|
||||
assert config.muxing["default_language"]["audio"] == "no"
|
||||
assert config.audio["language_priority"] == ["no", "da"]
|
||||
|
||||
|
||||
@@ -38,7 +38,7 @@ def test_safe_cache_key_accepts_plain_filename():
|
||||
|
||||
|
||||
def test_safe_cache_key_accepts_nested_key_and_normalises_separators():
|
||||
# A Cacher key with a subdirectory, as NF uses ("session_web/<sha1>"), keeps its
|
||||
# A Cacher key with a subdirectory ("session_web/<sha1>") keeps its
|
||||
# posix form; a backslash from a Windows peer names the same nested file.
|
||||
assert safe_cache_key("session_web/abc") == "session_web/abc"
|
||||
assert safe_cache_key("MSL/a/b") == "MSL/a/b"
|
||||
|
||||
@@ -228,14 +228,14 @@ def test_refresh_reports_discarded_local_edits(monkeypatch, tmp_path):
|
||||
|
||||
def run(args, **kw):
|
||||
if "diff" in args and "--name-only" in args:
|
||||
return subprocess.CompletedProcess(args, 0, b"CR/__init__.py\n", b"") # local edit present
|
||||
return subprocess.CompletedProcess(args, 0, b"EXAMPLE/__init__.py\n", b"") # local edit present
|
||||
if "rev-parse" in args:
|
||||
return subprocess.CompletedProcess(args, 0, b"samehead\n", b"") # upstream did not advance
|
||||
return subprocess.CompletedProcess(args, 0, b"", b"") # fetch/reset/diff
|
||||
|
||||
monkeypatch.setattr(service_repo.subprocess, "run", run)
|
||||
_, changes = service_repo.refresh_repo("a/b")
|
||||
assert changes == ["!CR"]
|
||||
assert changes == ["!EXAMPLE"]
|
||||
|
||||
|
||||
def test_changed_services_up_to_date(monkeypatch):
|
||||
|
||||
@@ -43,8 +43,7 @@ def sent_timeout(adapter: HTTPAdapter) -> Any:
|
||||
def test_subclass_adapter_keeps_its_class_and_tls_context() -> None:
|
||||
"""A service-mounted SSLCiphers must survive the growth with its TLS context.
|
||||
|
||||
CBS, friDay, iP, MY5 and PMTP all mount SSLCiphers on the service session and none
|
||||
override get_session(). Replacing it with a plain adapter drops DEFAULT:@SECLEVEL=2 and
|
||||
Services mount SSLCiphers on the service session without overriding get_session(). Replacing it with a plain adapter drops DEFAULT:@SECLEVEL=2 and
|
||||
check_hostname=False, so metadata succeeds and every segment then fails at TLS or 403.
|
||||
"""
|
||||
session = requests.Session()
|
||||
@@ -75,13 +74,13 @@ def test_a_distinct_http_adapter_is_grown_not_overwritten() -> None:
|
||||
narrow = SSLCiphers(security_level=2)
|
||||
plain = TimeoutHTTPAdapter(pool_connections=10, pool_maxsize=10)
|
||||
session.mount("https://", secure)
|
||||
session.mount("https://cbs.example/", narrow)
|
||||
session.mount("https://svc.example/", narrow)
|
||||
session.mount("http://", plain)
|
||||
|
||||
grow_session_pool(session, 64)
|
||||
|
||||
assert session.get_adapter("https://x") is secure
|
||||
assert session.get_adapter("https://cbs.example/a") is narrow
|
||||
assert session.get_adapter("https://svc.example/a") is narrow
|
||||
assert session.get_adapter("http://x") is plain
|
||||
assert session.get_adapter("http://x") is not session.get_adapter("https://x")
|
||||
for adapter in (secure, narrow, plain):
|
||||
|
||||
@@ -390,7 +390,7 @@ def test_only_the_kid_whose_window_fails_is_flagged(tmp_path: Path, monkeypatch:
|
||||
lead = UUID(int=7)
|
||||
local = SQLite("local", tmp_path / "local.db")
|
||||
poisoned = Remote("poisoned", tmp_path / "poisoned.db")
|
||||
poisoned.add_key("SVC", lead, BAD) # the same wrong key under both KIDs, as the NF title had
|
||||
poisoned.add_key("SVC", lead, BAD) # the same wrong key under both KIDs
|
||||
poisoned.add_key("SVC", KID, BAD)
|
||||
other = Remote("other", tmp_path / "other.db")
|
||||
other.add_key("SVC", KID, GOOD)
|
||||
|
||||
@@ -48,8 +48,7 @@ DISTINCT_PAIRS = [
|
||||
]
|
||||
|
||||
# A full real-world audio language set for one title, the kind of mix a streaming manifest ships,
|
||||
# which must resolve 1:1 with no collision or loss. Tags are drawn from published streaming
|
||||
# language references (Warner Bros. Discovery / Netflix) and include regional and script variants.
|
||||
# which must resolve 1:1 with no collision or loss. Tags include regional and script variants.
|
||||
REAL_WORLD_AUDIO_LANGS = [
|
||||
"ar",
|
||||
"ar-EG",
|
||||
|
||||
@@ -136,12 +136,12 @@ def test_ring_masks_host_paths_and_secrets_in_records() -> None:
|
||||
logger = logging.getLogger("test.ring.redact")
|
||||
logger.setLevel(logging.INFO)
|
||||
logger.addHandler(ring)
|
||||
logger.error(f'File "{root}/unshackle/services/HMAX/__init__.py" 404: https://api.x/y?token=abc')
|
||||
logger.error(f'File "{root}/unshackle/services/EXAMPLE/__init__.py" 404: https://api.x/y?token=abc')
|
||||
logger.removeHandler(ring)
|
||||
|
||||
msg = ring.records[-1]["msg"]
|
||||
assert root not in msg
|
||||
assert "<unshackle>/unshackle/services/HMAX" in msg
|
||||
assert "<unshackle>/unshackle/services/EXAMPLE" in msg
|
||||
assert "token=abc" not in msg
|
||||
assert "https://api.x/y" in msg
|
||||
assert ring.since(0, logger="test") and not ring.since(0, logger="other")
|
||||
|
||||
@@ -11,8 +11,8 @@ from unshackle.core.config import config
|
||||
|
||||
SERVICES = [
|
||||
{
|
||||
"tag": "AMZN",
|
||||
"aliases": ["amazon"],
|
||||
"tag": "EXAMPLE",
|
||||
"aliases": ["sample"],
|
||||
"cli_params": [{"kind": "option", "name": "extras"}, {"kind": "argument", "name": "title"}],
|
||||
}
|
||||
]
|
||||
@@ -20,7 +20,7 @@ INFO = SERVICES[0]
|
||||
|
||||
|
||||
class FakeRemote:
|
||||
service_tag = "AMZN"
|
||||
service_tag = "EXAMPLE"
|
||||
|
||||
def __init__(self) -> None:
|
||||
self.closed = False
|
||||
@@ -47,7 +47,7 @@ def lease(data, **extra):
|
||||
return handlers.lease_remote_service({**data, **extra}, "srv", INFO, None, "show")
|
||||
|
||||
|
||||
DATA = {"service": "AMZN", "title_id": "show", "remote": "srv"}
|
||||
DATA = {"service": "EXAMPLE", "title_id": "show", "remote": "srv"}
|
||||
|
||||
|
||||
async def test_concurrent_leases_share_one_session(pool):
|
||||
@@ -89,17 +89,17 @@ def remote_config(monkeypatch):
|
||||
|
||||
|
||||
def test_remote_server_resolves_case_and_aliases(remote_config):
|
||||
assert handlers.remote_server({"remote": "srv", "service": "amzn"}) == ("srv", INFO)
|
||||
assert handlers.remote_server({"remote": "srv", "service": "Amazon"}) == ("srv", INFO)
|
||||
assert handlers.remote_server({"service": "AMZN"}) is None
|
||||
assert handlers.remote_server({"remote": "srv", "service": "example"}) == ("srv", INFO)
|
||||
assert handlers.remote_server({"remote": "srv", "service": "Sample"}) == ("srv", INFO)
|
||||
assert handlers.remote_server({"service": "EXAMPLE"}) is None
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"data, code",
|
||||
[
|
||||
({"remote": "nope", "service": "AMZN"}, APIErrorCode.INVALID_PARAMETERS),
|
||||
({"remote": ["srv"], "service": "AMZN"}, APIErrorCode.INVALID_PARAMETERS),
|
||||
({"remote": "srv", "service": "NF"}, APIErrorCode.INVALID_SERVICE),
|
||||
({"remote": "nope", "service": "EXAMPLE"}, APIErrorCode.INVALID_PARAMETERS),
|
||||
({"remote": ["srv"], "service": "EXAMPLE"}, APIErrorCode.INVALID_PARAMETERS),
|
||||
({"remote": "srv", "service": "OTHER"}, APIErrorCode.INVALID_SERVICE),
|
||||
],
|
||||
)
|
||||
def test_remote_server_rejects(remote_config, data, code):
|
||||
@@ -109,31 +109,31 @@ def test_remote_server_rejects(remote_config, data, code):
|
||||
|
||||
|
||||
def test_remote_server_applies_the_allowlist_and_reports_an_unreachable_server(remote_config, monkeypatch):
|
||||
monkeypatch.setattr(handlers, "get_allowed_services", lambda request=None: ["NF"])
|
||||
monkeypatch.setattr(handlers, "get_allowed_services", lambda request=None: ["OTHER"])
|
||||
with pytest.raises(APIError) as e:
|
||||
handlers.remote_server({"remote": "srv", "service": "AMZN"})
|
||||
handlers.remote_server({"remote": "srv", "service": "EXAMPLE"})
|
||||
assert e.value.error_code == APIErrorCode.INVALID_SERVICE
|
||||
|
||||
monkeypatch.setattr(remote_service, "remote_service_list", lambda name: None)
|
||||
with pytest.raises(APIError) as e:
|
||||
handlers.remote_server({"remote": "srv", "service": "AMZN"})
|
||||
handlers.remote_server({"remote": "srv", "service": "EXAMPLE"})
|
||||
assert e.value.error_code == APIErrorCode.SERVICE_UNAVAILABLE
|
||||
|
||||
|
||||
def test_make_remote_service_passes_only_cli_options(remote_config, monkeypatch):
|
||||
built = {}
|
||||
monkeypatch.setattr(remote_service, "resolve_server", lambda name: ("https://srv", "k", {"AMZN": {"x": 1}}))
|
||||
monkeypatch.setattr(remote_service, "resolve_server", lambda name: ("https://srv", "k", {"EXAMPLE": {"x": 1}}))
|
||||
monkeypatch.setattr(remote_service, "RemoteService", lambda *a, **kw: built.update(args=a, **kw))
|
||||
|
||||
remote_service.make_remote_service(
|
||||
None, "srv", "AMZN", "id", {"service_params": {"extras": "y", "service": "DSNP", "proxy": "x"}}, False
|
||||
None, "srv", "EXAMPLE", "id", {"service_params": {"extras": "y", "service": "OTHER", "proxy": "x"}}, False
|
||||
)
|
||||
assert built["service_params"] == {"extras": "y"}
|
||||
assert "AMZN" not in built["args"][5] # apply_overrides=False keeps the remote's overrides out
|
||||
assert "EXAMPLE" not in built["args"][5] # apply_overrides=False keeps the remote's overrides out
|
||||
|
||||
remote_service.make_remote_service(None, "srv", "AMZN", "id", {"extras": "z", "service": "DSNP", "quality": 1})
|
||||
remote_service.make_remote_service(None, "srv", "EXAMPLE", "id", {"extras": "z", "service": "OTHER", "quality": 1})
|
||||
assert built["service_params"] == {"extras": "z"}
|
||||
assert built["args"][5]["AMZN"] == {"x": 1}
|
||||
assert built["args"][5]["EXAMPLE"] == {"x": 1}
|
||||
|
||||
|
||||
def test_remote_error_carries_its_reason_and_exits_a_cli_run():
|
||||
@@ -145,8 +145,8 @@ def test_remote_error_carries_its_reason_and_exits_a_cli_run():
|
||||
|
||||
def test_a_geofenced_remote_service_gets_its_first_allowed_region_as_proxy(monkeypatch):
|
||||
monkeypatch.setattr(handlers, "server_proxy_allowed", lambda request=None: True)
|
||||
fenced = {"tag": "PCOK", "geofence": ["US", "ca"], "geoblock": ["us"]}
|
||||
assert handlers.geofence_proxy({}, {"tag": "PCOK", "geofence": ["US", "ca"]}) == {"proxy": "us"}
|
||||
fenced = {"tag": "EXAMPLE", "geofence": ["US", "ca"], "geoblock": ["us"]}
|
||||
assert handlers.geofence_proxy({}, {"tag": "EXAMPLE", "geofence": ["US", "ca"]}) == {"proxy": "us"}
|
||||
assert handlers.geofence_proxy({}, fenced) == {"proxy": "ca"}
|
||||
assert handlers.geofence_proxy({"proxy": "gb"}, fenced) == {}
|
||||
assert handlers.geofence_proxy({"no_proxy": True}, fenced) == {}
|
||||
@@ -194,7 +194,7 @@ async def test_a_remote_login_prompt_waits_for_its_owners_answer(monkeypatch):
|
||||
class Remote:
|
||||
auth_notice = None
|
||||
|
||||
login = handlers.RemoteLogin("owner", "srv", "DSNP", "show", Remote())
|
||||
login = handlers.RemoteLogin("owner", "srv", "OTHER", "show", Remote())
|
||||
monkeypatch.setattr(handlers, "_remote_logins", {login.id: login})
|
||||
monkeypatch.setattr(handlers, "is_admin", lambda request: False)
|
||||
caller = {"key": "owner"}
|
||||
|
||||
@@ -320,7 +320,7 @@ def test_load_cache_files_wiring(tmp_path, monkeypatch) -> None:
|
||||
|
||||
|
||||
def test_load_cache_files_forwards_nested_keys(tmp_path, monkeypatch) -> None:
|
||||
"""A service that keeps its cache in subfolders (NF: session_web/<sha1>.json) must have
|
||||
"""A service that keeps its cache in subfolders (session_web/<sha1>.json) must have
|
||||
those files forwarded under the Cacher key, and the credential filter must still apply
|
||||
to the basename of a nested file."""
|
||||
import base64
|
||||
|
||||
@@ -69,14 +69,14 @@ def _wv_drm():
|
||||
|
||||
def _session(track):
|
||||
# handle_single_server_cdm looks config.cdm up by the service class name, so the fake needs the tag as its name
|
||||
service = type("PCOK", (SimpleNamespace,), {})(
|
||||
service = type("EXAMPLE", (SimpleNamespace,), {})(
|
||||
get_widevine_service_certificate=lambda **k: None,
|
||||
get_widevine_license=lambda **k: b"",
|
||||
get_playready_license=lambda **k: b"",
|
||||
)
|
||||
return SimpleNamespace(
|
||||
lock=asyncio.Lock(),
|
||||
service_tag="PCOK",
|
||||
service_tag="EXAMPLE",
|
||||
service_instance=service,
|
||||
served_keys={},
|
||||
tracks=SimpleNamespace(get={"vid": track}.get),
|
||||
@@ -170,7 +170,10 @@ async def test_quality_keyed_mapping_resolves_to_the_device_it_loads(env, monkey
|
||||
monkeypatch.setattr(
|
||||
handlers.config,
|
||||
"cdm",
|
||||
{"PCOK": {"<=1080": "wv_tier", ">1080": {"widevine": "wv_tier", "playready": "pr_tier"}}, "default": "pr_dev"},
|
||||
{
|
||||
"EXAMPLE": {"<=1080": "wv_tier", ">1080": {"widevine": "wv_tier", "playready": "pr_tier"}},
|
||||
"default": "pr_dev",
|
||||
},
|
||||
raising=False,
|
||||
)
|
||||
|
||||
@@ -183,7 +186,9 @@ async def test_quality_keyed_mapping_resolves_to_the_device_it_loads(env, monkey
|
||||
|
||||
async def test_both_systems_mapped_follow_the_client(env, monkeypatch):
|
||||
"""A mapping with a device per system settles nothing, so the client's choice goes first."""
|
||||
monkeypatch.setattr(handlers.config, "cdm", {"PCOK": {"widevine": "wv_dev", "playready": "pr_dev"}}, raising=False)
|
||||
monkeypatch.setattr(
|
||||
handlers.config, "cdm", {"EXAMPLE": {"widevine": "wv_dev", "playready": "pr_dev"}}, raising=False
|
||||
)
|
||||
|
||||
track = SimpleNamespace(id="vid", drm=[_pr_drm(), _wv_drm()])
|
||||
body = await _license(monkeypatch, track, {"drm_type": "widevine"})
|
||||
@@ -192,7 +197,7 @@ async def test_both_systems_mapped_follow_the_client(env, monkeypatch):
|
||||
|
||||
|
||||
async def test_no_candidate_header_is_a_client_error(env, monkeypatch):
|
||||
monkeypatch.setattr(handlers.config, "cdm", {"PCOK": "pr_dev"}, raising=False)
|
||||
monkeypatch.setattr(handlers.config, "cdm", {"EXAMPLE": "pr_dev"}, raising=False)
|
||||
|
||||
track = SimpleNamespace(id="vid", drm=[_wv_drm()])
|
||||
with pytest.raises(APIError, match="No PSSH on track"):
|
||||
@@ -201,7 +206,7 @@ async def test_no_candidate_header_is_a_client_error(env, monkeypatch):
|
||||
|
||||
async def test_track_preference_goes_first_when_the_server_has_that_device(env, monkeypatch):
|
||||
"""A track that prefers PlayReady licenses with PlayReady even on a Widevine-mapped server."""
|
||||
monkeypatch.setattr(handlers.config, "cdm", {"PCOK": {"widevine": "wv_dev", "default": "pr_dev"}}, raising=False)
|
||||
monkeypatch.setattr(handlers.config, "cdm", {"EXAMPLE": {"widevine": "wv_dev", "default": "pr_dev"}}, raising=False)
|
||||
|
||||
track = SimpleNamespace(id="vid", drm=[_pr_drm(), _wv_drm()], drm_preference="pr")
|
||||
body = await _license(monkeypatch, track, {"drm_type": "widevine"})
|
||||
@@ -212,7 +217,7 @@ async def test_track_preference_goes_first_when_the_server_has_that_device(env,
|
||||
|
||||
async def test_track_preference_falls_back_with_a_warning(env, monkeypatch, caplog):
|
||||
"""With no PlayReady device the preference cannot be met: Widevine licenses and the client hears why."""
|
||||
monkeypatch.setattr(handlers.config, "cdm", {"PCOK": "wv_dev"}, raising=False)
|
||||
monkeypatch.setattr(handlers.config, "cdm", {"EXAMPLE": "wv_dev"}, raising=False)
|
||||
|
||||
track = SimpleNamespace(id="vid", drm=[_pr_drm(), _wv_drm()], drm_preference="pr")
|
||||
with caplog.at_level("WARNING", logger=handlers.log.name):
|
||||
|
||||
@@ -310,7 +310,7 @@ def test_sdh_stripping_ass_without_subtitleedit_content_matches_codec(tmp_path,
|
||||
assert "door creaks" not in text # bracketed effect removed
|
||||
|
||||
|
||||
# Segmented fVTT (wvtt) / fTTML (stpp) ship from DASH/HLS as fragmented MP4 (e.g. HBO Max). The
|
||||
# Segmented fVTT (wvtt) / fTTML (stpp) ship from DASH/HLS as fragmented MP4. The
|
||||
# downloader concatenates init + media segments into one file; parse() reads the MP4 boxes directly.
|
||||
|
||||
FIXTURES = pathlib.Path(__file__).parent / "fixtures"
|
||||
|
||||
Reference in New Issue
Block a user