package app import ( "context" "encoding/json" "path/filepath" "testing" "sub2api-cn-relay-manager/internal/store/sqlite" ) func TestAPIListPortalLogicalGroups(t *testing.T) { handler := NewAPIHandler("secret-token", ActionSet{ ListPortalLogicalGroups: func(_ context.Context) ([]PortalLogicalGroupInfo, error) { return []PortalLogicalGroupInfo{{ LogicalGroupID: "gpt-shared", DisplayName: "GPT Shared", Status: "active", RouteCount: 2, ActiveRouteCount: 1, PublicModels: []PortalLogicalGroupModel{ {PublicModel: "gpt-5.4", Status: "active"}, }, }}, nil }, }) request := httptestRequest(t, "GET", "/api/portal/logical-groups", nil, "") response := httptestRecorder(handler, request) assertStatusCode(t, response, 200) var listPayload struct { LogicalGroups []PortalLogicalGroupInfo `json:"logical_groups"` } if err := json.Unmarshal(response.Body().Bytes(), &listPayload); err != nil { t.Fatalf("json.Unmarshal() error = %v", err) } if len(listPayload.LogicalGroups) != 1 || listPayload.LogicalGroups[0].LogicalGroupID != "gpt-shared" || listPayload.LogicalGroups[0].RouteCount != 2 { t.Fatalf("portal logical groups payload = %+v", listPayload) } } func TestAPIGetPortalLogicalGroupModels(t *testing.T) { handler := NewAPIHandler("secret-token", ActionSet{ ListPortalLogicalGroupModels: func(_ context.Context, logicalGroupID string) ([]PortalLogicalGroupModel, error) { if logicalGroupID != "gpt-shared" { t.Fatalf("logicalGroupID = %q, want gpt-shared", logicalGroupID) } return []PortalLogicalGroupModel{{PublicModel: "gpt-5.4", Status: "active"}}, nil }, }) request := httptestRequest(t, "GET", "/api/portal/logical-groups/gpt-shared/models", nil, "") response := httptestRecorder(handler, request) assertStatusCode(t, response, 200) var modelsPayload struct { Models []PortalLogicalGroupModel `json:"models"` } if err := json.Unmarshal(response.Body().Bytes(), &modelsPayload); err != nil { t.Fatalf("json.Unmarshal() error = %v", err) } if len(modelsPayload.Models) != 1 || modelsPayload.Models[0].PublicModel != "gpt-5.4" { t.Fatalf("portal models payload = %+v", modelsPayload) } } func TestNewActionSetPortalLogicalGroups(t *testing.T) { dbPath := filepath.Join(t.TempDir(), "portal-logical-groups.db") dsn := "file:" + filepath.ToSlash(dbPath) + "?_busy_timeout=5000" actions := NewActionSet(dsn) ctx := context.Background() store, err := sqlite.Open(ctx, dsn) if err != nil { t.Fatalf("sqlite.Open() error = %v", err) } defer store.Close() if _, err := store.LogicalGroups().Create(ctx, sqlite.LogicalGroup{ LogicalGroupID: "gpt-shared", DisplayName: "GPT Shared", Status: "active", Description: "Public GPT product", RoutePolicy: "priority", StickyMode: "conversation_preferred", ConversationTTLSeconds: 7200, UserModelTTLSeconds: 1800, FailoverThreshold: 2, CooldownSeconds: 600, }); err != nil { t.Fatalf("LogicalGroups().Create(active) error = %v", err) } if _, err := store.LogicalGroups().Create(ctx, sqlite.LogicalGroup{ LogicalGroupID: "disabled-group", DisplayName: "Disabled Group", Status: "disabled", }); err != nil { t.Fatalf("LogicalGroups().Create(disabled) error = %v", err) } if _, err := store.LogicalGroupModels().Create(ctx, sqlite.LogicalGroupModel{ LogicalGroupID: "gpt-shared", PublicModel: "gpt-5.4", Status: "active", }); err != nil { t.Fatalf("LogicalGroupModels().Create(active) error = %v", err) } if _, err := store.LogicalGroupModels().Create(ctx, sqlite.LogicalGroupModel{ LogicalGroupID: "gpt-shared", PublicModel: "gpt-hidden", Status: "disabled", }); err != nil { t.Fatalf("LogicalGroupModels().Create(disabled) error = %v", err) } if _, err := store.LogicalGroupRoutes().Create(ctx, sqlite.LogicalGroupRoute{ RouteID: "asxs", LogicalGroupID: "gpt-shared", Name: "ASXS", Status: "active", Priority: 10, Weight: 100, ShadowGroupID: "9", ShadowHostID: "remote43", UpstreamBaseURLHint: "https://api.asxs.top/v1", }); err != nil { t.Fatalf("LogicalGroupRoutes().Create(active) error = %v", err) } if _, err := store.LogicalGroupRoutes().Create(ctx, sqlite.LogicalGroupRoute{ RouteID: "backup", LogicalGroupID: "gpt-shared", Name: "Backup", Status: "cooldown", Priority: 20, Weight: 100, ShadowGroupID: "9", ShadowHostID: "remote43", UpstreamBaseURLHint: "https://backup.example.com", }); err != nil { t.Fatalf("LogicalGroupRoutes().Create(cooldown) error = %v", err) } listed, err := actions.ListPortalLogicalGroups(ctx) if err != nil { t.Fatalf("ListPortalLogicalGroups() error = %v", err) } if len(listed) != 1 || listed[0].LogicalGroupID != "gpt-shared" { t.Fatalf("ListPortalLogicalGroups() = %+v", listed) } if listed[0].RouteCount != 2 || listed[0].ActiveRouteCount != 1 { t.Fatalf("ListPortalLogicalGroups() route counts = %+v", listed[0]) } if len(listed[0].PublicModels) != 1 || listed[0].PublicModels[0].PublicModel != "gpt-5.4" { t.Fatalf("ListPortalLogicalGroups() public models = %+v", listed[0].PublicModels) } group, err := actions.GetPortalLogicalGroup(ctx, "gpt-shared") if err != nil { t.Fatalf("GetPortalLogicalGroup() error = %v", err) } if group.DisplayName != "GPT Shared" || group.RoutePolicy != "priority" { t.Fatalf("GetPortalLogicalGroup() = %+v", group) } models, err := actions.ListPortalLogicalGroupModels(ctx, "gpt-shared") if err != nil { t.Fatalf("ListPortalLogicalGroupModels() error = %v", err) } if len(models) != 1 || models[0].PublicModel != "gpt-5.4" { t.Fatalf("ListPortalLogicalGroupModels() = %+v", models) } request := httptestRequest(t, "GET", "/api/portal/logical-groups/gpt-shared", nil, "") response := httptestRecorder(NewAPIHandler("secret-token", actions), request) assertStatusCode(t, response, 200) var payload map[string]PortalLogicalGroupInfo if err := json.Unmarshal(response.Body().Bytes(), &payload); err != nil { t.Fatalf("json.Unmarshal() error = %v", err) } if payload["logical_group"].LogicalGroupID != "gpt-shared" { t.Fatalf("portal logical group payload = %+v", payload) } }