Mechanical only, and separated from the judgment calls that follow so the reviewable changes are not buried in a 98-file whitespace diff. 227 automatic fixes: 60 blank lines carrying whitespace, 60 unsorted import blocks, 34 Optional[X] to X | None, 28 unused imports, 16 deprecated typing imports, 12 datetime.timezone.utc to datetime.UTC, and assorted smaller modernisations. Then `ruff format` over src and tests: 98 files reformatted, 35 already conforming. No file among the unused-import findings defines __all__ or is an __init__.py, so nothing here removes a re-export. `make test`: 658 passed, unchanged from HEAD. Two things observed while verifying, neither addressed here: `pytest tests/` cannot collect — tests/e2e/test_orchestration_e2e.py uses an `e2e` marker that is not registered, and the config is strict about markers. This fails identically at HEAD, so it predates this change; `make test` passes because it ignores tests/e2e, tests/integration and tests/contracts. test_tatlock_tool_call_logging_calculator is flaky. It failed once in a full run with these changes and passed on the next, passes in isolation with them, and fails in isolation at HEAD. It is order- or timing-dependent, not a regression from this commit — established by running the full suite both ways rather than by reasoning about which change could have caused it. Co-Authored-By: Claude <noreply@anthropic.com>
245 lines
7.2 KiB
Python
245 lines
7.2 KiB
Python
"""
|
|
Tests for main application setup and configuration.
|
|
|
|
Tests:
|
|
- App creation
|
|
- CORS configuration
|
|
- Lifespan events
|
|
- Router registration
|
|
- Exception handlers
|
|
"""
|
|
|
|
import pytest
|
|
from fastapi import FastAPI
|
|
from fastapi.testclient import TestClient
|
|
|
|
|
|
@pytest.mark.unit
|
|
def test_app_creation():
|
|
"""Test that application is created correctly."""
|
|
from src.main import app
|
|
|
|
assert isinstance(app, FastAPI)
|
|
assert app.title == "OpenAI-Compatible API"
|
|
# Version testing is brittle - just verify it's set
|
|
assert app.version is not None
|
|
|
|
|
|
@pytest.mark.unit
|
|
def test_app_routers_registered():
|
|
"""Test that all routers are registered."""
|
|
from src.main import app
|
|
|
|
# Get all registered routes
|
|
routes = [route.path for route in app.routes]
|
|
|
|
# Core routes
|
|
assert "/" in routes
|
|
assert "/health" in routes
|
|
|
|
# API routes should be present (check prefixes)
|
|
api_routes = [r for r in routes if r.startswith("/v1/")]
|
|
|
|
# Should have chat completions, responses, and models endpoints
|
|
assert any("/v1/chat/completions" in r for r in api_routes)
|
|
assert any("/v1/responses" in r for r in api_routes)
|
|
assert any("/v1/models" in r for r in api_routes)
|
|
|
|
|
|
@pytest.mark.unit
|
|
def test_cors_middleware():
|
|
"""Test CORS middleware configuration."""
|
|
from src.main import app
|
|
|
|
# Check if CORS middleware is registered
|
|
# Middleware is wrapped, so we check that at least one middleware exists
|
|
assert len(app.user_middleware) > 0
|
|
|
|
# Better check: verify CORS functionality by checking middleware stack
|
|
# The middleware should be present (wrapped as Middleware)
|
|
middleware_types = [
|
|
type(m.cls).__name__ if hasattr(m, "cls") else type(m).__name__ for m in app.user_middleware
|
|
]
|
|
assert "CORSMiddleware" in middleware_types or len(app.user_middleware) > 0
|
|
|
|
|
|
@pytest.mark.unit
|
|
def test_cors_allows_all_origins(client: TestClient):
|
|
"""Test that CORS allows requests from any origin."""
|
|
# Make OPTIONS request with Origin header
|
|
response = client.options(
|
|
"/v1/models",
|
|
headers={"Origin": "http://example.com", "Access-Control-Request-Method": "GET"},
|
|
)
|
|
|
|
# Should allow CORS
|
|
assert response.status_code == 200
|
|
assert "access-control-allow-origin" in response.headers
|
|
|
|
|
|
@pytest.mark.unit
|
|
def test_cors_allows_credentials(client: TestClient):
|
|
"""Test that CORS allows credentials."""
|
|
response = client.get("/health", headers={"Origin": "http://example.com"})
|
|
|
|
# Should include CORS headers
|
|
assert response.status_code == 200
|
|
# Access-Control-Allow-Credentials should be set
|
|
# (or Access-Control-Allow-Origin should be present)
|
|
assert "access-control-allow-origin" in response.headers
|
|
|
|
|
|
@pytest.mark.unit
|
|
def test_exception_handlers_registered():
|
|
"""Test that custom exception handlers are registered."""
|
|
from fastapi.exceptions import RequestValidationError
|
|
|
|
from src.core.exceptions import AppException
|
|
from src.main import app
|
|
|
|
# App should have exception handlers
|
|
assert len(app.exception_handlers) > 0
|
|
|
|
# Should handle AppException (parent of ModelNotFoundError)
|
|
assert AppException in app.exception_handlers
|
|
|
|
# Should handle validation errors
|
|
assert RequestValidationError in app.exception_handlers
|
|
|
|
|
|
@pytest.mark.unit
|
|
def test_model_not_found_exception_handler(client: TestClient):
|
|
"""Test ModelNotFoundError exception handler."""
|
|
# Try to use non-existent model
|
|
response = client.post(
|
|
"/v1/responses",
|
|
json={
|
|
"model": "non-existent-model-xyz",
|
|
"input": [{"role": "user", "content": "test"}],
|
|
"stream": False,
|
|
},
|
|
)
|
|
|
|
assert response.status_code == 404
|
|
data = response.json()
|
|
assert "detail" in data
|
|
|
|
|
|
@pytest.mark.unit
|
|
def test_app_exception_handler(client: TestClient):
|
|
"""Test AppException exception handler."""
|
|
# Trigger validation error (which raises AppException in handler)
|
|
response = client.post(
|
|
"/v1/responses",
|
|
json={
|
|
"model": "lorem-tester",
|
|
"input": [], # Empty input might cause issues
|
|
"temperature": 3.0, # Invalid temperature
|
|
},
|
|
)
|
|
|
|
# Should return validation error
|
|
assert response.status_code == 422
|
|
|
|
|
|
@pytest.mark.unit
|
|
@pytest.mark.asyncio
|
|
async def test_lifespan_startup():
|
|
"""Test lifespan startup event."""
|
|
|
|
from src.main import app
|
|
|
|
# The lifespan context manager should be defined
|
|
assert hasattr(app.router, "lifespan_context")
|
|
|
|
# Lifespan should handle startup/shutdown
|
|
# We test this by creating the app (which happens in conftest.py)
|
|
# If startup fails, the test client wouldn't work
|
|
from fastapi.testclient import TestClient
|
|
|
|
with TestClient(app) as test_client:
|
|
# If we can make a request, startup succeeded
|
|
response = test_client.get("/health")
|
|
assert response.status_code == 200
|
|
|
|
|
|
@pytest.mark.unit
|
|
def test_openapi_schema_generated():
|
|
"""Test that OpenAPI schema is generated."""
|
|
from src.main import app
|
|
|
|
schema = app.openapi()
|
|
|
|
assert schema is not None
|
|
assert "openapi" in schema
|
|
assert "info" in schema
|
|
assert schema["info"]["title"] == "OpenAI-Compatible API"
|
|
assert "paths" in schema
|
|
|
|
# Should have our endpoints
|
|
paths = schema["paths"]
|
|
assert "/v1/chat/completions" in paths
|
|
assert "/v1/responses" in paths
|
|
assert "/v1/models" in paths
|
|
|
|
|
|
@pytest.mark.unit
|
|
def test_openapi_docs_accessible(client: TestClient):
|
|
"""Test that OpenAPI docs are accessible."""
|
|
# Swagger UI
|
|
response = client.get("/docs")
|
|
assert response.status_code == 200
|
|
assert "text/html" in response.headers["content-type"]
|
|
|
|
# ReDoc
|
|
response = client.get("/redoc")
|
|
assert response.status_code == 200
|
|
assert "text/html" in response.headers["content-type"]
|
|
|
|
|
|
@pytest.mark.unit
|
|
def test_app_metadata():
|
|
"""Test application metadata."""
|
|
from src.main import app
|
|
|
|
assert app.title == "OpenAI-Compatible API"
|
|
# Version testing is brittle - just verify it's set
|
|
assert app.version is not None
|
|
# Description is not set in main.py, so it will be empty
|
|
# We just verify the important metadata is present
|
|
assert app.debug is not None # Debug flag should be set
|
|
|
|
|
|
@pytest.mark.unit
|
|
def test_app_contact_info():
|
|
"""Test that app has contact information."""
|
|
from src.main import app
|
|
|
|
# OpenAPI schema should have contact info if configured
|
|
schema = app.openapi()
|
|
assert "info" in schema
|
|
|
|
# Title and version should be set
|
|
assert schema["info"]["title"] == "OpenAI-Compatible API"
|
|
# Version testing is brittle - just verify it exists
|
|
assert "version" in schema["info"]
|
|
assert schema["info"]["version"] is not None
|
|
|
|
|
|
@pytest.mark.unit
|
|
def test_validation_error_handler(client: TestClient):
|
|
"""Test that validation errors are handled properly."""
|
|
# Send invalid request (missing required field)
|
|
response = client.post(
|
|
"/v1/chat/completions",
|
|
json={
|
|
# Missing "model" field
|
|
"messages": [{"role": "user", "content": "test"}]
|
|
},
|
|
)
|
|
|
|
assert response.status_code == 422
|
|
data = response.json()
|
|
assert "error" in data
|
|
assert data["error"]["type"] == "invalid_request_error"
|