Files
sdb2-backend/tests/services/test_auth_service.py

258 lines
8.6 KiB
Python

"""Tests for authentication service."""
import pytest
import pytest_asyncio
from fastapi import HTTPException
from sqlmodel.ext.asyncio.session import AsyncSession
from app.models.plan import Plan
from app.models.user import User
from app.schemas.auth import UserLoginRequest, UserRegisterRequest
from app.services.auth import AuthService
# Constants
HTTP_400_BAD_REQUEST = 400
HTTP_401_UNAUTHORIZED = 401
HTTP_404_NOT_FOUND = 404
class TestAuthService:
"""Test authentication service operations."""
@pytest_asyncio.fixture
def auth_service(self, test_session: AsyncSession) -> AuthService:
"""Create an auth service instance."""
return AuthService(test_session)
@pytest.mark.asyncio
async def test_register_success(
self,
auth_service: AuthService,
ensure_plans: tuple[Plan, Plan],
test_user_data: dict[str, str],
) -> None:
"""Test successful user registration."""
request = UserRegisterRequest(**test_user_data)
response = await auth_service.register(request)
# Check user data
assert response.user.email == test_user_data["email"]
assert response.user.name == test_user_data["name"]
assert response.user.role == "admin" # First user gets admin role
assert response.user.is_active is True
# First user gets pro plan
free_plan, pro_plan = ensure_plans
assert response.user.credits == pro_plan.credits
assert response.user.plan["code"] == pro_plan.code
# Check token
assert response.token.access_token is not None
assert response.token.token_type == "bearer"
assert response.token.expires_in > 0
@pytest.mark.asyncio
async def test_register_duplicate_email(
self,
auth_service: AuthService,
test_user: User,
) -> None:
"""Test registration with duplicate email."""
request = UserRegisterRequest(
email=test_user.email,
password="password123",
name="Another User",
)
with pytest.raises(HTTPException) as exc_info:
await auth_service.register(request)
assert exc_info.value.status_code == HTTP_400_BAD_REQUEST
assert "Email address is already registered" in exc_info.value.detail
@pytest.mark.asyncio
async def test_login_success(
self,
auth_service: AuthService,
test_user: User,
test_login_data: dict[str, str],
) -> None:
"""Test successful user login."""
request = UserLoginRequest(**test_login_data)
response = await auth_service.login(request)
# Check user data
assert response.user.id == test_user.id
assert response.user.email == test_user.email
assert response.user.name == test_user.name
assert response.user.role == test_user.role
assert response.user.is_active == test_user.is_active
# Check token
assert response.token.access_token is not None
assert response.token.token_type == "bearer"
assert response.token.expires_in > 0
@pytest.mark.asyncio
async def test_login_invalid_email(self, auth_service: AuthService) -> None:
"""Test login with invalid email."""
request = UserLoginRequest(
email="nonexistent@example.com",
password="password123",
)
with pytest.raises(HTTPException) as exc_info:
await auth_service.login(request)
assert exc_info.value.status_code == HTTP_401_UNAUTHORIZED
assert "Invalid email or password" in exc_info.value.detail
@pytest.mark.asyncio
async def test_login_invalid_password(
self,
auth_service: AuthService,
test_user: User,
) -> None:
"""Test login with invalid password."""
request = UserLoginRequest(email=test_user.email, password="wrongpassword")
with pytest.raises(HTTPException) as exc_info:
await auth_service.login(request)
assert exc_info.value.status_code == HTTP_401_UNAUTHORIZED
assert "Invalid email or password" in exc_info.value.detail
@pytest.mark.asyncio
async def test_login_inactive_user(
self,
auth_service: AuthService,
test_user: User,
test_session: AsyncSession,
) -> None:
"""Test login with inactive user."""
# Store the email before deactivating
user_email = test_user.email
# Deactivate the user
test_user.is_active = False
await test_session.commit()
request = UserLoginRequest(email=user_email, password="testpassword123")
with pytest.raises(HTTPException) as exc_info:
await auth_service.login(request)
assert exc_info.value.status_code == HTTP_401_UNAUTHORIZED
assert "Account is deactivated" in exc_info.value.detail
@pytest.mark.asyncio
async def test_login_user_without_password(
self,
auth_service: AuthService,
test_user: User,
test_session: AsyncSession,
) -> None:
"""Test login with user that has no password hash."""
# Store the email before removing password
user_email = test_user.email
# Remove password hash
test_user.password_hash = None
await test_session.commit()
request = UserLoginRequest(email=user_email, password="anypassword")
with pytest.raises(HTTPException) as exc_info:
await auth_service.login(request)
assert exc_info.value.status_code == HTTP_401_UNAUTHORIZED
assert "Invalid email or password" in exc_info.value.detail
@pytest.mark.asyncio
async def test_get_current_user_success(
self,
auth_service: AuthService,
test_user: User,
) -> None:
"""Test getting current user successfully."""
user = await auth_service.get_current_user(test_user.id)
assert user.id == test_user.id
assert user.email == test_user.email
assert user.name == test_user.name
assert user.is_active == test_user.is_active
@pytest.mark.asyncio
async def test_get_current_user_not_found(self, auth_service: AuthService) -> None:
"""Test getting current user when user doesn't exist."""
with pytest.raises(HTTPException) as exc_info:
await auth_service.get_current_user(99999)
assert exc_info.value.status_code == HTTP_404_NOT_FOUND
assert "User not found" in exc_info.value.detail
@pytest.mark.asyncio
async def test_get_current_user_inactive(
self,
auth_service: AuthService,
test_user: User,
test_session: AsyncSession,
) -> None:
"""Test getting current user when user is inactive."""
# Store the user ID before deactivating
user_id = test_user.id
# Deactivate the user
test_user.is_active = False
await test_session.commit()
with pytest.raises(HTTPException) as exc_info:
await auth_service.get_current_user(user_id)
assert exc_info.value.status_code == HTTP_401_UNAUTHORIZED
assert "Account is deactivated" in exc_info.value.detail
@pytest.mark.asyncio
async def test_create_access_token(
self,
auth_service: AuthService,
test_user: User,
) -> None:
"""Test access token creation."""
token_response = auth_service._create_access_token(test_user)
assert token_response.access_token is not None
assert token_response.token_type == "bearer"
assert token_response.expires_in > 0
# Verify token contains correct data
from app.utils.auth import JWTUtils
decoded = JWTUtils.decode_access_token(token_response.access_token)
assert decoded["sub"] == str(test_user.id)
assert decoded["email"] == test_user.email
assert decoded["role"] == test_user.role
@pytest.mark.asyncio
async def test_create_user_response(
self,
auth_service: AuthService,
test_user: User,
test_session: AsyncSession,
) -> None:
"""Test user response creation."""
# Ensure plan relationship is loaded
await test_session.refresh(test_user, ["plan"])
user_response = await auth_service.create_user_response(test_user)
assert user_response.id == test_user.id
assert user_response.email == test_user.email
assert user_response.name == test_user.name
assert user_response.role == test_user.role
assert user_response.credits == test_user.credits
assert user_response.is_active == test_user.is_active
assert user_response.plan["id"] == test_user.plan.id
assert user_response.plan["code"] == test_user.plan.code