Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
82 changes: 82 additions & 0 deletions tests/test_connexion_extractors.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
"""Request and response info extraction for the connexion backend (issue #137)."""
from types import SimpleNamespace

import pytest
from helpers.imports import undo_imports_from_package


@pytest.fixture
def connexion_extractors():
import json_logging
from json_logging.framework import connexion as connexion_framework

yield json_logging, connexion_framework
undo_imports_from_package("json_logging")


def _request(**overrides):
values = dict(
authorization=None,
headers={"X-Present": "yes"},
environ={"SERVER_PROTOCOL": "HTTP/1.1", "REMOTE_PORT": "5555"},
path="/hello",
content_length=12,
method="POST",
remote_addr="10.0.0.1",
)
values.update(overrides)
return SimpleNamespace(**values)


def test_init_request_instrument_rejects_non_connexion_app(connexion_extractors):
json_logging, _ = connexion_extractors
json_logging.init_connexion()

with pytest.raises(RuntimeError, match="not a valid connexion"):
json_logging.init_request_instrument(SimpleNamespace(app=object()))


def test_remote_user_defaults_to_empty_value(connexion_extractors):
json_logging, framework = connexion_extractors

assert framework.ConnexionRequestInfoExtractor().get_remote_user(_request()) == json_logging.EMPTY_VALUE


def test_remote_user_is_the_basic_auth_username(connexion_extractors):
_, framework = connexion_extractors
request = _request(authorization=SimpleNamespace(username="alice"))

assert framework.ConnexionRequestInfoExtractor().get_remote_user(request) == "alice"


def test_header_lookup_falls_back_to_default(connexion_extractors):
_, framework = connexion_extractors
extractor = framework.ConnexionRequestInfoExtractor()

assert extractor.get_http_header(_request(), "X-Present") == "yes"
assert extractor.get_http_header(_request(), "X-Missing", "fallback") == "fallback"
assert extractor.get_http_header(object(), "X-Missing", "fallback") == "fallback"


def test_request_fields_are_read_from_the_request(connexion_extractors):
_, framework = connexion_extractors
extractor = framework.ConnexionRequestInfoExtractor()
request = _request()

assert extractor.get_protocol(request) == "HTTP/1.1"
assert extractor.get_path(request) == "/hello"
assert extractor.get_content_length(request) == 12
assert extractor.get_method(request) == "POST"
assert extractor.get_remote_ip(request) == "10.0.0.1"
assert extractor.get_remote_port(request) == "5555"


def test_response_fields_are_read_from_the_response(connexion_extractors):
_, framework = connexion_extractors
extractor = framework.ConnexionResponseInfoExtractor()
response = SimpleNamespace(
status_code=201, calculate_content_length=lambda: 42, content_type="application/json")

assert extractor.get_status_code(response) == 201
assert extractor.get_response_size(response) == 42
assert extractor.get_content_type(response) == "application/json"
94 changes: 94 additions & 0 deletions tests/test_flask_extractors.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
"""Request and response info extraction for the flask and connexion backends (issue #137)."""
import base64
import json
import logging
from types import SimpleNamespace

import flask
import pytest
from helpers.handler import FormattedMessageCollectorHandler
from helpers.imports import undo_imports_from_package


@pytest.fixture
def flask_app_and_request_handler():
import json_logging
from json_logging.formatters import JSONRequestLogFormatter

app = flask.Flask(__name__)
request_logger = logging.getLogger("flask-request-logger")
handler = FormattedMessageCollectorHandler()
handler.setFormatter(JSONRequestLogFormatter())

json_logging.init_flask(enable_json=True)
json_logging.init_request_instrument(app)
request_logger.addHandler(handler)

@app.route("/hello")
def hello():
return {"hello": "world"}

yield app, handler

request_logger.removeHandler(handler)
undo_imports_from_package("json_logging")


def test_flask_request_log_has_request_and_response_details(flask_app_and_request_handler):
app, handler = flask_app_and_request_handler

response = app.test_client().get("/hello", headers={"X-Correlation-Id": "abc-def"})

assert response.status_code == 200
msg = json.loads(handler.messages[0])
assert msg["method"] == "GET"
assert msg["request"] == "/hello"
assert msg["protocol"] == "HTTP/1.1"
assert msg["correlation_id"] == "abc-def"
assert msg["response_status"] == 200
assert msg["response_content_type"] == "application/json"
assert msg["response_size_b"] == len(response.data)
assert msg["remote_user"] == "-"


def test_flask_request_log_has_basic_auth_user(flask_app_and_request_handler):
app, handler = flask_app_and_request_handler
token = base64.b64encode(b"alice:secret").decode()

app.test_client().get("/hello", headers={"Authorization": "Basic " + token})

assert json.loads(handler.messages[0])["remote_user"] == "alice"


def test_flask_init_request_instrument_rejects_non_flask_app(flask_app_and_request_handler):
import json_logging

with pytest.raises(RuntimeError, match="not a valid flask"):
json_logging.init_request_instrument(object())


def test_flask_extractor_outside_request_context_has_no_current_request(flask_app_and_request_handler):
from json_logging.framework.flask import FlaskRequestInfoExtractor

assert FlaskRequestInfoExtractor.get_current_request() is None


def test_flask_extractor_header_lookup_falls_back_to_default(flask_app_and_request_handler):
from json_logging.framework.flask import FlaskRequestInfoExtractor

extractor = FlaskRequestInfoExtractor()
request = SimpleNamespace(headers={"X-Present": "yes"})

assert extractor.get_http_header(request, "X-Present") == "yes"
assert extractor.get_http_header(request, "X-Missing", "fallback") == "fallback"
# an object without usable headers must not raise
assert extractor.get_http_header(object(), "X-Missing", "fallback") == "fallback"


def test_flask_extractor_correlation_id_outside_request_context_is_none(flask_app_and_request_handler):
from json_logging.framework.flask import FlaskRequestInfoExtractor

extractor = FlaskRequestInfoExtractor()

extractor.set_correlation_id(None, "ignored") # no app context: must not raise
assert extractor.get_correlation_id_in_request_context(None) is None
Loading