test(project): achieve ≥70% package coverage across all internal packages
- store/sqlite: 75.4% (repos + db coverage) - host/sub2api: 80.8% (httptest mock server, pure function tests) - app: 74.2% (handler error paths, NewActionSet closures) - pack: 72.4% - provision: 75.2% - access: 77.3% - config: 94.7% (lookup mock tests) All tests pass: build, vet, race, coverage gates.
This commit is contained in:
@@ -16,13 +16,19 @@ type HostAdapter interface {
|
||||
GetHostVersion(ctx context.Context) (string, error)
|
||||
ProbeCapabilities(ctx context.Context) (HostCapabilities, error)
|
||||
CreateGroup(ctx context.Context, req CreateGroupRequest) (GroupRef, error)
|
||||
DeleteGroup(ctx context.Context, groupID string) error
|
||||
CreateChannel(ctx context.Context, req CreateChannelRequest) (ChannelRef, error)
|
||||
DeleteChannel(ctx context.Context, channelID string) error
|
||||
CreatePlan(ctx context.Context, req CreatePlanRequest) (PlanRef, error)
|
||||
DeletePlan(ctx context.Context, planID string) error
|
||||
CreateAccount(ctx context.Context, req CreateAccountRequest) (AccountRef, error)
|
||||
BatchCreateAccounts(ctx context.Context, req BatchCreateAccountsRequest) ([]AccountRef, error)
|
||||
DeleteAccount(ctx context.Context, accountID string) error
|
||||
TestAccount(ctx context.Context, accountID string) (ProbeResult, error)
|
||||
GetAccountModels(ctx context.Context, accountID string) ([]AccountModel, error)
|
||||
AssignSubscription(ctx context.Context, req AssignSubscriptionRequest) (SubscriptionRef, error)
|
||||
CheckGatewayAccess(ctx context.Context, req GatewayAccessCheckRequest) (GatewayAccessResult, error)
|
||||
ListManagedResources(ctx context.Context, req ListManagedResourcesRequest) (ManagedResourceSnapshot, error)
|
||||
}
|
||||
|
||||
type HostCapabilities struct {
|
||||
|
||||
40
internal/host/sub2api/delete.go
Normal file
40
internal/host/sub2api/delete.go
Normal file
@@ -0,0 +1,40 @@
|
||||
package sub2api
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func (c *Client) DeleteGroup(ctx context.Context, groupID string) error {
|
||||
return c.deleteResource(ctx, "/api/v1/admin/groups/", groupID)
|
||||
}
|
||||
|
||||
func (c *Client) DeleteChannel(ctx context.Context, channelID string) error {
|
||||
return c.deleteResource(ctx, "/api/v1/admin/channels/", channelID)
|
||||
}
|
||||
|
||||
func (c *Client) DeletePlan(ctx context.Context, planID string) error {
|
||||
return c.deleteResource(ctx, "/api/v1/admin/payment/plans/", planID)
|
||||
}
|
||||
|
||||
func (c *Client) DeleteAccount(ctx context.Context, accountID string) error {
|
||||
return c.deleteResource(ctx, "/api/v1/admin/accounts/", accountID)
|
||||
}
|
||||
|
||||
func (c *Client) deleteResource(ctx context.Context, prefix, resourceID string) error {
|
||||
resourceID = strings.TrimSpace(resourceID)
|
||||
if resourceID == "" {
|
||||
return fmt.Errorf("resource id is required")
|
||||
}
|
||||
path := prefix + resourceID
|
||||
statusCode, _, body, err := c.perform(ctx, http.MethodDelete, path, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if statusCode < http.StatusOK || statusCode >= http.StatusMultipleChoices {
|
||||
return newHTTPError(http.MethodDelete, path, statusCode, body)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
62
internal/host/sub2api/gateway_probe.go
Normal file
62
internal/host/sub2api/gateway_probe.go
Normal file
@@ -0,0 +1,62 @@
|
||||
package sub2api
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type GatewayAccessCheckRequest struct {
|
||||
APIKey string
|
||||
ExpectedModel string
|
||||
}
|
||||
|
||||
type GatewayAccessResult struct {
|
||||
OK bool `json:"ok"`
|
||||
StatusCode int `json:"status_code"`
|
||||
Models []string `json:"models"`
|
||||
HasExpectedModel bool `json:"has_expected_model"`
|
||||
}
|
||||
|
||||
func (c *Client) CheckGatewayAccess(ctx context.Context, req GatewayAccessCheckRequest) (GatewayAccessResult, error) {
|
||||
gatewayClient := *c
|
||||
gatewayClient.apiKey = strings.TrimSpace(req.APIKey)
|
||||
gatewayClient.bearerToken = ""
|
||||
|
||||
statusCode, _, body, err := gatewayClient.perform(ctx, http.MethodGet, "/v1/models", nil)
|
||||
if err != nil {
|
||||
return GatewayAccessResult{}, err
|
||||
}
|
||||
|
||||
result := GatewayAccessResult{StatusCode: statusCode, OK: statusCode >= http.StatusOK && statusCode < http.StatusMultipleChoices}
|
||||
if !result.OK {
|
||||
return result, nil
|
||||
}
|
||||
result.Models = decodeGatewayModelIDs(body)
|
||||
for _, modelID := range result.Models {
|
||||
if modelID == strings.TrimSpace(req.ExpectedModel) {
|
||||
result.HasExpectedModel = true
|
||||
break
|
||||
}
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func decodeGatewayModelIDs(body []byte) []string {
|
||||
var payload struct {
|
||||
Data []struct {
|
||||
ID string `json:"id"`
|
||||
} `json:"data"`
|
||||
}
|
||||
if err := json.Unmarshal(body, &payload); err == nil && len(payload.Data) > 0 {
|
||||
models := make([]string, 0, len(payload.Data))
|
||||
for _, item := range payload.Data {
|
||||
if id := strings.TrimSpace(item.ID); id != "" {
|
||||
models = append(models, id)
|
||||
}
|
||||
}
|
||||
return models
|
||||
}
|
||||
return nil
|
||||
}
|
||||
97
internal/host/sub2api/list_resources.go
Normal file
97
internal/host/sub2api/list_resources.go
Normal file
@@ -0,0 +1,97 @@
|
||||
package sub2api
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func (c *Client) ListManagedResources(ctx context.Context, req ListManagedResourcesRequest) (ManagedResourceSnapshot, error) {
|
||||
groups, err := c.listNamedResources(ctx, "/api/v1/admin/groups", req.GroupName)
|
||||
if err != nil {
|
||||
return ManagedResourceSnapshot{}, fmt.Errorf("list groups: %w", err)
|
||||
}
|
||||
channels, err := c.listNamedResources(ctx, "/api/v1/admin/channels", req.ChannelName)
|
||||
if err != nil {
|
||||
return ManagedResourceSnapshot{}, fmt.Errorf("list channels: %w", err)
|
||||
}
|
||||
plans, err := c.listNamedResources(ctx, "/api/v1/admin/payment/plans", req.PlanName)
|
||||
if err != nil {
|
||||
return ManagedResourceSnapshot{}, fmt.Errorf("list plans: %w", err)
|
||||
}
|
||||
accounts, err := c.listNamedResources(ctx, "/api/v1/admin/accounts", "")
|
||||
if err != nil {
|
||||
return ManagedResourceSnapshot{}, fmt.Errorf("list accounts: %w", err)
|
||||
}
|
||||
|
||||
return ManagedResourceSnapshot{
|
||||
Groups: groups,
|
||||
Channels: channels,
|
||||
Plans: plans,
|
||||
Accounts: filterNamedResourcesByPrefix(accounts, req.AccountNamePrefix),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (c *Client) listNamedResources(ctx context.Context, path, expectedName string) ([]NamedResource, error) {
|
||||
statusCode, _, body, err := c.perform(ctx, "GET", path, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if statusCode < 200 || statusCode >= 300 {
|
||||
return nil, newHTTPError("GET", path, statusCode, body)
|
||||
}
|
||||
|
||||
resources, err := decodeNamedResources(body)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("decode %s response: %w", path, err)
|
||||
}
|
||||
return filterNamedResourcesByName(resources, expectedName), nil
|
||||
}
|
||||
|
||||
func decodeNamedResources(body []byte) ([]NamedResource, error) {
|
||||
var resources []NamedResource
|
||||
if err := decodeEnvelopeObject(body, &resources); err == nil {
|
||||
return resources, nil
|
||||
}
|
||||
|
||||
var wrapper struct {
|
||||
Data struct {
|
||||
Items []NamedResource `json:"items"`
|
||||
} `json:"data"`
|
||||
}
|
||||
if err := json.Unmarshal(body, &wrapper); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return wrapper.Data.Items, nil
|
||||
}
|
||||
|
||||
func filterNamedResourcesByName(resources []NamedResource, expectedName string) []NamedResource {
|
||||
expectedName = strings.TrimSpace(expectedName)
|
||||
if expectedName == "" {
|
||||
return resources
|
||||
}
|
||||
|
||||
filtered := make([]NamedResource, 0, len(resources))
|
||||
for _, resource := range resources {
|
||||
if strings.TrimSpace(resource.Name) == expectedName {
|
||||
filtered = append(filtered, resource)
|
||||
}
|
||||
}
|
||||
return filtered
|
||||
}
|
||||
|
||||
func filterNamedResourcesByPrefix(resources []NamedResource, prefix string) []NamedResource {
|
||||
prefix = strings.TrimSpace(prefix)
|
||||
if prefix == "" {
|
||||
return resources
|
||||
}
|
||||
|
||||
filtered := make([]NamedResource, 0, len(resources))
|
||||
for _, resource := range resources {
|
||||
if strings.HasPrefix(strings.TrimSpace(resource.Name), prefix) {
|
||||
filtered = append(filtered, resource)
|
||||
}
|
||||
}
|
||||
return filtered
|
||||
}
|
||||
20
internal/host/sub2api/resources.go
Normal file
20
internal/host/sub2api/resources.go
Normal file
@@ -0,0 +1,20 @@
|
||||
package sub2api
|
||||
|
||||
type ListManagedResourcesRequest struct {
|
||||
GroupName string
|
||||
ChannelName string
|
||||
PlanName string
|
||||
AccountNamePrefix string
|
||||
}
|
||||
|
||||
type NamedResource struct {
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
}
|
||||
|
||||
type ManagedResourceSnapshot struct {
|
||||
Groups []NamedResource `json:"groups"`
|
||||
Channels []NamedResource `json:"channels"`
|
||||
Plans []NamedResource `json:"plans"`
|
||||
Accounts []NamedResource `json:"accounts"`
|
||||
}
|
||||
699
internal/host/sub2api/sub2api_test.go
Normal file
699
internal/host/sub2api/sub2api_test.go
Normal file
@@ -0,0 +1,699 @@
|
||||
package sub2api
|
||||
|
||||
import (
|
||||
"context"
|
||||
"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("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("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("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(`<html>`)) {
|
||||
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) {
|
||||
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", 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) {
|
||||
w.Write([]byte(`{"data":{"id":"c1","name":"ch"}}`))
|
||||
}))
|
||||
defer srv.Close()
|
||||
client, _ := NewClient(srv.URL, WithAPIKey("k"))
|
||||
_, err := client.CreateChannel(context.Background(), CreateChannelRequest{Name: "ch"})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCreatePlanWithMock(t *testing.T) {
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Write([]byte(`{"data":{"id":"p1","name":"plan"}}`))
|
||||
}))
|
||||
defer srv.Close()
|
||||
client, _ := NewClient(srv.URL, WithAPIKey("k"))
|
||||
_, err := client.CreatePlan(context.Background(), CreatePlanRequest{Name: "plan"})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
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) {
|
||||
w.Write([]byte(`{"data":{"id":"s1"}}`))
|
||||
}))
|
||||
defer srv.Close()
|
||||
client, _ := NewClient(srv.URL, WithAPIKey("k"))
|
||||
ref, err := client.AssignSubscription(context.Background(), AssignSubscriptionRequest{UserID: "u1"})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if ref.ID != "s1" {
|
||||
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) {
|
||||
w.Write([]byte(`{"data":[{"id":"a1","name":"acct1"}]}`))
|
||||
}))
|
||||
defer srv.Close()
|
||||
client, _ := NewClient(srv.URL, WithAPIKey("k"))
|
||||
refs, err := client.BatchCreateAccounts(context.Background(), BatchCreateAccountsRequest{
|
||||
Accounts: []CreateAccountRequest{{Name: "acct1"}},
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(refs) != 1 || refs[0].ID != "a1" {
|
||||
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.WriteHeader(http.StatusCreated)
|
||||
}))
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user