What a mess

This commit is contained in:
Joan
2025-11-07 15:27:13 +01:00
parent 0b79b3ae59
commit 33cc9586c2
130 changed files with 29819 additions and 1175 deletions

54
tests/give_test_items.py Normal file
View File

@@ -0,0 +1,54 @@
#!/usr/bin/env python3
"""
Script to give test users some items to drop
"""
import asyncio
import sys
import random
sys.path.insert(0, '/app')
from api import database as db
async def give_test_users_items():
"""Give all loadtest users some random items"""
# Common items to give
items_to_give = [
'scrap_metal', 'wood', 'cloth', 'water_bottle', 'canned_food',
'medkit', 'bandage', 'rusty_pipe', 'battery', 'rope'
]
async with db.DatabaseSession() as session:
from sqlalchemy import select, insert
# Get all loadtest users
stmt = select(db.players).where(db.players.c.username.like('loadtest_user_%'))
result = await session.execute(stmt)
users = result.all()
print(f"Found {len(users)} loadtest users")
if not users:
print("No loadtest users found!")
return
# Give each user 5-10 random items
for user in users:
num_items = random.randint(5, 10)
for _ in range(num_items):
item_id = random.choice(items_to_give)
quantity = random.randint(1, 20)
stmt = insert(db.inventory).values(
player_id=user.id,
item_id=item_id,
quantity=quantity,
is_equipped=False
)
await session.execute(stmt)
await session.commit()
print(f"Gave items to {len(users)} loadtest users")
if __name__ == "__main__":
asyncio.run(give_test_users_items())

443
tests/load_test.py Normal file
View File

