Files
sub2api/backend/internal/service/openai_quota_reset_credits_test.go
T
李建琦 6d655c9903
Release / update-version (push) Has been cancelled
Release / build-frontend (push) Has been cancelled
Release / release (push) Has been cancelled
Release / sync-version-file (push) Has been cancelled
CI / shell (push) Canceled after 0s
CI / test (push) Canceled after 0s
CI / frontend (push) Canceled after 0s
CI / golangci-lint (push) Canceled after 0s
Security Scan / backend-security (push) Canceled after 0s
Security Scan / frontend-security (push) Canceled after 0s
Sub2API v1.0 - AI API 网关(二开初始版本,基于上游 Wei-Shaw/sub2api)
2026-08-21 18:30:13 +08:00

189 lines
6.2 KiB
Go

package service
import (
"context"
"net/http"
"net/http/httptest"
"testing"
"github.com/stretchr/testify/require"
)
func TestParseOpenAIRateLimitResetCreditDetails_PreservesAvailableCreditOrder(t *testing.T) {
body := []byte(`{
"availableCount":"2",
"credits":[
{"reset_type":"codex_rate_limits","status":"redeemed","expires_at":"2026-07-01T04:05:06Z"},
{"reset_type":"codex_rate_limits","status":"available","expires_at":"2026-07-04T04:05:06Z"},
{"resetType":"codex_rate_limits","status":"available","expiresAt":"2026-07-03T04:05:06Z"},
{"reset_type":"other","status":"available","expires_at":"2026-07-02T04:05:06Z"}
]
}`)
details, err := parseOpenAIRateLimitResetCreditDetails(body)
require.NoError(t, err)
require.NotNil(t, details.AvailableCount)
require.Equal(t, 2, *details.AvailableCount)
require.Equal(t, []OpenAIRateLimitResetCreditDetail{
{ExpiresAt: "2026-07-04T04:05:06Z"},
{ExpiresAt: "2026-07-03T04:05:06Z"},
}, details.Credits)
}
func TestQueryUsageResetCreditCountPrecedence(t *testing.T) {
tests := []struct {
name string
usageBody string
detailBody string
wantCount int
wantCredits int
wantNil bool
}{
{
name: "detail count creates missing usage credits",
usageBody: `{}`,
detailBody: `{"available_count":3,"credits":[{"expires_at":"2026-07-03T04:05:06Z"}]}`,
wantCount: 3, wantCredits: 1,
},
{
name: "explicit detail zero overrides usage and records",
usageBody: `{"rate_limit_reset_credits":{"available_count":4}}`,
detailBody: `{"available_count":0,"credits":[{"expires_at":"2026-07-03T04:05:06Z"}]}`,
wantCount: 0, wantCredits: 1,
},
{
name: "available records override usage when detail count is absent",
usageBody: `{"rate_limit_reset_credits":{"available_count":7}}`,
detailBody: `{"credits":[{"expires_at":"2026-07-03T04:05:06Z"},{"expiresAt":"2026-07-04T04:05:06Z"}]}`,
wantCount: 2, wantCredits: 2,
},
{
name: "empty detail list overrides usage with zero",
usageBody: `{"rate_limit_reset_credits":{"available_count":7}}`,
detailBody: `{"credits":[]}`,
wantCount: 0,
},
{
name: "fully filtered list overrides usage with zero",
usageBody: `{"rate_limit_reset_credits":{"available_count":7}}`,
detailBody: `{"credits":[{"reset_type":"codex_rate_limits","status":"redeemed","expires_at":"2026-07-03T04:05:06Z"},{"reset_type":"other","status":"available","expires_at":"2026-07-04T04:05:06Z"}]}`,
wantCount: 0,
},
{
name: "available records without expiry still count",
usageBody: `{"rate_limit_reset_credits":{"available_count":7}}`,
detailBody: `{"credits":[{"status":"available"},{"status":"available","expires_at":"2026-07-04T04:05:06Z"}]}`,
wantCount: 2, wantCredits: 1,
},
{
name: "shape without count or list preserves usage details",
usageBody: `{"rate_limit_reset_credits":{"available_count":5,"credits":[{"expires_at":"usage-expiry"}]}}`,
detailBody: `{}`,
wantCount: 5,
wantCredits: 1,
},
{
name: "valid detail count survives malformed authoritative list",
usageBody: `{"rate_limit_reset_credits":{"available_count":7,"credits":[{"expires_at":"usage-expiry"}]}}`,
detailBody: `{"available_count":2,"credits":"malformed"}`,
wantCount: 2,
wantCredits: 1,
},
{
name: "valid detail count creates quota despite malformed authoritative list",
usageBody: `{}`,
detailBody: `{"available_count":2,"credits":"malformed"}`,
wantCount: 2,
},
{
name: "negative detail count without list preserves usage",
usageBody: `{"rate_limit_reset_credits":{"available_count":4}}`,
detailBody: `{"available_count":-1}`,
wantCount: 4,
},
{
name: "negative detail count falls back to available records",
usageBody: `{"rate_limit_reset_credits":{"available_count":4}}`,
detailBody: `{"available_count":-1,"credits":[{"status":"available","expires_at":"2026-07-04T04:05:06Z"}]}`,
wantCount: 1, wantCredits: 1,
},
{
name: "empty object preserves missing usage credits",
usageBody: `{}`,
detailBody: `{}`,
wantNil: true,
},
{
name: "null body preserves missing usage credits",
usageBody: `{}`,
detailBody: `null`,
wantNil: true,
},
{
name: "empty body preserves missing usage credits",
usageBody: `{}`,
detailBody: ``,
wantNil: true,
},
{
name: "null object record is not counted",
usageBody: `{"rate_limit_reset_credits":{"available_count":7}}`,
detailBody: `{"credits":[null]}`,
wantCount: 0,
},
{
name: "null top level record is not counted",
usageBody: `{"rate_limit_reset_credits":{"available_count":7}}`,
detailBody: `[null]`,
wantCount: 0,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
account := &Account{
ID: 100,
Platform: PlatformOpenAI,
Type: AccountTypeOAuth,
Status: StatusActive,
Credentials: map[string]any{
"chatgpt_account_id": "org-parent123",
},
}
repo := &stubQuotaAccountRepo{accounts: map[int64]*Account{100: account}}
tokenCache := &stubQuotaTokenCache{tokens: map[string]string{
OpenAITokenCacheKey(account): "fake-token",
}}
tokenProvider := NewOpenAITokenProvider(repo, tokenCache, nil)
var detailCalls int
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("content-type", "application/json")
switch r.URL.Path {
case "/backend-api/wham/usage":
_, _ = w.Write([]byte(tt.usageBody))
case "/backend-api/wham/rate-limit-reset-credits":
detailCalls++
_, _ = w.Write([]byte(tt.detailBody))
default:
http.NotFound(w, r)
}
}))
defer srv.Close()
svc := NewOpenAIQuotaService(repo, nil, tokenProvider, newQuotaRedirectingFactory(srv))
usage, err := svc.QueryUsage(context.Background(), 100)
require.NoError(t, err)
require.NotNil(t, usage)
require.Equal(t, 1, detailCalls)
if tt.wantNil {
require.Nil(t, usage.RateLimitResetCredits)
return
}
require.NotNil(t, usage.RateLimitResetCredits)
require.Equal(t, tt.wantCount, usage.RateLimitResetCredits.AvailableCount)
require.Len(t, usage.RateLimitResetCredits.Credits, tt.wantCredits)
})
}
}