Files
CLIProxyAPI/sdk/cliproxy/auth/error_events_test.go
Luis Pater fd30944830 feat(auth): add error event publishing and Redis queue integration
- 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
2026-06-04 00:53:43 +08:00

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
}
}