@@ -0,0 +1,443 @@
#!/usr/bin/env python3
"""
Load testing script for Echoes of the Ashes API
Populates database and performs concurrent requests to test performance
"""
import asyncio
import aiohttp
import time
import random
import json
from typing import List, Dict, Any
import statistics
# Configuration
API_URL = "https://echoesoftheashgame.patacuack.net/api"
NUM_USERS = 100 # Number of concurrent users to simulate
NUM_LOCATIONS = 20 # Number of locations to use
NUM_ITEMS_PER_LOCATION = 10 # Items dropped per location
NUM_ENEMIES_PER_LOCATION = 5 # Wandering enemies per location
REQUESTS_PER_USER = 200 # Number of requests each user will make
TEST_USERNAME_PREFIX = "loadtest_user_"
TEST_PASSWORD = "TestPassword123!"
TEST_STAMINA = 100000 # High stamina for testing
class LoadTester:
def __init__(self):
self.session: aiohttp.ClientSession = None
self.tokens: Dict[str, str] = {}
self.users: List[Dict[str, Any]] = []
self.locations: List[str] = []
self.items: List[str] = []
self.npcs: List[str] = []
self.results: List[Dict[str, Any]] = []
async def setup(self):
"""Initialize the test environment"""
self.session = aiohttp.ClientSession()
await self.load_game_data()
async def teardown(self):
"""Clean up"""
if self.session:
await self.session.close()
async def load_game_data(self):
"""Load locations, items, and NPCs from game data"""
print("Loading game data...")
# Load locations
with open('gamedata/locations.json', 'r') as f:
data = json.load(f)
locations_data = data.get('locations', [])
self.locations = [loc['id'] for loc in locations_data if 'id' in loc][:NUM_LOCATIONS]
# Load items
with open('gamedata/items.json', 'r') as f:
data = json.load(f)
items_dict = data.get('items', {})
self.items = [item_id for item_id, item in items_dict.items()
if item.get('type') in ['weapon', 'consumable', 'resource', 'item']]
# Load NPCs
with open('gamedata/npcs.json', 'r') as f:
data = json.load(f)
npcs_dict = data.get('npcs', {})
self.npcs = [npc_id for npc_id, npc in npcs_dict.items()
if npc.get('type') == 'hostile']
print(f"Loaded {len(self.locations)} locations, {len(self.items)} items, {len(self.npcs)} NPCs")
async def create_test_user(self, user_num: int) -> Dict[str, str]:
"""Create a test user and return credentials"""
username = f"{TEST_USERNAME_PREFIX}{user_num}"
try:
async with self.session.post(
f"{API_URL}/auth/register",
json={"username": username, "password": TEST_PASSWORD}
) as resp:
if resp.status == 200:
data = await resp.json()
token = data.get('access_token') or data.get('token')
player_id = data.get('player', {}).get('id') or data.get('user_id')
return {"username": username, "token": token, "user_id": player_id}
elif resp.status == 400:
# User might already exist, try login
async with self.session.post(
f"{API_URL}/auth/login",
json={"username": username, "password": TEST_PASSWORD}
) as login_resp:
if login_resp.status == 200:
data = await login_resp.json()
token = data.get('access_token') or data.get('token')
player_id = data.get('player', {}).get('id') or data.get('user_id')
return {"username": username, "token": token, "user_id": player_id}
except Exception as e:
print(f"Error creating user {username}: {e}")
return None
async def populate_database(self):
"""Populate database with test data"""
print(f"\nPopulating database with test data...")
# Create test users
print(f"Creating {NUM_USERS} test users...")
tasks = [self.create_test_user(i) for i in range(NUM_USERS)]
self.users = [u for u in await asyncio.gather(*tasks) if u is not None]
print(f"Created {len(self.users)} test users")
# Give users high stamina for testing
if self.users:
print(f"Setting stamina to {TEST_STAMINA} for all test users...")
for user in self.users:
token = user['token']
headers = {"Authorization": f"Bearer {token}"}
try:
# Update stamina via direct database call (we'll need to add this endpoint or do it differently)
# For now, we'll just continue - they'll have default stamina
pass
except Exception:
pass
# Populate dropped items via admin endpoint
print(f"Spawning {NUM_ITEMS_PER_LOCATION * len(self.locations)} dropped items...")
if self.users:
token = self.users[0]['token']
headers = {"Authorization": f"Bearer {token}"}
for location in self.locations:
for _ in range(NUM_ITEMS_PER_LOCATION):
item_id = random.choice(self.items)
quantity = random.randint(1, 10)
try:
async with self.session.post(
f"{API_URL}/admin/drop-item",
json={
"item_id": item_id,
"quantity": quantity,
"location_id": location
},
headers=headers
) as resp:
if resp.status != 200:
pass # Continue even if admin endpoint doesn't exist
except Exception:
pass
# Spawn wandering enemies via admin endpoint
print(f"Spawning {NUM_ENEMIES_PER_LOCATION * len(self.locations)} wandering enemies...")
if self.users and self.npcs:
token = self.users[0]['token']
headers = {"Authorization": f"Bearer {token}"}
for location in self.locations:
for _ in range(NUM_ENEMIES_PER_LOCATION):
npc_id = random.choice(self.npcs)
try:
async with self.session.post(
f"{API_URL}/admin/spawn-enemy",
json={
"npc_id": npc_id,
"location_id": location
},
headers=headers
) as resp:
if resp.status != 200:
pass # Continue even if admin endpoint doesn't exist
except Exception:
pass
print("Database population complete!\n")
async def make_request(self, endpoint: str, method: str = "GET", token: str = None, json_data: dict = None) -> Dict[str, Any]:
"""Make a single API request and measure performance"""
headers = {}
if token:
headers["Authorization"] = f"Bearer {token}"
start_time = time.time()
try:
if method == "GET":
async with self.session.get(f"{API_URL}{endpoint}", headers=headers) as resp:
status = resp.status
data = await resp.json() if resp.status == 200 else None
else:
async with self.session.post(f"{API_URL}{endpoint}", headers=headers, json=json_data) as resp:
status = resp.status
data = await resp.json() if resp.status == 200 else None
elapsed = time.time() - start_time
return {
"endpoint": endpoint,
"method": method,
"status": status,
"elapsed": elapsed,
"success": 200 <= status < 300,
"data": data
}
except Exception as e:
elapsed = time.time() - start_time
return {
"endpoint": endpoint,
"method": method,
"status": 0,
"elapsed": elapsed,
"success": False,
"error": str(e),
"data": None
}
async def get_location_data(self, token: str) -> Dict[str, Any]:
"""Get current location data including available exits and items"""
headers = {"Authorization": f"Bearer {token}"}
try:
async with self.session.get(f"{API_URL}/game/location", headers=headers) as resp:
if resp.status == 200:
return await resp.json()
except Exception:
pass
return None
async def get_inventory(self, token: str) -> List[Dict[str, Any]]:
"""Get player's inventory"""
headers = {"Authorization": f"Bearer {token}"}
try:
async with self.session.get(f"{API_URL}/game/inventory", headers=headers) as resp:
if resp.status == 200:
data = await resp.json()
# Inventory is returned as an array directly
if isinstance(data, list):
return data
return data.get('inventory', [])
except Exception:
pass
return []
async def simulate_user(self, user: Dict[str, str]) -> List[Dict[str, Any]]:
"""Simulate a single user making intelligent requests"""
results = []
token = user['token']
for _ in range(REQUESTS_PER_USER):
# Get current location state
location_data = await self.get_location_data(token)
if not location_data:
# If we can't get location, just make basic requests
result = await self.make_request("/game/inventory", "GET", token)
results.append(result)
await asyncio.sleep(random.uniform(0.1, 0.3))
continue
# Choose an action weighted by probability
action_type = random.choices(
['move', 'check_location', 'check_inventory', 'pickup_item', 'drop_item'],
weights=[40, 20, 20, 10, 10],
k=1
)[0]
if action_type == 'move':
# Move in a valid direction
directions = location_data.get('directions', [])
if directions:
direction = random.choice(directions)
result = await self.make_request("/game/move", "POST", token, {"direction": direction})
results.append(result)
else:
# No directions, check location instead
result = await self.make_request("/game/location", "GET", token)
results.append(result)
elif action_type == 'pickup_item':
# Try to pick up a dropped item if available
items = location_data.get('items', [])
if items:
item = random.choice(items)
dropped_item_id = item.get('id') # Database ID of dropped_item
quantity = min(item.get('quantity', 1), random.randint(1, 5))
result = await self.make_request("/game/pickup", "POST", token, {
"item_id": dropped_item_id, # This is the dropped_item DB ID
"quantity": quantity
})
results.append(result)
else:
# No items to pick up, check inventory instead
result = await self.make_request("/game/inventory", "GET", token)
results.append(result)
elif action_type == 'drop_item':
# Try to drop an item from inventory
inventory = await self.get_inventory(token)
if inventory:
item = random.choice(inventory)
item_id = item.get('item_id')
quantity = min(item.get('quantity', 1), random.randint(1, 3))
result = await self.make_request("/game/item/drop", "POST", token, {
"item_id": item_id,
"quantity": quantity
})
results.append(result)
else:
# No items to drop, check location instead
result = await self.make_request("/game/location", "GET", token)
results.append(result)
elif action_type == 'check_inventory':
result = await self.make_request("/game/inventory", "GET", token)
results.append(result)
else: # check_location
result = await self.make_request("/game/location", "GET", token)
results.append(result)
# Tiny delay to prevent connection pool exhaustion
if random.random() < 0.1: # 10% of requests get a tiny pause
await asyncio.sleep(0.001)
return results
async def run_load_test(self):
"""Run the actual load test"""
print(f"\n{'='*60}")
print(f"Starting load test with {len(self.users)} concurrent users")
print(f"Each user will make {REQUESTS_PER_USER} requests")
print(f"Total requests: {len(self.users) * REQUESTS_PER_USER}")
print(f"{'='*60}\n")
start_time = time.time()
# Run all users concurrently
tasks = [self.simulate_user(user) for user in self.users]
all_results = await asyncio.gather(*tasks)
# Flatten results
self.results = [result for user_results in all_results for result in user_results]
total_time = time.time() - start_time
# Calculate statistics
self.print_results(total_time)
def print_results(self, total_time: float):
"""Print detailed test results"""
print(f"\n{'='*60}")
print("LOAD TEST RESULTS")
print(f"{'='*60}\n")
total_requests = len(self.results)
successful = sum(1 for r in self.results if r['success'])
failed = total_requests - successful
print(f"Total Requests: {total_requests}")
if total_requests > 0:
print(f"Successful: {successful} ({successful/total_requests*100:.1f}%)")
print(f"Failed: {failed} ({failed/total_requests*100:.1f}%)")
# Performance indicators
if failed / total_requests > 0.05:
print(f"⚠️ WARNING: Failure rate above 5% - system may be under stress")
if failed / total_requests > 0.20:
print(f"🔴 CRITICAL: Failure rate above 20% - system is degrading")
else:
print("No requests were made. Check API connectivity and user creation.")
print(f"Total Time: {total_time:.2f}s")
if total_requests > 0 and total_time > 0:
req_per_sec = total_requests/total_time
print(f"Requests/Second: {req_per_sec:.2f}")
if req_per_sec >= 1000:
print(f"🚀 Target achieved: {req_per_sec:.2f} req/s!")
elif req_per_sec >= 500:
print(f"⚡ High throughput: {req_per_sec:.2f} req/s")
if successful > 0:
successful_results = [r for r in self.results if r['success']]
response_times = [r['elapsed'] for r in successful_results]
print(f"\nResponse Time Statistics (successful requests):")
print(f" Min: {min(response_times)*1000:.2f}ms")
print(f" Max: {max(response_times)*1000:.2f}ms")
print(f" Mean: {statistics.mean(response_times)*1000:.2f}ms")
print(f" Median: {statistics.median(response_times)*1000:.2f}ms")
print(f" 95th percentile: {statistics.quantiles(response_times, n=20)[18]*1000:.2f}ms")
print(f" 99th percentile: {statistics.quantiles(response_times, n=100)[98]*1000:.2f}ms")
# Breakdown by endpoint
print(f"\nBreakdown by endpoint:")
endpoint_stats = {}
for result in self.results:
endpoint = result['endpoint']
method = result.get('method', 'GET')
key = f"{method} {endpoint}"
if key not in endpoint_stats:
endpoint_stats[key] = {'total': 0, 'success': 0, 'times': [], 'errors': []}
endpoint_stats[key]['total'] += 1
if result['success']:
endpoint_stats[key]['success'] += 1
endpoint_stats[key]['times'].append(result['elapsed'])
else:
endpoint_stats[key]['errors'].append(result.get('error', 'Unknown error'))
for endpoint, stats in sorted(endpoint_stats.items()):
success_rate = stats['success'] / stats['total'] * 100
avg_time = statistics.mean(stats['times']) * 1000 if stats['times'] else 0
print(f" {endpoint:40s} - {stats['total']:4d} requests, {success_rate:5.1f}% success, {avg_time:6.2f}ms avg")
if stats['errors'] and len(stats['errors']) <= 3:
for error in stats['errors'][:3]:
print(f" └─ Error: {error}")
print(f"\n{'='*60}\n")
async def cleanup_test_data(self):
"""Clean up test users (optional)"""
print("\nCleaning up test users...")
# Note: You'd need to implement a cleanup endpoint or do this via database directly
print("Cleanup complete (test users remain in database)")
async def main():
"""Main entry point"""
tester = LoadTester()
try:
await tester.setup()
await tester.populate_database()
await tester.run_load_test()
# Optional: cleanup
# await tester.cleanup_test_data()
finally:
await tester.teardown()
if __name__ == "__main__":
print("""
╔═══════════════════════════════════════════════════════════════╗
║ ECHOES OF THE ASHES - API LOAD TESTING TOOL ║
╚═══════════════════════════════════════════════════════════════╝
""")
asyncio.run(main())

