Add comprehensive test suite and utilities for user functionality

This commit introduces a suite of tests for user models, schemas, CRUD operations, and authentication services. It also adds utilities for in-memory database setup to support these tests and updates environment settings for consistency.
This commit is contained in:
2025-03-04 19:10:54 +01:00
parent 481b6d618e
commit 162e586e13
40 changed files with 2948 additions and 11 deletions

View File

View File

@@ -0,0 +1,369 @@
# tests/api/routes/test_auth.py
import json
import uuid
from datetime import datetime, timezone
from unittest.mock import patch, MagicMock, Mock
import pytest
from fastapi import FastAPI, Depends
from fastapi.testclient import TestClient
from sqlalchemy.orm import Session
from app.api.routes.auth import router as auth_router
from app.core.auth import get_password_hash
from app.core.database import get_db
from app.models.user import User
from app.services.auth_service import AuthService, AuthenticationError
from app.core.auth import TokenExpiredError, TokenInvalidError
# Mock the get_db dependency
@pytest.fixture
def override_get_db(db_session):
"""Override get_db dependency for testing."""
return db_session
@pytest.fixture
def app(override_get_db):
"""Create a FastAPI test application with overridden dependencies."""
app = FastAPI()
app.include_router(auth_router, prefix="/auth", tags=["auth"])
# Override the get_db dependency
app.dependency_overrides[get_db] = lambda: override_get_db
return app
@pytest.fixture
def client(app):
"""Create a FastAPI test client."""
return TestClient(app)
class TestRegisterUser:
"""Tests for the register_user endpoint."""
def test_register_user_success(self, client, monkeypatch, db_session):
"""Test successful user registration."""
# Mock the service method with a valid complete User object
mock_user = User(
id=uuid.uuid4(),
email="newuser@example.com",
password_hash="hashed_password",
first_name="New",
last_name="User",
is_active=True,
is_superuser=False,
created_at=datetime.now(timezone.utc),
updated_at=datetime.now(timezone.utc)
)
# Use patch for mocking
with patch.object(AuthService, 'create_user', return_value=mock_user):
# Test request
response = client.post(
"/auth/register",
json={
"email": "newuser@example.com",
"password": "Password123",
"first_name": "New",
"last_name": "User"
}
)
# Assertions
assert response.status_code == 201
data = response.json()
assert data["email"] == "newuser@example.com"
assert data["first_name"] == "New"
assert data["last_name"] == "User"
assert "password" not in data
def test_register_user_duplicate_email(self, client, db_session):
"""Test registration with duplicate email."""
# Use patch for mocking with a side effect
with patch.object(AuthService, 'create_user',
side_effect=AuthenticationError("User with this email already exists")):
# Test request
response = client.post(
"/auth/register",
json={
"email": "existing@example.com",
"password": "Password123",
"first_name": "Existing",
"last_name": "User"
}
)
# Assertions
assert response.status_code == 409
assert "already exists" in response.json()["detail"]
class TestLogin:
"""Tests for the login endpoint."""
def test_login_success(self, client, mock_user, db_session):
"""Test successful login."""
# Ensure mock_user has required attributes
if not hasattr(mock_user, 'created_at') or mock_user.created_at is None:
mock_user.created_at = datetime.now(timezone.utc)
if not hasattr(mock_user, 'updated_at') or mock_user.updated_at is None:
mock_user.updated_at = datetime.now(timezone.utc)
# Create mock tokens
mock_tokens = MagicMock(
access_token="mock_access_token",
refresh_token="mock_refresh_token",
token_type="bearer"
)
# Use context managers for patching
with patch.object(AuthService, 'authenticate_user', return_value=mock_user), \
patch.object(AuthService, 'create_tokens', return_value=mock_tokens):
# Test request
response = client.post(
"/auth/login",
json={
"email": "user@example.com",
"password": "Password123"
}
)
# Assertions
assert response.status_code == 200
data = response.json()
assert data["access_token"] == "mock_access_token"
assert data["refresh_token"] == "mock_refresh_token"
assert data["token_type"] == "bearer"
def test_login_invalid_credentials_debug(self, client, app):
"""Improved test for login with invalid credentials."""
# Print response for debugging
from app.services.auth_service import AuthService
# Create a complete mock for AuthService
class MockAuthService:
@staticmethod
def authenticate_user(db, email, password):
print(f"Mock called with: {email}, {password}")
return None
# Replace the entire class with our mock
original_service = AuthService
try:
# Replace with our mock
import sys
sys.modules['app.services.auth_service'].AuthService = MockAuthService
# Make the request
response = client.post(
"/auth/login",
json={
"email": "user@example.com",
"password": "WrongPassword"
}
)
# Print response details for debugging
print(f"Response status: {response.status_code}")
print(f"Response body: {response.text}")
# Assertions
assert response.status_code == 401
assert "Invalid email or password" in response.json()["detail"]
finally:
# Restore original service
sys.modules['app.services.auth_service'].AuthService = original_service
def test_login_inactive_user(self, client, db_session):
"""Test login with inactive user."""
# Mock authentication to raise an error
with patch.object(AuthService, 'authenticate_user',
side_effect=AuthenticationError("User account is inactive")):
# Test request
response = client.post(
"/auth/login",
json={
"email": "inactive@example.com",
"password": "Password123"
}
)
# Assertions
assert response.status_code == 401
assert "inactive" in response.json()["detail"]
class TestRefreshToken:
"""Tests for the refresh_token endpoint."""
def test_refresh_token_success(self, client, db_session):
"""Test successful token refresh."""
# Mock refresh to return tokens
mock_tokens = MagicMock(
access_token="new_access_token",
refresh_token="new_refresh_token",
token_type="bearer"
)
with patch.object(AuthService, 'refresh_tokens', return_value=mock_tokens):
# Test request
response = client.post(
"/auth/refresh",
json={
"refresh_token": "valid_refresh_token"
}
)
# Assertions
assert response.status_code == 200
data = response.json()
assert data["access_token"] == "new_access_token"
assert data["refresh_token"] == "new_refresh_token"
assert data["token_type"] == "bearer"
def test_refresh_token_expired(self, client, db_session):
"""Test refresh with expired token."""
# Mock refresh to raise expired token error
with patch.object(AuthService, 'refresh_tokens',
side_effect=TokenExpiredError("Token expired")):
# Test request
response = client.post(
"/auth/refresh",
json={
"refresh_token": "expired_refresh_token"
}
)
# Assertions
assert response.status_code == 401
assert "expired" in response.json()["detail"]
def test_refresh_token_invalid(self, client, db_session):
"""Test refresh with invalid token."""
# Mock refresh to raise invalid token error
with patch.object(AuthService, 'refresh_tokens',
side_effect=TokenInvalidError("Invalid token")):
# Test request
response = client.post(
"/auth/refresh",
json={
"refresh_token": "invalid_refresh_token"
}
)
# Assertions
assert response.status_code == 401
assert "Invalid" in response.json()["detail"]
class TestChangePassword:
"""Tests for the change_password endpoint."""
def test_change_password_success(self, client, mock_user, db_session, app):
"""Test successful password change."""
# Ensure mock_user has required attributes
if not hasattr(mock_user, 'created_at') or mock_user.created_at is None:
mock_user.created_at = datetime.now(timezone.utc)
if not hasattr(mock_user, 'updated_at') or mock_user.updated_at is None:
mock_user.updated_at = datetime.now(timezone.utc)
# Override get_current_user dependency
from app.api.dependencies.auth import get_current_user
app.dependency_overrides[get_current_user] = lambda: mock_user
# Mock password change to return success
with patch.object(AuthService, 'change_password', return_value=True):
# Test request
response = client.post(
"/auth/change-password",
json={
"current_password": "OldPassword123",
"new_password": "NewPassword123"
}
)
# Assertions
assert response.status_code == 200
assert "success" in response.json()["message"].lower()
# Clean up override
if get_current_user in app.dependency_overrides:
del app.dependency_overrides[get_current_user]
def test_change_password_incorrect_current_password(self, client, mock_user, db_session, app):
"""Test change password with incorrect current password."""
# Ensure mock_user has required attributes
if not hasattr(mock_user, 'created_at') or mock_user.created_at is None:
mock_user.created_at = datetime.now(timezone.utc)
if not hasattr(mock_user, 'updated_at') or mock_user.updated_at is None:
mock_user.updated_at = datetime.now(timezone.utc)
# Override get_current_user dependency
from app.api.dependencies.auth import get_current_user
app.dependency_overrides[get_current_user] = lambda: mock_user
# Mock password change to raise error
with patch.object(AuthService, 'change_password',
side_effect=AuthenticationError("Current password is incorrect")):
# Test request
response = client.post(
"/auth/change-password",
json={
"current_password": "WrongPassword",
"new_password": "NewPassword123"
}
)
# Assertions
assert response.status_code == 400
assert "incorrect" in response.json()["detail"].lower()
# Clean up override
if get_current_user in app.dependency_overrides:
del app.dependency_overrides[get_current_user]
class TestGetCurrentUserInfo:
"""Tests for the get_current_user_info endpoint."""
def test_get_current_user_info(self, client, mock_user, app):
"""Test getting current user info."""
# Ensure mock_user has required attributes
if not hasattr(mock_user, 'created_at') or mock_user.created_at is None:
mock_user.created_at = datetime.now(timezone.utc)
if not hasattr(mock_user, 'updated_at') or mock_user.updated_at is None:
mock_user.updated_at = datetime.now(timezone.utc)
# Override get_current_user dependency
from app.api.dependencies.auth import get_current_user
app.dependency_overrides[get_current_user] = lambda: mock_user
# Test request
response = client.get("/auth/me")
# Assertions
assert response.status_code == 200
data = response.json()
assert data["email"] == mock_user.email
assert data["first_name"] == mock_user.first_name
assert data["last_name"] == mock_user.last_name
assert "password" not in data
# Clean up override
if get_current_user in app.dependency_overrides:
del app.dependency_overrides[get_current_user]
def test_get_current_user_info_unauthorized(self, client):
"""Test getting user info without authentication."""
# Test request without authentication
response = client.get("/auth/me")
# Assertions
assert response.status_code == 401

