| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121 |
- """API-level tests for POST /api/funnels/trend (按日折线).
- Repository dependency overridden with the in-memory fake — no Postgres.
- """
- from __future__ import annotations
- from datetime import date, timedelta
- import pytest
- from httpx import ASGITransport, AsyncClient
- from app.api.funnels import get_repository
- from app.main import app
- from tests.conftest import FakeRepository
- def _daily(uvs: list[int | None]) -> dict[str, int | None]:
- keys = ["start", "show", "detail", "order", "paid"]
- return {f"uv_{k}": uv for k, uv in zip(keys, uvs)}
- def _dt(days_ago: int) -> str:
- return (date.today() - timedelta(days=days_ago)).strftime("%Y%m%d")
- @pytest.fixture
- def client_and_repo():
- repo = FakeRepository()
- app.dependency_overrides[get_repository] = lambda: repo
- transport = ASGITransport(app=app)
- client = AsyncClient(transport=transport, base_url="http://test")
- yield client, repo
- app.dependency_overrides.clear()
- async def test_trend_default_window_returns_ascending_points(client_and_repo) -> None:
- client, repo = client_and_repo
- # 3 active days ending yesterday.
- for d in (3, 2, 1):
- repo.set_daily(_dt(d), _daily([1000 - d, 800 - d, 500 - d, 200 - d, 100 - d]))
- async with client:
- resp = await client.post("/api/funnels/trend", json={})
- assert resp.status_code == 200
- body = resp.json()
- assert body["data_status"] == "ready"
- dts = [p["dt"] for p in body["points"]]
- assert dts == [_dt(3), _dt(2), _dt(1)] # ascending
- assert body["start_dt"] == _dt(3)
- assert body["end_dt"] == _dt(1)
- # each point carries the fixed 5-step funnel
- assert all(len(p["results"]) == 5 for p in body["points"])
- async def test_trend_trims_leading_zero_days(client_and_repo) -> None:
- client, repo = client_and_repo
- repo.set_daily(_dt(5), _daily([0, 0, 0, 0, 0])) # pre-launch
- repo.set_daily(_dt(4), _daily([0, 0, 0, 0, 0])) # pre-launch
- repo.set_daily(_dt(3), _daily([900, 700, 400, 150, 80]))
- repo.set_daily(_dt(2), _daily([950, 720, 410, 160, 90]))
- repo.set_daily(_dt(1), _daily([0, 0, 0, 0, 0])) # mid/trailing zero — kept
- async with client:
- resp = await client.post("/api/funnels/trend", json={})
- body = resp.json()
- dts = [p["dt"] for p in body["points"]]
- assert dts == [_dt(3), _dt(2), _dt(1)] # leading zeros dropped, trailing zero kept
- assert body["start_dt"] == _dt(3)
- async def test_trend_conversion_matches_funnel_口径(client_and_repo) -> None:
- client, repo = client_and_repo
- repo.set_daily(_dt(1), _daily([1000, 800, 500, 200, 100]))
- async with client:
- resp = await client.post("/api/funnels/trend", json={})
- rows = resp.json()["points"][0]["results"]
- assert rows[0]["conversion_rate"] is None # step 1
- assert rows[1]["conversion_rate"] == pytest.approx(800 / 1000)
- assert rows[3]["conversion_rate"] == pytest.approx(200 / 500)
- async def test_trend_custom_range_subsets(client_and_repo) -> None:
- client, repo = client_and_repo
- for d in (4, 3, 2, 1):
- repo.set_daily(_dt(d), _daily([1000, 800, 500, 200, 100]))
- start = (date.today() - timedelta(days=3)).isoformat()
- end = (date.today() - timedelta(days=2)).isoformat()
- async with client:
- resp = await client.post(
- "/api/funnels/trend", json={"start_dt": start, "end_dt": end}
- )
- dts = [p["dt"] for p in resp.json()["points"]]
- assert dts == [_dt(3), _dt(2)]
- async def test_trend_no_data_is_missing(client_and_repo) -> None:
- client, _ = client_and_repo
- async with client:
- resp = await client.post("/api/funnels/trend", json={})
- body = resp.json()
- assert body["data_status"] == "missing"
- assert body["points"] == []
- assert body["start_dt"] is None and body["end_dt"] is None
- async def test_trend_future_end_rejected(client_and_repo) -> None:
- client, _ = client_and_repo
- today = date.today().isoformat()
- async with client:
- resp = await client.post("/api/funnels/trend", json={"end_dt": today})
- assert resp.status_code == 422
- async def test_trend_inverted_range_rejected(client_and_repo) -> None:
- client, _ = client_and_repo
- start = (date.today() - timedelta(days=1)).isoformat()
- end = (date.today() - timedelta(days=5)).isoformat()
- async with client:
- resp = await client.post(
- "/api/funnels/trend", json={"start_dt": start, "end_dt": end}
- )
- assert resp.status_code == 422
|