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:
张鹏
2026-04-29 01:20:12 +08:00
commit f9f4560459
808 changed files with 151724 additions and 0 deletions

View 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"])