import asyncio from dataclasses import replace import httpx from cryptography.fernet import Fernet from fastapi import Query from backend.bootstrap.application import create_application from backend.bootstrap.settings import Settings from backend.http.errors import AppError from tests.support import request def test_health_reports_runtime_environment(tmp_path) -> None: application = create_application(Settings.for_test(tmp_path)) response = request(application, "/api/health") assert response.status_code == 200 assert response.json() == { "status": "ok", "environment": "test", "components": {"process": "ok", "database": "ok"}, } assert len(response.headers["X-Request-ID"]) == 32 assert response.headers["X-Content-Type-Options"] == "nosniff" assert response.headers["X-Frame-Options"] == "DENY" def test_unknown_failure_uses_safe_error_contract(tmp_path) -> None: application = create_application(Settings.for_test(tmp_path)) @application.get("/api/test/failure") def fail() -> None: raise RuntimeError("secret provider details") response = request(application, "/api/test/failure") payload = response.json()["error"] assert response.status_code == 500 assert payload["code"] == "internal_error" assert payload["message"] == "服务暂时不可用,请稍后重试。" assert "secret provider details" not in response.text assert len(payload["request_id"]) == 32 assert response.headers["X-Request-ID"] == payload["request_id"] def test_application_error_uses_business_message(tmp_path) -> None: application = create_application(Settings.for_test(tmp_path)) @application.get("/api/test/locked") def locked() -> None: raise AppError("membership_required", "该功能仅对会员开放。", 403) response = request(application, "/api/test/locked") assert response.status_code == 403 assert response.json()["error"]["code"] == "membership_required" assert response.json()["error"]["message"] == "该功能仅对会员开放。" def test_framework_404_uses_the_same_safe_contract(tmp_path) -> None: application = create_application(Settings.for_test(tmp_path)) response = request(application, "/api/does-not-exist") assert response.status_code == 404 assert response.json()["error"]["code"] == "not_found" assert response.json()["error"]["message"] == "请求的内容不存在。" def test_validation_error_does_not_expose_framework_details(tmp_path) -> None: application = create_application(Settings.for_test(tmp_path)) @application.get("/api/test/validated") def validated(value: int = Query(ge=1)) -> dict[str, int]: return {"value": value} response = request(application, "/api/test/validated?value=wrong") assert response.status_code == 422 assert response.json()["error"]["code"] == "invalid_request" assert "integer" not in response.text.lower() def test_production_frontend_serves_spa_without_capturing_api_404(tmp_path) -> None: settings = Settings.for_test(tmp_path) settings.frontend_dist_directory.mkdir(parents=True) settings.frontend_dist_directory.joinpath("index.html").write_text( "
application
", encoding="utf-8" ) settings.frontend_dist_directory.joinpath("assets").mkdir() settings.frontend_dist_directory.joinpath("assets", "asset.js").write_text( "window.ready=true", encoding="utf-8" ) application = create_application(settings) assert request(application, "/review/history").text == "
application
" asset_response = request(application, "/assets/asset.js") assert asset_response.text == "window.ready=true" assert asset_response.headers["Cache-Control"] == "public,max-age=31536000,immutable" api_response = request(application, "/api/unknown") assert api_response.status_code == 404 assert api_response.json()["error"]["code"] == "not_found" def test_production_security_headers_are_strict_on_https(tmp_path) -> None: settings = Settings.for_test(tmp_path) settings = replace( settings, environment="production", encryption_key=Fernet.generate_key().decode("ascii"), ) application = create_application(settings) async def get(): transport = httpx.ASGITransport(app=application) async with application.router.lifespan_context(application): async with httpx.AsyncClient( transport=transport, base_url="https://testserver" ) as client: return await client.get("/api/health") response = asyncio.run(get()) assert "default-src 'self'" in response.headers["Content-Security-Policy"] assert response.headers["Strict-Transport-Security"].startswith("max-age=31536000")