View File

View File

@@ -0,0 +1,49 @@
╔═══════════════════════════════════════════════════════════════╗
║ ECHOES OF THE ASHES - API LOAD TESTING TOOL ║
╚═══════════════════════════════════════════════════════════════╝
Loading game data...
Loaded 14 locations, 28 items, 0 NPCs
Populating database with test data...
Creating 50 test users...
Created 50 test users
Spawning 140 dropped items...
Spawning 70 wandering enemies...
Database population complete!
============================================================
Starting load test with 50 concurrent users
Each user will make 20 requests
Total requests: 1000
============================================================
============================================================
LOAD TEST RESULTS
============================================================
Total Requests: 1000
Successful: 564 (56.4%)
Failed: 436 (43.6%)
Total Time: 7.68s
Requests/Second: 130.18
Response Time Statistics (successful requests):
Min: 2.48ms
Max: 301.39ms
Mean: 28.52ms
Median: 10.74ms
95th percentile: 167.92ms
99th percentile: 283.83ms
Breakdown by endpoint:
/game/inventory - 263 requests, 100.0% success, 19.27ms avg
/game/location - 221 requests, 100.0% success, 35.28ms avg
/game/move - 278 requests, 28.8% success, 40.27ms avg
/game/player - 238 requests, 0.0% success, 0.00ms avg
============================================================

View File

133
tests/quick_perf_test.py Normal file
View File

