forked from cardosofelipe/fast-next-template
Convert password reset and auth dependencies tests to async
- Refactored all `password reset` and `auth dependency` tests to utilize async patterns for compatibility with async database sessions. - Enhanced test fixtures with `pytest-asyncio` to support asynchronous database operations. - Improved user handling with async context management for `test_user` and `async_mock_user`. - Introduced `await` syntax for route calls, token generation, and database transactions in test cases.
This commit is contained in:
0
backend/tests/api/dependencies/__init__.py
Normal file → Executable file
0
backend/tests/api/dependencies/__init__.py
Normal file → Executable file
242
backend/tests/api/dependencies/test_auth_dependencies.py
Normal file → Executable file
242
backend/tests/api/dependencies/test_auth_dependencies.py
Normal file → Executable file
@@ -1,5 +1,6 @@
|
||||
# tests/api/dependencies/test_auth_dependencies.py
|
||||
import pytest
|
||||
import pytest_asyncio
|
||||
import uuid
|
||||
from unittest.mock import patch
|
||||
from fastapi import HTTPException
|
||||
@@ -10,7 +11,8 @@ from app.api.dependencies.auth import (
|
||||
get_current_superuser,
|
||||
get_optional_current_user
|
||||
)
|
||||
from app.core.auth import TokenExpiredError, TokenInvalidError
|
||||
from app.core.auth import TokenExpiredError, TokenInvalidError, get_password_hash
|
||||
from app.models.user import User
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
@@ -19,79 +21,119 @@ def mock_token():
|
||||
return "mock.jwt.token"
|
||||
|
||||
|
||||
@pytest_asyncio.fixture
|
||||
async def async_mock_user(async_test_db):
|
||||
"""Async fixture to create and return a mock User instance."""
|
||||
test_engine, AsyncTestingSessionLocal = async_test_db
|
||||
async with AsyncTestingSessionLocal() as session:
|
||||
mock_user = User(
|
||||
id=uuid.uuid4(),
|
||||
email="mockuser@example.com",
|
||||
password_hash=get_password_hash("mockhashedpassword"),
|
||||
first_name="Mock",
|
||||
last_name="User",
|
||||
phone_number="1234567890",
|
||||
is_active=True,
|
||||
is_superuser=False,
|
||||
preferences=None,
|
||||
)
|
||||
session.add(mock_user)
|
||||
await session.commit()
|
||||
await session.refresh(mock_user)
|
||||
return mock_user
|
||||
|
||||
|
||||
class TestGetCurrentUser:
|
||||
"""Tests for get_current_user dependency"""
|
||||
|
||||
def test_get_current_user_success(self, db_session, mock_user, mock_token):
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_current_user_success(self, async_test_db, async_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
|
||||
test_engine, AsyncTestingSessionLocal = async_test_db
|
||||
async with AsyncTestingSessionLocal() as session:
|
||||
# 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 = async_mock_user.id
|
||||
|
||||
# Call the dependency
|
||||
user = get_current_user(db=db_session, token=mock_token)
|
||||
# Call the dependency
|
||||
user = await get_current_user(db=session, token=mock_token)
|
||||
|
||||
# Verify the correct user was returned
|
||||
assert user.id == mock_user.id
|
||||
assert user.email == mock_user.email
|
||||
# Verify the correct user was returned
|
||||
assert user.id == async_mock_user.id
|
||||
assert user.email == async_mock_user.email
|
||||
|
||||
def test_get_current_user_nonexistent(self, db_session, mock_token):
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_current_user_nonexistent(self, async_test_db, 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
|
||||
nonexistent_id = uuid.UUID("11111111-1111-1111-1111-111111111111")
|
||||
test_engine, AsyncTestingSessionLocal = async_test_db
|
||||
async with AsyncTestingSessionLocal() as session:
|
||||
# Mock get_token_data to return a non-existent user ID
|
||||
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
|
||||
with patch('app.api.dependencies.auth.get_token_data') as mock_get_data:
|
||||
mock_get_data.return_value.user_id = nonexistent_id
|
||||
|
||||
# Should raise HTTPException with 404 status
|
||||
with pytest.raises(HTTPException) as exc_info:
|
||||
get_current_user(db=db_session, token=mock_token)
|
||||
# Should raise HTTPException with 404 status
|
||||
with pytest.raises(HTTPException) as exc_info:
|
||||
await get_current_user(db=session, token=mock_token)
|
||||
|
||||
assert exc_info.value.status_code == 404
|
||||
assert "User not found" in exc_info.value.detail
|
||||
assert exc_info.value.status_code == 404
|
||||
assert "User not found" in exc_info.value.detail
|
||||
|
||||
def test_get_current_user_inactive(self, db_session, mock_user, mock_token):
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_current_user_inactive(self, async_test_db, async_mock_user, mock_token):
|
||||
"""Test when the user is inactive"""
|
||||
# Make the user inactive
|
||||
mock_user.is_active = False
|
||||
db_session.commit()
|
||||
test_engine, AsyncTestingSessionLocal = async_test_db
|
||||
async with AsyncTestingSessionLocal() as session:
|
||||
# Get the user in this session and make it inactive
|
||||
from sqlalchemy import select
|
||||
result = await session.execute(select(User).where(User.id == async_mock_user.id))
|
||||
user_in_session = result.scalar_one_or_none()
|
||||
user_in_session.is_active = False
|
||||
await 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
|
||||
# Mock get_token_data
|
||||
with patch('app.api.dependencies.auth.get_token_data') as mock_get_data:
|
||||
mock_get_data.return_value.user_id = async_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)
|
||||
# Should raise HTTPException with 403 status
|
||||
with pytest.raises(HTTPException) as exc_info:
|
||||
await get_current_user(db=session, token=mock_token)
|
||||
|
||||
assert exc_info.value.status_code == 403
|
||||
assert "Inactive user" in exc_info.value.detail
|
||||
assert exc_info.value.status_code == 403
|
||||
assert "Inactive user" in exc_info.value.detail
|
||||
|
||||
def test_get_current_user_expired_token(self, db_session, mock_token):
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_current_user_expired_token(self, async_test_db, 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")
|
||||
test_engine, AsyncTestingSessionLocal = async_test_db
|
||||
async with AsyncTestingSessionLocal() as session:
|
||||
# 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)
|
||||
# Should raise HTTPException with 401 status
|
||||
with pytest.raises(HTTPException) as exc_info:
|
||||
await get_current_user(db=session, token=mock_token)
|
||||
|
||||
assert exc_info.value.status_code == 401
|
||||
assert "Token expired" in exc_info.value.detail
|
||||
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):
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_current_user_invalid_token(self, async_test_db, 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")
|
||||
test_engine, AsyncTestingSessionLocal = async_test_db
|
||||
async with AsyncTestingSessionLocal() as session:
|
||||
# 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)
|
||||
# Should raise HTTPException with 401 status
|
||||
with pytest.raises(HTTPException) as exc_info:
|
||||
await get_current_user(db=session, token=mock_token)
|
||||
|
||||
assert exc_info.value.status_code == 401
|
||||
assert "Could not validate credentials" in exc_info.value.detail
|
||||
assert exc_info.value.status_code == 401
|
||||
assert "Could not validate credentials" in exc_info.value.detail
|
||||
|
||||
|
||||
class TestGetCurrentActiveUser:
|
||||
@@ -151,63 +193,81 @@ class TestGetCurrentSuperuser:
|
||||
class TestGetOptionalCurrentUser:
|
||||
"""Tests for get_optional_current_user dependency"""
|
||||
|
||||
def test_get_optional_current_user_with_token(self, db_session, mock_user, mock_token):
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_optional_current_user_with_token(self, async_test_db, async_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
|
||||
test_engine, AsyncTestingSessionLocal = async_test_db
|
||||
async with AsyncTestingSessionLocal() as session:
|
||||
# Mock get_token_data
|
||||
with patch('app.api.dependencies.auth.get_token_data') as mock_get_data:
|
||||
mock_get_data.return_value.user_id = async_mock_user.id
|
||||
|
||||
# Call the dependency
|
||||
user = get_optional_current_user(db=db_session, token=mock_token)
|
||||
# Call the dependency
|
||||
user = await get_optional_current_user(db=session, token=mock_token)
|
||||
|
||||
# Should return the correct user
|
||||
assert user is not None
|
||||
assert user.id == mock_user.id
|
||||
# Should return the correct user
|
||||
assert user is not None
|
||||
assert user.id == async_mock_user.id
|
||||
|
||||
def test_get_optional_current_user_no_token(self, db_session):
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_optional_current_user_no_token(self, async_test_db):
|
||||
"""Test getting optional user with no token"""
|
||||
# Call the dependency with no token
|
||||
user = get_optional_current_user(db=db_session, token=None)
|
||||
test_engine, AsyncTestingSessionLocal = async_test_db
|
||||
async with AsyncTestingSessionLocal() as session:
|
||||
# Call the dependency with no token
|
||||
user = await get_optional_current_user(db=session, token=None)
|
||||
|
||||
# Should return None
|
||||
assert user is None
|
||||
# Should return None
|
||||
assert user is None
|
||||
|
||||
def test_get_optional_current_user_invalid_token(self, db_session, mock_token):
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_optional_current_user_invalid_token(self, async_test_db, 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")
|
||||
test_engine, AsyncTestingSessionLocal = async_test_db
|
||||
async with AsyncTestingSessionLocal() as session:
|
||||
# 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)
|
||||
# Call the dependency
|
||||
user = await get_optional_current_user(db=session, token=mock_token)
|
||||
|
||||
# Should return None, not raise an exception
|
||||
assert user is None
|
||||
# Should return None, not raise an exception
|
||||
assert user is None
|
||||
|
||||
def test_get_optional_current_user_expired_token(self, db_session, mock_token):
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_optional_current_user_expired_token(self, async_test_db, 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")
|
||||
test_engine, AsyncTestingSessionLocal = async_test_db
|
||||
async with AsyncTestingSessionLocal() as session:
|
||||
# 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)
|
||||
# Call the dependency
|
||||
user = await get_optional_current_user(db=session, token=mock_token)
|
||||
|
||||
# Should return None, not raise an exception
|
||||
assert user is None
|
||||
# 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):
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_optional_current_user_inactive(self, async_test_db, async_mock_user, mock_token):
|
||||
"""Test getting optional user when user is inactive"""
|
||||
# Make the user inactive
|
||||
mock_user.is_active = False
|
||||
db_session.commit()
|
||||
test_engine, AsyncTestingSessionLocal = async_test_db
|
||||
async with AsyncTestingSessionLocal() as session:
|
||||
# Get the user in this session and make it inactive
|
||||
from sqlalchemy import select
|
||||
result = await session.execute(select(User).where(User.id == async_mock_user.id))
|
||||
user_in_session = result.scalar_one_or_none()
|
||||
user_in_session.is_active = False
|
||||
await 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
|
||||
# Mock get_token_data
|
||||
with patch('app.api.dependencies.auth.get_token_data') as mock_get_data:
|
||||
mock_get_data.return_value.user_id = async_mock_user.id
|
||||
|
||||
# Call the dependency
|
||||
user = get_optional_current_user(db=db_session, token=mock_token)
|
||||
# Call the dependency
|
||||
user = await get_optional_current_user(db=session, token=mock_token)
|
||||
|
||||
# Should return None for inactive users
|
||||
assert user is None
|
||||
# Should return None for inactive users
|
||||
assert user is None
|
||||
|
||||
0
backend/tests/api/routes/__init__.py
Normal file → Executable file
0
backend/tests/api/routes/__init__.py
Normal file → Executable file
0
backend/tests/api/routes/test_auth.py
Normal file → Executable file
0
backend/tests/api/routes/test_auth.py
Normal file → Executable file
0
backend/tests/api/routes/test_health.py
Normal file → Executable file
0
backend/tests/api/routes/test_health.py
Normal file → Executable file
0
backend/tests/api/routes/test_rate_limiting.py
Normal file → Executable file
0
backend/tests/api/routes/test_rate_limiting.py
Normal file → Executable file
0
backend/tests/api/routes/test_users.py
Normal file → Executable file
0
backend/tests/api/routes/test_users.py
Normal file → Executable file
246
backend/tests/api/test_auth_dependencies.py
Normal file → Executable file
246
backend/tests/api/test_auth_dependencies.py
Normal file → Executable file
@@ -1,6 +1,8 @@
|
||||
# tests/api/dependencies/test_auth_dependencies.py
|
||||
import pytest
|
||||
from unittest.mock import patch, MagicMock
|
||||
import pytest_asyncio
|
||||
import uuid
|
||||
from unittest.mock import patch
|
||||
from fastapi import HTTPException
|
||||
|
||||
from app.api.dependencies.auth import (
|
||||
@@ -9,87 +11,129 @@ from app.api.dependencies.auth import (
|
||||
get_current_superuser,
|
||||
get_optional_current_user
|
||||
)
|
||||
from app.core.auth import TokenExpiredError, TokenInvalidError
|
||||
from app.core.auth import TokenExpiredError, TokenInvalidError, get_password_hash
|
||||
from app.models.user import User
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_token():
|
||||
"""Fixture providing a mock JWT token"""
|
||||
return "mock.jwt.token"
|
||||
|
||||
|
||||
@pytest_asyncio.fixture
|
||||
async def async_mock_user(async_test_db):
|
||||
"""Async fixture to create and return a mock User instance."""
|
||||
test_engine, AsyncTestingSessionLocal = async_test_db
|
||||
async with AsyncTestingSessionLocal() as session:
|
||||
mock_user = User(
|
||||
id=uuid.uuid4(),
|
||||
email="mockuser@example.com",
|
||||
password_hash=get_password_hash("mockhashedpassword"),
|
||||
first_name="Mock",
|
||||
last_name="User",
|
||||
phone_number="1234567890",
|
||||
is_active=True,
|
||||
is_superuser=False,
|
||||
preferences=None,
|
||||
)
|
||||
session.add(mock_user)
|
||||
await session.commit()
|
||||
await session.refresh(mock_user)
|
||||
return mock_user
|
||||
|
||||
|
||||
class TestGetCurrentUser:
|
||||
"""Tests for get_current_user dependency"""
|
||||
|
||||
def test_get_current_user_success(self, db_session, mock_user, mock_token):
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_current_user_success(self, async_test_db, async_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
|
||||
test_engine, AsyncTestingSessionLocal = async_test_db
|
||||
async with AsyncTestingSessionLocal() as session:
|
||||
# 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 = async_mock_user.id
|
||||
|
||||
# Call the dependency
|
||||
user = get_current_user(db=db_session, token=mock_token)
|
||||
# Call the dependency
|
||||
user = await get_current_user(db=session, token=mock_token)
|
||||
|
||||
# Verify the correct user was returned
|
||||
assert user.id == mock_user.id
|
||||
assert user.email == mock_user.email
|
||||
# Verify the correct user was returned
|
||||
assert user.id == async_mock_user.id
|
||||
assert user.email == async_mock_user.email
|
||||
|
||||
def test_get_current_user_nonexistent(self, db_session, mock_token):
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_current_user_nonexistent(self, async_test_db, 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")
|
||||
test_engine, AsyncTestingSessionLocal = async_test_db
|
||||
async with AsyncTestingSessionLocal() as session:
|
||||
# Mock get_token_data to return a non-existent user ID
|
||||
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
|
||||
with patch('app.api.dependencies.auth.get_token_data') as mock_get_data:
|
||||
mock_get_data.return_value.user_id = nonexistent_id
|
||||
|
||||
# Should raise HTTPException with 404 status
|
||||
with pytest.raises(HTTPException) as exc_info:
|
||||
get_current_user(db=db_session, token=mock_token)
|
||||
# Should raise HTTPException with 404 status
|
||||
with pytest.raises(HTTPException) as exc_info:
|
||||
await get_current_user(db=session, token=mock_token)
|
||||
|
||||
assert exc_info.value.status_code == 404
|
||||
assert exc_info.value.status_code == 404
|
||||
assert "User not found" in exc_info.value.detail
|
||||
|
||||
def test_get_current_user_inactive(self, db_session, mock_user, mock_token):
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_current_user_inactive(self, async_test_db, async_mock_user, mock_token):
|
||||
"""Test when the user is inactive"""
|
||||
# Make the user inactive
|
||||
mock_user.is_active = False
|
||||
db_session.commit()
|
||||
test_engine, AsyncTestingSessionLocal = async_test_db
|
||||
async with AsyncTestingSessionLocal() as session:
|
||||
# Get the user in this session and make it inactive
|
||||
from sqlalchemy import select
|
||||
result = await session.execute(select(User).where(User.id == async_mock_user.id))
|
||||
user_in_session = result.scalar_one_or_none()
|
||||
user_in_session.is_active = False
|
||||
await 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
|
||||
# Mock get_token_data
|
||||
with patch('app.api.dependencies.auth.get_token_data') as mock_get_data:
|
||||
mock_get_data.return_value.user_id = async_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)
|
||||
# Should raise HTTPException with 403 status
|
||||
with pytest.raises(HTTPException) as exc_info:
|
||||
await get_current_user(db=session, token=mock_token)
|
||||
|
||||
assert exc_info.value.status_code == 403
|
||||
assert exc_info.value.status_code == 403
|
||||
assert "Inactive user" in exc_info.value.detail
|
||||
|
||||
def test_get_current_user_expired_token(self, db_session, mock_token):
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_current_user_expired_token(self, async_test_db, 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")
|
||||
test_engine, AsyncTestingSessionLocal = async_test_db
|
||||
async with AsyncTestingSessionLocal() as session:
|
||||
# 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)
|
||||
# Should raise HTTPException with 401 status
|
||||
with pytest.raises(HTTPException) as exc_info:
|
||||
await get_current_user(db=session, token=mock_token)
|
||||
|
||||
assert exc_info.value.status_code == 401
|
||||
assert "Token expired" in exc_info.value.detail
|
||||
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):
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_current_user_invalid_token(self, async_test_db, 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")
|
||||
test_engine, AsyncTestingSessionLocal = async_test_db
|
||||
async with AsyncTestingSessionLocal() as session:
|
||||
# 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)
|
||||
# Should raise HTTPException with 401 status
|
||||
with pytest.raises(HTTPException) as exc_info:
|
||||
await get_current_user(db=session, token=mock_token)
|
||||
|
||||
assert exc_info.value.status_code == 401
|
||||
assert "Could not validate credentials" in exc_info.value.detail
|
||||
assert exc_info.value.status_code == 401
|
||||
assert "Could not validate credentials" in exc_info.value.detail
|
||||
|
||||
|
||||
class TestGetCurrentActiveUser:
|
||||
@@ -149,63 +193,81 @@ class TestGetCurrentSuperuser:
|
||||
class TestGetOptionalCurrentUser:
|
||||
"""Tests for get_optional_current_user dependency"""
|
||||
|
||||
def test_get_optional_current_user_with_token(self, db_session, mock_user, mock_token):
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_optional_current_user_with_token(self, async_test_db, async_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
|
||||
test_engine, AsyncTestingSessionLocal = async_test_db
|
||||
async with AsyncTestingSessionLocal() as session:
|
||||
# Mock get_token_data
|
||||
with patch('app.api.dependencies.auth.get_token_data') as mock_get_data:
|
||||
mock_get_data.return_value.user_id = async_mock_user.id
|
||||
|
||||
# Call the dependency
|
||||
user = get_optional_current_user(db=db_session, token=mock_token)
|
||||
# Call the dependency
|
||||
user = await get_optional_current_user(db=session, token=mock_token)
|
||||
|
||||
# Should return the correct user
|
||||
assert user is not None
|
||||
assert user.id == mock_user.id
|
||||
# Should return the correct user
|
||||
assert user is not None
|
||||
assert user.id == async_mock_user.id
|
||||
|
||||
def test_get_optional_current_user_no_token(self, db_session):
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_optional_current_user_no_token(self, async_test_db):
|
||||
"""Test getting optional user with no token"""
|
||||
# Call the dependency with no token
|
||||
user = get_optional_current_user(db=db_session, token=None)
|
||||
test_engine, AsyncTestingSessionLocal = async_test_db
|
||||
async with AsyncTestingSessionLocal() as session:
|
||||
# Call the dependency with no token
|
||||
user = await get_optional_current_user(db=session, token=None)
|
||||
|
||||
# Should return None
|
||||
assert user is None
|
||||
# Should return None
|
||||
assert user is None
|
||||
|
||||
def test_get_optional_current_user_invalid_token(self, db_session, mock_token):
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_optional_current_user_invalid_token(self, async_test_db, 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")
|
||||
test_engine, AsyncTestingSessionLocal = async_test_db
|
||||
async with AsyncTestingSessionLocal() as session:
|
||||
# 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)
|
||||
# Call the dependency
|
||||
user = await get_optional_current_user(db=session, token=mock_token)
|
||||
|
||||
# Should return None, not raise an exception
|
||||
assert user is None
|
||||
# Should return None, not raise an exception
|
||||
assert user is None
|
||||
|
||||
def test_get_optional_current_user_expired_token(self, db_session, mock_token):
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_optional_current_user_expired_token(self, async_test_db, 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")
|
||||
test_engine, AsyncTestingSessionLocal = async_test_db
|
||||
async with AsyncTestingSessionLocal() as session:
|
||||
# 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)
|
||||
# Call the dependency
|
||||
user = await get_optional_current_user(db=session, token=mock_token)
|
||||
|
||||
# Should return None, not raise an exception
|
||||
assert user is None
|
||||
# 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):
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_optional_current_user_inactive(self, async_test_db, async_mock_user, mock_token):
|
||||
"""Test getting optional user when user is inactive"""
|
||||
# Make the user inactive
|
||||
mock_user.is_active = False
|
||||
db_session.commit()
|
||||
test_engine, AsyncTestingSessionLocal = async_test_db
|
||||
async with AsyncTestingSessionLocal() as session:
|
||||
# Get the user in this session and make it inactive
|
||||
from sqlalchemy import select
|
||||
result = await session.execute(select(User).where(User.id == async_mock_user.id))
|
||||
user_in_session = result.scalar_one_or_none()
|
||||
user_in_session.is_active = False
|
||||
await 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
|
||||
# Mock get_token_data
|
||||
with patch('app.api.dependencies.auth.get_token_data') as mock_get_data:
|
||||
mock_get_data.return_value.user_id = async_mock_user.id
|
||||
|
||||
# Call the dependency
|
||||
user = get_optional_current_user(db=db_session, token=mock_token)
|
||||
# Call the dependency
|
||||
user = await get_optional_current_user(db=session, token=mock_token)
|
||||
|
||||
# Should return None for inactive users
|
||||
assert user is None
|
||||
# Should return None for inactive users
|
||||
assert user is None
|
||||
|
||||
161
backend/tests/api/test_auth_endpoints.py
Normal file → Executable file
161
backend/tests/api/test_auth_endpoints.py
Normal file → Executable file
@@ -3,8 +3,10 @@
|
||||
Tests for authentication endpoints.
|
||||
"""
|
||||
import pytest
|
||||
import pytest_asyncio
|
||||
from unittest.mock import patch, MagicMock
|
||||
from fastapi import status
|
||||
from sqlalchemy import select
|
||||
|
||||
from app.models.user import User
|
||||
from app.schemas.users import UserCreate
|
||||
@@ -21,9 +23,10 @@ def disable_rate_limit():
|
||||
class TestRegisterEndpoint:
|
||||
"""Tests for POST /auth/register endpoint."""
|
||||
|
||||
def test_register_success(self, client, test_db):
|
||||
@pytest.mark.asyncio
|
||||
async def test_register_success(self, client):
|
||||
"""Test successful user registration."""
|
||||
response = client.post(
|
||||
response = await client.post(
|
||||
"/api/v1/auth/register",
|
||||
json={
|
||||
"email": "newuser@example.com",
|
||||
@@ -39,12 +42,13 @@ class TestRegisterEndpoint:
|
||||
assert data["first_name"] == "New"
|
||||
assert "password" not in data
|
||||
|
||||
def test_register_duplicate_email(self, client, test_user):
|
||||
@pytest.mark.asyncio
|
||||
async def test_register_duplicate_email(self, client, async_test_user):
|
||||
"""Test registering with existing email."""
|
||||
response = client.post(
|
||||
response = await client.post(
|
||||
"/api/v1/auth/register",
|
||||
json={
|
||||
"email": test_user.email,
|
||||
"email": async_test_user.email,
|
||||
"password": "SecurePassword123",
|
||||
"first_name": "Duplicate",
|
||||
"last_name": "User"
|
||||
@@ -55,9 +59,10 @@ class TestRegisterEndpoint:
|
||||
data = response.json()
|
||||
assert data["success"] is False
|
||||
|
||||
def test_register_weak_password(self, client):
|
||||
@pytest.mark.asyncio
|
||||
async def test_register_weak_password(self, client):
|
||||
"""Test registration with weak password."""
|
||||
response = client.post(
|
||||
response = await client.post(
|
||||
"/api/v1/auth/register",
|
||||
json={
|
||||
"email": "weakpass@example.com",
|
||||
@@ -69,12 +74,13 @@ class TestRegisterEndpoint:
|
||||
|
||||
assert response.status_code == status.HTTP_422_UNPROCESSABLE_ENTITY
|
||||
|
||||
def test_register_unexpected_error(self, client, test_db):
|
||||
@pytest.mark.asyncio
|
||||
async def test_register_unexpected_error(self, client):
|
||||
"""Test registration with unexpected error."""
|
||||
with patch('app.services.auth_service.AuthService.create_user') as mock_create:
|
||||
mock_create.side_effect = Exception("Unexpected error")
|
||||
|
||||
response = client.post(
|
||||
response = await client.post(
|
||||
"/api/v1/auth/register",
|
||||
json={
|
||||
"email": "error@example.com",
|
||||
@@ -90,12 +96,13 @@ class TestRegisterEndpoint:
|
||||
class TestLoginEndpoint:
|
||||
"""Tests for POST /auth/login endpoint."""
|
||||
|
||||
def test_login_success(self, client, test_user):
|
||||
@pytest.mark.asyncio
|
||||
async def test_login_success(self, client, async_test_user):
|
||||
"""Test successful login."""
|
||||
response = client.post(
|
||||
response = await client.post(
|
||||
"/api/v1/auth/login",
|
||||
json={
|
||||
"email": test_user.email,
|
||||
"email": async_test_user.email,
|
||||
"password": "TestPassword123"
|
||||
}
|
||||
)
|
||||
@@ -106,21 +113,23 @@ class TestLoginEndpoint:
|
||||
assert "refresh_token" in data
|
||||
assert data["token_type"] == "bearer"
|
||||
|
||||
def test_login_wrong_password(self, client, test_user):
|
||||
@pytest.mark.asyncio
|
||||
async def test_login_wrong_password(self, client, async_test_user):
|
||||
"""Test login with wrong password."""
|
||||
response = client.post(
|
||||
response = await client.post(
|
||||
"/api/v1/auth/login",
|
||||
json={
|
||||
"email": test_user.email,
|
||||
"email": async_test_user.email,
|
||||
"password": "WrongPassword123"
|
||||
}
|
||||
)
|
||||
|
||||
assert response.status_code == status.HTTP_401_UNAUTHORIZED
|
||||
|
||||
def test_login_nonexistent_user(self, client):
|
||||
@pytest.mark.asyncio
|
||||
async def test_login_nonexistent_user(self, client):
|
||||
"""Test login with non-existent email."""
|
||||
response = client.post(
|
||||
response = await client.post(
|
||||
"/api/v1/auth/login",
|
||||
json={
|
||||
"email": "nonexistent@example.com",
|
||||
@@ -130,31 +139,37 @@ class TestLoginEndpoint:
|
||||
|
||||
assert response.status_code == status.HTTP_401_UNAUTHORIZED
|
||||
|
||||
def test_login_inactive_user(self, client, test_user, test_db):
|
||||
@pytest.mark.asyncio
|
||||
async def test_login_inactive_user(self, client, async_test_user, async_test_db):
|
||||
"""Test login with inactive user."""
|
||||
test_user.is_active = False
|
||||
test_db.add(test_user)
|
||||
test_db.commit()
|
||||
test_engine, AsyncTestingSessionLocal = async_test_db
|
||||
async with AsyncTestingSessionLocal() as session:
|
||||
# Get the user in this session and make it inactive
|
||||
result = await session.execute(select(User).where(User.id == async_test_user.id))
|
||||
user_in_session = result.scalar_one_or_none()
|
||||
user_in_session.is_active = False
|
||||
await session.commit()
|
||||
|
||||
response = client.post(
|
||||
response = await client.post(
|
||||
"/api/v1/auth/login",
|
||||
json={
|
||||
"email": test_user.email,
|
||||
"email": async_test_user.email,
|
||||
"password": "TestPassword123"
|
||||
}
|
||||
)
|
||||
|
||||
assert response.status_code == status.HTTP_401_UNAUTHORIZED
|
||||
|
||||
def test_login_unexpected_error(self, client, test_user):
|
||||
@pytest.mark.asyncio
|
||||
async def test_login_unexpected_error(self, client, async_test_user):
|
||||
"""Test login with unexpected error."""
|
||||
with patch('app.services.auth_service.AuthService.authenticate_user') as mock_auth:
|
||||
mock_auth.side_effect = Exception("Database error")
|
||||
|
||||
response = client.post(
|
||||
response = await client.post(
|
||||
"/api/v1/auth/login",
|
||||
json={
|
||||
"email": test_user.email,
|
||||
"email": async_test_user.email,
|
||||
"password": "TestPassword123"
|
||||
}
|
||||
)
|
||||
@@ -165,12 +180,13 @@ class TestLoginEndpoint:
|
||||
class TestOAuthLoginEndpoint:
|
||||
"""Tests for POST /auth/login/oauth endpoint."""
|
||||
|
||||
def test_oauth_login_success(self, client, test_user):
|
||||
@pytest.mark.asyncio
|
||||
async def test_oauth_login_success(self, client, async_test_user):
|
||||
"""Test successful OAuth login."""
|
||||
response = client.post(
|
||||
response = await client.post(
|
||||
"/api/v1/auth/login/oauth",
|
||||
data={
|
||||
"username": test_user.email,
|
||||
"username": async_test_user.email,
|
||||
"password": "TestPassword123"
|
||||
}
|
||||
)
|
||||
@@ -180,43 +196,50 @@ class TestOAuthLoginEndpoint:
|
||||
assert "access_token" in data
|
||||
assert "refresh_token" in data
|
||||
|
||||
def test_oauth_login_wrong_credentials(self, client, test_user):
|
||||
@pytest.mark.asyncio
|
||||
async def test_oauth_login_wrong_credentials(self, client, async_test_user):
|
||||
"""Test OAuth login with wrong credentials."""
|
||||
response = client.post(
|
||||
response = await client.post(
|
||||
"/api/v1/auth/login/oauth",
|
||||
data={
|
||||
"username": test_user.email,
|
||||
"username": async_test_user.email,
|
||||
"password": "WrongPassword"
|
||||
}
|
||||
)
|
||||
|
||||
assert response.status_code == status.HTTP_401_UNAUTHORIZED
|
||||
|
||||
def test_oauth_login_inactive_user(self, client, test_user, test_db):
|
||||
@pytest.mark.asyncio
|
||||
async def test_oauth_login_inactive_user(self, client, async_test_user, async_test_db):
|
||||
"""Test OAuth login with inactive user."""
|
||||
test_user.is_active = False
|
||||
test_db.add(test_user)
|
||||
test_db.commit()
|
||||
test_engine, AsyncTestingSessionLocal = async_test_db
|
||||
async with AsyncTestingSessionLocal() as session:
|
||||
# Get the user in this session and make it inactive
|
||||
result = await session.execute(select(User).where(User.id == async_test_user.id))
|
||||
user_in_session = result.scalar_one_or_none()
|
||||
user_in_session.is_active = False
|
||||
await session.commit()
|
||||
|
||||
response = client.post(
|
||||
response = await client.post(
|
||||
"/api/v1/auth/login/oauth",
|
||||
data={
|
||||
"username": test_user.email,
|
||||
"username": async_test_user.email,
|
||||
"password": "TestPassword123"
|
||||
}
|
||||
)
|
||||
|
||||
assert response.status_code == status.HTTP_401_UNAUTHORIZED
|
||||
|
||||
def test_oauth_login_unexpected_error(self, client, test_user):
|
||||
@pytest.mark.asyncio
|
||||
async def test_oauth_login_unexpected_error(self, client, async_test_user):
|
||||
"""Test OAuth login with unexpected error."""
|
||||
with patch('app.services.auth_service.AuthService.authenticate_user') as mock_auth:
|
||||
mock_auth.side_effect = Exception("Unexpected error")
|
||||
|
||||
response = client.post(
|
||||
response = await client.post(
|
||||
"/api/v1/auth/login/oauth",
|
||||
data={
|
||||
"username": test_user.email,
|
||||
"username": async_test_user.email,
|
||||
"password": "TestPassword123"
|
||||
}
|
||||
)
|
||||
@@ -227,20 +250,21 @@ class TestOAuthLoginEndpoint:
|
||||
class TestRefreshTokenEndpoint:
|
||||
"""Tests for POST /auth/refresh endpoint."""
|
||||
|
||||
def test_refresh_token_success(self, client, test_user):
|
||||
@pytest.mark.asyncio
|
||||
async def test_refresh_token_success(self, client, async_test_user):
|
||||
"""Test successful token refresh."""
|
||||
# First, login to get a refresh token
|
||||
login_response = client.post(
|
||||
login_response = await client.post(
|
||||
"/api/v1/auth/login",
|
||||
json={
|
||||
"email": test_user.email,
|
||||
"email": async_test_user.email,
|
||||
"password": "TestPassword123"
|
||||
}
|
||||
)
|
||||
refresh_token = login_response.json()["refresh_token"]
|
||||
|
||||
# Now refresh the token
|
||||
response = client.post(
|
||||
response = await client.post(
|
||||
"/api/v1/auth/refresh",
|
||||
json={"refresh_token": refresh_token}
|
||||
)
|
||||
@@ -250,36 +274,39 @@ class TestRefreshTokenEndpoint:
|
||||
assert "access_token" in data
|
||||
assert "refresh_token" in data
|
||||
|
||||
def test_refresh_token_expired(self, client):
|
||||
@pytest.mark.asyncio
|
||||
async def test_refresh_token_expired(self, client):
|
||||
"""Test refresh with expired token."""
|
||||
from app.core.auth import TokenExpiredError
|
||||
|
||||
with patch('app.services.auth_service.AuthService.refresh_tokens') as mock_refresh:
|
||||
mock_refresh.side_effect = TokenExpiredError("Token expired")
|
||||
|
||||
response = client.post(
|
||||
response = await client.post(
|
||||
"/api/v1/auth/refresh",
|
||||
json={"refresh_token": "some_token"}
|
||||
)
|
||||
|
||||
assert response.status_code == status.HTTP_401_UNAUTHORIZED
|
||||
|
||||
def test_refresh_token_invalid(self, client):
|
||||
@pytest.mark.asyncio
|
||||
async def test_refresh_token_invalid(self, client):
|
||||
"""Test refresh with invalid token."""
|
||||
response = client.post(
|
||||
response = await client.post(
|
||||
"/api/v1/auth/refresh",
|
||||
json={"refresh_token": "invalid_token"}
|
||||
)
|
||||
|
||||
assert response.status_code == status.HTTP_401_UNAUTHORIZED
|
||||
|
||||
def test_refresh_token_unexpected_error(self, client, test_user):
|
||||
@pytest.mark.asyncio
|
||||
async def test_refresh_token_unexpected_error(self, client, async_test_user):
|
||||
"""Test refresh with unexpected error."""
|
||||
# Get a valid refresh token first
|
||||
login_response = client.post(
|
||||
login_response = await client.post(
|
||||
"/api/v1/auth/login",
|
||||
json={
|
||||
"email": test_user.email,
|
||||
"email": async_test_user.email,
|
||||
"password": "TestPassword123"
|
||||
}
|
||||
)
|
||||
@@ -288,7 +315,7 @@ class TestRefreshTokenEndpoint:
|
||||
with patch('app.services.auth_service.AuthService.refresh_tokens') as mock_refresh:
|
||||
mock_refresh.side_effect = Exception("Unexpected error")
|
||||
|
||||
response = client.post(
|
||||
response = await client.post(
|
||||
"/api/v1/auth/refresh",
|
||||
json={"refresh_token": refresh_token}
|
||||
)
|
||||
@@ -299,48 +326,52 @@ class TestRefreshTokenEndpoint:
|
||||
class TestGetCurrentUserEndpoint:
|
||||
"""Tests for GET /auth/me endpoint."""
|
||||
|
||||
def test_get_current_user_success(self, client, test_user):
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_current_user_success(self, client, async_test_user):
|
||||
"""Test getting current user info."""
|
||||
# First, login to get an access token
|
||||
login_response = client.post(
|
||||
login_response = await client.post(
|
||||
"/api/v1/auth/login",
|
||||
json={
|
||||
"email": test_user.email,
|
||||
"email": async_test_user.email,
|
||||
"password": "TestPassword123"
|
||||
}
|
||||
)
|
||||
access_token = login_response.json()["access_token"]
|
||||
|
||||
# Get current user info
|
||||
response = client.get(
|
||||
response = await client.get(
|
||||
"/api/v1/auth/me",
|
||||
headers={"Authorization": f"Bearer {access_token}"}
|
||||
)
|
||||
|
||||
assert response.status_code == status.HTTP_200_OK
|
||||
data = response.json()
|
||||
assert data["email"] == test_user.email
|
||||
assert data["first_name"] == test_user.first_name
|
||||
assert data["email"] == async_test_user.email
|
||||
assert data["first_name"] == async_test_user.first_name
|
||||
|
||||
def test_get_current_user_no_token(self, client):
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_current_user_no_token(self, client):
|
||||
"""Test getting current user without token."""
|
||||
response = client.get("/api/v1/auth/me")
|
||||
response = await client.get("/api/v1/auth/me")
|
||||
|
||||
assert response.status_code == status.HTTP_401_UNAUTHORIZED
|
||||
|
||||
def test_get_current_user_invalid_token(self, client):
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_current_user_invalid_token(self, client):
|
||||
"""Test getting current user with invalid token."""
|
||||
response = client.get(
|
||||
response = await client.get(
|
||||
"/api/v1/auth/me",
|
||||
headers={"Authorization": "Bearer invalid_token"}
|
||||
)
|
||||
|
||||
assert response.status_code == status.HTTP_401_UNAUTHORIZED
|
||||
|
||||
def test_get_current_user_expired_token(self, client):
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_current_user_expired_token(self, client):
|
||||
"""Test getting current user with expired token."""
|
||||
# Use a clearly invalid/malformed token
|
||||
response = client.get(
|
||||
response = await client.get(
|
||||
"/api/v1/auth/me",
|
||||
headers={"Authorization": "Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.invalid"}
|
||||
)
|
||||
|
||||
152
backend/tests/api/test_auth_password_reset.py
Normal file → Executable file
152
backend/tests/api/test_auth_password_reset.py
Normal file → Executable file
@@ -3,11 +3,14 @@
|
||||
Tests for password reset endpoints.
|
||||
"""
|
||||
import pytest
|
||||
import pytest_asyncio
|
||||
from unittest.mock import patch, AsyncMock, MagicMock
|
||||
from fastapi import status
|
||||
from sqlalchemy import select
|
||||
|
||||
from app.schemas.users import PasswordResetRequest, PasswordResetConfirm
|
||||
from app.utils.security import create_password_reset_token
|
||||
from app.models.user import User
|
||||
|
||||
|
||||
# Disable rate limiting for tests
|
||||
@@ -22,14 +25,14 @@ class TestPasswordResetRequest:
|
||||
"""Tests for POST /auth/password-reset/request endpoint."""
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_password_reset_request_valid_email(self, client, test_user):
|
||||
async def test_password_reset_request_valid_email(self, client, async_test_user):
|
||||
"""Test password reset request with valid email."""
|
||||
with patch('app.api.routes.auth.email_service.send_password_reset_email') as mock_send:
|
||||
mock_send.return_value = True
|
||||
|
||||
response = client.post(
|
||||
response = await client.post(
|
||||
"/api/v1/auth/password-reset/request",
|
||||
json={"email": test_user.email}
|
||||
json={"email": async_test_user.email}
|
||||
)
|
||||
|
||||
assert response.status_code == status.HTTP_200_OK
|
||||
@@ -40,15 +43,15 @@ class TestPasswordResetRequest:
|
||||
# Verify email was sent
|
||||
mock_send.assert_called_once()
|
||||
call_args = mock_send.call_args
|
||||
assert call_args.kwargs["to_email"] == test_user.email
|
||||
assert call_args.kwargs["user_name"] == test_user.first_name
|
||||
assert call_args.kwargs["to_email"] == async_test_user.email
|
||||
assert call_args.kwargs["user_name"] == async_test_user.first_name
|
||||
assert "reset_token" in call_args.kwargs
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_password_reset_request_nonexistent_email(self, client):
|
||||
"""Test password reset request with non-existent email."""
|
||||
with patch('app.api.routes.auth.email_service.send_password_reset_email') as mock_send:
|
||||
response = client.post(
|
||||
response = await client.post(
|
||||
"/api/v1/auth/password-reset/request",
|
||||
json={"email": "nonexistent@example.com"}
|
||||
)
|
||||
@@ -62,17 +65,20 @@ class TestPasswordResetRequest:
|
||||
mock_send.assert_not_called()
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_password_reset_request_inactive_user(self, client, test_db, test_user):
|
||||
async def test_password_reset_request_inactive_user(self, client, async_test_db, async_test_user):
|
||||
"""Test password reset request with inactive user."""
|
||||
# Deactivate user
|
||||
test_user.is_active = False
|
||||
test_db.add(test_user)
|
||||
test_db.commit()
|
||||
test_engine, AsyncTestingSessionLocal = async_test_db
|
||||
async with AsyncTestingSessionLocal() as session:
|
||||
result = await session.execute(select(User).where(User.id == async_test_user.id))
|
||||
user_in_session = result.scalar_one_or_none()
|
||||
user_in_session.is_active = False
|
||||
await session.commit()
|
||||
|
||||
with patch('app.api.routes.auth.email_service.send_password_reset_email') as mock_send:
|
||||
response = client.post(
|
||||
response = await client.post(
|
||||
"/api/v1/auth/password-reset/request",
|
||||
json={"email": test_user.email}
|
||||
json={"email": async_test_user.email}
|
||||
)
|
||||
|
||||
# Should still return success to prevent email enumeration
|
||||
@@ -86,7 +92,7 @@ class TestPasswordResetRequest:
|
||||
@pytest.mark.asyncio
|
||||
async def test_password_reset_request_invalid_email_format(self, client):
|
||||
"""Test password reset request with invalid email format."""
|
||||
response = client.post(
|
||||
response = await client.post(
|
||||
"/api/v1/auth/password-reset/request",
|
||||
json={"email": "not-an-email"}
|
||||
)
|
||||
@@ -96,7 +102,7 @@ class TestPasswordResetRequest:
|
||||
@pytest.mark.asyncio
|
||||
async def test_password_reset_request_missing_email(self, client):
|
||||
"""Test password reset request without email."""
|
||||
response = client.post(
|
||||
response = await client.post(
|
||||
"/api/v1/auth/password-reset/request",
|
||||
json={}
|
||||
)
|
||||
@@ -104,14 +110,14 @@ class TestPasswordResetRequest:
|
||||
assert response.status_code == status.HTTP_422_UNPROCESSABLE_ENTITY
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_password_reset_request_email_service_error(self, client, test_user):
|
||||
async def test_password_reset_request_email_service_error(self, client, async_test_user):
|
||||
"""Test password reset when email service fails."""
|
||||
with patch('app.api.routes.auth.email_service.send_password_reset_email') as mock_send:
|
||||
mock_send.side_effect = Exception("SMTP Error")
|
||||
|
||||
response = client.post(
|
||||
response = await client.post(
|
||||
"/api/v1/auth/password-reset/request",
|
||||
json={"email": test_user.email}
|
||||
json={"email": async_test_user.email}
|
||||
)
|
||||
|
||||
# Should still return success even if email fails
|
||||
@@ -120,16 +126,16 @@ class TestPasswordResetRequest:
|
||||
assert data["success"] is True
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_password_reset_request_rate_limiting(self, client, test_user):
|
||||
async def test_password_reset_request_rate_limiting(self, client, async_test_user):
|
||||
"""Test that password reset requests are rate limited."""
|
||||
with patch('app.api.routes.auth.email_service.send_password_reset_email') as mock_send:
|
||||
mock_send.return_value = True
|
||||
|
||||
# Make multiple requests quickly (3/minute limit)
|
||||
for _ in range(3):
|
||||
response = client.post(
|
||||
response = await client.post(
|
||||
"/api/v1/auth/password-reset/request",
|
||||
json={"email": test_user.email}
|
||||
json={"email": async_test_user.email}
|
||||
)
|
||||
assert response.status_code == status.HTTP_200_OK
|
||||
|
||||
@@ -137,13 +143,14 @@ class TestPasswordResetRequest:
|
||||
class TestPasswordResetConfirm:
|
||||
"""Tests for POST /auth/password-reset/confirm endpoint."""
|
||||
|
||||
def test_password_reset_confirm_valid_token(self, client, test_user, test_db):
|
||||
@pytest.mark.asyncio
|
||||
async def test_password_reset_confirm_valid_token(self, client, async_test_user, async_test_db):
|
||||
"""Test password reset confirmation with valid token."""
|
||||
# Generate valid token
|
||||
token = create_password_reset_token(test_user.email)
|
||||
token = create_password_reset_token(async_test_user.email)
|
||||
new_password = "NewSecure123"
|
||||
|
||||
response = client.post(
|
||||
response = await client.post(
|
||||
"/api/v1/auth/password-reset/confirm",
|
||||
json={
|
||||
"token": token,
|
||||
@@ -157,21 +164,25 @@ class TestPasswordResetConfirm:
|
||||
assert "successfully" in data["message"].lower()
|
||||
|
||||
# Verify user can login with new password
|
||||
test_db.refresh(test_user)
|
||||
from app.core.auth import verify_password
|
||||
assert verify_password(new_password, test_user.password_hash) is True
|
||||
test_engine, AsyncTestingSessionLocal = async_test_db
|
||||
async with AsyncTestingSessionLocal() as session:
|
||||
result = await session.execute(select(User).where(User.id == async_test_user.id))
|
||||
updated_user = result.scalar_one_or_none()
|
||||
from app.core.auth import verify_password
|
||||
assert verify_password(new_password, updated_user.password_hash) is True
|
||||
|
||||
def test_password_reset_confirm_expired_token(self, client, test_user):
|
||||
@pytest.mark.asyncio
|
||||
async def test_password_reset_confirm_expired_token(self, client, async_test_user):
|
||||
"""Test password reset confirmation with expired token."""
|
||||
import time as time_module
|
||||
|
||||
# Create token that expires immediately
|
||||
token = create_password_reset_token(test_user.email, expires_in=1)
|
||||
token = create_password_reset_token(async_test_user.email, expires_in=1)
|
||||
|
||||
# Wait for token to expire
|
||||
time_module.sleep(2)
|
||||
|
||||
response = client.post(
|
||||
response = await client.post(
|
||||
"/api/v1/auth/password-reset/confirm",
|
||||
json={
|
||||
"token": token,
|
||||
@@ -186,9 +197,10 @@ class TestPasswordResetConfirm:
|
||||
error_msg = data["errors"][0]["message"].lower() if "errors" in data else ""
|
||||
assert "invalid" in error_msg or "expired" in error_msg
|
||||
|
||||
def test_password_reset_confirm_invalid_token(self, client):
|
||||
@pytest.mark.asyncio
|
||||
async def test_password_reset_confirm_invalid_token(self, client):
|
||||
"""Test password reset confirmation with invalid token."""
|
||||
response = client.post(
|
||||
response = await client.post(
|
||||
"/api/v1/auth/password-reset/confirm",
|
||||
json={
|
||||
"token": "invalid_token_xyz",
|
||||
@@ -202,13 +214,14 @@ class TestPasswordResetConfirm:
|
||||
error_msg = data["errors"][0]["message"].lower() if "errors" in data else ""
|
||||
assert "invalid" in error_msg or "expired" in error_msg
|
||||
|
||||
def test_password_reset_confirm_tampered_token(self, client, test_user):
|
||||
@pytest.mark.asyncio
|
||||
async def test_password_reset_confirm_tampered_token(self, client, async_test_user):
|
||||
"""Test password reset confirmation with tampered token."""
|
||||
import base64
|
||||
import json
|
||||
|
||||
# Create valid token and tamper with it
|
||||
token = create_password_reset_token(test_user.email)
|
||||
token = create_password_reset_token(async_test_user.email)
|
||||
decoded = base64.urlsafe_b64decode(token.encode('utf-8')).decode('utf-8')
|
||||
token_data = json.loads(decoded)
|
||||
token_data["payload"]["email"] = "hacker@example.com"
|
||||
@@ -216,7 +229,7 @@ class TestPasswordResetConfirm:
|
||||
# Re-encode tampered token
|
||||
tampered = base64.urlsafe_b64encode(json.dumps(token_data).encode('utf-8')).decode('utf-8')
|
||||
|
||||
response = client.post(
|
||||
response = await client.post(
|
||||
"/api/v1/auth/password-reset/confirm",
|
||||
json={
|
||||
"token": tampered,
|
||||
@@ -226,12 +239,13 @@ class TestPasswordResetConfirm:
|
||||
|
||||
assert response.status_code == status.HTTP_400_BAD_REQUEST
|
||||
|
||||
def test_password_reset_confirm_nonexistent_user(self, client):
|
||||
@pytest.mark.asyncio
|
||||
async def test_password_reset_confirm_nonexistent_user(self, client):
|
||||
"""Test password reset confirmation for non-existent user."""
|
||||
# Create token for email that doesn't exist
|
||||
token = create_password_reset_token("nonexistent@example.com")
|
||||
|
||||
response = client.post(
|
||||
response = await client.post(
|
||||
"/api/v1/auth/password-reset/confirm",
|
||||
json={
|
||||
"token": token,
|
||||
@@ -245,16 +259,20 @@ class TestPasswordResetConfirm:
|
||||
error_msg = data["errors"][0]["message"].lower() if "errors" in data else ""
|
||||
assert "not found" in error_msg
|
||||
|
||||
def test_password_reset_confirm_inactive_user(self, client, test_user, test_db):
|
||||
@pytest.mark.asyncio
|
||||
async def test_password_reset_confirm_inactive_user(self, client, async_test_user, async_test_db):
|
||||
"""Test password reset confirmation for inactive user."""
|
||||
# Deactivate user
|
||||
test_user.is_active = False
|
||||
test_db.add(test_user)
|
||||
test_db.commit()
|
||||
test_engine, AsyncTestingSessionLocal = async_test_db
|
||||
async with AsyncTestingSessionLocal() as session:
|
||||
result = await session.execute(select(User).where(User.id == async_test_user.id))
|
||||
user_in_session = result.scalar_one_or_none()
|
||||
user_in_session.is_active = False
|
||||
await session.commit()
|
||||
|
||||
token = create_password_reset_token(test_user.email)
|
||||
token = create_password_reset_token(async_test_user.email)
|
||||
|
||||
response = client.post(
|
||||
response = await client.post(
|
||||
"/api/v1/auth/password-reset/confirm",
|
||||
json={
|
||||
"token": token,
|
||||
@@ -268,9 +286,10 @@ class TestPasswordResetConfirm:
|
||||
error_msg = data["errors"][0]["message"].lower() if "errors" in data else ""
|
||||
assert "inactive" in error_msg
|
||||
|
||||
def test_password_reset_confirm_weak_password(self, client, test_user):
|
||||
@pytest.mark.asyncio
|
||||
async def test_password_reset_confirm_weak_password(self, client, async_test_user):
|
||||
"""Test password reset confirmation with weak password."""
|
||||
token = create_password_reset_token(test_user.email)
|
||||
token = create_password_reset_token(async_test_user.email)
|
||||
|
||||
# Test various weak passwords
|
||||
weak_passwords = [
|
||||
@@ -280,7 +299,7 @@ class TestPasswordResetConfirm:
|
||||
]
|
||||
|
||||
for weak_password in weak_passwords:
|
||||
response = client.post(
|
||||
response = await client.post(
|
||||
"/api/v1/auth/password-reset/confirm",
|
||||
json={
|
||||
"token": token,
|
||||
@@ -290,10 +309,11 @@ class TestPasswordResetConfirm:
|
||||
|
||||
assert response.status_code == status.HTTP_422_UNPROCESSABLE_ENTITY
|
||||
|
||||
def test_password_reset_confirm_missing_fields(self, client):
|
||||
@pytest.mark.asyncio
|
||||
async def test_password_reset_confirm_missing_fields(self, client):
|
||||
"""Test password reset confirmation with missing fields."""
|
||||
# Missing token
|
||||
response = client.post(
|
||||
response = await client.post(
|
||||
"/api/v1/auth/password-reset/confirm",
|
||||
json={"new_password": "NewSecure123"}
|
||||
)
|
||||
@@ -301,20 +321,22 @@ class TestPasswordResetConfirm:
|
||||
|
||||
# Missing password
|
||||
token = create_password_reset_token("test@example.com")
|
||||
response = client.post(
|
||||
response = await client.post(
|
||||
"/api/v1/auth/password-reset/confirm",
|
||||
json={"token": token}
|
||||
)
|
||||
assert response.status_code == status.HTTP_422_UNPROCESSABLE_ENTITY
|
||||
|
||||
def test_password_reset_confirm_database_error(self, client, test_user, test_db):
|
||||
@pytest.mark.asyncio
|
||||
async def test_password_reset_confirm_database_error(self, client, async_test_user):
|
||||
"""Test password reset confirmation with database error."""
|
||||
token = create_password_reset_token(test_user.email)
|
||||
token = create_password_reset_token(async_test_user.email)
|
||||
|
||||
with patch.object(test_db, 'commit') as mock_commit:
|
||||
mock_commit.side_effect = Exception("Database error")
|
||||
# Mock the password update to raise an exception
|
||||
with patch('app.api.routes.auth.user_crud.update') as mock_update:
|
||||
mock_update.side_effect = Exception("Database error")
|
||||
|
||||
response = client.post(
|
||||
response = await client.post(
|
||||
"/api/v1/auth/password-reset/confirm",
|
||||
json={
|
||||
"token": token,
|
||||
@@ -328,18 +350,19 @@ class TestPasswordResetConfirm:
|
||||
error_msg = data["errors"][0]["message"].lower() if "errors" in data else ""
|
||||
assert "error" in error_msg or "resetting" in error_msg
|
||||
|
||||
def test_password_reset_full_flow(self, client, test_user, test_db):
|
||||
@pytest.mark.asyncio
|
||||
async def test_password_reset_full_flow(self, client, async_test_user, async_test_db):
|
||||
"""Test complete password reset flow."""
|
||||
original_password = test_user.password_hash
|
||||
original_password = async_test_user.password_hash
|
||||
new_password = "BrandNew123"
|
||||
|
||||
# Step 1: Request password reset
|
||||
with patch('app.api.routes.auth.email_service.send_password_reset_email') as mock_send:
|
||||
mock_send.return_value = True
|
||||
|
||||
response = client.post(
|
||||
response = await client.post(
|
||||
"/api/v1/auth/password-reset/request",
|
||||
json={"email": test_user.email}
|
||||
json={"email": async_test_user.email}
|
||||
)
|
||||
|
||||
assert response.status_code == status.HTTP_200_OK
|
||||
@@ -349,7 +372,7 @@ class TestPasswordResetConfirm:
|
||||
reset_token = call_args.kwargs["reset_token"]
|
||||
|
||||
# Step 2: Confirm password reset
|
||||
response = client.post(
|
||||
response = await client.post(
|
||||
"/api/v1/auth/password-reset/confirm",
|
||||
json={
|
||||
"token": reset_token,
|
||||
@@ -360,15 +383,18 @@ class TestPasswordResetConfirm:
|
||||
assert response.status_code == status.HTTP_200_OK
|
||||
|
||||
# Step 3: Verify old password doesn't work
|
||||
test_db.refresh(test_user)
|
||||
from app.core.auth import verify_password
|
||||
assert test_user.password_hash != original_password
|
||||
test_engine, AsyncTestingSessionLocal = async_test_db
|
||||
async with AsyncTestingSessionLocal() as session:
|
||||
result = await session.execute(select(User).where(User.id == async_test_user.id))
|
||||
updated_user = result.scalar_one_or_none()
|
||||
from app.core.auth import verify_password
|
||||
assert updated_user.password_hash != original_password
|
||||
|
||||
# Step 4: Verify new password works
|
||||
response = client.post(
|
||||
response = await client.post(
|
||||
"/api/v1/auth/login",
|
||||
json={
|
||||
"email": test_user.email,
|
||||
"email": async_test_user.email,
|
||||
"password": new_password
|
||||
}
|
||||
)
|
||||
|
||||
0
backend/tests/api/test_security_headers.py
Normal file → Executable file
0
backend/tests/api/test_security_headers.py
Normal file → Executable file
0
backend/tests/api/test_session_management.py
Normal file → Executable file
0
backend/tests/api/test_session_management.py
Normal file → Executable file
254
backend/tests/api/test_user_routes.py
Normal file → Executable file
254
backend/tests/api/test_user_routes.py
Normal file → Executable file
@@ -4,10 +4,13 @@ Comprehensive tests for user management endpoints.
|
||||
These tests focus on finding potential bugs, not just coverage.
|
||||
"""
|
||||
import pytest
|
||||
import pytest_asyncio
|
||||
from unittest.mock import patch
|
||||
from fastapi import status
|
||||
import uuid
|
||||
|
||||
from sqlalchemy import select
|
||||
from app.models.user import User
|
||||
from app.models.user import User
|
||||
from app.schemas.users import UserUpdate
|
||||
|
||||
@@ -21,9 +24,9 @@ def disable_rate_limit():
|
||||
yield
|
||||
|
||||
|
||||
def get_auth_headers(client, email, password):
|
||||
async def get_auth_headers(client, email, password):
|
||||
"""Helper to get authentication headers."""
|
||||
response = client.post(
|
||||
response = await client.post(
|
||||
"/api/v1/auth/login",
|
||||
json={"email": email, "password": password}
|
||||
)
|
||||
@@ -34,11 +37,12 @@ def get_auth_headers(client, email, password):
|
||||
class TestListUsers:
|
||||
"""Tests for GET /users endpoint."""
|
||||
|
||||
def test_list_users_as_superuser(self, client, test_superuser):
|
||||
@pytest.mark.asyncio
|
||||
async def test_list_users_as_superuser(self, client, async_test_superuser):
|
||||
"""Test listing users as superuser."""
|
||||
headers = get_auth_headers(client, test_superuser.email, "SuperPassword123")
|
||||
headers = await get_auth_headers(client, async_test_superuser.email, "SuperPassword123")
|
||||
|
||||
response = client.get("/api/v1/users", headers=headers)
|
||||
response = await client.get("/api/v1/users", headers=headers)
|
||||
|
||||
assert response.status_code == status.HTTP_200_OK
|
||||
data = response.json()
|
||||
@@ -46,15 +50,17 @@ class TestListUsers:
|
||||
assert "pagination" in data
|
||||
assert isinstance(data["data"], list)
|
||||
|
||||
def test_list_users_as_regular_user(self, client, test_user):
|
||||
@pytest.mark.asyncio
|
||||
async def test_list_users_as_regular_user(self, client, async_test_user):
|
||||
"""Test that regular users cannot list users."""
|
||||
headers = get_auth_headers(client, test_user.email, "TestPassword123")
|
||||
headers = await get_auth_headers(client, async_test_user.email, "TestPassword123")
|
||||
|
||||
response = client.get("/api/v1/users", headers=headers)
|
||||
response = await client.get("/api/v1/users", headers=headers)
|
||||
|
||||
assert response.status_code == status.HTTP_403_FORBIDDEN
|
||||
|
||||
def test_list_users_pagination(self, client, test_superuser, test_db):
|
||||
@pytest.mark.asyncio
|
||||
async def test_list_users_pagination(self, client, async_test_superuser, test_db):
|
||||
"""Test pagination works correctly."""
|
||||
# Create multiple users
|
||||
for i in range(15):
|
||||
@@ -68,17 +74,18 @@ class TestListUsers:
|
||||
test_db.add(user)
|
||||
test_db.commit()
|
||||
|
||||
headers = get_auth_headers(client, test_superuser.email, "SuperPassword123")
|
||||
headers = await get_auth_headers(client, async_test_superuser.email, "SuperPassword123")
|
||||
|
||||
# Get first page
|
||||
response = client.get("/api/v1/users?page=1&limit=5", headers=headers)
|
||||
response = await client.get("/api/v1/users?page=1&limit=5", headers=headers)
|
||||
assert response.status_code == status.HTTP_200_OK
|
||||
data = response.json()
|
||||
assert len(data["data"]) == 5
|
||||
assert data["pagination"]["page"] == 1
|
||||
assert data["pagination"]["total"] >= 15
|
||||
|
||||
def test_list_users_filter_active(self, client, test_superuser, test_db):
|
||||
@pytest.mark.asyncio
|
||||
async def test_list_users_filter_active(self, client, async_test_superuser, test_db):
|
||||
"""Test filtering by active status."""
|
||||
# Create active and inactive users
|
||||
active_user = User(
|
||||
@@ -98,35 +105,37 @@ class TestListUsers:
|
||||
test_db.add_all([active_user, inactive_user])
|
||||
test_db.commit()
|
||||
|
||||
headers = get_auth_headers(client, test_superuser.email, "SuperPassword123")
|
||||
headers = await get_auth_headers(client, async_test_superuser.email, "SuperPassword123")
|
||||
|
||||
# Filter for active users
|
||||
response = client.get("/api/v1/users?is_active=true", headers=headers)
|
||||
response = await client.get("/api/v1/users?is_active=true", headers=headers)
|
||||
data = response.json()
|
||||
emails = [u["email"] for u in data["data"]]
|
||||
assert "activefilter@example.com" in emails
|
||||
assert "inactivefilter@example.com" not in emails
|
||||
|
||||
# Filter for inactive users
|
||||
response = client.get("/api/v1/users?is_active=false", headers=headers)
|
||||
response = await client.get("/api/v1/users?is_active=false", headers=headers)
|
||||
data = response.json()
|
||||
emails = [u["email"] for u in data["data"]]
|
||||
assert "inactivefilter@example.com" in emails
|
||||
assert "activefilter@example.com" not in emails
|
||||
|
||||
def test_list_users_sort_by_email(self, client, test_superuser):
|
||||
@pytest.mark.asyncio
|
||||
async def test_list_users_sort_by_email(self, client, async_test_superuser):
|
||||
"""Test sorting users by email."""
|
||||
headers = get_auth_headers(client, test_superuser.email, "SuperPassword123")
|
||||
headers = await get_auth_headers(client, async_test_superuser.email, "SuperPassword123")
|
||||
|
||||
response = client.get("/api/v1/users?sort_by=email&sort_order=asc", headers=headers)
|
||||
response = await client.get("/api/v1/users?sort_by=email&sort_order=asc", headers=headers)
|
||||
assert response.status_code == status.HTTP_200_OK
|
||||
data = response.json()
|
||||
emails = [u["email"] for u in data["data"]]
|
||||
assert emails == sorted(emails)
|
||||
|
||||
def test_list_users_no_auth(self, client):
|
||||
@pytest.mark.asyncio
|
||||
async def test_list_users_no_auth(self, client):
|
||||
"""Test that unauthenticated requests are rejected."""
|
||||
response = client.get("/api/v1/users")
|
||||
response = await client.get("/api/v1/users")
|
||||
assert response.status_code == status.HTTP_401_UNAUTHORIZED
|
||||
|
||||
# Note: Removed test_list_users_unexpected_error because mocking at CRUD level
|
||||
@@ -136,31 +145,34 @@ class TestListUsers:
|
||||
class TestGetCurrentUserProfile:
|
||||
"""Tests for GET /users/me endpoint."""
|
||||
|
||||
def test_get_own_profile(self, client, test_user):
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_own_profile(self, client, async_test_user):
|
||||
"""Test getting own profile."""
|
||||
headers = get_auth_headers(client, test_user.email, "TestPassword123")
|
||||
headers = await get_auth_headers(client, async_test_user.email, "TestPassword123")
|
||||
|
||||
response = client.get("/api/v1/users/me", headers=headers)
|
||||
response = await client.get("/api/v1/users/me", headers=headers)
|
||||
|
||||
assert response.status_code == status.HTTP_200_OK
|
||||
data = response.json()
|
||||
assert data["email"] == test_user.email
|
||||
assert data["first_name"] == test_user.first_name
|
||||
assert data["email"] == async_test_user.email
|
||||
assert data["first_name"] == async_test_user.first_name
|
||||
|
||||
def test_get_profile_no_auth(self, client):
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_profile_no_auth(self, client):
|
||||
"""Test that unauthenticated requests are rejected."""
|
||||
response = client.get("/api/v1/users/me")
|
||||
response = await client.get("/api/v1/users/me")
|
||||
assert response.status_code == status.HTTP_401_UNAUTHORIZED
|
||||
|
||||
|
||||
class TestUpdateCurrentUser:
|
||||
"""Tests for PATCH /users/me endpoint."""
|
||||
|
||||
def test_update_own_profile(self, client, test_user, test_db):
|
||||
@pytest.mark.asyncio
|
||||
async def test_update_own_profile(self, client, async_test_user, test_db):
|
||||
"""Test updating own profile."""
|
||||
headers = get_auth_headers(client, test_user.email, "TestPassword123")
|
||||
headers = await get_auth_headers(client, async_test_user.email, "TestPassword123")
|
||||
|
||||
response = client.patch(
|
||||
response = await client.patch(
|
||||
"/api/v1/users/me",
|
||||
headers=headers,
|
||||
json={"first_name": "Updated", "last_name": "Name"}
|
||||
@@ -172,14 +184,15 @@ class TestUpdateCurrentUser:
|
||||
assert data["last_name"] == "Name"
|
||||
|
||||
# Verify in database
|
||||
test_db.refresh(test_user)
|
||||
assert test_user.first_name == "Updated"
|
||||
test_db.refresh(async_test_user)
|
||||
assert async_test_user.first_name == "Updated"
|
||||
|
||||
def test_update_profile_phone_number(self, client, test_user, test_db):
|
||||
@pytest.mark.asyncio
|
||||
async def test_update_profile_phone_number(self, client, async_test_user, test_db):
|
||||
"""Test updating phone number with validation."""
|
||||
headers = get_auth_headers(client, test_user.email, "TestPassword123")
|
||||
headers = await get_auth_headers(client, async_test_user.email, "TestPassword123")
|
||||
|
||||
response = client.patch(
|
||||
response = await client.patch(
|
||||
"/api/v1/users/me",
|
||||
headers=headers,
|
||||
json={"phone_number": "+19876543210"}
|
||||
@@ -189,11 +202,12 @@ class TestUpdateCurrentUser:
|
||||
data = response.json()
|
||||
assert data["phone_number"] == "+19876543210"
|
||||
|
||||
def test_update_profile_invalid_phone(self, client, test_user):
|
||||
@pytest.mark.asyncio
|
||||
async def test_update_profile_invalid_phone(self, client, async_test_user):
|
||||
"""Test that invalid phone numbers are rejected."""
|
||||
headers = get_auth_headers(client, test_user.email, "TestPassword123")
|
||||
headers = await get_auth_headers(client, async_test_user.email, "TestPassword123")
|
||||
|
||||
response = client.patch(
|
||||
response = await client.patch(
|
||||
"/api/v1/users/me",
|
||||
headers=headers,
|
||||
json={"phone_number": "invalid"}
|
||||
@@ -201,13 +215,14 @@ class TestUpdateCurrentUser:
|
||||
|
||||
assert response.status_code == status.HTTP_422_UNPROCESSABLE_ENTITY
|
||||
|
||||
def test_cannot_elevate_to_superuser(self, client, test_user):
|
||||
@pytest.mark.asyncio
|
||||
async def test_cannot_elevate_to_superuser(self, client, async_test_user):
|
||||
"""Test that users cannot make themselves superuser."""
|
||||
headers = get_auth_headers(client, test_user.email, "TestPassword123")
|
||||
headers = await get_auth_headers(client, async_test_user.email, "TestPassword123")
|
||||
|
||||
# Note: is_superuser is not in UserUpdate schema, but the endpoint checks for it
|
||||
# This tests that even if someone tries to send it, it's rejected
|
||||
response = client.patch(
|
||||
response = await client.patch(
|
||||
"/api/v1/users/me",
|
||||
headers=headers,
|
||||
json={"first_name": "Test", "is_superuser": True}
|
||||
@@ -220,9 +235,10 @@ class TestUpdateCurrentUser:
|
||||
# Verify user is still not a superuser
|
||||
assert data["is_superuser"] is False
|
||||
|
||||
def test_update_profile_no_auth(self, client):
|
||||
@pytest.mark.asyncio
|
||||
async def test_update_profile_no_auth(self, client):
|
||||
"""Test that unauthenticated requests are rejected."""
|
||||
response = client.patch(
|
||||
response = await client.patch(
|
||||
"/api/v1/users/me",
|
||||
json={"first_name": "Hacker"}
|
||||
)
|
||||
@@ -234,17 +250,19 @@ class TestUpdateCurrentUser:
|
||||
class TestGetUserById:
|
||||
"""Tests for GET /users/{user_id} endpoint."""
|
||||
|
||||
def test_get_own_profile_by_id(self, client, test_user):
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_own_profile_by_id(self, client, async_test_user):
|
||||
"""Test getting own profile by ID."""
|
||||
headers = get_auth_headers(client, test_user.email, "TestPassword123")
|
||||
headers = await get_auth_headers(client, async_test_user.email, "TestPassword123")
|
||||
|
||||
response = client.get(f"/api/v1/users/{test_user.id}", headers=headers)
|
||||
response = await client.get(f"/api/v1/users/{async_test_user.id}", headers=headers)
|
||||
|
||||
assert response.status_code == status.HTTP_200_OK
|
||||
data = response.json()
|
||||
assert data["email"] == test_user.email
|
||||
assert data["email"] == async_test_user.email
|
||||
|
||||
def test_get_other_user_as_regular_user(self, client, test_user, test_db):
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_other_user_as_regular_user(self, client, async_test_user, test_db):
|
||||
"""Test that regular users cannot view other profiles."""
|
||||
# Create another user
|
||||
other_user = User(
|
||||
@@ -258,36 +276,39 @@ class TestGetUserById:
|
||||
test_db.commit()
|
||||
test_db.refresh(other_user)
|
||||
|
||||
headers = get_auth_headers(client, test_user.email, "TestPassword123")
|
||||
headers = await get_auth_headers(client, async_test_user.email, "TestPassword123")
|
||||
|
||||
response = client.get(f"/api/v1/users/{other_user.id}", headers=headers)
|
||||
response = await client.get(f"/api/v1/users/{other_user.id}", headers=headers)
|
||||
|
||||
assert response.status_code == status.HTTP_403_FORBIDDEN
|
||||
|
||||
def test_get_other_user_as_superuser(self, client, test_superuser, test_user):
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_other_user_as_superuser(self, client, async_test_superuser, async_test_user):
|
||||
"""Test that superusers can view other profiles."""
|
||||
headers = get_auth_headers(client, test_superuser.email, "SuperPassword123")
|
||||
headers = await get_auth_headers(client, async_test_superuser.email, "SuperPassword123")
|
||||
|
||||
response = client.get(f"/api/v1/users/{test_user.id}", headers=headers)
|
||||
response = await client.get(f"/api/v1/users/{async_test_user.id}", headers=headers)
|
||||
|
||||
assert response.status_code == status.HTTP_200_OK
|
||||
data = response.json()
|
||||
assert data["email"] == test_user.email
|
||||
assert data["email"] == async_test_user.email
|
||||
|
||||
def test_get_nonexistent_user(self, client, test_superuser):
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_nonexistent_user(self, client, async_test_superuser):
|
||||
"""Test getting non-existent user."""
|
||||
headers = get_auth_headers(client, test_superuser.email, "SuperPassword123")
|
||||
headers = await get_auth_headers(client, async_test_superuser.email, "SuperPassword123")
|
||||
fake_id = uuid.uuid4()
|
||||
|
||||
response = client.get(f"/api/v1/users/{fake_id}", headers=headers)
|
||||
response = await client.get(f"/api/v1/users/{fake_id}", headers=headers)
|
||||
|
||||
assert response.status_code == status.HTTP_404_NOT_FOUND
|
||||
|
||||
def test_get_user_invalid_uuid(self, client, test_superuser):
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_user_invalid_uuid(self, client, async_test_superuser):
|
||||
"""Test getting user with invalid UUID format."""
|
||||
headers = get_auth_headers(client, test_superuser.email, "SuperPassword123")
|
||||
headers = await get_auth_headers(client, async_test_superuser.email, "SuperPassword123")
|
||||
|
||||
response = client.get("/api/v1/users/not-a-uuid", headers=headers)
|
||||
response = await client.get("/api/v1/users/not-a-uuid", headers=headers)
|
||||
|
||||
assert response.status_code == status.HTTP_422_UNPROCESSABLE_ENTITY
|
||||
|
||||
@@ -295,12 +316,13 @@ class TestGetUserById:
|
||||
class TestUpdateUserById:
|
||||
"""Tests for PATCH /users/{user_id} endpoint."""
|
||||
|
||||
def test_update_own_profile_by_id(self, client, test_user, test_db):
|
||||
@pytest.mark.asyncio
|
||||
async def test_update_own_profile_by_id(self, client, async_test_user, test_db):
|
||||
"""Test updating own profile by ID."""
|
||||
headers = get_auth_headers(client, test_user.email, "TestPassword123")
|
||||
headers = await get_auth_headers(client, async_test_user.email, "TestPassword123")
|
||||
|
||||
response = client.patch(
|
||||
f"/api/v1/users/{test_user.id}",
|
||||
response = await client.patch(
|
||||
f"/api/v1/users/{async_test_user.id}",
|
||||
headers=headers,
|
||||
json={"first_name": "SelfUpdated"}
|
||||
)
|
||||
@@ -309,7 +331,8 @@ class TestUpdateUserById:
|
||||
data = response.json()
|
||||
assert data["first_name"] == "SelfUpdated"
|
||||
|
||||
def test_update_other_user_as_regular_user(self, client, test_user, test_db):
|
||||
@pytest.mark.asyncio
|
||||
async def test_update_other_user_as_regular_user(self, client, async_test_user, test_db):
|
||||
"""Test that regular users cannot update other profiles."""
|
||||
# Create another user
|
||||
other_user = User(
|
||||
@@ -323,9 +346,9 @@ class TestUpdateUserById:
|
||||
test_db.commit()
|
||||
test_db.refresh(other_user)
|
||||
|
||||
headers = get_auth_headers(client, test_user.email, "TestPassword123")
|
||||
headers = await get_auth_headers(client, async_test_user.email, "TestPassword123")
|
||||
|
||||
response = client.patch(
|
||||
response = await client.patch(
|
||||
f"/api/v1/users/{other_user.id}",
|
||||
headers=headers,
|
||||
json={"first_name": "Hacked"}
|
||||
@@ -337,12 +360,13 @@ class TestUpdateUserById:
|
||||
test_db.refresh(other_user)
|
||||
assert other_user.first_name == "Other"
|
||||
|
||||
def test_update_other_user_as_superuser(self, client, test_superuser, test_user, test_db):
|
||||
@pytest.mark.asyncio
|
||||
async def test_update_other_user_as_superuser(self, client, async_test_superuser, async_test_user, test_db):
|
||||
"""Test that superusers can update other profiles."""
|
||||
headers = get_auth_headers(client, test_superuser.email, "SuperPassword123")
|
||||
headers = await get_auth_headers(client, async_test_superuser.email, "SuperPassword123")
|
||||
|
||||
response = client.patch(
|
||||
f"/api/v1/users/{test_user.id}",
|
||||
response = await client.patch(
|
||||
f"/api/v1/users/{async_test_user.id}",
|
||||
headers=headers,
|
||||
json={"first_name": "AdminUpdated"}
|
||||
)
|
||||
@@ -351,14 +375,15 @@ class TestUpdateUserById:
|
||||
data = response.json()
|
||||
assert data["first_name"] == "AdminUpdated"
|
||||
|
||||
def test_regular_user_cannot_modify_superuser_status(self, client, test_user):
|
||||
@pytest.mark.asyncio
|
||||
async def test_regular_user_cannot_modify_superuser_status(self, client, async_test_user):
|
||||
"""Test that regular users cannot change superuser status even if they try."""
|
||||
headers = get_auth_headers(client, test_user.email, "TestPassword123")
|
||||
headers = await get_auth_headers(client, async_test_user.email, "TestPassword123")
|
||||
|
||||
# is_superuser not in UserUpdate schema, so it gets ignored by Pydantic
|
||||
# Just verify the user stays the same
|
||||
response = client.patch(
|
||||
f"/api/v1/users/{test_user.id}",
|
||||
response = await client.patch(
|
||||
f"/api/v1/users/{async_test_user.id}",
|
||||
headers=headers,
|
||||
json={"first_name": "Test"}
|
||||
)
|
||||
@@ -367,12 +392,13 @@ class TestUpdateUserById:
|
||||
data = response.json()
|
||||
assert data["is_superuser"] is False
|
||||
|
||||
def test_superuser_can_update_users(self, client, test_superuser, test_user, test_db):
|
||||
@pytest.mark.asyncio
|
||||
async def test_superuser_can_update_users(self, client, async_test_superuser, async_test_user, test_db):
|
||||
"""Test that superusers can update other users."""
|
||||
headers = get_auth_headers(client, test_superuser.email, "SuperPassword123")
|
||||
headers = await get_auth_headers(client, async_test_superuser.email, "SuperPassword123")
|
||||
|
||||
response = client.patch(
|
||||
f"/api/v1/users/{test_user.id}",
|
||||
response = await client.patch(
|
||||
f"/api/v1/users/{async_test_user.id}",
|
||||
headers=headers,
|
||||
json={"first_name": "AdminChanged", "is_active": False}
|
||||
)
|
||||
@@ -382,12 +408,13 @@ class TestUpdateUserById:
|
||||
assert data["first_name"] == "AdminChanged"
|
||||
assert data["is_active"] is False
|
||||
|
||||
def test_update_nonexistent_user(self, client, test_superuser):
|
||||
@pytest.mark.asyncio
|
||||
async def test_update_nonexistent_user(self, client, async_test_superuser):
|
||||
"""Test updating non-existent user."""
|
||||
headers = get_auth_headers(client, test_superuser.email, "SuperPassword123")
|
||||
headers = await get_auth_headers(client, async_test_superuser.email, "SuperPassword123")
|
||||
fake_id = uuid.uuid4()
|
||||
|
||||
response = client.patch(
|
||||
response = await client.patch(
|
||||
f"/api/v1/users/{fake_id}",
|
||||
headers=headers,
|
||||
json={"first_name": "Ghost"}
|
||||
@@ -401,11 +428,12 @@ class TestUpdateUserById:
|
||||
class TestChangePassword:
|
||||
"""Tests for PATCH /users/me/password endpoint."""
|
||||
|
||||
def test_change_password_success(self, client, test_user, test_db):
|
||||
@pytest.mark.asyncio
|
||||
async def test_change_password_success(self, client, async_test_user, test_db):
|
||||
"""Test successful password change."""
|
||||
headers = get_auth_headers(client, test_user.email, "TestPassword123")
|
||||
headers = await get_auth_headers(client, async_test_user.email, "TestPassword123")
|
||||
|
||||
response = client.patch(
|
||||
response = await client.patch(
|
||||
"/api/v1/users/me/password",
|
||||
headers=headers,
|
||||
json={
|
||||
@@ -419,20 +447,21 @@ class TestChangePassword:
|
||||
assert data["success"] is True
|
||||
|
||||
# Verify can login with new password
|
||||
login_response = client.post(
|
||||
login_response = await client.post(
|
||||
"/api/v1/auth/login",
|
||||
json={
|
||||
"email": test_user.email,
|
||||
"email": async_test_user.email,
|
||||
"password": "NewPassword123"
|
||||
}
|
||||
)
|
||||
assert login_response.status_code == status.HTTP_200_OK
|
||||
|
||||
def test_change_password_wrong_current(self, client, test_user):
|
||||
@pytest.mark.asyncio
|
||||
async def test_change_password_wrong_current(self, client, async_test_user):
|
||||
"""Test that wrong current password is rejected."""
|
||||
headers = get_auth_headers(client, test_user.email, "TestPassword123")
|
||||
headers = await get_auth_headers(client, async_test_user.email, "TestPassword123")
|
||||
|
||||
response = client.patch(
|
||||
response = await client.patch(
|
||||
"/api/v1/users/me/password",
|
||||
headers=headers,
|
||||
json={
|
||||
@@ -443,11 +472,12 @@ class TestChangePassword:
|
||||
|
||||
assert response.status_code == status.HTTP_403_FORBIDDEN
|
||||
|
||||
def test_change_password_weak_new_password(self, client, test_user):
|
||||
@pytest.mark.asyncio
|
||||
async def test_change_password_weak_new_password(self, client, async_test_user):
|
||||
"""Test that weak new passwords are rejected."""
|
||||
headers = get_auth_headers(client, test_user.email, "TestPassword123")
|
||||
headers = await get_auth_headers(client, async_test_user.email, "TestPassword123")
|
||||
|
||||
response = client.patch(
|
||||
response = await client.patch(
|
||||
"/api/v1/users/me/password",
|
||||
headers=headers,
|
||||
json={
|
||||
@@ -458,9 +488,10 @@ class TestChangePassword:
|
||||
|
||||
assert response.status_code == status.HTTP_422_UNPROCESSABLE_ENTITY
|
||||
|
||||
def test_change_password_no_auth(self, client):
|
||||
@pytest.mark.asyncio
|
||||
async def test_change_password_no_auth(self, client):
|
||||
"""Test that unauthenticated requests are rejected."""
|
||||
response = client.patch(
|
||||
response = await client.patch(
|
||||
"/api/v1/users/me/password",
|
||||
json={
|
||||
"current_password": "TestPassword123",
|
||||
@@ -475,7 +506,8 @@ class TestChangePassword:
|
||||
class TestDeleteUser:
|
||||
"""Tests for DELETE /users/{user_id} endpoint."""
|
||||
|
||||
def test_delete_user_as_superuser(self, client, test_superuser, test_db):
|
||||
@pytest.mark.asyncio
|
||||
async def test_delete_user_as_superuser(self, client, async_test_superuser, test_db):
|
||||
"""Test deleting a user as superuser."""
|
||||
# Create a user to delete
|
||||
user_to_delete = User(
|
||||
@@ -489,9 +521,9 @@ class TestDeleteUser:
|
||||
test_db.commit()
|
||||
test_db.refresh(user_to_delete)
|
||||
|
||||
headers = get_auth_headers(client, test_superuser.email, "SuperPassword123")
|
||||
headers = await get_auth_headers(client, async_test_superuser.email, "SuperPassword123")
|
||||
|
||||
response = client.delete(f"/api/v1/users/{user_to_delete.id}", headers=headers)
|
||||
response = await client.delete(f"/api/v1/users/{user_to_delete.id}", headers=headers)
|
||||
|
||||
assert response.status_code == status.HTTP_200_OK
|
||||
data = response.json()
|
||||
@@ -501,15 +533,17 @@ class TestDeleteUser:
|
||||
test_db.refresh(user_to_delete)
|
||||
assert user_to_delete.deleted_at is not None
|
||||
|
||||
def test_cannot_delete_self(self, client, test_superuser):
|
||||
@pytest.mark.asyncio
|
||||
async def test_cannot_delete_self(self, client, async_test_superuser):
|
||||
"""Test that users cannot delete their own account."""
|
||||
headers = get_auth_headers(client, test_superuser.email, "SuperPassword123")
|
||||
headers = await get_auth_headers(client, async_test_superuser.email, "SuperPassword123")
|
||||
|
||||
response = client.delete(f"/api/v1/users/{test_superuser.id}", headers=headers)
|
||||
response = await client.delete(f"/api/v1/users/{async_test_superuser.id}", headers=headers)
|
||||
|
||||
assert response.status_code == status.HTTP_403_FORBIDDEN
|
||||
|
||||
def test_delete_user_as_regular_user(self, client, test_user, test_db):
|
||||
@pytest.mark.asyncio
|
||||
async def test_delete_user_as_regular_user(self, client, async_test_user, test_db):
|
||||
"""Test that regular users cannot delete users."""
|
||||
# Create another user
|
||||
other_user = User(
|
||||
@@ -523,24 +557,26 @@ class TestDeleteUser:
|
||||
test_db.commit()
|
||||
test_db.refresh(other_user)
|
||||
|
||||
headers = get_auth_headers(client, test_user.email, "TestPassword123")
|
||||
headers = await get_auth_headers(client, async_test_user.email, "TestPassword123")
|
||||
|
||||
response = client.delete(f"/api/v1/users/{other_user.id}", headers=headers)
|
||||
response = await client.delete(f"/api/v1/users/{other_user.id}", headers=headers)
|
||||
|
||||
assert response.status_code == status.HTTP_403_FORBIDDEN
|
||||
|
||||
def test_delete_nonexistent_user(self, client, test_superuser):
|
||||
@pytest.mark.asyncio
|
||||
async def test_delete_nonexistent_user(self, client, async_test_superuser):
|
||||
"""Test deleting non-existent user."""
|
||||
headers = get_auth_headers(client, test_superuser.email, "SuperPassword123")
|
||||
headers = await get_auth_headers(client, async_test_superuser.email, "SuperPassword123")
|
||||
fake_id = uuid.uuid4()
|
||||
|
||||
response = client.delete(f"/api/v1/users/{fake_id}", headers=headers)
|
||||
response = await client.delete(f"/api/v1/users/{fake_id}", headers=headers)
|
||||
|
||||
assert response.status_code == status.HTTP_404_NOT_FOUND
|
||||
|
||||
def test_delete_user_no_auth(self, client, test_user):
|
||||
@pytest.mark.asyncio
|
||||
async def test_delete_user_no_auth(self, client, async_test_user):
|
||||
"""Test that unauthenticated requests are rejected."""
|
||||
response = client.delete(f"/api/v1/users/{test_user.id}")
|
||||
response = await client.delete(f"/api/v1/users/{async_test_user.id}")
|
||||
assert response.status_code == status.HTTP_401_UNAUTHORIZED
|
||||
|
||||
# Note: Removed test_delete_user_unexpected_error - see comment above
|
||||
|
||||
90
backend/tests/conftest.py
Normal file → Executable file
90
backend/tests/conftest.py
Normal file → Executable file
@@ -4,14 +4,15 @@ import uuid
|
||||
from datetime import datetime, timezone
|
||||
|
||||
import pytest
|
||||
from fastapi.testclient import TestClient
|
||||
import pytest_asyncio
|
||||
from httpx import AsyncClient
|
||||
|
||||
# Set IS_TEST environment variable BEFORE importing app
|
||||
# This prevents the scheduler from starting during tests
|
||||
os.environ["IS_TEST"] = "True"
|
||||
|
||||
from app.main import app
|
||||
from app.core.database import get_db
|
||||
from app.core.database_async import get_async_db
|
||||
from app.models.user import User
|
||||
from app.core.auth import get_password_hash
|
||||
from app.utils.test_utils import setup_test_db, teardown_test_db, setup_async_test_db, teardown_async_test_db
|
||||
@@ -35,7 +36,7 @@ def db_session():
|
||||
teardown_test_db(test_engine)
|
||||
|
||||
|
||||
@pytest.fixture(scope="function") # Define a fixture
|
||||
@pytest_asyncio.fixture(scope="function") # Define a fixture
|
||||
async def async_test_db():
|
||||
"""Fixture provides new testing engine and session for each test run to improve isolation."""
|
||||
|
||||
@@ -92,22 +93,25 @@ def test_db():
|
||||
teardown_test_db(test_engine)
|
||||
|
||||
|
||||
@pytest.fixture(scope="function")
|
||||
def client(test_db):
|
||||
@pytest_asyncio.fixture(scope="function")
|
||||
async def client(async_test_db):
|
||||
"""
|
||||
Create a FastAPI test client with a test database.
|
||||
Create a FastAPI async test client with a test database.
|
||||
|
||||
This overrides the get_db dependency to use the test database.
|
||||
This overrides the get_async_db dependency to use the test database.
|
||||
"""
|
||||
def override_get_db():
|
||||
try:
|
||||
yield test_db
|
||||
finally:
|
||||
pass
|
||||
test_engine, AsyncTestingSessionLocal = async_test_db
|
||||
|
||||
app.dependency_overrides[get_db] = override_get_db
|
||||
async def override_get_async_db():
|
||||
async with AsyncTestingSessionLocal() as session:
|
||||
try:
|
||||
yield session
|
||||
finally:
|
||||
pass
|
||||
|
||||
with TestClient(app) as test_client:
|
||||
app.dependency_overrides[get_async_db] = override_get_async_db
|
||||
|
||||
async with AsyncClient(app=app, base_url="http://test") as test_client:
|
||||
yield test_client
|
||||
|
||||
app.dependency_overrides.clear()
|
||||
@@ -116,7 +120,7 @@ def client(test_db):
|
||||
@pytest.fixture
|
||||
def test_user(test_db):
|
||||
"""
|
||||
Create a test user in the database.
|
||||
Create a test user in the database (sync version for legacy tests).
|
||||
|
||||
Password: TestPassword123
|
||||
"""
|
||||
@@ -140,7 +144,7 @@ def test_user(test_db):
|
||||
@pytest.fixture
|
||||
def test_superuser(test_db):
|
||||
"""
|
||||
Create a test superuser in the database.
|
||||
Create a test superuser in the database (sync version for legacy tests).
|
||||
|
||||
Password: SuperPassword123
|
||||
"""
|
||||
@@ -158,4 +162,56 @@ def test_superuser(test_db):
|
||||
test_db.add(user)
|
||||
test_db.commit()
|
||||
test_db.refresh(user)
|
||||
return user
|
||||
return user
|
||||
|
||||
|
||||
@pytest_asyncio.fixture
|
||||
async def async_test_user(async_test_db):
|
||||
"""
|
||||
Create a test user in the database (async version).
|
||||
|
||||
Password: TestPassword123
|
||||
"""
|
||||
test_engine, AsyncTestingSessionLocal = async_test_db
|
||||
async with AsyncTestingSessionLocal() as session:
|
||||
user = User(
|
||||
id=uuid.uuid4(),
|
||||
email="testuser@example.com",
|
||||
password_hash=get_password_hash("TestPassword123"),
|
||||
first_name="Test",
|
||||
last_name="User",
|
||||
phone_number="+1234567890",
|
||||
is_active=True,
|
||||
is_superuser=False,
|
||||
preferences=None,
|
||||
)
|
||||
session.add(user)
|
||||
await session.commit()
|
||||
await session.refresh(user)
|
||||
return user
|
||||
|
||||
|
||||
@pytest_asyncio.fixture
|
||||
async def async_test_superuser(async_test_db):
|
||||
"""
|
||||
Create a test superuser in the database (async version).
|
||||
|
||||
Password: SuperPassword123
|
||||
"""
|
||||
test_engine, AsyncTestingSessionLocal = async_test_db
|
||||
async with AsyncTestingSessionLocal() as session:
|
||||
user = User(
|
||||
id=uuid.uuid4(),
|
||||
email="superuser@example.com",
|
||||
password_hash=get_password_hash("SuperPassword123"),
|
||||
first_name="Super",
|
||||
last_name="User",
|
||||
phone_number="+9876543210",
|
||||
is_active=True,
|
||||
is_superuser=True,
|
||||
preferences=None,
|
||||
)
|
||||
session.add(user)
|
||||
await session.commit()
|
||||
await session.refresh(user)
|
||||
return user
|
||||
0
backend/tests/core/__init__.py
Normal file → Executable file
0
backend/tests/core/__init__.py
Normal file → Executable file
0
backend/tests/core/test_auth.py
Normal file → Executable file
0
backend/tests/core/test_auth.py
Normal file → Executable file
0
backend/tests/core/test_config.py
Normal file → Executable file
0
backend/tests/core/test_config.py
Normal file → Executable file
0
backend/tests/crud/__init__.py
Normal file → Executable file
0
backend/tests/crud/__init__.py
Normal file → Executable file
0
backend/tests/crud/test_crud_base.py
Normal file → Executable file
0
backend/tests/crud/test_crud_base.py
Normal file → Executable file
0
backend/tests/crud/test_crud_error_paths.py
Normal file → Executable file
0
backend/tests/crud/test_crud_error_paths.py
Normal file → Executable file
0
backend/tests/crud/test_soft_delete.py
Normal file → Executable file
0
backend/tests/crud/test_soft_delete.py
Normal file → Executable file
0
backend/tests/crud/test_user.py
Normal file → Executable file
0
backend/tests/crud/test_user.py
Normal file → Executable file
0
backend/tests/models/__init__.py
Normal file → Executable file
0
backend/tests/models/__init__.py
Normal file → Executable file
0
backend/tests/models/test_user.py
Normal file → Executable file
0
backend/tests/models/test_user.py
Normal file → Executable file
0
backend/tests/schemas/__init__.py
Normal file → Executable file
0
backend/tests/schemas/__init__.py
Normal file → Executable file
0
backend/tests/schemas/test_user_schemas.py
Normal file → Executable file
0
backend/tests/schemas/test_user_schemas.py
Normal file → Executable file
0
backend/tests/services/__init__.py
Normal file → Executable file
0
backend/tests/services/__init__.py
Normal file → Executable file
0
backend/tests/services/test_auth_service.py
Normal file → Executable file
0
backend/tests/services/test_auth_service.py
Normal file → Executable file
0
backend/tests/services/test_email_service.py
Normal file → Executable file
0
backend/tests/services/test_email_service.py
Normal file → Executable file
0
backend/tests/test_init_db.py
Normal file → Executable file
0
backend/tests/test_init_db.py
Normal file → Executable file
0
backend/tests/utils/__init__.py
Normal file → Executable file
0
backend/tests/utils/__init__.py
Normal file → Executable file
0
backend/tests/utils/test_security.py
Normal file → Executable file
0
backend/tests/utils/test_security.py
Normal file → Executable file
Reference in New Issue
Block a user