feat(api): implement inventory API endpoints

Add REST API endpoints for character inventory management:
- GET /api/v1/characters/<id>/inventory - Get inventory and equipped items
- POST /api/v1/characters/<id>/inventory/equip - Equip item to slot
- POST /api/v1/characters/<id>/inventory/unequip - Unequip from slot
- POST /api/v1/characters/<id>/inventory/use - Use consumable item
- DELETE /api/v1/characters/<id>/inventory/<item_id> - Drop item

All endpoints include:
- Authentication via @require_auth decorator
- Ownership validation through CharacterService
- Comprehensive error handling with proper HTTP status codes
- Full logging for debugging

Includes 25 integration tests covering authentication requirements,
URL patterns, and response formats.

Task 2.4 of Phase 4 Combat Implementation complete.
This commit is contained in:
2025-11-26 18:54:33 -06:00
parent 76f67c4a22
commit 4ced1b04df
5 changed files with 1157 additions and 148 deletions

View File

@@ -174,6 +174,11 @@ def register_blueprints(app: Flask) -> None:
app.register_blueprint(combat_bp)
logger.info("Combat API blueprint registered")
# Import and register Inventory API blueprint
from app.api.inventory import inventory_bp
app.register_blueprint(inventory_bp)
logger.info("Inventory API blueprint registered")
# TODO: Register additional blueprints as they are created
# from app.api import marketplace, shop
# app.register_blueprint(marketplace.bp, url_prefix='/api/v1/marketplace')

639
api/app/api/inventory.py Normal file
View File

