"""Tests for new glossary endpoints: POST /series//glossary/suggest and GET /glossary/export.""" import json from unittest.mock import MagicMock # ─── Suggest endpoint ───────────────────────────────────────────────────────── def test_suggest_returns_candidates(client, monkeypatch): """POST /series//glossary/suggest returns extracted candidates from glossary_extractor.""" fake_candidates = [ {"source_term": "Naruto", "term_type": "character", "frequency": 42, "confidence": 0.92}, {"source_term": "Konoha", "term_type": "place", "frequency": 17, "confidence": 0.67}, ] mock_sonarr = MagicMock() mock_sonarr.get_series_by_id.return_value = {"id": 1, "path": "/media/naruto"} monkeypatch.setattr("sonarr_client.get_sonarr_client", lambda: mock_sonarr) monkeypatch.setattr( "routes.library.extract_candidates", lambda directory, source_lang, min_freq, max_candidates: fake_candidates, ) resp = client.post( "/api/v1/series/1/glossary/suggest", data=json.dumps({"source_lang": "en", "min_freq": 2}), content_type="application/json", ) assert resp.status_code == 200 data = resp.get_json() assert data["series_id"] == 1 assert data["candidates"] == fake_candidates def test_suggest_series_not_found(client, monkeypatch): """POST /series//glossary/suggest returns empty candidates when series not in Sonarr.""" mock_sonarr = MagicMock() mock_sonarr.get_series_by_id.return_value = None monkeypatch.setattr("sonarr_client.get_sonarr_client", lambda: mock_sonarr) resp = client.post( "/api/v1/series/999/glossary/suggest", data=json.dumps({}), content_type="application/json", ) assert resp.status_code == 200 data = resp.get_json() assert data["candidates"] == [] assert "message" in data def test_suggest_sonarr_not_configured(client, monkeypatch): """POST /series//glossary/suggest returns empty candidates when Sonarr not configured.""" monkeypatch.setattr("sonarr_client.get_sonarr_client", lambda: None) resp = client.post( "/api/v1/series/1/glossary/suggest", data=json.dumps({}), content_type="application/json", ) assert resp.status_code == 200 data = resp.get_json() assert data["candidates"] == [] assert "message" in data def test_suggest_standalone_series_uses_folder_path(client, monkeypatch): """Without Sonarr the standalone series folder is scanned for candidates.""" from config import reload_settings # The folder must lie inside the media folder (1.18 standalone containment). monkeypatch.setenv("SUBLARR_MEDIA_PATH", "/media") reload_settings() monkeypatch.setattr("sonarr_client.get_sonarr_client", lambda: None) monkeypatch.setattr( "db.standalone.get_standalone_series", lambda sid: {"id": sid, "folder_path": "/media/standalone/naruto"}, ) captured = {} def fake_extract(directory, source_lang, min_freq, max_candidates, path_filter=None): captured["directory"] = directory return [{"source_term": "Konoha", "term_type": "place", "frequency": 5}] monkeypatch.setattr("routes.library.extract_candidates", fake_extract) resp = client.post( "/api/v1/series/7/glossary/suggest", data=json.dumps({}), content_type="application/json", ) assert resp.status_code == 200 assert captured["directory"] == "/media/standalone/naruto" assert resp.get_json()["candidates"][0]["source_term"] == "Konoha" def test_suggest_uses_defaults_when_no_body(client, monkeypatch): """POST /series//glossary/suggest uses source_lang='en' and min_freq=3 by default.""" captured = {} def fake_extract(directory, source_lang, min_freq, max_candidates): captured["source_lang"] = source_lang captured["min_freq"] = min_freq return [] mock_sonarr = MagicMock() mock_sonarr.get_series_by_id.return_value = {"id": 1, "path": "/media/test"} monkeypatch.setattr("sonarr_client.get_sonarr_client", lambda: mock_sonarr) monkeypatch.setattr("routes.library.extract_candidates", fake_extract) resp = client.post( "/api/v1/series/1/glossary/suggest", data=json.dumps({}), content_type="application/json", ) assert resp.status_code == 200 assert captured["source_lang"] == "en" assert captured["min_freq"] == 3 # ─── Export endpoint ─────────────────────────────────────────────────────────── def test_export_tsv_has_header(client): """GET /glossary/export returns a TSV with a header row as first line.""" resp = client.get("/api/v1/glossary/export") assert resp.status_code == 200 text = resp.data.decode("utf-8") first_line = text.splitlines()[0] assert first_line == "source_term\ttarget_term\tterm_type\tnotes" def test_export_tsv_content_type(client): """GET /glossary/export sets Content-Type to text/tab-separated-values; charset=utf-8.""" resp = client.get("/api/v1/glossary/export") assert resp.status_code == 200 assert "text/tab-separated-values" in resp.content_type assert "utf-8" in resp.content_type def test_export_tsv_filename_global(client): """GET /glossary/export (no series_id) uses filename glossary_global.tsv.""" resp = client.get("/api/v1/glossary/export") assert resp.status_code == 200 disposition = resp.headers.get("Content-Disposition", "") assert "glossary_global.tsv" in disposition def test_export_tsv_series_scoped(client): """GET /glossary/export?series_id=5 uses filename glossary_series_5.tsv.""" resp = client.get("/api/v1/glossary/export?series_id=5") assert resp.status_code == 200 disposition = resp.headers.get("Content-Disposition", "") assert "glossary_series_5.tsv" in disposition def test_export_tsv_content(client): """GET /glossary/export includes glossary entries as TSV rows after the header.""" # Insert entries via the API to avoid mixing app contexts client.post( "/api/v1/glossary", data=json.dumps({"source_term": "Naruto", "target_term": "Naruto", "notes": ""}), content_type="application/json", ) client.post( "/api/v1/glossary", data=json.dumps( {"source_term": "Konoha", "target_term": "Konoha", "notes": "leaf village"} ), content_type="application/json", ) resp = client.get("/api/v1/glossary/export") assert resp.status_code == 200 text = resp.data.decode("utf-8") lines = text.splitlines() # Header + at least 2 data rows assert len(lines) >= 3 # Each non-header line must have exactly 3 tabs (4 columns) for line in lines[1:]: assert line.count("\t") == 3 def test_export_tsv_notes_tabs_replaced(client): """GET /glossary/export replaces tabs in notes with spaces.""" # Insert an entry with a tab in notes via the API client.post( "/api/v1/glossary", data=json.dumps( {"source_term": "Term", "target_term": "Term", "notes": "note\twith\ttabs"} ), content_type="application/json", ) resp = client.get("/api/v1/glossary/export") assert resp.status_code == 200 text = resp.data.decode("utf-8") # Notes column should not contain literal tabs (they'd be replaced by spaces) lines = text.splitlines() data_lines = [line for line in lines[1:] if line.startswith("Term\t")] assert len(data_lines) == 1 # Split on tab — 4 columns exactly cols = data_lines[0].split("\t") assert len(cols) == 4 # Notes column (index 3) has no remaining tabs assert "\t" not in cols[3]