View File

@@ -0,0 +1,211 @@
# tests/api/dependencies/test_auth_dependencies.py
import pytest
from unittest.mock import patch, MagicMock
from fastapi import HTTPException
from app.api.dependencies.auth import (
get_current_user,
get_current_active_user,
get_current_superuser,
get_optional_current_user
)
from app.core.auth import TokenExpiredError, TokenInvalidError
@pytest.fixture
def mock_token():
return "mock.jwt.token"
class TestGetCurrentUser:
"""Tests for get_current_user dependency"""
def test_get_current_user_success(self, db_session, mock_user, mock_token):
"""Test successfully getting the current user"""
# Mock get_token_data to return user_id that matches our mock_user
with patch('app.api.dependencies.auth.get_token_data') as mock_get_data:
mock_get_data.return_value.user_id = mock_user.id
# Call the dependency
user = get_current_user(db=db_session, token=mock_token)
# Verify the correct user was returned
assert user.id == mock_user.id
assert user.email == mock_user.email
def test_get_current_user_nonexistent(self, db_session, mock_token):
"""Test when the token contains a user ID that doesn't exist"""
# Mock get_token_data to return a non-existent user ID
# Use a real UUID object instead of a string
import uuid
nonexistent_id = uuid.UUID("11111111-1111-1111-1111-111111111111")
with patch('app.api.dependencies.auth.get_token_data') as mock_get_data:
mock_get_data.return_value.user_id = nonexistent_id # Using UUID object, not string
# Should raise HTTPException with 404 status
with pytest.raises(HTTPException) as exc_info:
get_current_user(db=db_session, token=mock_token)
assert exc_info.value.status_code == 404
def test_get_current_user_inactive(self, db_session, mock_user, mock_token):
"""Test when the user is inactive"""
# Make the user inactive
mock_user.is_active = False
db_session.commit()
# Mock get_token_data
with patch('app.api.dependencies.auth.get_token_data') as mock_get_data:
mock_get_data.return_value.user_id = mock_user.id
# Should raise HTTPException with 403 status
with pytest.raises(HTTPException) as exc_info:
get_current_user(db=db_session, token=mock_token)
assert exc_info.value.status_code == 403
def test_get_current_user_expired_token(self, db_session, mock_token):
"""Test with an expired token"""
# Mock get_token_data to raise TokenExpiredError
with patch('app.api.dependencies.auth.get_token_data') as mock_get_data:
mock_get_data.side_effect = TokenExpiredError("Token expired")
# Should raise HTTPException with 401 status
with pytest.raises(HTTPException) as exc_info:
get_current_user(db=db_session, token=mock_token)
assert exc_info.value.status_code == 401
assert "Token expired" in exc_info.value.detail
def test_get_current_user_invalid_token(self, db_session, mock_token):
"""Test with an invalid token"""
# Mock get_token_data to raise TokenInvalidError
with patch('app.api.dependencies.auth.get_token_data') as mock_get_data:
mock_get_data.side_effect = TokenInvalidError("Invalid token")
# Should raise HTTPException with 401 status
with pytest.raises(HTTPException) as exc_info:
get_current_user(db=db_session, token=mock_token)
assert exc_info.value.status_code == 401
assert "Could not validate credentials" in exc_info.value.detail
class TestGetCurrentActiveUser:
"""Tests for get_current_active_user dependency"""
def test_get_current_active_user(self, mock_user):
"""Test getting an active user"""
# Ensure user is active
mock_user.is_active = True
# Call the dependency with mocked current_user
user = get_current_active_user(current_user=mock_user)
# Should return the same user
assert user == mock_user
def test_get_current_inactive_user(self, mock_user):
"""Test getting an inactive user"""
# Make user inactive
mock_user.is_active = False
# Should raise HTTPException with 403 status
with pytest.raises(HTTPException) as exc_info:
get_current_active_user(current_user=mock_user)
assert exc_info.value.status_code == 403
assert "Inactive user" in exc_info.value.detail
class TestGetCurrentSuperuser:
"""Tests for get_current_superuser dependency"""
def test_get_current_superuser(self, mock_user):
"""Test getting a superuser"""
# Make user a superuser
mock_user.is_superuser = True
# Call the dependency with mocked current_user
user = get_current_superuser(current_user=mock_user)
# Should return the same user
assert user == mock_user
def test_get_current_non_superuser(self, mock_user):
"""Test getting a non-superuser"""
# Ensure user is not a superuser
mock_user.is_superuser = False
# Should raise HTTPException with 403 status
with pytest.raises(HTTPException) as exc_info:
get_current_superuser(current_user=mock_user)
assert exc_info.value.status_code == 403
assert "Not enough permissions" in exc_info.value.detail
class TestGetOptionalCurrentUser:
"""Tests for get_optional_current_user dependency"""
def test_get_optional_current_user_with_token(self, db_session, mock_user, mock_token):
"""Test getting optional user with a valid token"""
# Mock get_token_data
with patch('app.api.dependencies.auth.get_token_data') as mock_get_data:
mock_get_data.return_value.user_id = mock_user.id
# Call the dependency
user = get_optional_current_user(db=db_session, token=mock_token)
# Should return the correct user
assert user is not None
assert user.id == mock_user.id
def test_get_optional_current_user_no_token(self, db_session):
"""Test getting optional user with no token"""
# Call the dependency with no token
user = get_optional_current_user(db=db_session, token=None)
# Should return None
assert user is None
def test_get_optional_current_user_invalid_token(self, db_session, mock_token):
"""Test getting optional user with an invalid token"""
# Mock get_token_data to raise TokenInvalidError
with patch('app.api.dependencies.auth.get_token_data') as mock_get_data:
mock_get_data.side_effect = TokenInvalidError("Invalid token")
# Call the dependency
user = get_optional_current_user(db=db_session, token=mock_token)
# Should return None, not raise an exception
assert user is None
def test_get_optional_current_user_expired_token(self, db_session, mock_token):
"""Test getting optional user with an expired token"""
# Mock get_token_data to raise TokenExpiredError
with patch('app.api.dependencies.auth.get_token_data') as mock_get_data:
mock_get_data.side_effect = TokenExpiredError("Token expired")
# Call the dependency
user = get_optional_current_user(db=db_session, token=mock_token)
# Should return None, not raise an exception
assert user is None
def test_get_optional_current_user_inactive(self, db_session, mock_user, mock_token):
"""Test getting optional user when user is inactive"""
# Make the user inactive
mock_user.is_active = False
db_session.commit()
# Mock get_token_data
with patch('app.api.dependencies.auth.get_token_data') as mock_get_data:
mock_get_data.return_value.user_id = mock_user.id
# Call the dependency
user = get_optional_current_user(db=db_session, token=mock_token)
# Should return None for inactive users
assert user is None