308 lines
13 KiB
Go
308 lines
13 KiB
Go
package provision
|
|
|
|
import (
|
|
"context"
|
|
"testing"
|
|
|
|
"sub2api-cn-relay-manager/internal/host/sub2api"
|
|
"sub2api-cn-relay-manager/internal/pack"
|
|
"sub2api-cn-relay-manager/internal/store/sqlite"
|
|
)
|
|
|
|
func TestReconcileServiceReturnsActiveAfterProbeRerun(t *testing.T) {
|
|
store := openProvisionTestStore(t)
|
|
defer closeProvisionTestStore(t, store)
|
|
|
|
host := &fakeHostAdapter{
|
|
batchAccounts: []sub2api.AccountRef{{ID: "account_1", Name: "deepseek-01"}, {ID: "account_2", Name: "deepseek-02"}},
|
|
testResults: map[string]sub2api.ProbeResult{
|
|
"account_1": {OK: true, Status: "passed"},
|
|
"account_2": {OK: true, Status: "passed"},
|
|
},
|
|
models: map[string][]sub2api.AccountModel{
|
|
"account_1": {{ID: "deepseek-chat"}},
|
|
"account_2": {{ID: "deepseek-chat"}},
|
|
},
|
|
gatewayResult: sub2api.GatewayAccessResult{OK: true, StatusCode: 200, HasExpectedModel: true, Models: []string{"deepseek-chat"}},
|
|
}
|
|
|
|
batchID := seedRuntimeImportForReconcile(t, store, host)
|
|
host.managedSnapshot = sub2api.ManagedResourceSnapshot{
|
|
Groups: []sub2api.NamedResource{{ID: "group_1", Name: "DeepSeek 默认分组-self-service"}},
|
|
Channels: []sub2api.NamedResource{{ID: "channel_1", Name: "DeepSeek 默认渠道-self-service"}},
|
|
Accounts: []sub2api.NamedResource{{ID: "account_1", Name: "deepseek-01"}, {ID: "account_2", Name: "deepseek-02"}},
|
|
}
|
|
|
|
result, err := NewReconcileService(store, host).Reconcile(context.Background(), ReconcileRequest{
|
|
HostID: "host-1",
|
|
HostBaseURL: "https://sub2api.example.com",
|
|
AccessProbeAPIKey: "user-key",
|
|
Pack: pack.LoadedPack{Manifest: pack.Manifest{PackID: "openai-cn-pack", Version: "1.0.0", TargetHost: "sub2api", MinHostVersion: "0.1.126", MaxHostVersion: "0.2.x"}},
|
|
Provider: sampleProviderManifest(),
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("Reconcile() error = %v", err)
|
|
}
|
|
if result.BatchID != batchID {
|
|
t.Fatalf("BatchID = %d, want %d", result.BatchID, batchID)
|
|
}
|
|
if result.Status != "active" {
|
|
t.Fatalf("Status = %q, want active", result.Status)
|
|
}
|
|
if result.ProbeFailureCount != 0 {
|
|
t.Fatalf("ProbeFailureCount = %d, want 0", result.ProbeFailureCount)
|
|
}
|
|
if result.AccessStatus != AccessStatusSelfServiceReady {
|
|
t.Fatalf("AccessStatus = %q, want %q", result.AccessStatus, AccessStatusSelfServiceReady)
|
|
}
|
|
if got := queryCount(t, store.SQLDB(), "probe_results"); got != 4 {
|
|
t.Fatalf("probe_results row count = %d, want 4 after rerun", got)
|
|
}
|
|
if got := queryCount(t, store.SQLDB(), "access_closure_records"); got != 2 {
|
|
t.Fatalf("access_closure_records row count = %d, want 2 after rerun", got)
|
|
}
|
|
}
|
|
|
|
func TestReconcileServiceReturnsDegradedWhenProbeRerunFails(t *testing.T) {
|
|
store := openProvisionTestStore(t)
|
|
defer closeProvisionTestStore(t, store)
|
|
|
|
host := &fakeHostAdapter{
|
|
batchAccounts: []sub2api.AccountRef{{ID: "account_1", Name: "deepseek-01"}, {ID: "account_2", Name: "deepseek-02"}},
|
|
testResults: map[string]sub2api.ProbeResult{
|
|
"account_1": {OK: true, Status: "passed"},
|
|
"account_2": {OK: false, Status: "failed", Message: "bad key"},
|
|
},
|
|
models: map[string][]sub2api.AccountModel{
|
|
"account_1": {{ID: "deepseek-chat"}},
|
|
"account_2": {{ID: "deepseek-chat"}},
|
|
},
|
|
gatewayResult: sub2api.GatewayAccessResult{OK: true, StatusCode: 200, HasExpectedModel: true, Models: []string{"deepseek-chat"}},
|
|
}
|
|
|
|
seedRuntimeImportForReconcile(t, store, host)
|
|
host.managedSnapshot = sub2api.ManagedResourceSnapshot{
|
|
Groups: []sub2api.NamedResource{{ID: "group_1", Name: "DeepSeek 默认分组-self-service"}},
|
|
Channels: []sub2api.NamedResource{{ID: "channel_1", Name: "DeepSeek 默认渠道-self-service"}},
|
|
Accounts: []sub2api.NamedResource{{ID: "account_1", Name: "deepseek-01"}, {ID: "account_2", Name: "deepseek-02"}},
|
|
}
|
|
|
|
result, err := NewReconcileService(store, host).Reconcile(context.Background(), ReconcileRequest{
|
|
HostID: "host-1",
|
|
HostBaseURL: "https://sub2api.example.com",
|
|
AccessProbeAPIKey: "user-key",
|
|
Pack: pack.LoadedPack{Manifest: pack.Manifest{PackID: "openai-cn-pack", Version: "1.0.0", TargetHost: "sub2api", MinHostVersion: "0.1.126", MaxHostVersion: "0.2.x"}},
|
|
Provider: sampleProviderManifest(),
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("Reconcile() error = %v", err)
|
|
}
|
|
if result.Status != "degraded" {
|
|
t.Fatalf("Status = %q, want degraded", result.Status)
|
|
}
|
|
if result.ProbeFailureCount != 1 {
|
|
t.Fatalf("ProbeFailureCount = %d, want 1", result.ProbeFailureCount)
|
|
}
|
|
}
|
|
|
|
func TestReconcileServiceReturnsDriftedWhenManagedResourceMissing(t *testing.T) {
|
|
store := openProvisionTestStore(t)
|
|
defer closeProvisionTestStore(t, store)
|
|
|
|
host := &fakeHostAdapter{
|
|
batchAccounts: []sub2api.AccountRef{{ID: "account_1", Name: "deepseek-01"}, {ID: "account_2", Name: "deepseek-02"}},
|
|
testResults: map[string]sub2api.ProbeResult{
|
|
"account_1": {OK: true, Status: "passed"},
|
|
"account_2": {OK: true, Status: "passed"},
|
|
},
|
|
models: map[string][]sub2api.AccountModel{
|
|
"account_1": {{ID: "deepseek-chat"}},
|
|
"account_2": {{ID: "deepseek-chat"}},
|
|
},
|
|
gatewayResult: sub2api.GatewayAccessResult{OK: true, StatusCode: 200, HasExpectedModel: true, Models: []string{"deepseek-chat"}},
|
|
}
|
|
|
|
seedRuntimeImportForReconcile(t, store, host)
|
|
host.managedSnapshot = sub2api.ManagedResourceSnapshot{
|
|
Groups: []sub2api.NamedResource{{ID: "group_1", Name: "DeepSeek 默认分组-self-service"}},
|
|
Channels: []sub2api.NamedResource{{ID: "channel_1", Name: "DeepSeek 默认渠道-self-service"}},
|
|
Accounts: []sub2api.NamedResource{{ID: "account_1", Name: "deepseek-01"}},
|
|
}
|
|
|
|
result, err := NewReconcileService(store, host).Reconcile(context.Background(), ReconcileRequest{
|
|
HostID: "host-1",
|
|
HostBaseURL: "https://sub2api.example.com",
|
|
AccessProbeAPIKey: "user-key",
|
|
Pack: pack.LoadedPack{Manifest: pack.Manifest{PackID: "openai-cn-pack", Version: "1.0.0", TargetHost: "sub2api", MinHostVersion: "0.1.126", MaxHostVersion: "0.2.x"}},
|
|
Provider: sampleProviderManifest(),
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("Reconcile() error = %v", err)
|
|
}
|
|
if result.Status != "drifted" {
|
|
t.Fatalf("Status = %q, want drifted", result.Status)
|
|
}
|
|
if result.MissingCount != 1 {
|
|
t.Fatalf("MissingCount = %d, want 1", result.MissingCount)
|
|
}
|
|
}
|
|
|
|
func TestReconcileServiceIgnoresSubscriptionPlanForSelfServiceBatch(t *testing.T) {
|
|
store := openProvisionTestStore(t)
|
|
defer closeProvisionTestStore(t, store)
|
|
|
|
host := &fakeHostAdapter{
|
|
batchAccounts: []sub2api.AccountRef{{ID: "account_1", Name: "deepseek-01"}, {ID: "account_2", Name: "deepseek-02"}},
|
|
testResults: map[string]sub2api.ProbeResult{
|
|
"account_1": {OK: true, Status: "passed"},
|
|
"account_2": {OK: true, Status: "passed"},
|
|
},
|
|
models: map[string][]sub2api.AccountModel{
|
|
"account_1": {{ID: "deepseek-chat"}},
|
|
"account_2": {{ID: "deepseek-chat"}},
|
|
},
|
|
gatewayResult: sub2api.GatewayAccessResult{OK: true, StatusCode: 200, HasExpectedModel: true, Models: []string{"deepseek-chat"}},
|
|
}
|
|
|
|
seedRuntimeImportForReconcile(t, store, host)
|
|
host.managedSnapshot = sub2api.ManagedResourceSnapshot{
|
|
Groups: []sub2api.NamedResource{{ID: "group_1", Name: "DeepSeek 默认分组-self-service"}},
|
|
Channels: []sub2api.NamedResource{{ID: "channel_1", Name: "DeepSeek 默认渠道-self-service"}},
|
|
Plans: []sub2api.NamedResource{{ID: "plan_1", Name: "DeepSeek 默认套餐"}},
|
|
Accounts: []sub2api.NamedResource{{ID: "account_1", Name: "deepseek-01"}, {ID: "account_2", Name: "deepseek-02"}},
|
|
}
|
|
|
|
result, err := NewReconcileService(store, host).Reconcile(context.Background(), ReconcileRequest{
|
|
HostID: "host-1",
|
|
HostBaseURL: "https://sub2api.example.com",
|
|
AccessProbeAPIKey: "user-key",
|
|
Pack: pack.LoadedPack{Manifest: pack.Manifest{PackID: "openai-cn-pack", Version: "1.0.0", TargetHost: "sub2api", MinHostVersion: "0.1.126", MaxHostVersion: "0.2.x"}},
|
|
Provider: sampleProviderManifest(),
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("Reconcile() error = %v", err)
|
|
}
|
|
if result.Status != "active" {
|
|
t.Fatalf("Status = %q, want active", result.Status)
|
|
}
|
|
if result.ExtraCount != 0 {
|
|
t.Fatalf("ExtraCount = %d, want 0", result.ExtraCount)
|
|
}
|
|
if host.listManagedReq.PlanName != "" {
|
|
t.Fatalf("PlanName = %q, want empty for self_service reconcile", host.listManagedReq.PlanName)
|
|
}
|
|
}
|
|
|
|
func TestReconcileServicePassesAccountNamePrefixToManagedResourceSnapshot(t *testing.T) {
|
|
store := openProvisionTestStore(t)
|
|
defer closeProvisionTestStore(t, store)
|
|
|
|
host := &fakeHostAdapter{
|
|
batchAccounts: []sub2api.AccountRef{{ID: "account_1", Name: "deepseek-01"}, {ID: "account_2", Name: "deepseek-02"}},
|
|
testResults: map[string]sub2api.ProbeResult{
|
|
"account_1": {OK: true, Status: "passed"},
|
|
"account_2": {OK: true, Status: "passed"},
|
|
},
|
|
models: map[string][]sub2api.AccountModel{
|
|
"account_1": {{ID: "deepseek-chat"}},
|
|
"account_2": {{ID: "deepseek-chat"}},
|
|
},
|
|
gatewayResult: sub2api.GatewayAccessResult{OK: true, StatusCode: 200, HasExpectedModel: true, Models: []string{"deepseek-chat"}},
|
|
}
|
|
|
|
seedRuntimeImportForReconcile(t, store, host)
|
|
host.managedSnapshot = sub2api.ManagedResourceSnapshot{
|
|
Groups: []sub2api.NamedResource{{ID: "group_1", Name: "DeepSeek 默认分组-self-service"}},
|
|
Channels: []sub2api.NamedResource{{ID: "channel_1", Name: "DeepSeek 默认渠道-self-service"}},
|
|
Accounts: []sub2api.NamedResource{{ID: "account_1", Name: "deepseek-01"}, {ID: "account_2", Name: "deepseek-02"}},
|
|
}
|
|
|
|
provider := sampleProviderManifest()
|
|
if _, err := NewReconcileService(store, host).Reconcile(context.Background(), ReconcileRequest{
|
|
HostID: "host-1",
|
|
HostBaseURL: "https://sub2api.example.com",
|
|
AccessProbeAPIKey: "user-key",
|
|
Pack: pack.LoadedPack{Manifest: pack.Manifest{PackID: "openai-cn-pack", Version: "1.0.0", TargetHost: "sub2api", MinHostVersion: "0.1.126", MaxHostVersion: "0.2.x"}},
|
|
Provider: provider,
|
|
}); err != nil {
|
|
t.Fatalf("Reconcile() error = %v", err)
|
|
}
|
|
if host.listManagedReq.AccountNamePrefix != "deepseek-" {
|
|
t.Fatalf("AccountNamePrefix = %q, want %q", host.listManagedReq.AccountNamePrefix, "deepseek-")
|
|
}
|
|
}
|
|
|
|
func TestReconcileServiceRejectsRolledBackLatestBatch(t *testing.T) {
|
|
store := openProvisionTestStore(t)
|
|
defer closeProvisionTestStore(t, store)
|
|
|
|
host := &fakeHostAdapter{
|
|
batchAccounts: []sub2api.AccountRef{{ID: "account_1", Name: "deepseek-01"}},
|
|
testResults: map[string]sub2api.ProbeResult{
|
|
"account_1": {OK: true, Status: "passed"},
|
|
},
|
|
models: map[string][]sub2api.AccountModel{
|
|
"account_1": {{ID: "deepseek-chat"}},
|
|
},
|
|
gatewayResult: sub2api.GatewayAccessResult{OK: true, StatusCode: 200, HasExpectedModel: true, Models: []string{"deepseek-chat"}},
|
|
}
|
|
|
|
batchID := seedRuntimeImportForReconcile(t, store, host)
|
|
if err := store.ImportBatches().UpdateStatus(context.Background(), batchID, BatchStatusRolledBack, AccessStatusBroken); err != nil {
|
|
t.Fatalf("UpdateStatus() error = %v", err)
|
|
}
|
|
_, err := NewReconcileService(store, host).Reconcile(context.Background(), ReconcileRequest{
|
|
HostID: "host-1",
|
|
HostBaseURL: "https://sub2api.example.com",
|
|
AccessProbeAPIKey: "user-key",
|
|
Pack: pack.LoadedPack{Manifest: pack.Manifest{PackID: "openai-cn-pack", Version: "1.0.0", TargetHost: "sub2api", MinHostVersion: "0.1.126", MaxHostVersion: "0.2.x"}},
|
|
Provider: sampleProviderManifest(),
|
|
})
|
|
if err == nil || err.Error() != "latest import batch is rolled_back; run import again before reconcile" {
|
|
t.Fatalf("Reconcile() error = %v, want rolled_back guard", err)
|
|
}
|
|
}
|
|
|
|
func TestDeriveHealthyAccessStatus(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
closureType string
|
|
want string
|
|
}{
|
|
{name: "subscription", closureType: AccessModeSubscription, want: AccessStatusSubscriptionReady},
|
|
{name: "self-service", closureType: AccessModeSelfService, want: AccessStatusSelfServiceReady},
|
|
{name: "unknown", closureType: "other", want: "unknown"},
|
|
}
|
|
for _, tc := range tests {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
if got := deriveHealthyAccessStatus(tc.closureType); got != tc.want {
|
|
t.Fatalf("deriveHealthyAccessStatus(%q) = %q, want %q", tc.closureType, got, tc.want)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func seedRuntimeImportForReconcile(t *testing.T, store *sqlite.DB, host *fakeHostAdapter) int64 {
|
|
t.Helper()
|
|
seedProvisionHost(t, store, "host-1", "https://sub2api.example.com")
|
|
result, err := NewRuntimeImportService(store, host).Import(context.Background(), RuntimeImportRequest{
|
|
HostID: "host-1",
|
|
HostBaseURL: "https://sub2api.example.com",
|
|
Pack: pack.LoadedPack{
|
|
Manifest: pack.Manifest{PackID: "openai-cn-pack", Version: "1.0.0", TargetHost: "sub2api", MinHostVersion: "0.1.126", MaxHostVersion: "0.2.x"},
|
|
Checksum: "checksum-1",
|
|
},
|
|
Provider: sampleProviderManifest(),
|
|
Mode: ImportModePartial,
|
|
Keys: []string{"key-1", "key-2"},
|
|
Access: AccessRequest{
|
|
Mode: AccessModeSelfService,
|
|
ProbeAPIKey: "user-key",
|
|
},
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("seed RuntimeImportService.Import() error = %v", err)
|
|
}
|
|
return result.BatchID
|
|
}
|