mirror of
https://github.com/router-for-me/CLIProxyAPI.git
synced 2026-10-28 02:21:59 +08:00
Reject any home models payload containing an error key before parsing provider sections. Array-shaped error values were previously treated as model sections, masking upstream errors as valid or empty catalogs instead of returning an error.
131 lines
5.7 KiB
Go
131 lines
5.7 KiB
Go
package api
|
|
|
|
import (
|
|
"encoding/json"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"testing"
|
|
|
|
"github.com/gin-gonic/gin"
|
|
"github.com/router-for-me/CLIProxyAPI/v8/internal/home"
|
|
"github.com/router-for-me/CLIProxyAPI/v8/internal/registry"
|
|
)
|
|
|
|
func TestDecodeHomeModelsEmptyCatalog(t *testing.T) {
|
|
for _, raw := range []string{"{}", " { } ", `{"claude":[],"codex-pro":[]}`} {
|
|
entries, errDecode := decodeHomeModels([]byte(raw))
|
|
if errDecode != nil || entries == nil || len(entries) != 0 {
|
|
t.Errorf("decodeHomeModels(%q) = %v, %v, want empty list", raw, entries, errDecode)
|
|
}
|
|
}
|
|
for _, raw := range []string{
|
|
"", "null", "[]", "{bad}", `{"claude":[{}]}`, `{"claude":null}`, `{"error":null}`,
|
|
`{"error":{"type":"internal_error","message":"unavailable"}}`,
|
|
`{"error":[]}`, `{"error":[],"claude":[]}`,
|
|
`{"error":[{"id":"not-a-model"}]}`, `{"error":[],"claude":[{"id":"model-a"}]}`,
|
|
} {
|
|
if _, errDecode := decodeHomeModels([]byte(raw)); errDecode == nil {
|
|
t.Errorf("decodeHomeModels(%q) accepted malformed or error payload", raw)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestHomeEmptyModelCatalogResponses(t *testing.T) {
|
|
server := newTestServer(t)
|
|
server.cfg.Home.Enabled = true
|
|
engine := gin.New()
|
|
engine.GET("/v1/models", server.unifiedModelsHandler(nil, nil))
|
|
engine.GET("/v1beta/models", server.geminiModelsHandler(nil))
|
|
engine.GET("/v1beta/models/*action", server.geminiGetHandler(nil))
|
|
client := newHomeCatalogClient(t, "{}")
|
|
previousHome := home.Current()
|
|
home.SetCurrent(client)
|
|
t.Cleanup(func() { home.SetCurrent(previousHome) })
|
|
registry.GetGlobalRegistry().RegisterClient("empty-catalog-local", "codex", []*registry.ModelInfo{{ID: "local-only-model"}})
|
|
t.Cleanup(func() { registry.GetGlobalRegistry().UnregisterClient("empty-catalog-local") })
|
|
|
|
tests := []struct {
|
|
name string
|
|
path string
|
|
headers map[string]string
|
|
field string
|
|
}{
|
|
{name: "openai", path: "/v1/models", field: "data"},
|
|
{name: "claude", path: "/v1/models", headers: map[string]string{"Anthropic-Version": "2023-06-01"}, field: "data"},
|
|
{name: "codex", path: "/v1/models?client_version=0.153.4", field: "models"},
|
|
{name: "cpa", path: "/v1/models?client_version=cpa", field: "models"},
|
|
{name: "gemini", path: "/v1beta/models", field: "models"},
|
|
{name: "grok", path: "/v1/models", headers: map[string]string{"User-Agent": "grok-shell"}, field: "data"},
|
|
}
|
|
for _, test := range tests {
|
|
t.Run(test.name, func(t *testing.T) {
|
|
request := httptest.NewRequest(http.MethodGet, test.path, nil)
|
|
request.Header.Set("Authorization", "Bearer scoped-key")
|
|
for key, value := range test.headers {
|
|
request.Header.Set(key, value)
|
|
}
|
|
recorder := httptest.NewRecorder()
|
|
engine.ServeHTTP(recorder, request)
|
|
if recorder.Code != http.StatusOK {
|
|
t.Fatalf("status = %d; body=%s", recorder.Code, recorder.Body.String())
|
|
}
|
|
var payload map[string]json.RawMessage
|
|
if errUnmarshal := json.Unmarshal(recorder.Body.Bytes(), &payload); errUnmarshal != nil {
|
|
t.Fatal(errUnmarshal)
|
|
}
|
|
if string(payload[test.field]) != "[]" {
|
|
t.Fatalf("%s = %s, want []; body=%s", test.field, payload[test.field], recorder.Body.String())
|
|
}
|
|
})
|
|
}
|
|
recorder := httptest.NewRecorder()
|
|
engine.ServeHTTP(recorder, httptest.NewRequest(http.MethodGet, "/v1beta/models/inaccessible-model", nil))
|
|
if recorder.Code != http.StatusNotFound {
|
|
t.Fatalf("inaccessible model status = %d, want 404; body=%s", recorder.Code, recorder.Body.String())
|
|
}
|
|
}
|
|
|
|
func TestHomeModelCatalogErrorsRemainErrors(t *testing.T) {
|
|
server := newTestServer(t)
|
|
server.cfg.Home.Enabled = true
|
|
engine := gin.New()
|
|
engine.GET("/v1/models", server.unifiedModelsHandler(nil, nil))
|
|
for _, test := range []struct {
|
|
name string
|
|
payload string
|
|
status int
|
|
}{
|
|
{name: "missing credentials", payload: `{"error":{"type":"no_credentials","message":"Missing API key"}}`, status: http.StatusUnauthorized},
|
|
{name: "invalid credentials", payload: `{"error":{"type":"invalid_credential","message":"Invalid API key"}}`, status: http.StatusUnauthorized},
|
|
{name: "policy failure", payload: `{"error":{"type":"internal_error","message":"Model access service error"}}`, status: http.StatusBadGateway},
|
|
{name: "empty array error", payload: `{"error":[]}`, status: http.StatusBadGateway},
|
|
{name: "error alongside empty section", payload: `{"error":[],"claude":[]}`, status: http.StatusBadGateway},
|
|
{name: "error containing model-shaped records", payload: `{"error":[{"id":"not-a-model"}]}`, status: http.StatusBadGateway},
|
|
{name: "error alongside models", payload: `{"error":[],"claude":[{"id":"model-a"}]}`, status: http.StatusBadGateway},
|
|
{name: "empty response", payload: "", status: http.StatusBadGateway},
|
|
{name: "malformed JSON", payload: "{bad}", status: http.StatusBadGateway},
|
|
{name: "null catalog", payload: "null", status: http.StatusBadGateway},
|
|
{name: "null section", payload: `{"claude":null}`, status: http.StatusBadGateway},
|
|
{name: "missing model IDs", payload: `{"claude":[{}]}`, status: http.StatusBadGateway},
|
|
} {
|
|
t.Run(test.name, func(t *testing.T) {
|
|
client := newHomeCatalogClient(t, test.payload)
|
|
previousHome := home.Current()
|
|
home.SetCurrent(client)
|
|
t.Cleanup(func() { home.SetCurrent(previousHome) })
|
|
recorder := httptest.NewRecorder()
|
|
engine.ServeHTTP(recorder, httptest.NewRequest(http.MethodGet, "/v1/models", nil))
|
|
if recorder.Code != test.status {
|
|
t.Fatalf("status = %d, want %d; body=%s", recorder.Code, test.status, recorder.Body.String())
|
|
}
|
|
var payload map[string]json.RawMessage
|
|
if errUnmarshal := json.Unmarshal(recorder.Body.Bytes(), &payload); errUnmarshal != nil {
|
|
t.Fatal(errUnmarshal)
|
|
}
|
|
if _, exists := payload["error"]; !exists {
|
|
t.Fatalf("expected error response; body=%s", recorder.Body.String())
|
|
}
|
|
})
|
|
}
|
|
}
|