Ctrl K
ComunidadGuías…

backend/tests/test_glossary_routes_new.py

Rama master · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
"""Tests for new glossary endpoints: POST /series/<id>/glossary/suggest and GET /glossary/export."""

import json
from unittest.mock import MagicMock

# ─── Suggest endpoint ─────────────────────────────────────────────────────────


def test_suggest_returns_candidates(client, monkeypatch):
    """POST /series/<id>/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/<id>/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/<id>/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/<id>/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]