| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192 |
- """API-level tests for POST /api/funnels/query.
- The repository dependency is overridden with an in-memory fake, so these run
- without 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 _rolling_7d(uvs: list[int | None]) -> dict[str, int | None]:
- keys = ["start", "show", "detail", "order", "paid"]
- values: dict[str, int | None] = {f"uv_{k}_7d": uv for k, uv in zip(keys, uvs)}
- for k in keys:
- values[f"uv_{k}_30d"] = None
- return values
- @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_health(client_and_repo) -> None:
- client, _ = client_and_repo
- async with client:
- resp = await client.get("/health")
- assert resp.status_code == 200
- assert resp.json() == {"status": "ok"}
- async def test_query_ready_exact_shape(client_and_repo) -> None:
- client, repo = client_and_repo
- repo.set_rolling("20260623", _rolling_7d([10000, 8200, 5100, 2200, 1800]))
- async with client:
- resp = await client.post("/api/funnels/query", json={"period": "last_7d"})
- assert resp.status_code == 200
- data = resp.json()
- assert data["period"] == "last_7d"
- assert data["snapshot_dt"] == "20260623"
- assert data["data_status"] == "ready"
- assert data["results"][0] == {
- "step_index": 1,
- "name": "启动",
- "event_key": "start",
- "uv": 10000,
- "conversion_rate": None,
- "dropoff_rate": None,
- }
- # Step 3: event_key=detail, name=拼团详情.
- assert data["results"][2]["event_key"] == "detail"
- assert data["results"][2]["name"] == "拼团详情"
- assert data["results"][1]["conversion_rate"] == pytest.approx(0.82)
- assert data["results"][1]["dropoff_rate"] == pytest.approx(0.18)
- assert [r["event_key"] for r in data["results"]] == [
- "start",
- "show",
- "detail",
- "order",
- "paid",
- ]
- assert [r["name"] for r in data["results"]] == [
- "启动",
- "曝光",
- "拼团详情",
- "下单",
- "成功",
- ]
- async def test_query_day_latest(client_and_repo) -> None:
- client, repo = client_and_repo
- repo.set_daily("20260623", _daily([100, 80, 50, 20, 10]))
- async with client:
- resp = await client.post("/api/funnels/query", json={"period": "day"})
- assert resp.status_code == 200
- data = resp.json()
- assert data["snapshot_dt"] == "20260623"
- assert [r["uv"] for r in data["results"]] == [100, 80, 50, 20, 10]
- async def test_query_day_specific_dt(client_and_repo) -> None:
- client, repo = client_and_repo
- yesterday = date.today() - timedelta(days=1)
- older = yesterday - timedelta(days=2)
- repo.set_daily(yesterday.strftime("%Y%m%d"), _daily([100, 80, 50, 20, 10]))
- repo.set_daily(older.strftime("%Y%m%d"), _daily([90, 70, 40, 15, 8]))
- async with client:
- resp = await client.post(
- "/api/funnels/query",
- json={"period": "day", "snapshot_dt": older.isoformat()},
- )
- assert resp.status_code == 200
- data = resp.json()
- assert data["snapshot_dt"] == older.strftime("%Y%m%d")
- assert [r["uv"] for r in data["results"]] == [90, 70, 40, 15, 8]
- async def test_query_day_nonexistent_dt_missing(client_and_repo) -> None:
- client, repo = client_and_repo
- yesterday = date.today() - timedelta(days=1)
- repo.set_daily(yesterday.strftime("%Y%m%d"), _daily([100, 80, 50, 20, 10]))
- far_past = yesterday - timedelta(days=300)
- async with client:
- resp = await client.post(
- "/api/funnels/query",
- json={"period": "day", "snapshot_dt": far_past.isoformat()},
- )
- assert resp.status_code == 200
- data = resp.json()
- assert data["data_status"] == "missing"
- assert data["snapshot_dt"] is None
- async def test_query_day_future_dt_rejected(client_and_repo) -> None:
- client, _ = client_and_repo
- future = (date.today() + timedelta(days=1)).isoformat()
- async with client:
- resp = await client.post(
- "/api/funnels/query",
- json={"period": "day", "snapshot_dt": future},
- )
- assert resp.status_code == 422
- async def test_query_day_today_dt_rejected(client_and_repo) -> None:
- client, _ = client_and_repo
- async with client:
- resp = await client.post(
- "/api/funnels/query",
- json={"period": "day", "snapshot_dt": date.today().isoformat()},
- )
- assert resp.status_code == 422
- async def test_query_rolling_ignores_snapshot_dt(client_and_repo) -> None:
- client, repo = client_and_repo
- repo.set_rolling("20260623", _rolling_7d([700, 560, 350, 140, 70]))
- future = (date.today() + timedelta(days=5)).isoformat()
- async with client:
- resp = await client.post(
- "/api/funnels/query",
- json={"period": "last_7d", "snapshot_dt": future},
- )
- # snapshot_dt ignored for rolling -> not a 422, normal ready response.
- assert resp.status_code == 200
- data = resp.json()
- assert data["data_status"] == "ready"
- assert data["snapshot_dt"] == "20260623"
- async def test_query_missing_no_row(client_and_repo) -> None:
- client, _repo = client_and_repo
- async with client:
- resp = await client.post("/api/funnels/query", json={"period": "last_30d"})
- assert resp.status_code == 200
- data = resp.json()
- assert data["data_status"] == "missing"
- assert data["snapshot_dt"] is None
- assert data["results"] == []
- async def test_query_bad_period_rejected(client_and_repo) -> None:
- client, _ = client_and_repo
- async with client:
- resp = await client.post("/api/funnels/query", json={"period": "yesterday"})
- assert resp.status_code == 422
- async def test_query_missing_period_field_rejected(client_and_repo) -> None:
- client, _ = client_and_repo
- async with client:
- resp = await client.post("/api/funnels/query", json={})
- assert resp.status_code == 422
|