package management import ( "context" "encoding/json" "net/http" "net/http/httptest" "testing" "github.com/gin-gonic/gin" "github.com/router-for-me/CLIProxyAPI/v6/internal/config" coreauth "github.com/router-for-me/CLIProxyAPI/v6/sdk/cliproxy/auth" ) func sumRecentRequestBuckets(buckets []coreauth.RecentRequestBucket) (int64, int64) { var success int64 var failed int64 for _, bucket := range buckets { success += bucket.Success failed += bucket.Failed } return success, failed } func TestGetAPIKeyUsage_GroupsByProviderAndAPIKey(t *testing.T) { t.Setenv("MANAGEMENT_PASSWORD", "") gin.SetMode(gin.TestMode) manager := coreauth.NewManager(nil, nil, nil) if _, err := manager.Register(context.Background(), &coreauth.Auth{ ID: "codex-auth", Provider: "codex", Attributes: map[string]string{ "api_key": "codex-key", "base_url": "https://codex.example.com", }, }); err != nil { t.Fatalf("register codex auth: %v", err) } if _, err := manager.Register(context.Background(), &coreauth.Auth{ ID: "claude-auth", Provider: "claude", Attributes: map[string]string{ "api_key": "claude-key", "base_url": "https://claude.example.com", }, }); err != nil { t.Fatalf("register claude auth: %v", err) } manager.MarkResult(context.Background(), coreauth.Result{AuthID: "codex-auth", Provider: "codex", Model: "gpt-5", Success: true}) manager.MarkResult(context.Background(), coreauth.Result{AuthID: "codex-auth", Provider: "codex", Model: "gpt-5", Success: false}) manager.MarkResult(context.Background(), coreauth.Result{AuthID: "claude-auth", Provider: "claude", Model: "claude-4", Success: true}) h := NewHandlerWithoutConfigFilePath(&config.Config{AuthDir: t.TempDir()}, manager) rec := httptest.NewRecorder() ginCtx, _ := gin.CreateTestContext(rec) req := httptest.NewRequest(http.MethodGet, "/v0/management/api-key-usage", nil) ginCtx.Request = req h.GetAPIKeyUsage(ginCtx) if rec.Code != http.StatusOK { t.Fatalf("status = %d, want %d body=%s", rec.Code, http.StatusOK, rec.Body.String()) } var payload map[string]map[string]apiKeyUsageEntry if err := json.Unmarshal(rec.Body.Bytes(), &payload); err != nil { t.Fatalf("decode payload: %v", err) } codexEntry := payload["codex"]["https://codex.example.com|codex-key"] if codexEntry.Success != 1 || codexEntry.Failed != 1 { t.Fatalf("codex totals = %d/%d, want 1/1", codexEntry.Success, codexEntry.Failed) } if len(codexEntry.RecentRequests) != 20 { t.Fatalf("codex buckets len = %d, want 20", len(codexEntry.RecentRequests)) } codexSuccess, codexFailed := sumRecentRequestBuckets(codexEntry.RecentRequests) if codexSuccess != 1 || codexFailed != 1 { t.Fatalf("codex totals = %d/%d, want 1/1", codexSuccess, codexFailed) } claudeEntry := payload["claude"]["https://claude.example.com|claude-key"] if claudeEntry.Success != 1 || claudeEntry.Failed != 0 { t.Fatalf("claude totals = %d/%d, want 1/0", claudeEntry.Success, claudeEntry.Failed) } if len(claudeEntry.RecentRequests) != 20 { t.Fatalf("claude buckets len = %d, want 20", len(claudeEntry.RecentRequests)) } claudeSuccess, claudeFailed := sumRecentRequestBuckets(claudeEntry.RecentRequests) if claudeSuccess != 1 || claudeFailed != 0 { t.Fatalf("claude totals = %d/%d, want 1/0", claudeSuccess, claudeFailed) } }