127 lines
5.3 KiB
Python
127 lines
5.3 KiB
Python
from datetime import UTC, date, datetime
|
||
from math import exp
|
||
from types import SimpleNamespace
|
||
|
||
from test_app import login
|
||
|
||
from app.extensions import db
|
||
from app.fitness import effort, history
|
||
from app.heart_rate import zones
|
||
from app.models import Activity, User
|
||
|
||
pytest_plugins = ("test_app",)
|
||
|
||
|
||
def run(seconds=600, histogram=None):
|
||
return SimpleNamespace(
|
||
sport="running",
|
||
duration_s=seconds,
|
||
analysis_summary={"intervals": [{"hr_seconds": histogram or {}}]},
|
||
)
|
||
|
||
|
||
def test_effort_boundaries_weights_and_coverage():
|
||
scored = effort(run(histogram={"120": 200, "140": 200, "160": 200}), [140, 160])
|
||
assert scored["score"] == 20
|
||
assert scored["coverage"] == 100
|
||
assert effort(run(histogram={"140": 479}), [140, 160])["score"] is None
|
||
assert effort(run(histogram={"140": 480}), [140, 160])["score"] == 16
|
||
assert effort(run(histogram={"140": 600}), None)["score"] is None
|
||
assert effort(run(histogram={"140": 600}), None)["coverage"] == 100
|
||
assert effort(run(histogram={"250": 600}), [140, 160])["score"] is None
|
||
five = effort(
|
||
run(histogram={"121": 120, "122": 120, "152": 120, "167": 120, "181": 120}),
|
||
[122, 152, 167, 181],
|
||
)
|
||
assert five["score"] == 30
|
||
assert five["zone_minutes"] == [2, 2, 2, 2, 2]
|
||
|
||
|
||
def test_automatic_zones_update_on_birthday_manual_overrides():
|
||
user = SimpleNamespace(
|
||
timezone="Australia/Sydney", hr_zones={"mode": "auto", "birthday": "1990-10-22"}
|
||
)
|
||
assert zones(user, date(2026, 10, 21))["max_hr"] == 185
|
||
assert zones(user, date(2026, 10, 22))["max_hr"] == 184
|
||
user.hr_zones = {"mode": "max", "max_hr": 195, "birthday": "1990-10-22"}
|
||
assert zones(user, date(2026, 10, 22))["max_hr"] == 195
|
||
user.hr_zones = None
|
||
assert zones(user)["max_hr"] == 190
|
||
user.hr_zones = {"mode": "custom", "boundaries": [122, 152, 167, 181]}
|
||
assert zones(user)["labels"] == ["0–121", "122–151", "152–166", "167–180", "181+"]
|
||
|
||
|
||
def test_history_local_days_sum_decay_missing_and_owner_scope(app):
|
||
with app.app_context():
|
||
db.session.delete(db.session.get(Activity, 1))
|
||
user = db.session.get(User, 1)
|
||
user.hr_zones = {"mode": "custom", "boundaries": [140, 160, 180, 200]}
|
||
for key, owner, hour, histogram in [
|
||
("a", 1, 13, {"140": 600}),
|
||
("b", 1, 14, {"140": 600}),
|
||
("other", 2, 14, {"160": 600}),
|
||
("missing", 1, 15, {}),
|
||
]:
|
||
row = Activity(
|
||
owner_id=owner,
|
||
source_key=key,
|
||
title=key,
|
||
sport="running",
|
||
started_at=datetime(2026, 1, 1, hour, tzinfo=UTC),
|
||
duration_s=600,
|
||
analysis_summary=run(histogram=histogram).analysis_summary,
|
||
)
|
||
db.session.add(row)
|
||
db.session.commit()
|
||
data = history(user, date(2026, 1, 3))
|
||
assert len(data["runs"]) == 3
|
||
assert data["series"][0]["day"] == "2026-01-02"
|
||
first, second = data["series"]
|
||
assert first["load"] is None
|
||
assert first["missing"] == 1
|
||
assert first["fitness"] == round(40 * (1 - exp(-1 / 42)), 2)
|
||
assert first["fatigue"] == round(40 * (1 - exp(-1 / 7)), 2)
|
||
assert first["form"] == round(40 * (exp(-1 / 7) - exp(-1 / 42)), 2)
|
||
assert second["load"] == 0
|
||
assert second["fitness"] == round(40 * (1 - exp(-1 / 42)) * exp(-1 / 42), 2)
|
||
assert second["provisional"]
|
||
assert history(user, date(2026, 1, 3)) == data
|
||
user.hr_zones = {"mode": "custom", "boundaries": [160, 180, 200, 220]}
|
||
recalculated = history(user, date(2026, 1, 3))
|
||
assert recalculated["series"][0]["fitness"] == round(20 * (1 - exp(-1 / 42)), 2)
|
||
assert history(user, date(2026, 1, 1))["series"] == []
|
||
|
||
|
||
def test_fitness_page_private_and_renders_history(app):
|
||
client = app.test_client()
|
||
assert client.get("/fitness").status_code == 302
|
||
assert client.get("/fitness/data").status_code == 302
|
||
login(client)
|
||
response = client.get("/fitness")
|
||
assert response.status_code == 200
|
||
assert len(client.get("/fitness/data").json["runs"]) == 1
|
||
assert b"A quiet morning run" in response.data
|
||
with app.app_context():
|
||
db.session.get(User, 1).hr_bands = [140, 160]
|
||
db.session.commit()
|
||
assert b"fitness-chart" in client.get("/fitness").data
|
||
assert b"Run effort:" in client.get("/activities/1").data
|
||
|
||
|
||
def test_zone_settings_validation_persistence_and_privacy(app):
|
||
client = app.test_client()
|
||
assert client.post("/training/zones", data={"mode": "auto"}).status_code == 302
|
||
login(client)
|
||
valid = {"mode": "custom", "z2": "122", "z3": "152", "z4": "167", "z5": "181"}
|
||
assert client.post("/training/zones", data=valid).status_code == 302
|
||
assert client.post("/training/zones", data={**valid, "z3": "100"}).status_code == 400
|
||
assert client.post("/training/zones", data={"mode": "max", "max_hr": "999"}).status_code == 400
|
||
assert (
|
||
client.post("/training/zones", data={"mode": "auto", "birthday": "2099-01-01"}).status_code
|
||
== 400
|
||
)
|
||
with app.app_context():
|
||
assert db.session.get(User, 1).hr_zones["boundaries"] == [122, 152, 167, 181]
|
||
assert db.session.get(User, 2).hr_zones is None
|
||
assert b"Manual zone boundaries" in client.get("/settings").data
|