package usage import ( "context" "errors" "fmt" "reflect" "testing" "time" ) func TestPersonalReportFiltersDuplicateAPIAndMockEvents(t *testing.T) { repo := &reportRepository{events: []Event{{ID: "1", JobID: "job", OwnerID: "user", Source: "platform", Capability: "image.generate", Provider: "bailian", CreatedAt: "2026-08-13T01:00:00Z"}, {ID: "duplicate", JobID: "job", OwnerID: "user", Source: "platform", Capability: "image.generate", Provider: "bailian", CreatedAt: "2026-08-13T00:00:00Z"}, {ID: "api", JobID: "api", OwnerID: "user", Source: "api", Capability: "image.generate", Provider: "bailian", CreatedAt: "2026-08-13T00:00:00Z"}, {ID: "mock", JobID: "mock", OwnerID: "user", Source: "platform", Capability: "image.generate", Provider: "mock", CreatedAt: "2026-08-13T00:00:00Z"}}} report, err := (Service{Repository: repo}).Personal(context.Background(), PersonalRequest{AccountID: "user", Preset: PresetToday, Now: time.Date(2026, 8, 13, 1, 0, 0, 0, time.UTC)}) if err != nil { t.Fatal(err) } if report.Total != 1 || len(report.Recent) != 1 || report.ByCapability[0].Count != 1 { t.Fatalf("report=%+v", report) } } func TestAdminReportEmitsEmptyOptionArraysInsteadOfNull(t *testing.T) { report, err := (Service{Repository: &reportRepository{}}).Admin(context.Background(), AdminRequest{Now: time.Date(2026, 8, 13, 1, 0, 0, 0, time.UTC)}) if err != nil { t.Fatal(err) } if report.Options.Organizations == nil || report.Options.Accounts == nil || report.Options.Providers == nil || report.Trend == nil { t.Fatalf("report contains nil collection: %#v", report) } } func TestAdminReportBuildsAccountOptionsFromBaseRangeBeforeReportFilters(t *testing.T) { repo := &reportRepository{events: []Event{ {ID: "selected", JobID: "job-selected", OwnerID: "account-a", Source: "platform", Capability: "image.generate", Provider: "bailian", OrganizationID: "org-a", AccountDisplayName: "甲账号", AccountUsername: "13800000001", CreatedAt: "2026-08-12T01:00:00Z"}, {ID: "same-org", JobID: "job-same-org", OwnerID: "account-b", Source: "platform", Capability: "video.generate", Provider: "seedance", OrganizationID: "org-a", AccountDisplayName: "乙账号", CreatedAt: "2026-08-12T00:00:00Z"}, {ID: "other-org", JobID: "job-other-org", OwnerID: "account-c", Source: "platform", Capability: "image.generate", Provider: "evolink", OrganizationID: "org-b", AccountDisplayName: "丙账号", CreatedAt: "2026-08-11T00:00:00Z"}, }} report, err := (Service{Repository: repo}).Admin(context.Background(), AdminRequest{ Requester: Requester{OrganizationID: "org-a"}, OwnerID: "account-a", Capability: "image.generate", Provider: "bailian", Now: time.Date(2026, 8, 13, 1, 0, 0, 0, time.UTC), }) if err != nil { t.Fatal(err) } if report.Summary.Total != 1 { t.Fatalf("filtered report total = %d, want 1", report.Summary.Total) } want := []Option{{Value: "account-b", Label: "乙账号"}, {Value: "account-a", Label: "甲账号(13800000001)"}} if !reflect.DeepEqual(report.Options.Accounts, want) { t.Fatalf("account options = %#v, want %#v", report.Options.Accounts, want) } } func TestAdminReportIncludesInjectedOrganizationsWithoutUsage(t *testing.T) { source := organizationOptionSourceStub{options: []Option{ {Value: "org-no-usage", Label: "零用量组织"}, {Value: "org-used", Label: "组织目录名称"}, }} repo := &reportRepository{events: []Event{{ID: "used", JobID: "job-used", OwnerID: "account", Source: "platform", Capability: "image.generate", Provider: "bailian", OrganizationID: "org-used", CreatedAt: "2026-08-12T01:00:00Z"}}} report, err := (Service{Repository: repo, OrganizationOptions: source}).Admin(context.Background(), AdminRequest{Now: time.Date(2026, 8, 13, 1, 0, 0, 0, time.UTC)}) if err != nil { t.Fatal(err) } want := []Option{{Value: "org-used", Label: "组织目录名称"}, {Value: "org-no-usage", Label: "零用量组织"}} if !reflect.DeepEqual(report.Options.Organizations, want) { t.Fatalf("organization options = %#v, want %#v", report.Options.Organizations, want) } recent := report.Recent.([]Record) if len(recent) != 1 || recent[0].OrganizationName != "组织目录名称" { t.Fatalf("recent records = %#v", recent) } } func TestAdminReportAggregatesLongRangesByMonth(t *testing.T) { repo := &reportRepository{events: []Event{ {ID: "jan", JobID: "job-jan", OwnerID: "account", Source: "platform", Capability: "image.generate", Provider: "bailian", CreatedAt: "2026-01-31T16:30:00Z"}, {ID: "apr", JobID: "job-apr", OwnerID: "account", Source: "platform", Capability: "image.generate", Provider: "bailian", CreatedAt: "2026-04-01T00:00:00Z"}, }} report, err := (Service{Repository: repo}).Admin(context.Background(), AdminRequest{StartDate: "2026-01-01", EndDate: "2026-04-30"}) if err != nil { t.Fatal(err) } want := []TrendPoint{{Date: "2026-02", Label: "2026-02", Count: 1}, {Date: "2026-04", Label: "2026-04", Count: 1}} if !reflect.DeepEqual(report.Trend, want) { t.Fatalf("trend = %#v, want %#v", report.Trend, want) } } func TestAdminReportRejectsRangesLongerThan3660Days(t *testing.T) { _, err := (Service{Repository: &reportRepository{}}).Admin(context.Background(), AdminRequest{StartDate: "2016-01-01", EndDate: "2026-08-13"}) if !errors.Is(err, ErrDateRangeTooLong) { t.Fatalf("error = %v, want ErrDateRangeTooLong", err) } } func TestAdminReportLimitsRecentAndUsesCanonicalLabels(t *testing.T) { events := make([]Event, 101) for i := range events { provider := "evolink" if i == 0 { provider = "volcengine-visual" } events[i] = Event{ID: fmt.Sprintf("event-%03d", i), JobID: fmt.Sprintf("job-%03d", i), OwnerID: "account", Source: "platform", Capability: "image.generate", Provider: provider, CreatedAt: fmt.Sprintf("2026-08-12T%02d:%02d:00Z", i/60, i%60)} } report, err := (Service{Repository: &reportRepository{events: events}}).Admin(context.Background(), AdminRequest{Now: time.Date(2026, 8, 13, 1, 0, 0, 0, time.UTC)}) if err != nil { t.Fatal(err) } recent := report.Recent.([]Record) if len(recent) != 100 { t.Fatalf("recent length = %d, want 100", len(recent)) } if recent[99].ID != "event-001" { t.Fatalf("last recent id = %q, want event-001", recent[99].ID) } if recent[0].OrganizationID != UnassignedOrganizationID { t.Fatalf("organization id = %q, want %q", recent[0].OrganizationID, UnassignedOrganizationID) } if recent[0].ProviderLabel != "EvoLink" || report.Options.Providers[1] != (Option{Value: "volcengine-visual", Label: "即梦图片"}) { t.Fatalf("provider labels: record=%q options=%#v", recent[0].ProviderLabel, report.Options.Providers) } } func TestAdminReportIncludesRangeLabelAndDeterministicBreakdowns(t *testing.T) { repo := &reportRepository{events: []Event{ {ID: "unassigned", JobID: "job-unassigned", OwnerID: "account-z", AccountDisplayName: "乙账号", Source: "platform", Capability: "image.generate", Provider: "bailian", CreatedAt: "2026-08-12T03:00:00Z"}, {ID: "org-b", JobID: "job-b", OwnerID: "account-b", AccountDisplayName: "甲账号", OrganizationID: "org-b", OrganizationName: "乙组织", Source: "platform", Capability: "image.generate", Provider: "bailian", CreatedAt: "2026-08-12T02:00:00Z"}, {ID: "org-a", JobID: "job-a", OwnerID: "account-a", AccountDisplayName: "丙账号", OrganizationID: "org-a", OrganizationName: "甲组织", Source: "platform", Capability: "image.generate", Provider: "bailian", CreatedAt: "2026-08-12T01:00:00Z"}, }} report, err := (Service{Repository: repo}).Admin(context.Background(), AdminRequest{StartDate: "2026-08-11", EndDate: "2026-08-12"}) if err != nil { t.Fatal(err) } if report.Range.Label != "2026-08-11 至 2026-08-12" { t.Fatalf("range label = %q", report.Range.Label) } organizations := report.Organizations.([]organizationRow) if got := []string{organizations[0].OrganizationID, organizations[1].OrganizationID, organizations[2].OrganizationID}; !reflect.DeepEqual(got, []string{"org-a", "org-b", UnassignedOrganizationID}) { t.Fatalf("organization order = %#v", got) } accounts := report.Accounts.([]accountRow) if got := []string{accounts[0].OwnerID, accounts[1].OwnerID, accounts[2].OwnerID}; !reflect.DeepEqual(got, []string{"account-z", "account-b", "account-a"}) { t.Fatalf("account order = %#v", got) } } type reportRepository struct{ events []Event } func (r *reportRepository) Insert(e Event) (Event, bool, error) { return e, true, nil } func (r *reportRepository) List(Filters) ([]Event, error) { return r.events, nil } type organizationOptionSourceStub struct{ options []Option } func (s organizationOptionSourceStub) ListOrganizationOptions(context.Context, Requester) ([]Option, error) { return s.options, nil }