From 4f27b8e4ae1bd3de9a250983126cff04cf63d38a Mon Sep 17 00:00:00 2001 From: chottokun <29515187+chottokun@users.noreply.github.com> Date: Mon, 12 Oct 2026 00:31:10 +0000 Subject: [PATCH] test: add unit tests for request logging and security headers middlewares - Add test for `RequestLoggingMiddleware` exception handling and `request_id_var` cleanup in `src/tests/test_logger.py` - Add test for `SecurityHeadersMiddleware` injection on 500 error responses in `src/tests/test_security_headers.py` Co-authored-by: google-labs-jules[bot] <161369871+google-labs-jules[bot]@users.noreply.github.com> --- src/tests/test_logger.py | 36 ++++++++++++++++++++++++++++++ src/tests/test_security_headers.py | 20 +++++++++++++++++ 2 files changed, 56 insertions(+) diff --git a/src/tests/test_logger.py b/src/tests/test_logger.py index 503b922..bfaf3e1 100644 --- a/src/tests/test_logger.py +++ b/src/tests/test_logger.py @@ -1,6 +1,9 @@ from unittest.mock import patch +import pytest +from fastapi import Request, Response from fastapi.testclient import TestClient from app.main import app +from app.middleware.logging import RequestLoggingMiddleware, request_id_var client = TestClient(app) @@ -30,3 +33,36 @@ def test_x_request_id_passed(): assert response.status_code == 200 assert response.headers["X-Request-ID"] == test_id mock_logger.assert_called_once() + + +@pytest.mark.anyio +async def test_logger_middleware_unit_exception(): + # Unit test RequestLoggingMiddleware directly when call_next raises an exception. + middleware = RequestLoggingMiddleware(app) + mock_request = Request( + { + "type": "http", + "method": "GET", + "path": "/error-test", + "headers": [(b"x-request-id", b"unit-test-id")], + } + ) + + async def mock_call_next(req): + raise RuntimeError("Downstream failure") + + with patch("app.middleware.logging.logger.info") as mock_logger: + with pytest.raises(RuntimeError, match="Downstream failure"): + await middleware.dispatch(mock_request, mock_call_next) + + mock_logger.assert_called_once() + args, kwargs = mock_logger.call_args + + assert "GET /error-test - 500" in args[0] + assert kwargs["extra"]["path"] == "/error-test" + assert kwargs["extra"]["method"] == "GET" + assert kwargs["extra"]["status_code"] == 500 + assert "latency" in kwargs["extra"] + + # Verify contextvar request_id_var was properly reset + assert request_id_var.get() == "" diff --git a/src/tests/test_security_headers.py b/src/tests/test_security_headers.py index b9afd55..891f697 100644 --- a/src/tests/test_security_headers.py +++ b/src/tests/test_security_headers.py @@ -1,3 +1,4 @@ +from unittest.mock import patch from fastapi.testclient import TestClient from app.main import app @@ -32,3 +33,22 @@ def test_security_headers(): headers["Content-Security-Policy"] == "default-src 'self'; frame-ancestors 'none';" ) + + +def test_security_headers_on_500_exception(): + # Test that security headers are present on 500 Internal Server Error responses + err_client = TestClient(app, raise_server_exceptions=False) + with patch("app.main.get_model", side_effect=RuntimeError("Unexpected error")): + response = err_client.post( + "/v1/embeddings", json={"input": "test", "model": "cl-nagoya/ruri-v3-30m"} + ) + + assert response.status_code == 500 + headers = response.headers + + assert headers.get("X-Content-Type-Options") == "nosniff" + assert headers.get("X-Frame-Options") == "DENY" + assert headers.get("X-XSS-Protection") == "1; mode=block" + assert headers.get("Strict-Transport-Security") == "max-age=31536000; includeSubDomains" + assert headers.get("Referrer-Policy") == "strict-origin-when-cross-origin" + assert headers.get("Content-Security-Policy") == "default-src 'self'; frame-ancestors 'none';"