167 lines
5.9 KiB
Python
167 lines
5.9 KiB
Python
from datetime import UTC, datetime
|
|
|
|
import pytest
|
|
from sqlalchemy import event
|
|
from test_app import add_history, login
|
|
|
|
from app.extensions import db
|
|
from app.models import Activity
|
|
from app.stats import explore, parse_query, query_text
|
|
|
|
pytest_plugins = ("test_app",)
|
|
|
|
|
|
def test_query_round_trip_and_validation():
|
|
query = parse_query(
|
|
{
|
|
"metric": "cadence",
|
|
"secondary": "none",
|
|
"weeks": "26",
|
|
"minimum": "8",
|
|
"maximum": "20",
|
|
"hr_min": "140",
|
|
"hr_max": "150",
|
|
"end": "2026-10-02",
|
|
"compare": "no",
|
|
}
|
|
)
|
|
assert parse_query({"query": query_text(query)}) == query
|
|
tiny = parse_query({"minimum": "0.0000001"})
|
|
assert parse_query({"query": query_text(tiny)}) == tiny
|
|
for args in (
|
|
{"weeks": "105"},
|
|
{"query": ""},
|
|
{"minimum": "nan"},
|
|
{"minimum": "12", "maximum": "4"},
|
|
{"metric": "password"},
|
|
{"end": "2026-02-30"},
|
|
{"query": "runs\nwhere owner_id = 2"},
|
|
{"query": "runs\nsummarize pace\nby week\nby month"},
|
|
):
|
|
with pytest.raises(ValueError):
|
|
parse_query(args)
|
|
|
|
|
|
def test_weighting_missing_data_and_private_scope(app):
|
|
with app.app_context():
|
|
db.session.add_all(
|
|
[
|
|
Activity(
|
|
owner_id=1,
|
|
source_key="stats:long",
|
|
title="Long",
|
|
sport="running",
|
|
started_at=datetime(2026, 9, 29, tzinfo=UTC),
|
|
distance_m=15000,
|
|
duration_s=4500,
|
|
avg_hr=None,
|
|
),
|
|
Activity(
|
|
owner_id=2,
|
|
source_key="stats:private",
|
|
title="Secret",
|
|
sport="running",
|
|
started_at=datetime(2026, 9, 29, tzinfo=UTC),
|
|
distance_m=100000,
|
|
duration_s=10000,
|
|
avg_hr=200,
|
|
),
|
|
Activity(
|
|
owner_id=1,
|
|
source_key="stats:bike",
|
|
title="Bike",
|
|
sport="cycling",
|
|
started_at=datetime(2026, 9, 29, tzinfo=UTC),
|
|
distance_m=100000,
|
|
duration_s=10000,
|
|
),
|
|
]
|
|
)
|
|
db.session.commit()
|
|
statements = []
|
|
|
|
def record(_conn, _cursor, statement, _parameters, _context, _many):
|
|
statements.append(statement)
|
|
|
|
event.listen(db.engine, "before_cursor_execute", record)
|
|
try:
|
|
result = explore(1, "Australia/Sydney", parse_query({"end": "2026-10-02"}))
|
|
finally:
|
|
event.remove(db.engine, "before_cursor_execute", record)
|
|
assert result["count"] == 2
|
|
assert result["cards"][0]["value"] == "5:15"
|
|
assert result["cards"][1]["value"] == "140.0"
|
|
assert result["cards"][1]["coverage"] == 1
|
|
assert all("samples" not in sql and "route_geometry" not in sql for sql in statements)
|
|
|
|
|
|
def test_stats_routes_and_drillthrough(app):
|
|
client = app.test_client()
|
|
assert client.get("/stats").status_code == 302
|
|
assert client.get("/stats/runs").status_code == 302
|
|
login(client)
|
|
response = client.get("/stats?end=2026-10-02")
|
|
assert response.status_code == 200
|
|
assert b"A quiet morning run" in response.data
|
|
assert b"5:00" not in response.data # recorded pace is 6:00/km
|
|
response = client.get("/stats/runs?end=2026-10-02&bucket=28+Sep+2026")
|
|
assert response.status_code == 200
|
|
assert response.json["count"] == 1
|
|
assert response.json["rows"][0]["values"] == ["6:00", "140.0"]
|
|
assert client.get("/stats/runs?bucket=bad").status_code == 400
|
|
assert client.get("/stats?metric=unknown").status_code == 400
|
|
assert b"No runs match" in client.get("/stats?minimum=1000").data
|
|
client.post("/logout")
|
|
login(client, "other")
|
|
assert b"A quiet morning run" not in client.get("/stats?end=2026-10-02").data
|
|
assert client.get("/stats/runs?end=2026-10-02&bucket=28+Sep+2026").json["rows"] == []
|
|
|
|
|
|
def test_local_date_boundary_and_previous_alignment(app):
|
|
with app.app_context():
|
|
db.session.add(
|
|
Activity(
|
|
owner_id=1,
|
|
source_key="stats:boundary",
|
|
title="Local midnight",
|
|
sport="running",
|
|
started_at=datetime(2026, 9, 24, 14, tzinfo=UTC),
|
|
distance_m=1000,
|
|
duration_s=300,
|
|
)
|
|
)
|
|
db.session.add(
|
|
Activity(
|
|
owner_id=1,
|
|
source_key="stats:prior",
|
|
title="Prior",
|
|
sport="running",
|
|
started_at=datetime(2026, 9, 20, tzinfo=UTC),
|
|
distance_m=2000,
|
|
duration_s=800,
|
|
)
|
|
)
|
|
db.session.commit()
|
|
result = explore(1, "Australia/Sydney", parse_query({"weeks": "1", "end": "2026-10-01"}))
|
|
assert result["start"].isoformat() == "2026-09-25"
|
|
assert result["count"] == 2
|
|
assert result["prior_count"] == 1
|
|
assert 400 in result["chart"]["series"][0]["previous"]
|
|
|
|
|
|
def test_evidence_pagination(app):
|
|
with app.app_context():
|
|
add_history(40)
|
|
db.session.commit()
|
|
client = app.test_client()
|
|
login(client)
|
|
args = {"end": "2026-10-15", "group": "month", "bucket": "01 Sep 2026"}
|
|
response = client.get("/stats/runs", query_string=args)
|
|
assert response.status_code == 200
|
|
assert response.json["count"] == 31
|
|
assert len(response.json["rows"]) == 25
|
|
second = client.get("/stats/runs", query_string={**args, "page": 2})
|
|
assert len(second.json["rows"]) == 6
|
|
assert not {r["id"] for r in response.json["rows"]} & {r["id"] for r in second.json["rows"]}
|
|
assert client.get("/stats/runs", query_string={**args, "page": 3}).status_code == 400
|