""" Integration tests for Inventory API endpoints. Tests the REST API endpoints for inventory management functionality. """ import pytest from unittest.mock import Mock, patch, MagicMock from flask import Flask import json from app import create_app from app.api.inventory import inventory_bp from app.models.items import Item from app.models.character import Character from app.models.stats import Stats from app.models.skills import PlayerClass from app.models.origins import Origin from app.models.enums import ItemType, ItemRarity, DamageType from app.services.inventory_service import ( InventoryService, ItemNotFoundError, CannotEquipError, InvalidSlotError, CannotUseItemError, InventoryFullError, VALID_SLOTS, ) from app.services.character_service import CharacterNotFound # ============================================================================= # Test Fixtures # ============================================================================= @pytest.fixture def app(): """Create test Flask application.""" app = create_app('development') app.config['TESTING'] = True return app @pytest.fixture def client(app): """Create test client.""" return app.test_client() @pytest.fixture def sample_stats(): """Sample stats for testing.""" return Stats( strength=12, dexterity=14, constitution=10, intelligence=10, wisdom=10, charisma=10, luck=10 ) @pytest.fixture def sample_weapon(): """Sample weapon item.""" return Item( item_id="test_sword_001", name="Iron Sword", item_type=ItemType.WEAPON, rarity=ItemRarity.COMMON, description="A sturdy iron sword", value=50, damage=8, damage_type=DamageType.PHYSICAL, crit_chance=0.05, crit_multiplier=2.0, required_level=1, ) @pytest.fixture def sample_armor(): """Sample armor item.""" return Item( item_id="test_helmet_001", name="Iron Helmet", item_type=ItemType.ARMOR, rarity=ItemRarity.COMMON, description="A sturdy iron helmet", value=30, defense=5, resistance=2, required_level=1, ) @pytest.fixture def sample_consumable(): """Sample consumable item.""" return Item( item_id="health_potion_small", name="Small Health Potion", item_type=ItemType.CONSUMABLE, rarity=ItemRarity.COMMON, description="Restores a small amount of health", value=25, effects_on_use=[], # Simplified for testing ) @pytest.fixture def sample_class(): """Sample player class.""" return PlayerClass( class_id="vanguard", name="Vanguard", description="A heavily armored warrior", base_stats=Stats( strength=14, dexterity=10, constitution=14, intelligence=8, wisdom=8, charisma=10, luck=10 ), skill_trees=[], starting_equipment=[], starting_abilities=[], ) @pytest.fixture def sample_origin(): """Sample origin.""" return Origin( id="soul_revenant", name="Soul Revenant", description="Returned from death", starting_location={"area": "graveyard", "name": "Graveyard"}, narrative_hooks=[], starting_bonus={}, ) @pytest.fixture def sample_character(sample_class, sample_origin, sample_weapon, sample_armor, sample_consumable): """Sample character with inventory.""" char = Character( character_id="test_char_001", user_id="test_user_001", name="Test Hero", player_class=sample_class, origin=sample_origin, level=5, experience=0, gold=100, inventory=[sample_weapon, sample_armor, sample_consumable], equipped={}, unlocked_skills=[], ) return char # ============================================================================= # GET Inventory Endpoint Tests # ============================================================================= class TestGetInventoryEndpoint: """Tests for GET /api/v1/characters//inventory endpoint.""" def test_get_inventory_requires_auth(self, client): """Test that inventory endpoint requires authentication.""" response = client.get('/api/v1/characters/test_char_001/inventory') # Should return 401 Unauthorized without valid session assert response.status_code == 401 def test_get_inventory_character_not_found(self, client): """Test getting inventory for non-existent character returns 404 (after auth).""" # Without auth, returns 401 regardless response = client.get('/api/v1/characters/nonexistent_12345/inventory') assert response.status_code == 401 # ============================================================================= # POST Equip Endpoint Tests # ============================================================================= class TestEquipEndpoint: """Tests for POST /api/v1/characters//inventory/equip endpoint.""" def test_equip_requires_auth(self, client): """Test that equip endpoint requires authentication.""" response = client.post( '/api/v1/characters/test_char_001/inventory/equip', json={ 'item_id': 'test_sword_001', 'slot': 'weapon' } ) # Should return 401 Unauthorized without valid session assert response.status_code == 401 def test_equip_missing_item_id(self, client): """Test equip without item_id still requires auth first.""" response = client.post( '/api/v1/characters/test_char_001/inventory/equip', json={'slot': 'weapon'} ) # Without auth, returns 401 regardless of payload issues assert response.status_code == 401 def test_equip_missing_slot(self, client): """Test equip without slot still requires auth first.""" response = client.post( '/api/v1/characters/test_char_001/inventory/equip', json={'item_id': 'test_sword_001'} ) # Without auth, returns 401 regardless of payload issues assert response.status_code == 401 def test_equip_missing_body(self, client): """Test equip without request body still requires auth first.""" response = client.post('/api/v1/characters/test_char_001/inventory/equip') # Without auth, returns 401 regardless of payload issues assert response.status_code == 401 # ============================================================================= # POST Unequip Endpoint Tests # ============================================================================= class TestUnequipEndpoint: """Tests for POST /api/v1/characters//inventory/unequip endpoint.""" def test_unequip_requires_auth(self, client): """Test that unequip endpoint requires authentication.""" response = client.post( '/api/v1/characters/test_char_001/inventory/unequip', json={'slot': 'weapon'} ) # Should return 401 Unauthorized without valid session assert response.status_code == 401 def test_unequip_missing_slot(self, client): """Test unequip without slot still requires auth first.""" response = client.post( '/api/v1/characters/test_char_001/inventory/unequip', json={} ) # Without auth, returns 401 regardless of payload issues assert response.status_code == 401 def test_unequip_missing_body(self, client): """Test unequip without request body still requires auth first.""" response = client.post('/api/v1/characters/test_char_001/inventory/unequip') # Without auth, returns 401 regardless of payload issues assert response.status_code == 401 # ============================================================================= # POST Use Item Endpoint Tests # ============================================================================= class TestUseItemEndpoint: """Tests for POST /api/v1/characters//inventory/use endpoint.""" def test_use_requires_auth(self, client): """Test that use item endpoint requires authentication.""" response = client.post( '/api/v1/characters/test_char_001/inventory/use', json={'item_id': 'health_potion_small'} ) # Should return 401 Unauthorized without valid session assert response.status_code == 401 def test_use_missing_item_id(self, client): """Test use item without item_id still requires auth first.""" response = client.post( '/api/v1/characters/test_char_001/inventory/use', json={} ) # Without auth, returns 401 regardless of payload issues assert response.status_code == 401 def test_use_missing_body(self, client): """Test use item without request body still requires auth first.""" response = client.post('/api/v1/characters/test_char_001/inventory/use') # Without auth, returns 401 regardless of payload issues assert response.status_code == 401 # ============================================================================= # DELETE Drop Item Endpoint Tests # ============================================================================= class TestDropItemEndpoint: """Tests for DELETE /api/v1/characters//inventory/ endpoint.""" def test_drop_requires_auth(self, client): """Test that drop item endpoint requires authentication.""" response = client.delete( '/api/v1/characters/test_char_001/inventory/test_sword_001' ) # Should return 401 Unauthorized without valid session assert response.status_code == 401 # ============================================================================= # Valid Slot Tests (Unit level) # ============================================================================= class TestValidSlots: """Tests to verify slot configuration.""" def test_valid_slots_defined(self): """Test that all expected slots are defined.""" expected_slots = { 'weapon', 'off_hand', 'helmet', 'chest', 'gloves', 'boots', 'accessory_1', 'accessory_2' } assert VALID_SLOTS == expected_slots def test_valid_slots_count(self): """Test that we have exactly 8 equipment slots.""" assert len(VALID_SLOTS) == 8 # ============================================================================= # Endpoint URL Pattern Tests # ============================================================================= class TestEndpointURLPatterns: """Tests to verify correct URL patterns.""" def test_get_inventory_url(self, client): """Test GET inventory URL pattern.""" response = client.get('/api/v1/characters/any_id/inventory') # Should be 401 (auth required), not 404 (route not found) assert response.status_code == 401 def test_equip_url(self, client): """Test POST equip URL pattern.""" response = client.post( '/api/v1/characters/any_id/inventory/equip', json={'item_id': 'x', 'slot': 'weapon'} ) # Should be 401 (auth required), not 404 (route not found) assert response.status_code == 401 def test_unequip_url(self, client): """Test POST unequip URL pattern.""" response = client.post( '/api/v1/characters/any_id/inventory/unequip', json={'slot': 'weapon'} ) # Should be 401 (auth required), not 404 (route not found) assert response.status_code == 401 def test_use_url(self, client): """Test POST use URL pattern.""" response = client.post( '/api/v1/characters/any_id/inventory/use', json={'item_id': 'x'} ) # Should be 401 (auth required), not 404 (route not found) assert response.status_code == 401 def test_drop_url(self, client): """Test DELETE drop URL pattern.""" response = client.delete('/api/v1/characters/any_id/inventory/item_123') # Should be 401 (auth required), not 404 (route not found) assert response.status_code == 401 # ============================================================================= # Response Format Tests (verifying blueprint registration) # ============================================================================= class TestResponseFormats: """Tests to verify API response format consistency.""" def test_get_inventory_401_format(self, client): """Test that 401 response follows standard format.""" response = client.get('/api/v1/characters/test_char_001/inventory') assert response.status_code == 401 data = response.get_json() # Standard response format should include status assert data is not None assert 'status' in data assert data['status'] == 401 def test_equip_401_format(self, client): """Test that equip 401 response follows standard format.""" response = client.post( '/api/v1/characters/test_char_001/inventory/equip', json={'item_id': 'test', 'slot': 'weapon'} ) assert response.status_code == 401 data = response.get_json() assert data is not None assert 'status' in data assert data['status'] == 401 def test_unequip_401_format(self, client): """Test that unequip 401 response follows standard format.""" response = client.post( '/api/v1/characters/test_char_001/inventory/unequip', json={'slot': 'weapon'} ) assert response.status_code == 401 data = response.get_json() assert data is not None assert 'status' in data assert data['status'] == 401 def test_use_401_format(self, client): """Test that use 401 response follows standard format.""" response = client.post( '/api/v1/characters/test_char_001/inventory/use', json={'item_id': 'test'} ) assert response.status_code == 401 data = response.get_json() assert data is not None assert 'status' in data assert data['status'] == 401 def test_drop_401_format(self, client): """Test that drop 401 response follows standard format.""" response = client.delete( '/api/v1/characters/test_char_001/inventory/test_item' ) assert response.status_code == 401 data = response.get_json() assert data is not None assert 'status' in data assert data['status'] == 401