forked from Rockachopa/Timmy-time-dashboard
This commit is contained in:
170
tests/unit/test_content_indexer.py
Normal file
170
tests/unit/test_content_indexer.py
Normal file
@@ -0,0 +1,170 @@
|
||||
"""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 == []
|
||||
Reference in New Issue
Block a user