Spaces:
Running
Running
| # 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 | |