test_api.py 6.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192
  1. """API-level tests for POST /api/funnels/query.
  2. The repository dependency is overridden with an in-memory fake, so these run
  3. without Postgres.
  4. """
  5. from __future__ import annotations
  6. from datetime import date, timedelta
  7. import pytest
  8. from httpx import ASGITransport, AsyncClient
  9. from app.api.funnels import get_repository
  10. from app.main import app
  11. from tests.conftest import FakeRepository
  12. def _daily(uvs: list[int | None]) -> dict[str, int | None]:
  13. keys = ["start", "show", "detail", "order", "paid"]
  14. return {f"uv_{k}": uv for k, uv in zip(keys, uvs)}
  15. def _rolling_7d(uvs: list[int | None]) -> dict[str, int | None]:
  16. keys = ["start", "show", "detail", "order", "paid"]
  17. values: dict[str, int | None] = {f"uv_{k}_7d": uv for k, uv in zip(keys, uvs)}
  18. for k in keys:
  19. values[f"uv_{k}_30d"] = None
  20. return values
  21. @pytest.fixture
  22. def client_and_repo():
  23. repo = FakeRepository()
  24. app.dependency_overrides[get_repository] = lambda: repo
  25. transport = ASGITransport(app=app)
  26. client = AsyncClient(transport=transport, base_url="http://test")
  27. yield client, repo
  28. app.dependency_overrides.clear()
  29. async def test_health(client_and_repo) -> None:
  30. client, _ = client_and_repo
  31. async with client:
  32. resp = await client.get("/health")
  33. assert resp.status_code == 200
  34. assert resp.json() == {"status": "ok"}
  35. async def test_query_ready_exact_shape(client_and_repo) -> None:
  36. client, repo = client_and_repo
  37. repo.set_rolling("20260623", _rolling_7d([10000, 8200, 5100, 2200, 1800]))
  38. async with client:
  39. resp = await client.post("/api/funnels/query", json={"period": "last_7d"})
  40. assert resp.status_code == 200
  41. data = resp.json()
  42. assert data["period"] == "last_7d"
  43. assert data["snapshot_dt"] == "20260623"
  44. assert data["data_status"] == "ready"
  45. assert data["results"][0] == {
  46. "step_index": 1,
  47. "name": "启动",
  48. "event_key": "start",
  49. "uv": 10000,
  50. "conversion_rate": None,
  51. "dropoff_rate": None,
  52. }
  53. # Step 3: event_key=detail, name=拼团详情.
  54. assert data["results"][2]["event_key"] == "detail"
  55. assert data["results"][2]["name"] == "拼团详情"
  56. assert data["results"][1]["conversion_rate"] == pytest.approx(0.82)
  57. assert data["results"][1]["dropoff_rate"] == pytest.approx(0.18)
  58. assert [r["event_key"] for r in data["results"]] == [
  59. "start",
  60. "show",
  61. "detail",
  62. "order",
  63. "paid",
  64. ]
  65. assert [r["name"] for r in data["results"]] == [
  66. "启动",
  67. "曝光",
  68. "拼团详情",
  69. "下单",
  70. "成功",
  71. ]
  72. async def test_query_day_latest(client_and_repo) -> None:
  73. client, repo = client_and_repo
  74. repo.set_daily("20260623", _daily([100, 80, 50, 20, 10]))
  75. async with client:
  76. resp = await client.post("/api/funnels/query", json={"period": "day"})
  77. assert resp.status_code == 200
  78. data = resp.json()
  79. assert data["snapshot_dt"] == "20260623"
  80. assert [r["uv"] for r in data["results"]] == [100, 80, 50, 20, 10]
  81. async def test_query_day_specific_dt(client_and_repo) -> None:
  82. client, repo = client_and_repo
  83. yesterday = date.today() - timedelta(days=1)
  84. older = yesterday - timedelta(days=2)
  85. repo.set_daily(yesterday.strftime("%Y%m%d"), _daily([100, 80, 50, 20, 10]))
  86. repo.set_daily(older.strftime("%Y%m%d"), _daily([90, 70, 40, 15, 8]))
  87. async with client:
  88. resp = await client.post(
  89. "/api/funnels/query",
  90. json={"period": "day", "snapshot_dt": older.isoformat()},
  91. )
  92. assert resp.status_code == 200
  93. data = resp.json()
  94. assert data["snapshot_dt"] == older.strftime("%Y%m%d")
  95. assert [r["uv"] for r in data["results"]] == [90, 70, 40, 15, 8]
  96. async def test_query_day_nonexistent_dt_missing(client_and_repo) -> None:
  97. client, repo = client_and_repo
  98. yesterday = date.today() - timedelta(days=1)
  99. repo.set_daily(yesterday.strftime("%Y%m%d"), _daily([100, 80, 50, 20, 10]))
  100. far_past = yesterday - timedelta(days=300)
  101. async with client:
  102. resp = await client.post(
  103. "/api/funnels/query",
  104. json={"period": "day", "snapshot_dt": far_past.isoformat()},
  105. )
  106. assert resp.status_code == 200
  107. data = resp.json()
  108. assert data["data_status"] == "missing"
  109. assert data["snapshot_dt"] is None
  110. async def test_query_day_future_dt_rejected(client_and_repo) -> None:
  111. client, _ = client_and_repo
  112. future = (date.today() + timedelta(days=1)).isoformat()
  113. async with client:
  114. resp = await client.post(
  115. "/api/funnels/query",
  116. json={"period": "day", "snapshot_dt": future},
  117. )
  118. assert resp.status_code == 422
  119. async def test_query_day_today_dt_rejected(client_and_repo) -> None:
  120. client, _ = client_and_repo
  121. async with client:
  122. resp = await client.post(
  123. "/api/funnels/query",
  124. json={"period": "day", "snapshot_dt": date.today().isoformat()},
  125. )
  126. assert resp.status_code == 422
  127. async def test_query_rolling_ignores_snapshot_dt(client_and_repo) -> None:
  128. client, repo = client_and_repo
  129. repo.set_rolling("20260623", _rolling_7d([700, 560, 350, 140, 70]))
  130. future = (date.today() + timedelta(days=5)).isoformat()
  131. async with client:
  132. resp = await client.post(
  133. "/api/funnels/query",
  134. json={"period": "last_7d", "snapshot_dt": future},
  135. )
  136. # snapshot_dt ignored for rolling -> not a 422, normal ready response.
  137. assert resp.status_code == 200
  138. data = resp.json()
  139. assert data["data_status"] == "ready"
  140. assert data["snapshot_dt"] == "20260623"
  141. async def test_query_missing_no_row(client_and_repo) -> None:
  142. client, _repo = client_and_repo
  143. async with client:
  144. resp = await client.post("/api/funnels/query", json={"period": "last_30d"})
  145. assert resp.status_code == 200
  146. data = resp.json()
  147. assert data["data_status"] == "missing"
  148. assert data["snapshot_dt"] is None
  149. assert data["results"] == []
  150. async def test_query_bad_period_rejected(client_and_repo) -> None:
  151. client, _ = client_and_repo
  152. async with client:
  153. resp = await client.post("/api/funnels/query", json={"period": "yesterday"})
  154. assert resp.status_code == 422
  155. async def test_query_missing_period_field_rejected(client_and_repo) -> None:
  156. client, _ = client_and_repo
  157. async with client:
  158. resp = await client.post("/api/funnels/query", json={})
  159. assert resp.status_code == 422