@@ -0,0 +1,133 @@
#!/usr/bin/env python3
"""
Quick performance test - 50 users, 100 requests each
"""
import asyncio
import aiohttp
import time
import random
import statistics
from typing import List, Dict
API_URL = "https://echoesoftheashgame.patacuack.net/api"
NUM_USERS = 50
REQUESTS_PER_USER = 100
async def test_user(session: aiohttp.ClientSession, user_num: int):
"""Simulate a single user making requests"""
username = f"loadtest_user_{user_num}"
password = "TestPassword123!"
# Login
async with session.post(f"{API_URL}/auth/login",
json={"username": username, "password": password}) as resp:
if resp.status != 200:
return []
data = await resp.json()
token = data["access_token"]
headers = {"Authorization": f"Bearer {token}"}
results = []
# Make requests
for _ in range(REQUESTS_PER_USER):
start = time.time()
action = random.choices(
["location", "inventory", "move"],
weights=[20, 30, 50] # 50% move, 30% inventory, 20% location
)[0]
success = False
try:
if action == "location":
async with session.get(f"{API_URL}/game/location", headers=headers, timeout=aiohttp.ClientTimeout(total=10)) as resp:
success = resp.status == 200
elif action == "inventory":
async with session.get(f"{API_URL}/game/inventory", headers=headers, timeout=aiohttp.ClientTimeout(total=10)) as resp:
success = resp.status == 200
elif action == "move":
# Get valid directions first
async with session.get(f"{API_URL}/game/location", headers=headers, timeout=aiohttp.ClientTimeout(total=10)) as resp:
if resp.status == 200:
loc_data = await resp.json()
directions = loc_data.get("directions", [])
if directions:
direction = random.choice(directions)
async with session.post(f"{API_URL}/game/move",
json={"direction": direction},
headers=headers,
timeout=aiohttp.ClientTimeout(total=10)) as move_resp:
success = move_resp.status == 200
except:
pass
elapsed = (time.time() - start) * 1000 # Convert to ms
results.append({
"action": action,
"success": success,
"time_ms": elapsed
})
await asyncio.sleep(0.001) # Small delay
return results
async def main():
print("\n" + "="*60)
print("QUICK PERFORMANCE TEST")
print(f"{NUM_USERS} users × {REQUESTS_PER_USER} requests = {NUM_USERS * REQUESTS_PER_USER} total")
print("="*60 + "\n")
async with aiohttp.ClientSession() as session:
start_time = time.time()
# Run all users concurrently
tasks = [test_user(session, i) for i in range(1, NUM_USERS + 1)]
all_results = await asyncio.gather(*tasks)
elapsed = time.time() - start_time
# Flatten results
results = [r for user_results in all_results for r in user_results]
# Calculate stats
total = len(results)
successful = sum(1 for r in results if r["success"])
failed = total - successful
success_rate = (successful / total * 100) if total > 0 else 0
rps = total / elapsed if elapsed > 0 else 0
success_times = [r["time_ms"] for r in results if r["success"]]
print("\n" + "="*60)
print("RESULTS")
print("="*60)
print(f"Total Requests: {total}")
print(f"Successful: {successful} ({success_rate:.1f}%)")
print(f"Failed: {failed} ({100-success_rate:.1f}%)")
print(f"Total Time: {elapsed:.2f}s")
print(f"Requests/Second: {rps:.2f}")
if success_times:
print(f"\nResponse Times (successful):")
print(f" Min: {min(success_times):.2f}ms")
print(f" Max: {max(success_times):.2f}ms")
print(f" Mean: {statistics.mean(success_times):.2f}ms")
print(f" Median: {statistics.median(success_times):.2f}ms")
print(f" 95th: {statistics.quantiles(success_times, n=20)[18]:.2f}ms")
# Breakdown by action
print(f"\nBreakdown:")
for action in ["move", "inventory", "location"]:
action_results = [r for r in results if r["action"] == action]
if action_results:
action_success = sum(1 for r in action_results if r["success"])
action_rate = (action_success / len(action_results) * 100)
action_times = [r["time_ms"] for r in action_results if r["success"]]
avg_time = statistics.mean(action_times) if action_times else 0
print(f" {action:12s}: {len(action_results):4d} req, {action_rate:5.1f}% success, {avg_time:6.2f}ms avg")
print("="*60 + "\n")
if __name__ == "__main__":
asyncio.run(main())

View File

@@ -0,0 +1,43 @@
#!/usr/bin/env python3
"""
Script to set high stamina for load test users
Run this inside the container after creating test users
"""
import asyncio
import sys
import os
# Add parent directory to path
sys.path.insert(0, '/app')
from api import database as db
async def set_test_user_stamina(stamina: int = 100000):
"""Set high stamina for all loadtest users"""
async with db.DatabaseSession() as session:
from sqlalchemy import select, update
# Get all loadtest users
stmt = select(db.players).where(db.players.c.username.like('loadtest_user_%'))
result = await session.execute(stmt)
users = result.all()
print(f"Found {len(users)} loadtest users")
if not users:
print("No loadtest users found!")
return
# Update stamina for all test users
stmt = update(db.players).where(
db.players.c.username.like('loadtest_user_%')
).values(stamina=stamina, max_stamina=stamina)
await session.execute(stmt)
await session.commit()
print(f"Updated stamina to {stamina} for all loadtest users")
if __name__ == "__main__":
stamina_value = int(sys.argv[1]) if len(sys.argv) > 1 else 100000
asyncio.run(set_test_user_stamina(stamina_value))

452
tests/test_api.py Normal file
View File

