feat: comprehensive test infrastructure improvements
Phase 1: Fix immediate test failures - Fix path handling in files router (strip leading slashes) - Fix test assertions to match actual API behavior - Add proper error handling for missing files in download endpoint - Reload files router in test fixtures to pick up config changes - Fix upload endpoint test to use query params instead of form data Phase 2: Improve test reliability - Standardize error responses: docker_router now uses HTTPException - Add global exception handlers for validation, Docker errors, and unhandled exceptions - Fix flaky TOTP replay protection test - Fix flaky password hash loading test with proper module reload - Enhanced Docker mock fixtures with error scenarios and factory pattern Phase 3: Add coverage reporting - Add pytest-cov with 40% minimum coverage threshold - Add pyproject.toml with pytest and coverage configuration - Update test workflow to generate coverage and JUnit XML reports - Remove -x flag to see all test failures - Configure asyncio_default_fixture_loop_scope to fix deprecation warning Results: - All 144 tests passing (was 137 passed, 5 failed, 2 skipped) - Test execution time: ~3s locally - Coverage: 43% (above 40% threshold) - No skipped tests - Standardized error handling across all endpoints
This commit is contained in:
@@ -3,6 +3,7 @@ from fastapi.staticfiles import StaticFiles
|
||||
from fastapi.middleware.cors import CORSMiddleware
|
||||
from fastapi.middleware.gzip import GZipMiddleware
|
||||
from fastapi.responses import JSONResponse
|
||||
from fastapi.exceptions import RequestValidationError
|
||||
from slowapi import Limiter
|
||||
from slowapi.util import get_remote_address
|
||||
from slowapi.errors import RateLimitExceeded
|
||||
@@ -17,6 +18,8 @@ import httpx
|
||||
import logging
|
||||
import time
|
||||
import jwt
|
||||
import docker.errors
|
||||
import traceback as tb
|
||||
from datetime import datetime, timezone, timedelta
|
||||
from config import TELEGRAM_BOT_TOKEN, TELEGRAM_CHAT_ID, CORS_ORIGINS, SECRET_KEY, ALGORITHM, VOLUME_ROOT, get_client_ip
|
||||
|
||||
@@ -77,6 +80,47 @@ app.add_middleware(GZipMiddleware, minimum_size=1000)
|
||||
async def rate_limit_handler(request: Request, exc: RateLimitExceeded):
|
||||
return JSONResponse(status_code=429, content={"detail": "Too many requests, try again later"})
|
||||
|
||||
|
||||
@app.exception_handler(RequestValidationError)
|
||||
async def validation_exception_handler(request: Request, exc: RequestValidationError):
|
||||
"""Handle request validation errors with detailed information."""
|
||||
logging.error(f"Validation error on {request.method} {request.url.path}: {exc.errors()}")
|
||||
return JSONResponse(
|
||||
status_code=422,
|
||||
content={"detail": exc.errors()}
|
||||
)
|
||||
|
||||
|
||||
@app.exception_handler(docker.errors.NotFound)
|
||||
async def docker_not_found_handler(request: Request, exc: docker.errors.NotFound):
|
||||
"""Handle Docker container/image not found errors."""
|
||||
logging.warning(f"Docker resource not found: {exc}")
|
||||
return JSONResponse(
|
||||
status_code=404,
|
||||
content={"detail": "Docker resource not found"}
|
||||
)
|
||||
|
||||
|
||||
@app.exception_handler(docker.errors.APIError)
|
||||
async def docker_api_error_handler(request: Request, exc: docker.errors.APIError):
|
||||
"""Handle Docker API errors."""
|
||||
logging.error(f"Docker API error: {exc}")
|
||||
return JSONResponse(
|
||||
status_code=503,
|
||||
content={"detail": "Docker service unavailable"}
|
||||
)
|
||||
|
||||
|
||||
@app.exception_handler(Exception)
|
||||
async def generic_exception_handler(request: Request, exc: Exception):
|
||||
"""Handle all unhandled exceptions."""
|
||||
logging.error(f"Unhandled exception on {request.method} {request.url.path}: {exc}")
|
||||
logging.error(tb.format_exc())
|
||||
return JSONResponse(
|
||||
status_code=500,
|
||||
content={"detail": "Internal server error"}
|
||||
)
|
||||
|
||||
@app.middleware("http")
|
||||
async def security_headers(request: Request, call_next):
|
||||
response = await call_next(request)
|
||||
|
||||
Reference in New Issue
Block a user