package sub2api import ( "context" "encoding/json" "errors" "net/http" "net/http/httptest" "testing" ) func TestHTTPErrorErrorMessage(t *testing.T) { e := newHTTPError("POST", "/api/v1/admin/groups", http.StatusTeapot, []byte("short and stout")) want := "sub2api POST /api/v1/admin/groups returned 418: short and stout" if got := e.Error(); got != want { t.Fatalf("HTTPError.Error() = %q, want %q", got, want) } } func TestWithHTTPClientAndOptions(t *testing.T) { customHTTP := &http.Client{Timeout: 123} client, err := NewClient("http://localhost:8080", WithHTTPClient(customHTTP), WithAPIKey(" sk-abc "), WithBearerToken(" tok-xyz "), ) if err != nil { t.Fatal(err) } if client.httpClient != customHTTP { t.Fatal("WithHTTPClient not applied") } if client.apiKey != "sk-abc" { t.Fatalf("apiKey = %q, want %q", client.apiKey, "sk-abc") } if client.bearerToken != "tok-xyz" { t.Fatalf("bearerToken = %q, want %q", client.bearerToken, "tok-xyz") } } func TestNewClient_RejectsInvalidURLs(t *testing.T) { tests := []struct { name string url string }{ {"empty", ""}, {"no scheme", "localhost:8080"}, {"no host", "http://"}, {"garbage", "://foo"}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { _, err := NewClient(tt.url) if err == nil { t.Fatalf("NewClient(%q) error = nil, want error", tt.url) } }) } } func TestResolvePath(t *testing.T) { client, err := NewClient("http://host:9090") if err != nil { t.Fatal(err) } tests := []struct { path string want string }{ {"/v1/models", "http://host:9090/v1/models"}, {"v1/models", "http://host:9090/v1/models"}, {"/v1/models?key=val", "http://host:9090/v1/models?key=val"}, } for _, tt := range tests { t.Run(tt.path, func(t *testing.T) { if got := client.resolvePath(tt.path); got != tt.want { t.Fatalf("resolvePath(%q) = %q, want %q", tt.path, got, tt.want) } }) } } func TestApplyAuth(t *testing.T) { t.Run("api key preferred", func(t *testing.T) { c, _ := NewClient("http://h:8080", WithAPIKey("key1"), WithBearerToken("btok")) req, _ := http.NewRequest("GET", "http://h:8080/path", nil) c.applyAuth(req) if h := req.Header.Get("x-api-key"); h != "key1" { t.Fatalf("x-api-key = %q, want %q", h, "key1") } if h := req.Header.Get("Authorization"); h != "" { t.Fatalf("Authorization should be empty, got %q", h) } }) t.Run("bearer token fallback", func(t *testing.T) { c, _ := NewClient("http://h:8080", WithBearerToken("btok")) req, _ := http.NewRequest("GET", "http://h:8080/path", nil) c.applyAuth(req) if h := req.Header.Get("Authorization"); h != "Bearer btok" { t.Fatalf("Authorization = %q, want %q", h, "Bearer btok") } }) t.Run("no auth", func(t *testing.T) { c, _ := NewClient("http://h:8080") req, _ := http.NewRequest("GET", "http://h:8080/path", nil) c.applyAuth(req) if h := req.Header.Get("x-api-key"); h != "" { t.Fatalf("x-api-key should be empty, got %q", h) } if h := req.Header.Get("Authorization"); h != "" { t.Fatalf("Authorization should be empty, got %q", h) } }) } func TestDecodeEnvelopeObject(t *testing.T) { t.Run("standard envelope", func(t *testing.T) { body := []byte(`{"data":{"id":"g1","name":"test"}}`) var ref GroupRef if err := decodeEnvelopeObject(body, &ref); err != nil { t.Fatal(err) } if ref.ID != "g1" || ref.Name != "test" { t.Fatalf("got %+v, want {ID:g1 Name:test}", ref) } }) t.Run("flat response (no data wrapper)", func(t *testing.T) { body := []byte(`{"id":"g2","name":"flat"}`) var ref GroupRef if err := decodeEnvelopeObject(body, &ref); err != nil { t.Fatal(err) } if ref.ID != "g2" || ref.Name != "flat" { t.Fatalf("got %+v, want {ID:g2 Name:flat}", ref) } }) t.Run("data:null returns flat", func(t *testing.T) { body := []byte(`{"data":null,"id":"g3"}`) var ref GroupRef if err := decodeEnvelopeObject(body, &ref); err != nil { t.Fatal(err) } if ref.ID != "g3" { t.Fatalf("id = %q, want %q", ref.ID, "g3") } }) t.Run("invalid json returns error", func(t *testing.T) { var ref GroupRef if err := decodeEnvelopeObject([]byte(`not json`), &ref); err == nil { t.Fatal("expected error") } }) } func TestDecodeGatewayModelIDs(t *testing.T) { t.Run("standard list", func(t *testing.T) { ids := decodeGatewayModelIDs([]byte(`{"data":[{"id":"gpt-4"},{"id":" claude-3 "}]}`)) if len(ids) != 2 || ids[0] != "gpt-4" || ids[1] != "claude-3" { t.Fatalf("got %v, want [gpt-4 claude-3]", ids) } }) t.Run("empty data", func(t *testing.T) { if ids := decodeGatewayModelIDs([]byte(`{}`)); ids != nil { t.Fatalf("expected nil, got %v", ids) } }) t.Run("invalid json", func(t *testing.T) { if ids := decodeGatewayModelIDs([]byte(`not json`)); ids != nil { t.Fatalf("expected nil, got %v", ids) } }) t.Run("empty array", func(t *testing.T) { if ids := decodeGatewayModelIDs([]byte(`{"data":[]}`)); ids != nil { t.Fatalf("expected nil, got %v", ids) } }) } func TestFilterNamedResourcesByName(t *testing.T) { resources := []NamedResource{ {Name: "group-a", ID: "g1"}, {Name: "group-b", ID: "g2"}, {Name: " group-a ", ID: "g3"}, } t.Run("match", func(t *testing.T) { got := filterNamedResourcesByName(resources, "group-a") if len(got) != 2 || got[0].ID != "g1" || got[1].ID != "g3" { t.Fatalf("got %+v, want 2 matches", got) } }) t.Run("no match", func(t *testing.T) { if got := filterNamedResourcesByName(resources, "nonexistent"); len(got) != 0 { t.Fatalf("expected 0, got %d", len(got)) } }) t.Run("empty name returns all", func(t *testing.T) { if got := filterNamedResourcesByName(resources, ""); len(got) != 3 { t.Fatalf("expected 3, got %d", len(got)) } }) } func TestFilterNamedResourcesByPrefix(t *testing.T) { resources := []NamedResource{ {Name: "deepseek-proxy", ID: "r1"}, {Name: "deepseek-us", ID: "r2"}, {Name: "claude-eu", ID: "r3"}, } t.Run("prefix matches", func(t *testing.T) { got := filterNamedResourcesByPrefix(resources, "deepseek") if len(got) != 2 { t.Fatalf("expected 2, got %d", len(got)) } }) t.Run("no prefix match", func(t *testing.T) { if got := filterNamedResourcesByPrefix(resources, "nope"); len(got) != 0 { t.Fatalf("expected 0, got %d", len(got)) } }) t.Run("empty prefix returns all", func(t *testing.T) { if got := filterNamedResourcesByPrefix(resources, ""); len(got) != 3 { t.Fatalf("expected 3, got %d", len(got)) } }) } func TestDecodeNamedResources(t *testing.T) { t.Run("envelope", func(t *testing.T) { resources, err := decodeNamedResources([]byte(`{"data":[{"id":"r1","name":"n1"}]}`)) if err != nil { t.Fatal(err) } if len(resources) != 1 || resources[0].ID != "r1" { t.Fatalf("got %+v", resources) } }) t.Run("numeric id", func(t *testing.T) { resources, err := decodeNamedResources([]byte(`{"data":{"items":[{"id":1,"name":"default"}]}}`)) if err != nil { t.Fatal(err) } if len(resources) != 1 || resources[0].ID != "1" { t.Fatalf("got %+v", resources) } }) t.Run("wrapper with items", func(t *testing.T) { resources, err := decodeNamedResources([]byte(`{"data":{"items":[{"id":"r2","name":"n2"}]}}`)) if err != nil { t.Fatal(err) } if len(resources) != 1 || resources[0].ID != "r2" { t.Fatalf("got %+v", resources) } }) t.Run("invalid json", func(t *testing.T) { _, err := decodeNamedResources([]byte(`not json`)) if err == nil { t.Fatal("expected error") } }) } func TestDecodeAccountRefs(t *testing.T) { t.Run("envelope", func(t *testing.T) { refs, err := decodeAccountRefs([]byte(`{"data":[{"id":"a1"}]}`)) if err != nil { t.Fatal(err) } if len(refs) != 1 || refs[0].ID != "a1" { t.Fatalf("got %+v", refs) } }) t.Run("numeric id", func(t *testing.T) { refs, err := decodeAccountRefs([]byte(`{"data":{"items":[{"id":42}]}}`)) if err != nil { t.Fatal(err) } if len(refs) != 1 || refs[0].ID != "42" { t.Fatalf("got %+v", refs) } }) t.Run("wrapper with items", func(t *testing.T) { refs, err := decodeAccountRefs([]byte(`{"data":{"items":[{"id":"a2"}]}}`)) if err != nil { t.Fatal(err) } if len(refs) != 1 || refs[0].ID != "a2" { t.Fatalf("got %+v", refs) } }) t.Run("batch results", func(t *testing.T) { refs, err := decodeAccountRefs([]byte(`{"success":1,"failed":0,"results":[{"name":"k1","id":123,"success":true}]}`)) if err != nil { t.Fatal(err) } if len(refs) != 1 || refs[0].ID != "123" || refs[0].Name != "k1" { t.Fatalf("got %+v", refs) } }) t.Run("batch results ignores failed items", func(t *testing.T) { refs, err := decodeAccountRefs([]byte(`{"success":1,"failed":1,"results":[{"name":"k1","id":123,"success":true},{"name":"k2","id":456,"success":false}]}`)) if err != nil { t.Fatal(err) } if len(refs) != 1 || refs[0].ID != "123" { t.Fatalf("got %+v", refs) } }) t.Run("data wrapped batch results", func(t *testing.T) { refs, err := decodeAccountRefs([]byte(`{"code":0,"message":"success","data":{"failed":0,"results":[{"id":5,"name":"deepseek-01","success":true}],"success":1}}`)) if err != nil { t.Fatal(err) } if len(refs) != 1 || refs[0].ID != "5" || refs[0].Name != "deepseek-01" { t.Fatalf("got %+v", refs) } }) t.Run("invalid json", func(t *testing.T) { _, err := decodeAccountRefs([]byte(`not json`)) if err == nil { t.Fatal("expected error") } }) } func TestDecodeAccountModels(t *testing.T) { t.Run("envelope", func(t *testing.T) { models, err := decodeAccountModels([]byte(`{"data":[{"id":"gpt4","display_name":"GPT-4","type":"chat"}]}`)) if err != nil { t.Fatal(err) } if len(models) != 1 || models[0].ID != "gpt4" { t.Fatalf("got %+v", models) } }) t.Run("wrapper with items", func(t *testing.T) { models, err := decodeAccountModels([]byte(`{"data":{"items":[{"id":"cl3","display_name":"Claude 3","type":"chat"}]}}`)) if err != nil { t.Fatal(err) } if len(models) != 1 || models[0].ID != "cl3" { t.Fatalf("got %+v", models) } }) t.Run("invalid json", func(t *testing.T) { _, err := decodeAccountModels([]byte(`not json`)) if err == nil { t.Fatal("expected error") } }) } func TestParseProbeResult(t *testing.T) { t.Run("SSE with ok=true", func(t *testing.T) { result, err := parseProbeResult([]byte("data: {\"status\":\"passed\",\"ok\":true}\n")) if err != nil { t.Fatal(err) } if !result.OK || result.Status != "passed" { t.Fatalf("got %+v, want OK=true Status=passed", result) } }) t.Run("SSE with success=true", func(t *testing.T) { result, err := parseProbeResult([]byte("data: {\"status\":\"succeeded\",\"success\":true}\n")) if err != nil { t.Fatal(err) } if !result.OK || result.Status != "passed" { t.Fatalf("got %+v", result) } }) t.Run("SSE with ok=false", func(t *testing.T) { result, err := parseProbeResult([]byte("data: {\"status\":\"failed\",\"ok\":false}\n")) if err != nil { t.Fatal(err) } if result.OK || result.Status != "failed" { t.Fatalf("got %+v", result) } }) t.Run("SSE with status-based ok", func(t *testing.T) { result, err := parseProbeResult([]byte("data: {\"status\":\"pass\",\"message\":\"all good\"}\n")) if err != nil { t.Fatal(err) } if !result.OK || result.Message != "all good" { t.Fatalf("got %+v", result) } }) t.Run("multiple SSE events picks last", func(t *testing.T) { result, err := parseProbeResult([]byte("data: {\"status\":\"running\"}\ndata: {\"status\":\"passed\",\"ok\":true}\n")) if err != nil { t.Fatal(err) } if !result.OK { t.Fatalf("expected OK=true from last event, got %+v", result) } }) t.Run("no data events", func(t *testing.T) { _, err := parseProbeResult([]byte("not data\n")) if err == nil { t.Fatal("expected error") } }) } func TestNormalizeProbeStatus(t *testing.T) { tests := []struct { status string ok bool want string }{ {"pass", true, "passed"}, {"PASSED", true, "passed"}, {"Ok", true, "passed"}, {"success", true, "passed"}, {"succeeded", true, "passed"}, {"fail", false, "failed"}, {"FAILED", false, "failed"}, {"error", false, "failed"}, {"custom_ok", true, "passed"}, {"custom_fail", false, "failed"}, } for _, tt := range tests { t.Run(tt.status, func(t *testing.T) { if got := normalizeProbeStatus(tt.status, tt.ok); got != tt.want { t.Fatalf("normalizeProbeStatus(%q, %v) = %q, want %q", tt.status, tt.ok, got, tt.want) } }) } } func TestLooksLikeExistingEndpoint(t *testing.T) { t.Run("json content type", func(t *testing.T) { h := http.Header{"Content-Type": []string{"application/json"}} if !looksLikeExistingEndpoint(h, nil) { t.Fatal("expected true with json content type") } }) t.Run("sse content type", func(t *testing.T) { h := http.Header{"Content-Type": []string{"text/event-stream"}} if !looksLikeExistingEndpoint(h, nil) { t.Fatal("expected true with sse content type") } }) t.Run("empty body and no content type", func(t *testing.T) { if looksLikeExistingEndpoint(http.Header{}, nil) { t.Fatal("expected false") } }) t.Run("json-like body", func(t *testing.T) { if !looksLikeExistingEndpoint(http.Header{}, []byte(`{"error":"not found"}`)) { t.Fatal("expected true for json body") } }) t.Run("array body", func(t *testing.T) { if !looksLikeExistingEndpoint(http.Header{}, []byte(`[]`)) { t.Fatal("expected true for array body") } }) t.Run("html body", func(t *testing.T) { if looksLikeExistingEndpoint(http.Header{}, []byte(``)) { t.Fatal("expected false for html body") } }) } // Tests for NamedResource type used by the filter functions. // Defined locally since it's in the same package. func TestNewClientWithNilOption(t *testing.T) { client, err := NewClient("http://localhost:8080", nil) if err != nil { t.Fatal(err) } if client == nil { t.Fatal("client is nil") } } func TestNewHTTPError(t *testing.T) { e := newHTTPError("GET", "/v1/models", 200, []byte(`{"ok":true}`)) if e.Method != "GET" || e.Path != "/v1/models" || e.StatusCode != 200 || e.Body != `{"ok":true}` { t.Fatalf("unexpected http error: %+v", e) } } func TestPerformWithMockServer(t *testing.T) { srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { switch r.URL.Path { case "/api/v1/admin/system/version": w.Write([]byte(`{"data":{"version":"v1.2.3"}}`)) case "/api/v1/admin/groups": w.Write([]byte(`{"data":{"id":"g1","name":"test-group"}}`)) case "/api/v1/admin/channels": w.WriteHeader(http.StatusInternalServerError) w.Write([]byte(`{"error":"panic"}`)) default: w.WriteHeader(http.StatusNotFound) } })) defer srv.Close() client, err := NewClient(srv.URL, WithAPIKey("test-key")) if err != nil { t.Fatal(err) } t.Run("GetHostVersion", func(t *testing.T) { ver, err := client.GetHostVersion(context.Background()) if err != nil { t.Fatal(err) } if ver != "v1.2.3" { t.Fatalf("version = %q, want %q", ver, "v1.2.3") } }) t.Run("postJSON success", func(t *testing.T) { var ref GroupRef if err := client.postJSON(context.Background(), "/api/v1/admin/groups", CreateGroupRequest{Name: "test"}, &ref); err != nil { t.Fatal(err) } if ref.ID != "g1" || ref.Name != "test-group" { t.Fatalf("got %+v, want {ID:g1 Name:test-group}", ref) } }) t.Run("postJSON error status", func(t *testing.T) { var ref GroupRef err := client.postJSON(context.Background(), "/api/v1/admin/channels", nil, &ref) if err == nil { t.Fatal("expected error") } var httpErr *HTTPError if !errors.As(err, &httpErr) { t.Fatalf("expected HTTPError, got %T: %v", err, err) } if httpErr.StatusCode != 500 { t.Fatalf("status code = %d, want 500", httpErr.StatusCode) } }) t.Run("getJSON success", func(t *testing.T) { var ref GroupRef if err := client.getJSON(context.Background(), "/api/v1/admin/groups", &ref); err != nil { t.Fatal(err) } }) t.Run("getJSON error status", func(t *testing.T) { var ref GroupRef err := client.getJSON(context.Background(), "/bad/path", &ref) if err == nil { t.Fatal("expected error") } }) } func TestCreateGroupWithMock(t *testing.T) { srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { var req struct { Name string `json:"name"` Platform string `json:"platform"` RateMultiplier float64 `json:"rate_multiplier"` SubscriptionType string `json:"subscription_type"` } if err := json.NewDecoder(r.Body).Decode(&req); err != nil { t.Fatalf("decode request: %v", err) } if req.Name != "demo" || req.Platform != "openai" || req.RateMultiplier != 1.0 { t.Fatalf("unexpected request: %+v", req) } w.Write([]byte(`{"data":{"id":"g1","name":"demo"}}`)) })) defer srv.Close() client, err := NewClient(srv.URL, WithAPIKey("k")) if err != nil { t.Fatal(err) } ref, err := client.CreateGroup(context.Background(), CreateGroupRequest{Name: "demo", Platform: "openai", RateMultiplier: 1.0}) if err != nil { t.Fatal(err) } if ref.ID != "g1" || ref.Name != "demo" { t.Fatalf("got %+v", ref) } } func TestCreateChannelWithMock(t *testing.T) { srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { var req struct { Name string `json:"name"` GroupIDs []int64 `json:"group_ids"` } if err := json.NewDecoder(r.Body).Decode(&req); err != nil { t.Fatalf("decode request: %v", err) } if req.Name != "ch" { t.Fatalf("name = %q, want ch", req.Name) } if len(req.GroupIDs) != 1 || req.GroupIDs[0] != 101 { t.Fatalf("group_ids = %v, want [101]", req.GroupIDs) } w.Write([]byte(`{"data":{"id":201,"name":"ch"}}`)) })) defer srv.Close() client, _ := NewClient(srv.URL, WithAPIKey("k")) ref, err := client.CreateChannel(context.Background(), CreateChannelRequest{Name: "ch", GroupIDs: []string{"101"}}) if err != nil { t.Fatal(err) } if ref.ID != "201" { t.Fatalf("id = %q, want 201", ref.ID) } } func TestCreatePlanWithMock(t *testing.T) { srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { var req struct { GroupID int64 `json:"group_id"` Name string `json:"name"` Price float64 `json:"price"` ValidityDays int `json:"validity_days"` ValidityUnit string `json:"validity_unit"` } if err := json.NewDecoder(r.Body).Decode(&req); err != nil { t.Fatalf("decode request: %v", err) } if req.GroupID != 101 || req.Name != "plan" || req.Price != 19.9 || req.ValidityDays != 30 || req.ValidityUnit != "day" { t.Fatalf("unexpected request: %+v", req) } w.Write([]byte(`{"data":{"id":301,"name":"plan"}}`)) })) defer srv.Close() client, _ := NewClient(srv.URL, WithAPIKey("k")) ref, err := client.CreatePlan(context.Background(), CreatePlanRequest{GroupID: "101", Name: "plan", Price: 19.9, ValidityDays: 30, ValidityUnit: "day"}) if err != nil { t.Fatal(err) } if ref.ID != "301" { t.Fatalf("id = %q, want 301", ref.ID) } } func TestDeleteWithMock(t *testing.T) { srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusNoContent) })) defer srv.Close() client, _ := NewClient(srv.URL, WithAPIKey("k")) t.Run("DeleteGroup", func(t *testing.T) { if err := client.DeleteGroup(context.Background(), "g1"); err != nil { t.Fatal(err) } }) t.Run("DeleteChannel", func(t *testing.T) { if err := client.DeleteChannel(context.Background(), "c1"); err != nil { t.Fatal(err) } }) t.Run("DeletePlan", func(t *testing.T) { if err := client.DeletePlan(context.Background(), "p1"); err != nil { t.Fatal(err) } }) t.Run("DeleteAccount", func(t *testing.T) { if err := client.DeleteAccount(context.Background(), "a1"); err != nil { t.Fatal(err) } }) } func TestAssignSubscriptionWithMock(t *testing.T) { srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { var req struct { UserID int64 `json:"user_id"` GroupID int64 `json:"group_id"` DurationDays int `json:"validity_days"` } if err := json.NewDecoder(r.Body).Decode(&req); err != nil { t.Fatalf("decode request: %v", err) } if req.UserID != 501 || req.GroupID != 101 || req.DurationDays != 30 { t.Fatalf("unexpected request: %+v", req) } w.Write([]byte(`{"data":{"id":401}}`)) })) defer srv.Close() client, _ := NewClient(srv.URL, WithAPIKey("k")) ref, err := client.AssignSubscription(context.Background(), AssignSubscriptionRequest{UserID: "501", GroupID: "101", DurationDays: 30}) if err != nil { t.Fatal(err) } if ref.ID != "401" { t.Fatalf("id = %q", ref.ID) } } func TestCheckGatewayAccessWithMock(t *testing.T) { srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Write([]byte(`{"data":[{"id":"gpt-4"},{"id":"claude-3"}]}`)) })) defer srv.Close() client, _ := NewClient(srv.URL, WithAPIKey("k")) result, err := client.CheckGatewayAccess(context.Background(), GatewayAccessCheckRequest{APIKey: "gk", ExpectedModel: "gpt-4"}) if err != nil { t.Fatal(err) } if !result.OK { t.Fatal("expected OK=true") } if !result.HasExpectedModel { t.Fatal("expected HasExpectedModel=true") } } func TestBatchCreateAccountsWithMock(t *testing.T) { srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { var req struct { Accounts []struct { Name string `json:"name"` Platform string `json:"platform"` Type string `json:"type"` Credentials map[string]any `json:"credentials"` GroupIDs []int64 `json:"group_ids"` } `json:"accounts"` } if err := json.NewDecoder(r.Body).Decode(&req); err != nil { t.Fatalf("decode request: %v", err) } if len(req.Accounts) != 1 { t.Fatalf("accounts len = %d, want 1", len(req.Accounts)) } acct := req.Accounts[0] if acct.Name != "acct1" || acct.Platform != "openai" || acct.Type != "apikey" { t.Fatalf("unexpected account metadata: %+v", acct) } if len(acct.GroupIDs) != 1 || acct.GroupIDs[0] != 101 { t.Fatalf("group_ids = %v, want [101]", acct.GroupIDs) } w.Write([]byte(`{"data":[{"id":601,"name":"acct1"}]}`)) })) defer srv.Close() client, _ := NewClient(srv.URL, WithAPIKey("k")) refs, err := client.BatchCreateAccounts(context.Background(), BatchCreateAccountsRequest{ Accounts: []CreateAccountRequest{{Name: "acct1", Platform: "openai", Type: "apikey", GroupIDs: []string{"101"}, Credentials: map[string]any{"api_key": "sk-test", "base_url": "https://api.example.com"}}}, }) if err != nil { t.Fatal(err) } if len(refs) != 1 || refs[0].ID != "601" { t.Fatalf("got %+v", refs) } } func TestProbeCapabilitiesWithMock(t *testing.T) { callCount := 0 srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { callCount++ w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusOK) _, _ = w.Write([]byte(`{"data":[]}`)) })) defer srv.Close() client, _ := NewClient(srv.URL, WithAPIKey("k")) caps, err := client.ProbeCapabilities(context.Background()) if err != nil { t.Fatal(err) } if !caps.Groups || !caps.Channels || !caps.Plans || !caps.Accounts || !caps.AccountTest || !caps.AccountModels || !caps.Subscriptions { t.Fatalf("all capabilities should be true, got %+v", caps) } if callCount != 7 { t.Fatalf("callCount = %d, want 7", callCount) } } func TestProbeCapabilitiesDoesNotTreat404AsSupportForAccountOrSubscriptionRoutes(t *testing.T) { srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json") switch r.URL.Path { case "/api/v1/admin/groups", "/api/v1/admin/channels", "/api/v1/admin/payment/plans", "/api/v1/admin/accounts": w.WriteHeader(http.StatusOK) _, _ = w.Write([]byte(`{"data":[]}`)) case "/api/v1/admin/accounts/__probe__/test", "/api/v1/admin/accounts/__probe__/models", "/api/v1/admin/subscriptions/assign": w.WriteHeader(http.StatusNotFound) _, _ = w.Write([]byte(`{"error":"not found"}`)) default: w.WriteHeader(http.StatusNotFound) } })) defer srv.Close() client, _ := NewClient(srv.URL, WithAPIKey("k")) caps, err := client.ProbeCapabilities(context.Background()) if err != nil { t.Fatalf("ProbeCapabilities() error = %v", err) } if caps.AccountTest { t.Fatal("AccountTest = true, want false on 404 probe route") } if caps.AccountModels { t.Fatal("AccountModels = true, want false on 404 probe route") } if caps.Subscriptions { t.Fatal("Subscriptions = true, want false on 404 probe route") } } func TestListManagedResourcesWithMock(t *testing.T) { srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Write([]byte(`{"data":{"items":[ {"id":"r1","name":"resource-1"} ]}}`)) })) defer srv.Close() client, _ := NewClient(srv.URL, WithAPIKey("k")) snapshot, err := client.ListManagedResources(context.Background(), ListManagedResourcesRequest{}) if err != nil { t.Fatal(err) } if len(snapshot.Groups) != 1 { t.Fatalf("expected 1 group, got %d", len(snapshot.Groups)) } } func TestTestAccountWithMock(t *testing.T) { srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Write([]byte("data: {\"status\":\"passed\",\"ok\":true}\n")) })) defer srv.Close() client, _ := NewClient(srv.URL, WithAPIKey("k")) result, err := client.TestAccount(context.Background(), "a1") if err != nil { t.Fatal(err) } if !result.OK { t.Fatal("expected OK=true") } } func TestGetAccountModelsWithMock(t *testing.T) { srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Write([]byte(`{"data":[{"id":"m1","display_name":"M1","type":"chat"}]}`)) })) defer srv.Close() client, _ := NewClient(srv.URL, WithAPIKey("k")) models, err := client.GetAccountModels(context.Background(), "a1") if err != nil { t.Fatal(err) } if len(models) != 1 || models[0].ID != "m1" { t.Fatalf("got %+v", models) } }