mirror of
https://github.com/unshackle-dl/unshackle.git
synced 2026-11-12 03:55:05 +00:00
Renames 314 defs and 41 module/class-level constants. Keeps 11 names underscored where the stripped form collides in scope, plus 5.5.0 shims for the three symbols external service repos call.
184 lines
7.5 KiB
Python
184 lines
7.5 KiB
Python
"""IMDb GraphQL provider: persisted-query flow, field mapping, and search constraints."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
from typing import Any, Optional
|
|
|
|
import pytest
|
|
|
|
from unshackle.core.providers.imdb import IMDBProvider, escape_graphql
|
|
|
|
|
|
class _Response:
|
|
def __init__(self, payload: dict) -> None:
|
|
self._payload = payload
|
|
|
|
def json(self) -> dict:
|
|
return self._payload
|
|
|
|
|
|
def node(
|
|
imdb_id: str = "tt3581920",
|
|
text: str = "The Last of Us",
|
|
year: Optional[int] = 2023,
|
|
title_type: str = "tvSeries",
|
|
languages: Any = ({"id": "en", "text": "English"}, {"id": "id", "text": "Indonesian"}),
|
|
) -> dict:
|
|
return {
|
|
"id": imdb_id,
|
|
"titleText": {"text": text},
|
|
"originalTitleText": {"text": text},
|
|
"releaseYear": {"year": year} if year else None,
|
|
"titleType": {"id": title_type},
|
|
"spokenLanguages": {"spokenLanguages": list(languages)} if languages is not None else None,
|
|
}
|
|
|
|
|
|
class _FakeSession:
|
|
"""Records GET/POST calls and replays queued payloads."""
|
|
|
|
def __init__(self, responses: list[dict]) -> None:
|
|
self._responses = responses
|
|
self.gets: list[dict] = []
|
|
self.posts: list[dict] = []
|
|
|
|
def get(self, url: str, params: dict, headers: dict, timeout: int) -> _Response:
|
|
self.gets.append(params)
|
|
return _Response(self._responses.pop(0))
|
|
|
|
def post(self, url: str, json: dict, headers: dict, timeout: int) -> _Response: # noqa: A002
|
|
self.posts.append(json)
|
|
return _Response(self._responses.pop(0))
|
|
|
|
|
|
def _provider(monkeypatch: pytest.MonkeyPatch, responses: list[dict]) -> tuple[IMDBProvider, _FakeSession]:
|
|
p = IMDBProvider()
|
|
session = _FakeSession(responses)
|
|
monkeypatch.setattr(type(p), "session", property(lambda _self: session))
|
|
return p, session
|
|
|
|
|
|
NOT_FOUND = {"errors": [{"message": "PersistedQueryNotFound", "extensions": {"code": "PERSISTED_QUERY_NOT_FOUND"}}]}
|
|
|
|
|
|
def test_is_available_without_any_key() -> None:
|
|
assert IMDBProvider().is_available() is True
|
|
|
|
|
|
def test_cache_hit_uses_one_get(monkeypatch: pytest.MonkeyPatch) -> None:
|
|
p, session = _provider(monkeypatch, [{"data": {"title": node()}}])
|
|
result = p.get_by_id("tt3581920", "tv")
|
|
assert result is not None and result.title == "The Last of Us"
|
|
assert len(session.gets) == 1 and session.posts == []
|
|
|
|
|
|
def test_persisted_query_miss_falls_back_to_one_post(monkeypatch: pytest.MonkeyPatch) -> None:
|
|
p, session = _provider(monkeypatch, [NOT_FOUND, {"data": {"title": node()}}])
|
|
result = p.get_by_id("tt3581920", "tv")
|
|
assert result is not None and result.external_ids.imdb_id == "tt3581920"
|
|
assert len(session.gets) == 1 and len(session.posts) == 1
|
|
# the POST registers the query by sending its text, keyed by the same hash the GET sent
|
|
assert "query UnshackleTitle" in session.posts[0]["query"]
|
|
sent_hash = json.loads(session.gets[0]["extensions"])["persistedQuery"]["sha256Hash"]
|
|
assert session.posts[0]["extensions"]["persistedQuery"]["sha256Hash"] == sent_hash
|
|
|
|
|
|
def test_get_params_carry_no_spaces(monkeypatch: pytest.MonkeyPatch) -> None:
|
|
"""IMDb's GraphQL gateway decodes "+" literally, so spaced JSON would come back as a 400."""
|
|
p, session = _provider(monkeypatch, [{"data": {"title": node()}}])
|
|
p.get_by_id("tt3581920", "tv")
|
|
assert " " not in session.gets[0]["extensions"]
|
|
assert " " not in session.gets[0]["variables"]
|
|
|
|
|
|
def test_get_by_id_maps_every_field(monkeypatch: pytest.MonkeyPatch) -> None:
|
|
p, _ = _provider(monkeypatch, [{"data": {"title": node()}}])
|
|
result = p.get_by_id("tt3581920", "tv")
|
|
assert result is not None
|
|
assert (result.title, result.year, result.kind) == ("The Last of Us", 2023, "tv")
|
|
assert result.original_language == "en"
|
|
assert result.source == "imdb"
|
|
assert result.external_ids.imdb_id == "tt3581920"
|
|
|
|
|
|
@pytest.mark.parametrize("title_type", ["movie", "tvSeries", "videoGame"])
|
|
def test_caller_kind_wins_over_the_response_title_type(monkeypatch: pytest.MonkeyPatch, title_type: str) -> None:
|
|
"""kind stays the caller's, so it cannot desync from the tmdb_kind resolve_by_ids seeds."""
|
|
p, _ = _provider(monkeypatch, [{"data": {"title": node(title_type=title_type)}}])
|
|
result = p.get_by_id("tt1375666", "tv")
|
|
assert result is not None and result.kind == "tv"
|
|
|
|
|
|
@pytest.mark.parametrize("payload", [{"data": {"title": None}}, {"errors": [{"message": "boom"}]}, {"data": {}}])
|
|
def test_get_by_id_returns_none_on_no_answer(monkeypatch: pytest.MonkeyPatch, payload: dict) -> None:
|
|
p, _ = _provider(monkeypatch, [payload])
|
|
assert p.get_by_id("tt3581920", "tv") is None
|
|
|
|
|
|
def edges(*nodes: dict) -> dict:
|
|
return {"data": {"advancedTitleSearch": {"edges": [{"node": {"title": n}} for n in nodes]}}}
|
|
|
|
|
|
def test_search_uses_its_own_operation_and_one_get_on_a_cache_hit(monkeypatch: pytest.MonkeyPatch) -> None:
|
|
p, session = _provider(monkeypatch, [edges(node())])
|
|
result = p.search("The Last of Us", 2023, "tv")
|
|
assert result is not None and result.external_ids.imdb_id == "tt3581920"
|
|
assert session.gets[0]["operationName"] == "UnshackleTitleSearch"
|
|
assert session.posts == []
|
|
|
|
|
|
def test_search_query_text_carries_the_constraints(monkeypatch: pytest.MonkeyPatch) -> None:
|
|
p, session = _provider(monkeypatch, [NOT_FOUND, edges(node())])
|
|
p.search("The Last of Us", 2023, "tv")
|
|
query = session.posts[0]["query"]
|
|
assert 'searchTerm: "The Last of Us"' in query
|
|
assert 'start: "2023-01-01", end: "2023-12-31"' in query
|
|
assert '["tvSeries", "tvMiniSeries"]' in query or '["tvSeries","tvMiniSeries"]' in query
|
|
|
|
|
|
def test_search_omits_the_year_range_when_unknown(monkeypatch: pytest.MonkeyPatch) -> None:
|
|
p, session = _provider(monkeypatch, [NOT_FOUND, edges(node(title_type="movie"))])
|
|
p.search("Inception", None, "movie")
|
|
query = session.posts[0]["query"]
|
|
assert "releaseDateConstraint" not in query
|
|
assert '["movie", "tvMovie"]' in query or '["movie","tvMovie"]' in query
|
|
|
|
|
|
def test_search_picks_the_closest_title_not_the_first(monkeypatch: pytest.MonkeyPatch) -> None:
|
|
noise = node(imdb_id="tt27478132", text="Making of: The Last of Us")
|
|
exact = node()
|
|
p, _ = _provider(monkeypatch, [edges(noise, exact)])
|
|
result = p.search("The Last of Us", 2023, "tv")
|
|
assert result is not None and result.external_ids.imdb_id == "tt3581920"
|
|
|
|
|
|
def test_search_rejects_a_title_that_is_not_a_close_match(monkeypatch: pytest.MonkeyPatch) -> None:
|
|
p, _ = _provider(monkeypatch, [edges(node(imdb_id="tt999", text="Something Else Entirely"))])
|
|
assert p.search("The Last of Us", 2023, "tv") is None
|
|
|
|
|
|
def test_search_survives_a_null_language_block(monkeypatch: pytest.MonkeyPatch) -> None:
|
|
p, _ = _provider(monkeypatch, [edges(node(languages=None))])
|
|
result = p.search("The Last of Us", 2023, "tv")
|
|
assert result is not None and result.original_language is None
|
|
|
|
|
|
def test_search_returns_none_when_nothing_comes_back(monkeypatch: pytest.MonkeyPatch) -> None:
|
|
p, _ = _provider(monkeypatch, [edges()])
|
|
assert p.search("The Last of Us", 2023, "tv") is None
|
|
|
|
|
|
def test_escape_graphql_closes_the_string_injection() -> None:
|
|
assert escape_graphql('a"b\\c') == 'a\\"b\\\\c'
|
|
|
|
|
|
def test_search_term_with_a_quote_stays_inside_the_string(monkeypatch: pytest.MonkeyPatch) -> None:
|
|
p, session = _provider(monkeypatch, [NOT_FOUND, edges(node(text='The "Best" Show'))])
|
|
p.search('The "Best" Show', None, "tv")
|
|
assert 'searchTerm: "The \\"Best\\" Show"' in session.posts[0]["query"]
|
|
|
|
|
|
def test_get_external_ids_echoes_the_id() -> None:
|
|
assert IMDBProvider().get_external_ids("tt1375666", "movie").imdb_id == "tt1375666"
|