forked from cardosofelipe/pragma-stack
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
|
# tests/api/dependencies/test_auth_dependencies.py
|
||||||
import pytest
|
import pytest
|
||||||
|
import pytest_asyncio
|
||||||
import uuid
|
import uuid
|
||||||
from unittest.mock import patch
|
from unittest.mock import patch
|
||||||
from fastapi import HTTPException
|
from fastapi import HTTPException
|
||||||
@@ -10,7 +11,8 @@ from app.api.dependencies.auth import (
|
|||||||
get_current_superuser,
|
get_current_superuser,
|
||||||
get_optional_current_user
|
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
|
@pytest.fixture
|
||||||
@@ -19,79 +21,119 @@ def mock_token():
|
|||||||
return "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:
|
class TestGetCurrentUser:
|
||||||
"""Tests for get_current_user dependency"""
|
"""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"""
|
"""Test successfully getting the current user"""
|
||||||
# Mock get_token_data to return user_id that matches our mock_user
|
test_engine, AsyncTestingSessionLocal = async_test_db
|
||||||
with patch('app.api.dependencies.auth.get_token_data') as mock_get_data:
|
async with AsyncTestingSessionLocal() as session:
|
||||||
mock_get_data.return_value.user_id = mock_user.id
|
# 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
|
# Call the dependency
|
||||||
user = get_current_user(db=db_session, token=mock_token)
|
user = await get_current_user(db=session, token=mock_token)
|
||||||
|
|
||||||
# Verify the correct user was returned
|
# Verify the correct user was returned
|
||||||
assert user.id == mock_user.id
|
assert user.id == async_mock_user.id
|
||||||
assert user.email == mock_user.email
|
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"""
|
"""Test when the token contains a user ID that doesn't exist"""
|
||||||
# Mock get_token_data to return a non-existent user ID
|
test_engine, AsyncTestingSessionLocal = async_test_db
|
||||||
nonexistent_id = uuid.UUID("11111111-1111-1111-1111-111111111111")
|
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:
|
with patch('app.api.dependencies.auth.get_token_data') as mock_get_data:
|
||||||
mock_get_data.return_value.user_id = nonexistent_id
|
mock_get_data.return_value.user_id = nonexistent_id
|
||||||
|
|
||||||
# Should raise HTTPException with 404 status
|
# Should raise HTTPException with 404 status
|
||||||
with pytest.raises(HTTPException) as exc_info:
|
with pytest.raises(HTTPException) as exc_info:
|
||||||
get_current_user(db=db_session, token=mock_token)
|
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
|
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"""
|
"""Test when the user is inactive"""
|
||||||
# Make the user inactive
|
test_engine, AsyncTestingSessionLocal = async_test_db
|
||||||
mock_user.is_active = False
|
async with AsyncTestingSessionLocal() as session:
|
||||||
db_session.commit()
|
# 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
|
# Mock get_token_data
|
||||||
with patch('app.api.dependencies.auth.get_token_data') as mock_get_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_data.return_value.user_id = async_mock_user.id
|
||||||
|
|
||||||
# Should raise HTTPException with 403 status
|
# Should raise HTTPException with 403 status
|
||||||
with pytest.raises(HTTPException) as exc_info:
|
with pytest.raises(HTTPException) as exc_info:
|
||||||
get_current_user(db=db_session, token=mock_token)
|
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
|
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"""
|
"""Test with an expired token"""
|
||||||
# Mock get_token_data to raise TokenExpiredError
|
test_engine, AsyncTestingSessionLocal = async_test_db
|
||||||
with patch('app.api.dependencies.auth.get_token_data') as mock_get_data:
|
async with AsyncTestingSessionLocal() as session:
|
||||||
mock_get_data.side_effect = TokenExpiredError("Token expired")
|
# 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
|
# Should raise HTTPException with 401 status
|
||||||
with pytest.raises(HTTPException) as exc_info:
|
with pytest.raises(HTTPException) as exc_info:
|
||||||
get_current_user(db=db_session, token=mock_token)
|
await get_current_user(db=session, token=mock_token)
|
||||||
|
|
||||||
assert exc_info.value.status_code == 401
|
assert exc_info.value.status_code == 401
|
||||||
assert "Token expired" in exc_info.value.detail
|
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"""
|
"""Test with an invalid token"""
|
||||||
# Mock get_token_data to raise TokenInvalidError
|
test_engine, AsyncTestingSessionLocal = async_test_db
|
||||||
with patch('app.api.dependencies.auth.get_token_data') as mock_get_data:
|
async with AsyncTestingSessionLocal() as session:
|
||||||
mock_get_data.side_effect = TokenInvalidError("Invalid token")
|
# Mock get_token_data to raise TokenInvalidError
|
||||||
|
with patch('app.api.dependencies.auth.get_token_data') as mock_get_data:
|
||||||
|
mock_get_data.side_effect = TokenInvalidError("Invalid token")
|
||||||
|
|
||||||
# Should raise HTTPException with 401 status
|
# Should raise HTTPException with 401 status
|
||||||
with pytest.raises(HTTPException) as exc_info:
|
with pytest.raises(HTTPException) as exc_info:
|
||||||
get_current_user(db=db_session, token=mock_token)
|
await get_current_user(db=session, token=mock_token)
|
||||||
|
|
||||||
assert exc_info.value.status_code == 401
|
assert exc_info.value.status_code == 401
|
||||||
assert "Could not validate credentials" in exc_info.value.detail
|
assert "Could not validate credentials" in exc_info.value.detail
|
||||||
|
|
||||||
|
|
||||||
class TestGetCurrentActiveUser:
|
class TestGetCurrentActiveUser:
|
||||||
@@ -151,63 +193,81 @@ class TestGetCurrentSuperuser:
|
|||||||
class TestGetOptionalCurrentUser:
|
class TestGetOptionalCurrentUser:
|
||||||
"""Tests for get_optional_current_user dependency"""
|
"""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"""
|
"""Test getting optional user with a valid token"""
|
||||||
# Mock get_token_data
|
test_engine, AsyncTestingSessionLocal = async_test_db
|
||||||
with patch('app.api.dependencies.auth.get_token_data') as mock_get_data:
|
async with AsyncTestingSessionLocal() as session:
|
||||||
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
|
# Call the dependency
|
||||||
user = get_optional_current_user(db=db_session, token=mock_token)
|
user = await get_optional_current_user(db=session, token=mock_token)
|
||||||
|
|
||||||
# Should return the correct user
|
# Should return the correct user
|
||||||
assert user is not None
|
assert user is not None
|
||||||
assert user.id == mock_user.id
|
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"""
|
"""Test getting optional user with no token"""
|
||||||
# Call the dependency with no token
|
test_engine, AsyncTestingSessionLocal = async_test_db
|
||||||
user = get_optional_current_user(db=db_session, token=None)
|
async with AsyncTestingSessionLocal() as session:
|
||||||
|
# Call the dependency with no token
|
||||||
|
user = await get_optional_current_user(db=session, token=None)
|
||||||
|
|
||||||
# Should return None
|
# Should return None
|
||||||
assert user is 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"""
|
"""Test getting optional user with an invalid token"""
|
||||||
# Mock get_token_data to raise TokenInvalidError
|
test_engine, AsyncTestingSessionLocal = async_test_db
|
||||||
with patch('app.api.dependencies.auth.get_token_data') as mock_get_data:
|
async with AsyncTestingSessionLocal() as session:
|
||||||
mock_get_data.side_effect = TokenInvalidError("Invalid token")
|
# Mock get_token_data to raise TokenInvalidError
|
||||||
|
with patch('app.api.dependencies.auth.get_token_data') as mock_get_data:
|
||||||
|
mock_get_data.side_effect = TokenInvalidError("Invalid token")
|
||||||
|
|
||||||
# Call the dependency
|
# Call the dependency
|
||||||
user = get_optional_current_user(db=db_session, token=mock_token)
|
user = await get_optional_current_user(db=session, token=mock_token)
|
||||||
|
|
||||||
# Should return None, not raise an exception
|
# Should return None, not raise an exception
|
||||||
assert user is None
|
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"""
|
"""Test getting optional user with an expired token"""
|
||||||
# Mock get_token_data to raise TokenExpiredError
|
test_engine, AsyncTestingSessionLocal = async_test_db
|
||||||
with patch('app.api.dependencies.auth.get_token_data') as mock_get_data:
|
async with AsyncTestingSessionLocal() as session:
|
||||||
mock_get_data.side_effect = TokenExpiredError("Token expired")
|
# 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
|
# Call the dependency
|
||||||
user = get_optional_current_user(db=db_session, token=mock_token)
|
user = await get_optional_current_user(db=session, token=mock_token)
|
||||||
|
|
||||||
# Should return None, not raise an exception
|
# Should return None, not raise an exception
|
||||||
assert user is None
|
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"""
|
"""Test getting optional user when user is inactive"""
|
||||||
# Make the user inactive
|
test_engine, AsyncTestingSessionLocal = async_test_db
|
||||||
mock_user.is_active = False
|
async with AsyncTestingSessionLocal() as session:
|
||||||
db_session.commit()
|
# 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
|
# Mock get_token_data
|
||||||
with patch('app.api.dependencies.auth.get_token_data') as mock_get_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_data.return_value.user_id = async_mock_user.id
|
||||||
|
|
||||||
# Call the dependency
|
# Call the dependency
|
||||||
user = get_optional_current_user(db=db_session, token=mock_token)
|
user = await get_optional_current_user(db=session, token=mock_token)
|
||||||
|
|
||||||
# Should return None for inactive users
|
# Should return None for inactive users
|
||||||
assert user is None
|
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
|
# tests/api/dependencies/test_auth_dependencies.py
|
||||||
import pytest
|
import pytest
|
||||||
from unittest.mock import patch, MagicMock
|
import pytest_asyncio
|
||||||
|
import uuid
|
||||||
|
from unittest.mock import patch
|
||||||
from fastapi import HTTPException
|
from fastapi import HTTPException
|
||||||
|
|
||||||
from app.api.dependencies.auth import (
|
from app.api.dependencies.auth import (
|
||||||
@@ -9,87 +11,129 @@ from app.api.dependencies.auth import (
|
|||||||
get_current_superuser,
|
get_current_superuser,
|
||||||
get_optional_current_user
|
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
|
@pytest.fixture
|
||||||
def mock_token():
|
def mock_token():
|
||||||
|
"""Fixture providing a mock JWT token"""
|
||||||
return "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:
|
class TestGetCurrentUser:
|
||||||
"""Tests for get_current_user dependency"""
|
"""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"""
|
"""Test successfully getting the current user"""
|
||||||
# Mock get_token_data to return user_id that matches our mock_user
|
test_engine, AsyncTestingSessionLocal = async_test_db
|
||||||
with patch('app.api.dependencies.auth.get_token_data') as mock_get_data:
|
async with AsyncTestingSessionLocal() as session:
|
||||||
mock_get_data.return_value.user_id = mock_user.id
|
# 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
|
# Call the dependency
|
||||||
user = get_current_user(db=db_session, token=mock_token)
|
user = await get_current_user(db=session, token=mock_token)
|
||||||
|
|
||||||
# Verify the correct user was returned
|
# Verify the correct user was returned
|
||||||
assert user.id == mock_user.id
|
assert user.id == async_mock_user.id
|
||||||
assert user.email == mock_user.email
|
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"""
|
"""Test when the token contains a user ID that doesn't exist"""
|
||||||
# Mock get_token_data to return a non-existent user ID
|
test_engine, AsyncTestingSessionLocal = async_test_db
|
||||||
# Use a real UUID object instead of a string
|
async with AsyncTestingSessionLocal() as session:
|
||||||
import uuid
|
# Mock get_token_data to return a non-existent user ID
|
||||||
nonexistent_id = uuid.UUID("11111111-1111-1111-1111-111111111111")
|
nonexistent_id = uuid.UUID("11111111-1111-1111-1111-111111111111")
|
||||||
|
|
||||||
with patch('app.api.dependencies.auth.get_token_data') as mock_get_data:
|
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
|
mock_get_data.return_value.user_id = nonexistent_id
|
||||||
|
|
||||||
# Should raise HTTPException with 404 status
|
# Should raise HTTPException with 404 status
|
||||||
with pytest.raises(HTTPException) as exc_info:
|
with pytest.raises(HTTPException) as exc_info:
|
||||||
get_current_user(db=db_session, token=mock_token)
|
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"""
|
"""Test when the user is inactive"""
|
||||||
# Make the user inactive
|
test_engine, AsyncTestingSessionLocal = async_test_db
|
||||||
mock_user.is_active = False
|
async with AsyncTestingSessionLocal() as session:
|
||||||
db_session.commit()
|
# 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
|
# Mock get_token_data
|
||||||
with patch('app.api.dependencies.auth.get_token_data') as mock_get_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_data.return_value.user_id = async_mock_user.id
|
||||||
|
|
||||||
# Should raise HTTPException with 403 status
|
# Should raise HTTPException with 403 status
|
||||||
with pytest.raises(HTTPException) as exc_info:
|
with pytest.raises(HTTPException) as exc_info:
|
||||||
get_current_user(db=db_session, token=mock_token)
|
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"""
|
"""Test with an expired token"""
|
||||||
# Mock get_token_data to raise TokenExpiredError
|
test_engine, AsyncTestingSessionLocal = async_test_db
|
||||||
with patch('app.api.dependencies.auth.get_token_data') as mock_get_data:
|
async with AsyncTestingSessionLocal() as session:
|
||||||
mock_get_data.side_effect = TokenExpiredError("Token expired")
|
# 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
|
# Should raise HTTPException with 401 status
|
||||||
with pytest.raises(HTTPException) as exc_info:
|
with pytest.raises(HTTPException) as exc_info:
|
||||||
get_current_user(db=db_session, token=mock_token)
|
await get_current_user(db=session, token=mock_token)
|
||||||
|
|
||||||
assert exc_info.value.status_code == 401
|
assert exc_info.value.status_code == 401
|
||||||
assert "Token expired" in exc_info.value.detail
|
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"""
|
"""Test with an invalid token"""
|
||||||
# Mock get_token_data to raise TokenInvalidError
|
test_engine, AsyncTestingSessionLocal = async_test_db
|
||||||
with patch('app.api.dependencies.auth.get_token_data') as mock_get_data:
|
async with AsyncTestingSessionLocal() as session:
|
||||||
mock_get_data.side_effect = TokenInvalidError("Invalid token")
|
# Mock get_token_data to raise TokenInvalidError
|
||||||
|
with patch('app.api.dependencies.auth.get_token_data') as mock_get_data:
|
||||||
|
mock_get_data.side_effect = TokenInvalidError("Invalid token")
|
||||||
|
|
||||||
# Should raise HTTPException with 401 status
|
# Should raise HTTPException with 401 status
|
||||||
with pytest.raises(HTTPException) as exc_info:
|
with pytest.raises(HTTPException) as exc_info:
|
||||||
get_current_user(db=db_session, token=mock_token)
|
await get_current_user(db=session, token=mock_token)
|
||||||
|
|
||||||
assert exc_info.value.status_code == 401
|
assert exc_info.value.status_code == 401
|
||||||
assert "Could not validate credentials" in exc_info.value.detail
|
assert "Could not validate credentials" in exc_info.value.detail
|
||||||
|
|
||||||
|
|
||||||
class TestGetCurrentActiveUser:
|
class TestGetCurrentActiveUser:
|
||||||
@@ -149,63 +193,81 @@ class TestGetCurrentSuperuser:
|
|||||||
class TestGetOptionalCurrentUser:
|
class TestGetOptionalCurrentUser:
|
||||||
"""Tests for get_optional_current_user dependency"""
|
"""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"""
|
"""Test getting optional user with a valid token"""
|
||||||
# Mock get_token_data
|
test_engine, AsyncTestingSessionLocal = async_test_db
|
||||||
with patch('app.api.dependencies.auth.get_token_data') as mock_get_data:
|
async with AsyncTestingSessionLocal() as session:
|
||||||
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
|
# Call the dependency
|
||||||
user = get_optional_current_user(db=db_session, token=mock_token)
|
user = await get_optional_current_user(db=session, token=mock_token)
|
||||||
|
|
||||||
# Should return the correct user
|
# Should return the correct user
|
||||||
assert user is not None
|
assert user is not None
|
||||||
assert user.id == mock_user.id
|
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"""
|
"""Test getting optional user with no token"""
|
||||||
# Call the dependency with no token
|
test_engine, AsyncTestingSessionLocal = async_test_db
|
||||||
user = get_optional_current_user(db=db_session, token=None)
|
async with AsyncTestingSessionLocal() as session:
|
||||||
|
# Call the dependency with no token
|
||||||
|
user = await get_optional_current_user(db=session, token=None)
|
||||||
|
|
||||||
# Should return None
|
# Should return None
|
||||||
assert user is 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"""
|
"""Test getting optional user with an invalid token"""
|
||||||
# Mock get_token_data to raise TokenInvalidError
|
test_engine, AsyncTestingSessionLocal = async_test_db
|
||||||
with patch('app.api.dependencies.auth.get_token_data') as mock_get_data:
|
async with AsyncTestingSessionLocal() as session:
|
||||||
mock_get_data.side_effect = TokenInvalidError("Invalid token")
|
# Mock get_token_data to raise TokenInvalidError
|
||||||
|
with patch('app.api.dependencies.auth.get_token_data') as mock_get_data:
|
||||||
|
mock_get_data.side_effect = TokenInvalidError("Invalid token")
|
||||||
|
|
||||||
# Call the dependency
|
# Call the dependency
|
||||||
user = get_optional_current_user(db=db_session, token=mock_token)
|
user = await get_optional_current_user(db=session, token=mock_token)
|
||||||
|
|
||||||
# Should return None, not raise an exception
|
# Should return None, not raise an exception
|
||||||
assert user is None
|
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"""
|
"""Test getting optional user with an expired token"""
|
||||||
# Mock get_token_data to raise TokenExpiredError
|
test_engine, AsyncTestingSessionLocal = async_test_db
|
||||||
with patch('app.api.dependencies.auth.get_token_data') as mock_get_data:
|
async with AsyncTestingSessionLocal() as session:
|
||||||
mock_get_data.side_effect = TokenExpiredError("Token expired")
|
# 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
|
# Call the dependency
|
||||||
user = get_optional_current_user(db=db_session, token=mock_token)
|
user = await get_optional_current_user(db=session, token=mock_token)
|
||||||
|
|
||||||
# Should return None, not raise an exception
|
# Should return None, not raise an exception
|
||||||
assert user is None
|
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"""
|
"""Test getting optional user when user is inactive"""
|
||||||
# Make the user inactive
|
test_engine, AsyncTestingSessionLocal = async_test_db
|
||||||
mock_user.is_active = False
|
async with AsyncTestingSessionLocal() as session:
|
||||||
db_session.commit()
|
# 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
|
# Mock get_token_data
|
||||||
with patch('app.api.dependencies.auth.get_token_data') as mock_get_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_data.return_value.user_id = async_mock_user.id
|
||||||
|
|
||||||
# Call the dependency
|
# Call the dependency
|
||||||
user = get_optional_current_user(db=db_session, token=mock_token)
|
user = await get_optional_current_user(db=session, token=mock_token)
|
||||||
|
|
||||||
# Should return None for inactive users
|
# Should return None for inactive users
|
||||||
assert user is None
|
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.
|
Tests for authentication endpoints.
|
||||||
"""
|
"""
|
||||||
import pytest
|
import pytest
|
||||||
|
import pytest_asyncio
|
||||||
from unittest.mock import patch, MagicMock
|
from unittest.mock import patch, MagicMock
|
||||||
from fastapi import status
|
from fastapi import status
|
||||||
|
from sqlalchemy import select
|
||||||
|
|
||||||
from app.models.user import User
|
from app.models.user import User
|
||||||
from app.schemas.users import UserCreate
|
from app.schemas.users import UserCreate
|
||||||
@@ -21,9 +23,10 @@ def disable_rate_limit():
|
|||||||
class TestRegisterEndpoint:
|
class TestRegisterEndpoint:
|
||||||
"""Tests for POST /auth/register endpoint."""
|
"""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."""
|
"""Test successful user registration."""
|
||||||
response = client.post(
|
response = await client.post(
|
||||||
"/api/v1/auth/register",
|
"/api/v1/auth/register",
|
||||||
json={
|
json={
|
||||||
"email": "newuser@example.com",
|
"email": "newuser@example.com",
|
||||||
@@ -39,12 +42,13 @@ class TestRegisterEndpoint:
|
|||||||
assert data["first_name"] == "New"
|
assert data["first_name"] == "New"
|
||||||
assert "password" not in data
|
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."""
|
"""Test registering with existing email."""
|
||||||
response = client.post(
|
response = await client.post(
|
||||||
"/api/v1/auth/register",
|
"/api/v1/auth/register",
|
||||||
json={
|
json={
|
||||||
"email": test_user.email,
|
"email": async_test_user.email,
|
||||||
"password": "SecurePassword123",
|
"password": "SecurePassword123",
|
||||||
"first_name": "Duplicate",
|
"first_name": "Duplicate",
|
||||||
"last_name": "User"
|
"last_name": "User"
|
||||||
@@ -55,9 +59,10 @@ class TestRegisterEndpoint:
|
|||||||
data = response.json()
|
data = response.json()
|
||||||
assert data["success"] is False
|
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."""
|
"""Test registration with weak password."""
|
||||||
response = client.post(
|
response = await client.post(
|
||||||
"/api/v1/auth/register",
|
"/api/v1/auth/register",
|
||||||
json={
|
json={
|
||||||
"email": "weakpass@example.com",
|
"email": "weakpass@example.com",
|
||||||
@@ -69,12 +74,13 @@ class TestRegisterEndpoint:
|
|||||||
|
|
||||||
assert response.status_code == status.HTTP_422_UNPROCESSABLE_ENTITY
|
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."""
|
"""Test registration with unexpected error."""
|
||||||
with patch('app.services.auth_service.AuthService.create_user') as mock_create:
|
with patch('app.services.auth_service.AuthService.create_user') as mock_create:
|
||||||
mock_create.side_effect = Exception("Unexpected error")
|
mock_create.side_effect = Exception("Unexpected error")
|
||||||
|
|
||||||
response = client.post(
|
response = await client.post(
|
||||||
"/api/v1/auth/register",
|
"/api/v1/auth/register",
|
||||||
json={
|
json={
|
||||||
"email": "error@example.com",
|
"email": "error@example.com",
|
||||||
@@ -90,12 +96,13 @@ class TestRegisterEndpoint:
|
|||||||
class TestLoginEndpoint:
|
class TestLoginEndpoint:
|
||||||
"""Tests for POST /auth/login endpoint."""
|
"""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."""
|
"""Test successful login."""
|
||||||
response = client.post(
|
response = await client.post(
|
||||||
"/api/v1/auth/login",
|
"/api/v1/auth/login",
|
||||||
json={
|
json={
|
||||||
"email": test_user.email,
|
"email": async_test_user.email,
|
||||||
"password": "TestPassword123"
|
"password": "TestPassword123"
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
@@ -106,21 +113,23 @@ class TestLoginEndpoint:
|
|||||||
assert "refresh_token" in data
|
assert "refresh_token" in data
|
||||||
assert data["token_type"] == "bearer"
|
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."""
|
"""Test login with wrong password."""
|
||||||
response = client.post(
|
response = await client.post(
|
||||||
"/api/v1/auth/login",
|
"/api/v1/auth/login",
|
||||||
json={
|
json={
|
||||||
"email": test_user.email,
|
"email": async_test_user.email,
|
||||||
"password": "WrongPassword123"
|
"password": "WrongPassword123"
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
assert response.status_code == status.HTTP_401_UNAUTHORIZED
|
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."""
|
"""Test login with non-existent email."""
|
||||||
response = client.post(
|
response = await client.post(
|
||||||
"/api/v1/auth/login",
|
"/api/v1/auth/login",
|
||||||
json={
|
json={
|
||||||
"email": "nonexistent@example.com",
|
"email": "nonexistent@example.com",
|
||||||
@@ -130,31 +139,37 @@ class TestLoginEndpoint:
|
|||||||
|
|
||||||
assert response.status_code == status.HTTP_401_UNAUTHORIZED
|
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 login with inactive user."""
|
||||||
test_user.is_active = False
|
test_engine, AsyncTestingSessionLocal = async_test_db
|
||||||
test_db.add(test_user)
|
async with AsyncTestingSessionLocal() as session:
|
||||||
test_db.commit()
|
# 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",
|
"/api/v1/auth/login",
|
||||||
json={
|
json={
|
||||||
"email": test_user.email,
|
"email": async_test_user.email,
|
||||||
"password": "TestPassword123"
|
"password": "TestPassword123"
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
assert response.status_code == status.HTTP_401_UNAUTHORIZED
|
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."""
|
"""Test login with unexpected error."""
|
||||||
with patch('app.services.auth_service.AuthService.authenticate_user') as mock_auth:
|
with patch('app.services.auth_service.AuthService.authenticate_user') as mock_auth:
|
||||||
mock_auth.side_effect = Exception("Database error")
|
mock_auth.side_effect = Exception("Database error")
|
||||||
|
|
||||||
response = client.post(
|
response = await client.post(
|
||||||
"/api/v1/auth/login",
|
"/api/v1/auth/login",
|
||||||
json={
|
json={
|
||||||
"email": test_user.email,
|
"email": async_test_user.email,
|
||||||
"password": "TestPassword123"
|
"password": "TestPassword123"
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
@@ -165,12 +180,13 @@ class TestLoginEndpoint:
|
|||||||
class TestOAuthLoginEndpoint:
|
class TestOAuthLoginEndpoint:
|
||||||
"""Tests for POST /auth/login/oauth endpoint."""
|
"""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."""
|
"""Test successful OAuth login."""
|
||||||
response = client.post(
|
response = await client.post(
|
||||||
"/api/v1/auth/login/oauth",
|
"/api/v1/auth/login/oauth",
|
||||||
data={
|
data={
|
||||||
"username": test_user.email,
|
"username": async_test_user.email,
|
||||||
"password": "TestPassword123"
|
"password": "TestPassword123"
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
@@ -180,43 +196,50 @@ class TestOAuthLoginEndpoint:
|
|||||||
assert "access_token" in data
|
assert "access_token" in data
|
||||||
assert "refresh_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."""
|
"""Test OAuth login with wrong credentials."""
|
||||||
response = client.post(
|
response = await client.post(
|
||||||
"/api/v1/auth/login/oauth",
|
"/api/v1/auth/login/oauth",
|
||||||
data={
|
data={
|
||||||
"username": test_user.email,
|
"username": async_test_user.email,
|
||||||
"password": "WrongPassword"
|
"password": "WrongPassword"
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
assert response.status_code == status.HTTP_401_UNAUTHORIZED
|
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 OAuth login with inactive user."""
|
||||||
test_user.is_active = False
|
test_engine, AsyncTestingSessionLocal = async_test_db
|
||||||
test_db.add(test_user)
|
async with AsyncTestingSessionLocal() as session:
|
||||||
test_db.commit()
|
# 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",
|
"/api/v1/auth/login/oauth",
|
||||||
data={
|
data={
|
||||||
"username": test_user.email,
|
"username": async_test_user.email,
|
||||||
"password": "TestPassword123"
|
"password": "TestPassword123"
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
assert response.status_code == status.HTTP_401_UNAUTHORIZED
|
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."""
|
"""Test OAuth login with unexpected error."""
|
||||||
with patch('app.services.auth_service.AuthService.authenticate_user') as mock_auth:
|
with patch('app.services.auth_service.AuthService.authenticate_user') as mock_auth:
|
||||||
mock_auth.side_effect = Exception("Unexpected error")
|
mock_auth.side_effect = Exception("Unexpected error")
|
||||||
|
|
||||||
response = client.post(
|
response = await client.post(
|
||||||
"/api/v1/auth/login/oauth",
|
"/api/v1/auth/login/oauth",
|
||||||
data={
|
data={
|
||||||
"username": test_user.email,
|
"username": async_test_user.email,
|
||||||
"password": "TestPassword123"
|
"password": "TestPassword123"
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
@@ -227,20 +250,21 @@ class TestOAuthLoginEndpoint:
|
|||||||
class TestRefreshTokenEndpoint:
|
class TestRefreshTokenEndpoint:
|
||||||
"""Tests for POST /auth/refresh endpoint."""
|
"""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."""
|
"""Test successful token refresh."""
|
||||||
# First, login to get a refresh token
|
# First, login to get a refresh token
|
||||||
login_response = client.post(
|
login_response = await client.post(
|
||||||
"/api/v1/auth/login",
|
"/api/v1/auth/login",
|
||||||
json={
|
json={
|
||||||
"email": test_user.email,
|
"email": async_test_user.email,
|
||||||
"password": "TestPassword123"
|
"password": "TestPassword123"
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
refresh_token = login_response.json()["refresh_token"]
|
refresh_token = login_response.json()["refresh_token"]
|
||||||
|
|
||||||
# Now refresh the token
|
# Now refresh the token
|
||||||
response = client.post(
|
response = await client.post(
|
||||||
"/api/v1/auth/refresh",
|
"/api/v1/auth/refresh",
|
||||||
json={"refresh_token": refresh_token}
|
json={"refresh_token": refresh_token}
|
||||||
)
|
)
|
||||||
@@ -250,36 +274,39 @@ class TestRefreshTokenEndpoint:
|
|||||||
assert "access_token" in data
|
assert "access_token" in data
|
||||||
assert "refresh_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."""
|
"""Test refresh with expired token."""
|
||||||
from app.core.auth import TokenExpiredError
|
from app.core.auth import TokenExpiredError
|
||||||
|
|
||||||
with patch('app.services.auth_service.AuthService.refresh_tokens') as mock_refresh:
|
with patch('app.services.auth_service.AuthService.refresh_tokens') as mock_refresh:
|
||||||
mock_refresh.side_effect = TokenExpiredError("Token expired")
|
mock_refresh.side_effect = TokenExpiredError("Token expired")
|
||||||
|
|
||||||
response = client.post(
|
response = await client.post(
|
||||||
"/api/v1/auth/refresh",
|
"/api/v1/auth/refresh",
|
||||||
json={"refresh_token": "some_token"}
|
json={"refresh_token": "some_token"}
|
||||||
)
|
)
|
||||||
|
|
||||||
assert response.status_code == status.HTTP_401_UNAUTHORIZED
|
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."""
|
"""Test refresh with invalid token."""
|
||||||
response = client.post(
|
response = await client.post(
|
||||||
"/api/v1/auth/refresh",
|
"/api/v1/auth/refresh",
|
||||||
json={"refresh_token": "invalid_token"}
|
json={"refresh_token": "invalid_token"}
|
||||||
)
|
)
|
||||||
|
|
||||||
assert response.status_code == status.HTTP_401_UNAUTHORIZED
|
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."""
|
"""Test refresh with unexpected error."""
|
||||||
# Get a valid refresh token first
|
# Get a valid refresh token first
|
||||||
login_response = client.post(
|
login_response = await client.post(
|
||||||
"/api/v1/auth/login",
|
"/api/v1/auth/login",
|
||||||
json={
|
json={
|
||||||
"email": test_user.email,
|
"email": async_test_user.email,
|
||||||
"password": "TestPassword123"
|
"password": "TestPassword123"
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
@@ -288,7 +315,7 @@ class TestRefreshTokenEndpoint:
|
|||||||
with patch('app.services.auth_service.AuthService.refresh_tokens') as mock_refresh:
|
with patch('app.services.auth_service.AuthService.refresh_tokens') as mock_refresh:
|
||||||
mock_refresh.side_effect = Exception("Unexpected error")
|
mock_refresh.side_effect = Exception("Unexpected error")
|
||||||
|
|
||||||
response = client.post(
|
response = await client.post(
|
||||||
"/api/v1/auth/refresh",
|
"/api/v1/auth/refresh",
|
||||||
json={"refresh_token": refresh_token}
|
json={"refresh_token": refresh_token}
|
||||||
)
|
)
|
||||||
@@ -299,48 +326,52 @@ class TestRefreshTokenEndpoint:
|
|||||||
class TestGetCurrentUserEndpoint:
|
class TestGetCurrentUserEndpoint:
|
||||||
"""Tests for GET /auth/me endpoint."""
|
"""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."""
|
"""Test getting current user info."""
|
||||||
# First, login to get an access token
|
# First, login to get an access token
|
||||||
login_response = client.post(
|
login_response = await client.post(
|
||||||
"/api/v1/auth/login",
|
"/api/v1/auth/login",
|
||||||
json={
|
json={
|
||||||
"email": test_user.email,
|
"email": async_test_user.email,
|
||||||
"password": "TestPassword123"
|
"password": "TestPassword123"
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
access_token = login_response.json()["access_token"]
|
access_token = login_response.json()["access_token"]
|
||||||
|
|
||||||
# Get current user info
|
# Get current user info
|
||||||
response = client.get(
|
response = await client.get(
|
||||||
"/api/v1/auth/me",
|
"/api/v1/auth/me",
|
||||||
headers={"Authorization": f"Bearer {access_token}"}
|
headers={"Authorization": f"Bearer {access_token}"}
|
||||||
)
|
)
|
||||||
|
|
||||||
assert response.status_code == status.HTTP_200_OK
|
assert response.status_code == status.HTTP_200_OK
|
||||||
data = response.json()
|
data = response.json()
|
||||||
assert data["email"] == test_user.email
|
assert data["email"] == async_test_user.email
|
||||||
assert data["first_name"] == test_user.first_name
|
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."""
|
"""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
|
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."""
|
"""Test getting current user with invalid token."""
|
||||||
response = client.get(
|
response = await client.get(
|
||||||
"/api/v1/auth/me",
|
"/api/v1/auth/me",
|
||||||
headers={"Authorization": "Bearer invalid_token"}
|
headers={"Authorization": "Bearer invalid_token"}
|
||||||
)
|
)
|
||||||
|
|
||||||
assert response.status_code == status.HTTP_401_UNAUTHORIZED
|
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."""
|
"""Test getting current user with expired token."""
|
||||||
# Use a clearly invalid/malformed token
|
# Use a clearly invalid/malformed token
|
||||||
response = client.get(
|
response = await client.get(
|
||||||
"/api/v1/auth/me",
|
"/api/v1/auth/me",
|
||||||
headers={"Authorization": "Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.invalid"}
|
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.
|
Tests for password reset endpoints.
|
||||||
"""
|
"""
|
||||||
import pytest
|
import pytest
|
||||||
|
import pytest_asyncio
|
||||||
from unittest.mock import patch, AsyncMock, MagicMock
|
from unittest.mock import patch, AsyncMock, MagicMock
|
||||||
from fastapi import status
|
from fastapi import status
|
||||||
|
from sqlalchemy import select
|
||||||
|
|
||||||
from app.schemas.users import PasswordResetRequest, PasswordResetConfirm
|
from app.schemas.users import PasswordResetRequest, PasswordResetConfirm
|
||||||
from app.utils.security import create_password_reset_token
|
from app.utils.security import create_password_reset_token
|
||||||
|
from app.models.user import User
|
||||||
|
|
||||||
|
|
||||||
# Disable rate limiting for tests
|
# Disable rate limiting for tests
|
||||||
@@ -22,14 +25,14 @@ class TestPasswordResetRequest:
|
|||||||
"""Tests for POST /auth/password-reset/request endpoint."""
|
"""Tests for POST /auth/password-reset/request endpoint."""
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@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."""
|
"""Test password reset request with valid email."""
|
||||||
with patch('app.api.routes.auth.email_service.send_password_reset_email') as mock_send:
|
with patch('app.api.routes.auth.email_service.send_password_reset_email') as mock_send:
|
||||||
mock_send.return_value = True
|
mock_send.return_value = True
|
||||||
|
|
||||||
response = client.post(
|
response = await client.post(
|
||||||
"/api/v1/auth/password-reset/request",
|
"/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
|
assert response.status_code == status.HTTP_200_OK
|
||||||
@@ -40,15 +43,15 @@ class TestPasswordResetRequest:
|
|||||||
# Verify email was sent
|
# Verify email was sent
|
||||||
mock_send.assert_called_once()
|
mock_send.assert_called_once()
|
||||||
call_args = mock_send.call_args
|
call_args = mock_send.call_args
|
||||||
assert call_args.kwargs["to_email"] == test_user.email
|
assert call_args.kwargs["to_email"] == async_test_user.email
|
||||||
assert call_args.kwargs["user_name"] == test_user.first_name
|
assert call_args.kwargs["user_name"] == async_test_user.first_name
|
||||||
assert "reset_token" in call_args.kwargs
|
assert "reset_token" in call_args.kwargs
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_password_reset_request_nonexistent_email(self, client):
|
async def test_password_reset_request_nonexistent_email(self, client):
|
||||||
"""Test password reset request with non-existent email."""
|
"""Test password reset request with non-existent email."""
|
||||||
with patch('app.api.routes.auth.email_service.send_password_reset_email') as mock_send:
|
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",
|
"/api/v1/auth/password-reset/request",
|
||||||
json={"email": "nonexistent@example.com"}
|
json={"email": "nonexistent@example.com"}
|
||||||
)
|
)
|
||||||
@@ -62,17 +65,20 @@ class TestPasswordResetRequest:
|
|||||||
mock_send.assert_not_called()
|
mock_send.assert_not_called()
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@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."""
|
"""Test password reset request with inactive user."""
|
||||||
# Deactivate user
|
# Deactivate user
|
||||||
test_user.is_active = False
|
test_engine, AsyncTestingSessionLocal = async_test_db
|
||||||
test_db.add(test_user)
|
async with AsyncTestingSessionLocal() as session:
|
||||||
test_db.commit()
|
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:
|
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",
|
"/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
|
# Should still return success to prevent email enumeration
|
||||||
@@ -86,7 +92,7 @@ class TestPasswordResetRequest:
|
|||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_password_reset_request_invalid_email_format(self, client):
|
async def test_password_reset_request_invalid_email_format(self, client):
|
||||||
"""Test password reset request with invalid email format."""
|
"""Test password reset request with invalid email format."""
|
||||||
response = client.post(
|
response = await client.post(
|
||||||
"/api/v1/auth/password-reset/request",
|
"/api/v1/auth/password-reset/request",
|
||||||
json={"email": "not-an-email"}
|
json={"email": "not-an-email"}
|
||||||
)
|
)
|
||||||
@@ -96,7 +102,7 @@ class TestPasswordResetRequest:
|
|||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_password_reset_request_missing_email(self, client):
|
async def test_password_reset_request_missing_email(self, client):
|
||||||
"""Test password reset request without email."""
|
"""Test password reset request without email."""
|
||||||
response = client.post(
|
response = await client.post(
|
||||||
"/api/v1/auth/password-reset/request",
|
"/api/v1/auth/password-reset/request",
|
||||||
json={}
|
json={}
|
||||||
)
|
)
|
||||||
@@ -104,14 +110,14 @@ class TestPasswordResetRequest:
|
|||||||
assert response.status_code == status.HTTP_422_UNPROCESSABLE_ENTITY
|
assert response.status_code == status.HTTP_422_UNPROCESSABLE_ENTITY
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@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."""
|
"""Test password reset when email service fails."""
|
||||||
with patch('app.api.routes.auth.email_service.send_password_reset_email') as mock_send:
|
with patch('app.api.routes.auth.email_service.send_password_reset_email') as mock_send:
|
||||||
mock_send.side_effect = Exception("SMTP Error")
|
mock_send.side_effect = Exception("SMTP Error")
|
||||||
|
|
||||||
response = client.post(
|
response = await client.post(
|
||||||
"/api/v1/auth/password-reset/request",
|
"/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
|
# Should still return success even if email fails
|
||||||
@@ -120,16 +126,16 @@ class TestPasswordResetRequest:
|
|||||||
assert data["success"] is True
|
assert data["success"] is True
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@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."""
|
"""Test that password reset requests are rate limited."""
|
||||||
with patch('app.api.routes.auth.email_service.send_password_reset_email') as mock_send:
|
with patch('app.api.routes.auth.email_service.send_password_reset_email') as mock_send:
|
||||||
mock_send.return_value = True
|
mock_send.return_value = True
|
||||||
|
|
||||||
# Make multiple requests quickly (3/minute limit)
|
# Make multiple requests quickly (3/minute limit)
|
||||||
for _ in range(3):
|
for _ in range(3):
|
||||||
response = client.post(
|
response = await client.post(
|
||||||
"/api/v1/auth/password-reset/request",
|
"/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
|
assert response.status_code == status.HTTP_200_OK
|
||||||
|
|
||||||
@@ -137,13 +143,14 @@ class TestPasswordResetRequest:
|
|||||||
class TestPasswordResetConfirm:
|
class TestPasswordResetConfirm:
|
||||||
"""Tests for POST /auth/password-reset/confirm endpoint."""
|
"""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."""
|
"""Test password reset confirmation with valid token."""
|
||||||
# Generate 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"
|
new_password = "NewSecure123"
|
||||||
|
|
||||||
response = client.post(
|
response = await client.post(
|
||||||
"/api/v1/auth/password-reset/confirm",
|
"/api/v1/auth/password-reset/confirm",
|
||||||
json={
|
json={
|
||||||
"token": token,
|
"token": token,
|
||||||
@@ -157,21 +164,25 @@ class TestPasswordResetConfirm:
|
|||||||
assert "successfully" in data["message"].lower()
|
assert "successfully" in data["message"].lower()
|
||||||
|
|
||||||
# Verify user can login with new password
|
# Verify user can login with new password
|
||||||
test_db.refresh(test_user)
|
test_engine, AsyncTestingSessionLocal = async_test_db
|
||||||
from app.core.auth import verify_password
|
async with AsyncTestingSessionLocal() as session:
|
||||||
assert verify_password(new_password, test_user.password_hash) is True
|
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."""
|
"""Test password reset confirmation with expired token."""
|
||||||
import time as time_module
|
import time as time_module
|
||||||
|
|
||||||
# Create token that expires immediately
|
# 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
|
# Wait for token to expire
|
||||||
time_module.sleep(2)
|
time_module.sleep(2)
|
||||||
|
|
||||||
response = client.post(
|
response = await client.post(
|
||||||
"/api/v1/auth/password-reset/confirm",
|
"/api/v1/auth/password-reset/confirm",
|
||||||
json={
|
json={
|
||||||
"token": token,
|
"token": token,
|
||||||
@@ -186,9 +197,10 @@ class TestPasswordResetConfirm:
|
|||||||
error_msg = data["errors"][0]["message"].lower() if "errors" in data else ""
|
error_msg = data["errors"][0]["message"].lower() if "errors" in data else ""
|
||||||
assert "invalid" in error_msg or "expired" in error_msg
|
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."""
|
"""Test password reset confirmation with invalid token."""
|
||||||
response = client.post(
|
response = await client.post(
|
||||||
"/api/v1/auth/password-reset/confirm",
|
"/api/v1/auth/password-reset/confirm",
|
||||||
json={
|
json={
|
||||||
"token": "invalid_token_xyz",
|
"token": "invalid_token_xyz",
|
||||||
@@ -202,13 +214,14 @@ class TestPasswordResetConfirm:
|
|||||||
error_msg = data["errors"][0]["message"].lower() if "errors" in data else ""
|
error_msg = data["errors"][0]["message"].lower() if "errors" in data else ""
|
||||||
assert "invalid" in error_msg or "expired" in error_msg
|
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."""
|
"""Test password reset confirmation with tampered token."""
|
||||||
import base64
|
import base64
|
||||||
import json
|
import json
|
||||||
|
|
||||||
# Create valid token and tamper with it
|
# 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')
|
decoded = base64.urlsafe_b64decode(token.encode('utf-8')).decode('utf-8')
|
||||||
token_data = json.loads(decoded)
|
token_data = json.loads(decoded)
|
||||||
token_data["payload"]["email"] = "hacker@example.com"
|
token_data["payload"]["email"] = "hacker@example.com"
|
||||||
@@ -216,7 +229,7 @@ class TestPasswordResetConfirm:
|
|||||||
# Re-encode tampered token
|
# Re-encode tampered token
|
||||||
tampered = base64.urlsafe_b64encode(json.dumps(token_data).encode('utf-8')).decode('utf-8')
|
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",
|
"/api/v1/auth/password-reset/confirm",
|
||||||
json={
|
json={
|
||||||
"token": tampered,
|
"token": tampered,
|
||||||
@@ -226,12 +239,13 @@ class TestPasswordResetConfirm:
|
|||||||
|
|
||||||
assert response.status_code == status.HTTP_400_BAD_REQUEST
|
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."""
|
"""Test password reset confirmation for non-existent user."""
|
||||||
# Create token for email that doesn't exist
|
# Create token for email that doesn't exist
|
||||||
token = create_password_reset_token("nonexistent@example.com")
|
token = create_password_reset_token("nonexistent@example.com")
|
||||||
|
|
||||||
response = client.post(
|
response = await client.post(
|
||||||
"/api/v1/auth/password-reset/confirm",
|
"/api/v1/auth/password-reset/confirm",
|
||||||
json={
|
json={
|
||||||
"token": token,
|
"token": token,
|
||||||
@@ -245,16 +259,20 @@ class TestPasswordResetConfirm:
|
|||||||
error_msg = data["errors"][0]["message"].lower() if "errors" in data else ""
|
error_msg = data["errors"][0]["message"].lower() if "errors" in data else ""
|
||||||
assert "not found" in error_msg
|
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."""
|
"""Test password reset confirmation for inactive user."""
|
||||||
# Deactivate user
|
# Deactivate user
|
||||||
test_user.is_active = False
|
test_engine, AsyncTestingSessionLocal = async_test_db
|
||||||
test_db.add(test_user)
|
async with AsyncTestingSessionLocal() as session:
|
||||||
test_db.commit()
|
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",
|
"/api/v1/auth/password-reset/confirm",
|
||||||
json={
|
json={
|
||||||
"token": token,
|
"token": token,
|
||||||
@@ -268,9 +286,10 @@ class TestPasswordResetConfirm:
|
|||||||
error_msg = data["errors"][0]["message"].lower() if "errors" in data else ""
|
error_msg = data["errors"][0]["message"].lower() if "errors" in data else ""
|
||||||
assert "inactive" in error_msg
|
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."""
|
"""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
|
# Test various weak passwords
|
||||||
weak_passwords = [
|
weak_passwords = [
|
||||||
@@ -280,7 +299,7 @@ class TestPasswordResetConfirm:
|
|||||||
]
|
]
|
||||||
|
|
||||||
for weak_password in weak_passwords:
|
for weak_password in weak_passwords:
|
||||||
response = client.post(
|
response = await client.post(
|
||||||
"/api/v1/auth/password-reset/confirm",
|
"/api/v1/auth/password-reset/confirm",
|
||||||
json={
|
json={
|
||||||
"token": token,
|
"token": token,
|
||||||
@@ -290,10 +309,11 @@ class TestPasswordResetConfirm:
|
|||||||
|
|
||||||
assert response.status_code == status.HTTP_422_UNPROCESSABLE_ENTITY
|
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."""
|
"""Test password reset confirmation with missing fields."""
|
||||||
# Missing token
|
# Missing token
|
||||||
response = client.post(
|
response = await client.post(
|
||||||
"/api/v1/auth/password-reset/confirm",
|
"/api/v1/auth/password-reset/confirm",
|
||||||
json={"new_password": "NewSecure123"}
|
json={"new_password": "NewSecure123"}
|
||||||
)
|
)
|
||||||
@@ -301,20 +321,22 @@ class TestPasswordResetConfirm:
|
|||||||
|
|
||||||
# Missing password
|
# Missing password
|
||||||
token = create_password_reset_token("test@example.com")
|
token = create_password_reset_token("test@example.com")
|
||||||
response = client.post(
|
response = await client.post(
|
||||||
"/api/v1/auth/password-reset/confirm",
|
"/api/v1/auth/password-reset/confirm",
|
||||||
json={"token": token}
|
json={"token": token}
|
||||||
)
|
)
|
||||||
assert response.status_code == status.HTTP_422_UNPROCESSABLE_ENTITY
|
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."""
|
"""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 the password update to raise an exception
|
||||||
mock_commit.side_effect = Exception("Database error")
|
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",
|
"/api/v1/auth/password-reset/confirm",
|
||||||
json={
|
json={
|
||||||
"token": token,
|
"token": token,
|
||||||
@@ -328,18 +350,19 @@ class TestPasswordResetConfirm:
|
|||||||
error_msg = data["errors"][0]["message"].lower() if "errors" in data else ""
|
error_msg = data["errors"][0]["message"].lower() if "errors" in data else ""
|
||||||
assert "error" in error_msg or "resetting" in error_msg
|
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."""
|
"""Test complete password reset flow."""
|
||||||
original_password = test_user.password_hash
|
original_password = async_test_user.password_hash
|
||||||
new_password = "BrandNew123"
|
new_password = "BrandNew123"
|
||||||
|
|
||||||
# Step 1: Request password reset
|
# Step 1: Request password reset
|
||||||
with patch('app.api.routes.auth.email_service.send_password_reset_email') as mock_send:
|
with patch('app.api.routes.auth.email_service.send_password_reset_email') as mock_send:
|
||||||
mock_send.return_value = True
|
mock_send.return_value = True
|
||||||
|
|
||||||
response = client.post(
|
response = await client.post(
|
||||||
"/api/v1/auth/password-reset/request",
|
"/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
|
assert response.status_code == status.HTTP_200_OK
|
||||||
@@ -349,7 +372,7 @@ class TestPasswordResetConfirm:
|
|||||||
reset_token = call_args.kwargs["reset_token"]
|
reset_token = call_args.kwargs["reset_token"]
|
||||||
|
|
||||||
# Step 2: Confirm password reset
|
# Step 2: Confirm password reset
|
||||||
response = client.post(
|
response = await client.post(
|
||||||
"/api/v1/auth/password-reset/confirm",
|
"/api/v1/auth/password-reset/confirm",
|
||||||
json={
|
json={
|
||||||
"token": reset_token,
|
"token": reset_token,
|
||||||
@@ -360,15 +383,18 @@ class TestPasswordResetConfirm:
|
|||||||
assert response.status_code == status.HTTP_200_OK
|
assert response.status_code == status.HTTP_200_OK
|
||||||
|
|
||||||
# Step 3: Verify old password doesn't work
|
# Step 3: Verify old password doesn't work
|
||||||
test_db.refresh(test_user)
|
test_engine, AsyncTestingSessionLocal = async_test_db
|
||||||
from app.core.auth import verify_password
|
async with AsyncTestingSessionLocal() as session:
|
||||||
assert test_user.password_hash != original_password
|
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
|
# Step 4: Verify new password works
|
||||||
response = client.post(
|
response = await client.post(
|
||||||
"/api/v1/auth/login",
|
"/api/v1/auth/login",
|
||||||
json={
|
json={
|
||||||
"email": test_user.email,
|
"email": async_test_user.email,
|
||||||
"password": new_password
|
"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.
|
These tests focus on finding potential bugs, not just coverage.
|
||||||
"""
|
"""
|
||||||
import pytest
|
import pytest
|
||||||
|
import pytest_asyncio
|
||||||
from unittest.mock import patch
|
from unittest.mock import patch
|
||||||
from fastapi import status
|
from fastapi import status
|
||||||
import uuid
|
import uuid
|
||||||
|
|
||||||
|
from sqlalchemy import select
|
||||||
|
from app.models.user import User
|
||||||
from app.models.user import User
|
from app.models.user import User
|
||||||
from app.schemas.users import UserUpdate
|
from app.schemas.users import UserUpdate
|
||||||
|
|
||||||
@@ -21,9 +24,9 @@ def disable_rate_limit():
|
|||||||
yield
|
yield
|
||||||
|
|
||||||
|
|
||||||
def get_auth_headers(client, email, password):
|
async def get_auth_headers(client, email, password):
|
||||||
"""Helper to get authentication headers."""
|
"""Helper to get authentication headers."""
|
||||||
response = client.post(
|
response = await client.post(
|
||||||
"/api/v1/auth/login",
|
"/api/v1/auth/login",
|
||||||
json={"email": email, "password": password}
|
json={"email": email, "password": password}
|
||||||
)
|
)
|
||||||
@@ -34,11 +37,12 @@ def get_auth_headers(client, email, password):
|
|||||||
class TestListUsers:
|
class TestListUsers:
|
||||||
"""Tests for GET /users endpoint."""
|
"""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."""
|
"""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
|
assert response.status_code == status.HTTP_200_OK
|
||||||
data = response.json()
|
data = response.json()
|
||||||
@@ -46,15 +50,17 @@ class TestListUsers:
|
|||||||
assert "pagination" in data
|
assert "pagination" in data
|
||||||
assert isinstance(data["data"], list)
|
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."""
|
"""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
|
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."""
|
"""Test pagination works correctly."""
|
||||||
# Create multiple users
|
# Create multiple users
|
||||||
for i in range(15):
|
for i in range(15):
|
||||||
@@ -68,17 +74,18 @@ class TestListUsers:
|
|||||||
test_db.add(user)
|
test_db.add(user)
|
||||||
test_db.commit()
|
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
|
# 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
|
assert response.status_code == status.HTTP_200_OK
|
||||||
data = response.json()
|
data = response.json()
|
||||||
assert len(data["data"]) == 5
|
assert len(data["data"]) == 5
|
||||||
assert data["pagination"]["page"] == 1
|
assert data["pagination"]["page"] == 1
|
||||||
assert data["pagination"]["total"] >= 15
|
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."""
|
"""Test filtering by active status."""
|
||||||
# Create active and inactive users
|
# Create active and inactive users
|
||||||
active_user = User(
|
active_user = User(
|
||||||
@@ -98,35 +105,37 @@ class TestListUsers:
|
|||||||
test_db.add_all([active_user, inactive_user])
|
test_db.add_all([active_user, inactive_user])
|
||||||
test_db.commit()
|
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
|
# 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()
|
data = response.json()
|
||||||
emails = [u["email"] for u in data["data"]]
|
emails = [u["email"] for u in data["data"]]
|
||||||
assert "activefilter@example.com" in emails
|
assert "activefilter@example.com" in emails
|
||||||
assert "inactivefilter@example.com" not in emails
|
assert "inactivefilter@example.com" not in emails
|
||||||
|
|
||||||
# Filter for inactive users
|
# 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()
|
data = response.json()
|
||||||
emails = [u["email"] for u in data["data"]]
|
emails = [u["email"] for u in data["data"]]
|
||||||
assert "inactivefilter@example.com" in emails
|
assert "inactivefilter@example.com" in emails
|
||||||
assert "activefilter@example.com" not 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."""
|
"""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
|
assert response.status_code == status.HTTP_200_OK
|
||||||
data = response.json()
|
data = response.json()
|
||||||
emails = [u["email"] for u in data["data"]]
|
emails = [u["email"] for u in data["data"]]
|
||||||
assert emails == sorted(emails)
|
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."""
|
"""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
|
assert response.status_code == status.HTTP_401_UNAUTHORIZED
|
||||||
|
|
||||||
# Note: Removed test_list_users_unexpected_error because mocking at CRUD level
|
# Note: Removed test_list_users_unexpected_error because mocking at CRUD level
|
||||||
@@ -136,31 +145,34 @@ class TestListUsers:
|
|||||||
class TestGetCurrentUserProfile:
|
class TestGetCurrentUserProfile:
|
||||||
"""Tests for GET /users/me endpoint."""
|
"""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."""
|
"""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
|
assert response.status_code == status.HTTP_200_OK
|
||||||
data = response.json()
|
data = response.json()
|
||||||
assert data["email"] == test_user.email
|
assert data["email"] == async_test_user.email
|
||||||
assert data["first_name"] == test_user.first_name
|
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."""
|
"""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
|
assert response.status_code == status.HTTP_401_UNAUTHORIZED
|
||||||
|
|
||||||
|
|
||||||
class TestUpdateCurrentUser:
|
class TestUpdateCurrentUser:
|
||||||
"""Tests for PATCH /users/me endpoint."""
|
"""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."""
|
"""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",
|
"/api/v1/users/me",
|
||||||
headers=headers,
|
headers=headers,
|
||||||
json={"first_name": "Updated", "last_name": "Name"}
|
json={"first_name": "Updated", "last_name": "Name"}
|
||||||
@@ -172,14 +184,15 @@ class TestUpdateCurrentUser:
|
|||||||
assert data["last_name"] == "Name"
|
assert data["last_name"] == "Name"
|
||||||
|
|
||||||
# Verify in database
|
# Verify in database
|
||||||
test_db.refresh(test_user)
|
test_db.refresh(async_test_user)
|
||||||
assert test_user.first_name == "Updated"
|
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."""
|
"""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",
|
"/api/v1/users/me",
|
||||||
headers=headers,
|
headers=headers,
|
||||||
json={"phone_number": "+19876543210"}
|
json={"phone_number": "+19876543210"}
|
||||||
@@ -189,11 +202,12 @@ class TestUpdateCurrentUser:
|
|||||||
data = response.json()
|
data = response.json()
|
||||||
assert data["phone_number"] == "+19876543210"
|
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."""
|
"""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",
|
"/api/v1/users/me",
|
||||||
headers=headers,
|
headers=headers,
|
||||||
json={"phone_number": "invalid"}
|
json={"phone_number": "invalid"}
|
||||||
@@ -201,13 +215,14 @@ class TestUpdateCurrentUser:
|
|||||||
|
|
||||||
assert response.status_code == status.HTTP_422_UNPROCESSABLE_ENTITY
|
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."""
|
"""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
|
# 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
|
# This tests that even if someone tries to send it, it's rejected
|
||||||
response = client.patch(
|
response = await client.patch(
|
||||||
"/api/v1/users/me",
|
"/api/v1/users/me",
|
||||||
headers=headers,
|
headers=headers,
|
||||||
json={"first_name": "Test", "is_superuser": True}
|
json={"first_name": "Test", "is_superuser": True}
|
||||||
@@ -220,9 +235,10 @@ class TestUpdateCurrentUser:
|
|||||||
# Verify user is still not a superuser
|
# Verify user is still not a superuser
|
||||||
assert data["is_superuser"] is False
|
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."""
|
"""Test that unauthenticated requests are rejected."""
|
||||||
response = client.patch(
|
response = await client.patch(
|
||||||
"/api/v1/users/me",
|
"/api/v1/users/me",
|
||||||
json={"first_name": "Hacker"}
|
json={"first_name": "Hacker"}
|
||||||
)
|
)
|
||||||
@@ -234,17 +250,19 @@ class TestUpdateCurrentUser:
|
|||||||
class TestGetUserById:
|
class TestGetUserById:
|
||||||
"""Tests for GET /users/{user_id} endpoint."""
|
"""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."""
|
"""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
|
assert response.status_code == status.HTTP_200_OK
|
||||||
data = response.json()
|
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."""
|
"""Test that regular users cannot view other profiles."""
|
||||||
# Create another user
|
# Create another user
|
||||||
other_user = User(
|
other_user = User(
|
||||||
@@ -258,36 +276,39 @@ class TestGetUserById:
|
|||||||
test_db.commit()
|
test_db.commit()
|
||||||
test_db.refresh(other_user)
|
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
|
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."""
|
"""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
|
assert response.status_code == status.HTTP_200_OK
|
||||||
data = response.json()
|
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."""
|
"""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()
|
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
|
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."""
|
"""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
|
assert response.status_code == status.HTTP_422_UNPROCESSABLE_ENTITY
|
||||||
|
|
||||||
@@ -295,12 +316,13 @@ class TestGetUserById:
|
|||||||
class TestUpdateUserById:
|
class TestUpdateUserById:
|
||||||
"""Tests for PATCH /users/{user_id} endpoint."""
|
"""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."""
|
"""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(
|
response = await client.patch(
|
||||||
f"/api/v1/users/{test_user.id}",
|
f"/api/v1/users/{async_test_user.id}",
|
||||||
headers=headers,
|
headers=headers,
|
||||||
json={"first_name": "SelfUpdated"}
|
json={"first_name": "SelfUpdated"}
|
||||||
)
|
)
|
||||||
@@ -309,7 +331,8 @@ class TestUpdateUserById:
|
|||||||
data = response.json()
|
data = response.json()
|
||||||
assert data["first_name"] == "SelfUpdated"
|
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."""
|
"""Test that regular users cannot update other profiles."""
|
||||||
# Create another user
|
# Create another user
|
||||||
other_user = User(
|
other_user = User(
|
||||||
@@ -323,9 +346,9 @@ class TestUpdateUserById:
|
|||||||
test_db.commit()
|
test_db.commit()
|
||||||
test_db.refresh(other_user)
|
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}",
|
f"/api/v1/users/{other_user.id}",
|
||||||
headers=headers,
|
headers=headers,
|
||||||
json={"first_name": "Hacked"}
|
json={"first_name": "Hacked"}
|
||||||
@@ -337,12 +360,13 @@ class TestUpdateUserById:
|
|||||||
test_db.refresh(other_user)
|
test_db.refresh(other_user)
|
||||||
assert other_user.first_name == "Other"
|
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."""
|
"""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(
|
response = await client.patch(
|
||||||
f"/api/v1/users/{test_user.id}",
|
f"/api/v1/users/{async_test_user.id}",
|
||||||
headers=headers,
|
headers=headers,
|
||||||
json={"first_name": "AdminUpdated"}
|
json={"first_name": "AdminUpdated"}
|
||||||
)
|
)
|
||||||
@@ -351,14 +375,15 @@ class TestUpdateUserById:
|
|||||||
data = response.json()
|
data = response.json()
|
||||||
assert data["first_name"] == "AdminUpdated"
|
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."""
|
"""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
|
# is_superuser not in UserUpdate schema, so it gets ignored by Pydantic
|
||||||
# Just verify the user stays the same
|
# Just verify the user stays the same
|
||||||
response = client.patch(
|
response = await client.patch(
|
||||||
f"/api/v1/users/{test_user.id}",
|
f"/api/v1/users/{async_test_user.id}",
|
||||||
headers=headers,
|
headers=headers,
|
||||||
json={"first_name": "Test"}
|
json={"first_name": "Test"}
|
||||||
)
|
)
|
||||||
@@ -367,12 +392,13 @@ class TestUpdateUserById:
|
|||||||
data = response.json()
|
data = response.json()
|
||||||
assert data["is_superuser"] is False
|
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."""
|
"""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(
|
response = await client.patch(
|
||||||
f"/api/v1/users/{test_user.id}",
|
f"/api/v1/users/{async_test_user.id}",
|
||||||
headers=headers,
|
headers=headers,
|
||||||
json={"first_name": "AdminChanged", "is_active": False}
|
json={"first_name": "AdminChanged", "is_active": False}
|
||||||
)
|
)
|
||||||
@@ -382,12 +408,13 @@ class TestUpdateUserById:
|
|||||||
assert data["first_name"] == "AdminChanged"
|
assert data["first_name"] == "AdminChanged"
|
||||||
assert data["is_active"] is False
|
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."""
|
"""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()
|
fake_id = uuid.uuid4()
|
||||||
|
|
||||||
response = client.patch(
|
response = await client.patch(
|
||||||
f"/api/v1/users/{fake_id}",
|
f"/api/v1/users/{fake_id}",
|
||||||
headers=headers,
|
headers=headers,
|
||||||
json={"first_name": "Ghost"}
|
json={"first_name": "Ghost"}
|
||||||
@@ -401,11 +428,12 @@ class TestUpdateUserById:
|
|||||||
class TestChangePassword:
|
class TestChangePassword:
|
||||||
"""Tests for PATCH /users/me/password endpoint."""
|
"""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."""
|
"""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",
|
"/api/v1/users/me/password",
|
||||||
headers=headers,
|
headers=headers,
|
||||||
json={
|
json={
|
||||||
@@ -419,20 +447,21 @@ class TestChangePassword:
|
|||||||
assert data["success"] is True
|
assert data["success"] is True
|
||||||
|
|
||||||
# Verify can login with new password
|
# Verify can login with new password
|
||||||
login_response = client.post(
|
login_response = await client.post(
|
||||||
"/api/v1/auth/login",
|
"/api/v1/auth/login",
|
||||||
json={
|
json={
|
||||||
"email": test_user.email,
|
"email": async_test_user.email,
|
||||||
"password": "NewPassword123"
|
"password": "NewPassword123"
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
assert login_response.status_code == status.HTTP_200_OK
|
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."""
|
"""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",
|
"/api/v1/users/me/password",
|
||||||
headers=headers,
|
headers=headers,
|
||||||
json={
|
json={
|
||||||
@@ -443,11 +472,12 @@ class TestChangePassword:
|
|||||||
|
|
||||||
assert response.status_code == status.HTTP_403_FORBIDDEN
|
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."""
|
"""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",
|
"/api/v1/users/me/password",
|
||||||
headers=headers,
|
headers=headers,
|
||||||
json={
|
json={
|
||||||
@@ -458,9 +488,10 @@ class TestChangePassword:
|
|||||||
|
|
||||||
assert response.status_code == status.HTTP_422_UNPROCESSABLE_ENTITY
|
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."""
|
"""Test that unauthenticated requests are rejected."""
|
||||||
response = client.patch(
|
response = await client.patch(
|
||||||
"/api/v1/users/me/password",
|
"/api/v1/users/me/password",
|
||||||
json={
|
json={
|
||||||
"current_password": "TestPassword123",
|
"current_password": "TestPassword123",
|
||||||
@@ -475,7 +506,8 @@ class TestChangePassword:
|
|||||||
class TestDeleteUser:
|
class TestDeleteUser:
|
||||||
"""Tests for DELETE /users/{user_id} endpoint."""
|
"""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."""
|
"""Test deleting a user as superuser."""
|
||||||
# Create a user to delete
|
# Create a user to delete
|
||||||
user_to_delete = User(
|
user_to_delete = User(
|
||||||
@@ -489,9 +521,9 @@ class TestDeleteUser:
|
|||||||
test_db.commit()
|
test_db.commit()
|
||||||
test_db.refresh(user_to_delete)
|
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
|
assert response.status_code == status.HTTP_200_OK
|
||||||
data = response.json()
|
data = response.json()
|
||||||
@@ -501,15 +533,17 @@ class TestDeleteUser:
|
|||||||
test_db.refresh(user_to_delete)
|
test_db.refresh(user_to_delete)
|
||||||
assert user_to_delete.deleted_at is not None
|
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."""
|
"""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
|
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."""
|
"""Test that regular users cannot delete users."""
|
||||||
# Create another user
|
# Create another user
|
||||||
other_user = User(
|
other_user = User(
|
||||||
@@ -523,24 +557,26 @@ class TestDeleteUser:
|
|||||||
test_db.commit()
|
test_db.commit()
|
||||||
test_db.refresh(other_user)
|
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
|
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."""
|
"""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()
|
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
|
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."""
|
"""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
|
assert response.status_code == status.HTTP_401_UNAUTHORIZED
|
||||||
|
|
||||||
# Note: Removed test_delete_user_unexpected_error - see comment above
|
# 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
|
from datetime import datetime, timezone
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
from fastapi.testclient import TestClient
|
import pytest_asyncio
|
||||||
|
from httpx import AsyncClient
|
||||||
|
|
||||||
# Set IS_TEST environment variable BEFORE importing app
|
# Set IS_TEST environment variable BEFORE importing app
|
||||||
# This prevents the scheduler from starting during tests
|
# This prevents the scheduler from starting during tests
|
||||||
os.environ["IS_TEST"] = "True"
|
os.environ["IS_TEST"] = "True"
|
||||||
|
|
||||||
from app.main import app
|
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.models.user import User
|
||||||
from app.core.auth import get_password_hash
|
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
|
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)
|
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():
|
async def async_test_db():
|
||||||
"""Fixture provides new testing engine and session for each test run to improve isolation."""
|
"""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)
|
teardown_test_db(test_engine)
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture(scope="function")
|
@pytest_asyncio.fixture(scope="function")
|
||||||
def client(test_db):
|
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():
|
test_engine, AsyncTestingSessionLocal = async_test_db
|
||||||
try:
|
|
||||||
yield test_db
|
|
||||||
finally:
|
|
||||||
pass
|
|
||||||
|
|
||||||
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
|
yield test_client
|
||||||
|
|
||||||
app.dependency_overrides.clear()
|
app.dependency_overrides.clear()
|
||||||
@@ -116,7 +120,7 @@ def client(test_db):
|
|||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def test_user(test_db):
|
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
|
Password: TestPassword123
|
||||||
"""
|
"""
|
||||||
@@ -140,7 +144,7 @@ def test_user(test_db):
|
|||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def test_superuser(test_db):
|
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
|
Password: SuperPassword123
|
||||||
"""
|
"""
|
||||||
@@ -158,4 +162,56 @@ def test_superuser(test_db):
|
|||||||
test_db.add(user)
|
test_db.add(user)
|
||||||
test_db.commit()
|
test_db.commit()
|
||||||
test_db.refresh(user)
|
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