fix(api): stop promoting the ephemeral Garmin client after setup completes
handleSetupComplete reused the onboarding client object as-is in the
permanent per-user cache, but its garmin.Config.TokenStorePath was fixed
at construction to the ephemeral setup/{hash} directory and never
corrected after that directory was renamed to the permanent {userID}
path. The next respawn of that same client (e.g. any Profile-page save,
which unconditionally calls UpdateCredentials) wrote a fresh token file
back under setup/{hash}, forcing a real re-login/MFA on the next Garmin
connect even though a valid session already existed under {userID}.
Close the ephemeral client instead and let the next garminFor call build
a fresh one against the correct, already-renamed directory.
This commit is contained in:
@@ -92,8 +92,20 @@ func TestSetupGarminMFA_RejectsWithoutPriorLoginAttempt(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestSetupComplete_CreatesAccountWithGarminCredentialsAndPromotesClient(t *testing.T) {
|
||||
s, db, m := newUnprovisionedServer(t)
|
||||
func TestSetupComplete_CreatesAccountWithGarminCredentialsAndClosesEphemeralClient(t *testing.T) {
|
||||
db, err := store.Open(filepath.Join(t.TempDir(), "geniusrun_test.db"))
|
||||
if err != nil {
|
||||
t.Fatalf("store.Open: %v", err)
|
||||
}
|
||||
t.Cleanup(func() { db.Close() })
|
||||
m := &mock.Client{}
|
||||
tokenStoreRoot := t.TempDir()
|
||||
var factoryConfigs []garmin.Config
|
||||
garminFactory := func(cfg garmin.Config) garmin.Client {
|
||||
factoryConfigs = append(factoryConfigs, cfg)
|
||||
return m
|
||||
}
|
||||
s := NewServer(db, garminFactory, garmin.Config{TokenStorePath: tokenStoreRoot}, appsync.Config{}, &authmock.Verifier{}, testSessionConfig)
|
||||
router := s.Router()
|
||||
|
||||
rec := doJSON(t, router, http.MethodPost, "/api/setup/garmin/login", map[string]any{
|
||||
@@ -124,18 +136,27 @@ func TestSetupComplete_CreatesAccountWithGarminCredentialsAndPromotesClient(t *t
|
||||
}
|
||||
|
||||
if m.AuthenticateCalls != 1 {
|
||||
t.Errorf("AuthenticateCalls = %d, want 1 (no redundant re-authentication after promotion)", m.AuthenticateCalls)
|
||||
t.Errorf("AuthenticateCalls = %d, want 1 (only the original login, no redundant re-authentication)", m.AuthenticateCalls)
|
||||
}
|
||||
if m.ClosedCalled {
|
||||
t.Error("expected the promoted client to survive (not be Close()d)")
|
||||
if !m.ClosedCalled {
|
||||
t.Error("expected the ephemeral client to be Close()d at setup completion, not promoted as-is -- its cfg.TokenStorePath still points at the ephemeral setup/{hash} dir, which would go stale the moment anything (e.g. a Profile save) later respawns it")
|
||||
}
|
||||
|
||||
client, err := s.garminFor(newCtx(), u.ID)
|
||||
if err != nil {
|
||||
// A later real use must build a genuinely fresh client, configured
|
||||
// against the permanent {userID} token store directory -- never the
|
||||
// stale ephemeral setup/{hash} one the closed client was carrying.
|
||||
if _, err := s.garminFor(newCtx(), u.ID); err != nil {
|
||||
t.Fatalf("garminFor: %v", err)
|
||||
}
|
||||
if client != m {
|
||||
t.Error("expected garminFor to return the promoted (already-authenticated) client")
|
||||
wantTokenStorePath := filepath.Join(tokenStoreRoot, itoa(u.ID))
|
||||
var gotTokenStorePath string
|
||||
for _, cfg := range factoryConfigs {
|
||||
if cfg.GarminEmail == "runner@example.com" {
|
||||
gotTokenStorePath = cfg.TokenStorePath
|
||||
}
|
||||
}
|
||||
if gotTokenStorePath != wantTokenStorePath {
|
||||
t.Errorf("garminFor built client with TokenStorePath = %q, want %q", gotTokenStorePath, wantTokenStorePath)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user