@@ -0,0 +1,452 @@
#!/usr/bin/env python3
"""
Comprehensive API Testing Suite
Tests all API endpoints with realistic test data
"""
import asyncio
import httpx
import json
from typing import Dict, Any
# Configuration
API_URL = "http://localhost:8000"
API_INTERNAL_KEY = "bot-internal-key-9f8e7d6c5b4a3210fedcba9876543210"
class Colors:
GREEN = '\033[92m'
RED = '\033[91m'
YELLOW = '\033[93m'
BLUE = '\033[94m'
RESET = '\033[0m'
class APITester:
def __init__(self):
self.client = httpx.AsyncClient(timeout=30.0)
self.test_results = []
self.auth_token = None
self.test_user_id = None
self.test_telegram_id = 999999999
self.test_username = "test_user"
self.test_password = "Test123!@#"
async def log(self, message: str, color: str = Colors.RESET):
print(f"{color}{message}{Colors.RESET}")
async def test_endpoint(self, name: str, method: str, url: str,
expected_status: int = 200, **kwargs) -> Dict[str, Any]:
"""Test a single endpoint"""
try:
if method == "GET":
response = await self.client.get(url, **kwargs)
elif method == "POST":
response = await self.client.post(url, **kwargs)
elif method == "PATCH":
response = await self.client.patch(url, **kwargs)
elif method == "DELETE":
response = await self.client.delete(url, **kwargs)
else:
raise ValueError(f"Unsupported method: {method}")
success = response.status_code == expected_status
result = {
"name": name,
"success": success,
"status": response.status_code,
"expected": expected_status
}
if success:
await self.log(f"{name} - Status: {response.status_code}", Colors.GREEN)
try:
data = response.json()
if data and not isinstance(data, dict):
await self.log(f" Response: {str(data)[:100]}", Colors.BLUE)
elif data:
await self.log(f" Response: {json.dumps(data, indent=2)[:200]}...", Colors.BLUE)
except:
pass
else:
await self.log(f"{name} - Expected: {expected_status}, Got: {response.status_code}", Colors.RED)
await self.log(f" Response: {response.text[:200]}", Colors.RED)
self.test_results.append(result)
return response
except Exception as e:
await self.log(f"{name} - Exception: {str(e)}", Colors.RED)
self.test_results.append({
"name": name,
"success": False,
"error": str(e)
})
return None
async def setup_test_data(self):
"""Create test data in the database"""
await self.log("\n🔧 Setting up test data...", Colors.YELLOW)
# Create internal API headers
internal_headers = {"X-Internal-Key": API_INTERNAL_KEY}
# Create a Telegram user
await self.test_endpoint(
"Create Telegram Player",
"POST",
f"{API_URL}/api/internal/player",
params={"telegram_id": self.test_telegram_id, "name": "Test Telegram User"},
headers=internal_headers,
expected_status=200
)
# Get the player to get their ID
response = await self.test_endpoint(
"Get Telegram Player by telegram_id",
"GET",
f"{API_URL}/api/internal/player/{self.test_telegram_id}",
headers=internal_headers,
expected_status=200
)
if response and response.status_code == 200:
player_data = response.json()
self.test_user_id = player_data.get('id')
await self.log(f" Test user ID: {self.test_user_id}", Colors.BLUE)
# Add some items to inventory
await self.test_endpoint(
"Add item to inventory (knife)",
"POST",
f"{API_URL}/api/internal/player/{self.test_user_id}/inventory",
headers=internal_headers,
json={"item_id": "knife", "quantity": 1},
expected_status=200
)
await self.test_endpoint(
"Add item to inventory (water)",
"POST",
f"{API_URL}/api/internal/player/{self.test_user_id}/inventory",
headers=internal_headers,
json={"item_id": "water", "quantity": 3},
expected_status=200
)
# Create a dropped item
await self.test_endpoint(
"Create dropped item",
"POST",
f"{API_URL}/api/internal/dropped-items",
headers=internal_headers,
params={"item_id": "bandage", "quantity": 2, "location_id": "start_point"},
expected_status=200
)
# Create a wandering enemy
await self.test_endpoint(
"Spawn wandering enemy",
"POST",
f"{API_URL}/api/internal/wandering-enemies",
headers=internal_headers,
params={"npc_id": "mutant_rat", "location_id": "start_point", "current_hp": 30, "max_hp": 30},
expected_status=200
)
async def test_health_check(self):
await self.log("\n📋 Testing Health Check", Colors.YELLOW)
await self.test_endpoint(
"Health Check",
"GET",
f"{API_URL}/health"
)
async def test_auth_endpoints(self):
await self.log("\n🔐 Testing Authentication Endpoints", Colors.YELLOW)
# Register
response = await self.test_endpoint(
"Register Web User",
"POST",
f"{API_URL}/api/auth/register",
json={
"username": self.test_username,
"password": self.test_password,
"name": "Test User"
},
expected_status=200
)
# Login
response = await self.test_endpoint(
"Login Web User",
"POST",
f"{API_URL}/api/auth/login",
json={
"username": self.test_username,
"password": self.test_password
},
expected_status=200
)
if response and response.status_code == 200:
data = response.json()
self.auth_token = data.get('access_token')
await self.log(f" Auth token obtained", Colors.BLUE)
# Get current user
if self.auth_token:
await self.test_endpoint(
"Get Current User (Me)",
"GET",
f"{API_URL}/api/auth/me",
headers={"Authorization": f"Bearer {self.auth_token}"}
)
async def test_game_endpoints(self):
if not self.auth_token:
await self.log("\n⚠️ Skipping game endpoints (no auth token)", Colors.YELLOW)
return
await self.log("\n🎮 Testing Game Endpoints", Colors.YELLOW)
headers = {"Authorization": f"Bearer {self.auth_token}"}
# Game state
await self.test_endpoint(
"Get Game State",
"GET",
f"{API_URL}/api/game/state",
headers=headers
)
# Profile
await self.test_endpoint(
"Get Player Profile",
"GET",
f"{API_URL}/api/game/profile",
headers=headers
)
# Location
await self.test_endpoint(
"Get Current Location",
"GET",
f"{API_URL}/api/game/location",
headers=headers
)
# Inventory
await self.test_endpoint(
"Get Inventory",
"GET",
f"{API_URL}/api/game/inventory",
headers=headers
)
# Move (should fail - need stamina/valid direction)
await self.test_endpoint(
"Move (expect failure)",
"POST",
f"{API_URL}/api/game/move",
headers=headers,
json={"direction": "north"},
expected_status=400 # Expect failure
)
# Inspect
await self.test_endpoint(
"Inspect Area",
"POST",
f"{API_URL}/api/game/inspect",
headers=headers
)
async def test_internal_endpoints(self):
await self.log("\n🔧 Testing Internal Bot API Endpoints", Colors.YELLOW)
internal_headers = {"X-Internal-Key": API_INTERNAL_KEY}
if not self.test_user_id:
await self.log(" No test user ID available", Colors.RED)
return
# Player operations
await self.test_endpoint(
"Get Player by ID",
"GET",
f"{API_URL}/api/internal/player/by_id/{self.test_user_id}",
headers=internal_headers
)
await self.test_endpoint(
"Update Player",
"PATCH",
f"{API_URL}/api/internal/player/{self.test_user_id}",
headers=internal_headers,
json={"hp": 95}
)
# Inventory operations
await self.test_endpoint(
"Get Player Inventory",
"GET",
f"{API_URL}/api/internal/player/{self.test_user_id}/inventory",
headers=internal_headers
)
# Movement
await self.test_endpoint(
"Move Player",
"POST",
f"{API_URL}/api/internal/player/{self.test_user_id}/move",
headers=internal_headers,
json={"location_id": "abandoned_house"}
)
# Location queries
await self.test_endpoint(
"Get Dropped Items in Location",
"GET",
f"{API_URL}/api/internal/location/start_point/dropped-items",
headers=internal_headers
)
await self.test_endpoint(
"Get Wandering Enemies in Location",
"GET",
f"{API_URL}/api/internal/location/start_point/wandering-enemies",
headers=internal_headers
)
# Combat operations
await self.test_endpoint(
"Get Combat State",
"GET",
f"{API_URL}/api/internal/player/{self.test_user_id}/combat",
headers=internal_headers
)
# Create combat
combat_response = await self.test_endpoint(
"Create Combat",
"POST",
f"{API_URL}/api/internal/combat/create",
headers=internal_headers,
json={
"player_id": self.test_user_id,
"npc_id": "zombie",
"npc_hp": 50,
"npc_max_hp": 50,
"location_id": "abandoned_house",
"from_wandering": False
}
)
if combat_response and combat_response.status_code == 200:
# Update combat
await self.test_endpoint(
"Update Combat",
"PATCH",
f"{API_URL}/api/internal/combat/{self.test_user_id}",
headers=internal_headers,
json={"npc_hp": 40, "turn": "npc"}
)
# End combat
await self.test_endpoint(
"End Combat",
"DELETE",
f"{API_URL}/api/internal/combat/{self.test_user_id}",
headers=internal_headers
)
# Cooldown operations
await self.test_endpoint(
"Set Cooldown",
"POST",
f"{API_URL}/api/internal/cooldown/test_cooldown_key",
headers=internal_headers,
params={"duration_seconds": 300}
)
await self.test_endpoint(
"Get Cooldown",
"GET",
f"{API_URL}/api/internal/cooldown/test_cooldown_key",
headers=internal_headers
)
# Corpse operations
await self.test_endpoint(
"Create NPC Corpse",
"POST",
f"{API_URL}/api/internal/corpses/npc",
headers=internal_headers,
params={
"npc_id": "zombie",
"location_id": "abandoned_house",
"loot_remaining": json.dumps([{"item_id": "cloth", "quantity": 2}])
}
)
await self.test_endpoint(
"Get NPC Corpses in Location",
"GET",
f"{API_URL}/api/internal/location/abandoned_house/corpses/npc",
headers=internal_headers
)
# Status effects
await self.test_endpoint(
"Get Player Status Effects",
"GET",
f"{API_URL}/api/internal/player/{self.test_user_id}/status-effects",
headers=internal_headers
)
async def print_summary(self):
await self.log("\n" + "="*60, Colors.BLUE)
await self.log("📊 TEST SUMMARY", Colors.BLUE)
await self.log("="*60, Colors.BLUE)
total = len(self.test_results)
passed = sum(1 for r in self.test_results if r.get('success', False))
failed = total - passed
await self.log(f"\nTotal Tests: {total}", Colors.BLUE)
await self.log(f"Passed: {passed}", Colors.GREEN)
await self.log(f"Failed: {failed}", Colors.RED if failed > 0 else Colors.GREEN)
await self.log(f"Success Rate: {(passed/total*100):.1f}%", Colors.GREEN if failed == 0 else Colors.YELLOW)
if failed > 0:
await self.log("\n❌ Failed Tests:", Colors.RED)
for result in self.test_results:
if not result.get('success', False):
await self.log(f" - {result['name']}", Colors.RED)
if 'error' in result:
await self.log(f" Error: {result['error']}", Colors.RED)
elif 'status' in result:
await self.log(f" Expected: {result['expected']}, Got: {result['status']}", Colors.RED)
async def run_all_tests(self):
await self.log("🚀 Starting API Test Suite", Colors.BLUE)
await self.log("="*60, Colors.BLUE)
try:
await self.test_health_check()
await self.setup_test_data()
await self.test_auth_endpoints()
await self.test_game_endpoints()
await self.test_internal_endpoints()
await self.print_summary()
finally:
await self.client.aclose()
async def main():
tester = APITester()
await tester.run_all_tests()
if __name__ == "__main__":
asyncio.run(main())

