You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

test_ext_request_logging.py 8.9KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265
  1. import json
  2. import logging
  3. from unittest import mock
  4. import pytest
  5. from flask import Flask, Response
  6. from configs import dify_config
  7. from extensions import ext_request_logging
  8. from extensions.ext_request_logging import _is_content_type_json, _log_request_finished, init_app
  9. def test_is_content_type_json():
  10. """
  11. Test the _is_content_type_json function.
  12. """
  13. assert _is_content_type_json("application/json") is True
  14. # content type header with charset option.
  15. assert _is_content_type_json("application/json; charset=utf-8") is True
  16. # content type header with charset option, in uppercase.
  17. assert _is_content_type_json("APPLICATION/JSON; CHARSET=UTF-8") is True
  18. assert _is_content_type_json("text/html") is False
  19. assert _is_content_type_json("") is False
  20. _KEY_NEEDLE = "needle"
  21. _VALUE_NEEDLE = _KEY_NEEDLE[::-1]
  22. _RESPONSE_NEEDLE = "response"
  23. def _get_test_app():
  24. app = Flask(__name__)
  25. @app.route("/", methods=["GET", "POST"])
  26. def handler():
  27. return _RESPONSE_NEEDLE
  28. return app
  29. # NOTE(QuantumGhost): Due to the design of Flask, we need to use monkey patch to write tests.
  30. @pytest.fixture
  31. def mock_request_receiver(monkeypatch) -> mock.Mock:
  32. mock_log_request_started = mock.Mock()
  33. monkeypatch.setattr(ext_request_logging, "_log_request_started", mock_log_request_started)
  34. return mock_log_request_started
  35. @pytest.fixture
  36. def mock_response_receiver(monkeypatch) -> mock.Mock:
  37. mock_log_request_finished = mock.Mock()
  38. monkeypatch.setattr(ext_request_logging, "_log_request_finished", mock_log_request_finished)
  39. return mock_log_request_finished
  40. @pytest.fixture
  41. def mock_logger(monkeypatch) -> logging.Logger:
  42. _logger = mock.MagicMock(spec=logging.Logger)
  43. monkeypatch.setattr(ext_request_logging, "_logger", _logger)
  44. return _logger
  45. @pytest.fixture
  46. def enable_request_logging(monkeypatch):
  47. monkeypatch.setattr(dify_config, "ENABLE_REQUEST_LOGGING", True)
  48. class TestRequestLoggingExtension:
  49. def test_receiver_should_not_be_invoked_if_configuration_is_disabled(
  50. self,
  51. monkeypatch,
  52. mock_request_receiver,
  53. mock_response_receiver,
  54. ):
  55. monkeypatch.setattr(dify_config, "ENABLE_REQUEST_LOGGING", False)
  56. app = _get_test_app()
  57. init_app(app)
  58. with app.test_client() as client:
  59. client.get("/")
  60. mock_request_receiver.assert_not_called()
  61. mock_response_receiver.assert_not_called()
  62. def test_receiver_should_be_called_if_enabled(
  63. self,
  64. enable_request_logging,
  65. mock_request_receiver,
  66. mock_response_receiver,
  67. ):
  68. """
  69. Test the request logging extension with JSON data.
  70. """
  71. app = _get_test_app()
  72. init_app(app)
  73. with app.test_client() as client:
  74. client.post("/", json={_KEY_NEEDLE: _VALUE_NEEDLE})
  75. mock_request_receiver.assert_called_once()
  76. mock_response_receiver.assert_called_once()
  77. class TestLoggingLevel:
  78. @pytest.mark.usefixtures("enable_request_logging")
  79. def test_logging_should_be_skipped_if_level_is_above_debug(self, enable_request_logging, mock_logger):
  80. mock_logger.isEnabledFor.return_value = False
  81. app = _get_test_app()
  82. init_app(app)
  83. with app.test_client() as client:
  84. client.post("/", json={_KEY_NEEDLE: _VALUE_NEEDLE})
  85. mock_logger.debug.assert_not_called()
  86. class TestRequestReceiverLogging:
  87. @pytest.mark.usefixtures("enable_request_logging")
  88. def test_non_json_request(self, enable_request_logging, mock_logger, mock_response_receiver):
  89. mock_logger.isEnabledFor.return_value = True
  90. app = _get_test_app()
  91. init_app(app)
  92. with app.test_client() as client:
  93. client.post("/", data="plain text")
  94. assert mock_logger.debug.call_count == 1
  95. call_args = mock_logger.debug.call_args[0]
  96. assert "Received Request" in call_args[0]
  97. assert call_args[1] == "POST"
  98. assert call_args[2] == "/"
  99. assert "Request Body" not in call_args[0]
  100. @pytest.mark.usefixtures("enable_request_logging")
  101. def test_json_request(self, enable_request_logging, mock_logger, mock_response_receiver):
  102. mock_logger.isEnabledFor.return_value = True
  103. app = _get_test_app()
  104. init_app(app)
  105. with app.test_client() as client:
  106. client.post("/", json={_KEY_NEEDLE: _VALUE_NEEDLE})
  107. assert mock_logger.debug.call_count == 1
  108. call_args = mock_logger.debug.call_args[0]
  109. assert "Received Request" in call_args[0]
  110. assert "Request Body" in call_args[0]
  111. assert call_args[1] == "POST"
  112. assert call_args[2] == "/"
  113. assert _KEY_NEEDLE in call_args[3]
  114. @pytest.mark.usefixtures("enable_request_logging")
  115. def test_json_request_with_empty_body(self, enable_request_logging, mock_logger, mock_response_receiver):
  116. mock_logger.isEnabledFor.return_value = True
  117. app = _get_test_app()
  118. init_app(app)
  119. with app.test_client() as client:
  120. client.post("/", headers={"Content-Type": "application/json"})
  121. assert mock_logger.debug.call_count == 1
  122. call_args = mock_logger.debug.call_args[0]
  123. assert "Received Request" in call_args[0]
  124. assert "Request Body" not in call_args[0]
  125. assert call_args[1] == "POST"
  126. assert call_args[2] == "/"
  127. @pytest.mark.usefixtures("enable_request_logging")
  128. def test_json_request_with_invalid_json_as_body(self, enable_request_logging, mock_logger, mock_response_receiver):
  129. mock_logger.isEnabledFor.return_value = True
  130. app = _get_test_app()
  131. init_app(app)
  132. with app.test_client() as client:
  133. client.post(
  134. "/",
  135. headers={"Content-Type": "application/json"},
  136. data="{",
  137. )
  138. assert mock_logger.debug.call_count == 0
  139. assert mock_logger.exception.call_count == 1
  140. exception_call_args = mock_logger.exception.call_args[0]
  141. assert exception_call_args[0] == "Failed to parse JSON request"
  142. class TestResponseReceiverLogging:
  143. @pytest.mark.usefixtures("enable_request_logging")
  144. def test_non_json_response(self, enable_request_logging, mock_logger):
  145. mock_logger.isEnabledFor.return_value = True
  146. app = _get_test_app()
  147. response = Response(
  148. "OK",
  149. headers={"Content-Type": "text/plain"},
  150. )
  151. _log_request_finished(app, response)
  152. assert mock_logger.debug.call_count == 1
  153. call_args = mock_logger.debug.call_args[0]
  154. assert "Response" in call_args[0]
  155. assert "200" in call_args[1]
  156. assert call_args[2] == "text/plain"
  157. assert "Response Body" not in call_args[0]
  158. @pytest.mark.usefixtures("enable_request_logging")
  159. def test_json_response(self, enable_request_logging, mock_logger, mock_response_receiver):
  160. mock_logger.isEnabledFor.return_value = True
  161. app = _get_test_app()
  162. response = Response(
  163. json.dumps({_KEY_NEEDLE: _VALUE_NEEDLE}),
  164. headers={"Content-Type": "application/json"},
  165. )
  166. _log_request_finished(app, response)
  167. assert mock_logger.debug.call_count == 1
  168. call_args = mock_logger.debug.call_args[0]
  169. assert "Response" in call_args[0]
  170. assert "Response Body" in call_args[0]
  171. assert "200" in call_args[1]
  172. assert call_args[2] == "application/json"
  173. assert _KEY_NEEDLE in call_args[3]
  174. @pytest.mark.usefixtures("enable_request_logging")
  175. def test_json_request_with_invalid_json_as_body(self, enable_request_logging, mock_logger, mock_response_receiver):
  176. mock_logger.isEnabledFor.return_value = True
  177. app = _get_test_app()
  178. response = Response(
  179. "{",
  180. headers={"Content-Type": "application/json"},
  181. )
  182. _log_request_finished(app, response)
  183. assert mock_logger.debug.call_count == 0
  184. assert mock_logger.exception.call_count == 1
  185. exception_call_args = mock_logger.exception.call_args[0]
  186. assert exception_call_args[0] == "Failed to parse JSON response"
  187. class TestResponseUnmodified:
  188. def test_when_request_logging_disabled(self):
  189. app = _get_test_app()
  190. init_app(app)
  191. with app.test_client() as client:
  192. response = client.post(
  193. "/",
  194. headers={"Content-Type": "application/json"},
  195. data="{",
  196. )
  197. assert response.text == _RESPONSE_NEEDLE
  198. assert response.status_code == 200
  199. @pytest.mark.usefixtures("enable_request_logging")
  200. def test_when_request_logging_enabled(self, enable_request_logging):
  201. app = _get_test_app()
  202. init_app(app)
  203. with app.test_client() as client:
  204. response = client.post(
  205. "/",
  206. headers={"Content-Type": "application/json"},
  207. data="{",
  208. )
  209. assert response.text == _RESPONSE_NEEDLE
  210. assert response.status_code == 200