mirror of
https://github.com/router-for-me/CLIProxyAPI.git
synced 2026-07-02 18:24:37 +08:00
- Introduced `publishErrorEvent` in `Manager` to publish error events to Redis. - Implemented error event structure to capture authentication errors with detailed metadata. - Added test cases for error event publishing, subscription, and Redis protocol handling. - Enhanced error and usage queue handling with `SubscribeErrors` and `EnqueueError`. Closes: #3701
166 lines
4.9 KiB
Go
166 lines
4.9 KiB
Go
package auth
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"net/http"
|
|
"testing"
|
|
"time"
|
|
|
|
internalconfig "github.com/router-for-me/CLIProxyAPI/v7/internal/config"
|
|
"github.com/router-for-me/CLIProxyAPI/v7/internal/redisqueue"
|
|
)
|
|
|
|
func TestManagerMarkResultPublishesErrorEventAfterAuthStateUpdate(t *testing.T) {
|
|
withEnabledErrorQueue(t)
|
|
subscriber, unsubscribe := redisqueue.SubscribeErrors()
|
|
defer unsubscribe()
|
|
|
|
manager := NewManager(nil, nil, nil)
|
|
auth := &Auth{
|
|
ID: "auth-error-event",
|
|
Provider: "codex",
|
|
Metadata: map[string]any{
|
|
"type": "codex",
|
|
},
|
|
}
|
|
if _, errRegister := manager.Register(WithSkipPersist(context.Background()), auth); errRegister != nil {
|
|
t.Fatalf("Register returned error: %v", errRegister)
|
|
}
|
|
|
|
manager.MarkResult(context.Background(), Result{
|
|
AuthID: auth.ID,
|
|
Provider: "codex",
|
|
Model: "gpt-5",
|
|
Success: false,
|
|
Error: &Error{
|
|
Code: "rate_limit",
|
|
Message: `{"error":"quota"}`,
|
|
Retryable: true,
|
|
HTTPStatus: http.StatusTooManyRequests,
|
|
},
|
|
})
|
|
|
|
payload := requireErrorSubscriberPayload(t, subscriber)
|
|
|
|
var event struct {
|
|
Provider string `json:"provider"`
|
|
Model string `json:"model"`
|
|
AuthID string `json:"auth_id"`
|
|
AuthIndex string `json:"auth_index"`
|
|
StatusCode int `json:"status_code"`
|
|
Body string `json:"body"`
|
|
Code string `json:"code"`
|
|
Retryable bool `json:"retryable"`
|
|
AuthStatus struct {
|
|
Status Status `json:"status"`
|
|
StatusMessage string `json:"status_message"`
|
|
Unavailable bool `json:"unavailable"`
|
|
Quota *struct {
|
|
Exceeded bool `json:"exceeded"`
|
|
Reason string `json:"reason"`
|
|
} `json:"quota"`
|
|
Model *struct {
|
|
Name string `json:"name"`
|
|
Status Status `json:"status"`
|
|
Unavailable bool `json:"unavailable"`
|
|
Quota *struct {
|
|
Exceeded bool `json:"exceeded"`
|
|
Reason string `json:"reason"`
|
|
} `json:"quota"`
|
|
} `json:"model"`
|
|
} `json:"auth_status"`
|
|
}
|
|
if errUnmarshal := json.Unmarshal(payload, &event); errUnmarshal != nil {
|
|
t.Fatalf("unmarshal error event: %v body=%s", errUnmarshal, string(payload))
|
|
}
|
|
if event.Provider != "codex" || event.Model != "gpt-5" || event.AuthID != auth.ID {
|
|
t.Fatalf("unexpected event routing fields: %+v", event)
|
|
}
|
|
if event.AuthIndex == "" {
|
|
t.Fatalf("auth_index is empty in event: %s", string(payload))
|
|
}
|
|
if event.StatusCode != http.StatusTooManyRequests || event.Body != `{"error":"quota"}` {
|
|
t.Fatalf("unexpected error fields: status=%d body=%q", event.StatusCode, event.Body)
|
|
}
|
|
if event.Code != "rate_limit" || !event.Retryable {
|
|
t.Fatalf("unexpected error code fields: code=%q retryable=%t", event.Code, event.Retryable)
|
|
}
|
|
if event.AuthStatus.Status != StatusError || !event.AuthStatus.Unavailable {
|
|
t.Fatalf("unexpected auth status: %+v", event.AuthStatus)
|
|
}
|
|
if event.AuthStatus.Model == nil || event.AuthStatus.Model.Name != "gpt-5" || event.AuthStatus.Model.Status != StatusError || !event.AuthStatus.Model.Unavailable {
|
|
t.Fatalf("unexpected model status: %+v", event.AuthStatus.Model)
|
|
}
|
|
if event.AuthStatus.Quota == nil || !event.AuthStatus.Quota.Exceeded || event.AuthStatus.Quota.Reason != "quota" {
|
|
t.Fatalf("unexpected auth quota: %+v", event.AuthStatus.Quota)
|
|
}
|
|
if event.AuthStatus.Model.Quota == nil || !event.AuthStatus.Model.Quota.Exceeded || event.AuthStatus.Model.Quota.Reason != "quota" {
|
|
t.Fatalf("unexpected model quota: %+v", event.AuthStatus.Model.Quota)
|
|
}
|
|
}
|
|
|
|
func TestManagerMarkResultSkipsErrorEventInHomeMode(t *testing.T) {
|
|
withEnabledErrorQueue(t)
|
|
subscriber, unsubscribe := redisqueue.SubscribeErrors()
|
|
defer unsubscribe()
|
|
|
|
manager := NewManager(nil, nil, nil)
|
|
manager.SetConfig(&internalconfig.Config{Home: internalconfig.HomeConfig{Enabled: true}})
|
|
auth := &Auth{
|
|
ID: "home-auth-error-event",
|
|
Provider: "codex",
|
|
Metadata: map[string]any{
|
|
"type": "codex",
|
|
},
|
|
}
|
|
if _, errRegister := manager.Register(WithSkipPersist(context.Background()), auth); errRegister != nil {
|
|
t.Fatalf("Register returned error: %v", errRegister)
|
|
}
|
|
|
|
manager.MarkResult(context.Background(), Result{
|
|
AuthID: auth.ID,
|
|
Provider: "codex",
|
|
Model: "gpt-5",
|
|
Success: false,
|
|
Error: &Error{
|
|
Message: "unauthorized",
|
|
HTTPStatus: http.StatusUnauthorized,
|
|
},
|
|
})
|
|
|
|
select {
|
|
case got := <-subscriber:
|
|
t.Fatalf("received home-mode error event %q, want none", string(got))
|
|
default:
|
|
}
|
|
}
|
|
|
|
func withEnabledErrorQueue(t *testing.T) {
|
|
t.Helper()
|
|
|
|
prevQueueEnabled := redisqueue.Enabled()
|
|
redisqueue.SetEnabled(false)
|
|
redisqueue.SetEnabled(true)
|
|
|
|
t.Cleanup(func() {
|
|
redisqueue.SetEnabled(false)
|
|
redisqueue.SetEnabled(prevQueueEnabled)
|
|
})
|
|
}
|
|
|
|
func requireErrorSubscriberPayload(t *testing.T, subscriber <-chan []byte) []byte {
|
|
t.Helper()
|
|
|
|
select {
|
|
case got, ok := <-subscriber:
|
|
if !ok {
|
|
t.Fatalf("error subscriber closed before receiving payload")
|
|
}
|
|
return got
|
|
case <-time.After(time.Second):
|
|
t.Fatalf("timeout waiting for error subscriber payload")
|
|
return nil
|
|
}
|
|
}
|