107 lines
4.1 KiB
Go
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)
|
|
}
|
|
}
|