- Updated ExtractionInfo to include user_id, created_at, and updated_at fields. - Modified ExtractionService to return user and timestamp information in extraction responses. - Enhanced sound serialization in PlayerState to include extraction URL if available. - Adjusted PlaylistRepository to load sound extractions when retrieving playlist sounds. - Added tests for new fields in extraction and sound endpoints, ensuring proper response structure. - Created new test file endpoints for sound downloads and thumbnail retrievals, including success and error cases. - Refactored various test cases for consistency and clarity, ensuring proper mocking and assertions.
388 lines
13 KiB
Python
388 lines
13 KiB
Python
"""Tests for API token endpoints."""
|
|
|
|
from datetime import UTC, datetime, timedelta
|
|
from unittest.mock import patch
|
|
|
|
import pytest
|
|
from httpx import AsyncClient
|
|
|
|
from app.models.user import User
|
|
|
|
|
|
class TestApiTokenEndpoints:
|
|
"""Test API token management endpoints."""
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_generate_api_token_success(
|
|
self,
|
|
authenticated_client: AsyncClient,
|
|
authenticated_user: User,
|
|
) -> None:
|
|
"""Test successful API token generation."""
|
|
request_data = {"expires_days": 30}
|
|
|
|
response = await authenticated_client.post(
|
|
"/api/v1/auth/api-token",
|
|
json=request_data,
|
|
)
|
|
|
|
assert response.status_code == 200
|
|
data = response.json()
|
|
|
|
assert "api_token" in data
|
|
assert "expires_at" in data
|
|
assert len(data["api_token"]) > 0
|
|
|
|
# Verify token format (should be URL-safe base64)
|
|
import base64
|
|
|
|
try:
|
|
base64.urlsafe_b64decode(data["api_token"] + "===") # Add padding
|
|
except Exception:
|
|
pytest.fail("API token should be valid URL-safe base64")
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_generate_api_token_default_expiry(
|
|
self,
|
|
authenticated_client: AsyncClient,
|
|
) -> None:
|
|
"""Test API token generation with default expiry."""
|
|
response = await authenticated_client.post("/api/v1/auth/api-token", json={})
|
|
|
|
assert response.status_code == 200
|
|
data = response.json()
|
|
|
|
expires_at_str = data["expires_at"]
|
|
# Handle both ISO format with/without timezone info
|
|
if (
|
|
expires_at_str.endswith("Z")
|
|
or "+" in expires_at_str
|
|
or expires_at_str.count("-") > 2
|
|
):
|
|
expires_at = datetime.fromisoformat(expires_at_str)
|
|
else:
|
|
# Naive datetime, assume UTC
|
|
expires_at = datetime.fromisoformat(expires_at_str).replace(tzinfo=UTC)
|
|
|
|
expected_expiry = datetime.now(UTC) + timedelta(days=365)
|
|
|
|
# Allow 1 minute tolerance
|
|
assert abs((expires_at - expected_expiry).total_seconds()) < 60
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_generate_api_token_custom_expiry(
|
|
self,
|
|
authenticated_client: AsyncClient,
|
|
) -> None:
|
|
"""Test API token generation with custom expiry."""
|
|
expires_days = 90
|
|
request_data = {"expires_days": expires_days}
|
|
|
|
response = await authenticated_client.post(
|
|
"/api/v1/auth/api-token",
|
|
json=request_data,
|
|
)
|
|
|
|
assert response.status_code == 200
|
|
data = response.json()
|
|
|
|
expires_at_str = data["expires_at"]
|
|
# Handle both ISO format with/without timezone info
|
|
if (
|
|
expires_at_str.endswith("Z")
|
|
or "+" in expires_at_str
|
|
or expires_at_str.count("-") > 2
|
|
):
|
|
expires_at = datetime.fromisoformat(expires_at_str)
|
|
else:
|
|
# Naive datetime, assume UTC
|
|
expires_at = datetime.fromisoformat(expires_at_str).replace(tzinfo=UTC)
|
|
|
|
expected_expiry = datetime.now(UTC) + timedelta(days=expires_days)
|
|
|
|
# Allow 1 minute tolerance
|
|
assert abs((expires_at - expected_expiry).total_seconds()) < 60
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_generate_api_token_validation_errors(
|
|
self,
|
|
authenticated_client: AsyncClient,
|
|
) -> None:
|
|
"""Test API token generation with validation errors."""
|
|
# Test minimum validation
|
|
response = await authenticated_client.post(
|
|
"/api/v1/auth/api-token",
|
|
json={"expires_days": 0},
|
|
)
|
|
assert response.status_code == 422
|
|
|
|
# Test maximum validation
|
|
response = await authenticated_client.post(
|
|
"/api/v1/auth/api-token",
|
|
json={"expires_days": 4000},
|
|
)
|
|
assert response.status_code == 422
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_generate_api_token_unauthenticated(
|
|
self,
|
|
client: AsyncClient,
|
|
) -> None:
|
|
"""Test API token generation without authentication."""
|
|
response = await client.post(
|
|
"/api/v1/auth/api-token",
|
|
json={"expires_days": 30},
|
|
)
|
|
assert response.status_code == 401
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_get_api_token_status_no_token(
|
|
self,
|
|
authenticated_client: AsyncClient,
|
|
) -> None:
|
|
"""Test getting API token status when user has no token."""
|
|
response = await authenticated_client.get("/api/v1/auth/api-token/status")
|
|
|
|
assert response.status_code == 200
|
|
data = response.json()
|
|
|
|
assert data["has_token"] is False
|
|
assert data["expires_at"] is None
|
|
assert data["is_expired"] is False
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_get_api_token_status_with_token(
|
|
self,
|
|
authenticated_client: AsyncClient,
|
|
) -> None:
|
|
"""Test getting API token status when user has a token."""
|
|
# First generate a token
|
|
await authenticated_client.post(
|
|
"/api/v1/auth/api-token",
|
|
json={"expires_days": 30},
|
|
)
|
|
|
|
# Then check status
|
|
response = await authenticated_client.get("/api/v1/auth/api-token/status")
|
|
|
|
assert response.status_code == 200
|
|
data = response.json()
|
|
|
|
assert data["has_token"] is True
|
|
assert data["expires_at"] is not None
|
|
assert data["is_expired"] is False
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_get_api_token_status_expired_token(
|
|
self,
|
|
authenticated_client: AsyncClient,
|
|
authenticated_user: User,
|
|
) -> None:
|
|
"""Test getting API token status with expired token."""
|
|
# Mock expired token
|
|
with patch("app.utils.auth.TokenUtils.is_token_expired", return_value=True):
|
|
# Set a token on the user
|
|
authenticated_user.api_token = "expired_token"
|
|
authenticated_user.api_token_expires_at = datetime.now(UTC) - timedelta(
|
|
days=1,
|
|
)
|
|
|
|
response = await authenticated_client.get("/api/v1/auth/api-token/status")
|
|
|
|
assert response.status_code == 200
|
|
data = response.json()
|
|
|
|
assert data["has_token"] is True
|
|
assert data["expires_at"] is not None
|
|
assert data["is_expired"] is True
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_get_api_token_status_unauthenticated(
|
|
self,
|
|
client: AsyncClient,
|
|
) -> None:
|
|
"""Test getting API token status without authentication."""
|
|
response = await client.get("/api/v1/auth/api-token/status")
|
|
assert response.status_code == 401
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_revoke_api_token_success(
|
|
self,
|
|
authenticated_client: AsyncClient,
|
|
) -> None:
|
|
"""Test successful API token revocation."""
|
|
# First generate a token
|
|
await authenticated_client.post(
|
|
"/api/v1/auth/api-token",
|
|
json={"expires_days": 30},
|
|
)
|
|
|
|
# Verify token exists
|
|
status_response = await authenticated_client.get(
|
|
"/api/v1/auth/api-token/status",
|
|
)
|
|
assert status_response.json()["has_token"] is True
|
|
|
|
# Revoke the token
|
|
response = await authenticated_client.delete("/api/v1/auth/api-token")
|
|
|
|
assert response.status_code == 200
|
|
data = response.json()
|
|
assert data["message"] == "API token revoked successfully"
|
|
|
|
# Verify token is gone
|
|
status_response = await authenticated_client.get(
|
|
"/api/v1/auth/api-token/status",
|
|
)
|
|
assert status_response.json()["has_token"] is False
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_revoke_api_token_no_token(
|
|
self,
|
|
authenticated_client: AsyncClient,
|
|
) -> None:
|
|
"""Test revoking API token when user has no token."""
|
|
response = await authenticated_client.delete("/api/v1/auth/api-token")
|
|
|
|
assert response.status_code == 200
|
|
data = response.json()
|
|
assert data["message"] == "API token revoked successfully"
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_revoke_api_token_unauthenticated(self, client: AsyncClient) -> None:
|
|
"""Test revoking API token without authentication."""
|
|
response = await client.delete("/api/v1/auth/api-token")
|
|
assert response.status_code == 401
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_api_token_authentication_success(
|
|
self,
|
|
client: AsyncClient,
|
|
authenticated_client: AsyncClient,
|
|
) -> None:
|
|
"""Test successful authentication using API token."""
|
|
# Generate API token
|
|
token_response = await authenticated_client.post(
|
|
"/api/v1/auth/api-token",
|
|
json={"expires_days": 30},
|
|
)
|
|
api_token = token_response.json()["api_token"]
|
|
|
|
# Use API token to authenticate
|
|
headers = {"API-TOKEN": api_token}
|
|
response = await client.get("/api/v1/auth/me", headers=headers)
|
|
|
|
assert response.status_code == 200
|
|
data = response.json()
|
|
assert "id" in data
|
|
assert "email" in data
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_api_token_authentication_invalid_token(
|
|
self,
|
|
client: AsyncClient,
|
|
) -> None:
|
|
"""Test authentication with invalid API token."""
|
|
headers = {"API-TOKEN": "invalid_token"}
|
|
response = await client.get("/api/v1/auth/me", headers=headers)
|
|
|
|
assert response.status_code == 401
|
|
data = response.json()
|
|
assert "Invalid API token" in data["detail"]
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_api_token_authentication_expired_token(
|
|
self,
|
|
client: AsyncClient,
|
|
authenticated_client: AsyncClient,
|
|
) -> None:
|
|
"""Test authentication with expired API token."""
|
|
# Generate API token
|
|
token_response = await authenticated_client.post(
|
|
"/api/v1/auth/api-token",
|
|
json={"expires_days": 30},
|
|
)
|
|
api_token = token_response.json()["api_token"]
|
|
|
|
# Mock expired token
|
|
with patch("app.utils.auth.TokenUtils.is_token_expired", return_value=True):
|
|
headers = {"API-TOKEN": api_token}
|
|
response = await client.get("/api/v1/auth/me", headers=headers)
|
|
|
|
assert response.status_code == 401
|
|
data = response.json()
|
|
assert "API token has expired" in data["detail"]
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_api_token_authentication_empty_token(
|
|
self,
|
|
client: AsyncClient,
|
|
) -> None:
|
|
"""Test authentication with empty API-TOKEN header."""
|
|
# Empty token
|
|
headers = {"API-TOKEN": ""}
|
|
response = await client.get("/api/v1/auth/me", headers=headers)
|
|
|
|
assert response.status_code == 401
|
|
data = response.json()
|
|
assert "Could not validate credentials" in data["detail"]
|
|
|
|
# Whitespace only token
|
|
headers = {"API-TOKEN": " "}
|
|
response = await client.get("/api/v1/auth/me", headers=headers)
|
|
|
|
assert response.status_code == 401
|
|
data = response.json()
|
|
assert "API token required" in data["detail"]
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_api_token_authentication_inactive_user(
|
|
self,
|
|
client: AsyncClient,
|
|
authenticated_client: AsyncClient,
|
|
authenticated_user: User,
|
|
) -> None:
|
|
"""Test authentication with API token for inactive user."""
|
|
# Generate API token
|
|
token_response = await authenticated_client.post(
|
|
"/api/v1/auth/api-token",
|
|
json={"expires_days": 30},
|
|
)
|
|
api_token = token_response.json()["api_token"]
|
|
|
|
# Deactivate user
|
|
authenticated_user.is_active = False
|
|
|
|
# Try to authenticate with API token
|
|
headers = {"API-TOKEN": api_token}
|
|
response = await client.get("/api/v1/auth/me", headers=headers)
|
|
|
|
assert response.status_code == 401
|
|
data = response.json()
|
|
assert "Account is deactivated" in data["detail"]
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_flexible_authentication_prefers_api_token(
|
|
self,
|
|
client: AsyncClient,
|
|
authenticated_client: AsyncClient,
|
|
auth_cookies: dict[str, str],
|
|
) -> None:
|
|
"""Test that flexible authentication prefers API token over cookie."""
|
|
# Generate API token
|
|
token_response = await authenticated_client.post(
|
|
"/api/v1/auth/api-token",
|
|
json={"expires_days": 30},
|
|
)
|
|
api_token = token_response.json()["api_token"]
|
|
|
|
# Set both cookies and API-TOKEN header
|
|
client.cookies.update(auth_cookies)
|
|
headers = {"API-TOKEN": api_token}
|
|
|
|
# This should use API token authentication
|
|
response = await client.get("/api/v1/auth/me", headers=headers)
|
|
|
|
assert response.status_code == 200
|
|
# If it used API token auth, it should work even if cookies are invalid
|