Finance-Assessment / tests /test_auth.py
aayushhh-operator's picture
Upload 56 files
cf35fad verified
Raw
History Blame Contribute Delete
8.85 kB
from __future__ import annotations
from datetime import timedelta
import pytest
from httpx import AsyncClient
from app.services.auth_service import create_access_token
from conftest import TestUserData, create_test_user, login_payload
pytestmark = [pytest.mark.asyncio, pytest.mark.auth]
async def test_register_success_creates_user(client: AsyncClient, db_session) -> None:
"""Successful registration should create a user and hide password data."""
user_data = {
"email": "newuser@example.com",
"password": "SecurePass123",
"role": "viewer",
"full_name": "New User",
}
response = await client.post("/api/auth/register", json=user_data)
assert response.status_code == 201, f"Expected successful registration, got {response.text}"
payload = response.json()
assert payload["email"] == user_data["email"], "Registered email should match the request payload"
assert payload["role"] == "viewer", "Public registration should always create viewer accounts"
assert "password" not in payload, "Registration response must not expose the password"
async def test_register_duplicate_email_returns_400(client: AsyncClient, viewer_user: TestUserData) -> None:
"""Registering an existing email should return a conflict-style validation error."""
response = await client.post(
"/api/auth/register",
json={"email": viewer_user.user.email, "password": "AnotherPass123", "role": "viewer"},
)
assert response.status_code == 400, f"Duplicate email should be rejected, got {response.text}"
assert response.json()["detail"] == "Email already registered", "Duplicate email message should be explicit"
@pytest.mark.parametrize("password", ["short", "allletters", "12345678"])
async def test_register_weak_password_returns_422(client: AsyncClient, password: str) -> None:
"""Weak passwords should fail schema validation."""
response = await client.post(
"/api/auth/register",
json={"email": f"{password}@example.com", "password": password},
)
assert response.status_code == 422, f"Weak password {password!r} should fail validation"
assert response.json()["detail"] == "Validation error", "Weak password should use the normalized 422 format"
async def test_register_invalid_email_format_returns_422(client: AsyncClient) -> None:
"""Invalid email addresses should be rejected by Pydantic."""
response = await client.post(
"/api/auth/register",
json={"email": "not-an-email", "password": "SecurePass123"},
)
assert response.status_code == 422, f"Invalid email should return 422, got {response.text}"
assert response.json()["errors"][0]["field"] == "email", "Validation error should point at the email field"
async def test_register_missing_required_fields_returns_422(client: AsyncClient) -> None:
"""Missing required registration fields should return the normalized error payload."""
response = await client.post("/api/auth/register", json={})
assert response.status_code == 422, f"Missing registration fields should return 422, got {response.text}"
fields = {error["field"] for error in response.json()["errors"]}
assert {"email", "password"}.issubset(fields), "Both email and password fields should be reported missing"
async def test_register_default_role_is_viewer(client: AsyncClient) -> None:
"""Omitting the role should create a viewer by default."""
response = await client.post(
"/api/auth/register",
json={"email": "defaultrole@example.com", "password": "SecurePass123"},
)
assert response.status_code == 201, f"Registration without an explicit role should succeed, got {response.text}"
assert response.json()["role"] == "viewer", "Default role should be viewer"
async def test_cannot_register_as_admin(client: AsyncClient) -> None:
"""Registration should ignore privileged role requests and keep the account as viewer."""
response = await client.post(
"/api/auth/register",
json={"email": "hacker@example.com", "password": "SecurePass123", "role": "admin"},
)
assert response.status_code == 201, f"Role escalation attempt should not fail registration, got {response.text}"
assert response.json()["role"] == "viewer", "Privileged self-registration should be downgraded to viewer"
async def test_login_success_returns_token(client: AsyncClient, viewer_user: TestUserData) -> None:
"""Valid credentials should return a bearer token."""
response = await client.post("/api/auth/login", data=login_payload(viewer_user.user.email, viewer_user.password))
assert response.status_code == 200, f"Expected login success, got {response.text}"
payload = response.json()
assert payload["token_type"] == "bearer", "Token type should be bearer"
assert payload["access_token"], "Access token should be present in the login response"
@pytest.mark.parametrize(
("email", "password"),
[("viewer@example.com", "WrongPass123"), ("missing@example.com", "WrongPass123")],
)
async def test_login_invalid_credentials_returns_401(
client: AsyncClient,
viewer_user: TestUserData,
email: str,
password: str,
) -> None:
"""Incorrect or unknown credentials should return 401."""
response = await client.post("/api/auth/login", data=login_payload(email, password))
assert response.status_code == 401, f"Invalid credentials should return 401, got {response.text}"
assert response.json()["detail"] == "Invalid credentials", "Invalid login should use the expected error message"
async def test_login_inactive_user_returns_401(client: AsyncClient, db_session) -> None:
"""Inactive users should not be able to log in."""
inactive_user = create_test_user(
db_session,
email="inactive@example.com",
password="InactivePass123",
is_active=False,
)
response = await client.post("/api/auth/login", data=login_payload(inactive_user.user.email, inactive_user.password))
assert response.status_code == 401, f"Inactive users should be blocked from login, got {response.text}"
assert response.json()["detail"] == "User account is inactive", "Inactive login should explain the failure"
async def test_token_contains_valid_claims(client: AsyncClient, viewer_user: TestUserData, decode_jwt) -> None:
"""Returned JWT tokens should include the subject and expiration claims."""
response = await client.post("/api/auth/login", data=login_payload(viewer_user.user.email, viewer_user.password))
assert response.status_code == 200, f"Expected login success for token claims test, got {response.text}"
token = response.json()["access_token"]
claims = decode_jwt(token)
assert claims["sub"] == viewer_user.user.email, "JWT subject should be the user email"
assert claims["user_id"] == viewer_user.user.id, "JWT should include the user id"
assert claims["role"] == viewer_user.user.role.value, "JWT should include the current role"
assert "exp" in claims, "JWT should contain an expiration claim"
async def test_expired_token_returns_401(client: AsyncClient, viewer_user: TestUserData) -> None:
"""Expired JWTs should be rejected by authenticated endpoints."""
expired_token = create_access_token(
subject=viewer_user.user.email,
user_id=viewer_user.user.id,
role=viewer_user.user.role.value,
expires_delta=timedelta(minutes=-5),
)
response = await client.get("/api/users/me", headers={"Authorization": f"Bearer {expired_token}"})
assert response.status_code == 401, f"Expired token should return 401, got {response.text}"
assert response.json()["detail"] == "Could not validate credentials", "Expired token should use auth failure message"
async def test_invalid_token_format_returns_401(client: AsyncClient) -> None:
"""Malformed tokens should be rejected consistently."""
response = await client.get("/api/users/me", headers={"Authorization": "Bearer not.a.valid.token"})
assert response.status_code == 401, f"Invalid token should return 401, got {response.text}"
assert response.json()["detail"] == "Could not validate credentials", "Malformed token should be treated as invalid"
async def test_missing_token_returns_401(client: AsyncClient) -> None:
"""Protected routes should require a bearer token."""
response = await client.get("/api/users/me")
assert response.status_code == 401, f"Missing token should return 401, got {response.text}"
assert response.json()["detail"] == "Not authenticated", "Missing token should trigger the OAuth2 auth error"