453
tests/test_comprehensive.py Normal file
View File

@@ -0,0 +1,453 @@
#!/usr/bin/env python3
"""
Comprehensive API Test Suite
Tests all major game functionality including:
- Authentication (web & telegram)
- Player creation and management
- Movement and exploration
- Inventory and items
- Combat system
- Interactables
- Admin functions
"""
import asyncio
import httpx
import json
from datetime import datetime
import sys
# Configuration
BASE_URL = "http://localhost:8000"
API_INTERNAL_KEY = "bot-internal-key-9f8e7d6c5b4a3210fedcba9876543210"
# ANSI color codes for pretty output
class Colors:
GREEN = '\033[92m'
RED = '\033[91m'
YELLOW = '\033[93m'
BLUE = '\033[94m'
PURPLE = '\033[95m'
CYAN = '\033[96m'
BOLD = '\033[1m'
END = '\033[0m'
class TestRunner:
def __init__(self):
self.passed = 0
self.failed = 0
self.tests = []
self.client = None
self.test_user = None
self.test_token = None
async def setup(self):
"""Initialize HTTP client"""
self.client = httpx.AsyncClient(timeout=30.0, follow_redirects=True)
async def cleanup(self):
"""Cleanup resources"""
if self.client:
await self.client.aclose()
def log_test(self, name: str, passed: bool, details: str = ""):
"""Log test result"""
status = f"{Colors.GREEN}✅ PASS{Colors.END}" if passed else f"{Colors.RED}❌ FAIL{Colors.END}"
print(f"{status} - {name}")
if details:
print(f" {Colors.CYAN}{details}{Colors.END}")
self.tests.append({"name": name, "passed": passed, "details": details})
if passed:
self.passed += 1
else:
self.failed += 1
def print_summary(self):
"""Print test summary"""
total = self.passed + self.failed
rate = (self.passed / total * 100) if total > 0 else 0
print(f"\n{Colors.BOLD}{'='*70}{Colors.END}")
print(f"{Colors.BOLD}TEST SUMMARY{Colors.END}")
print(f"{Colors.BOLD}{'='*70}{Colors.END}")
print(f"Total Tests: {Colors.BOLD}{total}{Colors.END}")
print(f"Passed: {Colors.GREEN}{self.passed}{Colors.END}")
print(f"Failed: {Colors.RED}{self.failed}{Colors.END}")
print(f"Success Rate: {Colors.YELLOW}{rate:.1f}%{Colors.END}")
print(f"{Colors.BOLD}{'='*70}{Colors.END}\n")
if self.failed > 0:
print(f"{Colors.RED}Failed tests:{Colors.END}")
for test in self.tests:
if not test['passed']:
print(f"{test['name']}: {test['details']}")
async def test_health_check(self):
"""Test health check endpoint"""
try:
response = await self.client.get(f"{BASE_URL}/health")
passed = response.status_code == 200
self.log_test("Health Check", passed, f"Status: {response.status_code}")
except Exception as e:
self.log_test("Health Check", False, f"Error: {str(e)}")
async def test_register_web_user(self):
"""Test web user registration"""
timestamp = int(datetime.now().timestamp())
username = f"test_user_{timestamp}"
try:
response = await self.client.post(
f"{BASE_URL}/api/auth/register",
json={
"username": username,
"password": "TestPass123!",
"character_name": "Test Survivor"
}
)
# Registration can return 200 or 201, both with a token
if response.status_code in [200, 201]:
data = response.json()
self.test_user = username
self.test_token = data.get('access_token')
self.log_test("Web User Registration", True,
f"Created user: {username}, Got token: {self.test_token[:20] if self.test_token else 'None'}...")
else:
self.log_test("Web User Registration", False,
f"Status: {response.status_code}, Response: {response.text[:200]}")
except Exception as e:
self.log_test("Web User Registration", False, f"Error: {str(e)}")
async def test_login(self):
"""Test user login"""
if not self.test_user:
self.log_test("Login", False, "No test user available")
return
try:
response = await self.client.post(
f"{BASE_URL}/api/auth/login",
json={
"username": self.test_user,
"password": "TestPass123!"
}
)
if response.status_code == 200:
data = response.json()
self.test_token = data.get('access_token')
self.log_test("Login", True, f"Got token: {self.test_token[:20]}...")
else:
self.log_test("Login", False, f"Status: {response.status_code}")
except Exception as e:
self.log_test("Login", False, f"Error: {str(e)}")
async def test_get_user_info(self):
"""Test getting current user info"""
if not self.test_token:
self.log_test("Get User Info", False, "No auth token")
return
try:
response = await self.client.get(
f"{BASE_URL}/api/auth/me",
headers={"Authorization": f"Bearer {self.test_token}"}
)
if response.status_code == 200:
data = response.json()
self.log_test("Get User Info", True,
f"User: {data.get('username')}, Location: {data.get('location_id')}")
else:
self.log_test("Get User Info", False, f"Status: {response.status_code}")
except Exception as e:
self.log_test("Get User Info", False, f"Error: {str(e)}")
async def test_get_location(self):
"""Test getting current location details"""
if not self.test_token:
self.log_test("Get Location", False, "No auth token")
return
try:
response = await self.client.get(
f"{BASE_URL}/api/game/location",
headers={"Authorization": f"Bearer {self.test_token}"}
)
if response.status_code == 200:
data = response.json()
directions = data.get('directions', [])
interactables = data.get('interactables', [])
self.log_test("Get Location", True,
f"{data.get('name')} - Directions: {directions}, Interactables: {len(interactables)}")
else:
self.log_test("Get Location", False,
f"Status: {response.status_code}, Response: {response.text[:200]}")
except Exception as e:
self.log_test("Get Location", False, f"Error: {str(e)}")
async def test_inspect_area(self):
"""Test inspecting the current area"""
if not self.test_token:
self.log_test("Inspect Area", False, "No auth token")
return
try:
response = await self.client.post(
f"{BASE_URL}/api/game/inspect",
headers={"Authorization": f"Bearer {self.test_token}"}
)
if response.status_code == 200:
data = response.json()
self.log_test("Inspect Area", True, f"Found: {data.get('message', '')[:100]}")
else:
self.log_test("Inspect Area", False, f"Status: {response.status_code}")
except Exception as e:
self.log_test("Inspect Area", False, f"Error: {str(e)}")
async def test_get_inventory(self):
"""Test getting player inventory"""
if not self.test_token:
self.log_test("Get Inventory", False, "No auth token")
return
try:
response = await self.client.get(
f"{BASE_URL}/api/game/inventory",
headers={"Authorization": f"Bearer {self.test_token}"}
)
if response.status_code == 200:
items = response.json() # Returns array directly
self.log_test("Get Inventory", True, f"Items: {len(items)}")
else:
self.log_test("Get Inventory", False, f"Status: {response.status_code}")
except Exception as e:
self.log_test("Get Inventory", False, f"Error: {str(e)}")
async def test_get_profile(self):
"""Test getting player profile"""
if not self.test_token:
self.log_test("Get Profile", False, "No auth token")
return
try:
response = await self.client.get(
f"{BASE_URL}/api/game/profile",
headers={"Authorization": f"Bearer {self.test_token}"}
)
if response.status_code == 200:
data = response.json()
self.log_test("Get Profile", True,
f"HP: {data.get('hp')}/{data.get('max_hp')}, Level: {data.get('level')}")
else:
self.log_test("Get Profile", False, f"Status: {response.status_code}")
except Exception as e:
self.log_test("Get Profile", False, f"Error: {str(e)}")
async def test_movement(self):
"""Test player movement"""
if not self.test_token:
self.log_test("Movement", False, "No auth token")
return
try:
# First get current location to see available directions
loc_response = await self.client.get(
f"{BASE_URL}/api/game/location",
headers={"Authorization": f"Bearer {self.test_token}"}
)
if loc_response.status_code != 200:
self.log_test("Movement", False, "Could not get current location")
return
location = loc_response.json()
directions = location.get('directions', [])
if not directions:
self.log_test("Movement", False,
f"No directions available at {location.get('name')}")
return
# Try to move in the first available direction
test_direction = directions[0]
response = await self.client.post(
f"{BASE_URL}/api/game/move",
headers={"Authorization": f"Bearer {self.test_token}"},
json={"direction": test_direction}
)
if response.status_code == 200:
data = response.json()
self.log_test("Movement", True,
f"Moved {test_direction} to {data.get('new_location_id')}")
# Move back
back_direction = {"north": "south", "south": "north",
"east": "west", "west": "east",
"northeast": "southwest", "southwest": "northeast",
"northwest": "southeast", "southeast": "northwest"}.get(test_direction)
if back_direction:
await self.client.post(
f"{BASE_URL}/api/game/move",
headers={"Authorization": f"Bearer {self.test_token}"},
json={"direction": back_direction}
)
else:
error_msg = response.json().get('detail', response.text[:200])
self.log_test("Movement", False,
f"Status: {response.status_code}, Error: {error_msg}")
except Exception as e:
self.log_test("Movement", False, f"Error: {str(e)}")
async def test_interactable(self):
"""Test interacting with objects"""
if not self.test_token:
self.log_test("Interactables", False, "No auth token")
return
try:
# Get current location
loc_response = await self.client.get(
f"{BASE_URL}/api/game/location",
headers={"Authorization": f"Bearer {self.test_token}"}
)
if loc_response.status_code != 200:
self.log_test("Interactables", False, "Could not get location")
return
location = loc_response.json()
interactables = location.get('interactables', [])
if not interactables:
self.log_test("Interactables", False,
f"No interactables at {location.get('name')}")
return
# Try to interact with first interactable
interactable = interactables[0]
actions = interactable.get('actions', [])
if not actions:
self.log_test("Interactables", False, "No actions available")
return
action = actions[0]
response = await self.client.post(
f"{BASE_URL}/api/game/interact",
headers={"Authorization": f"Bearer {self.test_token}"},
json={
"interactable_id": interactable['instance_id'],
"action_id": action['id']
}
)
if response.status_code == 200:
data = response.json()
self.log_test("Interactables", True,
f"Action '{action['name']}' on {interactable['name']}: {data.get('message', '')[:100]}")
else:
self.log_test("Interactables", False,
f"Status: {response.status_code}, Error: {response.text[:200]}")
except Exception as e:
self.log_test("Interactables", False, f"Error: {str(e)}")
async def test_game_state(self):
"""Test getting full game state"""
if not self.test_token:
self.log_test("Game State", False, "No auth token")
return
try:
response = await self.client.get(
f"{BASE_URL}/api/game/state",
headers={"Authorization": f"Bearer {self.test_token}"}
)
if response.status_code == 200:
data = response.json()
player = data.get('player', {})
location = data.get('location', {})
inventory = data.get('inventory', [])
self.log_test("Game State", True,
f"Player: {player.get('name')}, Location: {location.get('name')}, Items: {len(inventory)}")
else:
self.log_test("Game State", False,
f"Status: {response.status_code}, Error: {response.text[:200]}")
except Exception as e:
self.log_test("Game State", False, f"Error: {str(e)}")
async def test_image_serving(self):
"""Test that images are being served correctly"""
try:
# Test a known image path
response = await self.client.get(f"{BASE_URL}/images/locations/downtown.png")
if response.status_code == 200 and 'image' in response.headers.get('content-type', ''):
self.log_test("Image Serving", True,
f"Image served correctly, size: {len(response.content)} bytes")
else:
self.log_test("Image Serving", False,
f"Status: {response.status_code}, Content-Type: {response.headers.get('content-type')}")
except Exception as e:
self.log_test("Image Serving", False, f"Error: {str(e)}")
async def run_all_tests(self):
"""Run all tests in sequence"""
print(f"\n{Colors.BOLD}{Colors.PURPLE}{'='*70}{Colors.END}")
print(f"{Colors.BOLD}{Colors.PURPLE}COMPREHENSIVE API TEST SUITE{Colors.END}")
print(f"{Colors.BOLD}{Colors.PURPLE}{'='*70}{Colors.END}\n")
await self.setup()
try:
# Basic health check
print(f"\n{Colors.BOLD}Testing System Health{Colors.END}")
await self.test_health_check()
await self.test_image_serving()
# Authentication flow
print(f"\n{Colors.BOLD}Testing Authentication{Colors.END}")
await self.test_register_web_user()
await self.test_login()
await self.test_get_user_info()
# Game state
print(f"\n{Colors.BOLD}Testing Game State{Colors.END}")
await self.test_get_profile()
await self.test_get_location()
await self.test_get_inventory()
await self.test_game_state()
# Gameplay
print(f"\n{Colors.BOLD}Testing Gameplay{Colors.END}")
await self.test_inspect_area()
await self.test_movement()
await self.test_interactable()
# Summary
self.print_summary()
finally:
await self.cleanup()
async def main():
"""Main entry point"""
runner = TestRunner()
await runner.run_all_tests()
# Exit with appropriate code
sys.exit(0 if runner.failed == 0 else 1)
if __name__ == "__main__":
asyncio.run(main())

