Files
sub2api-cn-relay-manager/internal/provision/runtime_import_service_test.go
phamnazage-jpg 71cbaf5fa6 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.
2026-05-15 19:26:25 +08:00

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
}