Refactor tests for improved consistency and readability

- Updated test cases in `test_auth_endpoints.py` to ensure consistent formatting and style.
- Enhanced `test_socket_endpoints.py` with consistent parameter formatting and improved readability.
- Cleaned up `conftest.py` by ensuring consistent parameter formatting in fixtures.
- Added comprehensive tests for API token dependencies in `test_api_token_dependencies.py`.
- Refactored `test_auth_service.py` to maintain consistent parameter formatting.
- Cleaned up `test_oauth_service.py` by removing unnecessary imports.
- Improved `test_socket_service.py` with consistent formatting and readability.
- Enhanced `test_cookies.py` by ensuring consistent formatting and readability.
- Introduced new tests for token utilities in `test_token_utils.py` to validate token generation and expiration logic.
This commit is contained in:
JSC
2025-07-27 15:11:47 +02:00
parent 42deab2409
commit 3dc21337f9
16 changed files with 991 additions and 159 deletions

View File

@@ -0,0 +1,343 @@
"""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,
):
"""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,
):
"""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"):
expires_at = datetime.fromisoformat(expires_at_str.replace("Z", "+00:00"))
elif "+" 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,
):
"""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"):
expires_at = datetime.fromisoformat(expires_at_str.replace("Z", "+00:00"))
elif "+" 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,
):
"""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):
"""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,
):
"""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,
):
"""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,
):
"""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):
"""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,
):
"""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,
):
"""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):
"""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,
):
"""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 = {"Authorization": f"Bearer {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):
"""Test authentication with invalid API token."""
headers = {"Authorization": "Bearer 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,
):
"""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 = {"Authorization": f"Bearer {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_malformed_header(self, client: AsyncClient):
"""Test authentication with malformed Authorization header."""
# Missing Bearer prefix
headers = {"Authorization": "invalid_format"}
response = await client.get("/api/v1/auth/me", headers=headers)
assert response.status_code == 401
data = response.json()
assert "Invalid authorization header format" in data["detail"]
# Empty token
headers = {"Authorization": "Bearer "}
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,
):
"""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 = {"Authorization": f"Bearer {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],
):
"""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 Authorization header
client.cookies.update(auth_cookies)
headers = {"Authorization": f"Bearer {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

View File

@@ -73,7 +73,7 @@ class TestAuthEndpoints:
@pytest.mark.asyncio
async def test_register_duplicate_email(
self, test_client: AsyncClient, test_user: User
self, test_client: AsyncClient, test_user: User,
) -> None:
"""Test registration with duplicate email."""
user_data = {
@@ -118,7 +118,7 @@ class TestAuthEndpoints:
async def test_register_missing_fields(self, test_client: AsyncClient) -> None:
"""Test registration with missing fields."""
user_data = {
"email": "test@example.com"
"email": "test@example.com",
# Missing password and name
}
@@ -128,7 +128,7 @@ class TestAuthEndpoints:
@pytest.mark.asyncio
async def test_login_success(
self, test_client: AsyncClient, test_user: User, test_login_data: dict[str, str]
self, test_client: AsyncClient, test_user: User, test_login_data: dict[str, str],
) -> None:
"""Test successful user login."""
response = await test_client.post("/api/v1/auth/login", json=test_login_data)
@@ -161,7 +161,7 @@ class TestAuthEndpoints:
@pytest.mark.asyncio
async def test_login_invalid_password(
self, test_client: AsyncClient, test_user: User
self, test_client: AsyncClient, test_user: User,
) -> None:
"""Test login with invalid password."""
login_data = {"email": test_user.email, "password": "wrongpassword"}
@@ -183,7 +183,7 @@ class TestAuthEndpoints:
@pytest.mark.asyncio
async def test_get_current_user_success(
self, test_client: AsyncClient, test_user: User, auth_cookies: dict[str, str]
self, test_client: AsyncClient, test_user: User, auth_cookies: dict[str, str],
) -> None:
"""Test getting current user info successfully."""
# Set cookies on client instance to avoid deprecation warning
@@ -210,7 +210,7 @@ class TestAuthEndpoints:
@pytest.mark.asyncio
async def test_get_current_user_invalid_token(
self, test_client: AsyncClient
self, test_client: AsyncClient,
) -> None:
"""Test getting current user with invalid token."""
# Set invalid cookies on client instance
@@ -223,7 +223,7 @@ class TestAuthEndpoints:
@pytest.mark.asyncio
async def test_get_current_user_expired_token(
self, test_client: AsyncClient, test_user: User
self, test_client: AsyncClient, test_user: User,
) -> None:
"""Test getting current user with expired token."""
from datetime import timedelta
@@ -237,7 +237,7 @@ class TestAuthEndpoints:
"role": "user",
}
expired_token = JWTUtils.create_access_token(
token_data, expires_delta=timedelta(seconds=-1)
token_data, expires_delta=timedelta(seconds=-1),
)
# Set expired cookies on client instance
@@ -262,7 +262,7 @@ class TestAuthEndpoints:
@pytest.mark.asyncio
async def test_admin_access_with_user_role(
self, test_client: AsyncClient, auth_cookies: dict[str, str]
self, test_client: AsyncClient, auth_cookies: dict[str, str],
) -> None:
"""Test that regular users cannot access admin endpoints."""
# This test would be for admin-only endpoints when they're created
@@ -293,7 +293,7 @@ class TestAuthEndpoints:
@pytest.mark.asyncio
async def test_admin_access_with_admin_role(
self, test_client: AsyncClient, admin_cookies: dict[str, str]
self, test_client: AsyncClient, admin_cookies: dict[str, str],
) -> None:
"""Test that admin users can access admin endpoints."""
from app.core.dependencies import get_admin_user
@@ -357,7 +357,7 @@ class TestAuthEndpoints:
@pytest.mark.asyncio
async def test_oauth_authorize_invalid_provider(
self, test_client: AsyncClient
self, test_client: AsyncClient,
) -> None:
"""Test OAuth authorization with invalid provider."""
response = await test_client.get("/api/v1/auth/invalid/authorize")
@@ -368,7 +368,7 @@ class TestAuthEndpoints:
@pytest.mark.asyncio
async def test_oauth_callback_new_user(
self, test_client: AsyncClient, ensure_plans: tuple[Any, Any]
self, test_client: AsyncClient, ensure_plans: tuple[Any, Any],
) -> None:
"""Test OAuth callback for new user creation."""
# Mock OAuth user info
@@ -400,7 +400,7 @@ class TestAuthEndpoints:
@pytest.mark.asyncio
async def test_oauth_callback_existing_user_link(
self, test_client: AsyncClient, test_user: Any, ensure_plans: tuple[Any, Any]
self, test_client: AsyncClient, test_user: Any, ensure_plans: tuple[Any, Any],
) -> None:
"""Test OAuth callback for linking to existing user."""
# Mock OAuth user info with same email as test user
@@ -442,7 +442,7 @@ class TestAuthEndpoints:
@pytest.mark.asyncio
async def test_oauth_callback_invalid_provider(
self, test_client: AsyncClient
self, test_client: AsyncClient,
) -> None:
"""Test OAuth callback with invalid provider."""
response = await test_client.get(

View File

@@ -1,8 +1,9 @@
"""Tests for socket API endpoints."""
from unittest.mock import AsyncMock, patch
import pytest
from httpx import AsyncClient
from unittest.mock import AsyncMock, patch
from app.models.user import User
@@ -10,7 +11,7 @@ from app.models.user import User
@pytest.fixture
def mock_socket_manager():
"""Mock socket manager for testing."""
with patch('app.api.v1.socket.socket_manager') as mock:
with patch("app.api.v1.socket.socket_manager") as mock:
mock.get_connected_users.return_value = ["1", "2", "3"]
mock.send_to_user = AsyncMock(return_value=True)
mock.broadcast_to_all = AsyncMock()
@@ -24,10 +25,10 @@ class TestSocketEndpoints:
async def test_get_socket_status_authenticated(self, authenticated_client: AsyncClient, authenticated_user: User, mock_socket_manager):
"""Test getting socket status for authenticated user."""
response = await authenticated_client.get("/api/v1/socket/status")
assert response.status_code == 200
data = response.json()
assert "connected" in data
assert "user_id" in data
assert "total_connected" in data
@@ -46,19 +47,19 @@ class TestSocketEndpoints:
"""Test sending message to specific user successfully."""
target_user_id = 2
message = "Hello there!"
response = await authenticated_client.post(
"/api/v1/socket/send-message",
params={"target_user_id": target_user_id, "message": message}
params={"target_user_id": target_user_id, "message": message},
)
assert response.status_code == 200
data = response.json()
assert data["success"] is True
assert data["target_user_id"] == target_user_id
assert data["message"] == "Message sent"
# Verify socket manager was called correctly
mock_socket_manager.send_to_user.assert_called_once_with(
str(target_user_id),
@@ -67,7 +68,7 @@ class TestSocketEndpoints:
"from_user_id": authenticated_user.id,
"from_user_name": authenticated_user.name,
"message": message,
}
},
)
@pytest.mark.asyncio
@@ -75,18 +76,18 @@ class TestSocketEndpoints:
"""Test sending message to user who is not connected."""
target_user_id = 999
message = "Hello there!"
# Mock user not connected
mock_socket_manager.send_to_user.return_value = False
response = await authenticated_client.post(
"/api/v1/socket/send-message",
params={"target_user_id": target_user_id, "message": message}
params={"target_user_id": target_user_id, "message": message},
)
assert response.status_code == 200
data = response.json()
assert data["success"] is False
assert data["target_user_id"] == target_user_id
assert data["message"] == "User not connected"
@@ -96,7 +97,7 @@ class TestSocketEndpoints:
"""Test sending message without authentication."""
response = await client.post(
"/api/v1/socket/send-message",
params={"target_user_id": 1, "message": "test"}
params={"target_user_id": 1, "message": "test"},
)
assert response.status_code == 401
@@ -104,18 +105,18 @@ class TestSocketEndpoints:
async def test_broadcast_message_success(self, authenticated_client: AsyncClient, authenticated_user: User, mock_socket_manager):
"""Test broadcasting message to all users successfully."""
message = "Important announcement!"
response = await authenticated_client.post(
"/api/v1/socket/broadcast",
params={"message": message}
params={"message": message},
)
assert response.status_code == 200
data = response.json()
assert data["success"] is True
assert data["message"] == "Message broadcasted to all users"
# Verify socket manager was called correctly
mock_socket_manager.broadcast_to_all.assert_called_once_with(
"broadcast_message",
@@ -123,7 +124,7 @@ class TestSocketEndpoints:
"from_user_id": authenticated_user.id,
"from_user_name": authenticated_user.name,
"message": message,
}
},
)
@pytest.mark.asyncio
@@ -131,7 +132,7 @@ class TestSocketEndpoints:
"""Test broadcasting message without authentication."""
response = await client.post(
"/api/v1/socket/broadcast",
params={"message": "test"}
params={"message": "test"},
)
assert response.status_code == 401
@@ -141,14 +142,14 @@ class TestSocketEndpoints:
# Missing target_user_id
response = await authenticated_client.post(
"/api/v1/socket/send-message",
params={"message": "test"}
params={"message": "test"},
)
assert response.status_code == 422
# Missing message
response = await authenticated_client.post(
"/api/v1/socket/send-message",
params={"target_user_id": 1}
params={"target_user_id": 1},
)
assert response.status_code == 422
@@ -163,7 +164,7 @@ class TestSocketEndpoints:
"""Test sending message with invalid user ID."""
response = await authenticated_client.post(
"/api/v1/socket/send-message",
params={"target_user_id": "invalid", "message": "test"}
params={"target_user_id": "invalid", "message": "test"},
)
assert response.status_code == 422
@@ -172,14 +173,14 @@ class TestSocketEndpoints:
"""Test that socket status correctly shows if user is connected."""
# Test when user is connected
mock_socket_manager.get_connected_users.return_value = [str(authenticated_user.id), "2", "3"]
response = await authenticated_client.get("/api/v1/socket/status")
data = response.json()
assert data["connected"] is True
# Test when user is not connected
mock_socket_manager.get_connected_users.return_value = ["2", "3", "4"]
response = await authenticated_client.get("/api/v1/socket/status")
data = response.json()
assert data["connected"] is False
assert data["connected"] is False