"""Tests for the flask logger plugin.""" import logging import uuid from typing import Any from unittest import mock import pytest from flask import Flask, Response, g from pytest_mock import MockerFixture from owslogger import constants, flask_logger from sample_flask import app @pytest.fixture def header_user_information() -> dict[str, str]: """User information in the headers.""" return { constants.GRASS_ACCOUNT_ID_HEADER: "2000", constants.GRASS_ACCOUNT_TYPE_HEADER: "subaccount", constants.USER_ID_HEADER: "007", } def test_setting_up_flask_app() -> None: """Test setting up a flask application.""" mock_app = Flask(__name__) flask_logger.setup(mock_app, "qa", "logging", logging.INFO, "service", "1.0.0") def test_get_ows() -> None: """Test getting ows.""" with app.app_context(): ows = flask_logger.get_ows() assert isinstance(ows, flask_logger.Ows) assert isinstance(g.ows, flask_logger.Ows) def test_setting_up_flask_app_with_excluded_paths( monkeypatch: pytest.MonkeyPatch, ) -> None: """Test setting up a flask application.""" mock_app = Flask(__name__) monkeypatch.setattr(flask_logger, "global_logger", mock.Mock()) flask_logger.setup( mock_app, "qa", "logging", logging.INFO, "service", "1.0.0", exclude_paths=["/hello/"], dsn="", ) with app.test_request_context("/hello/"): g.ows = mock.Mock() g.ows.log = mock.Mock() g.log = mock.Mock() mock_app.global_correlation_id() # type: ignore assert not g.log.info.called assert not g.ows.log.info.called def test_app_correlation_id_creation(monkeypatch: pytest.MonkeyPatch) -> None: """Test creation of the correlation id.""" monkeypatch.setattr(flask_logger, "global_logger", mock.Mock()) with app.test_request_context("/hello/"): g.ows = mock.Mock() g.ows.log = mock.Mock() g.log = mock.Mock() app.global_correlation_id() # type: ignore assert g.correlation_id assert g.ows.log.debug.called def test_app_correlation_id_returns_nothing(monkeypatch: pytest.MonkeyPatch) -> None: """Test correlation id doesn't return anything. A non-None return value would short-circuit the request in Flask. """ monkeypatch.setattr(flask_logger, "global_logger", mock.Mock()) with app.test_request_context("/hello/"): g.ows = mock.Mock() g.ows.log = mock.Mock() g.log = mock.Mock() g.correlation_id = "boo!" result = app.global_correlation_id() # type: ignore assert result is None assert g.correlation_id == "boo!" def test_app_correlation_id_reuse() -> None: """Test correlation id reuse. When passed from the header, the correlation id should be reused. """ correlation_id = str(uuid.uuid1()) with app.test_request_context( "/hello/", headers={constants.CORRELATION_ID_HEADER: correlation_id} ): app.global_correlation_id() # type: ignore assert g.correlation_id == correlation_id assert g.ows.correlation_id == correlation_id def test_logger_creation() -> None: """Test creation of the logger.""" with app.test_request_context("/hello/"): app.global_logger() # type: ignore assert g.correlation_id assert g.ows.correlation_id assert g.ows.correlation_id == g.correlation_id assert isinstance(g.log, logging.LoggerAdapter) assert isinstance(g.ows.log, logging.LoggerAdapter) assert "correlation_id" in g.log.extra assert "correlation_id" in g.ows.log.extra assert g.log.extra.get("correlation_id") == g.correlation_id assert g.ows.log.extra.get("correlation_id") == g.correlation_id def test_add_correlation_id_to_response() -> None: """Test correlation id is added to response header.""" test_client = app.test_client() response = test_client.get("/hello/") assert response.headers.get(constants.CORRELATION_ID_HEADER) @pytest.mark.parametrize( ("status_code", "log_method"), [ ("200", "info"), ("304", "warning"), ("404", "warning"), ("503", "error"), ], ) def test_autolog( monkeypatch: pytest.MonkeyPatch, status_code: str, log_method: str, header_user_information: dict[str, str], ) -> None: """Test autolog triggers a specific logger based on the response status.""" response = Response(response=b"body", status=status_code) headers = header_user_information with app.test_request_context("/hello/", headers=headers): app.global_logger() # type: ignore monkeypatch.setattr(g.ows.log, log_method, mock.Mock()) logger = getattr(g.ows.log, log_method) assert not logger.called assert flask_logger.autolog(response) is response assert logger.called logger.assert_called_with( f"{status_code} - GET /hello/", resources={ "account_id": headers.get(constants.GRASS_ACCOUNT_ID_HEADER), "account_type": headers.get(constants.GRASS_ACCOUNT_TYPE_HEADER), "user_id": headers.get(constants.USER_ID_HEADER), }, ) def test_autolog_with_extra_message(monkeypatch: pytest.MonkeyPatch) -> None: """Test autolog with an extra message.""" response = Response(response=b"body", status=200) with app.test_request_context("/hello/"): app.global_logger() # type: ignore monkeypatch.setattr(g.ows.log, "info", mock.Mock()) g.ows.log.extra_message = "Hello World" assert flask_logger.autolog(response) is response g.ows.log.info.assert_called_with( f"200 - GET /hello/\n{g.ows.log.extra_message}", resources={} ) def test_autolog_with_extra_resources(monkeypatch: pytest.MonkeyPatch) -> None: """Test autolog with extra resources.""" response = Response(response=b"body", status=200) with app.test_request_context("/hello/"): app.global_logger() # type: ignore monkeypatch.setattr(g.ows.log, "info", mock.Mock()) resources = {"isrc": "209873", "upc": "587649"} g.ows.log.resources.update(resources) assert flask_logger.autolog(response) is response g.ows.log.info.assert_called_with("200 - GET /hello/", resources=resources) def test_autolog_with_extra_resources_and_user_information( monkeypatch: pytest.MonkeyPatch, header_user_information: dict[str, str] ) -> None: """Test autolog with an extra message and user information.""" response = Response(response=b"body", status=200) headers = header_user_information with app.test_request_context("/hello/", headers=headers): app.global_logger() # type: ignore monkeypatch.setattr(g.ows.log, "info", mock.Mock()) resources: dict[str, Any] = {"isrc": "209873", "upc": "587649"} g.ows.log.resources.update(resources) assert flask_logger.autolog(response) is response resources.update( { "account_id": headers.get(constants.GRASS_ACCOUNT_ID_HEADER), "account_type": headers.get(constants.GRASS_ACCOUNT_TYPE_HEADER), "user_id": headers.get(constants.USER_ID_HEADER), } ) g.ows.log.info.assert_called_with("200 - GET /hello/", resources=resources) def test_autolog_with_excluded_paths(monkeypatch: pytest.MonkeyPatch) -> None: """Test autolog with excluded paths.""" path = "/hello/" response = Response(response=b"body", status=200) with app.test_request_context(path): app.global_logger() # type: ignore monkeypatch.setattr(g.ows, "log", mock.Mock()) assert flask_logger.autolog(response, exclude_paths=[path]) is response assert not g.ows.log.info.called def test_autolog_with_request_context(monkeypatch: pytest.MonkeyPatch) -> None: """Test autolog with a request context.""" response = Response(response=b"body", status=200) with app.test_request_context("/hello/"): app.global_logger() # type: ignore class RequestContext: def __init__(self) -> None: self.context_type = "profile" self.authorization = "token" self.profile_type = "A" self.brand = "orchard" g.request_context = RequestContext() monkeypatch.setattr(g.ows.log, "info", mock.Mock()) assert flask_logger.autolog(response) is response expected_resource_request_context = { "context_type": "profile", "profile_type": "A", "brand": "orchard", } g.ows.log.info.assert_called_with( "200 - GET /hello/", resources=expected_resource_request_context ) def test_autolog_dd_tags( monkeypatch: pytest.MonkeyPatch, mocker: MockerFixture ) -> None: """Test autolog with a request context.""" response = Response(response=b"body", status=200) with app.test_request_context( "/hello/", headers={constants.CORRELATION_ID_HEADER: "boo!"} ): app.global_logger() # type: ignore monkeypatch.setattr(g.ows.log, "info", mock.Mock()) root_span_mock = mock.Mock() mocker.patch( "owslogger.flask_logger.tracer.current_root_span", return_value=root_span_mock, ) flask_logger.autolog(response) root_span_mock.set_tag.assert_called_with("correlation-id", "boo!") def test_autolog_with_full_path(monkeypatch: pytest.MonkeyPatch) -> None: """Test autolog with a full path.""" response = Response(response=b"body", status=200) with app.test_request_context("/hello/?param=value"): app.global_logger() # type: ignore monkeypatch.setattr(g.ows.log, "info", mock.Mock()) assert flask_logger.autolog(response, autolog_full_path=True) is response g.ows.log.info.assert_called_with("200 - GET /hello/?param=value", resources={}) def test_autolog_with_ignoring_request_params(monkeypatch: pytest.MonkeyPatch) -> None: """Test autolog with ignoring request params.""" response = Response(response=b"body", status=200) with app.test_request_context("/hello/?param=value"): app.global_logger() # type: ignore monkeypatch.setattr(g.ows.log, "info", mock.Mock()) assert flask_logger.autolog(response, autolog_full_path=False) is response g.ows.log.info.assert_called_with("200 - GET /hello/", resources={}) @mock.patch("owslogger.logger.setup", return_value=mock.Mock()) def test_setup_clear_handlers(mock_setup: mock.Mock) -> None: """Test setting up a flask application.""" mock_app = Flask(__name__) flask_logger.setup(mock_app, "qa", "logging", logging.INFO, "service", "1.0.0") mock_setup.assert_called_with( "qa", "logging", 20, "service", "1.0.0", clear_handlers=True, dsn=None )