test_mock_api.py 4.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125
  1. """Mock API(API_PULL 对端)行为守卫:cursor 过滤、鉴权模式、错误注入、响应路径、审计脱敏。
  2. 用 FastAPI TestClient 进程内验证,不占用端口、不需要 Ai-DOP 在线。
  3. 契约对齐 MdpApiPullExecutor.cs:GET 单次请求、data.list、dedup_key_path=bizKey、?cursor=。
  4. """
  5. from __future__ import annotations
  6. import base64
  7. import pytest
  8. fastapi_testclient = pytest.importorskip("fastapi.testclient")
  9. TestClient = fastapi_testclient.TestClient
  10. from providers import mock_api # noqa: E402
  11. @pytest.fixture()
  12. def client():
  13. state = mock_api.MockApiState()
  14. state.settings.auth_mode = "NONE"
  15. return TestClient(mock_api.create_app(state)), state
  16. def test_health_and_endpoints_listed(client):
  17. c, _ = client
  18. assert c.get("/health").json()["status"] == "UP"
  19. body = c.get("/__endpoints").json()
  20. assert "/api/item" in body["endpoints"]
  21. def test_data_list_shape_and_dedup_key(client):
  22. c, _ = client
  23. body = c.get("/api/item").json()
  24. rows = body["data"]["list"]
  25. assert body["code"] == 0
  26. assert rows and all("bizKey" in r for r in rows)
  27. def test_cursor_returns_only_greater_bizkeys(client):
  28. c, _ = client
  29. rows = c.get("/api/sales-order-entry").json()["data"]["list"]
  30. keys = [r["bizKey"] for r in rows]
  31. assert keys == sorted(keys)
  32. cut = keys[0]
  33. # cursor 是查询参数,必须 URL 编码:样例 bizKey 含 '#'(片段分隔符),
  34. # 直接拼接 URL 会被截断,故用 params= 让 httpx 正确编码。
  35. after = c.get("/api/sales-order-entry", params={"cursor": cut}).json()["data"]["list"]
  36. assert [r["bizKey"] for r in after] == [k for k in keys if k > cut]
  37. def test_unknown_path_is_404(client):
  38. c, _ = client
  39. assert c.get("/api/not-registered-object").status_code == 404
  40. def test_token_auth_rejects_and_accepts(client):
  41. c, state = client
  42. state.settings.auth_mode = "TOKEN"
  43. state.settings.token = "sim-token"
  44. assert c.get("/api/item").status_code == 401
  45. assert c.get("/api/item", headers={"Authorization": "Bearer wrong"}).status_code == 401
  46. assert c.get("/api/item", headers={"Authorization": "Bearer sim-token"}).status_code == 200
  47. def test_basic_auth(client):
  48. c, state = client
  49. state.settings.auth_mode = "BASIC"
  50. state.settings.basic_user = "sim"
  51. state.settings.basic_password = "pw"
  52. token = base64.b64encode(b"sim:pw").decode()
  53. assert c.get("/api/item").status_code == 401
  54. assert c.get("/api/item", headers={"Authorization": f"Basic {token}"}).status_code == 200
  55. def test_apikey_auth(client):
  56. c, state = client
  57. state.settings.auth_mode = "APIKEY"
  58. state.settings.api_key_header = "X-Api-Key"
  59. state.settings.api_key_value = "k1"
  60. assert c.get("/api/item").status_code == 401
  61. assert c.get("/api/item", headers={"X-Api-Key": "k1"}).status_code == 200
  62. @pytest.mark.parametrize("status_code", [401, 403, 429, 500])
  63. def test_forced_http_errors(client, status_code):
  64. c, state = client
  65. state.settings.force_status = status_code
  66. assert c.get("/api/item").status_code == status_code
  67. def test_response_path_is_switchable(client):
  68. c, state = client
  69. state.settings.response_path = "result.rows"
  70. body = c.get("/api/item").json()
  71. assert isinstance(body["result"]["rows"], list)
  72. def test_override_rows_win_and_can_be_cleared(client):
  73. c, state = client
  74. state.overrides["/api/item"] = [{"bizKey": "SIM-OVERRIDE-2"}, {"bizKey": "SIM-OVERRIDE-1"}]
  75. rows = c.get("/api/item").json()["data"]["list"]
  76. assert [r["bizKey"] for r in rows] == ["SIM-OVERRIDE-1", "SIM-OVERRIDE-2"]
  77. state.overrides.clear()
  78. assert c.get("/api/item").json()["data"]["list"]
  79. def test_empty_result_is_still_success(client):
  80. c, state = client
  81. state.overrides["/api/item"] = []
  82. body = c.get("/api/item").json()
  83. assert body["data"]["list"] == []
  84. assert body["code"] == 0
  85. def test_audit_redacts_authorization_and_token(client):
  86. c, state = client
  87. state.settings.auth_mode = "TOKEN"
  88. state.settings.token = "super-secret-token"
  89. c.get("/api/item", headers={"Authorization": "Bearer super-secret-token"})
  90. entries = state.audit
  91. assert entries
  92. dumped = str(entries)
  93. assert "super-secret-token" not in dumped
  94. assert entries[-1]["headers"]["authorization"] == "***REDACTED***"