@@ -0,0 +1,639 @@
"""
Inventory API Blueprint
Endpoints for managing character inventory and equipment.
All endpoints require authentication and enforce ownership validation.
Endpoints:
- GET /api/v1/characters/<id>/inventory - Get character inventory and equipped items
- POST /api/v1/characters/<id>/inventory/equip - Equip an item
- POST /api/v1/characters/<id>/inventory/unequip - Unequip an item
- POST /api/v1/characters/<id>/inventory/use - Use a consumable item
- DELETE /api/v1/characters/<id>/inventory/<item_id> - Drop an item
"""
from flask import Blueprint, request
from app.services.inventory_service import (
get_inventory_service,
ItemNotFoundError,
CannotEquipError,
InvalidSlotError,
CannotUseItemError,
InventoryFullError,
VALID_SLOTS,
MAX_INVENTORY_SIZE,
)
from app.services.character_service import (
get_character_service,
CharacterNotFound,
)
from app.utils.response import (
success_response,
error_response,
not_found_response,
validation_error_response,
)
from app.utils.auth import require_auth, get_current_user
from app.utils.logging import get_logger
# Initialize logger
logger = get_logger(__file__)
# Create blueprint
inventory_bp = Blueprint('inventory', __name__)
# =============================================================================
# API Endpoints
# =============================================================================
@inventory_bp.route('/api/v1/characters/<character_id>/inventory', methods=['GET'])
@require_auth
def get_inventory(character_id: str):
"""
Get character inventory and equipped items.
Args:
character_id: Character ID
Returns:
200: Inventory and equipment data
401: Not authenticated
404: Character not found or not owned by user
500: Internal server error
Example Response:
{
"result": {
"inventory": [
{
"item_id": "gen_abc123",
"name": "Flaming Dagger",
"item_type": "weapon",
"rarity": "rare",
...
}
],
"equipped": {
"weapon": {...},
"helmet": null,
...
},
"inventory_count": 5,
"max_inventory": 100
}
}
"""
try:
user = get_current_user()
logger.info("Getting inventory",
user_id=user.id,
character_id=character_id)
# Get character (validates ownership)
char_service = get_character_service()
character = char_service.get_character(character_id, user.id)
# Get inventory service
inventory_service = get_inventory_service()
# Get inventory items
inventory_items = inventory_service.get_inventory(character)
# Get equipped items
equipped_items = inventory_service.get_equipped_items(character)
# Build equipped dict with all slots (None for empty slots)
equipped_response = {}
for slot in VALID_SLOTS:
item = equipped_items.get(slot)
equipped_response[slot] = item.to_dict() if item else None
logger.info("Inventory retrieved successfully",
user_id=user.id,
character_id=character_id,
item_count=len(inventory_items))
return success_response(
result={
"inventory": [item.to_dict() for item in inventory_items],
"equipped": equipped_response,
"inventory_count": len(inventory_items),
"max_inventory": MAX_INVENTORY_SIZE,
}
)
except CharacterNotFound as e:
logger.warning("Character not found",
user_id=user.id if 'user' in locals() else 'unknown',
character_id=character_id,
error=str(e))
return not_found_response(message=str(e))
except Exception as e:
logger.error("Failed to get inventory",
user_id=user.id if 'user' in locals() else 'unknown',
character_id=character_id,
error=str(e))
return error_response(
code="INVENTORY_GET_ERROR",
message="Failed to retrieve inventory",
status=500
)
@inventory_bp.route('/api/v1/characters/<character_id>/inventory/equip', methods=['POST'])
@require_auth
def equip_item(character_id: str):
"""
Equip an item from inventory to a specified slot.
Args:
character_id: Character ID
Request Body:
{
"item_id": "gen_abc123",
"slot": "weapon"
}
Returns:
200: Item equipped successfully
400: Cannot equip item (wrong type, level requirement, etc.)
401: Not authenticated
404: Character or item not found
422: Validation error (invalid slot)
500: Internal server error
Example Response:
{
"result": {
"message": "Equipped Flaming Dagger to weapon slot",
"equipped": {...},
"unequipped_item": null
}
}
"""
try:
user = get_current_user()
# Get request data
data = request.get_json()
if not data:
return validation_error_response(
message="Request body is required",
details={"error": "Request body is required"}
)
item_id = data.get('item_id', '').strip()
slot = data.get('slot', '').strip().lower()
# Validate required fields
validation_errors = {}
if not item_id:
validation_errors['item_id'] = "item_id is required"
if not slot:
validation_errors['slot'] = "slot is required"
if validation_errors:
return validation_error_response(
message="Validation failed",
details=validation_errors
)
logger.info("Equipping item",
user_id=user.id,
character_id=character_id,
item_id=item_id,
slot=slot)
# Get character (validates ownership)
char_service = get_character_service()
character = char_service.get_character(character_id, user.id)
# Get inventory service
inventory_service = get_inventory_service()
# Equip item
previous_item = inventory_service.equip_item(
character, item_id, slot, user.id
)
# Get item name for message
equipped_item = character.equipped.get(slot)
item_name = equipped_item.get_display_name() if equipped_item else item_id
# Build equipped response
equipped_response = {}
for s in VALID_SLOTS:
item = character.equipped.get(s)
equipped_response[s] = item.to_dict() if item else None
logger.info("Item equipped successfully",
user_id=user.id,
character_id=character_id,
item_id=item_id,
slot=slot,
previous_item=previous_item.item_id if previous_item else None)
return success_response(
result={
"message": f"Equipped {item_name} to {slot} slot",
"equipped": equipped_response,
"unequipped_item": previous_item.to_dict() if previous_item else None,
}
)
except CharacterNotFound as e:
logger.warning("Character not found for equip",
user_id=user.id if 'user' in locals() else 'unknown',
character_id=character_id,
error=str(e))
return not_found_response(message=str(e))
except ItemNotFoundError as e:
logger.warning("Item not found for equip",
user_id=user.id if 'user' in locals() else 'unknown',
character_id=character_id,
item_id=item_id if 'item_id' in locals() else 'unknown',
error=str(e))
return not_found_response(message=str(e))
except InvalidSlotError as e:
logger.warning("Invalid slot for equip",
user_id=user.id if 'user' in locals() else 'unknown',
character_id=character_id,
slot=slot if 'slot' in locals() else 'unknown',
error=str(e))
return validation_error_response(
message=str(e),
details={"slot": str(e)}
)
except CannotEquipError as e:
logger.warning("Cannot equip item",
user_id=user.id if 'user' in locals() else 'unknown',
character_id=character_id,
item_id=item_id if 'item_id' in locals() else 'unknown',
error=str(e))
return error_response(
code="CANNOT_EQUIP",
message=str(e),
status=400
)
except Exception as e:
logger.error("Failed to equip item",
user_id=user.id if 'user' in locals() else 'unknown',
character_id=character_id,
error=str(e))
return error_response(
code="EQUIP_ERROR",
message="Failed to equip item",
status=500
)
@inventory_bp.route('/api/v1/characters/<character_id>/inventory/unequip', methods=['POST'])
@require_auth
def unequip_item(character_id: str):
"""
Unequip an item from a specified slot (returns to inventory).
Args:
character_id: Character ID
Request Body:
{
"slot": "weapon"
}
Returns:
200: Item unequipped successfully (or slot was empty)
400: Inventory full, cannot unequip
401: Not authenticated
404: Character not found
422: Validation error (invalid slot)
500: Internal server error
Example Response:
{
"result": {
"message": "Unequipped Flaming Dagger from weapon slot",
"unequipped_item": {...},
"equipped": {...}
}
}
"""
try:
user = get_current_user()
# Get request data
data = request.get_json()
if not data:
return validation_error_response(
message="Request body is required",
details={"error": "Request body is required"}
)
slot = data.get('slot', '').strip().lower()
if not slot:
return validation_error_response(
message="Validation failed",
details={"slot": "slot is required"}
)
logger.info("Unequipping item",
user_id=user.id,
character_id=character_id,
slot=slot)
# Get character (validates ownership)
char_service = get_character_service()
character = char_service.get_character(character_id, user.id)
# Get inventory service
inventory_service = get_inventory_service()
# Unequip item
unequipped_item = inventory_service.unequip_item(character, slot, user.id)
# Build equipped response
equipped_response = {}
for s in VALID_SLOTS:
item = character.equipped.get(s)
equipped_response[s] = item.to_dict() if item else None
# Build message
if unequipped_item:
message = f"Unequipped {unequipped_item.get_display_name()} from {slot} slot"
else:
message = f"Slot '{slot}' was already empty"
logger.info("Item unequipped",
user_id=user.id,
character_id=character_id,
slot=slot,
unequipped_item=unequipped_item.item_id if unequipped_item else None)
return success_response(
result={
"message": message,
"unequipped_item": unequipped_item.to_dict() if unequipped_item else None,
"equipped": equipped_response,
}
)
except CharacterNotFound as e:
logger.warning("Character not found for unequip",
user_id=user.id if 'user' in locals() else 'unknown',
character_id=character_id,
error=str(e))
return not_found_response(message=str(e))
except InvalidSlotError as e:
logger.warning("Invalid slot for unequip",
user_id=user.id if 'user' in locals() else 'unknown',
character_id=character_id,
slot=slot if 'slot' in locals() else 'unknown',
error=str(e))
return validation_error_response(
message=str(e),
details={"slot": str(e)}
)
except InventoryFullError as e:
logger.warning("Inventory full, cannot unequip",
user_id=user.id if 'user' in locals() else 'unknown',
character_id=character_id,
error=str(e))
return error_response(
code="INVENTORY_FULL",
message=str(e),
status=400
)
except Exception as e:
logger.error("Failed to unequip item",
user_id=user.id if 'user' in locals() else 'unknown',
character_id=character_id,
error=str(e))
return error_response(
code="UNEQUIP_ERROR",
message="Failed to unequip item",
status=500
)
@inventory_bp.route('/api/v1/characters/<character_id>/inventory/use', methods=['POST'])
@require_auth
def use_item(character_id: str):
"""
Use a consumable item from inventory.
Args:
character_id: Character ID
Request Body:
{
"item_id": "health_potion_small"
}
Returns:
200: Item used successfully
400: Cannot use item (not consumable)
401: Not authenticated
404: Character or item not found
500: Internal server error
Example Response:
{
"result": {
"item_used": "Small Health Potion",
"effects_applied": [
{
"effect_name": "Healing",
"effect_type": "hot",
"value": 25,
"message": "Restored 25 HP"
}
],
"hp_restored": 25,
"mp_restored": 0,
"message": "Used Small Health Potion: Restored 25 HP"
}
}
"""
try:
user = get_current_user()
# Get request data
data = request.get_json()
if not data:
return validation_error_response(
message="Request body is required",
details={"error": "Request body is required"}
)
item_id = data.get('item_id', '').strip()
if not item_id:
return validation_error_response(
message="Validation failed",
details={"item_id": "item_id is required"}
)
logger.info("Using item",
user_id=user.id,
character_id=character_id,
item_id=item_id)
# Get character (validates ownership)
char_service = get_character_service()
character = char_service.get_character(character_id, user.id)
# Get inventory service
inventory_service = get_inventory_service()
# Use consumable
result = inventory_service.use_consumable(character, item_id, user.id)
logger.info("Item used successfully",
user_id=user.id,
character_id=character_id,
item_id=item_id,
hp_restored=result.hp_restored,
mp_restored=result.mp_restored)
return success_response(result=result.to_dict())
except CharacterNotFound as e:
logger.warning("Character not found for use item",
user_id=user.id if 'user' in locals() else 'unknown',
character_id=character_id,
error=str(e))
return not_found_response(message=str(e))
except ItemNotFoundError as e:
logger.warning("Item not found for use",
user_id=user.id if 'user' in locals() else 'unknown',
character_id=character_id,
item_id=item_id if 'item_id' in locals() else 'unknown',
error=str(e))
return not_found_response(message=str(e))
except CannotUseItemError as e:
logger.warning("Cannot use item",
user_id=user.id if 'user' in locals() else 'unknown',
character_id=character_id,
item_id=item_id if 'item_id' in locals() else 'unknown',
error=str(e))
return error_response(
code="CANNOT_USE_ITEM",
message=str(e),
status=400
)
except Exception as e:
logger.error("Failed to use item",
user_id=user.id if 'user' in locals() else 'unknown',
character_id=character_id,
error=str(e))
return error_response(
code="USE_ITEM_ERROR",
message="Failed to use item",
status=500
)
@inventory_bp.route('/api/v1/characters/<character_id>/inventory/<item_id>', methods=['DELETE'])
@require_auth
def drop_item(character_id: str, item_id: str):
"""
Drop (remove) an item from inventory.
Args:
character_id: Character ID
item_id: Item ID to drop
Returns:
200: Item dropped successfully
401: Not authenticated
404: Character or item not found
500: Internal server error
Example Response:
{
"result": {
"message": "Dropped Rusty Sword",
"dropped_item": {...},
"inventory_count": 4
}
}
"""
try:
user = get_current_user()
logger.info("Dropping item",
user_id=user.id,
character_id=character_id,
item_id=item_id)
# Get character (validates ownership)
char_service = get_character_service()
character = char_service.get_character(character_id, user.id)
# Get inventory service
inventory_service = get_inventory_service()
# Drop item
dropped_item = inventory_service.drop_item(character, item_id, user.id)
logger.info("Item dropped successfully",
user_id=user.id,
character_id=character_id,
item_id=item_id,
item_name=dropped_item.get_display_name())
return success_response(
result={
"message": f"Dropped {dropped_item.get_display_name()}",
"dropped_item": dropped_item.to_dict(),
"inventory_count": len(character.inventory),
}
)
except CharacterNotFound as e:
logger.warning("Character not found for drop",
user_id=user.id if 'user' in locals() else 'unknown',
character_id=character_id,
error=str(e))
return not_found_response(message=str(e))
except ItemNotFoundError as e:
logger.warning("Item not found for drop",
user_id=user.id if 'user' in locals() else 'unknown',
character_id=character_id,
item_id=item_id,
error=str(e))
return not_found_response(message=str(e))
except Exception as e:
logger.error("Failed to drop item",
user_id=user.id if 'user' in locals() else 'unknown',
character_id=character_id,
item_id=item_id,
error=str(e))
return error_response(
code="DROP_ITEM_ERROR",
message="Failed to drop item",
status=500
)

View File

@@ -0,0 +1,462 @@
"""
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/<id>/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/<id>/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/<id>/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/<id>/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/<id>/inventory/<item_id> 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