package handlers import ( "errors" "net/http" "net/http/httptest" "reflect" "strings" "testing" "github.com/gin-gonic/gin" "github.com/router-for-me/CLIProxyAPI/v6/internal/interfaces" coreauth "github.com/router-for-me/CLIProxyAPI/v6/sdk/cliproxy/auth" sdkconfig "github.com/router-for-me/CLIProxyAPI/v6/sdk/config" ) func TestWriteErrorResponse_AddonHeadersDisabledByDefault(t *testing.T) { gin.SetMode(gin.TestMode) recorder := httptest.NewRecorder() c, _ := gin.CreateTestContext(recorder) c.Request = httptest.NewRequest(http.MethodGet, "/", nil) handler := NewBaseAPIHandlers(nil, nil) handler.WriteErrorResponse(c, &interfaces.ErrorMessage{ StatusCode: http.StatusTooManyRequests, Error: errors.New("rate limit"), Addon: http.Header{ "Retry-After": {"30"}, "X-Request-Id": {"req-1"}, }, }) if recorder.Code != http.StatusTooManyRequests { t.Fatalf("status = %d, want %d", recorder.Code, http.StatusTooManyRequests) } if got := recorder.Header().Get("Retry-After"); got != "" { t.Fatalf("Retry-After should be empty when passthrough is disabled, got %q", got) } if got := recorder.Header().Get("X-Request-Id"); got != "" { t.Fatalf("X-Request-Id should be empty when passthrough is disabled, got %q", got) } } func TestWriteErrorResponse_AddonHeadersEnabled(t *testing.T) { gin.SetMode(gin.TestMode) recorder := httptest.NewRecorder() c, _ := gin.CreateTestContext(recorder) c.Request = httptest.NewRequest(http.MethodGet, "/", nil) c.Writer.Header().Set("X-Request-Id", "old-value") handler := NewBaseAPIHandlers(&sdkconfig.SDKConfig{PassthroughHeaders: true}, nil) handler.WriteErrorResponse(c, &interfaces.ErrorMessage{ StatusCode: http.StatusTooManyRequests, Error: errors.New("rate limit"), Addon: http.Header{ "Retry-After": {"30"}, "X-Request-Id": {"new-1", "new-2"}, }, }) if recorder.Code != http.StatusTooManyRequests { t.Fatalf("status = %d, want %d", recorder.Code, http.StatusTooManyRequests) } if got := recorder.Header().Get("Retry-After"); got != "30" { t.Fatalf("Retry-After = %q, want %q", got, "30") } if got := recorder.Header().Values("X-Request-Id"); !reflect.DeepEqual(got, []string{"new-1", "new-2"}) { t.Fatalf("X-Request-Id = %#v, want %#v", got, []string{"new-1", "new-2"}) } } func TestEnrichAuthSelectionError_DefaultsTo503WithContext(t *testing.T) { in := &coreauth.Error{Code: "auth_not_found", Message: "no auth available"} out := enrichAuthSelectionError(in, []string{"claude"}, "claude-sonnet-4-6") var got *coreauth.Error if !errors.As(out, &got) || got == nil { t.Fatalf("expected coreauth.Error, got %T", out) } if got.StatusCode() != http.StatusServiceUnavailable { t.Fatalf("status = %d, want %d", got.StatusCode(), http.StatusServiceUnavailable) } if !strings.Contains(got.Message, "providers=claude") { t.Fatalf("message missing provider context: %q", got.Message) } if !strings.Contains(got.Message, "model=claude-sonnet-4-6") { t.Fatalf("message missing model context: %q", got.Message) } if !strings.Contains(got.Message, "/v0/management/auth-files") { t.Fatalf("message missing management hint: %q", got.Message) } } func TestEnrichAuthSelectionError_PreservesExplicitStatus(t *testing.T) { in := &coreauth.Error{Code: "auth_unavailable", Message: "no auth available", HTTPStatus: http.StatusTooManyRequests} out := enrichAuthSelectionError(in, []string{"gemini"}, "gemini-2.5-pro") var got *coreauth.Error if !errors.As(out, &got) || got == nil { t.Fatalf("expected coreauth.Error, got %T", out) } if got.StatusCode() != http.StatusTooManyRequests { t.Fatalf("status = %d, want %d", got.StatusCode(), http.StatusTooManyRequests) } } func TestEnrichAuthSelectionError_IgnoresOtherErrors(t *testing.T) { in := errors.New("boom") out := enrichAuthSelectionError(in, []string{"claude"}, "claude-sonnet-4-6") if out != in { t.Fatalf("expected original error to be returned unchanged") } }