Initial commit: Pixel AI comic/video creation platform
- FastAPI backend with SQLModel, Alembic migrations, AgentScope agents - Next.js 15 frontend with React 19, Tailwind, Zustand, React Flow - Multi-provider AI system (DashScope, Kling, MiniMax, Volcengine, OpenAI, etc.) - All HTTP clients migrated from sync requests to async httpx - Admin-managed API keys via environment variables - SSRF vulnerability fixed in ensure_url()
This commit is contained in:
270
backend/tests/test_error_handling.py
Normal file
270
backend/tests/test_error_handling.py
Normal file
@@ -0,0 +1,270 @@
|
||||
"""
|
||||
Tests for unified error handling system
|
||||
|
||||
Tests the exception hierarchy, error handler middleware, and error response format.
|
||||
"""
|
||||
import pytest
|
||||
from datetime import datetime
|
||||
from src.utils.errors import (
|
||||
AppException,
|
||||
BusinessException,
|
||||
SystemException,
|
||||
ErrorCode,
|
||||
InvalidParameterException,
|
||||
ResourceNotFoundException,
|
||||
ProjectNotFoundException,
|
||||
TaskNotFoundException,
|
||||
TaskTimeoutException,
|
||||
TaskQueueFullException,
|
||||
ModelNotFoundException,
|
||||
GenerationFailedException,
|
||||
StorageException,
|
||||
RateLimitExceededException,
|
||||
UnauthorizedException,
|
||||
ForbiddenException,
|
||||
ConflictException
|
||||
)
|
||||
|
||||
|
||||
class TestExceptionHierarchy:
|
||||
"""Test exception class hierarchy and initialization"""
|
||||
|
||||
def test_app_exception_base(self):
|
||||
"""Test AppException base class"""
|
||||
exc = AppException(
|
||||
code=ErrorCode.UNKNOWN_ERROR,
|
||||
message="Test error",
|
||||
details={"key": "value"},
|
||||
status_code=500
|
||||
)
|
||||
|
||||
assert exc.code == ErrorCode.UNKNOWN_ERROR
|
||||
assert exc.message == "Test error"
|
||||
assert exc.details == {"key": "value"}
|
||||
assert exc.status_code == 500
|
||||
|
||||
# Test to_dict conversion
|
||||
exc_dict = exc.to_dict()
|
||||
assert exc_dict["code"] == "1000"
|
||||
assert exc_dict["message"] == "Test error"
|
||||
assert exc_dict["details"] == {"key": "value"}
|
||||
|
||||
def test_business_exception(self):
|
||||
"""Test BusinessException defaults to 400 status"""
|
||||
exc = BusinessException(
|
||||
code=ErrorCode.INVALID_PARAMETER,
|
||||
message="Invalid input"
|
||||
)
|
||||
|
||||
assert exc.status_code == 400
|
||||
assert isinstance(exc, AppException)
|
||||
|
||||
def test_system_exception(self):
|
||||
"""Test SystemException defaults to 500 status"""
|
||||
exc = SystemException(
|
||||
code=ErrorCode.UNKNOWN_ERROR,
|
||||
message="System failure"
|
||||
)
|
||||
|
||||
assert exc.status_code == 500
|
||||
assert isinstance(exc, AppException)
|
||||
|
||||
|
||||
class TestBusinessExceptions:
|
||||
"""Test specific business exception classes"""
|
||||
|
||||
def test_invalid_parameter_exception(self):
|
||||
"""Test InvalidParameterException"""
|
||||
exc = InvalidParameterException(field="email", reason="Invalid format")
|
||||
|
||||
assert exc.code == ErrorCode.INVALID_PARAMETER
|
||||
assert "email" in exc.message
|
||||
assert exc.details["field"] == "email"
|
||||
assert exc.details["reason"] == "Invalid format"
|
||||
assert exc.status_code == 400
|
||||
|
||||
def test_resource_not_found_exception(self):
|
||||
"""Test ResourceNotFoundException"""
|
||||
exc = ResourceNotFoundException(resource_type="User", resource_id="123")
|
||||
|
||||
assert exc.code == ErrorCode.NOT_FOUND
|
||||
assert "User" in exc.message
|
||||
assert exc.details["resource_type"] == "User"
|
||||
assert exc.details["resource_id"] == "123"
|
||||
|
||||
def test_project_not_found_exception(self):
|
||||
"""Test ProjectNotFoundException"""
|
||||
exc = ProjectNotFoundException(project_id="proj_123")
|
||||
|
||||
assert exc.code == ErrorCode.PROJECT_NOT_FOUND
|
||||
assert exc.details["project_id"] == "proj_123"
|
||||
assert isinstance(exc, BusinessException)
|
||||
|
||||
def test_task_not_found_exception(self):
|
||||
"""Test TaskNotFoundException"""
|
||||
exc = TaskNotFoundException(task_id="task_123")
|
||||
|
||||
assert exc.code == ErrorCode.TASK_NOT_FOUND
|
||||
assert exc.details["task_id"] == "task_123"
|
||||
|
||||
def test_model_not_found_exception(self):
|
||||
"""Test ModelNotFoundException"""
|
||||
exc = ModelNotFoundException(model_id="flux-pro")
|
||||
|
||||
assert exc.code == ErrorCode.MODEL_NOT_FOUND
|
||||
assert exc.details["model_id"] == "flux-pro"
|
||||
|
||||
def test_rate_limit_exceeded_exception(self):
|
||||
"""Test RateLimitExceededException"""
|
||||
exc = RateLimitExceededException(limit=100, window=60)
|
||||
|
||||
assert exc.code == ErrorCode.RATE_LIMIT_EXCEEDED
|
||||
assert exc.status_code == 429
|
||||
assert exc.details["limit"] == 100
|
||||
assert exc.details["window_seconds"] == 60
|
||||
|
||||
def test_unauthorized_exception(self):
|
||||
"""Test UnauthorizedException"""
|
||||
exc = UnauthorizedException(reason="Invalid token")
|
||||
|
||||
assert exc.code == ErrorCode.UNAUTHORIZED
|
||||
assert exc.status_code == 401
|
||||
assert exc.details["reason"] == "Invalid token"
|
||||
|
||||
def test_forbidden_exception(self):
|
||||
"""Test ForbiddenException"""
|
||||
exc = ForbiddenException(reason="Insufficient permissions")
|
||||
|
||||
assert exc.code == ErrorCode.FORBIDDEN
|
||||
assert exc.status_code == 403
|
||||
|
||||
def test_conflict_exception(self):
|
||||
"""Test ConflictException"""
|
||||
exc = ConflictException(resource_type="Project", reason="Name already exists")
|
||||
|
||||
assert exc.code == ErrorCode.CONFLICT
|
||||
assert exc.status_code == 409
|
||||
|
||||
|
||||
class TestSystemExceptions:
|
||||
"""Test specific system exception classes"""
|
||||
|
||||
def test_task_timeout_exception(self):
|
||||
"""Test TaskTimeoutException"""
|
||||
exc = TaskTimeoutException(task_id="task_123", timeout=300)
|
||||
|
||||
assert exc.code == ErrorCode.TASK_TIMEOUT
|
||||
assert exc.status_code == 500
|
||||
assert exc.details["task_id"] == "task_123"
|
||||
assert exc.details["timeout_seconds"] == 300
|
||||
assert isinstance(exc, SystemException)
|
||||
|
||||
def test_task_queue_full_exception(self):
|
||||
"""Test TaskQueueFullException"""
|
||||
exc = TaskQueueFullException(queue_size=1000)
|
||||
|
||||
assert exc.code == ErrorCode.TASK_QUEUE_FULL
|
||||
assert exc.status_code == 500
|
||||
assert exc.details["queue_size"] == 1000
|
||||
|
||||
def test_generation_failed_exception(self):
|
||||
"""Test GenerationFailedException"""
|
||||
exc = GenerationFailedException(reason="API error", provider="dashscope")
|
||||
|
||||
assert exc.code == ErrorCode.GENERATION_FAILED
|
||||
assert exc.status_code == 500
|
||||
assert exc.details["reason"] == "API error"
|
||||
assert exc.details["provider"] == "dashscope"
|
||||
|
||||
def test_storage_exception(self):
|
||||
"""Test StorageException"""
|
||||
exc = StorageException(operation="upload", reason="Disk full")
|
||||
|
||||
assert exc.code == ErrorCode.STORAGE_ERROR
|
||||
assert exc.status_code == 500
|
||||
assert exc.details["operation"] == "upload"
|
||||
assert exc.details["reason"] == "Disk full"
|
||||
|
||||
|
||||
class TestErrorCodes:
|
||||
"""Test error code enumeration"""
|
||||
|
||||
def test_error_code_values(self):
|
||||
"""Test error code values follow the format"""
|
||||
# Success
|
||||
assert ErrorCode.SUCCESS.value == "0000"
|
||||
|
||||
# General errors (1xxx)
|
||||
assert ErrorCode.UNKNOWN_ERROR.value == "1000"
|
||||
assert ErrorCode.INVALID_PARAMETER.value == "1001"
|
||||
assert ErrorCode.NOT_FOUND.value == "1004"
|
||||
|
||||
# Business errors (2xxx)
|
||||
assert ErrorCode.PROJECT_NOT_FOUND.value == "2001"
|
||||
assert ErrorCode.ASSET_NOT_FOUND.value == "2011"
|
||||
|
||||
# Task errors (3xxx)
|
||||
assert ErrorCode.TASK_NOT_FOUND.value == "3002"
|
||||
assert ErrorCode.TASK_TIMEOUT.value == "3003"
|
||||
|
||||
# AI service errors (4xxx)
|
||||
assert ErrorCode.MODEL_NOT_FOUND.value == "4001"
|
||||
assert ErrorCode.GENERATION_FAILED.value == "4003"
|
||||
|
||||
# Storage errors (5xxx)
|
||||
assert ErrorCode.STORAGE_ERROR.value == "5001"
|
||||
assert ErrorCode.FILE_NOT_FOUND.value == "5002"
|
||||
|
||||
def test_error_code_categories(self):
|
||||
"""Test error codes are properly categorized"""
|
||||
# All general errors start with 1
|
||||
assert ErrorCode.UNKNOWN_ERROR.value.startswith("1")
|
||||
assert ErrorCode.INVALID_PARAMETER.value.startswith("1")
|
||||
|
||||
# All business errors start with 2
|
||||
assert ErrorCode.PROJECT_NOT_FOUND.value.startswith("2")
|
||||
|
||||
# All task errors start with 3
|
||||
assert ErrorCode.TASK_TIMEOUT.value.startswith("3")
|
||||
|
||||
# All AI service errors start with 4
|
||||
assert ErrorCode.MODEL_NOT_FOUND.value.startswith("4")
|
||||
|
||||
# All storage errors start with 5
|
||||
assert ErrorCode.STORAGE_ERROR.value.startswith("5")
|
||||
|
||||
|
||||
class TestExceptionToDictConversion:
|
||||
"""Test exception to dictionary conversion for API responses"""
|
||||
|
||||
def test_simple_exception_to_dict(self):
|
||||
"""Test basic exception to dict conversion"""
|
||||
exc = ProjectNotFoundException(project_id="proj_123")
|
||||
exc_dict = exc.to_dict()
|
||||
|
||||
assert "code" in exc_dict
|
||||
assert "message" in exc_dict
|
||||
assert "details" in exc_dict
|
||||
assert exc_dict["code"] == "2001"
|
||||
assert exc_dict["details"]["project_id"] == "proj_123"
|
||||
|
||||
def test_exception_with_complex_details(self):
|
||||
"""Test exception with nested details"""
|
||||
exc = AppException(
|
||||
code=ErrorCode.INVALID_PARAMETER,
|
||||
message="Validation failed",
|
||||
details={
|
||||
"errors": [
|
||||
{"field": "email", "message": "Invalid format"},
|
||||
{"field": "age", "message": "Must be positive"}
|
||||
]
|
||||
}
|
||||
)
|
||||
|
||||
exc_dict = exc.to_dict()
|
||||
assert len(exc_dict["details"]["errors"]) == 2
|
||||
assert exc_dict["details"]["errors"][0]["field"] == "email"
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
pytest.main([__file__, "-v"])
|
||||
Reference in New Issue
Block a user