diff --git a/backend/app/core/database.py b/backend/app/core/database.py index 16b0f82..0b77b77 100644 --- a/backend/app/core/database.py +++ b/backend/app/core/database.py @@ -29,6 +29,9 @@ class Base(DeclarativeBase): pass +__all__ = ["Base", "get_db", "async_session", "engine"] + + async def get_db() -> AsyncSession: """FastAPI dependency that provides a database session.""" async with async_session() as session: diff --git a/backend/app/models/models.py b/backend/app/models/models.py index 0ab5ddd..7948ae7 100644 --- a/backend/app/models/models.py +++ b/backend/app/models/models.py @@ -15,7 +15,7 @@ class User(Base): id = Column(Integer, primary_key=True, index=True) username = Column(String(64), unique=True, nullable=False, index=True) - password_sha512 = Column(String(128), nullable=False) # bcrypt hash + password_hash = Column(String(128), nullable=False) # bcrypt hash salt = Column(String(128), nullable=False) # password salt email = Column(String(255), nullable=True, index=True) country = Column(String(2), nullable=True) diff --git a/backend/app/routers/admin.py b/backend/app/routers/admin.py index f89e8e0..15ce3b4 100644 --- a/backend/app/routers/admin.py +++ b/backend/app/routers/admin.py @@ -184,7 +184,7 @@ async def list_logs( ] -@router.post("/audit", response_model=AuditLog) +@router.post("/audit", response_model=dict) async def log_audit( action_type: str, target_user_id: Optional[int] = None, @@ -210,4 +210,11 @@ async def log_audit( db.add(new_audit) await db.flush() - return AuditLog.model_validate(new_audit) + return { + "id": new_audit.id, + "admin_id": new_audit.admin_id, + "action_type": new_audit.action_type, + "target_user_id": new_audit.target_user_id, + "details": new_audit.details, + "timestamp": new_audit.timestamp, + } diff --git a/backend/app/routers/auth.py b/backend/app/routers/auth.py index 7c347bb..6d3a7e9 100644 --- a/backend/app/routers/auth.py +++ b/backend/app/routers/auth.py @@ -4,6 +4,8 @@ from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy import select from typing import Optional +from datetime import datetime + from app.core.database import get_db from app.core.security import ( verify_password, @@ -33,7 +35,7 @@ async def login(request: LoginRequest, db: AsyncSession = Depends(get_db)): result = await db.execute(stmt) user = result.scalar_one_or_none() - if not user or not verify_password(request.password, user.password_sha512): + if not user or not verify_password(request.password, user.password_hash): raise HTTPException( status_code=status.HTTP_401_UNAUTHORIZED, detail="Invalid username or password", @@ -120,7 +122,7 @@ async def register(request: UserCreate, db: AsyncSession = Depends(get_db)): # Create new user new_user = User( username=request.username, - password_sha512=hash_password(request.password), + password_hash=hash_password(request.password), salt="random_salt", # In production, generate random salt email=request.email, country=request.country, diff --git a/backend/app/routers/users.py b/backend/app/routers/users.py index 4e7fb77..967805c 100644 --- a/backend/app/routers/users.py +++ b/backend/app/routers/users.py @@ -62,7 +62,7 @@ async def update_user( # Hash new password if provided if "new_password" in update_data: - update_data["password_sha512"] = hash_password(update_data.pop("new_password")) + update_data["password_hash"] = hash_password(update_data.pop("new_password")) # Update user stmt = ( diff --git a/backend/app/services/game_server.py b/backend/app/services/game_server.py index 160029f..bba48d5 100644 --- a/backend/app/services/game_server.py +++ b/backend/app/services/game_server.py @@ -236,7 +236,7 @@ async def game_websocket_endpoint(websocket: WebSocket, game_id: int): }) elif message.get("type") == "ping": # Respond to ping - await websocket.send_text(json.dumps({ + await room.send_to_player(player_id, { "type": "pong", "timestamp": datetime.now().isoformat(), })) @@ -294,7 +294,7 @@ async def process_game_command(room: GameRoom, player_id: int, command: dict): GAME_COMMAND_JUDGE, GAME_COMMAND_REVERSE_TURN, ]: - await websocket.send_text(json.dumps({ + await room.send_to_player(player_id, { "type": "error", "message": f"Invalid command type: {cmd_type}", })) diff --git a/backend/pyproject.toml b/backend/pyproject.toml index 1c9a750..41b4d92 100644 --- a/backend/pyproject.toml +++ b/backend/pyproject.toml @@ -1,6 +1,4 @@ -""" -Ruff configuration for linting and formatting. -""" +# Ruff configuration for linting and formatting [tool.ruff] # Target Python version target-version = "py312" diff --git a/backend/tests/conftest.py b/backend/tests/conftest.py index 210a9d1..5fae98a 100644 --- a/backend/tests/conftest.py +++ b/backend/tests/conftest.py @@ -1,122 +1,119 @@ -""" -Pytest configuration and fixtures for Cockatrice Web Application tests. -""" +"""Test configuration and fixtures.""" import pytest -import pytest_asyncio -from httpx import AsyncClient, ASGITransport -from sqlalchemy.ext.asyncio import AsyncSession, create_async_engine, async_sessionmaker -from sqlalchemy.orm import sessionmaker -from sqlalchemy.pool import StaticPool +import asyncio +from httpx import AsyncClient +from sqlalchemy.ext.asyncio import create_async_engine, AsyncSession, async_sessionmaker from app.main import app -from app.core.database import Base, get_db -from app.core.security import hash_password -from app.models.models import User +from app.core.database import get_db, Base -# Test database URL (in-memory SQLite for tests) -TEST_DATABASE_URL = "sqlite+aiosqlite:///:memory:" +# Test database URL (use SQLite for testing) +TEST_DATABASE_URL = "sqlite+aiosqlite:///./test.db" -# Create test engine and session -test_engine = create_async_engine( - TEST_DATABASE_URL, - connect_args={"check_same_thread": False}, - poolclass=StaticPool, -) - -TestSessionLocal = async_sessionmaker( - bind=test_engine, +# Create test engine and session factory +test_engine = create_async_engine(TEST_DATABASE_URL, echo=False) +TestAsyncSessionLocal = async_sessionmaker( + test_engine, class_=AsyncSession, expire_on_commit=False, ) @pytest.fixture(scope="session") -def event_loop(): - """Create an instance of the default event loop for the test session.""" - import asyncio - loop = asyncio.new_event_loop() - yield loop - loop.close() +def anyio_backend(): + """Specify the backend for anyio (pytest-asyncio).""" + return "asyncio" -@pytest_asyncio.fixture(scope="function") -async def db_session(): - """Create a new database session for each test.""" +@pytest.fixture(scope="session") +async def database(): + """Create and drop database tables for testing.""" async with test_engine.begin() as conn: await conn.run_sync(Base.metadata.create_all) - - async with TestSessionLocal() as session: - yield session - + yield async with test_engine.begin() as conn: await conn.run_sync(Base.metadata.drop_all) -@pytest_asyncio.fixture(scope="function") +@pytest.fixture +async def db_session(database): + """Create a new database session for each test.""" + async with TestAsyncSessionLocal() as session: + yield session + + +@pytest.fixture async def client(db_session): - """Create a test client with database dependency override.""" + """Create a test client with database session.""" async def override_get_db(): - try: - yield db_session - finally: - pass - + yield db_session + app.dependency_overrides[get_db] = override_get_db - - async with AsyncClient( - transport=ASGITransport(app=app), - base_url="http://test", - ) as ac: + + async with AsyncClient(app=app, base_url="http://test") as ac: yield ac - + app.dependency_overrides.clear() -@pytest_asyncio.fixture(scope="function") +@pytest.fixture async def test_user(db_session): """Create a test user.""" + from app.models.models import User + from app.core.security import hash_password + user = User( username="testuser", - password_sha512=hash_password("testpassword123"), - salt="test_salt", + password_hash=hash_password("testpassword123"), email="test@example.com", - country="US", - privlevel="User", + is_active=True, ) db_session.add(user) - await db_session.flush() + await db_session.commit() + await db_session.refresh(user) return user -@pytest_asyncio.fixture(scope="function") +@pytest.fixture async def admin_user(db_session): """Create an admin test user.""" + from app.models.models import User + from app.core.security import hash_password + user = User( - username="adminuser", - password_sha512=hash_password("adminpassword123"), - salt="admin_salt", + username="admin", + password_hash=hash_password("adminpassword123"), email="admin@example.com", - country="US", privlevel="Admin", + is_active=True, ) db_session.add(user) - await db_session.flush() + await db_session.commit() + await db_session.refresh(user) return user -@pytest_asyncio.fixture(scope="function") -async def auth_headers(client, test_user): - """Get authentication headers for test user.""" - from app.core.security import create_access_token, create_refresh_token - +@pytest.fixture +def auth_headers(test_user): + """Get auth headers for test user.""" + from app.core.security import create_access_token + access_token = create_access_token(str(test_user.id)) return {"Authorization": f"Bearer {access_token}"} -@pytest_asyncio.fixture(scope="function") -async def admin_headers(client, admin_user): - """Get authentication headers for admin user.""" +@pytest.fixture +def admin_headers(admin_user): + """Get auth headers for admin user.""" from app.core.security import create_access_token - + access_token = create_access_token(str(admin_user.id)) return {"Authorization": f"Bearer {access_token}"} + + +@pytest.fixture +def refresh_token(test_user): + """Get a refresh token for test user.""" + from app.core.security import create_refresh_token + + return create_refresh_token(str(test_user.id))