112 lines
4.1 KiB
Python
112 lines
4.1 KiB
Python
from __future__ import annotations
|
|
|
|
from dataclasses import dataclass, field
|
|
|
|
from auditui.library import LibraryClient
|
|
|
|
|
|
@dataclass(slots=True)
|
|
class MockClient:
|
|
"""Client double that records writes and serves configurable responses."""
|
|
|
|
put_calls: list[tuple[str, dict]] = field(default_factory=list)
|
|
post_calls: list[tuple[str, dict]] = field(default_factory=list)
|
|
_post_response: dict = field(default_factory=dict)
|
|
raise_on_put: bool = False
|
|
|
|
def put(self, path: str, body: dict) -> dict:
|
|
"""Record put payload or raise when configured."""
|
|
if self.raise_on_put:
|
|
raise RuntimeError("put failed")
|
|
self.put_calls.append((path, body))
|
|
return {}
|
|
|
|
def post(self, path: str, body: dict) -> dict:
|
|
"""Record post payload and return configured response."""
|
|
self.post_calls.append((path, body))
|
|
return self._post_response
|
|
|
|
def get(self, path: str, **kwargs: dict) -> dict:
|
|
"""Return empty data for extractor-focused tests."""
|
|
del path, kwargs
|
|
return {}
|
|
|
|
|
|
def build_item(
|
|
*,
|
|
title: str | None = None,
|
|
product_title: str | None = None,
|
|
authors: list[dict] | None = None,
|
|
runtime_min: int | None = None,
|
|
listening_status: dict | None = None,
|
|
percent_complete: int | float | None = None,
|
|
asin: str | None = None,
|
|
) -> dict:
|
|
"""Construct synthetic library items for extractor and finish tests."""
|
|
item: dict = {}
|
|
if title is not None:
|
|
item["title"] = title
|
|
if percent_complete is not None:
|
|
item["percent_complete"] = percent_complete
|
|
if listening_status is not None:
|
|
item["listening_status"] = listening_status
|
|
if asin is not None:
|
|
item["asin"] = asin
|
|
product: dict = {}
|
|
if product_title is not None:
|
|
product["title"] = product_title
|
|
if runtime_min is not None:
|
|
product["runtime_length"] = {"min": runtime_min}
|
|
if authors is not None:
|
|
product["authors"] = authors
|
|
if asin is not None:
|
|
product["asin"] = asin
|
|
if product:
|
|
item["product"] = product
|
|
if runtime_min is not None:
|
|
item["runtime_length_min"] = runtime_min
|
|
return item
|
|
|
|
|
|
def test_extract_title_prefers_product_title() -> None:
|
|
"""Ensure product title has precedence over outer item title."""
|
|
library = LibraryClient(MockClient()) # type: ignore[arg-type]
|
|
assert (
|
|
library.extract_title(build_item(title="Outer", product_title="Inner"))
|
|
== "Inner"
|
|
)
|
|
|
|
|
|
def test_extract_title_falls_back_to_asin() -> None:
|
|
"""Ensure title fallback uses product ASIN when no title exists."""
|
|
library = LibraryClient(MockClient()) # type: ignore[arg-type]
|
|
assert library.extract_title({"product": {"asin": "A1"}}) == "A1"
|
|
|
|
|
|
def test_extract_authors_joins_names() -> None:
|
|
"""Ensure author dictionaries are converted to a readable list."""
|
|
library = LibraryClient(MockClient()) # type: ignore[arg-type]
|
|
item = build_item(authors=[{"name": "A"}, {"name": "B"}])
|
|
assert library.extract_authors(item) == "A, B"
|
|
|
|
|
|
def test_extract_runtime_minutes_handles_dict_and_number() -> None:
|
|
"""Ensure runtime extraction supports dict and numeric payloads."""
|
|
library = LibraryClient(MockClient()) # type: ignore[arg-type]
|
|
assert library.extract_runtime_minutes(build_item(runtime_min=12)) == 12
|
|
assert library.extract_runtime_minutes({"runtime_length": 42}) == 42
|
|
|
|
|
|
def test_extract_progress_info_prefers_listening_status_when_needed() -> None:
|
|
"""Ensure progress can be sourced from listening_status when top-level is absent."""
|
|
library = LibraryClient(MockClient()) # type: ignore[arg-type]
|
|
item = build_item(listening_status={"percent_complete": 25.0})
|
|
assert library.extract_progress_info(item) == 25.0
|
|
|
|
|
|
def test_extract_asin_prefers_item_then_product() -> None:
|
|
"""Ensure ASIN extraction works from both item and product fields."""
|
|
library = LibraryClient(MockClient()) # type: ignore[arg-type]
|
|
assert library.extract_asin(build_item(asin="ASIN1")) == "ASIN1"
|
|
assert library.extract_asin({"product": {"asin": "ASIN2"}}) == "ASIN2"
|