package app import ( "context" "encoding/json" "errors" "fmt" "net/http" "net/http/httptest" "strings" "sync" "sync/atomic" "testing" "time" "codex-helper/internal/cliproxy" "codex-helper/internal/security" "codex-helper/internal/store" ) type fakeCPAClient struct { mu sync.Mutex configured bool snapshots map[string]cliproxy.Snapshot errors map[string]error calls []string } func (f *fakeCPAClient) Configured() bool { return f.configured } func (f *fakeCPAClient) Snapshot(_ context.Context, authIndex string) (cliproxy.Snapshot, error) { return f.snapshot(authIndex) } func (f *fakeCPAClient) snapshot(authIndex string) (cliproxy.Snapshot, error) { f.mu.Lock() defer f.mu.Unlock() f.calls = append(f.calls, authIndex) if err := f.errors[authIndex]; err != nil { return cliproxy.Snapshot{}, err } snapshot, ok := f.snapshots[authIndex] if !ok { return cliproxy.Snapshot{}, errors.New("auth 不存在") } return snapshot, nil } func testSnapshot(authIndex string) cliproxy.Snapshot { email := authIndex + "@example.com" plan := "plus" lifetime := int64(1234) peak := int64(321) return cliproxy.Snapshot{ Auth: cliproxy.AuthFile{AuthIndex: authIndex, Label: "CPA 标签", Email: &email, AccountID: "acct-1", PlanType: &plan}, Limits: []cliproxy.Limit{{LimitID: "codex", WindowType: "primary", UsedPercent: 12.5, WindowDurationMinutes: 300, ResetsAt: time.Now().Add(time.Hour).Unix(), PlanType: &plan}}, Summary: cliproxy.UsageSummary{LifetimeTokens: &lifetime, PeakDailyTokens: &peak}, Usage: []cliproxy.UsagePoint{{Date: time.Now().UTC().Format("2006-01-02"), TotalTokens: 100}}, ProfileAvailable: true, UsageAvailable: true, FetchedAt: time.Now().UTC(), } } type blockingCPAClient struct { started chan struct{} release chan struct{} active atomic.Int32 maximum atomic.Int32 } func (f *blockingCPAClient) Configured() bool { return true } func (f *blockingCPAClient) Snapshot(_ context.Context, authIndex string) (cliproxy.Snapshot, error) { active := f.active.Add(1) defer f.active.Add(-1) for { maximum := f.maximum.Load() if active <= maximum || f.maximum.CompareAndSwap(maximum, active) { break } } f.started <- struct{}{} <-f.release return testSnapshot(authIndex), nil } func configureTestCPA(a *App, authIndexes ...string) *fakeCPAClient { client := &fakeCPAClient{configured: true, snapshots: map[string]cliproxy.Snapshot{}, errors: map[string]error{}} for _, authIndex := range authIndexes { client.snapshots[authIndex] = testSnapshot(authIndex) } a.cpa = client return client } func authenticatedRequest(t *testing.T, a *App, method, path, body string) *http.Request { t.Helper() if err := a.store.Set("initialized", "true"); err != nil { t.Fatal(err) } session := "test-session" if _, err := a.store.DB.Exec("INSERT OR IGNORE INTO sessions(token_hash,expires_at,created_at) VALUES(?,?,?)", security.HashToken(session), time.Now().Add(time.Hour).Unix(), time.Now().Unix()); err != nil { t.Fatal(err) } request := httptest.NewRequest(method, path, strings.NewReader(body)) request.AddCookie(&http.Cookie{Name: "session", Value: session}) if method != http.MethodGet && method != http.MethodHead { request.Header.Set("X-Requested-With", "codex-helper") } return request } func TestSystemStatusUsesCPAFlagAndRejectsNonGET(t *testing.T) { a := newReminderTestApp(t) a.cpa = &fakeCPAClient{configured: true} recorder := httptest.NewRecorder() a.api(recorder, httptest.NewRequest(http.MethodGet, "/api/v1/system/status", nil)) if recorder.Code != http.StatusOK || !strings.Contains(recorder.Body.String(), `"cpa":true`) || strings.Contains(recorder.Body.String(), "appServer") { t.Fatalf("status = %d body = %s", recorder.Code, recorder.Body.String()) } recorder = httptest.NewRecorder() a.api(recorder, httptest.NewRequest(http.MethodPost, "/api/v1/system/status", nil)) if recorder.Code != http.StatusMethodNotAllowed { t.Fatalf("status = %d body = %s", recorder.Code, recorder.Body.String()) } } func TestSystemStatusReturnsBuildVersion(t *testing.T) { a := newReminderTestApp(t) originalVersion := Version Version = "1.2.3-test" t.Cleanup(func() { Version = originalVersion }) recorder := httptest.NewRecorder() a.api(recorder, httptest.NewRequest(http.MethodGet, "/api/v1/system/status", nil)) if recorder.Code != http.StatusOK { t.Fatalf("status = %d body = %s", recorder.Code, recorder.Body.String()) } var body struct { Version string `json:"version"` } if err := json.Unmarshal(recorder.Body.Bytes(), &body); err != nil { t.Fatal(err) } if body.Version != "1.2.3-test" { t.Fatalf("version = %q", body.Version) } } func TestAnonymousOverviewRedactsCPAAndIdentityData(t *testing.T) { a := newReminderTestApp(t) if err := a.store.Set("initialized", "true"); err != nil { t.Fatal(err) } account, err := a.store.CreateAccountWithVisibility("公开账号", "private-cpa-index", "personal", true) if err != nil { t.Fatal(err) } email, plan, authMode := "owner@example.com", "plus", "cliproxyapi" if err := a.store.UpdateAccount(account.ID, &email, &plan, true); err != nil { t.Fatal(err) } a.addRuntime(account.ID) a.runtime(account.ID).dash = Dashboard{ AccountID: account.ID, DisplayName: account.DisplayName, Account: AccountView{Email: &email, PlanType: &plan, AuthMode: &authMode, Connected: true}, Limits: []LimitBucket{}, Usage: []UsagePoint{}, ResetCredits: &ResetCreditsSummary{AvailableCount: 2, ExpiresAt: []int64{1784246400}}, FetchedAt: time.Now().Unix(), LastError: "private upstream detail", } accountsRecorder := httptest.NewRecorder() a.api(accountsRecorder, httptest.NewRequest(http.MethodGet, "/api/v1/accounts", nil)) if accountsRecorder.Code != http.StatusOK || strings.Contains(accountsRecorder.Body.String(), "private-cpa-index") || strings.Contains(accountsRecorder.Body.String(), "owner@example.com") { t.Fatalf("anonymous accounts status=%d body=%s", accountsRecorder.Code, accountsRecorder.Body.String()) } dashboardRecorder := httptest.NewRecorder() a.api(dashboardRecorder, httptest.NewRequest(http.MethodGet, "/api/v1/dashboard?accountId="+strconvID(account.ID), nil)) if dashboardRecorder.Code != http.StatusOK { t.Fatalf("dashboard status=%d body=%s", dashboardRecorder.Code, dashboardRecorder.Body.String()) } var dashboard Dashboard if err := json.Unmarshal(dashboardRecorder.Body.Bytes(), &dashboard); err != nil { t.Fatal(err) } if dashboard.Account.Email != nil || dashboard.Account.AuthMode != nil || dashboard.LastError != "" || dashboard.ResetCredits == nil || dashboard.ResetCredits.AvailableCount != 2 { t.Fatalf("anonymous dashboard = %#v", dashboard) } for _, target := range []struct { method string path string }{ {http.MethodGet, "/api/v1/settings/general"}, {http.MethodPost, "/api/v1/accounts"}, {http.MethodPost, "/api/v1/accounts/" + strconvID(account.ID) + "/sync"}, } { recorder := httptest.NewRecorder() a.api(recorder, httptest.NewRequest(target.method, target.path, nil)) if recorder.Code != http.StatusUnauthorized { t.Fatalf("anonymous %s %s status=%d", target.method, target.path, recorder.Code) } } } func TestAccountVisibilityFiltersAnonymousOverviewAndCanBeUpdated(t *testing.T) { a := newReminderTestApp(t) if err := a.store.Set("initialized", "true"); err != nil { t.Fatal(err) } publicAccount, err := a.store.CreateAccountWithVisibility("公开账号", "public-auth", "team", true) if err != nil { t.Fatal(err) } privateAccount, err := a.store.CreateAccount("私有账号", "private-auth", "personal") if err != nil { t.Fatal(err) } a.addRuntime(publicAccount.ID) a.addRuntime(privateAccount.ID) recorder := httptest.NewRecorder() a.api(recorder, httptest.NewRequest(http.MethodGet, "/api/v1/accounts", nil)) var accounts []store.Account if recorder.Code != http.StatusOK || json.Unmarshal(recorder.Body.Bytes(), &accounts) != nil || len(accounts) != 1 || accounts[0].ID != publicAccount.ID { t.Fatalf("anonymous accounts status=%d body=%s", recorder.Code, recorder.Body.String()) } privateDashboard := httptest.NewRecorder() a.api(privateDashboard, httptest.NewRequest(http.MethodGet, "/api/v1/dashboard?accountId="+strconvID(privateAccount.ID), nil)) if privateDashboard.Code != http.StatusNotFound { t.Fatalf("private dashboard status=%d", privateDashboard.Code) } update := httptest.NewRecorder() a.api(update, authenticatedRequest(t, a, http.MethodPut, "/api/v1/accounts/"+strconvID(privateAccount.ID), `{"displayName":"私有账号","expectedKind":"personal","publicVisible":true}`)) if update.Code != http.StatusOK { t.Fatalf("update status=%d body=%s", update.Code, update.Body.String()) } updated, err := a.store.Account(privateAccount.ID) if err != nil || !updated.PublicVisible { t.Fatalf("updated=%#v err=%v", updated, err) } } func TestDashboardSerializesNilListsAsEmptyArrays(t *testing.T) { a := newReminderTestApp(t) account, err := a.store.CreateAccount("账号", "auth-1") if err != nil { t.Fatal(err) } a.addRuntime(account.ID) rt := a.runtime(account.ID) rt.dash.Limits = nil rt.dash.Usage = nil recorder := httptest.NewRecorder() a.api(recorder, authenticatedRequest(t, a, http.MethodGet, "/api/v1/dashboard?accountId="+strconvID(account.ID), "")) if recorder.Code != http.StatusOK { t.Fatalf("status = %d body = %s", recorder.Code, recorder.Body.String()) } var body struct { Limits []LimitBucket `json:"limits"` Usage []UsagePoint `json:"usage"` } if err := json.Unmarshal(recorder.Body.Bytes(), &body); err != nil { t.Fatal(err) } if body.Limits == nil || body.Usage == nil { t.Fatalf("nil lists: %s", recorder.Body.String()) } } func TestCreateAccountRequiresAuthIndexAndImmediatelySyncs(t *testing.T) { a := newReminderTestApp(t) configureTestCPA(a, "auth-1") missing := httptest.NewRecorder() a.api(missing, authenticatedRequest(t, a, http.MethodPost, "/api/v1/accounts", `{"displayName":"A"}`)) if missing.Code != http.StatusBadRequest { t.Fatalf("missing authIndex status = %d body = %s", missing.Code, missing.Body.String()) } recorder := httptest.NewRecorder() a.api(recorder, authenticatedRequest(t, a, http.MethodPost, "/api/v1/accounts", `{"authIndex":"auth-1","expectedKind":"personal","publicVisible":true}`)) if recorder.Code != http.StatusCreated { t.Fatalf("status = %d body = %s", recorder.Code, recorder.Body.String()) } var created store.Account if err := json.Unmarshal(recorder.Body.Bytes(), &created); err != nil { t.Fatal(err) } if created.AuthIndex != "auth-1" || created.DisplayName != "CPA 标签" || !created.Connected || created.Email == nil { t.Fatalf("created = %#v", created) } rt := a.runtime(created.ID) if rt == nil || rt.dash.Stale || rt.dash.Account.AuthMode == nil || *rt.dash.Account.AuthMode != "cliproxyapi" || len(rt.dash.Limits) != 1 || rt.dash.Summary.PeakDailyTokens == nil || *rt.dash.Summary.PeakDailyTokens != 100 { t.Fatalf("dashboard = %#v", rt) } var usageRows int if err := a.store.DB.QueryRow("SELECT COUNT(*) FROM daily_usage WHERE account_id=?", created.ID).Scan(&usageRows); err != nil || usageRows != 1 { t.Fatalf("usage rows = %d err = %v", usageRows, err) } } func TestCreateAccountRejectsDuplicateAuthIndex(t *testing.T) { a := newReminderTestApp(t) configureTestCPA(a, "auth-1") first := httptest.NewRecorder() a.api(first, authenticatedRequest(t, a, http.MethodPost, "/api/v1/accounts", `{"displayName":"A","authIndex":"auth-1"}`)) if first.Code != http.StatusCreated { t.Fatalf("first status = %d body = %s", first.Code, first.Body.String()) } second := httptest.NewRecorder() a.api(second, authenticatedRequest(t, a, http.MethodPost, "/api/v1/accounts", `{"displayName":"B","authIndex":"auth-1"}`)) if second.Code != http.StatusConflict { t.Fatalf("second status = %d body = %s", second.Code, second.Body.String()) } accounts, _ := a.store.Accounts() if len(accounts) != 1 { t.Fatalf("accounts = %#v", accounts) } } func TestFailedCreateDoesNotPersistAccount(t *testing.T) { a := newReminderTestApp(t) client := configureTestCPA(a) client.errors["bad-auth"] = errors.New("CPA upstream unavailable") recorder := httptest.NewRecorder() a.api(recorder, authenticatedRequest(t, a, http.MethodPost, "/api/v1/accounts", `{"displayName":"A","authIndex":"bad-auth"}`)) if recorder.Code != http.StatusBadGateway { t.Fatalf("status = %d body = %s", recorder.Code, recorder.Body.String()) } accounts, _ := a.store.Accounts() if len(accounts) != 0 || len(a.runtimes) != 0 { t.Fatalf("partial account persisted: accounts=%#v runtimes=%d", accounts, len(a.runtimes)) } } func TestSyncSuccessAndFailureUpdatesStaleState(t *testing.T) { a := newReminderTestApp(t) client := configureTestCPA(a, "auth-1") account, err := a.store.CreateAccount("账号", "auth-1") if err != nil { t.Fatal(err) } a.addRuntime(account.ID) if err := a.syncAccount(context.Background(), account.ID); err != nil { t.Fatal(err) } rt := a.runtime(account.ID) if rt.dash.Stale || rt.dash.LastError != "" { t.Fatalf("successful dashboard = %#v", rt.dash) } client.errors["auth-1"] = errors.New("CPA upstream unavailable") if err := a.syncAccount(context.Background(), account.ID); err == nil { t.Fatal("failed sync unexpectedly succeeded") } if !rt.dash.Stale || rt.dash.LastError != "CPA upstream unavailable" || len(rt.dash.Limits) != 1 { t.Fatalf("stale dashboard = %#v", rt.dash) } } func TestSyncPreservesUsageWhenProfileBucketsAreUnavailable(t *testing.T) { a := newReminderTestApp(t) client := configureTestCPA(a, "auth-1") account, err := a.store.CreateAccount("账号", "auth-1") if err != nil { t.Fatal(err) } a.addRuntime(account.ID) if err := a.syncAccount(context.Background(), account.ID); err != nil { t.Fatal(err) } withoutProfile := client.snapshots["auth-1"] updatedLifetime := int64(9999) withoutProfile.ProfileAvailable = true withoutProfile.UsageAvailable = false withoutProfile.Summary = cliproxy.UsageSummary{LifetimeTokens: &updatedLifetime} withoutProfile.Usage = nil withoutProfile.Limits[0].UsedPercent = 55 client.snapshots["auth-1"] = withoutProfile if err := a.syncAccount(context.Background(), account.ID); err != nil { t.Fatal(err) } dashboard := a.runtime(account.ID).dash if len(dashboard.Limits) != 1 || dashboard.Limits[0].UsedPercent != 55 || dashboard.Summary.LifetimeTokens == nil || *dashboard.Summary.LifetimeTokens != 9999 || len(dashboard.Usage) != 1 || dashboard.CurrentCycle == nil { t.Fatalf("dashboard = %#v", dashboard) } } func TestUpdateAuthIndexFailureKeepsOldBinding(t *testing.T) { a := newReminderTestApp(t) client := configureTestCPA(a, "old-auth") client.errors["new-auth"] = errors.New("new auth unavailable") account, err := a.store.CreateAccount("账号", "old-auth") if err != nil { t.Fatal(err) } a.addRuntime(account.ID) if err := a.syncAccount(context.Background(), account.ID); err != nil { t.Fatal(err) } recorder := httptest.NewRecorder() path := "/api/v1/accounts/" + strconvID(account.ID) a.api(recorder, authenticatedRequest(t, a, http.MethodPut, path, `{"displayName":"新名称","authIndex":"new-auth"}`)) if recorder.Code != http.StatusBadGateway { t.Fatalf("status = %d body = %s", recorder.Code, recorder.Body.String()) } stored, err := a.store.Account(account.ID) if err != nil { t.Fatal(err) } if stored.AuthIndex != "old-auth" || stored.DisplayName != "账号" || a.runtime(account.ID).dash.DisplayName != "账号" { t.Fatalf("half-updated account=%#v dashboard=%#v", stored, a.runtime(account.ID).dash) } } func TestUpdateAuthIndexClearsPreviousIdentityHistory(t *testing.T) { a := newReminderTestApp(t) client := configureTestCPA(a, "old-auth", "new-auth") newSnapshot := client.snapshots["new-auth"] newSnapshot.Usage = []cliproxy.UsagePoint{{Date: "2026-08-15", TotalTokens: 999}} client.snapshots["new-auth"] = newSnapshot account, err := a.store.CreateAccount("账号", "old-auth") if err != nil { t.Fatal(err) } a.addRuntime(account.ID) if err := a.syncAccount(context.Background(), account.ID); err != nil { t.Fatal(err) } if _, err := a.store.DB.Exec(`INSERT INTO notifications (dedupe_key,channel,kind,status,scheduled_at,body) VALUES(?, 'configured', 'after', 'sent', 1, '{}')`, strconvID(account.ID)+":codex:primary:1:after"); err != nil { t.Fatal(err) } recorder := httptest.NewRecorder() path := "/api/v1/accounts/" + strconvID(account.ID) a.api(recorder, authenticatedRequest(t, a, http.MethodPut, path, `{"displayName":"新身份","authIndex":"new-auth"}`)) if recorder.Code != http.StatusOK { t.Fatalf("status=%d body=%s", recorder.Code, recorder.Body.String()) } var usageRows, limitRows, notificationRows int if err := a.store.DB.QueryRow("SELECT COUNT(*) FROM daily_usage WHERE account_id=?", account.ID).Scan(&usageRows); err != nil { t.Fatal(err) } if err := a.store.DB.QueryRow("SELECT COUNT(*) FROM limit_snapshots WHERE account_id=?", account.ID).Scan(&limitRows); err != nil { t.Fatal(err) } if err := a.store.DB.QueryRow("SELECT COUNT(*) FROM notifications WHERE dedupe_key GLOB ?", strconvID(account.ID)+":*").Scan(¬ificationRows); err != nil { t.Fatal(err) } var tokens int64 if err := a.store.DB.QueryRow("SELECT total_tokens FROM daily_usage WHERE account_id=? AND date='2026-08-15'", account.ID).Scan(&tokens); err != nil { t.Fatal(err) } if usageRows != 1 || limitRows != 1 || notificationRows != 0 || tokens != 999 { t.Fatalf("rows usage=%d limits=%d notifications=%d tokens=%d", usageRows, limitRows, notificationRows, tokens) } } func TestPublicAccountsDoNotExposeAuthIndex(t *testing.T) { a := newReminderTestApp(t) if err := a.store.Set("initialized", "true"); err != nil { t.Fatal(err) } account, err := a.store.CreateAccountWithVisibility("公开", "secret-auth-index", "any", true) if err != nil { t.Fatal(err) } a.addRuntime(account.ID) recorder := httptest.NewRecorder() a.api(recorder, httptest.NewRequest(http.MethodGet, "/api/v1/accounts", nil)) if recorder.Code != http.StatusOK || strings.Contains(recorder.Body.String(), "secret-auth-index") || strings.Contains(recorder.Body.String(), "authIndex") { t.Fatalf("status = %d body = %s", recorder.Code, recorder.Body.String()) } } func TestDeviceLoginAndLogoutRoutesAreRemoved(t *testing.T) { a := newReminderTestApp(t) account, err := a.store.CreateAccount("账号", "auth-1") if err != nil { t.Fatal(err) } a.addRuntime(account.ID) for _, suffix := range []string{"/login/device", "/logout"} { recorder := httptest.NewRecorder() path := "/api/v1/accounts/" + strconvID(account.ID) + suffix a.api(recorder, authenticatedRequest(t, a, http.MethodPost, path, "")) if recorder.Code != http.StatusNotFound { t.Fatalf("%s status = %d body = %s", suffix, recorder.Code, recorder.Body.String()) } } } func TestCurrentTokenCycleUsesLongestWindowAndFiltersDailyUsage(t *testing.T) { now := time.Date(2026, time.August, 14, 12, 0, 0, 0, time.UTC) reset := time.Date(2026, time.August, 15, 0, 0, 0, 0, time.UTC) cycle := currentTokenCycle( []LimitBucket{{LimitID: "codex", WindowType: "primary", WindowDurationMinutes: 300, ResetsAt: now.Add(5 * time.Hour).Unix()}, {LimitID: "codex", WindowType: "secondary", WindowDurationMinutes: 7 * 24 * 60, ResetsAt: reset.Unix()}}, []UsagePoint{{Date: "2026-08-07", TotalTokens: 50}, {Date: "2026-08-08", TotalTokens: 100}, {Date: "2026-08-10", TotalTokens: 200}, {Date: "2026-08-14", TotalTokens: 300}, {Date: "2026-08-15", TotalTokens: 400}}, now.Unix(), ) if cycle == nil || cycle.WindowType != "secondary" || cycle.TotalTokens != 600 { t.Fatalf("cycle = %#v", cycle) } } func TestCurrentTokenCycleRequiresAValidFutureResetWindow(t *testing.T) { cycle := currentTokenCycle( []LimitBucket{{WindowDurationMinutes: 0, ResetsAt: 1}}, []UsagePoint{{Date: "2026-08-14", TotalTokens: 100}}, time.Date(2026, time.August, 14, 12, 0, 0, 0, time.UTC).Unix(), ) if cycle != nil { t.Fatalf("cycle = %#v; want nil", cycle) } } func TestPeakDailyTokensUsesOnlyTheCurrentTokenCycle(t *testing.T) { now := time.Date(2026, time.August, 14, 12, 0, 0, 0, time.UTC) cycle := &TokenCycle{StartedAt: time.Date(2026, time.August, 8, 0, 0, 0, 0, time.UTC).Unix(), ResetsAt: time.Date(2026, time.August, 15, 0, 0, 0, 0, time.UTC).Unix()} peak := peakDailyTokensForCycle(cycle, []UsagePoint{ {Date: "2026-08-07", TotalTokens: 900}, {Date: "2026-08-08", TotalTokens: 100}, {Date: "2026-08-10", TotalTokens: 200}, {Date: "2026-08-14", TotalTokens: 300}, {Date: "2026-08-15", TotalTokens: 800}, }, now.Unix()) if peak == nil || *peak != 300 { t.Fatalf("peak = %v; want 300", peak) } } func TestSyncAllUsesBoundedConcurrency(t *testing.T) { a := newReminderTestApp(t) client := &blockingCPAClient{started: make(chan struct{}, 6), release: make(chan struct{})} a.cpa = client for i := 0; i < 6; i++ { account, err := a.store.CreateAccount(fmt.Sprintf("账号%d", i), fmt.Sprintf("auth-%d", i)) if err != nil { t.Fatal(err) } a.addRuntime(account.ID) } done := make(chan struct{}) go func() { a.syncAll(context.Background()) close(done) }() for i := 0; i < 4; i++ { select { case <-client.started: case <-time.After(time.Second): t.Fatal("four account syncs did not start concurrently") } } select { case <-client.started: t.Fatal("more than four account syncs started before a slot was released") case <-time.After(50 * time.Millisecond): } close(client.release) select { case <-done: case <-time.After(time.Second): t.Fatal("syncAll did not finish") } if maximum := client.maximum.Load(); maximum != 4 { t.Fatalf("maximum concurrency = %d; want 4", maximum) } } func TestAutomaticSyncIntervalIsFixedAtFiveMinutes(t *testing.T) { a := newReminderTestApp(t) if err := a.store.SetJSON("general", GeneralSettings{SyncMinutes: 60, RetentionDays: 90, BeforeMinutes: 30}); err != nil { t.Fatal(err) } if got := a.general().SyncMinutes; got != automaticSyncMinutes || automaticSyncInterval != 5*time.Minute { t.Fatalf("sync minutes = %d interval = %s", got, automaticSyncInterval) } } func strconvID(id int64) string { return fmt.Sprintf("%d", id) }