test_real_mysql_api.py 7.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211
  1. from collections.abc import Iterator
  2. from datetime import UTC, datetime
  3. import pytest
  4. from fastapi.testclient import TestClient
  5. from zbt.core.config import Settings
  6. from zbt.main import create_app
  7. settings = Settings()
  8. TEST_DATABASES = {
  9. "insurance_s1_test_core",
  10. "insurance_s1_test_agent",
  11. "insurance_s1_test_analytics",
  12. }
  13. configured_databases = {
  14. settings.mysql_core_database,
  15. settings.mysql_agent_database,
  16. settings.mysql_analytics_database,
  17. }
  18. pytestmark = pytest.mark.skipif(
  19. settings.app_env != "test" or configured_databases != TEST_DATABASES,
  20. reason="真实MySQL API集成测试只允许连接三个白名单测试库",
  21. )
  22. @pytest.fixture(scope="module")
  23. def client() -> Iterator[TestClient]:
  24. application = create_app(
  25. settings=settings,
  26. clock=lambda: datetime(2026, 7, 25, tzinfo=UTC),
  27. )
  28. with TestClient(application) as test_client:
  29. yield test_client
  30. engine = application.state.core_engine
  31. if engine is not None:
  32. engine.dispose()
  33. def h5_login(client: TestClient) -> dict[str, str]:
  34. response = client.post(
  35. "/api/v1/h5/auth/login",
  36. json={"mobile": "18899990001", "code": "147258"},
  37. )
  38. assert response.status_code == 200
  39. return response.json()["data"]["tokens"]
  40. def admin_login(client: TestClient, username: str) -> dict[str, str]:
  41. response = client.post(
  42. "/api/v1/admin/auth/login",
  43. json={"username": username, "password": "zaq1XSW@"},
  44. )
  45. assert response.status_code == 200
  46. return response.json()["data"]["tokens"]
  47. def test_h5_authentication_and_current_catalog_use_real_mysql(
  48. client: TestClient,
  49. ) -> None:
  50. request_code = client.post(
  51. "/api/v1/h5/auth/request-code",
  52. json={"mobile": "18899990001"},
  53. )
  54. assert request_code.status_code == 200
  55. assert request_code.json()["data"] == {"expires_in": 300}
  56. assert "code" not in request_code.json()["data"]
  57. tokens = h5_login(client)
  58. headers = {"Authorization": f"Bearer {tokens['access_token']}"}
  59. profile = client.get("/api/v1/h5/me", headers=headers)
  60. assert profile.status_code == 200
  61. assert profile.json()["data"]["mobile_masked"] == "188****0001"
  62. products = client.get("/api/v1/h5/products", headers=headers)
  63. assert products.status_code == 200
  64. assert products.json()["data"]["total"] == 4
  65. assert {item["product_code"] for item in products.json()["data"]["items"]} == {
  66. "MED-BASIC",
  67. "MED-UPGRADE",
  68. "MED-SENIOR",
  69. "ACC-FAMILY",
  70. }
  71. medical = client.get("/api/v1/h5/products?category=MEDICAL", headers=headers)
  72. accident = client.get("/api/v1/h5/products?category=ACCIDENT", headers=headers)
  73. unknown = client.get("/api/v1/h5/products?category=UNKNOWN", headers=headers)
  74. assert medical.json()["data"]["total"] == 3
  75. assert accident.json()["data"]["total"] == 1
  76. assert unknown.json()["data"]["total"] == 0
  77. def test_h5_validation_refresh_rotation_and_logout_use_real_mysql(
  78. client: TestClient,
  79. ) -> None:
  80. invalid_mobile = client.post(
  81. "/api/v1/h5/auth/login",
  82. json={"mobile": "1abcdefghij", "code": "147258"},
  83. )
  84. assert invalid_mobile.status_code == 400
  85. assert invalid_mobile.json()["error"]["code"] == "VALIDATION_ERROR"
  86. wrong_otp = client.post(
  87. "/api/v1/h5/auth/login",
  88. json={"mobile": "18899990001", "code": "000000"},
  89. )
  90. assert wrong_otp.status_code == 400
  91. assert wrong_otp.json()["error"]["code"] == "INVALID_OTP"
  92. tokens = h5_login(client)
  93. refreshed = client.post(
  94. "/api/v1/h5/auth/refresh",
  95. json={"refresh_token": tokens["refresh_token"]},
  96. )
  97. assert refreshed.status_code == 200
  98. refreshed_tokens = refreshed.json()["data"]["tokens"]
  99. reused = client.post(
  100. "/api/v1/h5/auth/refresh",
  101. json={"refresh_token": tokens["refresh_token"]},
  102. )
  103. assert reused.status_code == 401
  104. assert reused.json()["error"]["code"] == "AUTH_REQUIRED"
  105. headers = {"Authorization": f"Bearer {refreshed_tokens['access_token']}"}
  106. logout = client.post("/api/v1/h5/auth/logout", headers=headers)
  107. assert logout.status_code == 200
  108. after_logout = client.get("/api/v1/h5/me", headers=headers)
  109. assert after_logout.status_code == 401
  110. @pytest.mark.parametrize(
  111. ("username", "role", "scope"),
  112. [
  113. ("admin", "SUPER_ADMIN", "ALL"),
  114. ("operator01", "OPERATOR", "MASKED_ALL"),
  115. ("reviewer01", "REVIEWER", "READ_ONLY"),
  116. ("sales_a", "SALESPERSON", "SELF"),
  117. ("sales_b", "SALESPERSON", "SELF"),
  118. ],
  119. )
  120. def test_admin_role_and_scope_matrix_uses_real_mysql(
  121. client: TestClient,
  122. username: str,
  123. role: str,
  124. scope: str,
  125. ) -> None:
  126. tokens = admin_login(client, username)
  127. profile = client.get(
  128. "/api/v1/admin/me",
  129. headers={"Authorization": f"Bearer {tokens['access_token']}"},
  130. )
  131. assert profile.status_code == 200
  132. assert profile.json()["data"]["username"] == username
  133. assert profile.json()["data"]["roles"] == [role]
  134. assert profile.json()["data"]["data_scope"] == scope
  135. def test_admin_invalid_credentials_do_not_reveal_account_existence(
  136. client: TestClient,
  137. ) -> None:
  138. wrong_password = client.post(
  139. "/api/v1/admin/auth/login",
  140. json={"username": "admin", "password": "wrong-password"},
  141. )
  142. unknown_user = client.post(
  143. "/api/v1/admin/auth/login",
  144. json={"username": "not_exists", "password": "wrong-password"},
  145. )
  146. assert wrong_password.status_code == 401
  147. assert unknown_user.status_code == 401
  148. assert wrong_password.json()["error"] == unknown_user.json()["error"]
  149. def test_h5_and_admin_tokens_cannot_cross_audiences(client: TestClient) -> None:
  150. h5_tokens = h5_login(client)
  151. admin_tokens = admin_login(client, "admin")
  152. h5_headers = {"Authorization": f"Bearer {h5_tokens['access_token']}"}
  153. admin_headers = {"Authorization": f"Bearer {admin_tokens['access_token']}"}
  154. assert client.get("/api/v1/admin/me", headers=h5_headers).status_code == 401
  155. assert client.get("/api/v1/h5/me", headers=admin_headers).status_code == 401
  156. assert client.get("/api/v1/h5/products", headers=admin_headers).status_code == 401
  157. def test_request_id_and_cors_contract(client: TestClient) -> None:
  158. health = client.get(
  159. "/api/v1/system/health/live",
  160. headers={"X-Request-ID": "req_mysql_integration"},
  161. )
  162. assert health.headers["X-Request-ID"] == "req_mysql_integration"
  163. assert health.json()["meta"]["request_id"] == "req_mysql_integration"
  164. assert health.json()["meta"]["server_time"].endswith("Z")
  165. allowed = client.options(
  166. "/api/v1/h5/auth/login",
  167. headers={
  168. "Origin": "http://127.0.0.1:5173",
  169. "Access-Control-Request-Method": "POST",
  170. },
  171. )
  172. denied = client.options(
  173. "/api/v1/h5/auth/login",
  174. headers={
  175. "Origin": "http://untrusted.example",
  176. "Access-Control-Request-Method": "POST",
  177. },
  178. )
  179. assert allowed.headers["access-control-allow-origin"] == "http://127.0.0.1:5173"
  180. assert "access-control-allow-origin" not in denied.headers