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,339 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strconv"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestCalculateAnthropic429ResetTime_Only5hExceeded(t *testing.T) {
|
||||
headers := http.Header{}
|
||||
headers.Set("anthropic-ratelimit-unified-5h-utilization", "1.02")
|
||||
headers.Set("anthropic-ratelimit-unified-5h-reset", "1770998400")
|
||||
headers.Set("anthropic-ratelimit-unified-7d-utilization", "0.32")
|
||||
headers.Set("anthropic-ratelimit-unified-7d-reset", "1771549200")
|
||||
|
||||
result := calculateAnthropic429ResetTime(headers)
|
||||
assertAnthropicResult(t, result, 1770998400)
|
||||
|
||||
if result.fiveHourReset == nil || !result.fiveHourReset.Equal(time.Unix(1770998400, 0)) {
|
||||
t.Errorf("expected fiveHourReset=1770998400, got %v", result.fiveHourReset)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCalculateAnthropic429ResetTime_Only7dExceeded(t *testing.T) {
|
||||
headers := http.Header{}
|
||||
headers.Set("anthropic-ratelimit-unified-5h-utilization", "0.50")
|
||||
headers.Set("anthropic-ratelimit-unified-5h-reset", "1770998400")
|
||||
headers.Set("anthropic-ratelimit-unified-7d-utilization", "1.05")
|
||||
headers.Set("anthropic-ratelimit-unified-7d-reset", "1771549200")
|
||||
|
||||
result := calculateAnthropic429ResetTime(headers)
|
||||
assertAnthropicResult(t, result, 1771549200)
|
||||
|
||||
// fiveHourReset should still be populated for session window calculation
|
||||
if result.fiveHourReset == nil || !result.fiveHourReset.Equal(time.Unix(1770998400, 0)) {
|
||||
t.Errorf("expected fiveHourReset=1770998400, got %v", result.fiveHourReset)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCalculateAnthropic429ResetTime_BothExceeded(t *testing.T) {
|
||||
headers := http.Header{}
|
||||
headers.Set("anthropic-ratelimit-unified-5h-utilization", "1.10")
|
||||
headers.Set("anthropic-ratelimit-unified-5h-reset", "1770998400")
|
||||
headers.Set("anthropic-ratelimit-unified-7d-utilization", "1.02")
|
||||
headers.Set("anthropic-ratelimit-unified-7d-reset", "1771549200")
|
||||
|
||||
result := calculateAnthropic429ResetTime(headers)
|
||||
assertAnthropicResult(t, result, 1771549200)
|
||||
}
|
||||
|
||||
func TestCalculateAnthropic429ResetTime_NoPerWindowHeaders(t *testing.T) {
|
||||
headers := http.Header{}
|
||||
headers.Set("anthropic-ratelimit-unified-reset", "1771549200")
|
||||
|
||||
result := calculateAnthropic429ResetTime(headers)
|
||||
if result != nil {
|
||||
t.Errorf("expected nil result when no per-window headers, got resetAt=%v", result.resetAt)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCalculateAnthropic429ResetTime_NoHeaders(t *testing.T) {
|
||||
result := calculateAnthropic429ResetTime(http.Header{})
|
||||
if result != nil {
|
||||
t.Errorf("expected nil result for empty headers, got resetAt=%v", result.resetAt)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCalculateAnthropic429ResetTime_SurpassedThreshold(t *testing.T) {
|
||||
headers := http.Header{}
|
||||
headers.Set("anthropic-ratelimit-unified-5h-surpassed-threshold", "true")
|
||||
headers.Set("anthropic-ratelimit-unified-5h-reset", "1770998400")
|
||||
headers.Set("anthropic-ratelimit-unified-7d-surpassed-threshold", "false")
|
||||
headers.Set("anthropic-ratelimit-unified-7d-reset", "1771549200")
|
||||
|
||||
result := calculateAnthropic429ResetTime(headers)
|
||||
assertAnthropicResult(t, result, 1770998400)
|
||||
}
|
||||
|
||||
func TestCalculateAnthropic429ResetTime_UtilizationExactlyOne(t *testing.T) {
|
||||
headers := http.Header{}
|
||||
headers.Set("anthropic-ratelimit-unified-5h-utilization", "1.0")
|
||||
headers.Set("anthropic-ratelimit-unified-5h-reset", "1770998400")
|
||||
headers.Set("anthropic-ratelimit-unified-7d-utilization", "0.5")
|
||||
headers.Set("anthropic-ratelimit-unified-7d-reset", "1771549200")
|
||||
|
||||
result := calculateAnthropic429ResetTime(headers)
|
||||
assertAnthropicResult(t, result, 1770998400)
|
||||
}
|
||||
|
||||
func TestCalculateAnthropic429ResetTime_NeitherExceeded_UsesShorter(t *testing.T) {
|
||||
headers := http.Header{}
|
||||
headers.Set("anthropic-ratelimit-unified-5h-utilization", "0.95")
|
||||
headers.Set("anthropic-ratelimit-unified-5h-reset", "1770998400") // sooner
|
||||
headers.Set("anthropic-ratelimit-unified-7d-utilization", "0.80")
|
||||
headers.Set("anthropic-ratelimit-unified-7d-reset", "1771549200") // later
|
||||
|
||||
result := calculateAnthropic429ResetTime(headers)
|
||||
assertAnthropicResult(t, result, 1770998400)
|
||||
}
|
||||
|
||||
func TestCalculateAnthropic429ResetTime_Only5hResetHeader(t *testing.T) {
|
||||
headers := http.Header{}
|
||||
headers.Set("anthropic-ratelimit-unified-5h-utilization", "1.05")
|
||||
headers.Set("anthropic-ratelimit-unified-5h-reset", "1770998400")
|
||||
|
||||
result := calculateAnthropic429ResetTime(headers)
|
||||
assertAnthropicResult(t, result, 1770998400)
|
||||
}
|
||||
|
||||
func TestCalculateAnthropic429ResetTime_Only7dResetHeader(t *testing.T) {
|
||||
headers := http.Header{}
|
||||
headers.Set("anthropic-ratelimit-unified-7d-utilization", "1.03")
|
||||
headers.Set("anthropic-ratelimit-unified-7d-reset", "1771549200")
|
||||
|
||||
result := calculateAnthropic429ResetTime(headers)
|
||||
assertAnthropicResult(t, result, 1771549200)
|
||||
|
||||
if result.fiveHourReset != nil {
|
||||
t.Errorf("expected fiveHourReset=nil when no 5h headers, got %v", result.fiveHourReset)
|
||||
}
|
||||
}
|
||||
|
||||
func TestIsAnthropicWindowExceeded(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
headers http.Header
|
||||
window string
|
||||
expected bool
|
||||
}{
|
||||
{
|
||||
name: "utilization above 1.0",
|
||||
headers: makeHeader("anthropic-ratelimit-unified-5h-utilization", "1.02"),
|
||||
window: "5h",
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
name: "utilization exactly 1.0",
|
||||
headers: makeHeader("anthropic-ratelimit-unified-5h-utilization", "1.0"),
|
||||
window: "5h",
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
name: "utilization below 1.0",
|
||||
headers: makeHeader("anthropic-ratelimit-unified-5h-utilization", "0.99"),
|
||||
window: "5h",
|
||||
expected: false,
|
||||
},
|
||||
{
|
||||
name: "surpassed-threshold true",
|
||||
headers: makeHeader("anthropic-ratelimit-unified-7d-surpassed-threshold", "true"),
|
||||
window: "7d",
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
name: "surpassed-threshold True (case insensitive)",
|
||||
headers: makeHeader("anthropic-ratelimit-unified-7d-surpassed-threshold", "True"),
|
||||
window: "7d",
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
name: "surpassed-threshold false",
|
||||
headers: makeHeader("anthropic-ratelimit-unified-7d-surpassed-threshold", "false"),
|
||||
window: "7d",
|
||||
expected: false,
|
||||
},
|
||||
{
|
||||
name: "no headers",
|
||||
headers: http.Header{},
|
||||
window: "5h",
|
||||
expected: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
got := isAnthropicWindowExceeded(tc.headers, tc.window)
|
||||
if got != tc.expected {
|
||||
t.Errorf("expected %v, got %v", tc.expected, got)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestSelectAnthropicFableWindowLimit_RejectedStatus(t *testing.T) {
|
||||
now := time.Now()
|
||||
reset := now.Add(80 * time.Hour).Truncate(time.Second)
|
||||
|
||||
headers := http.Header{}
|
||||
headers.Set("anthropic-ratelimit-unified-7d_oi-status", "rejected")
|
||||
headers.Set("anthropic-ratelimit-unified-7d_oi-utilization", "1.0")
|
||||
headers.Set("anthropic-ratelimit-unified-7d_oi-surpassed-threshold", "1.0")
|
||||
headers.Set("anthropic-ratelimit-unified-7d_oi-reset", strconv.FormatInt(reset.Unix(), 10))
|
||||
|
||||
limit := selectAnthropicFableWindowLimit(headers, now)
|
||||
if limit == nil {
|
||||
t.Fatal("expected non-nil limit")
|
||||
}
|
||||
if !limit.resetAt.Equal(reset) {
|
||||
t.Errorf("expected resetAt=%v, got %v", reset, limit.resetAt)
|
||||
}
|
||||
if limit.reason != anthropicFableWindowReason {
|
||||
t.Errorf("expected reason=%q, got %q", anthropicFableWindowReason, limit.reason)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSelectAnthropicFableWindowLimit_UtilizationOnly(t *testing.T) {
|
||||
// 无 status 头时,utilization >= 1.0 也应视为超限
|
||||
now := time.Now()
|
||||
reset := now.Add(3 * 24 * time.Hour).Truncate(time.Second)
|
||||
|
||||
headers := http.Header{}
|
||||
headers.Set("anthropic-ratelimit-unified-7d_oi-utilization", "1.0")
|
||||
headers.Set("anthropic-ratelimit-unified-7d_oi-reset", strconv.FormatInt(reset.Unix(), 10))
|
||||
|
||||
limit := selectAnthropicFableWindowLimit(headers, now)
|
||||
if limit == nil {
|
||||
t.Fatal("expected non-nil limit")
|
||||
}
|
||||
if !limit.resetAt.Equal(reset) {
|
||||
t.Errorf("expected resetAt=%v, got %v", reset, limit.resetAt)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSelectAnthropicFableWindowLimit_AllowedReturnsNil(t *testing.T) {
|
||||
now := time.Now()
|
||||
headers := http.Header{}
|
||||
headers.Set("anthropic-ratelimit-unified-7d_oi-status", "allowed")
|
||||
headers.Set("anthropic-ratelimit-unified-7d_oi-utilization", "0.56")
|
||||
headers.Set("anthropic-ratelimit-unified-7d_oi-reset", strconv.FormatInt(now.Add(80*time.Hour).Unix(), 10))
|
||||
|
||||
if limit := selectAnthropicFableWindowLimit(headers, now); limit != nil {
|
||||
t.Errorf("expected nil limit for allowed window, got %+v", limit)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSelectAnthropicFableWindowLimit_NoHeadersReturnsNil(t *testing.T) {
|
||||
if limit := selectAnthropicFableWindowLimit(http.Header{}, time.Now()); limit != nil {
|
||||
t.Errorf("expected nil limit for empty headers, got %+v", limit)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSelectAnthropicFableWindowLimit_FallsBackToAggregateReset(t *testing.T) {
|
||||
// 7d_oi-reset 缺失时回退聚合 anthropic-ratelimit-unified-reset
|
||||
now := time.Now()
|
||||
reset := now.Add(80 * time.Hour).Truncate(time.Second)
|
||||
|
||||
headers := http.Header{}
|
||||
headers.Set("anthropic-ratelimit-unified-7d_oi-status", "rejected")
|
||||
headers.Set("anthropic-ratelimit-unified-reset", strconv.FormatInt(reset.Unix(), 10))
|
||||
|
||||
limit := selectAnthropicFableWindowLimit(headers, now)
|
||||
if limit == nil {
|
||||
t.Fatal("expected non-nil limit via aggregate reset fallback")
|
||||
}
|
||||
if !limit.resetAt.Equal(reset) {
|
||||
t.Errorf("expected resetAt=%v, got %v", reset, limit.resetAt)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSelectAnthropicFableWindowLimit_RejectedWithoutAnyResetReturnsNil(t *testing.T) {
|
||||
headers := http.Header{}
|
||||
headers.Set("anthropic-ratelimit-unified-7d_oi-status", "rejected")
|
||||
|
||||
if limit := selectAnthropicFableWindowLimit(headers, time.Now()); limit != nil {
|
||||
t.Errorf("expected nil limit when no reset time available, got %+v", limit)
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseAnthropicAggregateReset(t *testing.T) {
|
||||
now := time.Now()
|
||||
future := now.Add(80 * time.Hour).Truncate(time.Second)
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
value string
|
||||
want time.Time
|
||||
wantOK bool
|
||||
}{
|
||||
{"valid seconds", strconv.FormatInt(future.Unix(), 10), future, true},
|
||||
{"valid milliseconds", strconv.FormatInt(future.UnixMilli(), 10), future, true},
|
||||
{"empty", "", time.Time{}, false},
|
||||
{"garbage", "abc", time.Time{}, false},
|
||||
{"in the past", strconv.FormatInt(now.Add(-time.Hour).Unix(), 10), time.Time{}, false},
|
||||
{"too far in the future", strconv.FormatInt(now.Add(30*24*time.Hour).Unix(), 10), time.Time{}, false},
|
||||
}
|
||||
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
headers := http.Header{}
|
||||
if tc.value != "" {
|
||||
headers.Set("anthropic-ratelimit-unified-reset", tc.value)
|
||||
}
|
||||
got, ok := parseAnthropicAggregateReset(headers, now)
|
||||
if ok != tc.wantOK {
|
||||
t.Fatalf("expected ok=%v, got %v", tc.wantOK, ok)
|
||||
}
|
||||
if ok && !got.Equal(tc.want) {
|
||||
t.Errorf("expected %v, got %v", tc.want, got)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestIsAnthropicWindowRejected(t *testing.T) {
|
||||
headers := http.Header{}
|
||||
headers.Set("anthropic-ratelimit-unified-7d_oi-status", "Rejected")
|
||||
headers.Set("anthropic-ratelimit-unified-5h-status", "allowed")
|
||||
|
||||
if !isAnthropicWindowRejected(headers, "7d_oi") {
|
||||
t.Error("expected 7d_oi to be rejected (case insensitive)")
|
||||
}
|
||||
if isAnthropicWindowRejected(headers, "5h") {
|
||||
t.Error("expected 5h not rejected")
|
||||
}
|
||||
if isAnthropicWindowRejected(headers, "7d") {
|
||||
t.Error("expected missing 7d status not rejected")
|
||||
}
|
||||
}
|
||||
|
||||
// assertAnthropicResult is a test helper that verifies the result is non-nil and
|
||||
// has the expected resetAt unix timestamp.
|
||||
func assertAnthropicResult(t *testing.T, result *anthropic429Result, wantUnix int64) {
|
||||
t.Helper()
|
||||
if result == nil {
|
||||
t.Fatal("expected non-nil result")
|
||||
return // unreachable, but satisfies staticcheck SA5011
|
||||
}
|
||||
want := time.Unix(wantUnix, 0)
|
||||
if !result.resetAt.Equal(want) {
|
||||
t.Errorf("expected resetAt=%v, got %v", want, result.resetAt)
|
||||
}
|
||||
}
|
||||
|
||||
func makeHeader(key, value string) http.Header {
|
||||
h := http.Header{}
|
||||
h.Set(key, value)
|
||||
return h
|
||||
}
|
||||
Reference in New Issue
Block a user