test: add unit tests for billing, websearch, and notify systems

Billing (25 tests):
- CalculateCostUnified: nil resolver fallback, token/per_request/image modes
- GetModelPricingWithChannel: nil/partial/full channel overrides
- resolveAccountStatsCost: four-level priority chain integration tests

WebSearch (18 tests):
- PopulateWebSearchUsage: nil input, manager states, QuotaLimit nil/*int64
- ResetWebSearchUsage: nil manager error
- Manager.ResetUsage: nil Redis
- shouldEmulateWebSearch: full decision chain (8 scenarios)

Notify (36 tests):
- ParseNotifyEmails/MarshalNotifyEmails: old/new format, roundtrip
- crossedDownward: boundary values, threshold semantics
- checkQuotaDimCrossings: mixed dimensions, disabled/zero skip
This commit is contained in:
erio
2026-04-14 08:42:28 +08:00
parent 7c7292935e
commit 9028d2085f
8 changed files with 1369 additions and 0 deletions

View File

@@ -178,3 +178,227 @@ func TestGetSiteName_Configured(t *testing.T) {
repo.data[SettingKeySiteName] = "My Site"
require.Equal(t, "My Site", s.getSiteName(context.Background()))
}
// ---------- crossedDownward ----------
func TestCrossedDownward_CrossesBelow(t *testing.T) {
// oldBalance > threshold, newBalance < threshold → true
require.True(t, crossedDownward(100, 5, 10))
}
func TestCrossedDownward_ExactlyAtThreshold(t *testing.T) {
// oldBalance > threshold, newBalance == threshold → false (not below)
require.False(t, crossedDownward(100, 10, 10))
}
func TestCrossedDownward_OldExactlyAtThreshold_NewBelow(t *testing.T) {
// oldBalance == threshold, newBalance < threshold → true
// (at-or-above → below counts as a crossing)
require.True(t, crossedDownward(10, 5, 10))
}
func TestCrossedDownward_AlreadyBelow(t *testing.T) {
// oldBalance < threshold → false (already below, no new crossing)
require.False(t, crossedDownward(5, 3, 10))
}
func TestCrossedDownward_BothAbove(t *testing.T) {
// oldBalance > threshold, newBalance > threshold → false (no crossing)
require.False(t, crossedDownward(100, 50, 10))
}
func TestCrossedDownward_ZeroThreshold(t *testing.T) {
// threshold == 0 → oldV >= 0 is always true, but newV < 0 only for negatives
// Typical case: positive balances should not fire when threshold is 0.
require.False(t, crossedDownward(10, 5, 0))
require.False(t, crossedDownward(0, 0, 0))
}
func TestCrossedDownward_ZeroThreshold_NegativeNew(t *testing.T) {
// Edge case: newBalance goes negative with threshold=0.
require.True(t, crossedDownward(5, -1, 0))
}
func TestCrossedDownward_NegativeValues(t *testing.T) {
// Both already negative, threshold is positive → no crossing (already below).
require.False(t, crossedDownward(-5, -10, 10))
}
func TestCrossedDownward_LargeDecrement(t *testing.T) {
// A single large deduction crosses the threshold.
require.True(t, crossedDownward(1000, 0.5, 100))
}
func TestCrossedDownward_SmallDecrement_NoCrossing(t *testing.T) {
// A tiny deduction stays above threshold.
require.False(t, crossedDownward(100, 99.99, 10))
}
// ---------- checkQuotaDimCrossings ----------
func TestCheckQuotaDimCrossings_NoDimensions(t *testing.T) {
s, _ := newBalanceNotifyServiceForTest()
account := &Account{ID: 1, Name: "test", Platform: PlatformAnthropic}
// Empty dims → no crossing, no panic.
s.checkQuotaDimCrossings(account, nil, 10, []string{"admin@example.com"}, "TestSite")
s.checkQuotaDimCrossings(account, []quotaDim{}, 10, []string{"admin@example.com"}, "TestSite")
}
func TestCheckQuotaDimCrossings_DisabledDimension(t *testing.T) {
s, _ := newBalanceNotifyServiceForTest()
account := &Account{ID: 1, Name: "test", Platform: PlatformAnthropic}
dims := []quotaDim{
{
name: quotaDimDaily,
enabled: false, // disabled
threshold: 100,
thresholdType: thresholdTypeFixed,
currentUsed: 950,
limit: 1000,
},
}
// Disabled dimension should be skipped even if crossing would occur.
s.checkQuotaDimCrossings(account, dims, 50, []string{"admin@example.com"}, "TestSite")
}
func TestCheckQuotaDimCrossings_ZeroThresholdSkipped(t *testing.T) {
s, _ := newBalanceNotifyServiceForTest()
account := &Account{ID: 1, Name: "test", Platform: PlatformAnthropic}
dims := []quotaDim{
{
name: quotaDimDaily,
enabled: true,
threshold: 0, // zero threshold
thresholdType: thresholdTypeFixed,
currentUsed: 950,
limit: 1000,
},
}
// Zero threshold → skipped.
s.checkQuotaDimCrossings(account, dims, 50, []string{"admin@example.com"}, "TestSite")
}
func TestCheckQuotaDimCrossings_NoCrossing_BothBelowThreshold(t *testing.T) {
s, _ := newBalanceNotifyServiceForTest()
account := &Account{ID: 1, Name: "test", Platform: PlatformAnthropic}
// threshold=400 remaining, limit=1000 → effectiveThreshold = 600 (usage trigger)
// currentUsed=300 (after), oldUsed=300-50=250 (before). Both < 600, no crossing.
dims := []quotaDim{
{
name: quotaDimDaily,
enabled: true,
threshold: 400,
thresholdType: thresholdTypeFixed,
currentUsed: 300,
limit: 1000,
},
}
s.checkQuotaDimCrossings(account, dims, 50, []string{"admin@example.com"}, "TestSite")
}
func TestCheckQuotaDimCrossings_NoCrossing_BothAboveThreshold(t *testing.T) {
s, _ := newBalanceNotifyServiceForTest()
account := &Account{ID: 1, Name: "test", Platform: PlatformAnthropic}
// threshold=400 remaining, limit=1000 → effectiveThreshold = 600 (usage trigger)
// currentUsed=800 (after), oldUsed=800-50=750 (before). Both >= 600, no crossing.
dims := []quotaDim{
{
name: quotaDimDaily,
enabled: true,
threshold: 400,
thresholdType: thresholdTypeFixed,
currentUsed: 800,
limit: 1000,
},
}
s.checkQuotaDimCrossings(account, dims, 50, []string{"admin@example.com"}, "TestSite")
}
func TestCheckQuotaDimCrossings_NegativeResolvedThreshold_Skipped(t *testing.T) {
s, _ := newBalanceNotifyServiceForTest()
account := &Account{ID: 1, Name: "test", Platform: PlatformAnthropic}
// threshold=1200 remaining, limit=1000 → effectiveThreshold = 1000-1200 = -200
// Negative resolved threshold → skipped.
dims := []quotaDim{
{
name: quotaDimDaily,
enabled: true,
threshold: 1200,
thresholdType: thresholdTypeFixed,
currentUsed: 950,
limit: 1000,
},
}
s.checkQuotaDimCrossings(account, dims, 50, []string{"admin@example.com"}, "TestSite")
}
func TestCheckQuotaDimCrossings_PercentageThreshold_NoCrossing(t *testing.T) {
s, _ := newBalanceNotifyServiceForTest()
account := &Account{ID: 1, Name: "test", Platform: PlatformAnthropic}
// threshold=30%, limit=1000 → effectiveThreshold = 1000 * (1 - 0.30) = 700
// currentUsed=500, oldUsed=500-50=450. Both < 700, no crossing.
dims := []quotaDim{
{
name: quotaDimWeekly,
enabled: true,
threshold: 30,
thresholdType: thresholdTypePercentage,
currentUsed: 500,
limit: 1000,
},
}
s.checkQuotaDimCrossings(account, dims, 50, []string{"admin@example.com"}, "TestSite")
}
func TestCheckQuotaDimCrossings_ZeroLimit_Skipped(t *testing.T) {
s, _ := newBalanceNotifyServiceForTest()
account := &Account{ID: 1, Name: "test", Platform: PlatformAnthropic}
// limit=0 → resolvedThreshold returns 0 → skipped.
dims := []quotaDim{
{
name: quotaDimTotal,
enabled: true,
threshold: 100,
thresholdType: thresholdTypeFixed,
currentUsed: 50,
limit: 0,
},
}
s.checkQuotaDimCrossings(account, dims, 50, []string{"admin@example.com"}, "TestSite")
}
func TestCheckQuotaDimCrossings_MultipleDims_MixedResults(t *testing.T) {
s, _ := newBalanceNotifyServiceForTest()
account := &Account{ID: 1, Name: "test", Platform: PlatformAnthropic}
// dim1: no crossing (both below effective threshold)
// dim2: disabled (skipped)
// dim3: zero threshold (skipped)
dims := []quotaDim{
{
name: quotaDimDaily,
enabled: true,
threshold: 400,
thresholdType: thresholdTypeFixed,
currentUsed: 300, // oldUsed=250, effectiveThreshold=600, both below
limit: 1000,
},
{
name: quotaDimWeekly,
enabled: false,
threshold: 100,
thresholdType: thresholdTypeFixed,
currentUsed: 900,
limit: 1000,
},
{
name: quotaDimTotal,
enabled: true,
threshold: 0,
thresholdType: thresholdTypeFixed,
currentUsed: 500,
limit: 1000,
},
}
// None should trigger. No panic expected.
s.checkQuotaDimCrossings(account, dims, 50, []string{"admin@example.com"}, "TestSite")
}