forked from cardosofelipe/fast-next-template
Add extensive CRUD tests for session and user management; enhance cleanup logic
- Introduced new unit tests for session CRUD operations, including `update_refresh_token`, `cleanup_expired`, and multi-user session handling. - Added comprehensive tests for `CRUDBase` methods, covering edge cases, error handling, and UUID validation. - Reduced default test session creation from 5 to 2 for performance optimization. - Enhanced pagination, filtering, and sorting validations in `get_multi_with_total`. - Improved error handling with descriptive assertions for database exceptions. - Introduced tests for eager-loaded relationships in user sessions for comprehensive coverage.
This commit is contained in:
324
backend/tests/api/test_auth.py
Normal file
324
backend/tests/api/test_auth.py
Normal file
@@ -0,0 +1,324 @@
|
||||
# tests/api/test_auth.py
|
||||
"""
|
||||
Tests for authentication endpoints.
|
||||
"""
|
||||
import pytest
|
||||
import pytest_asyncio
|
||||
from fastapi import status
|
||||
|
||||
|
||||
class TestRegisterEndpoint:
|
||||
"""Tests for POST /auth/register endpoint."""
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_register_success(self, client):
|
||||
"""Test successful user registration."""
|
||||
response = await client.post(
|
||||
"/api/v1/auth/register",
|
||||
json={
|
||||
"email": "newuser@example.com",
|
||||
"password": "NewPassword123!",
|
||||
"first_name": "New",
|
||||
"last_name": "User"
|
||||
}
|
||||
)
|
||||
|
||||
assert response.status_code == status.HTTP_201_CREATED
|
||||
data = response.json()
|
||||
assert data["email"] == "newuser@example.com"
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_register_duplicate_email(self, client, async_test_user):
|
||||
"""Test registration with duplicate email."""
|
||||
response = await client.post(
|
||||
"/api/v1/auth/register",
|
||||
json={
|
||||
"email": async_test_user.email,
|
||||
"password": "TestPassword123!",
|
||||
"first_name": "Test",
|
||||
"last_name": "User"
|
||||
}
|
||||
)
|
||||
|
||||
assert response.status_code == status.HTTP_400_BAD_REQUEST
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_register_weak_password(self, client):
|
||||
"""Test registration with weak password."""
|
||||
response = await client.post(
|
||||
"/api/v1/auth/register",
|
||||
json={
|
||||
"email": "test@example.com",
|
||||
"password": "weak",
|
||||
"first_name": "Test",
|
||||
"last_name": "User"
|
||||
}
|
||||
)
|
||||
|
||||
assert response.status_code == status.HTTP_422_UNPROCESSABLE_ENTITY
|
||||
|
||||
|
||||
class TestLoginEndpoint:
|
||||
"""Tests for POST /auth/login endpoint."""
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_login_success(self, client, async_test_user):
|
||||
"""Test successful login."""
|
||||
response = await client.post(
|
||||
"/api/v1/auth/login",
|
||||
json={
|
||||
"email": "testuser@example.com",
|
||||
"password": "TestPassword123!"
|
||||
}
|
||||
)
|
||||
|
||||
assert response.status_code == status.HTTP_200_OK
|
||||
data = response.json()
|
||||
assert "access_token" in data
|
||||
assert "refresh_token" in data
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_login_invalid_credentials(self, client, async_test_user):
|
||||
"""Test login with invalid password."""
|
||||
response = await client.post(
|
||||
"/api/v1/auth/login",
|
||||
json={
|
||||
"email": "testuser@example.com",
|
||||
"password": "WrongPassword123!"
|
||||
}
|
||||
)
|
||||
|
||||
assert response.status_code == status.HTTP_401_UNAUTHORIZED
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_login_nonexistent_user(self, client):
|
||||
"""Test login with non-existent user."""
|
||||
response = await client.post(
|
||||
"/api/v1/auth/login",
|
||||
json={
|
||||
"email": "nonexistent@example.com",
|
||||
"password": "TestPassword123!"
|
||||
}
|
||||
)
|
||||
|
||||
assert response.status_code == status.HTTP_401_UNAUTHORIZED
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_login_inactive_user(self, client, async_test_db):
|
||||
"""Test login with inactive user."""
|
||||
test_engine, SessionLocal = async_test_db
|
||||
|
||||
async with SessionLocal() as session:
|
||||
from app.models.user import User
|
||||
from app.core.auth import get_password_hash
|
||||
inactive_user = User(
|
||||
email="inactive@example.com",
|
||||
password_hash=get_password_hash("TestPassword123!"),
|
||||
first_name="Inactive",
|
||||
last_name="User",
|
||||
is_active=False
|
||||
)
|
||||
session.add(inactive_user)
|
||||
await session.commit()
|
||||
|
||||
response = await client.post(
|
||||
"/api/v1/auth/login",
|
||||
json={
|
||||
"email": "inactive@example.com",
|
||||
"password": "TestPassword123!"
|
||||
}
|
||||
)
|
||||
|
||||
assert response.status_code == status.HTTP_401_UNAUTHORIZED
|
||||
|
||||
|
||||
class TestRefreshTokenEndpoint:
|
||||
"""Tests for POST /auth/refresh endpoint."""
|
||||
|
||||
@pytest_asyncio.fixture
|
||||
async def refresh_token(self, client, async_test_user):
|
||||
"""Get a refresh token for testing."""
|
||||
response = await client.post(
|
||||
"/api/v1/auth/login",
|
||||
json={
|
||||
"email": "testuser@example.com",
|
||||
"password": "TestPassword123!"
|
||||
}
|
||||
)
|
||||
return response.json()["refresh_token"]
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_refresh_token_success(self, client, refresh_token):
|
||||
"""Test successful token refresh."""
|
||||
response = await client.post(
|
||||
"/api/v1/auth/refresh",
|
||||
json={"refresh_token": refresh_token}
|
||||
)
|
||||
|
||||
assert response.status_code == status.HTTP_200_OK
|
||||
data = response.json()
|
||||
assert "access_token" in data
|
||||
assert "refresh_token" in data
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_refresh_token_invalid(self, client):
|
||||
"""Test refresh with invalid token."""
|
||||
response = await client.post(
|
||||
"/api/v1/auth/refresh",
|
||||
json={"refresh_token": "invalid.token.here"}
|
||||
)
|
||||
|
||||
assert response.status_code == status.HTTP_401_UNAUTHORIZED
|
||||
|
||||
|
||||
class TestLogoutEndpoint:
|
||||
"""Tests for POST /auth/logout endpoint."""
|
||||
|
||||
@pytest_asyncio.fixture
|
||||
async def tokens(self, client, async_test_user):
|
||||
"""Get tokens for testing."""
|
||||
response = await client.post(
|
||||
"/api/v1/auth/login",
|
||||
json={
|
||||
"email": "testuser@example.com",
|
||||
"password": "TestPassword123!"
|
||||
}
|
||||
)
|
||||
data = response.json()
|
||||
return {"access_token": data["access_token"], "refresh_token": data["refresh_token"]}
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_logout_success(self, client, tokens):
|
||||
"""Test successful logout."""
|
||||
response = await client.post(
|
||||
"/api/v1/auth/logout",
|
||||
headers={"Authorization": f"Bearer {tokens['access_token']}"},
|
||||
json={"refresh_token": tokens["refresh_token"]}
|
||||
)
|
||||
|
||||
assert response.status_code == status.HTTP_200_OK
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_logout_without_auth(self, client):
|
||||
"""Test logout without authentication."""
|
||||
response = await client.post(
|
||||
"/api/v1/auth/logout",
|
||||
json={"refresh_token": "some.token"}
|
||||
)
|
||||
assert response.status_code == status.HTTP_401_UNAUTHORIZED
|
||||
|
||||
|
||||
class TestPasswordResetRequest:
|
||||
"""Tests for POST /auth/password-reset/request endpoint."""
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_password_reset_request_success(self, client, async_test_user):
|
||||
"""Test password reset request with existing user."""
|
||||
response = await client.post(
|
||||
"/api/v1/auth/password-reset/request",
|
||||
json={"email": async_test_user.email}
|
||||
)
|
||||
|
||||
assert response.status_code == status.HTTP_200_OK
|
||||
data = response.json()
|
||||
assert data["success"] is True
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_password_reset_request_nonexistent_email(self, client):
|
||||
"""Test password reset request with non-existent email."""
|
||||
response = await client.post(
|
||||
"/api/v1/auth/password-reset/request",
|
||||
json={"email": "nonexistent@example.com"}
|
||||
)
|
||||
|
||||
assert response.status_code == status.HTTP_200_OK
|
||||
data = response.json()
|
||||
assert data["success"] is True
|
||||
|
||||
|
||||
class TestPasswordResetConfirm:
|
||||
"""Tests for POST /auth/password-reset/confirm endpoint."""
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_password_reset_confirm_invalid_token(self, client):
|
||||
"""Test password reset with invalid token."""
|
||||
response = await client.post(
|
||||
"/api/v1/auth/password-reset/confirm",
|
||||
json={
|
||||
"token": "invalid.token.here",
|
||||
"new_password": "NewPassword123!"
|
||||
}
|
||||
)
|
||||
|
||||
assert response.status_code == status.HTTP_400_BAD_REQUEST
|
||||
|
||||
|
||||
class TestLogoutAll:
|
||||
"""Tests for POST /auth/logout-all endpoint."""
|
||||
|
||||
@pytest_asyncio.fixture
|
||||
async def tokens(self, client, async_test_user):
|
||||
"""Get tokens for testing."""
|
||||
response = await client.post(
|
||||
"/api/v1/auth/login",
|
||||
json={
|
||||
"email": "testuser@example.com",
|
||||
"password": "TestPassword123!"
|
||||
}
|
||||
)
|
||||
data = response.json()
|
||||
return {"access_token": data["access_token"], "refresh_token": data["refresh_token"]}
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_logout_all_success(self, client, tokens):
|
||||
"""Test logout from all devices."""
|
||||
response = await client.post(
|
||||
"/api/v1/auth/logout-all",
|
||||
headers={"Authorization": f"Bearer {tokens['access_token']}"}
|
||||
)
|
||||
|
||||
assert response.status_code == status.HTTP_200_OK
|
||||
data = response.json()
|
||||
assert data["success"] is True
|
||||
assert "sessions terminated" in data["message"].lower()
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_logout_all_unauthorized(self, client):
|
||||
"""Test logout-all without authentication."""
|
||||
response = await client.post("/api/v1/auth/logout-all")
|
||||
|
||||
assert response.status_code == status.HTTP_401_UNAUTHORIZED
|
||||
|
||||
|
||||
class TestOAuthLogin:
|
||||
"""Tests for POST /auth/login/oauth endpoint."""
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_oauth_login_success(self, client, async_test_user):
|
||||
"""Test successful OAuth login."""
|
||||
response = await client.post(
|
||||
"/api/v1/auth/login/oauth",
|
||||
data={
|
||||
"username": "testuser@example.com",
|
||||
"password": "TestPassword123!"
|
||||
}
|
||||
)
|
||||
|
||||
assert response.status_code == status.HTTP_200_OK
|
||||
data = response.json()
|
||||
assert "access_token" in data
|
||||
assert "refresh_token" in data
|
||||
assert data["token_type"] == "bearer"
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_oauth_login_invalid_credentials(self, client, async_test_user):
|
||||
"""Test OAuth login with invalid credentials."""
|
||||
response = await client.post(
|
||||
"/api/v1/auth/login/oauth",
|
||||
data={
|
||||
"username": "testuser@example.com",
|
||||
"password": "WrongPassword"
|
||||
}
|
||||
)
|
||||
|
||||
assert response.status_code == status.HTTP_401_UNAUTHORIZED
|
||||
Reference in New Issue
Block a user