feat(control-plane): harden host-scoped reconcile and acceptance evidence
- add batch-scoped reconcile_runs persistence and queries - route batch detail and reconcile writes through batch_id/host_id - refresh production boards with host-scope acceptance artifacts - include latest real-host acceptance evidence for self_service and subscription
This commit is contained in:
@@ -2,6 +2,7 @@ package sub2api
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
@@ -234,6 +235,15 @@ func TestDecodeNamedResources(t *testing.T) {
|
||||
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 {
|
||||
@@ -261,6 +271,15 @@ func TestDecodeAccountRefs(t *testing.T) {
|
||||
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 {
|
||||
@@ -270,6 +289,33 @@ func TestDecodeAccountRefs(t *testing.T) {
|
||||
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 {
|
||||
@@ -439,7 +485,6 @@ func TestNewHTTPError(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
func TestPerformWithMockServer(t *testing.T) {
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
switch r.URL.Path {
|
||||
@@ -514,6 +559,18 @@ func TestPerformWithMockServer(t *testing.T) {
|
||||
|
||||
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()
|
||||
@@ -522,7 +579,7 @@ func TestCreateGroupWithMock(t *testing.T) {
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
ref, err := client.CreateGroup(context.Background(), CreateGroupRequest{Name: "demo", RateMultiplier: 1.0})
|
||||
ref, err := client.CreateGroup(context.Background(), CreateGroupRequest{Name: "demo", Platform: "openai", RateMultiplier: 1.0})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -533,26 +590,58 @@ func TestCreateGroupWithMock(t *testing.T) {
|
||||
|
||||
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"}}`))
|
||||
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"))
|
||||
_, err := client.CreateChannel(context.Background(), CreateChannelRequest{Name: "ch"})
|
||||
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) {
|
||||
w.Write([]byte(`{"data":{"id":"p1","name":"plan"}}`))
|
||||
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"))
|
||||
_, err := client.CreatePlan(context.Background(), CreatePlanRequest{Name: "plan"})
|
||||
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) {
|
||||
@@ -586,15 +675,26 @@ func TestDeleteWithMock(t *testing.T) {
|
||||
|
||||
func TestAssignSubscriptionWithMock(t *testing.T) {
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Write([]byte(`{"data":{"id":"s1"}}`))
|
||||
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: "u1"})
|
||||
ref, err := client.AssignSubscription(context.Background(), AssignSubscriptionRequest{UserID: "501", GroupID: "101", DurationDays: 30})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if ref.ID != "s1" {
|
||||
if ref.ID != "401" {
|
||||
t.Fatalf("id = %q", ref.ID)
|
||||
}
|
||||
}
|
||||
@@ -619,17 +719,39 @@ func TestCheckGatewayAccessWithMock(t *testing.T) {
|
||||
|
||||
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"}]}`))
|
||||
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"}},
|
||||
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 != "a1" {
|
||||
if len(refs) != 1 || refs[0].ID != "601" {
|
||||
t.Fatalf("got %+v", refs)
|
||||
}
|
||||
}
|
||||
@@ -638,7 +760,9 @@ func TestProbeCapabilitiesWithMock(t *testing.T) {
|
||||
callCount := 0
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
callCount++
|
||||
w.WriteHeader(http.StatusCreated)
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(http.StatusOK)
|
||||
_, _ = w.Write([]byte(`{"data":[]}`))
|
||||
}))
|
||||
defer srv.Close()
|
||||
client, _ := NewClient(srv.URL, WithAPIKey("k"))
|
||||
@@ -649,6 +773,41 @@ func TestProbeCapabilitiesWithMock(t *testing.T) {
|
||||
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) {
|
||||
|
||||
Reference in New Issue
Block a user