hale-api / tests /test_auth.py
Raunak211006's picture
Deploy HALE API v3.1
0ca664b verified
Raw
History Blame Contribute Delete
10.4 kB
# 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