test_fastapi_access_logger.py 4.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168
  1. from unittest import mock
  2. from uuid import uuid4
  3. import pytest
  4. from fastapi.routing import APIRoute
  5. from starlette.requests import Request
  6. from starlette.responses import JSONResponse
  7. from starlette.responses import StreamingResponse
  8. from clean_python import ctx
  9. from clean_python import InMemoryGateway
  10. from clean_python.fastapi import FastAPIAccessLogger
  11. @pytest.fixture
  12. def fastapi_access_logger():
  13. return FastAPIAccessLogger(hostname="myhost", gateway_override=InMemoryGateway([]))
  14. @pytest.fixture
  15. def req():
  16. # a copy-paste from a local session, with some values removed / shortened
  17. scope = {
  18. "type": "http",
  19. "asgi": {"version": "3.0", "spec_version": "2.3"},
  20. "http_version": "1.1",
  21. "server": ("172.20.0.6", 80),
  22. "client": ("172.20.0.1", 45584),
  23. "scheme": "http",
  24. "root_path": "/v1-beta",
  25. "headers": [
  26. (b"host", b"localhost:8000"),
  27. (b"connection", b"keep-alive"),
  28. (b"accept", b"application/json"),
  29. (b"authorization", b"..."),
  30. (b"user-agent", b"Mozilla/5.0 ..."),
  31. (b"referer", b"http://localhost:8000/v1-beta/docs"),
  32. (b"accept-encoding", b"gzip, deflate, br"),
  33. (b"accept-language", b"en-US,en;q=0.9"),
  34. (b"cookie", b"..."),
  35. ],
  36. "state": {},
  37. "method": "GET",
  38. "path": "/rasters",
  39. "raw_path": b"/v1-beta/rasters",
  40. "query_string": b"limit=50&offset=0&order_by=id",
  41. "path_params": {},
  42. "app_root_path": "",
  43. "route": APIRoute(
  44. endpoint=lambda x: x,
  45. path="/rasters",
  46. name="v1-beta/raster_list",
  47. methods=["GET"],
  48. ),
  49. }
  50. return Request(scope)
  51. @pytest.fixture
  52. def response():
  53. return JSONResponse({"foo": "bar"})
  54. @pytest.fixture
  55. def call_next(response):
  56. async def func(request):
  57. return response
  58. return func
  59. @pytest.fixture
  60. def correlation_id():
  61. uid = uuid4()
  62. ctx.correlation_id = uid
  63. yield uid
  64. ctx.correlation_id = None
  65. @mock.patch("time.time", return_value=0.0)
  66. async def test_logging(
  67. time, fastapi_access_logger, req, response, call_next, correlation_id
  68. ):
  69. await fastapi_access_logger(req, call_next)
  70. assert len(fastapi_access_logger.gateway.data) == 0
  71. await response.background()
  72. (actual,) = fastapi_access_logger.gateway.data.values()
  73. actual.pop("id")
  74. assert actual == {
  75. "tag_suffix": "access_log",
  76. "remote_address": "172.20.0.1",
  77. "method": "GET",
  78. "path": "/v1-beta/rasters",
  79. "portal": "localhost:8000",
  80. "referer": "http://localhost:8000/v1-beta/docs",
  81. "user_agent": "Mozilla/5.0 ...",
  82. "query_params": "limit=50&offset=0&order_by=id",
  83. "view_name": "v1-beta/raster_list",
  84. "status": 200,
  85. "content_type": "application/json",
  86. "content_length": 13,
  87. "time": 0.0,
  88. "request_time": 0.0,
  89. "correlation_id": str(correlation_id),
  90. }
  91. @pytest.fixture
  92. def req_minimal():
  93. # https://asgi.readthedocs.io/en/latest/specs/www.html#http-connection-scope
  94. scope = {
  95. "type": "http",
  96. "asgi": {"version": "3.0"},
  97. "http_version": "1.1",
  98. "method": "GET",
  99. "scheme": "http",
  100. "path": "/",
  101. "query_string": "",
  102. "headers": [],
  103. }
  104. return Request(scope)
  105. @pytest.fixture
  106. def streaming_response():
  107. async def numbers(minimum, maximum):
  108. yield ("<html><body><ul>")
  109. for number in range(minimum, maximum + 1):
  110. yield "<li>%d</li>" % number
  111. yield ("</ul></body></html>")
  112. return StreamingResponse(numbers(1, 3), media_type="text/html")
  113. @pytest.fixture
  114. def call_next_streaming(streaming_response):
  115. async def func(request):
  116. return streaming_response
  117. return func
  118. @mock.patch("time.time", return_value=0.0)
  119. async def test_logging_minimal(
  120. time, fastapi_access_logger, req_minimal, streaming_response, call_next_streaming
  121. ):
  122. await fastapi_access_logger(req_minimal, call_next_streaming)
  123. assert len(fastapi_access_logger.gateway.data) == 0
  124. await streaming_response.background()
  125. (actual,) = fastapi_access_logger.gateway.data.values()
  126. actual.pop("id")
  127. assert actual == {
  128. "tag_suffix": "access_log",
  129. "remote_address": None,
  130. "method": "GET",
  131. "path": "/",
  132. "portal": "",
  133. "referer": None,
  134. "user_agent": None,
  135. "query_params": "",
  136. "view_name": None,
  137. "status": 200,
  138. "content_type": "text/html; charset=utf-8",
  139. "content_length": None,
  140. "time": 0.0,
  141. "request_time": 0.0,
  142. "correlation_id": None,
  143. }