Files
pixel/backend/tests/test_error_handling.py
张鹏 f9f4560459 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()
2026-04-29 01:20:12 +08:00

271 lines
9.2 KiB
Python

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