api: build per-user garmin.Client/sync.Service via lazy caching

Server no longer holds one fixed Garmin/Sync pair -- garminFor/syncFor
build and cache one instance per user, keyed off their own profile's
Garmin credentials and a per-user token-store subdirectory. The background
incremental sync loop now iterates every provisioned user each tick
instead of syncing one global account.
This commit is contained in:
2026-07-25 18:10:09 +02:00
parent 2cda0adbf8
commit 5930cc4ef5
4 changed files with 216 additions and 92 deletions

View File

@@ -4,11 +4,27 @@ import (
"encoding/json"
"net/http"
"net/http/httptest"
"path/filepath"
"testing"
authmock "geniusrun/backend/internal/auth/mock"
"geniusrun/backend/internal/garmin"
"geniusrun/backend/internal/garmin/mock"
"geniusrun/backend/internal/store"
appsync "geniusrun/backend/internal/sync"
)
func TestSetup_ProvisionsNewUserWithDisplayName(t *testing.T) {
s, db := newTestServer(t)
// This test specifically needs an *unprovisioned* session, unlike every
// other test in this package -- build the server without the
// newTestServer helper's automatic ProvisionUser call.
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{}
s := NewServer(db, func(garmin.Config) garmin.Client { return m }, garmin.Config{}, appsync.Config{}, &authmock.Verifier{}, testSessionConfig)
router := s.Router()
rec := doJSON(t, router, http.MethodGet, "/api/session/me", nil)
@@ -39,7 +55,14 @@ func TestSetup_ProvisionsNewUserWithDisplayName(t *testing.T) {
}
func TestSetup_RejectsEmptyDisplayName(t *testing.T) {
s, _ := newTestServer(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{}
s := NewServer(db, func(garmin.Config) garmin.Client { return m }, garmin.Config{}, appsync.Config{}, &authmock.Verifier{}, testSessionConfig)
rec := doJSON(t, s.Router(), http.MethodPost, "/api/setup", map[string]any{"display_name": ""})
if rec.Code != http.StatusBadRequest {
t.Fatalf("status = %d, want 400", rec.Code)
@@ -47,10 +70,7 @@ func TestSetup_RejectsEmptyDisplayName(t *testing.T) {
}
func TestSetup_RejectsWhenAlreadyProvisioned(t *testing.T) {
s, db := newTestServer(t)
if _, err := db.ProvisionUser(newCtx(), "test-user", "Already Here"); err != nil {
t.Fatalf("ProvisionUser: %v", err)
}
s, _ := newTestServer(t)
rec := doJSON(t, s.Router(), http.MethodPost, "/api/setup", map[string]any{"display_name": "Someone Else"})
if rec.Code != http.StatusConflict {
t.Fatalf("status = %d, want 409, body = %s", rec.Code, rec.Body.String())