1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
|
package official_climb_test
import (
"database/sql"
"testing"
"time"
"github.com/jftuga/geodist"
"github.com/martinlehoux/biking_home/climb"
"github.com/martinlehoux/biking_home/internal/dbtest"
"github.com/martinlehoux/biking_home/official_climb"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func coord(lat, lon float64) geodist.Coord {
return geodist.Coord{Lat: lat, Lon: lon}
}
// sampleFixture is a plausible sample for tests that do not exercise matching.
func sampleFixture() climb.SegmentSample {
return climb.SegmentSample{DistanceM: 3000, ElevationGainM: 200}
}
func insertRide(t *testing.T, db *sql.DB, opts ...func(*rideInsert)) int64 {
t.Helper()
insert := rideInsert{startDate: "2026-09-01T08:00:00Z"}
for _, opt := range opts {
opt(&insert)
}
result, err := db.Exec(`
INSERT INTO rides (external_id, gpx_path, name, external_sport_type, start_date, distance_m, external_moving_time_s, elapsed_time_s, total_elevation_gain_m, average_speed_mps)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
`, "strava:test-ride-"+insert.startDate, "missing.gpx", "Test Ride", "Ride", insert.startDate, 20_000, 0, 0, 0, 0)
require.NoError(t, err)
id, err := result.LastInsertId()
require.NoError(t, err)
return id
}
type rideInsert struct {
startDate string
}
func withStartDate(startDate string) func(*rideInsert) {
return func(r *rideInsert) { r.startDate = startDate }
}
func attemptFixture(rideID, climbID int64, occurrence int) official_climb.Attempt {
averagePowerW := 235.5
return official_climb.Attempt{
RideID: rideID,
ClimbID: climbID,
Occurrence: occurrence,
DistanceM: 3200,
ElevationGainM: 210,
SlopePercent: 6.5,
Cotacol: 84.2,
AveragePowerW: &averagePowerW,
CotacolAlgoVersion: "v1",
StartedAt: time.Date(2026, 9, 1, 10, 0, 0, 0, time.UTC),
EndedAt: time.Date(2026, 9, 1, 10, 18, 30, 0, time.UTC),
}
}
func TestReplaceRideAttemptsRoundTrip(t *testing.T) {
db := dbtest.New(t)
created, err := official_climb.Create(db, official_climb.OfficialClimb{Name: "Col de Test", StartCoord: coord(43.6, 5.4), EndCoord: coord(43.62, 5.42), Sample: sampleFixture()})
require.NoError(t, err)
rideID := insertRide(t, db)
withoutPower := attemptFixture(rideID, created.ID, 1)
withoutPower.AveragePowerW = nil
require.NoError(t, official_climb.ReplaceRideAttempts(db, rideID, []official_climb.Attempt{
attemptFixture(rideID, created.ID, 0),
withoutPower,
}))
attempts, err := official_climb.AttemptsForClimb(db, created.ID)
require.NoError(t, err)
require.Len(t, attempts, 2)
assert.Equal(t, 0, attempts[0].Occurrence)
assert.Equal(t, 1, attempts[1].Occurrence)
assert.Equal(t, 3200.0, attempts[0].DistanceM)
assert.Equal(t, "v1", attempts[0].CotacolAlgoVersion)
assert.Equal(t, time.Date(2026, 9, 1, 10, 18, 30, 0, time.UTC), attempts[0].EndedAt)
require.NotNil(t, attempts[0].AveragePowerW)
assert.InDelta(t, 235.5, *attempts[0].AveragePowerW, 0.01)
assert.Nil(t, attempts[1].AveragePowerW, "a missing power meter must round-trip as null")
rideAttempts, err := official_climb.AttemptsForRide(db, rideID)
require.NoError(t, err)
require.Len(t, rideAttempts, 2)
}
func TestReplaceRideAttemptsKeepsRecordedAt(t *testing.T) {
db := dbtest.New(t)
created, err := official_climb.Create(db, official_climb.OfficialClimb{Name: "Col de Test", StartCoord: coord(43.6, 5.4), EndCoord: coord(43.62, 5.42), Sample: sampleFixture()})
require.NoError(t, err)
rideID := insertRide(t, db)
attempts := []official_climb.Attempt{attemptFixture(rideID, created.ID, 0)}
require.NoError(t, official_climb.ReplaceRideAttempts(db, rideID, attempts))
recorded := time.Date(2020, 1, 1, 0, 0, 0, 0, time.UTC)
_, err = db.Exec("UPDATE ride_official_climb_attempts SET created_at = ? WHERE ride_id = ?", recorded.Format(time.RFC3339), rideID)
require.NoError(t, err)
// Re-analyzing the same ride must not restamp the attempt it already has.
require.NoError(t, official_climb.ReplaceRideAttempts(db, rideID, attempts))
stored, err := official_climb.AttemptsForRide(db, rideID)
require.NoError(t, err)
require.Len(t, stored, 1)
assert.Equal(t, recorded, stored[0].CreatedAt.UTC())
}
func TestReplaceRideAttemptsDropsStaleRows(t *testing.T) {
db := dbtest.New(t)
first, err := official_climb.Create(db, official_climb.OfficialClimb{Name: "Col A", StartCoord: coord(43.6, 5.4), EndCoord: coord(43.62, 5.42), Sample: sampleFixture()})
require.NoError(t, err)
second, err := official_climb.Create(db, official_climb.OfficialClimb{Name: "Col B", StartCoord: coord(43.7, 5.5), EndCoord: coord(43.72, 5.52), Sample: sampleFixture()})
require.NoError(t, err)
rideID := insertRide(t, db)
require.NoError(t, official_climb.ReplaceRideAttempts(db, rideID, []official_climb.Attempt{attemptFixture(rideID, first.ID, 0)}))
require.NoError(t, official_climb.ReplaceRideAttempts(db, rideID, []official_climb.Attempt{attemptFixture(rideID, second.ID, 0)}))
attempts, err := official_climb.AttemptsForClimb(db, first.ID)
require.NoError(t, err)
assert.Empty(t, attempts)
attempts, err = official_climb.AttemptsForClimb(db, second.ID)
require.NoError(t, err)
require.Len(t, attempts, 1)
}
func TestReplaceRideAttemptsRejectsForeignRide(t *testing.T) {
db := dbtest.New(t)
created, err := official_climb.Create(db, official_climb.OfficialClimb{Name: "Col de Test", StartCoord: coord(43.6, 5.4), EndCoord: coord(43.62, 5.42), Sample: sampleFixture()})
require.NoError(t, err)
rideID := insertRide(t, db)
attempt := attemptFixture(rideID+1, created.ID, 0)
err = official_climb.ReplaceRideAttempts(db, rideID, []official_climb.Attempt{attempt})
assert.Error(t, err)
}
func TestAttemptsForClimbOrdersByRideDate(t *testing.T) {
db := dbtest.New(t)
created, err := official_climb.Create(db, official_climb.OfficialClimb{Name: "Col de Test", StartCoord: coord(43.6, 5.4), EndCoord: coord(43.62, 5.42), Sample: sampleFixture()})
require.NoError(t, err)
newer := insertRide(t, db, withStartDate("2026-09-10T08:00:00Z"))
older := insertRide(t, db, withStartDate("2026-09-02T08:00:00Z"))
require.NoError(t, official_climb.ReplaceRideAttempts(db, newer, []official_climb.Attempt{attemptFixture(newer, created.ID, 0)}))
require.NoError(t, official_climb.ReplaceRideAttempts(db, older, []official_climb.Attempt{attemptFixture(older, created.ID, 0)}))
attempts, err := official_climb.AttemptsForClimb(db, created.ID)
require.NoError(t, err)
require.Len(t, attempts, 2)
assert.Equal(t, older, attempts[0].RideID)
assert.Equal(t, newer, attempts[1].RideID)
}
|