feat: fix workout taxonomy to 7 types, add pace/HR-zone fields to the API

This commit is contained in:
2026-07-17 19:09:55 +02:00
parent 9c42931c9e
commit 078c87c413
3 changed files with 133 additions and 82 deletions

View File

@@ -58,51 +58,69 @@ func TestHealth(t *testing.T) {
}
}
func TestWorkoutKindCRUD(t *testing.T) {
func TestWorkoutKindList_ReturnsSevenSeededTypesWithPaceFields(t *testing.T) {
s, _ := newTestServer(t)
router := s.Router()
createBody := map[string]any{
"name": "Tempo",
"rule": json.RawMessage(`{"match":"all","conditions":[{"metric":"avg_pace_sec_per_km","op":"between","value":[270,300]}]}`),
}
rec := doJSON(t, router, http.MethodPost, "/api/workout-kinds/", createBody)
if rec.Code != http.StatusCreated {
t.Fatalf("create status = %d, body = %s", rec.Code, rec.Body.String())
}
var created store.WorkoutKind
if err := json.Unmarshal(rec.Body.Bytes(), &created); err != nil {
t.Fatalf("unmarshal created kind: %v", err)
}
if created.ID == 0 {
t.Fatal("expected non-zero id")
}
rec = doJSON(t, router, http.MethodGet, "/api/workout-kinds/", nil)
rec := doJSON(t, s.Router(), http.MethodGet, "/api/workout-kinds/", nil)
if rec.Code != http.StatusOK {
t.Fatalf("list status = %d", rec.Code)
t.Fatalf("status = %d", rec.Code)
}
var kinds []store.WorkoutKind
json.Unmarshal(rec.Body.Bytes(), &kinds)
if len(kinds) != 1 {
t.Fatalf("expected 1 kind, got %d", len(kinds))
var kinds []workoutKindResponse
if err := json.Unmarshal(rec.Body.Bytes(), &kinds); err != nil {
t.Fatalf("unmarshal: %v", err)
}
rec = doJSON(t, router, http.MethodDelete, "/api/workout-kinds/"+itoa(created.ID), nil)
if rec.Code != http.StatusNoContent {
t.Fatalf("delete status = %d", rec.Code)
if len(kinds) != 7 {
t.Fatalf("expected 7 seeded kinds, got %d", len(kinds))
}
rec = doJSON(t, router, http.MethodGet, "/api/workout-kinds/", nil)
json.Unmarshal(rec.Body.Bytes(), &kinds)
if len(kinds) != 0 {
t.Fatalf("expected 0 active kinds after soft delete, got %d", len(kinds))
for _, k := range kinds {
if k.PaceMinSecPerKm != nil || k.PaceMaxSecPerKm != nil || k.ExpectedHRZone != nil {
t.Errorf("expected freshly-seeded kind %q to have nil pace fields, got %+v", k.Name, k)
}
}
}
func TestWorkoutKindCreate_RejectsInvalidRule(t *testing.T) {
func TestWorkoutKindUpdate_SetsRuleAndPace(t *testing.T) {
s, _ := newTestServer(t)
rec := doJSON(t, s.Router(), http.MethodPost, "/api/workout-kinds/", map[string]any{
"name": "Bad",
router := s.Router()
rec := doJSON(t, router, http.MethodGet, "/api/workout-kinds/", nil)
var kinds []workoutKindResponse
json.Unmarshal(rec.Body.Bytes(), &kinds)
target := kinds[0]
minPace, maxPace, zone := 330.0, 420.0, 2
body := map[string]any{
"name": target.Name,
"rule": json.RawMessage(`{"match":"all","conditions":[{"metric":"avg_pace_sec_per_km","op":"between","value":[270,300]}]}`),
"pace_min_sec_per_km": minPace,
"pace_max_sec_per_km": maxPace,
"expected_hr_zone": zone,
}
rec = doJSON(t, router, http.MethodPut, "/api/workout-kinds/"+itoa(target.ID), body)
if rec.Code != http.StatusOK {
t.Fatalf("update status = %d, body = %s", rec.Code, rec.Body.String())
}
rec = doJSON(t, router, http.MethodGet, "/api/workout-kinds/"+itoa(target.ID), nil)
var updated workoutKindResponse
json.Unmarshal(rec.Body.Bytes(), &updated)
if updated.PaceMinSecPerKm == nil || *updated.PaceMinSecPerKm != 330 {
t.Errorf("PaceMinSecPerKm = %v, want 330", updated.PaceMinSecPerKm)
}
if updated.ExpectedHRZone == nil || *updated.ExpectedHRZone != 2 {
t.Errorf("ExpectedHRZone = %v, want 2", updated.ExpectedHRZone)
}
}
func TestWorkoutKindUpdate_RejectsInvalidRule(t *testing.T) {
s, _ := newTestServer(t)
router := s.Router()
rec := doJSON(t, router, http.MethodGet, "/api/workout-kinds/", nil)
var kinds []workoutKindResponse
json.Unmarshal(rec.Body.Bytes(), &kinds)
rec = doJSON(t, router, http.MethodPut, "/api/workout-kinds/"+itoa(kinds[0].ID), map[string]any{
"name": kinds[0].Name,
"rule": json.RawMessage(`{"match":"xor","conditions":[]}`),
})
if rec.Code != http.StatusBadRequest {
@@ -110,6 +128,24 @@ func TestWorkoutKindCreate_RejectsInvalidRule(t *testing.T) {
}
}
func TestWorkoutKindUpdate_RejectsInvalidHRZone(t *testing.T) {
s, _ := newTestServer(t)
router := s.Router()
rec := doJSON(t, router, http.MethodGet, "/api/workout-kinds/", nil)
var kinds []workoutKindResponse
json.Unmarshal(rec.Body.Bytes(), &kinds)
rec = doJSON(t, router, http.MethodPut, "/api/workout-kinds/"+itoa(kinds[0].ID), map[string]any{
"name": kinds[0].Name,
"rule": json.RawMessage(`{"match":"all","conditions":[]}`),
"expected_hr_zone": 9,
})
if rec.Code != http.StatusBadRequest {
t.Fatalf("status = %d, want 400, body = %s", rec.Code, rec.Body.String())
}
}
func TestReviewQueueResolve(t *testing.T) {
s, db := newTestServer(t)
ctx := newCtx()