From 6f62686d293248f61925c5eac5f272d56c3b8fc6 Mon Sep 17 00:00:00 2001 From: Christophe Vila Date: Fri, 17 Jul 2026 18:57:54 +0200 Subject: [PATCH] feat: support updating Garmin credentials at runtime Co-Authored-By: Claude Sonnet 5 --- backend/internal/garmin/client.go | 23 +++++++++++++++++++++++ backend/internal/garmin/client_test.go | 17 +++++++++++++++++ backend/internal/garmin/mock/mock.go | 7 +++++++ 3 files changed, 47 insertions(+) diff --git a/backend/internal/garmin/client.go b/backend/internal/garmin/client.go index df5db9b..3df701f 100644 --- a/backend/internal/garmin/client.go +++ b/backend/internal/garmin/client.go @@ -24,6 +24,12 @@ type Client interface { Authenticate(ctx context.Context) (AuthResult, error) // CompleteMFA submits an MFA code for a login started by Authenticate. CompleteMFA(ctx context.Context, code string) (AuthResult, error) + // UpdateCredentials replaces the Garmin email/password used to spawn + // the subprocess, and terminates any already-running subprocess (which + // would otherwise still be authenticated under the old credentials). + // The next call that needs the subprocess spawns a fresh one with the + // new credentials. + UpdateCredentials(email, password string) // GetActivities lists activities between start and end (YYYY-MM-DD). GetActivities(ctx context.Context, startDate, endDate string, limit int) ([]Activity, error) // GetActivitySplits fetches lap/split summaries for one activity. @@ -97,6 +103,23 @@ func (c *mcpClient) ensureStarted(ctx context.Context) error { return nil } +// UpdateCredentials implements Client. +func (c *mcpClient) UpdateCredentials(email, password string) { + c.mu.Lock() + defer c.mu.Unlock() + + c.cfg.GarminEmail = email + c.cfg.GarminPassword = password + + if c.started { + if c.inner != nil { + c.inner.Close() + } + c.inner = nil + c.started = false + } +} + // drainStderr forwards the subprocess's debug/log output so it isn't // silently dropped (mcp-garmin logs auth/rate-limit diagnostics there). func drainStderr(stdio *transport.Stdio) { diff --git a/backend/internal/garmin/client_test.go b/backend/internal/garmin/client_test.go index 72b9c68..368d6e6 100644 --- a/backend/internal/garmin/client_test.go +++ b/backend/internal/garmin/client_test.go @@ -20,3 +20,20 @@ func TestParseAuthResult(t *testing.T) { } } } + +func TestMcpClient_UpdateCredentials_ResetsStartedState(t *testing.T) { + c := &mcpClient{cfg: Config{GarminEmail: "old@example.com", GarminPassword: "old"}} + c.started = true // simulate an already-spawned subprocess + + c.UpdateCredentials("new@example.com", "new") + + if c.cfg.GarminEmail != "new@example.com" || c.cfg.GarminPassword != "new" { + t.Errorf("cfg after update = %+v, want new@example.com/new", c.cfg) + } + if c.started { + t.Error("started should be reset to false so the next call respawns the subprocess") + } + if c.inner != nil { + t.Error("inner should be cleared so ensureStarted spawns a fresh client") + } +} diff --git a/backend/internal/garmin/mock/mock.go b/backend/internal/garmin/mock/mock.go index e6c95fc..4428eb5 100644 --- a/backend/internal/garmin/mock/mock.go +++ b/backend/internal/garmin/mock/mock.go @@ -18,6 +18,8 @@ type Client struct { authResultCursor int ClosedCalled bool GetActivitiesCalls int + LastEmail string + LastPassword string } var _ garmin.Client = (*Client)(nil) @@ -70,6 +72,11 @@ func (c *Client) GetActivityDetails(ctx context.Context, activityID int64) (garm return c.Details[activityID], nil } +func (c *Client) UpdateCredentials(email, password string) { + c.LastEmail = email + c.LastPassword = password +} + func (c *Client) Close() error { c.ClosedCalled = true return nil