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