85 lines
3.5 KiB
Go
85 lines
3.5 KiB
Go
package billing
|
|||
|
|
|
||
|
|
import (
|
||
|
|
"context"
|
||
|
|
"errors"
|
||
|
|
"testing"
|
||
|
|
"time"
|
||
|
|
)
|
||
|
|
|
||
|
|
func TestValidatePriceInput(t *testing.T) {
|
||
|
|
input := CreatePriceBookInput{
|
||
|
|
TenantID: "10000000-0000-0000-0000-000000000010", Provider: "mock", Currency: "CNY",
|
||
|
|
SourceRef: "contract://mock/2026-v1", ChangeSummary: "本地回归单价", EffectiveFrom: time.Now().UTC(),
|
||
|
|
Rates: []RateInput{{UsageCategory: "LLM", MetricCode: "input_tokens", Unit: "token", UnitPriceMicros: 2}},
|
||
|
|
}
|
||
|
|
if err := validatePriceInput(input); err != nil {
|
||
|
|
t.Fatalf("valid price input rejected: %v", err)
|
||
|
|
}
|
||
|
|
input.Rates = append(input.Rates, input.Rates[0])
|
||
|
|
if err := validatePriceInput(input); !errors.Is(err, ErrInvalid) {
|
||
|
|
t.Fatalf("duplicate rate error = %v, want ErrInvalid", err)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
func TestValidateQuotaInput(t *testing.T) {
|
||
|
|
valid := QuotaInput{ScopeType: "TENANT", BillingTimezone: "Asia/Shanghai", Currency: "CNY", DailyLimitMicros: 10_000_000, MonthlyLimitMicros: 100_000_000, MaxConcurrentCalls: 10, MaxCallDurationSec: 3600, InProgressAction: "COMPLETE_CURRENT_TURN", ChangeSummary: "家庭试点额度"}
|
||
|
|
if err := validateQuotaInput(valid); err != nil {
|
||
|
|
t.Fatalf("valid quota rejected: %v", err)
|
||
|
|
}
|
||
|
|
for name, mutate := range map[string]func(*QuotaInput){
|
||
|
|
"tenant scope id": func(item *QuotaInput) { item.ScopeID = "10000000-0000-0000-0000-000000000511" },
|
||
|
|
"monthly below daily": func(item *QuotaInput) { item.MonthlyLimitMicros = item.DailyLimitMicros - 1 },
|
||
|
|
"invalid timezone": func(item *QuotaInput) { item.BillingTimezone = "China/Unknown" },
|
||
|
|
"short duration": func(item *QuotaInput) { item.MaxCallDurationSec = 59 },
|
||
|
|
} {
|
||
|
|
t.Run(name, func(t *testing.T) {
|
||
|
|
item := valid
|
||
|
|
mutate(&item)
|
||
|
|
if err := validateQuotaInput(item); !errors.Is(err, ErrInvalid) {
|
||
|
|
t.Fatalf("error = %v, want ErrInvalid", err)
|
||
|
|
}
|
||
|
|
})
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
func TestUsageQuantityPrecision(t *testing.T) {
|
||
|
|
for _, value := range []string{"0", "1", "1000", "0.000001", "999999999999999999.999999"} {
|
||
|
|
if !quantityPattern.MatchString(value) {
|
||
|
|
t.Fatalf("valid quantity %q rejected", value)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
for _, value := range []string{"-1", "01", "1.0000001", "1e3", "9999999999999999999"} {
|
||
|
|
if quantityPattern.MatchString(value) {
|
||
|
|
t.Fatalf("invalid quantity %q accepted", value)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
func TestRecordProviderUsageValidation(t *testing.T) {
|
||
|
|
ctx := context.Background()
|
||
|
|
if _, err := RecordProviderUsageTx(ctx, nil, "", "", "", "", "", time.Time{}, nil); !errors.Is(err, ErrInvalid) {
|
||
|
|
t.Fatalf("empty usage error=%v", err)
|
||
|
|
}
|
||
|
|
metrics := []UsageMetricInput{
|
||
|
|
{UsageCategory: " llm ", MetricCode: " INPUT_TOKENS ", Quantity: "12", Unit: " TOKEN "},
|
||
|
|
{UsageCategory: "LLM", MetricCode: "input_tokens", Quantity: "13", Unit: "token"},
|
||
|
|
}
|
||
|
|
if _, err := RecordProviderUsageTx(ctx, nil,
|
||
|
|
"10000000-0000-0000-0000-000000000001", "10000000-0000-0000-0000-000000000901",
|
||
|
|
"10000000-0000-0000-0000-000000000902", "mock", "usage-validation", time.Now().UTC(), metrics,
|
||
|
|
); !errors.Is(err, ErrInvalid) {
|
||
|
|
t.Fatalf("duplicate metric error=%v", err)
|
||
|
|
}
|
||
|
|
metrics = []UsageMetricInput{{UsageCategory: "UNKNOWN", MetricCode: "input_tokens", Quantity: "1", Unit: "token"}}
|
||
|
|
if _, err := RecordProviderUsageTx(ctx, nil,
|
||
|
|
"10000000-0000-0000-0000-000000000001", "10000000-0000-0000-0000-000000000901",
|
||
|
|
"10000000-0000-0000-0000-000000000902", "mock", "usage-validation", time.Now().UTC(), metrics,
|
||
|
|
); !errors.Is(err, ErrInvalid) {
|
||
|
|
t.Fatalf("invalid category error=%v", err)
|
||
|
|
}
|
||
|
|
if _, err := NewService(nil, nil); err == nil {
|
||
|
|
t.Fatal("invalid billing service dependencies were accepted")
|
||
|
|
}
|
||
|
|
}
|