- 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.
197 lines
7.0 KiB
Go
197 lines
7.0 KiB
Go
package provision
|
|
|
|
import (
|
|
"context"
|
|
"database/sql"
|
|
"fmt"
|
|
"path/filepath"
|
|
"testing"
|
|
|
|
_ "modernc.org/sqlite"
|
|
"sub2api-cn-relay-manager/internal/host/sub2api"
|
|
"sub2api-cn-relay-manager/internal/pack"
|
|
"sub2api-cn-relay-manager/internal/store/sqlite"
|
|
)
|
|
|
|
func TestRuntimeImportServicePersistsOperationalState(t *testing.T) {
|
|
store := openProvisionTestStore(t)
|
|
defer closeProvisionTestStore(t, store)
|
|
|
|
host := &fakeHostAdapter{
|
|
batchAccounts: []sub2api.AccountRef{{ID: "account_1"}, {ID: "account_2"}},
|
|
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"}},
|
|
}
|
|
|
|
svc := NewRuntimeImportService(store, host)
|
|
result, err := svc.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", "key-1"},
|
|
Access: AccessRequest{
|
|
Mode: AccessModeSelfService,
|
|
ProbeAPIKey: "user-key",
|
|
},
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("RuntimeImportService.Import() error = %v", err)
|
|
}
|
|
if result.BatchID <= 0 {
|
|
t.Fatalf("BatchID = %d, want positive id", result.BatchID)
|
|
}
|
|
if result.Report.BatchStatus != BatchStatusSucceeded {
|
|
t.Fatalf("BatchStatus = %q, want %q", result.Report.BatchStatus, BatchStatusSucceeded)
|
|
}
|
|
|
|
if got := queryCount(t, store.SQLDB(), "hosts"); got != 1 {
|
|
t.Fatalf("hosts row count = %d, want 1", got)
|
|
}
|
|
if got := queryCount(t, store.SQLDB(), "packs"); got != 1 {
|
|
t.Fatalf("packs row count = %d, want 1", got)
|
|
}
|
|
if got := queryCount(t, store.SQLDB(), "providers"); got != 1 {
|
|
t.Fatalf("providers row count = %d, want 1", got)
|
|
}
|
|
if got := queryCount(t, store.SQLDB(), "import_batches"); got != 1 {
|
|
t.Fatalf("import_batches row count = %d, want 1", got)
|
|
}
|
|
if got := queryCount(t, store.SQLDB(), "import_batch_items"); got != 2 {
|
|
t.Fatalf("import_batch_items row count = %d, want 2", got)
|
|
}
|
|
if got := queryCount(t, store.SQLDB(), "managed_resources"); got != 4 {
|
|
t.Fatalf("managed_resources row count = %d, want 4", got)
|
|
}
|
|
if got := queryCount(t, store.SQLDB(), "probe_results"); got != 2 {
|
|
t.Fatalf("probe_results row count = %d, want 2", got)
|
|
}
|
|
if got := queryCount(t, store.SQLDB(), "access_closure_records"); got != 1 {
|
|
t.Fatalf("access_closure_records row count = %d, want 1", got)
|
|
}
|
|
|
|
var batchStatus string
|
|
var accessStatus string
|
|
if err := store.SQLDB().QueryRowContext(context.Background(), "SELECT batch_status, access_status FROM import_batches WHERE id = ?", result.BatchID).Scan(&batchStatus, &accessStatus); err != nil {
|
|
t.Fatalf("query import batch state: %v", err)
|
|
}
|
|
if batchStatus != BatchStatusSucceeded {
|
|
t.Fatalf("persisted batch_status = %q, want %q", batchStatus, BatchStatusSucceeded)
|
|
}
|
|
if accessStatus != AccessStatusSelfServiceReady {
|
|
t.Fatalf("persisted access_status = %q, want %q", accessStatus, AccessStatusSelfServiceReady)
|
|
}
|
|
|
|
var fingerprint string
|
|
var accountStatus string
|
|
if err := store.SQLDB().QueryRowContext(context.Background(), "SELECT key_fingerprint, account_status FROM import_batch_items ORDER BY id LIMIT 1").Scan(&fingerprint, &accountStatus); err != nil {
|
|
t.Fatalf("query import batch item: %v", err)
|
|
}
|
|
if fingerprint == "key-1" || fingerprint == "key-2" || len(fingerprint) < 10 {
|
|
t.Fatalf("key_fingerprint = %q, want hashed fingerprint instead of raw key", fingerprint)
|
|
}
|
|
if accountStatus != "passed" {
|
|
t.Fatalf("account_status = %q, want passed", accountStatus)
|
|
}
|
|
}
|
|
|
|
func TestRuntimeImportServicePersistsFailedBatchAfterStrictRollback(t *testing.T) {
|
|
store := openProvisionTestStore(t)
|
|
defer closeProvisionTestStore(t, store)
|
|
|
|
host := &fakeHostAdapter{
|
|
batchAccounts: []sub2api.AccountRef{{ID: "account_1"}, {ID: "account_2"}},
|
|
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"}},
|
|
},
|
|
}
|
|
|
|
svc := NewRuntimeImportService(store, host)
|
|
result, err := svc.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: ImportModeStrict,
|
|
Keys: []string{"key-1", "key-2"},
|
|
Access: AccessRequest{
|
|
Mode: AccessModeSelfService,
|
|
ProbeAPIKey: "user-key",
|
|
},
|
|
})
|
|
if err == nil {
|
|
t.Fatal("RuntimeImportService.Import() error = nil, want strict failure")
|
|
}
|
|
if result.BatchID <= 0 {
|
|
t.Fatalf("BatchID = %d, want positive id", result.BatchID)
|
|
}
|
|
|
|
var batchStatus string
|
|
var accessStatus string
|
|
if err := store.SQLDB().QueryRowContext(context.Background(), "SELECT batch_status, access_status FROM import_batches WHERE id = ?", result.BatchID).Scan(&batchStatus, &accessStatus); err != nil {
|
|
t.Fatalf("query failed import batch state: %v", err)
|
|
}
|
|
if batchStatus != BatchStatusFailed {
|
|
t.Fatalf("persisted batch_status = %q, want %q", batchStatus, BatchStatusFailed)
|
|
}
|
|
if accessStatus != AccessStatusBroken {
|
|
t.Fatalf("persisted access_status = %q, want %q", accessStatus, AccessStatusBroken)
|
|
}
|
|
if got := queryCount(t, store.SQLDB(), "managed_resources"); got != 0 {
|
|
t.Fatalf("managed_resources row count = %d, want 0 after strict rollback", got)
|
|
}
|
|
if got := queryCount(t, store.SQLDB(), "probe_results"); got != 2 {
|
|
t.Fatalf("probe_results row count = %d, want 2", got)
|
|
}
|
|
if got := queryCount(t, store.SQLDB(), "access_closure_records"); got != 1 {
|
|
t.Fatalf("access_closure_records row count = %d, want 1", got)
|
|
}
|
|
}
|
|
|
|
func openProvisionTestStore(t *testing.T) *sqlite.DB {
|
|
t.Helper()
|
|
|
|
dbPath := filepath.Join(t.TempDir(), "state.db")
|
|
dsn := fmt.Sprintf("file:%s?_busy_timeout=5000&_pragma=foreign_keys(0)", filepath.ToSlash(dbPath))
|
|
store, err := sqlite.Open(context.Background(), dsn)
|
|
if err != nil {
|
|
t.Fatalf("sqlite.Open() error = %v", err)
|
|
}
|
|
return store
|
|
}
|
|
|
|
func closeProvisionTestStore(t *testing.T, store *sqlite.DB) {
|
|
t.Helper()
|
|
if err := store.Close(); err != nil {
|
|
t.Fatalf("store.Close() error = %v", err)
|
|
}
|
|
}
|
|
|
|
func queryCount(t *testing.T, db *sql.DB, table string) int {
|
|
t.Helper()
|
|
var count int
|
|
if err := db.QueryRowContext(context.Background(), "SELECT COUNT(*) FROM "+table).Scan(&count); err != nil {
|
|
t.Fatalf("count rows for %s: %v", table, err)
|
|
}
|
|
return count
|
|
}
|