171 lines
6.1 KiB
Python
171 lines
6.1 KiB
Python
"""Unit tests for content.archive.indexer."""
|
|
|
|
from __future__ import annotations
|
|
|
|
from unittest.mock import patch
|
|
|
|
import pytest
|
|
|
|
from content.archive.indexer import (
|
|
EpisodeDocument,
|
|
IndexResult,
|
|
_meilisearch_available,
|
|
index_episode,
|
|
search_episodes,
|
|
)
|
|
|
|
# ── _meilisearch_available ────────────────────────────────────────────────────
|
|
|
|
|
|
class TestMeilisearchAvailable:
|
|
def test_returns_bool(self):
|
|
assert isinstance(_meilisearch_available(), bool)
|
|
|
|
def test_false_when_spec_missing(self):
|
|
with patch("importlib.util.find_spec", return_value=None):
|
|
assert _meilisearch_available() is False
|
|
|
|
|
|
# ── EpisodeDocument ───────────────────────────────────────────────────────────
|
|
|
|
|
|
class TestEpisodeDocument:
|
|
def test_to_dict_contains_id(self):
|
|
doc = EpisodeDocument(id="ep-001", title="Test")
|
|
d = doc.to_dict()
|
|
assert d["id"] == "ep-001"
|
|
|
|
def test_to_dict_contains_title(self):
|
|
doc = EpisodeDocument(id="ep-001", title="My Episode")
|
|
assert doc.to_dict()["title"] == "My Episode"
|
|
|
|
def test_to_dict_defaults(self):
|
|
doc = EpisodeDocument(id="ep-001", title="T")
|
|
d = doc.to_dict()
|
|
assert d["tags"] == []
|
|
assert d["highlight_ids"] == []
|
|
assert d["duration"] == 0.0
|
|
assert d["clip_count"] == 0
|
|
|
|
def test_to_dict_preserves_tags(self):
|
|
doc = EpisodeDocument(id="ep-001", title="T", tags=["gaming", "highlights"])
|
|
assert doc.to_dict()["tags"] == ["gaming", "highlights"]
|
|
|
|
def test_to_dict_all_fields(self):
|
|
doc = EpisodeDocument(
|
|
id="ep-002",
|
|
title="Full",
|
|
description="Desc",
|
|
tags=["t"],
|
|
published_at="2026-03-23T00:00:00Z",
|
|
youtube_url="https://yt.com/x",
|
|
blossom_url="https://blossom.io/x",
|
|
duration=180.0,
|
|
clip_count=5,
|
|
highlight_ids=["h1", "h2"],
|
|
)
|
|
d = doc.to_dict()
|
|
assert d["description"] == "Desc"
|
|
assert d["youtube_url"] == "https://yt.com/x"
|
|
assert d["duration"] == 180.0
|
|
assert d["highlight_ids"] == ["h1", "h2"]
|
|
|
|
|
|
# ── index_episode ─────────────────────────────────────────────────────────────
|
|
|
|
|
|
class TestIndexEpisode:
|
|
@pytest.mark.asyncio
|
|
async def test_empty_id_returns_failure(self):
|
|
result = await index_episode("", "Title")
|
|
assert result.success is False
|
|
assert "episode_id" in result.error
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_whitespace_id_returns_failure(self):
|
|
result = await index_episode(" ", "Title")
|
|
assert result.success is False
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_returns_failure_when_meilisearch_missing(self):
|
|
with patch("content.archive.indexer._meilisearch_available", return_value=False):
|
|
result = await index_episode("ep-001", "Title")
|
|
assert result.success is False
|
|
assert "meilisearch" in result.error.lower()
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_successful_indexing(self):
|
|
fake_result = IndexResult(success=True, document_id="ep-001")
|
|
with (
|
|
patch("content.archive.indexer._meilisearch_available", return_value=True),
|
|
patch("asyncio.to_thread", return_value=fake_result),
|
|
):
|
|
result = await index_episode(
|
|
"ep-001",
|
|
"Test Episode",
|
|
description="A test",
|
|
tags=["gaming"],
|
|
published_at="2026-03-23T00:00:00Z",
|
|
youtube_url="https://yt.com/abc",
|
|
duration=120.0,
|
|
clip_count=3,
|
|
highlight_ids=["h1", "h2", "h3"],
|
|
)
|
|
|
|
assert result.success is True
|
|
assert result.document_id == "ep-001"
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_exception_from_thread_returns_failure(self):
|
|
with (
|
|
patch("content.archive.indexer._meilisearch_available", return_value=True),
|
|
patch("asyncio.to_thread", side_effect=RuntimeError("connection refused")),
|
|
):
|
|
result = await index_episode("ep-001", "Title")
|
|
|
|
assert result.success is False
|
|
assert "connection refused" in result.error
|
|
|
|
|
|
# ── search_episodes ───────────────────────────────────────────────────────────
|
|
|
|
|
|
class TestSearchEpisodes:
|
|
@pytest.mark.asyncio
|
|
async def test_returns_empty_when_library_missing(self):
|
|
with patch("content.archive.indexer._meilisearch_available", return_value=False):
|
|
results = await search_episodes("highlights")
|
|
assert results == []
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_returns_hits_on_success(self):
|
|
fake_hits = [{"id": "ep-001", "title": "Gaming Highlights"}]
|
|
with (
|
|
patch("content.archive.indexer._meilisearch_available", return_value=True),
|
|
patch("asyncio.to_thread", return_value=fake_hits),
|
|
):
|
|
results = await search_episodes("gaming")
|
|
|
|
assert len(results) == 1
|
|
assert results[0]["id"] == "ep-001"
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_returns_empty_on_exception(self):
|
|
with (
|
|
patch("content.archive.indexer._meilisearch_available", return_value=True),
|
|
patch("asyncio.to_thread", side_effect=RuntimeError("timeout")),
|
|
):
|
|
results = await search_episodes("query")
|
|
|
|
assert results == []
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_empty_list_when_no_results(self):
|
|
with (
|
|
patch("content.archive.indexer._meilisearch_available", return_value=True),
|
|
patch("asyncio.to_thread", return_value=[]),
|
|
):
|
|
results = await search_episodes("nothing matches")
|
|
|
|
assert results == []
|