daili-usage-keeper / internal /api /usage_analysis_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/redact"
"cpa-usage-keeper/internal/service"
)
type usageAnalysisStub struct {
analysis *service.UsageAnalysisSnapshot
err error
lastFilter service.UsageFilter
analysisCalls int
}
func (s *usageAnalysisStub) GetUsageWithFilter(context.Context, service.UsageFilter) (*cpa.StatisticsSnapshot, error) {
return nil, nil
}
func (s *usageAnalysisStub) GetUsageOverview(context.Context, service.UsageFilter) (*service.UsageOverviewSnapshot, error) {
return nil, nil
}
func (s *usageAnalysisStub) ListUsageEvents(context.Context, service.UsageFilter) (*service.UsageEventsPage, error) {
return nil, nil
}
func (s *usageAnalysisStub) ListUsageEventFilterOptions(context.Context, service.UsageFilter) (*service.UsageEventFilterOptions, error) {
return nil, nil
}
func (s *usageAnalysisStub) ListUsageCredentialStats(context.Context, service.UsageFilter) ([]service.UsageCredentialStat, error) {
return nil, nil
}
func (s *usageAnalysisStub) GetUsageAnalysis(_ context.Context, filter service.UsageFilter) (*service.UsageAnalysisSnapshot, error) {
s.lastFilter = filter
s.analysisCalls++
return s.analysis, s.err
}
func TestUsageAnalysisReturnsAggregatedRows(t *testing.T) {
provider := &usageAnalysisStub{analysis: &service.UsageAnalysisSnapshot{
APIs: []service.UsageAnalysisAPIStat{{
APIKey: "provider-a",
DisplayName: "provider-a",
TotalRequests: 2,
SuccessCount: 1,
FailureCount: 1,
TotalTokens: 42,
Models: []service.UsageAnalysisModelStat{{
Model: "claude-sonnet",
TotalRequests: 2,
SuccessCount: 1,
FailureCount: 1,
InputTokens: 30,
OutputTokens: 9,
ReasoningTokens: 2,
CachedTokens: 1,
TotalTokens: 42,
TotalLatencyMS: 350,
LatencySampleCount: 2,
}},
}},
Models: []service.UsageAnalysisModelStat{{
Model: "claude-sonnet",
TotalRequests: 2,
SuccessCount: 1,
FailureCount: 1,
InputTokens: 30,
OutputTokens: 9,
ReasoningTokens: 2,
CachedTokens: 1,
TotalTokens: 42,
TotalLatencyMS: 350,
LatencySampleCount: 2,
}},
}}
router := NewRouter(nil, nil, provider, nil, AuthConfig{}, nil, "")
req := httptest.NewRequest(http.MethodGet, "/api/v1/usage/analysis?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, `"apis":[`) || !contains(body, `"models":[`) {
t.Fatalf("unexpected response body: %s", body)
}
if !contains(body, `"display_name":"prov**er-a"`) {
t.Fatalf("expected display name in response body: %s", body)
}
if !contains(body, `"api_key":"`+redact.APIAlias("provider-a")+`"`) {
t.Fatalf("expected redacted api key alias in response body: %s", body)
}
if !contains(body, `"model":"claude-sonnet"`) || !contains(body, `"latency_sample_count":2`) || !contains(body, `"total_latency_ms":350`) {
t.Fatalf("expected model latency aggregates in response body: %s", body)
}
if provider.analysisCalls != 1 {
t.Fatalf("expected GetUsageAnalysis to be called once, got %d", provider.analysisCalls)
}
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)
}
}
func TestUsageAnalysisRequiresAuthWhenEnabled(t *testing.T) {
router := NewRouter(nil, nil, &usageAnalysisStub{}, nil, AuthConfig{Enabled: true, LoginPassword: "secret", SessionTTL: time.Hour}, nil, "")
req := httptest.NewRequest(http.MethodGet, "/api/v1/usage/analysis", nil)
resp := httptest.NewRecorder()
router.ServeHTTP(resp, req)
if resp.Code != http.StatusUnauthorized {
t.Fatalf("expected status 401, got %d", resp.Code)
}
}