From e2f3faa4a55dde0b984e5b956d18f24da6230af4 Mon Sep 17 00:00:00 2001 From: Srinivas Chippagiri Date: Sat, 24 Oct 2026 13:58:36 -0700 Subject: [PATCH] test: add Sanic and Quart unit tests Cover correlation id handling, request instrumentation, URL exclusion and app validation for the Sanic and Quart integrations using their test clients, instead of relying only on the smoke tests. Adds sanic-testing to the test requirements. Refs #137 Signed-off-by: Srinivas Chippagiri --- test-requirements.txt | 1 + tests/test_quart.py | 108 ++++++++++++++++++++++++++++++++++++++++++ tests/test_sanic.py | 99 ++++++++++++++++++++++++++++++++++++++ 3 files changed, 208 insertions(+) create mode 100644 tests/test_quart.py create mode 100644 tests/test_sanic.py diff --git a/test-requirements.txt b/test-requirements.txt index d118997..a41a729 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -3,6 +3,7 @@ flask connexion[flask,swagger-ui] quart sanic +sanic-testing fastapi uvicorn requests diff --git a/tests/test_quart.py b/tests/test_quart.py new file mode 100644 index 0000000..16cd959 --- /dev/null +++ b/tests/test_quart.py @@ -0,0 +1,108 @@ +"""Test suite for the quart backend (issue #137).""" +import asyncio +import json +import logging + +import pytest +import quart +from helpers.handler import FormattedMessageCollectorHandler +from helpers.imports import undo_imports_from_package + +LOGGER_NAME = "quart-test" +REQUEST_LOGGER_NAME = "quart-request-logger" + + +@pytest.fixture +def app_and_handlers(): + import json_logging + from json_logging.formatters import JSONRequestLogFormatter + + app = quart.Quart(__name__) + + logger = logging.getLogger(LOGGER_NAME) + logger.setLevel(logging.DEBUG) + log_handler = FormattedMessageCollectorHandler() + logger.addHandler(log_handler) + + request_logger = logging.getLogger(REQUEST_LOGGER_NAME) + request_handler = FormattedMessageCollectorHandler() + request_handler.setFormatter(JSONRequestLogFormatter()) + + json_logging.init_quart(enable_json=True) + json_logging.init_request_instrument(app, exclude_url_patterns=["^/no-request-instrumentation"]) + request_logger.addHandler(request_handler) + + @app.route("/log") + async def log(): + logger.info("info message") + return {} + + @app.route("/get-correlation-id") + async def get_correlation_id(): + return {"correlation_id": json_logging.get_correlation_id()} + + @app.route("/no-request-instrumentation") + async def excluded(): + return {} + + yield app, log_handler, request_handler + + logger.removeHandler(log_handler) + request_logger.removeHandler(request_handler) + undo_imports_from_package("json_logging") + + +def _get(app, path, **kwargs): + async def run(): + return await app.test_client().get(path, **kwargs) + + return asyncio.run(run()) + + +def test_log_inside_request_has_given_correlation_id(app_and_handlers): + app, log_handler, _ = app_and_handlers + + response = _get(app, "/log", headers={"X-Correlation-Id": "abc-def"}) + + assert response.status_code == 200 + msg = json.loads(log_handler.messages[0]) + assert msg["msg"] == "info message" + assert msg["correlation_id"] == "abc-def" + + +def test_get_correlation_id_returns_given_header(app_and_handlers): + app, _, _ = app_and_handlers + + response = _get(app, "/get-correlation-id", headers={"X-Correlation-Id": "abc-def"}) + + assert asyncio.run(response.get_json()) == {"correlation_id": "abc-def"} + + +def test_request_instrumentation_writes_request_log(app_and_handlers): + app, _, request_handler = app_and_handlers + + response = _get(app, "/log") + + assert response.status_code == 200 + assert len(request_handler.messages) == 1 + msg = json.loads(request_handler.messages[0]) + assert msg["method"] == "GET" + assert msg["request"] == "/log" + assert msg["response_status"] == 200 + assert msg["type"] == "request" + + +def test_excluded_url_is_not_request_logged(app_and_handlers): + app, _, request_handler = app_and_handlers + + response = _get(app, "/no-request-instrumentation") + + assert response.status_code == 200 + assert request_handler.messages == [] + + +def test_init_request_instrument_rejects_non_quart_app(app_and_handlers): + import json_logging + + with pytest.raises(RuntimeError, match="not a valid quart"): + json_logging.init_request_instrument(object()) diff --git a/tests/test_sanic.py b/tests/test_sanic.py new file mode 100644 index 0000000..185e4e0 --- /dev/null +++ b/tests/test_sanic.py @@ -0,0 +1,99 @@ +"""Test suite for the sanic backend (issue #137).""" +import json +import logging + +import pytest +import sanic +import sanic.response +from helpers.handler import FormattedMessageCollectorHandler +from helpers.imports import undo_imports_from_package + +LOGGER_NAME = "sanic-test" +REQUEST_LOGGER_NAME = "sanic-request" + + +@pytest.fixture +def app_and_handlers(): + import json_logging + from json_logging.formatters import JSONRequestLogFormatter + + app = sanic.Sanic("json-logging-sanic-test") + + logger = logging.getLogger(LOGGER_NAME) + logger.setLevel(logging.DEBUG) + log_handler = FormattedMessageCollectorHandler() + logger.addHandler(log_handler) + + request_logger = logging.getLogger(REQUEST_LOGGER_NAME) + request_handler = FormattedMessageCollectorHandler() + request_handler.setFormatter(JSONRequestLogFormatter()) + + json_logging.init_sanic(enable_json=True) + json_logging.init_request_instrument(app, exclude_url_patterns=["^/no-request-instrumentation"]) + request_logger.addHandler(request_handler) + + @app.route("/log") + async def log(request): + logger.info("info message") + return sanic.response.json({}) + + @app.route("/get-correlation-id") + async def get_correlation_id(request): + return sanic.response.json({"correlation_id": json_logging.get_correlation_id(request)}) + + @app.route("/no-request-instrumentation") + async def excluded(request): + return sanic.response.json({}) + + yield app, log_handler, request_handler + + logger.removeHandler(log_handler) + request_logger.removeHandler(request_handler) + undo_imports_from_package("json_logging") + sanic.Sanic._app_registry.pop("json-logging-sanic-test", None) + + +def test_get_correlation_id_returns_given_header(app_and_handlers): + app, _, _ = app_and_handlers + + _, response = app.test_client.get("/get-correlation-id", headers={"X-Correlation-Id": "abc-def"}) + + assert response.json == {"correlation_id": "abc-def"} + + +def test_get_correlation_id_is_generated_when_missing(app_and_handlers): + app, _, _ = app_and_handlers + + _, response = app.test_client.get("/get-correlation-id") + + assert response.json["correlation_id"] not in (None, "-", "") + + +def test_request_instrumentation_writes_request_log(app_and_handlers): + app, _, request_handler = app_and_handlers + + _, response = app.test_client.get("/log") + + assert response.status == 200 + assert len(request_handler.messages) == 1 + msg = json.loads(request_handler.messages[0]) + assert msg["method"] == "GET" + assert msg["request"] == "/log" + assert msg["response_status"] == 200 + assert msg["type"] == "request" + + +def test_excluded_url_is_not_request_logged(app_and_handlers): + app, _, request_handler = app_and_handlers + + _, response = app.test_client.get("/no-request-instrumentation") + + assert response.status == 200 + assert request_handler.messages == [] + + +def test_init_request_instrument_rejects_non_sanic_app(app_and_handlers): + import json_logging + + with pytest.raises(RuntimeError, match="not a valid Sanic"): + json_logging.init_request_instrument(object())