2026-08-04 16:04:18 +02:00
|
|
|
// MockClient support: a fake Client for tests and frontend/dev
|
2026-07-25 21:43:52 +02:00
|
|
|
// work without a live Garmin account or the wrapper subprocess.
|
2026-08-04 16:04:18 +02:00
|
|
|
package garmin
|
2026-07-17 18:33:06 +02:00
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
"context"
|
2026-07-27 09:01:30 +02:00
|
|
|
"time"
|
2026-07-17 18:33:06 +02:00
|
|
|
)
|
|
|
|
|
|
2026-08-04 16:04:18 +02:00
|
|
|
// MockClient is a fake Client returning data supplied by the test/caller.
|
|
|
|
|
type MockClient struct {
|
|
|
|
|
AuthResults []AuthResult // consumed in order by Authenticate/CompleteMFA calls
|
|
|
|
|
Activities []Activity
|
|
|
|
|
Splits map[int64]ActivitySplits
|
|
|
|
|
Details map[int64]ActivityDetails
|
|
|
|
|
Workouts map[int64]Workout
|
feat(sync): split FillPendingDetails into activities/workouts phases
Progress becomes phase-aware ({Phase, Done, Total} instead of a flat
{Done, Total}), with FullSync reporting an indeterminate "discovering"
phase during backfill/incremental sync (there's no meaningful total
before those calls have already happened), then FillPendingDetails
reporting a real Done/Total for its activities pass, then its new,
independent workouts pass.
alignWorkoutTargets now takes a lap count instead of a []garmin.Lap
slice, since it never used lap content, only length -- this lets the
new workouts pass call it against laps read back from the DB rather
than needing the original Garmin lap data again.
Splitting the passes also fixes a latent bug: an activity whose
details were fetched successfully but whose workout fetch failed in
that same run previously had no way to ever retry the workout fetch,
since ActivitiesMissingDetails stops returning it once
details_fetched_at/splits_fetched_at are set. ActivitiesMissingWorkout
queries workout_id/workout_raw_json independently, so it keeps
surfacing that activity until its workout is actually fetched.
2026-07-27 06:55:23 +02:00
|
|
|
// WorkoutErrByID, if set for a given workout ID, makes GetWorkoutByID
|
|
|
|
|
// return that error for that ID specifically -- independent of the
|
|
|
|
|
// all-calls-fail Err field below -- so a test can simulate one
|
|
|
|
|
// activity's workout fetch failing while others in the same batch
|
|
|
|
|
// succeed.
|
2026-07-27 09:01:30 +02:00
|
|
|
WorkoutErrByID map[int64]error
|
|
|
|
|
// Delay, if set, is slept (ctx-cancellable) at the start of every
|
|
|
|
|
// GetActivities call -- lets a test give sync.Service's discovering
|
|
|
|
|
// phase (backfillCore/incrementalSyncCore) real wall-clock duration, so
|
|
|
|
|
// a concurrent goroutine can observe Progress() mid-flight instead of
|
|
|
|
|
// the call returning instantly.
|
|
|
|
|
Delay time.Duration
|
2026-07-17 18:33:06 +02:00
|
|
|
Err error // if set, every call returns this error
|
|
|
|
|
authResultCursor int
|
|
|
|
|
ClosedCalled bool
|
|
|
|
|
GetActivitiesCalls int
|
2026-07-26 13:30:51 +02:00
|
|
|
AuthenticateCalls int
|
2026-07-17 18:57:54 +02:00
|
|
|
LastEmail string
|
|
|
|
|
LastPassword string
|
2026-07-17 18:33:06 +02:00
|
|
|
}
|
|
|
|
|
|
2026-08-04 16:04:18 +02:00
|
|
|
var _ Client = (*MockClient)(nil)
|
2026-07-17 18:33:06 +02:00
|
|
|
|
2026-08-04 16:04:18 +02:00
|
|
|
func (c *MockClient) nextAuthResult() AuthResult {
|
2026-07-17 18:33:06 +02:00
|
|
|
if c.authResultCursor >= len(c.AuthResults) {
|
2026-08-04 16:04:18 +02:00
|
|
|
return AuthResult{Status: AuthSuccess, Message: "Authenticated successfully."}
|
2026-07-17 18:33:06 +02:00
|
|
|
}
|
|
|
|
|
r := c.AuthResults[c.authResultCursor]
|
|
|
|
|
c.authResultCursor++
|
|
|
|
|
return r
|
|
|
|
|
}
|
|
|
|
|
|
2026-08-04 16:04:18 +02:00
|
|
|
func (c *MockClient) Authenticate(ctx context.Context) (AuthResult, error) {
|
2026-07-26 13:30:51 +02:00
|
|
|
c.AuthenticateCalls++
|
2026-07-17 18:33:06 +02:00
|
|
|
if c.Err != nil {
|
2026-08-04 16:04:18 +02:00
|
|
|
return AuthResult{}, c.Err
|
2026-07-17 18:33:06 +02:00
|
|
|
}
|
|
|
|
|
return c.nextAuthResult(), nil
|
|
|
|
|
}
|
|
|
|
|
|
2026-08-04 16:04:18 +02:00
|
|
|
func (c *MockClient) CompleteMFA(ctx context.Context, code string) (AuthResult, error) {
|
2026-07-17 18:33:06 +02:00
|
|
|
if c.Err != nil {
|
2026-08-04 16:04:18 +02:00
|
|
|
return AuthResult{}, c.Err
|
2026-07-17 18:33:06 +02:00
|
|
|
}
|
|
|
|
|
return c.nextAuthResult(), nil
|
|
|
|
|
}
|
|
|
|
|
|
2026-08-04 16:04:18 +02:00
|
|
|
func (c *MockClient) GetActivities(ctx context.Context, startDate, endDate string, limit int) ([]Activity, error) {
|
2026-07-17 18:33:06 +02:00
|
|
|
c.GetActivitiesCalls++
|
2026-07-27 09:01:30 +02:00
|
|
|
if c.Delay > 0 {
|
|
|
|
|
select {
|
|
|
|
|
case <-ctx.Done():
|
|
|
|
|
return nil, ctx.Err()
|
|
|
|
|
case <-time.After(c.Delay):
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-07-17 18:33:06 +02:00
|
|
|
if c.Err != nil {
|
|
|
|
|
return nil, c.Err
|
|
|
|
|
}
|
|
|
|
|
if limit > 0 && limit < len(c.Activities) {
|
|
|
|
|
return c.Activities[:limit], nil
|
|
|
|
|
}
|
|
|
|
|
return c.Activities, nil
|
|
|
|
|
}
|
|
|
|
|
|
2026-08-04 16:04:18 +02:00
|
|
|
func (c *MockClient) GetActivitySplits(ctx context.Context, activityID int64) (ActivitySplits, error) {
|
2026-07-17 18:33:06 +02:00
|
|
|
if c.Err != nil {
|
2026-08-04 16:04:18 +02:00
|
|
|
return ActivitySplits{}, c.Err
|
2026-07-17 18:33:06 +02:00
|
|
|
}
|
|
|
|
|
return c.Splits[activityID], nil
|
|
|
|
|
}
|
|
|
|
|
|
2026-08-04 16:04:18 +02:00
|
|
|
func (c *MockClient) GetActivityDetails(ctx context.Context, activityID int64) (ActivityDetails, error) {
|
2026-07-17 18:33:06 +02:00
|
|
|
if c.Err != nil {
|
2026-08-04 16:04:18 +02:00
|
|
|
return ActivityDetails{}, c.Err
|
2026-07-17 18:33:06 +02:00
|
|
|
}
|
|
|
|
|
return c.Details[activityID], nil
|
|
|
|
|
}
|
|
|
|
|
|
2026-08-04 16:04:18 +02:00
|
|
|
func (c *MockClient) GetWorkoutByID(ctx context.Context, workoutID int64) (Workout, error) {
|
2026-07-19 11:55:13 +02:00
|
|
|
if c.Err != nil {
|
2026-08-04 16:04:18 +02:00
|
|
|
return Workout{}, c.Err
|
2026-07-19 11:55:13 +02:00
|
|
|
}
|
feat(sync): split FillPendingDetails into activities/workouts phases
Progress becomes phase-aware ({Phase, Done, Total} instead of a flat
{Done, Total}), with FullSync reporting an indeterminate "discovering"
phase during backfill/incremental sync (there's no meaningful total
before those calls have already happened), then FillPendingDetails
reporting a real Done/Total for its activities pass, then its new,
independent workouts pass.
alignWorkoutTargets now takes a lap count instead of a []garmin.Lap
slice, since it never used lap content, only length -- this lets the
new workouts pass call it against laps read back from the DB rather
than needing the original Garmin lap data again.
Splitting the passes also fixes a latent bug: an activity whose
details were fetched successfully but whose workout fetch failed in
that same run previously had no way to ever retry the workout fetch,
since ActivitiesMissingDetails stops returning it once
details_fetched_at/splits_fetched_at are set. ActivitiesMissingWorkout
queries workout_id/workout_raw_json independently, so it keeps
surfacing that activity until its workout is actually fetched.
2026-07-27 06:55:23 +02:00
|
|
|
if err, ok := c.WorkoutErrByID[workoutID]; ok {
|
2026-08-04 16:04:18 +02:00
|
|
|
return Workout{}, err
|
feat(sync): split FillPendingDetails into activities/workouts phases
Progress becomes phase-aware ({Phase, Done, Total} instead of a flat
{Done, Total}), with FullSync reporting an indeterminate "discovering"
phase during backfill/incremental sync (there's no meaningful total
before those calls have already happened), then FillPendingDetails
reporting a real Done/Total for its activities pass, then its new,
independent workouts pass.
alignWorkoutTargets now takes a lap count instead of a []garmin.Lap
slice, since it never used lap content, only length -- this lets the
new workouts pass call it against laps read back from the DB rather
than needing the original Garmin lap data again.
Splitting the passes also fixes a latent bug: an activity whose
details were fetched successfully but whose workout fetch failed in
that same run previously had no way to ever retry the workout fetch,
since ActivitiesMissingDetails stops returning it once
details_fetched_at/splits_fetched_at are set. ActivitiesMissingWorkout
queries workout_id/workout_raw_json independently, so it keeps
surfacing that activity until its workout is actually fetched.
2026-07-27 06:55:23 +02:00
|
|
|
}
|
2026-07-19 11:55:13 +02:00
|
|
|
return c.Workouts[workoutID], nil
|
|
|
|
|
}
|
|
|
|
|
|
2026-08-04 16:04:18 +02:00
|
|
|
func (c *MockClient) UpdateCredentials(email, password string) {
|
2026-07-17 18:57:54 +02:00
|
|
|
c.LastEmail = email
|
|
|
|
|
c.LastPassword = password
|
|
|
|
|
}
|
|
|
|
|
|
2026-08-04 16:04:18 +02:00
|
|
|
func (c *MockClient) Close() error {
|
2026-07-17 18:33:06 +02:00
|
|
|
c.ClosedCalled = true
|
|
|
|
|
return nil
|
|
|
|
|
}
|