| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211 |
- from collections.abc import Iterator
- from datetime import UTC, datetime
- import pytest
- from fastapi.testclient import TestClient
- from zbt.core.config import Settings
- from zbt.main import create_app
- settings = Settings()
- TEST_DATABASES = {
- "insurance_s2_test_core",
- "insurance_s2_test_agent",
- "insurance_s2_test_analytics",
- }
- configured_databases = {
- settings.mysql_core_database,
- settings.mysql_agent_database,
- settings.mysql_analytics_database,
- }
- pytestmark = pytest.mark.skipif(
- settings.app_env != "test" or configured_databases != TEST_DATABASES,
- reason="真实MySQL API集成测试只允许连接三个白名单测试库",
- )
- @pytest.fixture(scope="module")
- def client() -> Iterator[TestClient]:
- application = create_app(
- settings=settings,
- clock=lambda: datetime(2026, 7, 25, tzinfo=UTC),
- )
- with TestClient(application) as test_client:
- yield test_client
- engine = application.state.core_engine
- if engine is not None:
- engine.dispose()
- def h5_login(client: TestClient) -> dict[str, str]:
- response = client.post(
- "/api/v1/h5/auth/login",
- json={"mobile": "18899990001", "code": "147258"},
- )
- assert response.status_code == 200
- return response.json()["data"]["tokens"]
- def admin_login(client: TestClient, username: str) -> dict[str, str]:
- response = client.post(
- "/api/v1/admin/auth/login",
- json={"username": username, "password": "zaq1XSW@"},
- )
- assert response.status_code == 200
- return response.json()["data"]["tokens"]
- def test_h5_authentication_and_current_catalog_use_real_mysql(
- client: TestClient,
- ) -> None:
- request_code = client.post(
- "/api/v1/h5/auth/request-code",
- json={"mobile": "18899990001"},
- )
- assert request_code.status_code == 200
- assert request_code.json()["data"] == {"expires_in": 300}
- assert "code" not in request_code.json()["data"]
- tokens = h5_login(client)
- headers = {"Authorization": f"Bearer {tokens['access_token']}"}
- profile = client.get("/api/v1/h5/me", headers=headers)
- assert profile.status_code == 200
- assert profile.json()["data"]["mobile_masked"] == "188****0001"
- products = client.get("/api/v1/h5/products", headers=headers)
- assert products.status_code == 200
- assert products.json()["data"]["total"] == 4
- assert {item["product_code"] for item in products.json()["data"]["items"]} == {
- "MED-BASIC",
- "MED-UPGRADE",
- "MED-SENIOR",
- "ACC-FAMILY",
- }
- medical = client.get("/api/v1/h5/products?category=MEDICAL", headers=headers)
- accident = client.get("/api/v1/h5/products?category=ACCIDENT", headers=headers)
- unknown = client.get("/api/v1/h5/products?category=UNKNOWN", headers=headers)
- assert medical.json()["data"]["total"] == 3
- assert accident.json()["data"]["total"] == 1
- assert unknown.json()["data"]["total"] == 0
- def test_h5_validation_refresh_rotation_and_logout_use_real_mysql(
- client: TestClient,
- ) -> None:
- invalid_mobile = client.post(
- "/api/v1/h5/auth/login",
- json={"mobile": "1abcdefghij", "code": "147258"},
- )
- assert invalid_mobile.status_code == 400
- assert invalid_mobile.json()["error"]["code"] == "VALIDATION_ERROR"
- wrong_otp = client.post(
- "/api/v1/h5/auth/login",
- json={"mobile": "18899990001", "code": "000000"},
- )
- assert wrong_otp.status_code == 400
- assert wrong_otp.json()["error"]["code"] == "INVALID_OTP"
- tokens = h5_login(client)
- refreshed = client.post(
- "/api/v1/h5/auth/refresh",
- json={"refresh_token": tokens["refresh_token"]},
- )
- assert refreshed.status_code == 200
- refreshed_tokens = refreshed.json()["data"]["tokens"]
- reused = client.post(
- "/api/v1/h5/auth/refresh",
- json={"refresh_token": tokens["refresh_token"]},
- )
- assert reused.status_code == 401
- assert reused.json()["error"]["code"] == "AUTH_REQUIRED"
- headers = {"Authorization": f"Bearer {refreshed_tokens['access_token']}"}
- logout = client.post("/api/v1/h5/auth/logout", headers=headers)
- assert logout.status_code == 200
- after_logout = client.get("/api/v1/h5/me", headers=headers)
- assert after_logout.status_code == 401
- @pytest.mark.parametrize(
- ("username", "role", "scope"),
- [
- ("admin", "SUPER_ADMIN", "ALL"),
- ("operator01", "OPERATOR", "MASKED_ALL"),
- ("reviewer01", "REVIEWER", "READ_ONLY"),
- ("sales_a", "SALESPERSON", "SELF"),
- ("sales_b", "SALESPERSON", "SELF"),
- ],
- )
- def test_admin_role_and_scope_matrix_uses_real_mysql(
- client: TestClient,
- username: str,
- role: str,
- scope: str,
- ) -> None:
- tokens = admin_login(client, username)
- profile = client.get(
- "/api/v1/admin/me",
- headers={"Authorization": f"Bearer {tokens['access_token']}"},
- )
- assert profile.status_code == 200
- assert profile.json()["data"]["username"] == username
- assert profile.json()["data"]["roles"] == [role]
- assert profile.json()["data"]["data_scope"] == scope
- def test_admin_invalid_credentials_do_not_reveal_account_existence(
- client: TestClient,
- ) -> None:
- wrong_password = client.post(
- "/api/v1/admin/auth/login",
- json={"username": "admin", "password": "wrong-password"},
- )
- unknown_user = client.post(
- "/api/v1/admin/auth/login",
- json={"username": "not_exists", "password": "wrong-password"},
- )
- assert wrong_password.status_code == 401
- assert unknown_user.status_code == 401
- assert wrong_password.json()["error"] == unknown_user.json()["error"]
- def test_h5_and_admin_tokens_cannot_cross_audiences(client: TestClient) -> None:
- h5_tokens = h5_login(client)
- admin_tokens = admin_login(client, "admin")
- h5_headers = {"Authorization": f"Bearer {h5_tokens['access_token']}"}
- admin_headers = {"Authorization": f"Bearer {admin_tokens['access_token']}"}
- assert client.get("/api/v1/admin/me", headers=h5_headers).status_code == 401
- assert client.get("/api/v1/h5/me", headers=admin_headers).status_code == 401
- assert client.get("/api/v1/h5/products", headers=admin_headers).status_code == 401
- def test_request_id_and_cors_contract(client: TestClient) -> None:
- health = client.get(
- "/api/v1/system/health/live",
- headers={"X-Request-ID": "req_mysql_integration"},
- )
- assert health.headers["X-Request-ID"] == "req_mysql_integration"
- assert health.json()["meta"]["request_id"] == "req_mysql_integration"
- assert health.json()["meta"]["server_time"].endswith("Z")
- allowed = client.options(
- "/api/v1/h5/auth/login",
- headers={
- "Origin": "http://127.0.0.1:5173",
- "Access-Control-Request-Method": "POST",
- },
- )
- denied = client.options(
- "/api/v1/h5/auth/login",
- headers={
- "Origin": "http://untrusted.example",
- "Access-Control-Request-Method": "POST",
- },
- )
- assert allowed.headers["access-control-allow-origin"] == "http://127.0.0.1:5173"
- assert "access-control-allow-origin" not in denied.headers
|