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
189 lines
6.2 KiB
Go
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)
|
|
})
|
|
}
|
|
}
|