package billing import ( "context" "errors" "os" "testing" "time" "data-terminal-cloud/internal/audit" "data-terminal-cloud/internal/database" "data-terminal-cloud/internal/identity" ) func TestValidateUsageOverviewRange(t *testing.T) { now := time.Now().UTC() if _, err := validateUsageOverviewRange("Asia/Shanghai", now.Add(-30*24*time.Hour), now); err != nil { t.Fatalf("valid range: %v", err) } for _, test := range []struct { name string timezone string from time.Time to time.Time }{ {name: "timezone", timezone: "not/a-zone", from: now.Add(-time.Hour), to: now}, {name: "reversed", timezone: "Asia/Shanghai", from: now, to: now.Add(-time.Hour)}, {name: "too long", timezone: "Asia/Shanghai", from: now.Add(-367 * 24 * time.Hour), to: now}, } { t.Run(test.name, func(t *testing.T) { if _, err := validateUsageOverviewRange(test.timezone, test.from, test.to); err == nil { t.Fatal("expected validation error") } }) } } func TestUsageStreaks(t *testing.T) { location, err := time.LoadLocation("Asia/Shanghai") if err != nil { t.Fatal(err) } now := time.Date(2026, 8, 29, 18, 0, 0, 0, location) active := map[string]struct{}{ "2026-08-20": {}, "2026-08-21": {}, "2026-08-26": {}, "2026-08-27": {}, "2026-08-28": {}, "2026-08-29": {}, } current, longest := usageStreaks(active, now) if current != 4 || longest != 4 { t.Fatalf("current=%d longest=%d", current, longest) } current, longest = usageStreaks(map[string]struct{}{"2026-08-20": {}}, now) if current != 0 || longest != 1 { t.Fatalf("stale current=%d longest=%d", current, longest) } } func TestConversationDurationMS(t *testing.T) { start := time.Date(2026, 8, 29, 10, 0, 0, 0, time.UTC) end := start.Add(15*time.Minute + 250*time.Millisecond) if got := conversationDurationMS(start, &end, start.Add(time.Hour)); got != 900250 { t.Fatalf("duration=%d", got) } if got := conversationDurationMS(start, nil, start.Add(3*time.Minute)); got != 180000 { t.Fatalf("active duration=%d", got) } } func TestUsageOverviewScopeIsolationIntegration(t *testing.T) { databaseURL := os.Getenv("TEST_DATABASE_URL") if databaseURL == "" { t.Skip("TEST_DATABASE_URL is not set") } ctx := context.Background() pool, err := database.Open(ctx, databaseURL, "usage-overview-test") if err != nil { t.Fatal(err) } defer pool.Close() service, err := NewService(pool, audit.NewRepository()) if err != nil { t.Fatal(err) } now := time.Now().UTC() owner := identity.Principal{Issuer: billingTestIssuer, Subject: "00000000-0000-0000-0000-000000000101"} var operatorIssuer, operatorSubject string if err = pool.QueryRow(ctx, `SELECT oidc_issuer,oidc_subject FROM user_account WHERE platform_role='ops_admin' AND account_kind='operator' AND status='active' ORDER BY created_at LIMIT 1`).Scan(&operatorIssuer, &operatorSubject); err != nil { t.Fatal(err) } operator := identity.Principal{Issuer: operatorIssuer, Subject: operatorSubject} family, err := service.FamilyUsageOverview(ctx, owner, "10000000-0000-0000-0000-000000000211", billingDevicePublicID, "Asia/Shanghai", now.Add(-365*24*time.Hour), now) if err != nil || family.Scope.HouseholdID != "10000000-0000-0000-0000-000000000211" || family.Scope.DeviceID != billingDevicePublicID { t.Fatalf("family overview=%#v err=%v", family.Scope, err) } if _, err = service.FamilyUsageOverview(ctx, owner, "20000000-0000-0000-0000-000000000211", "", "Asia/Shanghai", now.Add(-24*time.Hour), now); !errors.Is(err, ErrAccessDenied) { t.Fatalf("cross-household error=%v", err) } ops, err := service.OpsUsageOverview(ctx, operator, billingTenantPublicID, "10000000-0000-0000-0000-000000000211", billingDevicePublicID, "Asia/Shanghai", now.Add(-365*24*time.Hour), now) if err != nil || ops.Scope.TenantID != billingTenantPublicID || ops.Scope.HouseholdID == "" || ops.Scope.DeviceID == "" { t.Fatalf("ops overview=%#v err=%v", ops.Scope, err) } if _, err = service.OpsUsageOverview(ctx, operator, billingOtherTenantID, "", "", "Asia/Shanghai", now.Add(-24*time.Hour), now); !errors.Is(err, ErrAccessDenied) { t.Fatalf("cross-tenant error=%v", err) } }