235 lines
8.1 KiB
Python
235 lines
8.1 KiB
Python
"""Tests for API token authentication dependencies."""
|
|
|
|
from datetime import UTC, datetime, timedelta
|
|
from unittest.mock import AsyncMock
|
|
|
|
import pytest
|
|
from fastapi import HTTPException
|
|
|
|
from app.core.dependencies import get_current_user_api_token, get_current_user_flexible
|
|
from app.models.user import User
|
|
from app.services.auth import AuthService
|
|
|
|
# Constants
|
|
HTTP_401_UNAUTHORIZED = 401
|
|
|
|
|
|
class TestApiTokenDependencies:
|
|
"""Test API token authentication dependencies."""
|
|
|
|
@pytest.fixture
|
|
def mock_auth_service(self) -> AsyncMock:
|
|
"""Create a mock auth service."""
|
|
return AsyncMock(spec=AuthService)
|
|
|
|
@pytest.fixture
|
|
def test_user(self) -> User:
|
|
"""Create a test user."""
|
|
return User(
|
|
id=1,
|
|
email="test@example.com",
|
|
name="Test User",
|
|
role="user",
|
|
is_active=True,
|
|
plan_id=1,
|
|
credits=100,
|
|
api_token="test_api_token_123",
|
|
api_token_expires_at=datetime.now(UTC) + timedelta(days=30),
|
|
)
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_get_current_user_api_token_success(
|
|
self,
|
|
mock_auth_service: AsyncMock,
|
|
test_user: User,
|
|
) -> None:
|
|
"""Test successful API token authentication."""
|
|
mock_auth_service.get_user_by_api_token.return_value = test_user
|
|
|
|
api_token_header = "test_api_token_123"
|
|
|
|
result = await get_current_user_api_token(mock_auth_service, api_token_header)
|
|
|
|
assert result == test_user
|
|
mock_auth_service.get_user_by_api_token.assert_called_once_with(
|
|
"test_api_token_123",
|
|
)
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_get_current_user_api_token_no_header(
|
|
self,
|
|
mock_auth_service: AsyncMock,
|
|
) -> None:
|
|
"""Test API token authentication without API-TOKEN header."""
|
|
with pytest.raises(HTTPException) as exc_info:
|
|
await get_current_user_api_token(mock_auth_service, None)
|
|
|
|
assert exc_info.value.status_code == HTTP_401_UNAUTHORIZED
|
|
assert "API-TOKEN header required" in exc_info.value.detail
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_get_current_user_api_token_empty_token(
|
|
self,
|
|
mock_auth_service: AsyncMock,
|
|
) -> None:
|
|
"""Test API token authentication with empty token."""
|
|
api_token_header = " "
|
|
|
|
with pytest.raises(HTTPException) as exc_info:
|
|
await get_current_user_api_token(mock_auth_service, api_token_header)
|
|
|
|
assert exc_info.value.status_code == HTTP_401_UNAUTHORIZED
|
|
assert "API token required" in exc_info.value.detail
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_get_current_user_api_token_whitespace_token(
|
|
self,
|
|
mock_auth_service: AsyncMock,
|
|
) -> None:
|
|
"""Test API token authentication with whitespace-only token."""
|
|
api_token_header = " "
|
|
|
|
with pytest.raises(HTTPException) as exc_info:
|
|
await get_current_user_api_token(mock_auth_service, api_token_header)
|
|
|
|
assert exc_info.value.status_code == HTTP_401_UNAUTHORIZED
|
|
assert "API token required" in exc_info.value.detail
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_get_current_user_api_token_invalid_token(
|
|
self,
|
|
mock_auth_service: AsyncMock,
|
|
) -> None:
|
|
"""Test API token authentication with invalid token."""
|
|
mock_auth_service.get_user_by_api_token.return_value = None
|
|
|
|
api_token_header = "invalid_token"
|
|
|
|
with pytest.raises(HTTPException) as exc_info:
|
|
await get_current_user_api_token(mock_auth_service, api_token_header)
|
|
|
|
assert exc_info.value.status_code == HTTP_401_UNAUTHORIZED
|
|
assert "Invalid API token" in exc_info.value.detail
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_get_current_user_api_token_expired_token(
|
|
self,
|
|
mock_auth_service: AsyncMock,
|
|
test_user: User,
|
|
) -> None:
|
|
"""Test API token authentication with expired token."""
|
|
# Set expired token
|
|
test_user.api_token_expires_at = datetime.now(UTC) - timedelta(days=1)
|
|
mock_auth_service.get_user_by_api_token.return_value = test_user
|
|
|
|
api_token_header = "expired_token"
|
|
|
|
with pytest.raises(HTTPException) as exc_info:
|
|
await get_current_user_api_token(mock_auth_service, api_token_header)
|
|
|
|
assert exc_info.value.status_code == HTTP_401_UNAUTHORIZED
|
|
assert "API token has expired" in exc_info.value.detail
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_get_current_user_api_token_inactive_user(
|
|
self,
|
|
mock_auth_service: AsyncMock,
|
|
test_user: User,
|
|
) -> None:
|
|
"""Test API token authentication with inactive user."""
|
|
test_user.is_active = False
|
|
mock_auth_service.get_user_by_api_token.return_value = test_user
|
|
|
|
api_token_header = "test_token"
|
|
|
|
with pytest.raises(HTTPException) as exc_info:
|
|
await get_current_user_api_token(mock_auth_service, api_token_header)
|
|
|
|
assert exc_info.value.status_code == HTTP_401_UNAUTHORIZED
|
|
assert "Account is deactivated" in exc_info.value.detail
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_get_current_user_api_token_service_exception(
|
|
self,
|
|
mock_auth_service: AsyncMock,
|
|
) -> None:
|
|
"""Test API token authentication with service exception."""
|
|
mock_auth_service.get_user_by_api_token.side_effect = Exception(
|
|
"Database error",
|
|
)
|
|
|
|
api_token_header = "test_token"
|
|
|
|
with pytest.raises(HTTPException) as exc_info:
|
|
await get_current_user_api_token(mock_auth_service, api_token_header)
|
|
|
|
assert exc_info.value.status_code == HTTP_401_UNAUTHORIZED
|
|
assert "Could not validate API token" in exc_info.value.detail
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_get_current_user_flexible_uses_api_token(
|
|
self,
|
|
mock_auth_service: AsyncMock,
|
|
test_user: User,
|
|
) -> None:
|
|
"""Test flexible authentication uses API token when available."""
|
|
mock_auth_service.get_user_by_api_token.return_value = test_user
|
|
|
|
api_token_header = "test_api_token_123"
|
|
access_token = "jwt_token"
|
|
|
|
result = await get_current_user_flexible(
|
|
mock_auth_service,
|
|
access_token,
|
|
api_token_header,
|
|
)
|
|
|
|
assert result == test_user
|
|
mock_auth_service.get_user_by_api_token.assert_called_once_with(
|
|
"test_api_token_123",
|
|
)
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_get_current_user_flexible_falls_back_to_jwt(
|
|
self,
|
|
mock_auth_service: AsyncMock,
|
|
) -> None:
|
|
"""Test flexible authentication falls back to JWT when no API token."""
|
|
# Mock the get_current_user function (normally imported)
|
|
with pytest.raises(Exception, match=r"Database error|Could not validate"):
|
|
# This will fail because we can't easily mock the get_current_user import
|
|
# In a real test, you'd mock the import or use dependency injection
|
|
await get_current_user_flexible(mock_auth_service, "jwt_token", None)
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_api_token_no_expiry_never_expires(
|
|
self,
|
|
mock_auth_service: AsyncMock,
|
|
test_user: User,
|
|
) -> None:
|
|
"""Test API token with no expiry date never expires."""
|
|
test_user.api_token_expires_at = None
|
|
mock_auth_service.get_user_by_api_token.return_value = test_user
|
|
|
|
api_token_header = "test_token"
|
|
|
|
result = await get_current_user_api_token(mock_auth_service, api_token_header)
|
|
|
|
assert result == test_user
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_api_token_with_whitespace(
|
|
self,
|
|
mock_auth_service: AsyncMock,
|
|
test_user: User,
|
|
) -> None:
|
|
"""Test API token with leading/trailing whitespace is handled correctly."""
|
|
mock_auth_service.get_user_by_api_token.return_value = test_user
|
|
|
|
api_token_header = " test_token "
|
|
|
|
result = await get_current_user_api_token(mock_auth_service, api_token_header)
|
|
|
|
assert result == test_user
|
|
mock_auth_service.get_user_by_api_token.assert_called_once_with("test_token")
|