Sub2API v1.0 - AI API 网关(二开初始版本,基于上游 Wei-Shaw/sub2api)
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
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
This commit is contained in:
@@ -0,0 +1,188 @@
|
||||
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)
|
||||
})
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user