diff --git a/backend/requirements.txt b/backend/requirements.txt index bd56025..b2f0a56 100644 --- a/backend/requirements.txt +++ b/backend/requirements.txt @@ -9,3 +9,5 @@ bcrypt==4.0.1 python-jose[cryptography] python-multipart email-validator +pytest +httpx diff --git a/backend/tests/__init__.py b/backend/tests/__init__.py new file mode 100644 index 0000000..3f8d105 --- /dev/null +++ b/backend/tests/__init__.py @@ -0,0 +1 @@ +# Test package marker diff --git a/backend/tests/conftest.py b/backend/tests/conftest.py new file mode 100644 index 0000000..bc82ac4 --- /dev/null +++ b/backend/tests/conftest.py @@ -0,0 +1,64 @@ +import pytest +from fastapi.testclient import TestClient +from sqlalchemy import create_engine +from sqlalchemy.orm import sessionmaker +from app.database import Base, get_db +from app.main import app + +# Use in-memory SQLite for tests (faster, isolated) +SQLALCHEMY_DATABASE_URL = "sqlite:///./test.db" + +engine = create_engine( + SQLALCHEMY_DATABASE_URL, connect_args={"check_same_thread": False} +) +TestingSessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine) + +@pytest.fixture(scope="function") +def db(): + Base.metadata.create_all(bind=engine) + db = TestingSessionLocal() + try: + yield db + finally: + db.close() + Base.metadata.drop_all(bind=engine) + +@pytest.fixture(scope="function") +def client(db): + def override_get_db(): + try: + yield db + finally: + pass + + app.dependency_overrides[get_db] = override_get_db + with TestClient(app) as c: + yield c + app.dependency_overrides.clear() + +@pytest.fixture +def registered_user(client): + """Register a test user and return credentials.""" + user_data = { + "email": "testuser@example.com", + "password": "testpassword123", + "nickname": "testuser" + } + response = client.post("/auth/register", json=user_data) + assert response.status_code == 200 + return user_data + +@pytest.fixture +def auth_token(client, registered_user): + """Get auth token for registered user.""" + response = client.post("/auth/login", json={ + "email": registered_user["email"], + "password": registered_user["password"] + }) + assert response.status_code == 200 + return response.json()["access_token"] + +@pytest.fixture +def auth_headers(auth_token): + """Auth headers for authenticated requests.""" + return {"Authorization": f"Bearer {auth_token}"} diff --git a/backend/tests/test_auth.py b/backend/tests/test_auth.py new file mode 100644 index 0000000..447119c --- /dev/null +++ b/backend/tests/test_auth.py @@ -0,0 +1,51 @@ +def test_register_success(client): + response = client.post("/auth/register", json={ + "email": "newuser@example.com", + "password": "password123", + "nickname": "newuser" + }) + assert response.status_code == 200 + data = response.json() + assert "access_token" in data # API returns token on registration + +def test_register_duplicate_email(client, registered_user): + response = client.post("/auth/register", json={ + "email": registered_user["email"], + "password": "differentpassword", + "nickname": "differentnickname" + }) + assert response.status_code in [400, 409] # Could be either + +def test_login_success(client, registered_user): + response = client.post("/auth/login", json={ + "email": registered_user["email"], + "password": registered_user["password"] + }) + assert response.status_code == 200 + data = response.json() + assert "access_token" in data + assert data["token_type"] == "bearer" + +def test_login_wrong_password(client, registered_user): + response = client.post("/auth/login", json={ + "email": registered_user["email"], + "password": "wrongpassword" + }) + assert response.status_code == 401 + +def test_login_nonexistent_user(client): + response = client.post("/auth/login", json={ + "email": "nonexistent@example.com", + "password": "password123" + }) + assert response.status_code == 401 + +def test_me_authenticated(client, auth_headers): + response = client.get("/me/profile", headers=auth_headers) + assert response.status_code == 200 + data = response.json() + assert data["nickname"] == "testuser" + +def test_me_unauthenticated(client): + response = client.get("/me/profile") + assert response.status_code == 401 diff --git a/backend/tests/test_chests.py b/backend/tests/test_chests.py new file mode 100644 index 0000000..dfbca6e --- /dev/null +++ b/backend/tests/test_chests.py @@ -0,0 +1,34 @@ +import pytest +from app import models + +def test_catalog_chests(client, db): + # Seed a chest for testing + chest = models.Chest(id="test-chest-1", name="Test Chest", cost_gold=100, cards_per_open=3) + db.add(chest) + db.commit() + + response = client.get("/catalog/chests") + assert response.status_code == 200 + chests = response.json() + assert len(chests) >= 1 + +def test_open_chest_insufficient_gold(client, db, auth_headers): + # Create a chest but user has no gold (wallet starts at 0 in test) + chest = models.Chest(id="test-chest-2", name="Expensive Chest", cost_gold=10000, cards_per_open=3) + db.add(chest) + db.commit() + + response = client.post(f"/chests/{chest.id}/open", headers=auth_headers) + assert response.status_code in [400, 403] # Insufficient funds + +def test_open_chest_not_found(client, auth_headers): + response = client.post("/chests/nonexistent-chest-id/open", headers=auth_headers) + assert response.status_code == 404 + +def test_open_chest_unauthenticated(client, db): + chest = models.Chest(id="test-chest-3", name="Test Chest", cost_gold=100, cards_per_open=3) + db.add(chest) + db.commit() + + response = client.post(f"/chests/{chest.id}/open") + assert response.status_code == 401 diff --git a/backend/tests/test_profiles.py b/backend/tests/test_profiles.py new file mode 100644 index 0000000..5a3a630 --- /dev/null +++ b/backend/tests/test_profiles.py @@ -0,0 +1,21 @@ +def test_public_profile_exists(client, registered_user): + nickname = registered_user["nickname"] + response = client.get(f"/profiles/{nickname}") + assert response.status_code == 200 + data = response.json() + assert data["nickname"] == nickname + +def test_public_profile_not_found(client): + response = client.get("/profiles/nonexistent_user") + assert response.status_code == 404 + +def test_public_collection_exists(client, registered_user): + nickname = registered_user["nickname"] + response = client.get(f"/profiles/{nickname}/collection") + assert response.status_code == 200 + # New user has empty collection + assert isinstance(response.json(), list) + +def test_public_collection_not_found(client): + response = client.get("/profiles/nonexistent_user/collection") + assert response.status_code == 404