From 6d5051bbb22b8a6e5e5afb2dd043e0af2587d615 Mon Sep 17 00:00:00 2001 From: coder-bot Date: Wed, 19 Aug 2026 20:23:02 +0000 Subject: [PATCH] WIP checkpoint: Implementation: Login/Register UI wired to /api/auth --- backend/tests/test_auth_endpoints.py | 241 +++++++++++++++++++++++++++ scripts/smoke_test_auth.gd | 133 +++++++++++++++ 2 files changed, 374 insertions(+) create mode 100644 backend/tests/test_auth_endpoints.py create mode 100644 scripts/smoke_test_auth.gd diff --git a/backend/tests/test_auth_endpoints.py b/backend/tests/test_auth_endpoints.py new file mode 100644 index 0000000..cd0d463 --- /dev/null +++ b/backend/tests/test_auth_endpoints.py @@ -0,0 +1,241 @@ +"""Integration tests for the auth endpoints (register + login). + +These tests hit the real FastAPI app with an in-memory SQLite database, +so they exercise the full stack: Pydantic validation → DB → JWT → response. +""" + +import pytest +from fastapi.testclient import TestClient + +from app.main import app +from app.database import get_db, override_get_db +from app.routers.auth import pwd_context + + +# --------------------------------------------------------------------------- +# Test fixtures +# --------------------------------------------------------------------------- + +@pytest.fixture(autouse=True) +def _override_db(): + """Use an in-memory SQLite DB for every test.""" + from app.database import engine, Base, async_session_factory + + Base.metadata.create_all(bind=engine) + from app.database import SessionLocal + + async def _get_db(): + async_session = async_session_factory() + try: + yield async_session + await async_session.commit() + except Exception: + await async_session.rollback() + raise + finally: + await async_session.close() + + app.dependency_overrides[get_db] = _get_db + yield + app.dependency_overrides.clear() + + +# --------------------------------------------------------------------------- +# Register endpoint +# --------------------------------------------------------------------------- + +class TestRegister: + """Tests for POST /api/auth/register.""" + + def test_register_success(self): + """A valid registration returns 201 with a token.""" + client = TestClient(app) + resp = client.post( + "/api/auth/register", + json={ + "username": "newuser", + "email": "new@example.com", + "password": "secret123", + "date_of_birth": "2000-01-01", + }, + ) + assert resp.status_code == 201 + data = resp.json() + assert "access_token" in data + assert data["token_type"] == "bearer" + + def test_register_rejects_underage(self): + """Users under 18 get a 400 with a detail message.""" + client = TestClient(app) + resp = client.post( + "/api/auth/register", + json={ + "username": "kid", + "email": "kid@example.com", + "password": "secret123", + "date_of_birth": "2010-06-15", # ~15 years old + }, + ) + assert resp.status_code == 400 + assert "18 or older" in resp.json()["detail"] + + def test_register_rejects_duplicate_username(self): + """Registering the same username twice returns 409.""" + client = TestClient(app) + # First registration + client.post( + "/api/auth/register", + json={ + "username": "duplicate", + "email": "dup1@example.com", + "password": "secret123", + "date_of_birth": "2000-01-01", + }, + ) + # Second registration with same username, different email + resp = client.post( + "/api/auth/register", + json={ + "username": "duplicate", + "email": "dup2@example.com", + "password": "secret123", + "date_of_birth": "2000-01-01", + }, + ) + assert resp.status_code == 409 + assert "Username already registered" in resp.json()["detail"] + + def test_register_rejects_duplicate_email(self): + """Registering the same email twice returns 409.""" + client = TestClient(app) + client.post( + "/api/auth/register", + json={ + "username": "user1", + "email": "same@example.com", + "password": "secret123", + "date_of_birth": "2000-01-01", + }, + ) + resp = client.post( + "/api/auth/register", + json={ + "username": "user2", + "email": "same@example.com", + "password": "secret123", + "date_of_birth": "2000-01-01", + }, + ) + assert resp.status_code == 409 + assert "Email already registered" in resp.json()["detail"] + + def test_register_invalid_date_format(self): + """Invalid date format returns 422 validation error.""" + client = TestClient(app) + resp = client.post( + "/api/auth/register", + json={ + "username": "user", + "email": "user@example.com", + "password": "secret123", + "date_of_birth": "01-01-2000", # wrong format + }, + ) + assert resp.status_code == 422 + + def test_register_exactly_18(self): + """A user who just turned 18 can register.""" + client = TestClient(app) + resp = client.post( + "/api/auth/register", + json={ + "username": "just18", + "email": "just18@example.com", + "password": "secret123", + "date_of_birth": "2007-06-15", + }, + ) + assert resp.status_code == 201 + + +# --------------------------------------------------------------------------- +# Login endpoint +# --------------------------------------------------------------------------- + +class TestLogin: + """Tests for POST /api/auth/login.""" + + def _create_user(self, username: str, password: str, email: str = None): + """Helper to create a user via register.""" + client = TestClient(app) + if email is None: + email = f"{username}@example.com" + resp = client.post( + "/api/auth/register", + json={ + "username": username, + "email": email, + "password": password, + "date_of_birth": "2000-01-01", + }, + ) + assert resp.status_code == 201 + return resp.json()["access_token"] + + def test_login_success(self): + """Correct credentials return a token.""" + self._create_user("loginuser", "correctpass") + client = TestClient(app) + resp = client.post( + "/api/auth/login", + data={"username": "loginuser", "password": "correctpass"}, + ) + assert resp.status_code == 200 + data = resp.json() + assert "access_token" in data + assert data["token_type"] == "bearer" + + def test_login_wrong_password(self): + """Wrong password returns 401.""" + self._create_user("loginuser", "correctpass") + client = TestClient(app) + resp = client.post( + "/api/auth/login", + data={"username": "loginuser", "password": "wrongpass"}, + ) + assert resp.status_code == 401 + assert "Incorrect username or password" in resp.json()["detail"] + + def test_login_nonexistent_user(self): + """Non-existent user returns 401 (not 404).""" + client = TestClient(app) + resp = client.post( + "/api/auth/login", + data={"username": "nobody", "password": "any"}, + ) + assert resp.status_code == 401 + assert "Incorrect username or password" in resp.json()["detail"] + + def test_login_accepts_form_encoded(self): + """Login accepts application/x-www-form-urlencoded (OAuth2).""" + self._create_user("formuser", "formpass") + client = TestClient(app) + resp = client.post( + "/api/auth/login", + data={"username": "formuser", "password": "formpass"}, + ) + assert resp.status_code == 200 + assert "access_token" in resp.json() + + def test_login_token_is_valid_jwt(self): + """The returned token is a valid JWT that can be decoded.""" + self._create_user("jwttoken", "jwt123") + client = TestClient(app) + resp = client.post( + "/api/auth/login", + data={"username": "jwttoken", "password": "jwt123"}, + ) + token = resp.json()["access_token"] + # Verify it's a 3-part JWT + parts = token.split(".") + assert len(parts) == 3 diff --git a/scripts/smoke_test_auth.gd b/scripts/smoke_test_auth.gd new file mode 100644 index 0000000..63dfc22 --- /dev/null +++ b/scripts/smoke_test_auth.gd @@ -0,0 +1,133 @@ +extends SceneTree +## Headless smoke test for the AuthPanel login/register flow. +## +## Run with: +## godot --headless --path . --script res://scripts/smoke_test_auth.gd +## +## Requires the backend to be running (default base URL +## `application/agp/api_base_url` -> http://127.0.0.1:8000). +## +## Tests: +## 1. Register a unique user → 201 + token stored. +## 2. Register the same user again → 409 "Username already registered". +## 3. Register an under-18 user → 400 "Must be 18 or older". +## 4. Login with correct credentials → 200 + token stored. +## 5. Login with wrong password → 401 "Incorrect username or password". +## 6. Login with non-existent user → 401 "Incorrect username or password". +## +## Exits 0 on full pass, 1 on any failure. + +const API_BASE := "http://127.0.0.1:8000" +const TEST_USERNAME := "smoke_test_user_%d" +const TEST_EMAIL := "smoke_%d@test.example.com" +const TEST_PASSWORD := "SmokeTest123!" +const TEST_DOB_ADULT := "2000-01-01" +const TEST_DOB_CHILD := "2020-01-01" # ~5 years old + + +func _initialize() -> void: + _run() + + +func _run() -> void: + # Wait for autoloads to be ready. + await process_frame + + var api := root.get_node("ApiClient") as ApiClient + assert(api != null, "ApiClient autoload not found") + + var passed: int = 0 + var failed: int = 0 + + # --- Test 1: Register a unique user --- + var unique_id := Time.get_ticks_msec() + var username := "smoke_%d" % unique_id + var email := "smoke_%d@test.example.com" % unique_id + + print("\n=== Test 1: Register unique user ===") + var result := await api.register(username, email, TEST_PASSWORD, TEST_DOB_ADULT) + if _check("register returns ok", result.ok) \ + and _check("register status is 201", result.status == 201) \ + and _check("register stores token", api.has_token()): + passed += 1 + else: + failed += 1 + print(" detail: ", result.detail) + + # --- Test 2: Register same user again (duplicate) --- + print("\n=== Test 2: Duplicate username ===") + result = await api.register(username, email, TEST_PASSWORD, TEST_DOB_ADULT) + if _check("duplicate register not ok", not result.ok) \ + and _check("duplicate status is 409", result.status == 409) \ + and _check("duplicate detail mentions username", + "username" in result.detail.to_lower()): + passed += 1 + else: + failed += 1 + print(" detail: ", result.detail) + + # --- Test 3: Register under-18 user --- + print("\n=== Test 3: Under-18 registration ===") + var child_username := "child_%d" % unique_id + var child_email := "child_%d@test.example.com" % unique_id + result = await api.register(child_username, child_email, TEST_PASSWORD, TEST_DOB_CHILD) + if _check("under-18 register not ok", not result.ok) \ + and _check("under-18 status is 400", result.status == 400) \ + and _check("under-18 detail mentions age", + "18" in result.detail): + passed += 1 + else: + failed += 1 + print(" detail: ", result.detail) + + # --- Test 4: Login with correct credentials --- + print("\n=== Test 4: Correct login ===") + result = await api.login(username, TEST_PASSWORD) + if _check("login returns ok", result.ok) \ + and _check("login status is 200", result.status == 200) \ + and _check("login stores token", api.has_token()): + passed += 1 + else: + failed += 1 + print(" detail: ", result.detail) + + # --- Test 5: Login with wrong password --- + print("\n=== Test 5: Wrong password ===") + result = await api.login(username, "WrongPassword!") + if _check("wrong pwd login not ok", not result.ok) \ + and _check("wrong pwd status is 401", result.status == 401) \ + and _check("wrong pwd detail", + "Incorrect" in result.detail or "incorrect" in result.detail): + passed += 1 + else: + failed += 1 + print(" detail: ", result.detail) + + # --- Test 6: Login with non-existent user --- + print("\n=== Test 6: Non-existent user ===") + result = await api.login("nonexistent_user_xyz", TEST_PASSWORD) + if _check("missing user login not ok", not result.ok) \ + and _check("missing user status is 401", result.status == 401) \ + and _check("missing user detail", + "Incorrect" in result.detail or "incorrect" in result.detail): + passed += 1 + else: + failed += 1 + print(" detail: ", result.detail) + + # --- Summary --- + print("\n=== SMOKE RESULT: %d/%d passed ===" % [passed, passed + failed]) + if failed > 0: + quit(1) + else: + print("SMOKE RESULT: ALL PASS") + quit(0) + + +func _check(description: String, condition: bool) -> bool: + if condition: + print(" PASS: ", description) + return true + else: + printerr(" FAIL: ", description) + return false