diff options
| author | Martin Kagamino Lehoux <martin@lehoux.net> | 2026-09-13 21:55:59 +0200 |
|---|---|---|
| committer | Martin Kagamino Lehoux <martin@lehoux.net> | 2026-09-13 21:55:59 +0200 |
| commit | e3d0beee5be3dd2854b0f4fc10905a4fcdc08f47 (patch) | |
| tree | ec8234d3442c498ec170b6444bd2cfae5b29a476 /web/climbs_test.go | |
| parent | db4d2b40895db41ce188549f49a6f277ba95c890 (diff) | |
Persist official climb attempts with climbs pages
Record one attempt row per (ride, climb, occurrence) when a ride detail
loads, snapshotting distance, elevation gain, slope, cotacol and attempt
timestamps; a climbs-backfill CLI command seeds all existing rides.
New /climbs list page aggregates attempts (medians computed in Go, best
time as min duration) with sortable columns, and /climbs/{id} shows the
median scalar stats, a median normalized elevation profile (50 steps
over each attempt's own length), the full-precision recorded route of
the median-distance attempt on an OSM map, and every attempt with time
at second precision.
Diffstat (limited to 'web/climbs_test.go')
| -rw-r--r-- | web/climbs_test.go | 97 |
1 files changed, 97 insertions, 0 deletions
diff --git a/web/climbs_test.go b/web/climbs_test.go new file mode 100644 index 0000000..fbaf527 --- /dev/null +++ b/web/climbs_test.go @@ -0,0 +1,97 @@ +package web + +import ( + "database/sql" + "net/http" + "net/http/httptest" + "os" + "path/filepath" + "testing" + + "github.com/martinlehoux/biking_home/official_climb" + "github.com/martinlehoux/biking_home/ride" + "github.com/martinlehoux/biking_home/rideanalysis" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func recordFixtureAttempts(t *testing.T, server *Server, rideID int64, climb official_climb.OfficialClimb) { + t.Helper() + parsed, _ := testRideForClimb(t) + analysis := rideanalysis.Analyze(parsed, nil, []official_climb.OfficialClimb{climb}, official_climb.DefaultMatchPolicy()) + server.recordOfficialClimbAttempts(rideID, parsed, analysis) +} +func storeRideFixtureFile(t *testing.T, db *sql.DB, rideID int64) { + t.Helper() + parsed, _ := testRideForClimb(t) + fitPath := filepath.Join(t.TempDir(), "ride.fit") + fitFile, err := os.Create(fitPath) + require.NoError(t, err) + require.NoError(t, ride.EncodeFIT(parsed, fitFile)) + require.NoError(t, fitFile.Close()) + _, err = db.Exec("UPDATE rides SET fit_path = ? WHERE id = ?", fitPath, rideID) + require.NoError(t, err) +} + +func TestHandleClimbRendersAttemptsAndProfile(t *testing.T) { + server, db := newWebTestServer(t) + _, climbFixture := testRideForClimb(t) + created, err := official_climb.Create(db, climbFixture) + require.NoError(t, err) + rideID := insertTestRide(t, db) + storeRideFixtureFile(t, db, rideID) + recordFixtureAttempts(t, server, rideID, created) + + recorder := httptest.NewRecorder() + server.Handler().ServeHTTP(recorder, httptest.NewRequest(http.MethodGet, climbDetailURL(created.ID), nil)) + require.Equal(t, http.StatusOK, recorder.Code) + body := recorder.Body.String() + assert.Contains(t, body, "Col de Test") + assert.Contains(t, body, "climb-profile-data") + assert.Contains(t, body, "Test Ride") +} + +func TestHandleClimbNotFound(t *testing.T) { + server, _ := newWebTestServer(t) + recorder := httptest.NewRecorder() + server.Handler().ServeHTTP(recorder, httptest.NewRequest(http.MethodGet, "/climbs/999", nil)) + assert.Equal(t, http.StatusNotFound, recorder.Code) +} + +func TestBuildClimbProfileMediansAcrossRides(t *testing.T) { + server, db := newWebTestServer(t) + _, climbFixture := testRideForClimb(t) + created, err := official_climb.Create(db, climbFixture) + require.NoError(t, err) + rideID := insertTestRide(t, db) + storeRideFixtureFile(t, db, rideID) + parsed, _ := testRideForClimb(t) + analysis := rideanalysis.Analyze(parsed, nil, []official_climb.OfficialClimb{created}, official_climb.DefaultMatchPolicy()) + attempts := attemptsFromAnalysis(rideID, analysis, parsed) + + profile := server.buildClimbProfile(created, official_climb.AggregateAttempts(attempts), attempts) + assert.InDelta(t, 1.0, profile.DistanceKm, 0.01) + require.Len(t, profile.Elevations, 50) + assert.InDelta(t, 300, profile.Elevations[0], 1) + assert.InDelta(t, 447, profile.Elevations[49], 2) +} + +func TestBuildClimbProfileRouteKeepsRecordedPoints(t *testing.T) { + server, db := newWebTestServer(t) + parsed, climbFixture := testRideForClimb(t) + created, err := official_climb.Create(db, climbFixture) + require.NoError(t, err) + rideID := insertTestRide(t, db) + storeRideFixtureFile(t, db, rideID) + analysis := rideanalysis.Analyze(parsed, nil, []official_climb.OfficialClimb{created}, official_climb.DefaultMatchPolicy()) + attempts := attemptsFromAnalysis(rideID, analysis, parsed) + + profile := server.buildClimbProfile(created, official_climb.AggregateAttempts(attempts), attempts) + require.NotEmpty(t, profile.Route) + // Full recorded precision: every matched segment point is present, not a + // fixed 50-step resample. + assert.InDelta(t, parsed.Coords[0].Lon, profile.Route[0][0], 1e-6) + assert.InDelta(t, parsed.Coords[0].Lat, profile.Route[0][1], 1e-6) + assert.InDelta(t, parsed.Coords[1].Lon, profile.Route[1][0], 1e-6) + assert.InDelta(t, parsed.Coords[1].Lat, profile.Route[1][1], 1e-6) +} |
