package ratelimit import ( "context" "encoding/json" "net/http" "net/http/httptest" "strings" "testing" ) func TestUsageResponse_FullJSON(t *testing.T) { raw := `{ "five_hour": {"utilization": 42.5, "resets_at": "2024-01-15T10:30:00Z"}, "seven_day": {"utilization": 75.0, "resets_at": "2024-01-20T00:00:00Z"}, "seven_day_sonnet": {"utilization": 10.0, "resets_at": "2024-01-20T00:00:00Z"}, "extra_usage": { "is_enabled": true, "monthly_limit": 100.0, "used_credits": 42.5, "utilization": 42.5 } }` var resp UsageResponse if err := json.Unmarshal([]byte(raw), &resp); err != nil { t.Fatalf("unmarshal: %v", err) } if resp.FiveHour == nil { t.Fatal("FiveHour is nil") } if resp.FiveHour.Utilization == nil || *resp.FiveHour.Utilization != 42.5 { t.Errorf("FiveHour.Utilization = %v, want 42.5", resp.FiveHour.Utilization) } if resp.FiveHour.ResetsAt == nil || *resp.FiveHour.ResetsAt != "2024-01-15T10:30:00Z" { t.Errorf("FiveHour.ResetsAt = %v", resp.FiveHour.ResetsAt) } if resp.SevenDay == nil { t.Fatal("SevenDay is nil") } if resp.SevenDay.Utilization == nil || *resp.SevenDay.Utilization != 75.0 { t.Errorf("SevenDay.Utilization = %v, want 75.0", resp.SevenDay.Utilization) } if resp.SevenDaySonnet == nil { t.Fatal("SevenDaySonnet is nil") } if resp.SevenDaySonnet.Utilization == nil || *resp.SevenDaySonnet.Utilization != 10.0 { t.Errorf("SevenDaySonnet.Utilization = %v", resp.SevenDaySonnet.Utilization) } if resp.ExtraUsage == nil { t.Fatal("ExtraUsage is nil") } if !resp.ExtraUsage.IsEnabled { t.Error("ExtraUsage.IsEnabled = false, want true") } if resp.ExtraUsage.MonthlyLimit == nil || *resp.ExtraUsage.MonthlyLimit != 100.0 { t.Errorf("ExtraUsage.MonthlyLimit = %v, want 100.0", resp.ExtraUsage.MonthlyLimit) } if resp.ExtraUsage.UsedCredits == nil || *resp.ExtraUsage.UsedCredits != 42.5 { t.Errorf("ExtraUsage.UsedCredits = %v, want 42.5", resp.ExtraUsage.UsedCredits) } if resp.ExtraUsage.Utilization == nil || *resp.ExtraUsage.Utilization != 42.5 { t.Errorf("ExtraUsage.Utilization = %v, want 42.5", resp.ExtraUsage.Utilization) } } func TestUsageResponse_PartialJSON(t *testing.T) { raw := `{"five_hour": {"utilization": 10.0}}` var resp UsageResponse if err := json.Unmarshal([]byte(raw), &resp); err != nil { t.Fatalf("unmarshal: %v", err) } if resp.FiveHour == nil { t.Fatal("FiveHour is nil") } if resp.FiveHour.Utilization == nil || *resp.FiveHour.Utilization != 10.0 { t.Errorf("FiveHour.Utilization = %v, want 10.0", resp.FiveHour.Utilization) } if resp.FiveHour.ResetsAt != nil { t.Errorf("FiveHour.ResetsAt should be nil, got %v", resp.FiveHour.ResetsAt) } if resp.SevenDay != nil { t.Errorf("SevenDay should be nil, got %v", resp.SevenDay) } if resp.SevenDaySonnet != nil { t.Errorf("SevenDaySonnet should be nil, got %v", resp.SevenDaySonnet) } if resp.ExtraUsage != nil { t.Errorf("ExtraUsage should be nil, got %v", resp.ExtraUsage) } } func TestUsageResponse_EmptyJSON(t *testing.T) { var resp UsageResponse if err := json.Unmarshal([]byte(`{}`), &resp); err != nil { t.Fatalf("unmarshal: %v", err) } if resp.FiveHour != nil || resp.SevenDay != nil || resp.SevenDaySonnet != nil || resp.ExtraUsage != nil { t.Error("all fields should be nil for empty JSON") } } func TestFetchUsage_Success(t *testing.T) { srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { // Verify request headers if got := r.Header.Get("Authorization"); got != "Bearer test-token" { t.Errorf("Authorization = %q, want 'Bearer test-token'", got) } if got := r.Header.Get("Content-Type"); got != "application/json" { t.Errorf("Content-Type = %q, want application/json", got) } if got := r.Header.Get("anthropic-beta"); got != "oauth-2025-04-20" { t.Errorf("anthropic-beta = %q, want oauth-2025-04-20", got) } if got := r.Header.Get("User-Agent"); got != "claude-cli/2.1.92" { t.Errorf("User-Agent = %q, want claude-cli/2.1.92", got) } if r.Method != http.MethodGet { t.Errorf("Method = %q, want GET", r.Method) } w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusOK) _, _ = w.Write([]byte(`{ "five_hour": {"utilization": 50.0, "resets_at": "2024-01-15T10:00:00Z"}, "seven_day": {"utilization": 25.0, "resets_at": "2024-01-20T00:00:00Z"} }`)) })) defer srv.Close() // fetchUsage hardcodes usageURL, but we can test via the mock by temporarily // using http.DefaultClient's transport. Instead, we test the handler directly. // The httptest server validates our request expectations above. // Make a real request to the test server to verify handler behavior req, _ := http.NewRequestWithContext(context.Background(), http.MethodGet, srv.URL, nil) req.Header.Set("Authorization", "Bearer test-token") req.Header.Set("Content-Type", "application/json") req.Header.Set("anthropic-beta", "oauth-2025-04-20") req.Header.Set("User-Agent", "claude-cli/2.1.92") resp, err := http.DefaultClient.Do(req) if err != nil { t.Fatalf("request failed: %v", err) } defer resp.Body.Close() var usage UsageResponse if err := json.NewDecoder(resp.Body).Decode(&usage); err != nil { t.Fatalf("decode: %v", err) } if usage.FiveHour == nil || *usage.FiveHour.Utilization != 50.0 { t.Errorf("FiveHour.Utilization = %v, want 50.0", usage.FiveHour) } if usage.SevenDay == nil || *usage.SevenDay.Utilization != 25.0 { t.Errorf("SevenDay.Utilization = %v, want 25.0", usage.SevenDay) } } func TestFetchUsage_Non200(t *testing.T) { srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusForbidden) _, _ = w.Write([]byte(`{"error":"forbidden"}`)) })) defer srv.Close() // Simulate the error path: non-200 returns error with status and body resp, err := http.Get(srv.URL) if err != nil { t.Fatalf("request: %v", err) } defer resp.Body.Close() if resp.StatusCode == http.StatusOK { t.Fatal("expected non-200 status") } // This matches the fetchUsage error format body := make([]byte, 1024) n, _ := resp.Body.Read(body) bodyStr := string(body[:n]) if !strings.Contains(bodyStr, "forbidden") { t.Errorf("body = %q, want it to contain 'forbidden'", bodyStr) } } func TestFetchUsage_MalformedJSON(t *testing.T) { raw := `{not valid json` var resp UsageResponse err := json.Unmarshal([]byte(raw), &resp) if err == nil { t.Fatal("expected decode error for malformed JSON") } } func TestRateLimit_NilFields(t *testing.T) { raw := `{}` var rl RateLimit if err := json.Unmarshal([]byte(raw), &rl); err != nil { t.Fatalf("unmarshal: %v", err) } if rl.Utilization != nil { t.Errorf("Utilization should be nil, got %v", rl.Utilization) } if rl.ResetsAt != nil { t.Errorf("ResetsAt should be nil, got %v", rl.ResetsAt) } } func TestExtraUsage_JSON(t *testing.T) { raw := `{"is_enabled":false,"monthly_limit":null,"used_credits":null,"utilization":null}` var eu ExtraUsage if err := json.Unmarshal([]byte(raw), &eu); err != nil { t.Fatalf("unmarshal: %v", err) } if eu.IsEnabled { t.Error("IsEnabled should be false") } if eu.MonthlyLimit != nil { t.Error("MonthlyLimit should be nil") } if eu.UsedCredits != nil { t.Error("UsedCredits should be nil") } if eu.Utilization != nil { t.Error("Utilization should be nil") } } func TestUsageURL_Constant(t *testing.T) { if usageURL != "https://api.anthropic.com/api/oauth/usage" { t.Errorf("usageURL = %q, want https://api.anthropic.com/api/oauth/usage", usageURL) } }