test_fastapi_video_query_api.py 5.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143
  1. import asyncio
  2. from fastapi.testclient import TestClient
  3. from api.base import BaseApi
  4. from api.chui_zhi.videos import VideoQueryApi
  5. from api.fastapi_app import API_PATH, create_app
  6. class FakeLogger:
  7. def __init__(self):
  8. self.records = []
  9. def info(self, message):
  10. self.records.append(('info', message))
  11. def error(self, message):
  12. self.records.append(('error', message))
  13. def exception(self, message):
  14. self.records.append(('exception', message))
  15. class FakeAliyunLogger:
  16. def __init__(self):
  17. self.events = []
  18. def logging_batch(self, events):
  19. self.events.extend(events)
  20. def build_test_app(mysql):
  21. app = create_app(manage_resources=False)
  22. app.state.mysql = mysql
  23. app.state.query_semaphore = asyncio.Semaphore(10)
  24. app.state.logger = FakeLogger()
  25. app.state.aliyun_logger = FakeAliyunLogger()
  26. return app
  27. def test_fastapi_query_keeps_existing_contract_and_unified_logging():
  28. assert issubclass(VideoQueryApi, BaseApi)
  29. class FakeMySQL:
  30. async def fetch_all(self, sql, params):
  31. return [
  32. {'id': 1, 'platform': params[0], 'create_time': '2026-08-04 12:00:00'},
  33. {'id': 2, 'platform': params[0], 'create_time': '2026-08-04 11:00:00'},
  34. ]
  35. app = build_test_app(FakeMySQL())
  36. payload = {
  37. 'platforms': ['xiaoniangao'],
  38. 'start_time': 1785772800000,
  39. 'end_time': 1785859200000,
  40. 'filters': [{'field': 'like_cnt', 'operator': '>', 'value': 3}],
  41. 'limit': 1,
  42. }
  43. with TestClient(app, raise_server_exceptions=False) as client:
  44. response = client.post(
  45. API_PATH,
  46. json=payload,
  47. headers={'X-Request-ID': 'scheduler-request-1'},
  48. )
  49. body = response.json()
  50. assert response.status_code == 200
  51. assert response.headers['X-Request-ID'] == 'scheduler-request-1'
  52. assert response.headers['Cache-Control'] == 'no-store'
  53. assert body['code'] == 0
  54. assert body['data']['count'] == 1
  55. assert body['data']['has_more'] is True
  56. assert body['data']['next_cursor'] == {'id': 1}
  57. event = app.state.aliyun_logger.events[0]
  58. assert event['trace_id'] == 'scheduler-request-1'
  59. assert event['data']['request_params'] == payload
  60. assert event['data']['status_code'] == 200
  61. assert event['data']['query_result_count'] == 2
  62. def test_fastapi_validation_and_business_errors_use_same_exit_contract():
  63. class FakeMySQL:
  64. async def fetch_all(self, sql, params):
  65. raise AssertionError('参数错误时不应查询数据库')
  66. app = build_test_app(FakeMySQL())
  67. with TestClient(app, raise_server_exceptions=False) as client:
  68. validation = client.post(API_PATH, json={'unknown_parameter': 1})
  69. business = client.post(
  70. API_PATH,
  71. json={'filters': [{'field': 'like_cnt', 'operator': '>', 'value': 'invalid'}]},
  72. )
  73. wrong_method = client.get(API_PATH)
  74. assert validation.status_code == 400
  75. assert '不支持的参数: unknown_parameter' in validation.json()['msg']
  76. assert business.status_code == 422
  77. assert '必须是数字' in business.json()['msg']
  78. assert wrong_method.status_code == 405
  79. assert wrong_method.json() == {'code': 405, 'msg': 'Method Not Allowed', 'data': None}
  80. assert len(app.state.aliyun_logger.events) == 3
  81. business_event = app.state.aliyun_logger.events[1]
  82. assert business_event['data']['failure_stage'] == 'business_validation'
  83. assert business_event['data']['error_type'] == 'BusinessValidationError'
  84. assert 'Traceback (most recent call last)' in business_event['data']['message']
  85. assert '必须是数字' in business_event['data']['message']
  86. assert all('X-Request-ID' in response.headers for response in (validation, business, wrong_method))
  87. def test_fastapi_health_readiness_and_documentation_routes():
  88. class FakeMySQL:
  89. async def fetch_one(self, sql):
  90. assert sql == 'SELECT 1 AS ok'
  91. return {'ok': 1}
  92. app = build_test_app(FakeMySQL())
  93. with TestClient(app, raise_server_exceptions=False) as client:
  94. health = client.get('/health')
  95. ready = client.get('/ready')
  96. docs = client.get('/openapi.json')
  97. assert health.json()['data']['status'] == 'ok'
  98. assert ready.json()['data']['status'] == 'ready'
  99. assert API_PATH in docs.json()['paths']
  100. def test_fastapi_sls_failure_does_not_change_business_response():
  101. class FakeMySQL:
  102. async def fetch_all(self, sql, params):
  103. return []
  104. class BrokenAliyunLogger:
  105. def logging_batch(self, events):
  106. raise RuntimeError('SLS unavailable')
  107. app = build_test_app(FakeMySQL())
  108. app.state.aliyun_logger = BrokenAliyunLogger()
  109. with TestClient(app, raise_server_exceptions=False) as client:
  110. response = client.post(API_PATH, json={})
  111. assert response.status_code == 200
  112. assert response.json()['code'] == 0
  113. assert any('阿里云API日志上报失败' in message for _, message in app.state.logger.records)