daili-usage-keeper / internal /api /usage_overview_test.go
pjpjq's picture
fix: build usage keeper from source
b034029 verified
package api
import (
"context"
"net/http"
"net/http/httptest"
"testing"
"time"
"cpa-usage-keeper/internal/cpa"
"cpa-usage-keeper/internal/service"
)
type usageFilterStub struct {
usage *cpa.StatisticsSnapshot
overview *service.UsageOverviewSnapshot
err error
lastFilter service.UsageFilter
filterCalls int
overviewCalls int
}
func (s *usageFilterStub) GetUsageWithFilter(_ context.Context, filter service.UsageFilter) (*cpa.StatisticsSnapshot, error) {
s.lastFilter = filter
s.filterCalls++
return s.usage, s.err
}
func (s *usageFilterStub) GetUsageOverview(_ context.Context, filter service.UsageFilter) (*service.UsageOverviewSnapshot, error) {
s.lastFilter = filter
s.overviewCalls++
return s.overview, s.err
}
func (s *usageFilterStub) ListUsageEvents(context.Context, service.UsageFilter) (*service.UsageEventsPage, error) {
return nil, s.err
}
func (s *usageFilterStub) ListUsageEventFilterOptions(context.Context, service.UsageFilter) (*service.UsageEventFilterOptions, error) {
return nil, s.err
}
func (s *usageFilterStub) ListUsageCredentialStats(context.Context, service.UsageFilter) ([]service.UsageCredentialStat, error) {
return nil, s.err
}
func (s *usageFilterStub) GetUsageAnalysis(context.Context, service.UsageFilter) (*service.UsageAnalysisSnapshot, error) {
return nil, s.err
}
func mustParseTime(t *testing.T, value string) time.Time {
t.Helper()
parsed, err := time.Parse(time.RFC3339, value)
if err != nil {
t.Fatalf("time.Parse returned error: %v", err)
}
return parsed
}
func TestUsageOverviewResponseIncludesResolvedRangeAndTimezone(t *testing.T) {
previousLocal := time.Local
location, err := time.LoadLocation("Asia/Shanghai")
if err != nil {
t.Fatalf("load location: %v", err)
}
t.Cleanup(func() { time.Local = previousLocal })
time.Local = location
provider := &usageFilterStub{overview: &service.UsageOverviewSnapshot{}}
router := NewRouter(nil, nil, provider, nil, AuthConfig{}, nil, "")
req := httptest.NewRequest(http.MethodGet, "/api/v1/usage/overview?range=custom&start=2026-04-20&end=2026-04-21", nil)
resp := httptest.NewRecorder()
router.ServeHTTP(resp, req)
if resp.Code != http.StatusOK {
t.Fatalf("expected status 200, got %d", resp.Code)
}
expectedStart := time.Date(2026, 4, 20, 0, 0, 0, 0, location).UTC().Format(time.RFC3339Nano)
expectedEnd := time.Date(2026, 4, 22, 0, 0, 0, 0, location).Add(-time.Nanosecond).UTC().Format(time.RFC3339Nano)
body := resp.Body.String()
if !contains(body, `"timezone":"Asia/Shanghai"`) || !contains(body, `"range_start":"`+expectedStart+`"`) || !contains(body, `"range_end":"`+expectedEnd+`"`) {
t.Fatalf("expected overview response to include resolved range and timezone, got %s", body)
}
}
func TestUsageOverviewReturnsFilteredSnapshot(t *testing.T) {
provider := &usageFilterStub{overview: &service.UsageOverviewSnapshot{
Usage: &cpa.StatisticsSnapshot{
TotalRequests: 1,
SuccessCount: 1,
TotalTokens: 20,
RequestsByHour: map[string]int64{
"2026-04-22T11:00:00Z": 1,
},
TokensByHour: map[string]int64{
"2026-04-22T11:00:00Z": 20,
},
APIs: map[string]cpa.APISnapshot{
"provider-a": {
TotalRequests: 1,
SuccessCount: 1,
TotalTokens: 20,
Models: map[string]cpa.ModelSnapshot{
"claude-sonnet": {
TotalRequests: 1,
SuccessCount: 1,
TotalTokens: 20,
},
},
},
},
},
Summary: service.UsageOverviewSummary{
RequestCount: 1,
TokenCount: 20,
WindowMinutes: 1440,
RPM: 1.0 / 1440.0,
TPM: 20.0 / 1440.0,
TotalCost: 0.123,
CostAvailable: true,
CachedTokens: 2,
ReasoningTokens: 3,
},
Series: service.UsageOverviewSeries{
Requests: map[string]int64{"2026-04-22T11:00:00Z": 1},
Tokens: map[string]int64{"2026-04-22T11:00:00Z": 20},
RPM: map[string]float64{"2026-04-22T11:00:00Z": 1.0 / 60.0},
TPM: map[string]float64{"2026-04-22T11:00:00Z": 20.0 / 60.0},
Cost: map[string]float64{"2026-04-22T11:00:00Z": 0.123},
InputTokens: map[string]int64{"2026-04-22T11:00:00Z": 11},
OutputTokens: map[string]int64{"2026-04-22T11:00:00Z": 7},
CachedTokens: map[string]int64{"2026-04-22T11:00:00Z": 2},
ReasoningTokens: map[string]int64{"2026-04-22T11:00:00Z": 3},
},
Health: service.UsageOverviewHealth{
TotalSuccess: 1,
TotalFailure: 0,
SuccessRate: 100,
BlockDetails: []service.UsageOverviewHealthBlock{{
StartTime: mustParseTime(t, "2026-04-22T11:00:00Z"),
EndTime: mustParseTime(t, "2026-04-22T11:15:00Z"),
Success: 1,
Failure: 0,
Rate: 1,
}},
},
}}
router := NewRouter(nil, nil, provider, nil, AuthConfig{}, nil, "")
req := httptest.NewRequest(http.MethodGet, "/api/v1/usage/overview?range=24h", nil)
resp := httptest.NewRecorder()
router.ServeHTTP(resp, req)
if resp.Code != http.StatusOK {
t.Fatalf("expected status 200, got %d", resp.Code)
}
body := resp.Body.String()
if !contains(body, `"usage":`) || !contains(body, `"total_requests":1`) {
t.Fatalf("unexpected response body: %s", body)
}
if !contains(body, `"summary":{"request_count":1,"token_count":20`) {
t.Fatalf("expected backend summary in response body: %s", body)
}
if !contains(body, `"cost_available":true`) {
t.Fatalf("expected backend cost availability in response body: %s", body)
}
if !contains(body, `"series":{"requests":{"2026-04-22T11:00:00Z":1}`) {
t.Fatalf("expected backend series in response body: %s", body)
}
if !contains(body, `"input_tokens":{"2026-04-22T11:00:00Z":11}`) ||
!contains(body, `"output_tokens":{"2026-04-22T11:00:00Z":7}`) ||
!contains(body, `"cached_tokens":{"2026-04-22T11:00:00Z":2}`) ||
!contains(body, `"reasoning_tokens":{"2026-04-22T11:00:00Z":3}`) {
t.Fatalf("expected token breakdown series in response body: %s", body)
}
if !contains(body, `"service_health":{"total_success":1,"total_failure":0,"success_rate":100`) ||
!contains(body, `"block_details":[{"start_time":"2026-04-22T11:00:00Z","end_time":"2026-04-22T11:15:00Z","success":1,"failure":0,"rate":1}]`) {
t.Fatalf("expected service health in response body: %s", body)
}
if contains(body, `"details":`) {
t.Fatalf("expected overview response to omit request details: %s", body)
}
if provider.filterCalls != 0 {
t.Fatalf("expected GetUsageWithFilter not to be called, got %d", provider.filterCalls)
}
if provider.overviewCalls != 1 {
t.Fatalf("expected GetUsageOverview to be called once, got %d", provider.overviewCalls)
}
if provider.lastFilter.Range != "24h" {
t.Fatalf("expected range to be passed through, got %+v", provider.lastFilter)
}
if provider.lastFilter.StartTime == nil || provider.lastFilter.EndTime == nil {
t.Fatalf("expected resolved time bounds in filter, got %+v", provider.lastFilter)
}
}