Add InventoryService for managing character inventory, equipment, and consumable usage. Key features: - Add/remove items with inventory capacity checks - Equipment slot validation (weapon, off_hand, helmet, chest, gloves, boots, accessory_1, accessory_2) - Level and class requirement validation for equipment - Consumable usage with instant and duration-based effects - Combat-specific consumable method returning effects for combat system - Bulk operations (add_items, get_items_by_type, get_equippable_items) Design decision: Uses full Item object storage (not IDs) to support procedurally generated items with unique identifiers. Files added: - /api/app/services/inventory_service.py (560 lines) - /api/tests/test_inventory_service.py (51 tests passing) Task 2.3 of Phase 4 Combat Implementation complete.
820 lines
30 KiB
Python
820 lines
30 KiB
Python
"""
|
|
Unit tests for the InventoryService.
|
|
|
|
Tests cover:
|
|
- Adding and removing items
|
|
- Equipment slot validation
|
|
- Level and class requirement checks
|
|
- Consumable usage and effect application
|
|
- Bulk operations
|
|
- Error handling
|
|
"""
|
|
|
|
import pytest
|
|
from unittest.mock import Mock, patch, MagicMock
|
|
from typing import List
|
|
|
|
from app.models.character import Character
|
|
from app.models.items import Item
|
|
from app.models.effects import Effect
|
|
from app.models.stats import Stats
|
|
from app.models.enums import ItemType, ItemRarity, EffectType, StatType, DamageType
|
|
from app.models.skills import PlayerClass
|
|
from app.models.origins import Origin
|
|
from app.services.inventory_service import (
|
|
InventoryService,
|
|
ItemNotFoundError,
|
|
CannotEquipError,
|
|
InvalidSlotError,
|
|
CannotUseItemError,
|
|
InventoryFullError,
|
|
ConsumableResult,
|
|
VALID_SLOTS,
|
|
ITEM_TYPE_SLOTS,
|
|
MAX_INVENTORY_SIZE,
|
|
get_inventory_service,
|
|
)
|
|
|
|
|
|
# =============================================================================
|
|
# Test Fixtures
|
|
# =============================================================================
|
|
|
|
@pytest.fixture
|
|
def mock_character_service():
|
|
"""Create a mock CharacterService."""
|
|
service = Mock()
|
|
service.update_character = Mock()
|
|
return service
|
|
|
|
|
|
@pytest.fixture
|
|
def inventory_service(mock_character_service):
|
|
"""Create InventoryService with mocked dependencies."""
|
|
return InventoryService(character_service=mock_character_service)
|
|
|
|
|
|
@pytest.fixture
|
|
def mock_origin():
|
|
"""Create a minimal Origin for testing."""
|
|
from app.models.origins import StartingLocation, StartingBonus
|
|
|
|
starting_location = StartingLocation(
|
|
id="test_location",
|
|
name="Test Village",
|
|
region="Test Region",
|
|
description="A test location"
|
|
)
|
|
|
|
return Origin(
|
|
id="test_origin",
|
|
name="Test Origin",
|
|
description="A test origin for testing purposes",
|
|
starting_location=starting_location,
|
|
narrative_hooks=["test hook"],
|
|
starting_bonus=None,
|
|
)
|
|
|
|
|
|
@pytest.fixture
|
|
def mock_player_class():
|
|
"""Create a minimal PlayerClass for testing."""
|
|
return PlayerClass(
|
|
class_id="warrior",
|
|
name="Warrior",
|
|
description="A mighty warrior",
|
|
base_stats=Stats(
|
|
strength=14,
|
|
dexterity=10,
|
|
constitution=12,
|
|
intelligence=8,
|
|
wisdom=10,
|
|
charisma=8,
|
|
luck=8,
|
|
),
|
|
skill_trees=[],
|
|
starting_abilities=["basic_attack"],
|
|
)
|
|
|
|
|
|
@pytest.fixture
|
|
def test_character(mock_player_class, mock_origin):
|
|
"""Create a test character."""
|
|
return Character(
|
|
character_id="char_test_123",
|
|
user_id="user_test_456",
|
|
name="Test Hero",
|
|
player_class=mock_player_class,
|
|
origin=mock_origin,
|
|
level=5,
|
|
experience=0,
|
|
base_stats=mock_player_class.base_stats.copy(),
|
|
inventory=[],
|
|
equipped={},
|
|
gold=100,
|
|
)
|
|
|
|
|
|
@pytest.fixture
|
|
def test_weapon():
|
|
"""Create a test weapon item."""
|
|
return Item(
|
|
item_id="iron_sword",
|
|
name="Iron Sword",
|
|
item_type=ItemType.WEAPON,
|
|
rarity=ItemRarity.COMMON,
|
|
description="A sturdy iron sword",
|
|
value=50,
|
|
damage=10,
|
|
damage_type=DamageType.PHYSICAL,
|
|
crit_chance=0.05,
|
|
crit_multiplier=2.0,
|
|
required_level=1,
|
|
)
|
|
|
|
|
|
@pytest.fixture
|
|
def test_armor():
|
|
"""Create a test armor item."""
|
|
return Item(
|
|
item_id="leather_chest",
|
|
name="Leather Chestpiece",
|
|
item_type=ItemType.ARMOR,
|
|
rarity=ItemRarity.COMMON,
|
|
description="Simple leather armor",
|
|
value=40,
|
|
defense=5,
|
|
resistance=2,
|
|
required_level=1,
|
|
)
|
|
|
|
|
|
@pytest.fixture
|
|
def test_helmet():
|
|
"""Create a test helmet item."""
|
|
return Item(
|
|
item_id="iron_helm",
|
|
name="Iron Helmet",
|
|
item_type=ItemType.ARMOR,
|
|
rarity=ItemRarity.UNCOMMON,
|
|
description="A protective iron helmet",
|
|
value=30,
|
|
defense=3,
|
|
resistance=1,
|
|
required_level=3,
|
|
)
|
|
|
|
|
|
@pytest.fixture
|
|
def test_consumable():
|
|
"""Create a test consumable item (health potion)."""
|
|
return Item(
|
|
item_id="health_potion_small",
|
|
name="Small Health Potion",
|
|
item_type=ItemType.CONSUMABLE,
|
|
rarity=ItemRarity.COMMON,
|
|
description="Restores 25 HP",
|
|
value=10,
|
|
effects_on_use=[
|
|
Effect(
|
|
effect_id="heal_25",
|
|
name="Minor Healing",
|
|
effect_type=EffectType.HOT,
|
|
duration=1,
|
|
power=25,
|
|
stacks=1,
|
|
)
|
|
],
|
|
)
|
|
|
|
|
|
@pytest.fixture
|
|
def test_buff_potion():
|
|
"""Create a test buff potion."""
|
|
return Item(
|
|
item_id="strength_potion",
|
|
name="Potion of Strength",
|
|
item_type=ItemType.CONSUMABLE,
|
|
rarity=ItemRarity.UNCOMMON,
|
|
description="Increases strength temporarily",
|
|
value=25,
|
|
effects_on_use=[
|
|
Effect(
|
|
effect_id="str_buff",
|
|
name="Strength Boost",
|
|
effect_type=EffectType.BUFF,
|
|
duration=3,
|
|
power=5,
|
|
stat_affected=StatType.STRENGTH,
|
|
stacks=1,
|
|
)
|
|
],
|
|
)
|
|
|
|
|
|
@pytest.fixture
|
|
def test_quest_item():
|
|
"""Create a test quest item."""
|
|
return Item(
|
|
item_id="ancient_key",
|
|
name="Ancient Key",
|
|
item_type=ItemType.QUEST_ITEM,
|
|
rarity=ItemRarity.RARE,
|
|
description="An ornate key to the ancient tomb",
|
|
value=0,
|
|
is_tradeable=False,
|
|
)
|
|
|
|
|
|
@pytest.fixture
|
|
def high_level_weapon():
|
|
"""Create a weapon with high level requirement."""
|
|
return Item(
|
|
item_id="legendary_blade",
|
|
name="Blade of Ages",
|
|
item_type=ItemType.WEAPON,
|
|
rarity=ItemRarity.LEGENDARY,
|
|
description="A blade forged in ancient times",
|
|
value=5000,
|
|
damage=50,
|
|
damage_type=DamageType.PHYSICAL,
|
|
required_level=20, # Higher than test character's level 5
|
|
)
|
|
|
|
|
|
# =============================================================================
|
|
# Read Operation Tests
|
|
# =============================================================================
|
|
|
|
class TestGetInventory:
|
|
"""Tests for get_inventory() and related read operations."""
|
|
|
|
def test_get_empty_inventory(self, inventory_service, test_character):
|
|
"""Test getting inventory when it's empty."""
|
|
items = inventory_service.get_inventory(test_character)
|
|
assert items == []
|
|
|
|
def test_get_inventory_with_items(self, inventory_service, test_character, test_weapon, test_armor):
|
|
"""Test getting inventory with items."""
|
|
test_character.inventory = [test_weapon, test_armor]
|
|
|
|
items = inventory_service.get_inventory(test_character)
|
|
|
|
assert len(items) == 2
|
|
assert test_weapon in items
|
|
assert test_armor in items
|
|
|
|
def test_get_inventory_returns_copy(self, inventory_service, test_character, test_weapon):
|
|
"""Test that get_inventory returns a new list (not the original)."""
|
|
test_character.inventory = [test_weapon]
|
|
|
|
items = inventory_service.get_inventory(test_character)
|
|
items.append(test_weapon) # Modify returned list
|
|
|
|
# Original inventory should be unchanged
|
|
assert len(test_character.inventory) == 1
|
|
|
|
def test_get_item_by_id_found(self, inventory_service, test_character, test_weapon):
|
|
"""Test finding an item by ID."""
|
|
test_character.inventory = [test_weapon]
|
|
|
|
item = inventory_service.get_item_by_id(test_character, "iron_sword")
|
|
|
|
assert item is test_weapon
|
|
|
|
def test_get_item_by_id_not_found(self, inventory_service, test_character, test_weapon):
|
|
"""Test item not found returns None."""
|
|
test_character.inventory = [test_weapon]
|
|
|
|
item = inventory_service.get_item_by_id(test_character, "nonexistent_item")
|
|
|
|
assert item is None
|
|
|
|
def test_get_equipped_items_empty(self, inventory_service, test_character):
|
|
"""Test getting equipped items when nothing equipped."""
|
|
equipped = inventory_service.get_equipped_items(test_character)
|
|
assert equipped == {}
|
|
|
|
def test_get_equipped_items_with_equipment(self, inventory_service, test_character, test_weapon):
|
|
"""Test getting equipped items."""
|
|
test_character.equipped = {"weapon": test_weapon}
|
|
|
|
equipped = inventory_service.get_equipped_items(test_character)
|
|
|
|
assert "weapon" in equipped
|
|
assert equipped["weapon"] is test_weapon
|
|
|
|
def test_get_equipped_item_specific_slot(self, inventory_service, test_character, test_weapon):
|
|
"""Test getting item from a specific slot."""
|
|
test_character.equipped = {"weapon": test_weapon}
|
|
|
|
item = inventory_service.get_equipped_item(test_character, "weapon")
|
|
|
|
assert item is test_weapon
|
|
|
|
def test_get_equipped_item_empty_slot(self, inventory_service, test_character):
|
|
"""Test getting item from empty slot returns None."""
|
|
item = inventory_service.get_equipped_item(test_character, "weapon")
|
|
assert item is None
|
|
|
|
def test_get_inventory_count(self, inventory_service, test_character, test_weapon, test_armor):
|
|
"""Test counting inventory items."""
|
|
test_character.inventory = [test_weapon, test_armor]
|
|
|
|
count = inventory_service.get_inventory_count(test_character)
|
|
|
|
assert count == 2
|
|
|
|
|
|
# =============================================================================
|
|
# Add/Remove Item Tests
|
|
# =============================================================================
|
|
|
|
class TestAddItem:
|
|
"""Tests for add_item()."""
|
|
|
|
def test_add_item_success(self, inventory_service, test_character, test_weapon, mock_character_service):
|
|
"""Test successfully adding an item."""
|
|
inventory_service.add_item(test_character, test_weapon, "user_test_456")
|
|
|
|
assert test_weapon in test_character.inventory
|
|
mock_character_service.update_character.assert_called_once()
|
|
|
|
def test_add_item_without_save(self, inventory_service, test_character, test_weapon, mock_character_service):
|
|
"""Test adding item without persistence."""
|
|
inventory_service.add_item(test_character, test_weapon, "user_test_456", save=False)
|
|
|
|
assert test_weapon in test_character.inventory
|
|
mock_character_service.update_character.assert_not_called()
|
|
|
|
def test_add_multiple_items(self, inventory_service, test_character, test_weapon, test_armor, mock_character_service):
|
|
"""Test adding multiple items."""
|
|
inventory_service.add_item(test_character, test_weapon, "user_test_456")
|
|
inventory_service.add_item(test_character, test_armor, "user_test_456")
|
|
|
|
assert len(test_character.inventory) == 2
|
|
|
|
|
|
class TestRemoveItem:
|
|
"""Tests for remove_item()."""
|
|
|
|
def test_remove_item_success(self, inventory_service, test_character, test_weapon, mock_character_service):
|
|
"""Test successfully removing an item."""
|
|
test_character.inventory = [test_weapon]
|
|
|
|
removed = inventory_service.remove_item(test_character, "iron_sword", "user_test_456")
|
|
|
|
assert removed is test_weapon
|
|
assert test_weapon not in test_character.inventory
|
|
mock_character_service.update_character.assert_called_once()
|
|
|
|
def test_remove_item_not_found(self, inventory_service, test_character):
|
|
"""Test removing non-existent item raises error."""
|
|
with pytest.raises(ItemNotFoundError) as exc:
|
|
inventory_service.remove_item(test_character, "nonexistent", "user_test_456")
|
|
|
|
assert "nonexistent" in str(exc.value)
|
|
|
|
def test_remove_item_from_multiple(self, inventory_service, test_character, test_weapon, test_armor):
|
|
"""Test removing one item from multiple."""
|
|
test_character.inventory = [test_weapon, test_armor]
|
|
|
|
inventory_service.remove_item(test_character, "iron_sword", "user_test_456")
|
|
|
|
assert test_weapon not in test_character.inventory
|
|
assert test_armor in test_character.inventory
|
|
|
|
def test_drop_item_alias(self, inventory_service, test_character, test_weapon, mock_character_service):
|
|
"""Test drop_item is an alias for remove_item."""
|
|
test_character.inventory = [test_weapon]
|
|
|
|
dropped = inventory_service.drop_item(test_character, "iron_sword", "user_test_456")
|
|
|
|
assert dropped is test_weapon
|
|
assert test_weapon not in test_character.inventory
|
|
|
|
|
|
# =============================================================================
|
|
# Equipment Tests
|
|
# =============================================================================
|
|
|
|
class TestEquipItem:
|
|
"""Tests for equip_item()."""
|
|
|
|
def test_equip_weapon_to_weapon_slot(self, inventory_service, test_character, test_weapon, mock_character_service):
|
|
"""Test equipping a weapon to weapon slot."""
|
|
test_character.inventory = [test_weapon]
|
|
|
|
previous = inventory_service.equip_item(
|
|
test_character, "iron_sword", "weapon", "user_test_456"
|
|
)
|
|
|
|
assert previous is None
|
|
assert test_character.equipped.get("weapon") is test_weapon
|
|
assert test_weapon not in test_character.inventory
|
|
mock_character_service.update_character.assert_called_once()
|
|
|
|
def test_equip_armor_to_chest_slot(self, inventory_service, test_character, test_armor, mock_character_service):
|
|
"""Test equipping armor to chest slot."""
|
|
test_character.inventory = [test_armor]
|
|
|
|
inventory_service.equip_item(test_character, "leather_chest", "chest", "user_test_456")
|
|
|
|
assert test_character.equipped.get("chest") is test_armor
|
|
|
|
def test_equip_returns_previous_item(self, inventory_service, test_character, test_weapon, mock_character_service):
|
|
"""Test that equipping returns the previously equipped item."""
|
|
old_weapon = Item(
|
|
item_id="old_sword",
|
|
name="Old Sword",
|
|
item_type=ItemType.WEAPON,
|
|
damage=5,
|
|
)
|
|
test_character.inventory = [test_weapon]
|
|
test_character.equipped = {"weapon": old_weapon}
|
|
|
|
previous = inventory_service.equip_item(
|
|
test_character, "iron_sword", "weapon", "user_test_456"
|
|
)
|
|
|
|
assert previous is old_weapon
|
|
assert old_weapon in test_character.inventory # Returned to inventory
|
|
|
|
def test_equip_to_invalid_slot_raises_error(self, inventory_service, test_character, test_weapon):
|
|
"""Test equipping to invalid slot raises InvalidSlotError."""
|
|
test_character.inventory = [test_weapon]
|
|
|
|
with pytest.raises(InvalidSlotError) as exc:
|
|
inventory_service.equip_item(
|
|
test_character, "iron_sword", "invalid_slot", "user_test_456"
|
|
)
|
|
|
|
assert "invalid_slot" in str(exc.value)
|
|
assert "Valid slots" in str(exc.value)
|
|
|
|
def test_equip_weapon_to_armor_slot_raises_error(self, inventory_service, test_character, test_weapon):
|
|
"""Test equipping weapon to armor slot raises CannotEquipError."""
|
|
test_character.inventory = [test_weapon]
|
|
|
|
with pytest.raises(CannotEquipError) as exc:
|
|
inventory_service.equip_item(
|
|
test_character, "iron_sword", "chest", "user_test_456"
|
|
)
|
|
|
|
assert "weapon" in str(exc.value).lower()
|
|
|
|
def test_equip_armor_to_weapon_slot_raises_error(self, inventory_service, test_character, test_armor):
|
|
"""Test equipping armor to weapon slot raises CannotEquipError."""
|
|
test_character.inventory = [test_armor]
|
|
|
|
with pytest.raises(CannotEquipError) as exc:
|
|
inventory_service.equip_item(
|
|
test_character, "leather_chest", "weapon", "user_test_456"
|
|
)
|
|
|
|
assert "armor" in str(exc.value).lower()
|
|
|
|
def test_equip_item_not_in_inventory(self, inventory_service, test_character):
|
|
"""Test equipping item not in inventory raises ItemNotFoundError."""
|
|
with pytest.raises(ItemNotFoundError):
|
|
inventory_service.equip_item(
|
|
test_character, "nonexistent", "weapon", "user_test_456"
|
|
)
|
|
|
|
def test_equip_item_level_requirement_not_met(self, inventory_service, test_character, high_level_weapon):
|
|
"""Test equipping item with unmet level requirement raises error."""
|
|
test_character.inventory = [high_level_weapon]
|
|
test_character.level = 5 # Item requires level 20
|
|
|
|
with pytest.raises(CannotEquipError) as exc:
|
|
inventory_service.equip_item(
|
|
test_character, "legendary_blade", "weapon", "user_test_456"
|
|
)
|
|
|
|
assert "level 20" in str(exc.value)
|
|
assert "level 5" in str(exc.value)
|
|
|
|
def test_equip_consumable_raises_error(self, inventory_service, test_character, test_consumable):
|
|
"""Test equipping consumable raises CannotEquipError."""
|
|
test_character.inventory = [test_consumable]
|
|
|
|
with pytest.raises(CannotEquipError) as exc:
|
|
inventory_service.equip_item(
|
|
test_character, "health_potion_small", "weapon", "user_test_456"
|
|
)
|
|
|
|
assert "consumable" in str(exc.value).lower()
|
|
|
|
def test_equip_quest_item_raises_error(self, inventory_service, test_character, test_quest_item):
|
|
"""Test equipping quest item raises CannotEquipError."""
|
|
test_character.inventory = [test_quest_item]
|
|
|
|
with pytest.raises(CannotEquipError) as exc:
|
|
inventory_service.equip_item(
|
|
test_character, "ancient_key", "weapon", "user_test_456"
|
|
)
|
|
|
|
def test_equip_weapon_to_off_hand(self, inventory_service, test_character, test_weapon, mock_character_service):
|
|
"""Test equipping weapon to off_hand slot."""
|
|
test_character.inventory = [test_weapon]
|
|
|
|
inventory_service.equip_item(
|
|
test_character, "iron_sword", "off_hand", "user_test_456"
|
|
)
|
|
|
|
assert test_character.equipped.get("off_hand") is test_weapon
|
|
|
|
|
|
class TestUnequipItem:
|
|
"""Tests for unequip_item()."""
|
|
|
|
def test_unequip_item_success(self, inventory_service, test_character, test_weapon, mock_character_service):
|
|
"""Test successfully unequipping an item."""
|
|
test_character.equipped = {"weapon": test_weapon}
|
|
|
|
unequipped = inventory_service.unequip_item(test_character, "weapon", "user_test_456")
|
|
|
|
assert unequipped is test_weapon
|
|
assert "weapon" not in test_character.equipped
|
|
assert test_weapon in test_character.inventory
|
|
mock_character_service.update_character.assert_called_once()
|
|
|
|
def test_unequip_empty_slot_returns_none(self, inventory_service, test_character, mock_character_service):
|
|
"""Test unequipping from empty slot returns None."""
|
|
unequipped = inventory_service.unequip_item(test_character, "weapon", "user_test_456")
|
|
|
|
assert unequipped is None
|
|
|
|
def test_unequip_invalid_slot_raises_error(self, inventory_service, test_character):
|
|
"""Test unequipping from invalid slot raises InvalidSlotError."""
|
|
with pytest.raises(InvalidSlotError):
|
|
inventory_service.unequip_item(test_character, "invalid_slot", "user_test_456")
|
|
|
|
|
|
# =============================================================================
|
|
# Consumable Tests
|
|
# =============================================================================
|
|
|
|
class TestUseConsumable:
|
|
"""Tests for use_consumable()."""
|
|
|
|
def test_use_health_potion(self, inventory_service, test_character, test_consumable, mock_character_service):
|
|
"""Test using a health potion restores HP."""
|
|
test_character.inventory = [test_consumable]
|
|
|
|
result = inventory_service.use_consumable(
|
|
test_character, "health_potion_small", "user_test_456",
|
|
current_hp=50, max_hp=100
|
|
)
|
|
|
|
assert isinstance(result, ConsumableResult)
|
|
assert result.hp_restored == 25 # Potion restores 25, capped at missing HP
|
|
assert result.item_name == "Small Health Potion"
|
|
assert test_consumable not in test_character.inventory
|
|
mock_character_service.update_character.assert_called_once()
|
|
|
|
def test_use_health_potion_capped_at_max(self, inventory_service, test_character, test_consumable):
|
|
"""Test HP restoration is capped at max HP."""
|
|
test_character.inventory = [test_consumable]
|
|
|
|
result = inventory_service.use_consumable(
|
|
test_character, "health_potion_small", "user_test_456",
|
|
current_hp=90, max_hp=100 # Only missing 10 HP
|
|
)
|
|
|
|
assert result.hp_restored == 10 # Only restores missing amount
|
|
|
|
def test_use_consumable_at_full_hp(self, inventory_service, test_character, test_consumable):
|
|
"""Test using potion at full HP restores 0."""
|
|
test_character.inventory = [test_consumable]
|
|
|
|
result = inventory_service.use_consumable(
|
|
test_character, "health_potion_small", "user_test_456",
|
|
current_hp=100, max_hp=100
|
|
)
|
|
|
|
assert result.hp_restored == 0
|
|
|
|
def test_use_buff_potion(self, inventory_service, test_character, test_buff_potion, mock_character_service):
|
|
"""Test using a buff potion."""
|
|
test_character.inventory = [test_buff_potion]
|
|
|
|
result = inventory_service.use_consumable(
|
|
test_character, "strength_potion", "user_test_456"
|
|
)
|
|
|
|
assert result.item_name == "Potion of Strength"
|
|
assert len(result.effects_applied) == 1
|
|
assert result.effects_applied[0]["effect_type"] == "buff"
|
|
assert result.effects_applied[0]["stat_affected"] == "strength"
|
|
|
|
def test_use_non_consumable_raises_error(self, inventory_service, test_character, test_weapon):
|
|
"""Test using non-consumable item raises CannotUseItemError."""
|
|
test_character.inventory = [test_weapon]
|
|
|
|
with pytest.raises(CannotUseItemError) as exc:
|
|
inventory_service.use_consumable(
|
|
test_character, "iron_sword", "user_test_456"
|
|
)
|
|
|
|
assert "not a consumable" in str(exc.value)
|
|
|
|
def test_use_item_not_in_inventory_raises_error(self, inventory_service, test_character):
|
|
"""Test using item not in inventory raises ItemNotFoundError."""
|
|
with pytest.raises(ItemNotFoundError):
|
|
inventory_service.use_consumable(
|
|
test_character, "nonexistent", "user_test_456"
|
|
)
|
|
|
|
def test_consumable_result_to_dict(self, inventory_service, test_character, test_consumable):
|
|
"""Test ConsumableResult serialization."""
|
|
test_character.inventory = [test_consumable]
|
|
|
|
result = inventory_service.use_consumable(
|
|
test_character, "health_potion_small", "user_test_456",
|
|
current_hp=50, max_hp=100
|
|
)
|
|
|
|
result_dict = result.to_dict()
|
|
|
|
assert "item_name" in result_dict
|
|
assert "hp_restored" in result_dict
|
|
assert "effects_applied" in result_dict
|
|
assert "message" in result_dict
|
|
|
|
|
|
class TestUseConsumableInCombat:
|
|
"""Tests for use_consumable_in_combat()."""
|
|
|
|
def test_combat_consumable_returns_effects(self, inventory_service, test_character, test_buff_potion):
|
|
"""Test combat consumable returns duration effects."""
|
|
test_character.inventory = [test_buff_potion]
|
|
|
|
result, effects = inventory_service.use_consumable_in_combat(
|
|
test_character, "strength_potion", "user_test_456",
|
|
current_hp=50, max_hp=100
|
|
)
|
|
|
|
assert isinstance(result, ConsumableResult)
|
|
assert len(effects) == 1
|
|
assert effects[0].effect_type == EffectType.BUFF
|
|
assert effects[0].duration == 3
|
|
|
|
def test_combat_instant_heal_potion(self, inventory_service, test_character, test_consumable):
|
|
"""Test instant heal in combat."""
|
|
test_character.inventory = [test_consumable]
|
|
|
|
result, effects = inventory_service.use_consumable_in_combat(
|
|
test_character, "health_potion_small", "user_test_456",
|
|
current_hp=50, max_hp=100
|
|
)
|
|
|
|
# HOT with duration 1 should be returned as duration effect for combat tracking
|
|
assert len(effects) >= 0 # Implementation may vary
|
|
|
|
|
|
# =============================================================================
|
|
# Bulk Operation Tests
|
|
# =============================================================================
|
|
|
|
class TestBulkOperations:
|
|
"""Tests for bulk inventory operations."""
|
|
|
|
def test_add_items_bulk(self, inventory_service, test_character, test_weapon, test_armor, mock_character_service):
|
|
"""Test adding multiple items at once."""
|
|
items = [test_weapon, test_armor]
|
|
|
|
count = inventory_service.add_items(test_character, items, "user_test_456")
|
|
|
|
assert count == 2
|
|
assert len(test_character.inventory) == 2
|
|
mock_character_service.update_character.assert_called_once()
|
|
|
|
def test_get_items_by_type(self, inventory_service, test_character, test_weapon, test_armor, test_consumable):
|
|
"""Test filtering items by type."""
|
|
test_character.inventory = [test_weapon, test_armor, test_consumable]
|
|
|
|
weapons = inventory_service.get_items_by_type(test_character, ItemType.WEAPON)
|
|
armor = inventory_service.get_items_by_type(test_character, ItemType.ARMOR)
|
|
consumables = inventory_service.get_items_by_type(test_character, ItemType.CONSUMABLE)
|
|
|
|
assert len(weapons) == 1
|
|
assert test_weapon in weapons
|
|
assert len(armor) == 1
|
|
assert test_armor in armor
|
|
assert len(consumables) == 1
|
|
|
|
def test_get_equippable_items(self, inventory_service, test_character, test_weapon, test_armor, test_consumable):
|
|
"""Test getting only equippable items."""
|
|
test_character.inventory = [test_weapon, test_armor, test_consumable]
|
|
|
|
equippable = inventory_service.get_equippable_items(test_character)
|
|
|
|
assert test_weapon in equippable
|
|
assert test_armor in equippable
|
|
assert test_consumable not in equippable
|
|
|
|
def test_get_equippable_items_for_slot(self, inventory_service, test_character, test_weapon, test_armor):
|
|
"""Test getting equippable items for a specific slot."""
|
|
test_character.inventory = [test_weapon, test_armor]
|
|
|
|
for_weapon = inventory_service.get_equippable_items(test_character, slot="weapon")
|
|
for_chest = inventory_service.get_equippable_items(test_character, slot="chest")
|
|
|
|
assert test_weapon in for_weapon
|
|
assert test_armor not in for_weapon
|
|
assert test_armor in for_chest
|
|
assert test_weapon not in for_chest
|
|
|
|
def test_get_equippable_items_excludes_high_level(self, inventory_service, test_character, test_weapon, high_level_weapon):
|
|
"""Test that items above character level are excluded."""
|
|
test_character.inventory = [test_weapon, high_level_weapon]
|
|
test_character.level = 5
|
|
|
|
equippable = inventory_service.get_equippable_items(test_character)
|
|
|
|
assert test_weapon in equippable
|
|
assert high_level_weapon not in equippable
|
|
|
|
|
|
# =============================================================================
|
|
# Edge Cases and Error Handling
|
|
# =============================================================================
|
|
|
|
class TestEdgeCases:
|
|
"""Tests for edge cases and error handling."""
|
|
|
|
def test_valid_slots_constant(self):
|
|
"""Test VALID_SLOTS contains expected slots."""
|
|
expected = {"weapon", "off_hand", "helmet", "chest", "gloves", "boots", "accessory_1", "accessory_2"}
|
|
assert VALID_SLOTS == expected
|
|
|
|
def test_item_type_slots_mapping(self):
|
|
"""Test ITEM_TYPE_SLOTS mapping is correct."""
|
|
assert ItemType.WEAPON in ITEM_TYPE_SLOTS
|
|
assert "weapon" in ITEM_TYPE_SLOTS[ItemType.WEAPON]
|
|
assert "off_hand" in ITEM_TYPE_SLOTS[ItemType.WEAPON]
|
|
assert ItemType.ARMOR in ITEM_TYPE_SLOTS
|
|
assert "chest" in ITEM_TYPE_SLOTS[ItemType.ARMOR]
|
|
assert "helmet" in ITEM_TYPE_SLOTS[ItemType.ARMOR]
|
|
|
|
def test_generated_item_with_unique_id(self, inventory_service, test_character, mock_character_service):
|
|
"""Test handling of generated items with unique IDs."""
|
|
generated_item = Item(
|
|
item_id="gen_abc123", # Generated item ID format
|
|
name="Dagger",
|
|
item_type=ItemType.WEAPON,
|
|
rarity=ItemRarity.RARE,
|
|
damage=15,
|
|
is_generated=True,
|
|
generated_name="Flaming Dagger of Strength",
|
|
base_template_id="dagger",
|
|
applied_affixes=["flaming", "of_strength"],
|
|
)
|
|
|
|
inventory_service.add_item(test_character, generated_item, "user_test_456")
|
|
|
|
assert generated_item in test_character.inventory
|
|
assert generated_item.get_display_name() == "Flaming Dagger of Strength"
|
|
|
|
def test_equip_generated_item(self, inventory_service, test_character, mock_character_service):
|
|
"""Test equipping a generated item."""
|
|
generated_item = Item(
|
|
item_id="gen_xyz789",
|
|
name="Sword",
|
|
item_type=ItemType.WEAPON,
|
|
rarity=ItemRarity.EPIC,
|
|
damage=25,
|
|
is_generated=True,
|
|
generated_name="Blazing Sword of Power",
|
|
required_level=1,
|
|
)
|
|
test_character.inventory = [generated_item]
|
|
|
|
inventory_service.equip_item(test_character, "gen_xyz789", "weapon", "user_test_456")
|
|
|
|
assert test_character.equipped.get("weapon") is generated_item
|
|
|
|
|
|
# =============================================================================
|
|
# Global Instance Tests
|
|
# =============================================================================
|
|
|
|
class TestGlobalInstance:
|
|
"""Tests for the global singleton pattern."""
|
|
|
|
def test_get_inventory_service_returns_instance(self):
|
|
"""Test get_inventory_service returns InventoryService."""
|
|
with patch('app.services.inventory_service._service_instance', None):
|
|
with patch('app.services.inventory_service.get_character_service'):
|
|
service = get_inventory_service()
|
|
assert isinstance(service, InventoryService)
|
|
|
|
def test_get_inventory_service_returns_same_instance(self):
|
|
"""Test get_inventory_service returns singleton."""
|
|
with patch('app.services.inventory_service._service_instance', None):
|
|
with patch('app.services.inventory_service.get_character_service'):
|
|
service1 = get_inventory_service()
|
|
service2 = get_inventory_service()
|
|
assert service1 is service2
|