Skip to main content
Glama
by frap129
test_search_equipment.py21.9 kB
"""Tests for equipment search tool.""" import importlib import inspect from typing import Any from unittest.mock import AsyncMock, MagicMock import pytest from lorekeeper_mcp.api_clients.exceptions import ApiError, NetworkError from lorekeeper_mcp.models import Armor, MagicItem, Weapon from lorekeeper_mcp.tools.search_equipment import ( _repository_context, search_equipment, ) search_equipment_module = importlib.import_module("lorekeeper_mcp.tools.search_equipment") @pytest.fixture def mock_equipment_repository() -> MagicMock: """Create mock equipment repository for testing.""" repo = MagicMock() repo.search = AsyncMock() repo.get_all = AsyncMock() return repo @pytest.fixture def repository_context(mock_equipment_repository): """Fixture to inject mock repository via context for tests.""" search_equipment_module._repository_context["repository"] = mock_equipment_repository yield mock_equipment_repository # Clean up after test if "repository" in search_equipment_module._repository_context: del search_equipment_module._repository_context["repository"] @pytest.fixture def sample_longsword() -> Weapon: """Sample longsword weapon for tests.""" return Weapon( name="Longsword", slug="longsword", desc="A longsword with a straight blade", document_url="https://example.com/longsword", damage_dice="1d8", damage_type="Slashing", range=5, long_range=5, distance_unit="feet", is_simple=False, is_improvised=False, ) @pytest.fixture def sample_dagger() -> Weapon: """Sample dagger weapon for tests.""" return Weapon( name="Dagger", slug="dagger", desc="A small, sharp-pointed blade", document_url="https://example.com/dagger", damage_dice="1d4", damage_type="Piercing", range=20, long_range=60, distance_unit="feet", is_simple=True, is_improvised=False, ) @pytest.fixture def sample_chain_mail() -> Armor: """Sample chain mail armor for tests.""" return Armor( name="Chain Mail", slug="chain-mail", desc="Chain mail armor", document_url="https://example.com/chain-mail", category="Heavy", base_ac=16, ) @pytest.fixture def sample_leather() -> Armor: """Sample leather armor for tests.""" return Armor( name="Leather", slug="leather", desc="Light leather armor", document_url="https://example.com/leather", category="Light", dex_bonus=True, ) @pytest.fixture def sample_bag_of_holding() -> MagicItem: """Sample magic item for tests.""" return MagicItem( name="Bag of Holding", slug="bag-of-holding", desc="This bag holds much more than it appears to hold", document_url="https://example.com/bag-of-holding", rarity="uncommon", requires_attunement=False, wondrous=True, ) @pytest.fixture def sample_wand_of_fireballs() -> MagicItem: """Sample wand magic item for tests.""" return MagicItem( name="Wand of Fireballs", slug="wand-of-fireballs", desc="This wand has 7 charges and regains 1d6+1 charges daily at dawn", document_url="https://example.com/wand-of-fireballs", rarity="rare", requires_attunement=True, type="wand", ) @pytest.mark.asyncio async def test_search_weapon(repository_context, sample_longsword): """Test looking up a weapon.""" repository_context.search.return_value = [sample_longsword] result = await search_equipment(type="weapon", search="Longsword") assert len(result) == 1 assert result[0]["name"] == "Longsword" assert result[0]["damage_dice"] == "1d8" repository_context.search.assert_awaited_once() @pytest.mark.asyncio async def test_search_armor(repository_context, sample_chain_mail): """Test looking up armor.""" repository_context.search.return_value = [sample_chain_mail] result = await search_equipment(type="armor", search="Chain Mail") assert len(result) == 1 assert result[0]["name"] == "Chain Mail" assert result[0]["category"] == "Heavy" repository_context.search.assert_awaited_once() @pytest.mark.asyncio async def test_search_simple_weapons(repository_context, sample_dagger): """Test filtering for simple weapons.""" repository_context.search.return_value = [sample_dagger] result = await search_equipment(type="weapon", is_simple=True) assert len(result) == 1 assert result[0]["is_simple"] is True call_kwargs = repository_context.search.call_args[1] assert call_kwargs["item_type"] == "weapon" assert call_kwargs["is_simple"] is True @pytest.mark.asyncio async def test_search_armor_by_category(repository_context, sample_leather): """Test looking up armor by category.""" repository_context.search.return_value = [sample_leather] result = await search_equipment(type="armor") assert len(result) == 1 assert result[0]["category"] == "Light" call_kwargs = repository_context.search.call_args[1] assert call_kwargs["item_type"] == "armor" @pytest.mark.asyncio async def test_search_equipment_api_error(repository_context): """Test equipment search handles API errors gracefully.""" repository_context.search.side_effect = ApiError("API unavailable") with pytest.raises(ApiError, match="API unavailable"): await search_equipment(type="weapon") @pytest.mark.asyncio async def test_search_equipment_network_error(repository_context): """Test equipment search handles network errors.""" repository_context.search.side_effect = NetworkError("Connection timeout") with pytest.raises(NetworkError, match="Connection timeout"): await search_equipment(type="armor") @pytest.mark.asyncio async def test_equipment_search_by_search_param( repository_context, sample_longsword, sample_dagger ): """Test that equipment search passes search filter to repository for server-side filtering.""" # Repository returns only matching weapon when called with search filter repository_context.search.return_value = [sample_longsword] # Call with search filter - should pass to repository result = await search_equipment(type="weapon", search="longsword") # Should only return Longsword assert len(result) == 1 assert result[0]["name"] == "Longsword" # Verify repository.search was called with search parameter repository_context.search.assert_awaited_once_with( limit=20, item_type="weapon", search="longsword" ) @pytest.mark.asyncio async def test_search_equipment_limit_applied(repository_context): """Test that search_equipment passes limit to repository for server-side limiting.""" weapons = [ Weapon( name=f"Weapon {i}", slug=f"weapon-{i}", desc=f"Weapon {i}", document_url="https://example.com", damage_dice="1d8", damage_type="Slashing", range=5, long_range=5, distance_unit="feet", is_simple=False, is_improvised=False, ) for i in range(1, 6) # Only 5 weapons since limit=5 ] repository_context.search.return_value = weapons result = await search_equipment(type="weapon", limit=5) # Should return 5 weapons from repository (server-side limit) assert len(result) == 5 # Verify repository was called with limit parameter repository_context.search.assert_awaited_once_with(limit=5, item_type="weapon") @pytest.mark.asyncio async def test_search_equipment_default_repository(): """Test that search_equipment does NOT have repository parameter in signature.""" sig = inspect.signature(search_equipment) assert "repository" not in sig.parameters @pytest.mark.asyncio async def test_search_magic_items(repository_context): """Test looking up magic items.""" sample_bag = MagicItem( name="Bag of Holding", key="bag-of-holding", desc="This bag holds much more than it appears", rarity="uncommon", requires_attunement=False, ) repository_context.search.return_value = [sample_bag] result = await search_equipment(type="magic-item", search="Bag") assert len(result) == 1 assert result[0]["name"] == "Bag of Holding" assert result[0]["rarity"] == "uncommon" repository_context.search.assert_awaited_once() @pytest.mark.asyncio async def test_search_magic_items_by_rarity(repository_context): """Test filtering magic items by rarity.""" sample_rare = MagicItem( name="Wand of Fireballs", key="wand-of-fireballs", desc="This wand has 7 charges", rarity="rare", requires_attunement=True, ) repository_context.search.return_value = [sample_rare] result = await search_equipment(type="magic-item", rarity="rare") assert len(result) == 1 assert result[0]["rarity"] == "rare" call_kwargs = repository_context.search.call_args[1] assert call_kwargs["item_type"] == "magic-item" assert call_kwargs["rarity"] == "rare" @pytest.mark.asyncio async def test_search_magic_items_by_attunement(repository_context): """Test filtering magic items by attunement requirement.""" sample_attune = MagicItem( name="Ring of Protection", key="ring-of-protection", desc="This ring offers protection", rarity="rare", requires_attunement=True, ) repository_context.search.return_value = [sample_attune] result = await search_equipment( type="magic-item", requires_attunement="yes", ) assert len(result) == 1 assert result[0]["requires_attunement"] is True call_kwargs = repository_context.search.call_args[1] assert call_kwargs["item_type"] == "magic-item" assert call_kwargs["requires_attunement"] is True @pytest.mark.asyncio async def test_search_all_equipment_types(repository_context): """Test searching all equipment types together.""" sample_longsword = Weapon( name="Longsword", slug="longsword", desc="A longsword", document_url="https://example.com/longsword", damage_dice="1d8", damage_type="Slashing", range=5, long_range=5, distance_unit="feet", is_simple=False, is_improvised=False, ) sample_armor = Armor( name="Plate", slug="plate", desc="Plate armor", document_url="https://example.com/plate", category="Heavy", ) sample_magic = MagicItem( name="Cloak of Invisibility", slug="cloak-of-invisibility", desc="While wearing this cloak you are invisible", rarity="legendary", requires_attunement=False, ) # Mock returns different items based on item_type filter def search_side_effect(**kwargs: Any) -> list[Any]: """Return different results based on item_type.""" item_type = kwargs.get("item_type") if item_type == "weapon": return [sample_longsword] if item_type == "armor": return [sample_armor] if item_type == "magic-item": return [sample_magic] return [] repository_context.search.side_effect = search_side_effect result = await search_equipment(type="all", limit=20) # Should get one of each type assert len(result) == 3 names = {item["name"] for item in result} assert names == {"Longsword", "Plate", "Cloak of Invisibility"} @pytest.mark.asyncio async def test_search_magic_items_empty_results(repository_context): """Test magic item search when no results are found.""" repository_context.search.return_value = [] result = await search_equipment(type="magic-item", search="NonExistent") assert len(result) == 0 @pytest.mark.asyncio async def test_search_weapon_by_cost_min(repository_context, sample_longsword): """Test filtering weapons by minimum cost.""" repository_context.search.return_value = [sample_longsword] result = await search_equipment(type="weapon", cost_min=10) assert len(result) == 1 assert result[0]["name"] == "Longsword" call_kwargs = repository_context.search.call_args[1] assert call_kwargs["item_type"] == "weapon" assert call_kwargs["cost_min"] == 10 @pytest.mark.asyncio async def test_search_weapon_by_cost_max(repository_context, sample_dagger): """Test filtering weapons by maximum cost.""" repository_context.search.return_value = [sample_dagger] result = await search_equipment(type="weapon", cost_max=5) assert len(result) == 1 assert result[0]["name"] == "Dagger" call_kwargs = repository_context.search.call_args[1] assert call_kwargs["item_type"] == "weapon" assert call_kwargs["cost_max"] == 5 @pytest.mark.asyncio async def test_search_armor_by_cost_max(repository_context, sample_leather): """Test filtering armor by maximum cost.""" repository_context.search.return_value = [sample_leather] result = await search_equipment(type="armor", cost_max=10) assert len(result) == 1 assert result[0]["name"] == "Leather" call_kwargs = repository_context.search.call_args[1] assert call_kwargs["item_type"] == "armor" assert call_kwargs["cost_max"] == 10 @pytest.mark.asyncio async def test_search_weapon_by_weight_max(repository_context, sample_dagger): """Test filtering weapons by maximum weight.""" repository_context.search.return_value = [sample_dagger] result = await search_equipment(type="weapon", weight_max=3) assert len(result) == 1 assert result[0]["name"] == "Dagger" call_kwargs = repository_context.search.call_args[1] assert call_kwargs["item_type"] == "weapon" assert call_kwargs["weight_max"] == 3 @pytest.mark.asyncio async def test_search_weapon_by_is_finesse(repository_context, sample_dagger): """Test filtering weapons by finesse property.""" repository_context.search.return_value = [sample_dagger] result = await search_equipment(type="weapon", is_finesse=True) assert len(result) == 1 assert result[0]["name"] == "Dagger" call_kwargs = repository_context.search.call_args[1] assert call_kwargs["item_type"] == "weapon" assert call_kwargs["is_finesse"] is True @pytest.mark.asyncio async def test_search_weapon_by_is_light(repository_context, sample_dagger): """Test filtering weapons by light property.""" repository_context.search.return_value = [sample_dagger] result = await search_equipment(type="weapon", is_light=True) assert len(result) == 1 assert result[0]["name"] == "Dagger" call_kwargs = repository_context.search.call_args[1] assert call_kwargs["item_type"] == "weapon" assert call_kwargs["is_light"] is True @pytest.mark.asyncio async def test_search_weapon_by_is_magic(repository_context, sample_longsword): """Test filtering weapons by magic property.""" repository_context.search.return_value = [sample_longsword] result = await search_equipment(type="weapon", is_magic=True) assert len(result) == 1 assert result[0]["name"] == "Longsword" call_kwargs = repository_context.search.call_args[1] assert call_kwargs["item_type"] == "weapon" assert call_kwargs["is_magic"] is True @pytest.mark.asyncio async def test_search_weapon_multiple_new_filters(repository_context, sample_longsword): """Test filtering weapons with multiple new parameters.""" repository_context.search.return_value = [sample_longsword] result = await search_equipment( type="weapon", cost_min=5, cost_max=25, weight_max=4, is_finesse=True, ) assert len(result) == 1 assert result[0]["name"] == "Longsword" call_kwargs = repository_context.search.call_args[1] assert call_kwargs["item_type"] == "weapon" assert call_kwargs["cost_min"] == 5 assert call_kwargs["cost_max"] == 25 assert call_kwargs["weight_max"] == 4 assert call_kwargs["is_finesse"] is True @pytest.mark.asyncio async def test_search_equipment_backward_compatibility_no_new_params( repository_context, sample_longsword ): """Test backward compatibility when new parameters are not used.""" repository_context.search.return_value = [sample_longsword] # Old-style call with search instead of deprecated name parameter result = await search_equipment(type="weapon", search="longsword", limit=20) assert len(result) == 1 assert result[0]["name"] == "Longsword" call_kwargs = repository_context.search.call_args[1] assert call_kwargs["item_type"] == "weapon" assert call_kwargs["search"] == "longsword" assert call_kwargs["limit"] == 20 # New parameters should not be present assert "cost_min" not in call_kwargs assert "cost_max" not in call_kwargs assert "weight_max" not in call_kwargs assert "is_finesse" not in call_kwargs assert "is_light" not in call_kwargs assert "is_magic" not in call_kwargs @pytest.mark.asyncio async def test_search_weapon_with_document_filter(repository_context, sample_longsword): """Test filtering weapons by document name.""" repository_context.search.return_value = [sample_longsword] result = await search_equipment(type="weapon", documents=["System Reference Document 5.1"]) assert len(result) == 1 assert result[0]["name"] == "Longsword" call_kwargs = repository_context.search.call_args[1] assert call_kwargs["item_type"] == "weapon" assert call_kwargs["document"] == ["System Reference Document 5.1"] @pytest.mark.asyncio async def test_search_armor_with_document_filter(repository_context, sample_chain_mail): """Test filtering armor by document name.""" repository_context.search.return_value = [sample_chain_mail] result = await search_equipment(type="armor", documents=["System Reference Document 5.1"]) assert len(result) == 1 assert result[0]["name"] == "Chain Mail" call_kwargs = repository_context.search.call_args[1] assert call_kwargs["item_type"] == "armor" assert call_kwargs["document"] == ["System Reference Document 5.1"] @pytest.mark.asyncio async def test_search_magic_item_with_document_filter(repository_context, sample_bag_of_holding): """Test filtering magic items by document name.""" repository_context.search.return_value = [sample_bag_of_holding] result = await search_equipment(type="magic-item", documents=["System Reference Document 5.1"]) assert len(result) == 1 assert result[0]["name"] == "Bag of Holding" call_kwargs = repository_context.search.call_args[1] assert call_kwargs["item_type"] == "magic-item" assert call_kwargs["document"] == ["System Reference Document 5.1"] @pytest.mark.asyncio async def test_search_equipment_with_documents() -> None: """Test search_equipment with documents filter.""" # Mock repository class MockRepository: async def search(self, **kwargs: Any) -> list[Any]: # Verify document parameter is passed assert "document" in kwargs assert kwargs["document"] == ["srd-5e"] return [ Weapon( name="Longsword", slug="longsword", desc="A longsword with a straight blade", document_url="https://example.com/longsword", damage_dice="1d8", damage_type="Slashing", range=5, long_range=5, distance_unit="feet", is_simple=False, is_improvised=False, ) ] _repository_context["repository"] = MockRepository() try: results = await search_equipment(type="weapon", search="longsword", documents=["srd-5e"]) assert len(results) == 1 assert results[0]["name"] == "Longsword" finally: _repository_context.clear() @pytest.mark.asyncio async def test_search_equipment_with_search_param(repository_context, sample_longsword): """Test equipment search with search parameter.""" repository_context.search.return_value = [sample_longsword] result = await search_equipment(type="weapon", search="slashing blade for combat") assert len(result) == 1 assert result[0]["name"] == "Longsword" # 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"] == "slashing blade for combat" @pytest.mark.asyncio async def test_search_equipment_search_param_with_filters(repository_context, sample_dagger): """Test equipment search combining search with traditional filters.""" repository_context.search.return_value = [sample_dagger] result = await search_equipment(type="weapon", search="small throwing knife", is_simple=True) assert len(result) == 1 # Verify all parameters were passed to repository call_kwargs = repository_context.search.call_args[1] assert call_kwargs["search"] == "small throwing knife" assert call_kwargs["is_simple"] is True @pytest.mark.asyncio async def test_search_equipment_search_none_not_passed(repository_context, sample_longsword): """Test that search=None is not passed to repository.""" repository_context.search.return_value = [sample_longsword] # Call without search (using only type filter) await search_equipment(type="weapon") call_kwargs = repository_context.search.call_args[1] # search should not be in the params when None assert "search" not in call_kwargs

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/frap129/lorekeeper-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server