# tests/test_auth.py """Authentication endpoint tests — comprehensive edge case coverage.""" import pytest # ────────────────────────────────────────────── # Registration # ────────────────────────────────────────────── def test_register_success(client): resp = client.post("/api/auth/register", json={ "user_id": "newuser", "name": "New User", "password": "TestPass1", }) assert resp.status_code == 201 data = resp.json() assert data["user_id"] == "newuser" assert "access_token" in data assert "refresh_token" in data assert data["token_type"] == "bearer" def test_register_duplicate_username(client): payload = {"user_id": "dupuser", "password": "TestPass1"} client.post("/api/auth/register", json=payload) resp = client.post("/api/auth/register", json=payload) assert resp.status_code == 409 assert "taken" in resp.json()["detail"].lower() def test_register_duplicate_email(client): client.post("/api/auth/register", json={ "user_id": "user1", "email": "dup@test.com", "password": "TestPass1", }) resp = client.post("/api/auth/register", json={ "user_id": "user2", "email": "dup@test.com", "password": "TestPass1", }) assert resp.status_code == 409 def test_register_weak_password_too_short(client): resp = client.post("/api/auth/register", json={ "user_id": "weakpwduser", "password": "abc", }) assert resp.status_code == 422 def test_register_weak_password_no_uppercase(client): resp = client.post("/api/auth/register", json={ "user_id": "weakpwduser", "password": "testpass1", # no uppercase }) assert resp.status_code == 422 def test_register_weak_password_no_digit(client): resp = client.post("/api/auth/register", json={ "user_id": "weakpwduser", "password": "TestPassword", # no digit }) assert resp.status_code == 422 def test_register_invalid_user_id_chars(client): resp = client.post("/api/auth/register", json={ "user_id": "bad user!", "password": "TestPass1", }) assert resp.status_code == 422 def test_register_user_id_too_short(client): resp = client.post("/api/auth/register", json={ "user_id": "ab", "password": "TestPass1", }) assert resp.status_code == 422 # ────────────────────────────────────────────── # Login # ────────────────────────────────────────────── def test_login_success(client): client.post("/api/auth/register", json={"user_id": "loginuser", "password": "TestPass1"}) resp = client.post("/api/auth/login", json={"user_id": "loginuser", "password": "TestPass1"}) assert resp.status_code == 200 assert "access_token" in resp.json() assert "refresh_token" in resp.json() def test_login_wrong_password(client): client.post("/api/auth/register", json={"user_id": "loginuser2", "password": "TestPass1"}) resp = client.post("/api/auth/login", json={"user_id": "loginuser2", "password": "WrongPass1"}) assert resp.status_code == 401 def test_login_nonexistent_user(client): resp = client.post("/api/auth/login", json={"user_id": "ghost", "password": "TestPass1"}) assert resp.status_code == 401 def test_account_lockout_after_five_failures(client): """Account should lock after MAX_FAILED_LOGINS consecutive wrong passwords.""" client.post("/api/auth/register", json={"user_id": "lockme", "password": "TestPass1"}) for _ in range(5): client.post("/api/auth/login", json={"user_id": "lockme", "password": "WrongPass1"}) resp = client.post("/api/auth/login", json={"user_id": "lockme", "password": "TestPass1"}) # Should be locked (423) even with correct password assert resp.status_code == 423 def test_login_resets_failed_count(client): """Successful login clears failed count so subsequent login works normally.""" client.post("/api/auth/register", json={"user_id": "almostlocked", "password": "TestPass1"}) # 4 failures (not yet locked) for _ in range(4): client.post("/api/auth/login", json={"user_id": "almostlocked", "password": "WrongPass1"}) # Correct login resets counter resp = client.post("/api/auth/login", json={"user_id": "almostlocked", "password": "TestPass1"}) assert resp.status_code == 200 # Next failure starts from 0 again (not locked) resp2 = client.post("/api/auth/login", json={"user_id": "almostlocked", "password": "WrongPass1"}) assert resp2.status_code == 401 # not 423 # ────────────────────────────────────────────── # Token refresh # ────────────────────────────────────────────── def test_refresh_token_works(client): reg = client.post("/api/auth/register", json={"user_id": "refreshuser", "password": "TestPass1"}) refresh_token = reg.json()["refresh_token"] resp = client.post("/api/auth/refresh", json={"refresh_token": refresh_token}) assert resp.status_code == 200 data = resp.json() assert "access_token" in data assert "refresh_token" in data # New refresh token should differ from the old one assert data["refresh_token"] != refresh_token def test_refresh_token_invalid(client): resp = client.post("/api/auth/refresh", json={"refresh_token": "fake_token_that_doesnt_exist"}) assert resp.status_code == 401 def test_refresh_token_rotation(client): """Old refresh token should not work after rotation.""" reg = client.post("/api/auth/register", json={"user_id": "rotateuser", "password": "TestPass1"}) old_refresh = reg.json()["refresh_token"] # Rotate client.post("/api/auth/refresh", json={"refresh_token": old_refresh}) # Try the old one again — should fail resp = client.post("/api/auth/refresh", json={"refresh_token": old_refresh}) assert resp.status_code == 401 # ────────────────────────────────────────────── # Logout # ────────────────────────────────────────────── def test_logout_success(client): reg = client.post("/api/auth/register", json={"user_id": "logoutuser", "password": "TestPass1"}) token = reg.json()["access_token"] client.headers.update({"Authorization": f"Bearer {token}"}) resp = client.post("/api/auth/logout") assert resp.status_code == 200 def test_blacklisted_token_rejected(client): """Token should be rejected after logout.""" reg = client.post("/api/auth/register", json={"user_id": "bluser", "password": "TestPass1"}) token = reg.json()["access_token"] client.headers.update({"Authorization": f"Bearer {token}"}) client.post("/api/auth/logout") # Now use the same token resp = client.get("/api/auth/profile") assert resp.status_code == 401 # ────────────────────────────────────────────── # Change password # ────────────────────────────────────────────── def test_change_password_success(client): client.post("/api/auth/register", json={"user_id": "changepwduser", "password": "OldPass1"}) resp_login = client.post("/api/auth/login", json={"user_id": "changepwduser", "password": "OldPass1"}) token = resp_login.json()["access_token"] client.headers.update({"Authorization": f"Bearer {token}"}) resp = client.post("/api/auth/change-password", json={ "old_password": "OldPass1", "new_password": "NewPass1", }) assert resp.status_code == 200 # Old password no longer works resp2 = client.post("/api/auth/login", json={"user_id": "changepwduser", "password": "OldPass1"}) assert resp2.status_code == 401 def test_change_password_wrong_old(auth_client): client, token, user_id = auth_client resp = client.post("/api/auth/change-password", json={ "old_password": "WrongOld1", "new_password": "NewPass1", }) assert resp.status_code == 401 def test_change_password_same_as_old(auth_client): client, token, user_id = auth_client resp = client.post("/api/auth/change-password", json={ "old_password": "TestPass1", "new_password": "TestPass1", }) assert resp.status_code == 400 # ────────────────────────────────────────────── # Profile # ────────────────────────────────────────────── def test_profile_authenticated(auth_client): client, token, user_id = auth_client resp = client.get("/api/auth/profile") assert resp.status_code == 200 data = resp.json() assert data["user_id"] == user_id assert data["xp"] == 0 assert data["level"] == 1 def test_profile_unauthenticated(client): resp = client.get("/api/auth/profile") assert resp.status_code == 401 def test_update_profile_name(auth_client): client, token, user_id = auth_client resp = client.patch("/api/auth/profile", json={"name": "Updated Name"}) assert resp.status_code == 200 assert resp.json()["name"] == "Updated Name" def test_update_profile_valid_personality(auth_client): client, token, user_id = auth_client for personality in ["supportive", "drill_sergeant", "zen"]: resp = client.patch("/api/auth/profile", json={"coach_personality": personality}) assert resp.status_code == 200 assert resp.json()["coach_personality"] == personality def test_update_profile_invalid_personality(auth_client): client, token, user_id = auth_client resp = client.patch("/api/auth/profile", json={"coach_personality": "aggressive_hacker"}) assert resp.status_code == 422