From 7d13374eec8c33c4e14cc5b0c3de36e4f620ce0e Mon Sep 17 00:00:00 2001 From: Martin Kagamino Lehoux Date: Tue, 11 Aug 2026 14:19:01 +0200 Subject: fix: Continue Strava import after invalid activities --- web/server.go | 33 +++++++++++++++++++++++---- web/server_test.go | 62 ++++++++++++++++++++++++++++++++++++++++++++++++++ web/templates.templ | 2 +- web/templates_templ.go | 2 +- 4 files changed, 92 insertions(+), 7 deletions(-) (limited to 'web') diff --git a/web/server.go b/web/server.go index 53374ce..6f222ef 100644 --- a/web/server.go +++ b/web/server.go @@ -37,6 +37,11 @@ type Server struct { syncActive bool } +type syncClient interface { + List(from, to time.Time) ([]strava.Activity, error) + Get(id int64) (strava.Activity, []byte, error) +} + func NewServer(db *sql.DB, configPath string) *Server { return &Server{db: db, configPath: configPath} } @@ -263,7 +268,7 @@ func (s *Server) handleStravaCallback(w http.ResponseWriter, r *http.Request) { http.Redirect(w, r, returnTo, http.StatusSeeOther) } -func (s *Server) syncRides(client *strava.Client, gpxDir string, from, to time.Time, report func(SyncProgress) error) (SyncProgress, error) { +func (s *Server) syncRides(client syncClient, gpxDir string, from, to time.Time, report func(SyncProgress) error) (SyncProgress, error) { activities, err := client.List(from, to) if err != nil { return SyncProgress{}, err @@ -292,11 +297,18 @@ func (s *Server) syncRides(client *strava.Client, gpxDir string, from, to time.T } activity, gpxData, err := client.Get(summary.ID) if err != nil { - return progress, err + if err := skipSyncActivity(&progress, report, summary, err); err != nil { + return progress, err + } + continue } gpxPath := filepath.Join(gpxDir, fmt.Sprintf("activity_%d.gpx", activity.ID)) if err := os.WriteFile(gpxPath, gpxData, 0o644); err != nil { - return progress, fmt.Errorf("write GPX for activity %d: %w", activity.ID, err) + _ = os.Remove(gpxPath) + if reportErr := skipSyncActivity(&progress, report, summary, fmt.Errorf("write GPX: %w", err)); reportErr != nil { + return progress, reportErr + } + continue } activityType := activity.SportType if activityType == "" { @@ -314,7 +326,11 @@ func (s *Server) syncRides(client *strava.Client, gpxDir string, from, to time.T TotalElevationGainM: activity.TotalElevationGainM, AverageSpeedMps: activity.AverageSpeedMps, }); err != nil { - return progress, fmt.Errorf("save activity %d: %w", activity.ID, err) + _ = os.Remove(gpxPath) + if reportErr := skipSyncActivity(&progress, report, summary, fmt.Errorf("save activity: %w", err)); reportErr != nil { + return progress, reportErr + } + continue } progress.Imported++ progress.Completed++ @@ -326,6 +342,13 @@ func (s *Server) syncRides(client *strava.Client, gpxDir string, from, to time.T return progress, nil } +func skipSyncActivity(progress *SyncProgress, report func(SyncProgress) error, activity strava.Activity, err error) error { + progress.Skipped++ + progress.Completed++ + slog.Warn("Skipped Strava activity", "activity", activity.ID, "name", activity.Name, "error", err) + return reportSyncProgress(report, *progress) +} + func reportSyncProgress(report func(SyncProgress) error, progress SyncProgress) error { if report == nil { return nil @@ -469,7 +492,7 @@ func syncNotice(r *http.Request) string { if imported == "" { return "" } - return fmt.Sprintf("Sync complete: %s imported, %s already stored.", imported, r.URL.Query().Get("skipped")) + return fmt.Sprintf("Sync complete: %s imported, %s skipped.", imported, r.URL.Query().Get("skipped")) } func safeReturnTo(value string) string { diff --git a/web/server_test.go b/web/server_test.go index 32ab448..46828d1 100644 --- a/web/server_test.go +++ b/web/server_test.go @@ -16,6 +16,7 @@ import ( "github.com/martinlehoux/biking_home/config" "github.com/martinlehoux/biking_home/rides" + "github.com/martinlehoux/biking_home/strava" _ "github.com/mattn/go-sqlite3" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -61,6 +62,32 @@ func testGPXPath(t *testing.T, name string) string { return path } +type fakeSyncClient struct { + activities []strava.Activity + results map[int64]fakeSyncResult +} + +type fakeSyncResult struct { + activity strava.Activity + data []byte + err error +} + +func (c fakeSyncClient) List(time.Time, time.Time) ([]strava.Activity, error) { + return c.activities, nil +} + +func (c fakeSyncClient) Get(id int64) (strava.Activity, []byte, error) { + result, ok := c.results[id] + if !ok { + return strava.Activity{}, nil, fmt.Errorf("missing fake activity %d", id) + } + return result.activity, result.data, result.err +} + +const emptyTrackGPX = `` +const validTrackGPX = `100200` + func TestHandlerRendersRidesPage(t *testing.T) { server, db := newWebTestServer(t) require.NoError(t, rides.Save(db, rides.Ride{ @@ -224,6 +251,41 @@ func TestSyncParsesMultipartDateRange(t *testing.T) { assert.NotContains(t, response.Body.String(), "") } +func TestSyncRidesContinuesAfterInvalidActivity(t *testing.T) { + server, db := newWebTestServer(t) + invalidID := int64(14701658670) + validID := int64(14701658671) + startDate := time.Date(2026, 8, 1, 7, 0, 0, 0, time.UTC) + client := fakeSyncClient{ + activities: []strava.Activity{ + {ID: invalidID, Name: "Empty activity", Type: "Ride", StartDate: startDate}, + {ID: validID, Name: "Valid activity", Type: "Ride", StartDate: startDate}, + }, + results: map[int64]fakeSyncResult{ + invalidID: {activity: strava.Activity{ID: invalidID, Name: "Empty activity", Type: "Ride", StartDate: startDate}, data: []byte(emptyTrackGPX)}, + validID: {activity: strava.Activity{ID: validID, Name: "Valid activity", Type: "Ride", StartDate: startDate}, data: []byte(validTrackGPX)}, + }, + } + var updates []SyncProgress + gpxDir := t.TempDir() + + progress, err := server.syncRides(client, gpxDir, startDate, startDate.AddDate(0, 0, 1), func(progress SyncProgress) error { + updates = append(updates, progress) + return nil + }) + + require.NoError(t, err) + assert.Equal(t, SyncProgress{Total: 2, Completed: 2, Imported: 1, Skipped: 1}, progress) + assert.Len(t, updates, 3) + _, found, err := rides.GetByExternalID(db, "strava:14701658670") + require.NoError(t, err) + assert.False(t, found) + _, found, err = rides.GetByExternalID(db, "strava:14701658671") + require.NoError(t, err) + assert.True(t, found) + assert.NoFileExists(t, filepath.Join(gpxDir, "activity_14701658670.gpx")) +} + func TestSyncRejectsConcurrentImport(t *testing.T) { server, _ := newWebTestServer(t) appConfig, err := config.Load(server.configPath) diff --git a/web/templates.templ b/web/templates.templ index 74b0365..78591e1 100644 --- a/web/templates.templ +++ b/web/templates.templ @@ -208,7 +208,7 @@ templ SyncContent(data SyncPageData) { } if (eventName === "complete") { updateProgress(data); - notice.textContent = `Sync complete: ${data.imported} imported, ${data.skipped} already stored.`; + notice.textContent = `Sync complete: ${data.imported} imported, ${data.skipped} skipped.`; notice.hidden = false; return true; } diff --git a/web/templates_templ.go b/web/templates_templ.go index ba6910b..b66e732 100644 --- a/web/templates_templ.go +++ b/web/templates_templ.go @@ -630,7 +630,7 @@ func SyncContent(data SyncPageData) templ.Component { return templ_7745c5c3_Err } } - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("") + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } -- cgit v1.2.3