soulsync/tests/test_video_api.py
BoulderBadgeDad 6665ecaa12 video side: library scanner (server = source of truth) + scan API
Reads the active media server and mirrors it into video.db, adapting the music
scan pattern (ask the server, upsert, prune what's gone) — isolated from music.
- core/video/scanner.py: server-agnostic VideoLibraryScanner. Consumes a media
  source (duck-typed) yielding normalized dicts; upserts movies + show trees,
  prunes removed items, reports progress/state. Skips pruning when a scan
  returns nothing (transient-failure safety). Background thread + scan_sync.
- core/video/sources.py: Plex + Jellyfin adapters that REUSE the shared
  connected clients (MediaServerEngine) but own all video-section logic; produce
  normalized dicts. (Validated against a live server by design; scanner itself
  is fully unit-tested with a fake source.)
- api/video/scan.py: POST /api/video/scan/request, GET /api/video/scan/status.
- .gitignore: video_library.db + sidecars (mirrors music); tests inject a
  tmp DB so none is ever created in the repo.
Tests: scan populate/prune/empty-safety/no-source-error, isolation guard
(core/video imports nothing from music), scan routes registered. 101 green.
2026-06-13 23:13:50 -07:00

55 lines
2.1 KiB
Python

"""Seam tests for the isolated /api/video blueprint (experimental branch).
Verifies the blueprint builds with its route, the dashboard endpoint returns
real (zeroed) JSON against an empty video.db, and that the video API package
imports nothing from the music side.
"""
from __future__ import annotations
from pathlib import Path
from flask import Flask
def _make_client(tmp_path):
# Inject a tmp-backed DB directly so the endpoint never falls back to the
# real default path (no stray database/video_library.db in the repo).
import api.video as videoapi
from database.video_database import VideoDatabase
videoapi._video_db = VideoDatabase(database_path=str(tmp_path / "video_library.db"))
app = Flask(__name__)
app.register_blueprint(videoapi.create_video_blueprint(), url_prefix="/api/video")
return app.test_client(), videoapi
def test_blueprint_exposes_dashboard_route():
from api.video import create_video_blueprint
app = Flask(__name__)
app.register_blueprint(create_video_blueprint(), url_prefix="/api/video")
rules = {r.rule for r in app.url_map.iter_rules()}
assert "/api/video/dashboard" in rules
assert "/api/video/scan/request" in rules
assert "/api/video/scan/status" in rules
def test_dashboard_endpoint_returns_zeroed_json(tmp_path):
client, videoapi = _make_client(tmp_path)
try:
resp = client.get("/api/video/dashboard")
assert resp.status_code == 200
data = resp.get_json()
assert data["library"]["movies"] == 0
assert data["downloads"]["active"] == 0
assert data["watchlist"] == 0 and data["wishlist"] == 0
finally:
videoapi._video_db = None # don't leak the tmp DB to other tests
def test_video_api_imports_nothing_from_music():
base = Path(__file__).resolve().parent.parent / "api" / "video"
for py in base.glob("*.py"):
for line in py.read_text(encoding="utf-8").splitlines():
s = line.strip()
if s.startswith("import ") or s.startswith("from "):
assert "music" not in s.lower(), f"{py.name}: music import leaked: {s!r}"