test_trend.py 4.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121
  1. """API-level tests for POST /api/funnels/trend (按日折线).
  2. Repository dependency overridden with the in-memory fake — no Postgres.
  3. """
  4. from __future__ import annotations
  5. from datetime import date, timedelta
  6. import pytest
  7. from httpx import ASGITransport, AsyncClient
  8. from app.api.funnels import get_repository
  9. from app.main import app
  10. from tests.conftest import FakeRepository
  11. def _daily(uvs: list[int | None]) -> dict[str, int | None]:
  12. keys = ["start", "show", "detail", "order", "paid"]
  13. return {f"uv_{k}": uv for k, uv in zip(keys, uvs)}
  14. def _dt(days_ago: int) -> str:
  15. return (date.today() - timedelta(days=days_ago)).strftime("%Y%m%d")
  16. @pytest.fixture
  17. def client_and_repo():
  18. repo = FakeRepository()
  19. app.dependency_overrides[get_repository] = lambda: repo
  20. transport = ASGITransport(app=app)
  21. client = AsyncClient(transport=transport, base_url="http://test")
  22. yield client, repo
  23. app.dependency_overrides.clear()
  24. async def test_trend_default_window_returns_ascending_points(client_and_repo) -> None:
  25. client, repo = client_and_repo
  26. # 3 active days ending yesterday.
  27. for d in (3, 2, 1):
  28. repo.set_daily(_dt(d), _daily([1000 - d, 800 - d, 500 - d, 200 - d, 100 - d]))
  29. async with client:
  30. resp = await client.post("/api/funnels/trend", json={})
  31. assert resp.status_code == 200
  32. body = resp.json()
  33. assert body["data_status"] == "ready"
  34. dts = [p["dt"] for p in body["points"]]
  35. assert dts == [_dt(3), _dt(2), _dt(1)] # ascending
  36. assert body["start_dt"] == _dt(3)
  37. assert body["end_dt"] == _dt(1)
  38. # each point carries the fixed 5-step funnel
  39. assert all(len(p["results"]) == 5 for p in body["points"])
  40. async def test_trend_trims_leading_zero_days(client_and_repo) -> None:
  41. client, repo = client_and_repo
  42. repo.set_daily(_dt(5), _daily([0, 0, 0, 0, 0])) # pre-launch
  43. repo.set_daily(_dt(4), _daily([0, 0, 0, 0, 0])) # pre-launch
  44. repo.set_daily(_dt(3), _daily([900, 700, 400, 150, 80]))
  45. repo.set_daily(_dt(2), _daily([950, 720, 410, 160, 90]))
  46. repo.set_daily(_dt(1), _daily([0, 0, 0, 0, 0])) # mid/trailing zero — kept
  47. async with client:
  48. resp = await client.post("/api/funnels/trend", json={})
  49. body = resp.json()
  50. dts = [p["dt"] for p in body["points"]]
  51. assert dts == [_dt(3), _dt(2), _dt(1)] # leading zeros dropped, trailing zero kept
  52. assert body["start_dt"] == _dt(3)
  53. async def test_trend_conversion_matches_funnel_口径(client_and_repo) -> None:
  54. client, repo = client_and_repo
  55. repo.set_daily(_dt(1), _daily([1000, 800, 500, 200, 100]))
  56. async with client:
  57. resp = await client.post("/api/funnels/trend", json={})
  58. rows = resp.json()["points"][0]["results"]
  59. assert rows[0]["conversion_rate"] is None # step 1
  60. assert rows[1]["conversion_rate"] == pytest.approx(800 / 1000)
  61. assert rows[3]["conversion_rate"] == pytest.approx(200 / 500)
  62. async def test_trend_custom_range_subsets(client_and_repo) -> None:
  63. client, repo = client_and_repo
  64. for d in (4, 3, 2, 1):
  65. repo.set_daily(_dt(d), _daily([1000, 800, 500, 200, 100]))
  66. start = (date.today() - timedelta(days=3)).isoformat()
  67. end = (date.today() - timedelta(days=2)).isoformat()
  68. async with client:
  69. resp = await client.post(
  70. "/api/funnels/trend", json={"start_dt": start, "end_dt": end}
  71. )
  72. dts = [p["dt"] for p in resp.json()["points"]]
  73. assert dts == [_dt(3), _dt(2)]
  74. async def test_trend_no_data_is_missing(client_and_repo) -> None:
  75. client, _ = client_and_repo
  76. async with client:
  77. resp = await client.post("/api/funnels/trend", json={})
  78. body = resp.json()
  79. assert body["data_status"] == "missing"
  80. assert body["points"] == []
  81. assert body["start_dt"] is None and body["end_dt"] is None
  82. async def test_trend_future_end_rejected(client_and_repo) -> None:
  83. client, _ = client_and_repo
  84. today = date.today().isoformat()
  85. async with client:
  86. resp = await client.post("/api/funnels/trend", json={"end_dt": today})
  87. assert resp.status_code == 422
  88. async def test_trend_inverted_range_rejected(client_and_repo) -> None:
  89. client, _ = client_and_repo
  90. start = (date.today() - timedelta(days=1)).isoformat()
  91. end = (date.today() - timedelta(days=5)).isoformat()
  92. async with client:
  93. resp = await client.post(
  94. "/api/funnels/trend", json={"start_dt": start, "end_dt": end}
  95. )
  96. assert resp.status_code == 422