Files
story/internal/billing/overview_test.go
T
2026-08-29 21:56:51 +08:00

107 lines
4.1 KiB
Go

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)
}
}