test_search_character_option.py•7.68 kB
"""Tests for character option search tool."""
import importlib
import inspect
from unittest.mock import AsyncMock, MagicMock
import pytest
from lorekeeper_mcp.api_clients.exceptions import ApiError, NetworkError
from lorekeeper_mcp.tools.search_character_option import search_character_option
search_character_option_module = importlib.import_module(
"lorekeeper_mcp.tools.search_character_option"
)
@pytest.fixture
def mock_character_option_repository() -> MagicMock:
"""Create mock character option repository for testing."""
repo = MagicMock()
repo.search = AsyncMock()
repo.get_all = AsyncMock()
return repo
@pytest.mark.asyncio
async def test_search_class_with_repository(repository_context):
"""Test looking up a class using repository context."""
repository_context.search.return_value = [{"name": "Paladin", "hit_dice": "1d10"}]
result = await search_character_option(type="class", search="Paladin")
assert len(result) == 1
assert result[0]["name"] == "Paladin"
# Verify repository.search was called with option_type
repository_context.search.assert_awaited_once()
call_kwargs = repository_context.search.call_args[1]
assert call_kwargs["option_type"] == "class"
@pytest.mark.asyncio
async def test_search_race_with_repository(repository_context):
"""Test looking up a race using repository context."""
repository_context.search.return_value = [{"name": "Elf", "speed": 30}]
result = await search_character_option(type="race", search="Elf")
assert len(result) == 1
assert result[0]["name"] == "Elf"
call_kwargs = repository_context.search.call_args[1]
assert call_kwargs["option_type"] == "race"
@pytest.mark.asyncio
async def test_search_background_with_repository(repository_context):
"""Test looking up a background using repository context."""
repository_context.search.return_value = [{"name": "Acolyte"}]
result = await search_character_option(type="background", search="Acolyte")
assert len(result) == 1
assert result[0]["name"] == "Acolyte"
call_kwargs = repository_context.search.call_args[1]
assert call_kwargs["option_type"] == "background"
@pytest.mark.asyncio
async def test_search_feat_with_repository(repository_context):
"""Test looking up a feat using repository context."""
repository_context.search.return_value = [{"name": "Sharpshooter"}]
result = await search_character_option(type="feat", search="Sharpshooter")
assert len(result) == 1
assert result[0]["name"] == "Sharpshooter"
call_kwargs = repository_context.search.call_args[1]
assert call_kwargs["option_type"] == "feat"
@pytest.mark.asyncio
async def test_search_invalid_type():
"""Test invalid type parameter raises ValueError."""
with pytest.raises(ValueError, match="Invalid type"):
await search_character_option(type="invalid-type") # type: ignore[arg-type]
@pytest.mark.asyncio
async def test_search_character_option_with_limit(repository_context):
"""Test that limit parameter is passed to repository."""
options = [{"name": f"Option {i}", "id": i} for i in range(5)]
repository_context.search.return_value = options
result = await search_character_option(type="class", limit=5)
assert len(result) == 5
call_kwargs = repository_context.search.call_args[1]
assert call_kwargs["limit"] == 5
@pytest.mark.asyncio
async def test_search_character_option_empty_results(repository_context):
"""Test character option search with no results."""
repository_context.search.return_value = []
result = await search_character_option(type="class", search="NonexistentClass")
assert result == []
@pytest.mark.asyncio
async def test_search_character_option_api_error(repository_context):
"""Test character option search handles API errors gracefully."""
repository_context.search.side_effect = ApiError("API unavailable")
with pytest.raises(ApiError, match="API unavailable"):
await search_character_option(type="class")
@pytest.mark.asyncio
async def test_search_character_option_network_error(repository_context):
"""Test character option search handles network errors."""
repository_context.search.side_effect = NetworkError("Connection timeout")
with pytest.raises(NetworkError, match="Connection timeout"):
await search_character_option(type="class")
@pytest.mark.asyncio
async def test_search_character_option_no_repository_parameter():
"""Test that search_character_option no longer accepts repository parameter."""
# This test verifies the function does NOT accept repository parameter
# and instead uses context-based injection like other tools
sig = inspect.signature(search_character_option)
assert "repository" not in sig.parameters
@pytest.mark.asyncio
async def test_search_character_option_with_documents(
repository_context,
) -> None:
"""Test search_character_option with documents filter."""
repository_context.search.return_value = [{"name": "Barbarian", "document": "srd-5e"}]
result = await search_character_option(type="class", documents=["srd-5e"])
assert len(result) == 1
assert result[0]["name"] == "Barbarian"
# Verify document parameter is passed to repository
call_kwargs = repository_context.search.call_args[1]
assert "document" in call_kwargs
assert call_kwargs["document"] == ["srd-5e"]
@pytest.fixture
def repository_context(mock_character_option_repository):
"""Fixture to inject mock repository via context for tests."""
search_character_option_module._repository_context["repository"] = (
mock_character_option_repository
)
yield mock_character_option_repository
# Clean up after test
if "repository" in search_character_option_module._repository_context:
del search_character_option_module._repository_context["repository"]
@pytest.mark.asyncio
async def test_search_character_option_with_search_param(repository_context):
"""Test character option search with search parameter."""
repository_context.search.return_value = [{"name": "Fighter", "hit_dice": "1d10"}]
result = await search_character_option(type="class", search="martial combat warrior")
assert len(result) == 1
assert result[0]["name"] == "Fighter"
# Verify repository.search was called with search parameter
repository_context.search.assert_awaited_once()
call_kwargs = repository_context.search.call_args[1]
assert call_kwargs["search"] == "martial combat warrior"
@pytest.mark.asyncio
async def test_search_character_option_search_param_with_filters(repository_context):
"""Test character option search combining search with traditional filters."""
repository_context.search.return_value = [{"name": "Acolyte"}]
result = await search_character_option(
type="background", search="religious servant", documents=["srd-5e"]
)
assert len(result) == 1
# Verify all parameters were passed to repository
call_kwargs = repository_context.search.call_args[1]
assert call_kwargs["search"] == "religious servant"
assert call_kwargs["document"] == ["srd-5e"]
@pytest.mark.asyncio
async def test_search_character_option_search_none_not_passed(repository_context):
"""Test that search=None is not passed to repository."""
repository_context.search.return_value = [{"name": "Fighter"}]
# Call without search (default is None)
await search_character_option(type="class")
call_kwargs = repository_context.search.call_args[1]
# search should not be in the params when None
assert "search" not in call_kwargs