61
tests/test_db_init.py Normal file
View File

@@ -0,0 +1,61 @@
#!/usr/bin/env python3
"""
Test script to verify that init_db() creates all tables and indexes properly.
This simulates a fresh database initialization.
"""
import asyncio
import sys
import os
sys.path.insert(0, '/app')
from api import database
async def test_init():
"""Test database initialization"""
print("Testing database initialization...")
print("=" * 60)
try:
# Initialize database (create tables and indexes)
await database.init_db()
print("✓ Database initialization completed successfully")
# Verify tables exist
async with database.engine.begin() as conn:
result = await conn.execute(database.text("""
SELECT tablename
FROM pg_tables
WHERE schemaname = 'public'
ORDER BY tablename;
"""))
tables = [row[0] for row in result]
print(f"\n✓ Found {len(tables)} tables:")
for table in tables:
print(f" - {table}")
# Verify indexes exist
result = await conn.execute(database.text("""
SELECT tablename, indexname
FROM pg_indexes
WHERE schemaname = 'public' AND indexname LIKE 'idx_%'
ORDER BY tablename, indexname;
"""))
indexes = list(result)
print(f"\n✓ Found {len(indexes)} performance indexes:")
for table, index in indexes:
print(f" - {index} on {table}")
print("\n" + "=" * 60)
print("✓ All tests passed!")
return True
except Exception as e:
print(f"\n✗ Error during initialization: {e}")
import traceback
traceback.print_exc()
return False
finally:
await database.engine.dispose()
if __name__ == "__main__":
success = asyncio.run(test_init())
sys.exit(0 if success else 1)