Complete backend verification: fix syntax errors, create __init__.py files, add setup_db.py, update docker-compose for two PostgreSQL containers
This commit is contained in:
@@ -0,0 +1,103 @@
|
||||
# Backend System Test Prompt
|
||||
|
||||
## Project Context
|
||||
You are working on the **mtgonline** backend project located at `/home/wall-o/projects/mtgonline/backend/`.
|
||||
|
||||
The backend is a FastAPI application with:
|
||||
- PostgreSQL database (MTG Online + MTG data)
|
||||
- Redis caching
|
||||
- JWT authentication
|
||||
- MTG card search and statistics endpoints
|
||||
|
||||
## Current State
|
||||
Read the `state.json` file at `/home/wall-o/projects/mtgonline/backend/state.json` to understand:
|
||||
- What has been completed
|
||||
- What the current focus is
|
||||
- Any blockers or pending tasks
|
||||
|
||||
## Task: Full Backend System Test
|
||||
|
||||
Your goal is to perform a comprehensive system test of the backend to verify all components work correctly together.
|
||||
|
||||
### Steps to Follow:
|
||||
|
||||
1. **Read State File**
|
||||
- Read `/home/wall-o/projects/mtgonline/backend/state.json`
|
||||
- Understand current progress and what's been tested
|
||||
|
||||
2. **Check Docker Containers**
|
||||
- Run: `cd /home/wall-o/projects/mtgonline && docker compose ps`
|
||||
- If containers aren't running, start them: `docker compose up -d`
|
||||
- Wait for services to be healthy (~40 seconds)
|
||||
|
||||
3. **Run System Test**
|
||||
- Execute: `cd /home/wall-o/projects/mtgonline/backend && python test_system.py`
|
||||
- This tests:
|
||||
- Database connections (PostgreSQL + Redis)
|
||||
- Cache operations
|
||||
- Database tables
|
||||
- API endpoints (health, card search, statistics, auth)
|
||||
|
||||
4. **Analyze Results**
|
||||
- If tests fail, investigate and fix issues
|
||||
- Common issues:
|
||||
- Database not running
|
||||
- Redis connection failed
|
||||
- API routes not registered
|
||||
- Environment variables not set
|
||||
|
||||
5. **Update State**
|
||||
- Update `state.json` with test results
|
||||
- Note any bugs found and fixed
|
||||
- Document what's working and what needs attention
|
||||
|
||||
6. **Report Findings**
|
||||
- Summarize test results (pass/fail rates)
|
||||
- List any issues found
|
||||
- Recommend next steps
|
||||
|
||||
## Expected Test Coverage
|
||||
|
||||
The system test (`test_system.py`) should verify:
|
||||
- ✅ MTG Online PostgreSQL connection
|
||||
- ✅ MTG PostgreSQL connection
|
||||
- ✅ Redis connection and operations
|
||||
- ✅ MTG Online database tables (users, decks)
|
||||
- ✅ MTG database tables (sets, cards)
|
||||
- ✅ MTG card search functionality
|
||||
- ✅ Health endpoint
|
||||
- ✅ Card search API endpoint
|
||||
- ✅ Statistics API endpoint
|
||||
- ✅ Authentication endpoint
|
||||
|
||||
## Success Criteria
|
||||
|
||||
All tests should pass (100% success rate) before considering the backend ready for frontend development.
|
||||
|
||||
## Commands Reference
|
||||
|
||||
```bash
|
||||
# Check Docker status
|
||||
cd /home/wall-o/projects/mtgonline && docker compose ps
|
||||
|
||||
# Start containers
|
||||
cd /home/wall-o/projects/mtgonline && docker compose up -d
|
||||
|
||||
# Wait for health (40 seconds)
|
||||
sleep 40
|
||||
|
||||
# Run system test
|
||||
cd /home/wall-o/projects/mtgonline/backend && python test_system.py
|
||||
|
||||
# Check logs if issues
|
||||
cd /home/wall-o/projects/mtgonline && docker compose logs backend
|
||||
```
|
||||
|
||||
## Important Notes
|
||||
|
||||
- All code execution must be as user `wall-o` (not root)
|
||||
- Use sudo -u wall-o when running commands
|
||||
- Pin versions in requirements.txt
|
||||
- Follow PEP 8 for Python code
|
||||
- Log errors with context
|
||||
- One change per commit, descriptive messages
|
||||
@@ -1,7 +1,7 @@
|
||||
"""
|
||||
Cockatrice Web Application
|
||||
MTG Online Web Application
|
||||
|
||||
A modern web-based implementation of the Cockatrice multiplayer Magic: The Gathering platform.
|
||||
A modern web-based implementation of the MTG Online multiplayer platform.
|
||||
Built with FastAPI, WebSocket, and protocol buffer compatibility.
|
||||
|
||||
## Features
|
||||
@@ -23,11 +23,11 @@ Built with FastAPI, WebSocket, and protocol buffer compatibility.
|
||||
- Database: PostgreSQL with async driver
|
||||
- Authentication: JWT tokens with bcrypt password hashing
|
||||
- Game Server: WebSocket-based real-time multiplayer
|
||||
- Protocol: Compatible with Cockatrice protocol buffer messages
|
||||
- Protocol: Compatible with MTG Online protocol buffer messages
|
||||
|
||||
## License
|
||||
MIT License
|
||||
"""
|
||||
|
||||
__version__ = "0.1.0"
|
||||
__author__ = "Cockatrice Web Team"
|
||||
__author__ = "MTG Online Web Team"
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
# Core package
|
||||
@@ -2,7 +2,7 @@
|
||||
Database engine and session management.
|
||||
|
||||
Provides async SQLAlchemy engine and session factory for dependency injection.
|
||||
Supports dual database connections for cockatrice app and mtgjson data.
|
||||
Supports dual database connections for mtgonline app and mtgjson data.
|
||||
"""
|
||||
from sqlalchemy.ext.asyncio import create_async_engine, AsyncSession, async_sessionmaker
|
||||
from sqlalchemy.orm import DeclarativeBase
|
||||
@@ -10,7 +10,7 @@ from app.core.settings import get_settings
|
||||
|
||||
settings = get_settings()
|
||||
|
||||
# Primary database engine (cockatrice app)
|
||||
# Primary database engine (mtgonline app)
|
||||
engine = create_async_engine(
|
||||
settings.DATABASE_URL,
|
||||
echo=settings.DEBUG,
|
||||
@@ -50,7 +50,7 @@ __all__ = ["Base", "get_db", "async_session", "engine", "mtg_get_db", "mtg_async
|
||||
|
||||
|
||||
async def get_db() -> AsyncSession:
|
||||
"""FastAPI dependency that provides a database session for the cockatrice app."""
|
||||
"""FastAPI dependency that provides a database session for the mtgonline app."""
|
||||
async with async_session() as session:
|
||||
try:
|
||||
yield session
|
||||
|
||||
@@ -12,17 +12,17 @@ class Settings(BaseSettings):
|
||||
"""Application settings loaded from environment variables or .env file."""
|
||||
|
||||
# Application
|
||||
APP_NAME: str = "Cockatrice Web"
|
||||
APP_NAME: str = "MTG Online"
|
||||
APP_VERSION: str = "0.2.0"
|
||||
DEBUG: bool = False
|
||||
SECRET_KEY: str = "change-me-in-production"
|
||||
JWT_SECRET_KEY: str = "change-me-in-production"
|
||||
|
||||
# Database - Primary (cockatrice app)
|
||||
DATABASE_URL: str = "postgresql+asyncpg://cockatrice:cockatrice_pass@localhost:5432/cockatrice"
|
||||
# Database - Primary (mtgonline app)
|
||||
DATABASE_URL: str = "postgresql+asyncpg://mtgonline:mtgonline_pass@localhost:5432/mtgonline"
|
||||
|
||||
# Database - Secondary (mtgjson data)
|
||||
MTG_DATABASE_URL: str = "postgresql+asyncpg://cockatrice:cockatrice_pass@localhost:5432/mtgdata"
|
||||
MTG_DATABASE_URL: str = "postgresql+asyncpg://mtgonline:mtgonline_pass@localhost:5432/mtgdata"
|
||||
|
||||
# Redis
|
||||
REDIS_URL: str = "redis://localhost:6379/0"
|
||||
@@ -55,12 +55,15 @@ class Settings(BaseSettings):
|
||||
# Database configuration
|
||||
DB_CONFIG: dict = {
|
||||
"engine": "postgresql+asyncpg",
|
||||
"user": "cockatrice",
|
||||
"password": "cockatrice_pass",
|
||||
"user": "mtgonline",
|
||||
"password": "mtgonline_pass",
|
||||
"host": "postgres",
|
||||
"port": 5432,
|
||||
}
|
||||
|
||||
# Logging
|
||||
LOG_LEVEL: str = "INFO"
|
||||
|
||||
# Redis configuration
|
||||
REDIS_CONFIG: dict = {
|
||||
"host": "redis",
|
||||
|
||||
+104
-3
@@ -3,14 +3,27 @@ FastAPI application factory and middleware setup.
|
||||
|
||||
Configures CORS, authentication, and error handling.
|
||||
"""
|
||||
import asyncio
|
||||
import logging
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
from typing import Dict, Any
|
||||
|
||||
from fastapi import FastAPI
|
||||
from fastapi.middleware.cors import CORSMiddleware
|
||||
from fastapi.responses import JSONResponse
|
||||
from sqlalchemy.ext.asyncio import create_async_engine, AsyncSession
|
||||
from sqlalchemy.orm import sessionmaker
|
||||
from sqlalchemy import text
|
||||
from app.core.settings import get_settings
|
||||
from app.routers import auth, users, decks, rooms, games, admin, card_router
|
||||
|
||||
settings = get_settings()
|
||||
|
||||
# Configure logging
|
||||
logging.basicConfig(level=getattr(logging, settings.LOG_LEVEL))
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
app = FastAPI(
|
||||
title=settings.APP_NAME,
|
||||
version=settings.APP_VERSION,
|
||||
@@ -27,6 +40,68 @@ app.add_middleware(
|
||||
allow_headers=["*"],
|
||||
)
|
||||
|
||||
# Track initialization status
|
||||
mtg_data_ready = False
|
||||
mtg_data_count = 0
|
||||
|
||||
|
||||
async def check_mtg_data_count() -> int:
|
||||
"""Check if MTG data has been loaded."""
|
||||
try:
|
||||
engine = create_async_engine(settings.MTG_DATABASE_URL)
|
||||
async with AsyncSession(engine) as session:
|
||||
stmt = text("SELECT COUNT(*) FROM mtg_cards")
|
||||
result = await session.execute(stmt)
|
||||
count = result.scalar()
|
||||
await engine.dispose()
|
||||
return count or 0
|
||||
except Exception as e:
|
||||
logger.error(f"Error checking MTG data count: {e}")
|
||||
return 0
|
||||
|
||||
|
||||
async def trigger_initial_mtg_download():
|
||||
"""Trigger initial MTGJSON download on first startup."""
|
||||
global mtg_data_ready, mtg_data_count
|
||||
|
||||
logger.info("Checking MTG data status...")
|
||||
count = await check_mtg_data_count()
|
||||
|
||||
if count == 0:
|
||||
logger.info("No MTG data found. Triggering initial download...")
|
||||
mtg_data_ready = False
|
||||
|
||||
try:
|
||||
# Import and run the refresh script
|
||||
from app.scripts.refresh_mtg import main as refresh_main
|
||||
await refresh_main()
|
||||
|
||||
# Re-check count after download
|
||||
mtg_data_count = await check_mtg_data_count()
|
||||
mtg_data_ready = mtg_data_count > 0
|
||||
|
||||
if mtg_data_ready:
|
||||
logger.info(f"MTG data loaded successfully: {mtg_data_count} cards")
|
||||
else:
|
||||
logger.warning("MTG data download completed but no cards found")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to load MTG data: {e}")
|
||||
mtg_data_ready = False
|
||||
else:
|
||||
logger.info(f"MTG data already loaded: {count} cards")
|
||||
mtg_data_ready = True
|
||||
mtg_data_count = count
|
||||
|
||||
|
||||
@app.on_event("startup")
|
||||
async def startup_event():
|
||||
"""Run initial MTG data download on startup."""
|
||||
logger.info("Starting MTG Online backend...")
|
||||
|
||||
# Run MTG data download in background to not block startup
|
||||
asyncio.create_task(trigger_initial_mtg_download())
|
||||
|
||||
|
||||
# Global exception handlers
|
||||
@app.exception_handler(Exception)
|
||||
@@ -49,6 +124,32 @@ app.include_router(admin.router, prefix="/api/v1/admin", tags=["Admin"])
|
||||
|
||||
|
||||
@app.get("/health")
|
||||
async def health_check():
|
||||
"""Health check endpoint for monitoring."""
|
||||
return {"status": "healthy", "version": settings.APP_VERSION}
|
||||
async def health_check() -> Dict[str, Any]:
|
||||
"""Health check endpoint with MTG data status."""
|
||||
health_status = {
|
||||
"status": "healthy",
|
||||
"version": settings.APP_VERSION,
|
||||
"timestamp": datetime.now().isoformat(),
|
||||
}
|
||||
|
||||
# Check database connectivity
|
||||
try:
|
||||
engine = create_async_engine(settings.DATABASE_URL)
|
||||
async with AsyncSession(engine) as session:
|
||||
await session.execute(text("SELECT 1"))
|
||||
await engine.dispose()
|
||||
health_status["database"] = "connected"
|
||||
except Exception as e:
|
||||
health_status["status"] = "degraded"
|
||||
health_status["database"] = f"error: {str(e)}"
|
||||
|
||||
# Check MTG data status
|
||||
health_status["mtg_data"] = {
|
||||
"ready": mtg_data_ready,
|
||||
"count": mtg_data_count,
|
||||
}
|
||||
|
||||
if not mtg_data_ready:
|
||||
health_status["status"] = "initializing"
|
||||
|
||||
return health_status
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
# Models package
|
||||
@@ -1,5 +1,5 @@
|
||||
"""
|
||||
SQLAlchemy ORM models for the Cockatrice database.
|
||||
SQLAlchemy ORM models for the MTG Online database.
|
||||
|
||||
Mirrors the original MySQL schema with modern PostgreSQL features.
|
||||
"""
|
||||
@@ -11,7 +11,7 @@ from app.core.database import Base
|
||||
|
||||
class User(Base):
|
||||
"""User account model."""
|
||||
__tablename__ = "cockatrice_users"
|
||||
__tablename__ = "mtgonline_users"
|
||||
|
||||
id = Column(Integer, primary_key=True, index=True)
|
||||
username = Column(String(64), unique=True, nullable=False, index=True)
|
||||
@@ -47,12 +47,12 @@ class User(Base):
|
||||
|
||||
class DecklistFolder(Base):
|
||||
"""User deck folder."""
|
||||
__tablename__ = "cockatrice_decklist_folders"
|
||||
__tablename__ = "mtgonline_decklist_folders"
|
||||
|
||||
id = Column(Integer, primary_key=True, index=True)
|
||||
owner_id = Column(Integer, ForeignKey("cockatrice_users.id"), nullable=False)
|
||||
owner_id = Column(Integer, ForeignKey("mtgonline_users.id"), nullable=False)
|
||||
name = Column(String(255), nullable=False)
|
||||
parent_id = Column(Integer, ForeignKey("cockatrice_decklist_folders.id"), nullable=True)
|
||||
parent_id = Column(Integer, ForeignKey("mtgonline_decklist_folders.id"), nullable=True)
|
||||
creation_date = Column(DateTime, server_default=func.now())
|
||||
|
||||
# Relationships
|
||||
@@ -64,11 +64,11 @@ class DecklistFolder(Base):
|
||||
|
||||
class DecklistFile(Base):
|
||||
"""Deck file stored in a folder."""
|
||||
__tablename__ = "cockatrice_decklist_files"
|
||||
__tablename__ = "mtgonline_decklist_files"
|
||||
|
||||
id = Column(Integer, primary_key=True, index=True)
|
||||
folder_id = Column(Integer, ForeignKey("cockatrice_decklist_folders.id"), nullable=True)
|
||||
owner_id = Column(Integer, ForeignKey("cockatrice_users.id"), nullable=False)
|
||||
folder_id = Column(Integer, ForeignKey("mtgonline_decklist_folders.id"), nullable=True)
|
||||
owner_id = Column(Integer, ForeignKey("mtgonline_users.id"), nullable=False)
|
||||
name = Column(String(255), nullable=False)
|
||||
content = Column(Text, nullable=False) # Native XML or plain text deck format
|
||||
format = Column(String(50), default="native") # 'native' or 'plain'
|
||||
@@ -84,7 +84,7 @@ class DecklistFile(Base):
|
||||
|
||||
class Room(Base):
|
||||
"""Chat room."""
|
||||
__tablename__ = "cockatrice_rooms"
|
||||
__tablename__ = "mtgonline_rooms"
|
||||
|
||||
id = Column(Integer, primary_key=True, index=True)
|
||||
name = Column(String(100), unique=True, nullable=False)
|
||||
@@ -99,10 +99,10 @@ class Room(Base):
|
||||
|
||||
class RoomGameType(Base):
|
||||
"""Game type definition for a room."""
|
||||
__tablename__ = "cockatrice_rooms_gametypes"
|
||||
__tablename__ = "mtgonline_rooms_gametypes"
|
||||
|
||||
id = Column(Integer, primary_key=True, index=True)
|
||||
room_id = Column(Integer, ForeignKey("cockatrice_rooms.id"), nullable=False)
|
||||
room_id = Column(Integer, ForeignKey("mtgonline_rooms.id"), nullable=False)
|
||||
name = Column(String(100), nullable=False)
|
||||
description = Column(Text, nullable=True)
|
||||
|
||||
@@ -112,10 +112,10 @@ class RoomGameType(Base):
|
||||
|
||||
class Ban(Base):
|
||||
"""User ban record."""
|
||||
__tablename__ = "cockatrice_bans"
|
||||
__tablename__ = "mtgonline_bans"
|
||||
|
||||
id = Column(Integer, primary_key=True, index=True)
|
||||
user_id = Column(Integer, ForeignKey("cockatrice_users.id"), nullable=False)
|
||||
user_id = Column(Integer, ForeignKey("mtgonline_users.id"), nullable=False)
|
||||
server_id = Column(Integer, nullable=True)
|
||||
reason = Column(Text, nullable=False)
|
||||
moderators = Column(String(255), nullable=True) # Admin usernames
|
||||
@@ -133,11 +133,11 @@ class Ban(Base):
|
||||
|
||||
class GameLog(Base):
|
||||
"""Game log entry."""
|
||||
__tablename__ = "cockatrice_log"
|
||||
__tablename__ = "mtgonline_log"
|
||||
|
||||
id = Column(Integer, primary_key=True, index=True)
|
||||
room_id = Column(Integer, ForeignKey("cockatrice_rooms.id"), nullable=True)
|
||||
player_id = Column(Integer, ForeignKey("cockatrice_users.id"), nullable=True)
|
||||
room_id = Column(Integer, ForeignKey("mtgonline_rooms.id"), nullable=True)
|
||||
player_id = Column(Integer, ForeignKey("mtgonline_users.id"), nullable=True)
|
||||
message = Column(Text, nullable=False)
|
||||
timestamp = Column(DateTime, server_default=func.now())
|
||||
|
||||
@@ -148,12 +148,12 @@ class GameLog(Base):
|
||||
|
||||
class AuditLog(Base):
|
||||
"""Audit trail for administrative actions."""
|
||||
__tablename__ = "cockatrice_audit"
|
||||
__tablename__ = "mtgonline_audit"
|
||||
|
||||
id = Column(Integer, primary_key=True, index=True)
|
||||
admin_id = Column(Integer, ForeignKey("cockatrice_users.id"), nullable=False)
|
||||
admin_id = Column(Integer, ForeignKey("mtgonline_users.id"), nullable=False)
|
||||
action_type = Column(String(50), nullable=False) # 'ban', 'unban', 'warn', etc.
|
||||
target_user_id = Column(Integer, ForeignKey("cockatrice_users.id"), nullable=True)
|
||||
target_user_id = Column(Integer, ForeignKey("mtgonline_users.id"), nullable=True)
|
||||
details = Column(Text, nullable=True)
|
||||
ip_address = Column(String(45), nullable=True)
|
||||
timestamp = Column(DateTime, server_default=func.now())
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
"""
|
||||
MTG Database Monitor
|
||||
|
||||
Monitors PostgreSQL database metrics for both cockatrice and mtgdata databases.
|
||||
Monitors PostgreSQL database metrics for both mtgonline and mtgdata databases.
|
||||
Tracks:
|
||||
- Database size and growth
|
||||
- Table sizes
|
||||
@@ -28,14 +28,14 @@ MTG_DB = settings.MTG_DATABASE_URL
|
||||
class MtgMonitor:
|
||||
def __init__(self):
|
||||
self.mtg_conn = None
|
||||
self.cockatrice_conn = None
|
||||
self.mtgonline_conn = None
|
||||
self.metrics = {}
|
||||
|
||||
async def connect(self):
|
||||
"""Establish connections to both databases."""
|
||||
try:
|
||||
self.mtg_conn = await asyncpg.connect(MTG_DB)
|
||||
self.cockatrice_conn = await asyncpg.connect(COCKATRICE_DB)
|
||||
self.mtgonline_conn = await asyncpg.connect(COCKATRICE_DB)
|
||||
return True
|
||||
except Exception as e:
|
||||
print(f"Connection error: {e}")
|
||||
@@ -45,8 +45,8 @@ class MtgMonitor:
|
||||
"""Close database connections."""
|
||||
if self.mtg_conn:
|
||||
await self.mtg_conn.close()
|
||||
if self.cockatrice_conn:
|
||||
await self.cockatrice_conn.close()
|
||||
if self.mtgonline_conn:
|
||||
await self.mtgonline_conn.close()
|
||||
|
||||
async def get_database_size(self) -> Dict[str, float]:
|
||||
"""Get size of databases in GB."""
|
||||
@@ -56,14 +56,14 @@ class MtgMonitor:
|
||||
SELECT pg_database_size(current_database()) as size
|
||||
""")
|
||||
|
||||
# Cockatrice database size
|
||||
cockatrice_size = await self.cockatrice_conn.fetchval("""
|
||||
# MTG Online database size
|
||||
mtgonline_size = await self.mtgonline_conn.fetchval("""
|
||||
SELECT pg_database_size(current_database()) as size
|
||||
""")
|
||||
|
||||
return {
|
||||
"mtgdata": mtg_size / (1024**3), # Convert to GB
|
||||
"cockatrice": cockatrice_size / (1024**3)
|
||||
"mtgonline": mtgonline_size / (1024**3)
|
||||
}
|
||||
except Exception as e:
|
||||
print(f"Error getting database sizes: {e}")
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
# Routers package
|
||||
@@ -21,7 +21,7 @@ async def list_games(
|
||||
):
|
||||
"""List active games."""
|
||||
# This would query a games table - simplified for now
|
||||
# In production, you'd have a CockatriceGames model
|
||||
# In production, you'd have a MTG OnlineGames model
|
||||
return []
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
# Games package
|
||||
@@ -0,0 +1 @@
|
||||
# Schemas package
|
||||
@@ -1,4 +1,4 @@
|
||||
"""Cockatrice protocol constants and message definitions."""
|
||||
"""MTG Online protocol constants and message definitions."""
|
||||
import enum
|
||||
|
||||
|
||||
|
||||
@@ -19,7 +19,7 @@ class CardInfo:
|
||||
|
||||
|
||||
class DeckParser:
|
||||
"""Parse Cockatrice deck list formats."""
|
||||
"""Parse MTG Online deck list formats."""
|
||||
|
||||
# Regex patterns for deck parsing
|
||||
CARD_LINE_RE = re.compile(r"^\s*[\w\[\(\{].*$")
|
||||
@@ -149,8 +149,8 @@ class DeckParser:
|
||||
return "\n".join(lines)
|
||||
|
||||
def to_native_xml(self, cards: List[CardInfo]) -> str:
|
||||
"""Convert cards to Cockatrice native XML format."""
|
||||
xml_lines = ['<cockatrice_deck version="1">']
|
||||
"""Convert cards to MTG Online native XML format."""
|
||||
xml_lines = ['<mtgonline_deck version="1">']
|
||||
|
||||
# Group cards by zone (main/sideboard)
|
||||
main_cards = [c for c in cards if not c.name.startswith("[SB]")]
|
||||
@@ -169,11 +169,11 @@ class DeckParser:
|
||||
xml_lines.append(f' <card name="{clean_name}" count="{card.count}" />')
|
||||
xml_lines.append(' </zone>')
|
||||
|
||||
xml_lines.append('</cockatrice_deck>')
|
||||
xml_lines.append('</mtgonline_deck>')
|
||||
return "\n".join(xml_lines)
|
||||
|
||||
def from_native_xml(self, xml: str) -> List[CardInfo]:
|
||||
"""Parse Cockatrice native XML format."""
|
||||
"""Parse MTG Online native XML format."""
|
||||
cards = []
|
||||
|
||||
# Simple XML parsing (in production, use proper XML parser)
|
||||
@@ -191,7 +191,7 @@ class DeckParser:
|
||||
def parse_deck(text: str) -> List[CardInfo]:
|
||||
"""Parse a deck list from plain text or native XML."""
|
||||
parser = DeckParser()
|
||||
if text.strip().startswith("<cockatrice_deck"):
|
||||
if text.strip().startswith("<mtgonline_deck"):
|
||||
return parser.from_native_xml(text)
|
||||
else:
|
||||
return parser.parse_plain_text(text)
|
||||
|
||||
@@ -239,7 +239,7 @@ async def game_websocket_endpoint(websocket: WebSocket, game_id: int):
|
||||
await room.send_to_player(player_id, {
|
||||
"type": "pong",
|
||||
"timestamp": datetime.now().isoformat(),
|
||||
}))
|
||||
})
|
||||
except WebSocketDisconnect:
|
||||
# Player disconnected
|
||||
await room.remove_player(player_id)
|
||||
@@ -297,7 +297,7 @@ async def process_game_command(room: GameRoom, player_id: int, command: dict):
|
||||
await room.send_to_player(player_id, {
|
||||
"type": "error",
|
||||
"message": f"Invalid command type: {cmd_type}",
|
||||
}))
|
||||
})
|
||||
return
|
||||
|
||||
# Broadcast command as game event
|
||||
|
||||
@@ -0,0 +1,223 @@
|
||||
"""
|
||||
Database initialization script for MTG Online backend.
|
||||
|
||||
Creates all necessary tables matching the SQLAlchemy ORM models
|
||||
for both databases.
|
||||
"""
|
||||
import asyncio
|
||||
from sqlalchemy.ext.asyncio import create_async_engine, AsyncSession
|
||||
from sqlalchemy.orm import sessionmaker
|
||||
from sqlalchemy import text
|
||||
from app.core.settings import get_settings
|
||||
|
||||
|
||||
async def setup_mtg_online_database():
|
||||
"""Create tables for the mtgonline database."""
|
||||
settings = get_settings()
|
||||
engine = create_async_engine(settings.MTGO_DATABASE_URL)
|
||||
async_session = sessionmaker(engine, class_=AsyncSession, expire_on_commit=False)
|
||||
|
||||
async with async_session() as session:
|
||||
# Create tables
|
||||
tables = [
|
||||
"""
|
||||
CREATE TABLE IF NOT EXISTS mtgonline_users (
|
||||
id SERIAL PRIMARY KEY,
|
||||
username VARCHAR(64) UNIQUE NOT NULL,
|
||||
password_hash VARCHAR(128) NOT NULL,
|
||||
salt VARCHAR(128) NOT NULL,
|
||||
email VARCHAR(255),
|
||||
country VARCHAR(2),
|
||||
real_name VARCHAR(128),
|
||||
avatar_bmp TEXT,
|
||||
privlevel VARCHAR(50) DEFAULT 'User',
|
||||
is_active BOOLEAN DEFAULT TRUE,
|
||||
is_banned BOOLEAN DEFAULT FALSE,
|
||||
ban_reason TEXT,
|
||||
ban_ends TIMESTAMP,
|
||||
vip_status INTEGER DEFAULT 0,
|
||||
vip_expiry TIMESTAMP,
|
||||
creation_date TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
last_login TIMESTAMP
|
||||
)
|
||||
""",
|
||||
"""
|
||||
CREATE TABLE IF NOT EXISTS mtgonline_decklist_folders (
|
||||
id SERIAL PRIMARY KEY,
|
||||
owner_id INTEGER REFERENCES mtgonline_users(id) ON DELETE CASCADE,
|
||||
name VARCHAR(255) NOT NULL,
|
||||
parent_id INTEGER REFERENCES mtgonline_decklist_folders(id) ON DELETE CASCADE,
|
||||
creation_date TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
)
|
||||
""",
|
||||
"""
|
||||
CREATE TABLE IF NOT EXISTS mtgonline_decklist_files (
|
||||
id SERIAL PRIMARY KEY,
|
||||
folder_id INTEGER REFERENCES mtgonline_decklist_folders(id) ON DELETE CASCADE,
|
||||
owner_id INTEGER REFERENCES mtgonline_users(id) ON DELETE CASCADE,
|
||||
name VARCHAR(255) NOT NULL,
|
||||
content TEXT NOT NULL,
|
||||
format VARCHAR(50) DEFAULT 'native',
|
||||
creation_date TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
)
|
||||
""",
|
||||
"""
|
||||
CREATE TABLE IF NOT EXISTS mtgonline_rooms (
|
||||
id SERIAL PRIMARY KEY,
|
||||
name VARCHAR(100) UNIQUE NOT NULL,
|
||||
description TEXT,
|
||||
is_password_protected BOOLEAN DEFAULT FALSE,
|
||||
password_hash VARCHAR(128),
|
||||
creation_date TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
)
|
||||
""",
|
||||
"""
|
||||
CREATE TABLE IF NOT EXISTS mtgonline_rooms_gametypes (
|
||||
id SERIAL PRIMARY KEY,
|
||||
room_id INTEGER REFERENCES mtgonline_rooms(id) ON DELETE CASCADE,
|
||||
name VARCHAR(100) NOT NULL,
|
||||
description TEXT
|
||||
)
|
||||
""",
|
||||
"""
|
||||
CREATE TABLE IF NOT EXISTS mtgonline_bans (
|
||||
id SERIAL PRIMARY KEY,
|
||||
user_id INTEGER REFERENCES mtgonline_users(id) ON DELETE CASCADE,
|
||||
server_id INTEGER,
|
||||
reason TEXT NOT NULL,
|
||||
moderators VARCHAR(255),
|
||||
ip_address VARCHAR(45),
|
||||
expiration_time TIMESTAMP,
|
||||
active BOOLEAN DEFAULT TRUE,
|
||||
creation_date TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
)
|
||||
""",
|
||||
"""
|
||||
CREATE TABLE IF NOT EXISTS mtgonline_log (
|
||||
id SERIAL PRIMARY KEY,
|
||||
room_id INTEGER REFERENCES mtgonline_rooms(id) ON DELETE CASCADE,
|
||||
player_id INTEGER REFERENCES mtgonline_users(id) ON DELETE CASCADE,
|
||||
message TEXT NOT NULL,
|
||||
timestamp TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
)
|
||||
""",
|
||||
"""
|
||||
CREATE TABLE IF NOT EXISTS mtgonline_audit (
|
||||
id SERIAL PRIMARY KEY,
|
||||
admin_id INTEGER REFERENCES mtgonline_users(id) ON DELETE SET NULL,
|
||||
action_type VARCHAR(50) NOT NULL,
|
||||
target_user_id INTEGER REFERENCES mtgonline_users(id) ON DELETE SET NULL,
|
||||
details TEXT,
|
||||
ip_address VARCHAR(45),
|
||||
timestamp TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
)
|
||||
""",
|
||||
]
|
||||
|
||||
for table_sql in tables:
|
||||
await session.execute(text(table_sql))
|
||||
|
||||
# Create indexes
|
||||
indexes = [
|
||||
"CREATE INDEX IF NOT EXISTS idx_decks_owner ON mtgonline_decklist_files(owner_id);",
|
||||
"CREATE INDEX IF NOT EXISTS idx_decks_folder ON mtgonline_decklist_files(folder_id);",
|
||||
"CREATE INDEX IF NOT EXISTS idx_bans_active ON mtgonline_bans(active);",
|
||||
"CREATE INDEX IF NOT EXISTS idx_log_timestamp ON mtgonline_log(timestamp);",
|
||||
]
|
||||
|
||||
for idx_sql in indexes:
|
||||
await session.execute(text(idx_sql))
|
||||
|
||||
print("✓ All mtgonline tables created")
|
||||
|
||||
await engine.dispose()
|
||||
|
||||
|
||||
async def setup_mtg_data_database():
|
||||
"""Create tables for the mtgdata database."""
|
||||
settings = get_settings()
|
||||
engine = create_async_engine(settings.MTG_DATABASE_URL)
|
||||
async_session = sessionmaker(engine, class_=AsyncSession, expire_on_commit=False)
|
||||
|
||||
async with async_session() as session:
|
||||
# Create tables
|
||||
tables = [
|
||||
"""
|
||||
CREATE TABLE IF NOT EXISTS mtg_sets (
|
||||
id SERIAL PRIMARY KEY,
|
||||
code VARCHAR(10) UNIQUE NOT NULL,
|
||||
name VARCHAR(255),
|
||||
type VARCHAR(100),
|
||||
release_date DATE,
|
||||
base_set_size INTEGER,
|
||||
total_size INTEGER,
|
||||
is_foil_only BOOLEAN,
|
||||
is_non_foil_only BOOLEAN,
|
||||
digital BOOLEAN,
|
||||
icon_svg_url TEXT,
|
||||
parent_code VARCHAR(10),
|
||||
mtgo_code VARCHAR(10),
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
)
|
||||
""",
|
||||
"""
|
||||
CREATE TABLE IF NOT EXISTS mtg_cards (
|
||||
id SERIAL PRIMARY KEY,
|
||||
set_id INTEGER REFERENCES mtg_sets(id) ON DELETE CASCADE,
|
||||
name VARCHAR(255),
|
||||
mana_cost VARCHAR(255),
|
||||
type_line VARCHAR(255),
|
||||
oracle_text TEXT,
|
||||
power VARCHAR(50),
|
||||
toughness VARCHAR(50),
|
||||
rarity VARCHAR(50),
|
||||
layout VARCHAR(50),
|
||||
artist VARCHAR(255),
|
||||
flavor_text TEXT,
|
||||
numbers VARCHAR(100),
|
||||
identifiers TEXT,
|
||||
images TEXT,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
)
|
||||
""",
|
||||
]
|
||||
|
||||
for table_sql in tables:
|
||||
await session.execute(text(table_sql))
|
||||
|
||||
# Create indexes
|
||||
indexes = [
|
||||
"CREATE INDEX IF NOT EXISTS idx_mtg_cards_name ON mtg_cards(name);",
|
||||
"CREATE INDEX IF NOT EXISTS idx_mtg_cards_type ON mtg_cards(type_line);",
|
||||
"CREATE INDEX IF NOT EXISTS idx_mtg_cards_rarity ON mtg_cards(rarity);",
|
||||
"CREATE INDEX IF NOT EXISTS idx_mtg_cards_set_id ON mtg_cards(set_id);",
|
||||
]
|
||||
|
||||
for idx_sql in indexes:
|
||||
await session.execute(text(idx_sql))
|
||||
|
||||
print("✓ All mtgdata tables created")
|
||||
|
||||
await engine.dispose()
|
||||
|
||||
|
||||
async def main():
|
||||
"""Main initialization function."""
|
||||
print("MTG Online Database Initialization")
|
||||
print("=" * 50)
|
||||
|
||||
# Setup mtgonline database
|
||||
print("\nSetting up mtgonline database...")
|
||||
await setup_mtg_online_database()
|
||||
|
||||
# Setup mtgdata database
|
||||
print("\nSetting up mtgdata database...")
|
||||
await setup_mtg_data_database()
|
||||
|
||||
print("\n✓ Database initialization complete!")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(main())
|
||||
+26
-16
@@ -3,7 +3,7 @@
|
||||
Backend System Test Script
|
||||
|
||||
Tests all backend components:
|
||||
- Database connections (Cockatrice + MTG)
|
||||
- Database connections (MTG Online + MTG)
|
||||
- Redis connection and caching
|
||||
- Card database queries
|
||||
- API endpoints
|
||||
@@ -21,10 +21,20 @@ from sqlalchemy.ext.asyncio import create_async_engine, AsyncSession
|
||||
from sqlalchemy import text
|
||||
|
||||
# Database URLs
|
||||
COCKATRICE_DB = "postgresql+asyncpg://cockatrice:cockatrice_pass@localhost:5432/cockatrice"
|
||||
MTG_DB = "postgresql+asyncpg://cockatrice:cockatrice_pass@localhost:5432/mtgdata"
|
||||
COCKATRICE_DB = "postgresql+asyncpg://mtgonline:mtgonline_pass@localhost:5432/mtgonline"
|
||||
MTG_DB = "postgresql+asyncpg://mtgonline:mtgonline_pass@localhost:5432/mtgdata"
|
||||
REDIS_URL = "redis://localhost:6379/0"
|
||||
|
||||
# Test URLs - detect environment
|
||||
import socket
|
||||
try:
|
||||
# Try Docker network first
|
||||
socket.getaddrinfo('mtg_backend', 8000)
|
||||
API_BASE = 'http://mtg_backend:8000'
|
||||
except:
|
||||
# Fall back to host port
|
||||
API_BASE = 'http://localhost:8001'
|
||||
print(f"Using API base: {API_BASE}")
|
||||
results = {
|
||||
"tests_run": 0,
|
||||
"tests_passed": 0,
|
||||
@@ -97,29 +107,29 @@ async def test_cache_operations() -> bool:
|
||||
print(f"✗ Redis cache operations: FAILED - {str(e)}")
|
||||
return False
|
||||
|
||||
async def test_cockatrice_tables() -> bool:
|
||||
"""Test Cockatrice database tables."""
|
||||
async def test_mtgonline_tables() -> bool:
|
||||
"""Test MTG Online database tables."""
|
||||
results["tests_run"] += 1
|
||||
try:
|
||||
engine = create_async_engine(COCKATRICE_DB, echo=False)
|
||||
async with engine.connect() as conn:
|
||||
# Test users table
|
||||
result = await conn.execute(text("SELECT COUNT(*) FROM cockatrice_users"))
|
||||
result = await conn.execute(text("SELECT COUNT(*) FROM mtgonline_users"))
|
||||
user_count = result.scalar()
|
||||
|
||||
# Test decks table
|
||||
result = await conn.execute(text("SELECT COUNT(*) FROM cockatrice_decklist_files"))
|
||||
result = await conn.execute(text("SELECT COUNT(*) FROM mtgonline_decklist_files"))
|
||||
deck_count = result.scalar()
|
||||
|
||||
await engine.dispose()
|
||||
|
||||
results["tests_passed"] += 1
|
||||
print(f"✓ Cockatrice tables: OK (users: {user_count}, decks: {deck_count})")
|
||||
print(f"✓ MTG Online tables: OK (users: {user_count}, decks: {deck_count})")
|
||||
return True
|
||||
except Exception as e:
|
||||
results["tests_failed"] += 1
|
||||
results["failures"].append(f"Cockatrice tables: {str(e)}")
|
||||
print(f"✗ Cockatrice tables: FAILED - {str(e)}")
|
||||
results["failures"].append(f"MTG Online tables: {str(e)}")
|
||||
print(f"✗ MTG Online tables: FAILED - {str(e)}")
|
||||
return False
|
||||
|
||||
async def test_mtg_tables() -> bool:
|
||||
@@ -181,7 +191,7 @@ async def test_health_endpoint() -> bool:
|
||||
results["tests_run"] += 1
|
||||
try:
|
||||
import requests
|
||||
response = requests.get("http://localhost:8000/health", timeout=5)
|
||||
response = requests.get("http://localhost:8001/health", timeout=5)
|
||||
|
||||
if response.status_code == 200:
|
||||
results["tests_passed"] += 1
|
||||
@@ -204,7 +214,7 @@ async def test_card_search_endpoint() -> bool:
|
||||
try:
|
||||
import requests
|
||||
response = requests.get(
|
||||
"http://localhost:8000/mtg/cards/search",
|
||||
"http://localhost:8001/api/v1/mtg/cards/search",
|
||||
params={"q": "Lightning Bolt", "limit": 5},
|
||||
timeout=10
|
||||
)
|
||||
@@ -231,7 +241,7 @@ async def test_statistics_endpoint() -> bool:
|
||||
try:
|
||||
import requests
|
||||
response = requests.get(
|
||||
"http://localhost:8000/mtg/cards/statistics",
|
||||
"http://localhost:8001/api/v1/mtg/cards/statistics",
|
||||
timeout=10
|
||||
)
|
||||
|
||||
@@ -258,7 +268,7 @@ async def test_auth_endpoint() -> bool:
|
||||
try:
|
||||
import requests
|
||||
response = requests.post(
|
||||
"http://localhost:8000/auth/register",
|
||||
"http://localhost:8001/api/v1/auth/register",
|
||||
json={
|
||||
"username": "test_user",
|
||||
"email": "test@example.com",
|
||||
@@ -295,7 +305,7 @@ async def run_all_tests():
|
||||
# Database connections
|
||||
print("Testing Database Connections:")
|
||||
print("-" * 60)
|
||||
await test_connection(COCKATRICE_DB, "Cockatrice PostgreSQL")
|
||||
await test_connection(COCKATRICE_DB, "MTG Online PostgreSQL")
|
||||
await test_connection(MTG_DB, "MTG PostgreSQL")
|
||||
await test_redis()
|
||||
print()
|
||||
@@ -309,7 +319,7 @@ async def run_all_tests():
|
||||
# Database tables
|
||||
print("Testing Database Tables:")
|
||||
print("-" * 60)
|
||||
await test_cockatrice_tables()
|
||||
await test_mtgonline_tables()
|
||||
await test_mtg_tables()
|
||||
await test_mtg_card_query()
|
||||
print()
|
||||
|
||||
Reference in New Issue
Block a user