Expand app runtime coverage

This commit is contained in:
phamnazage-jpg
2026-05-23 09:44:54 +08:00
parent 7ae8caf216
commit 2ad277743d

View File

@@ -0,0 +1,785 @@
package app
import (
"context"
"database/sql"
"strings"
"testing"
"sub2api-cn-relay-manager/internal/batch"
"sub2api-cn-relay-manager/internal/host/sub2api"
"sub2api-cn-relay-manager/internal/provision"
"sub2api-cn-relay-manager/internal/store/sqlite"
)
func TestBatchImportResumeJobNameAndParseJSONStringList(t *testing.T) {
t.Parallel()
if got := (batchImportResumeJob{}).Name(); got != "batch import runtime scheduler" {
t.Fatalf("batchImportResumeJob.Name() = %q, want batch import runtime scheduler", got)
}
values := parseJSONStringList(`[" user-1 ","user-2"]`)
if len(values) != 2 || values[0] != " user-1 " || values[1] != "user-2" {
t.Fatalf("parseJSONStringList() = %v, want raw decoded values", values)
}
if got := parseJSONStringList("{"); len(got) != 0 {
t.Fatalf("parseJSONStringList(invalid) = %v, want empty", got)
}
}
func TestBatchImportResumeJobRunAndReconcileSweepJobName(t *testing.T) {
t.Parallel()
store := openAppTestStore(t)
defer closeAppTestStore(t, store)
mustCreateAppImportRun(t, store, sqlite.ImportRun{
RunID: "run-1",
HostID: "host-1",
Mode: "partial",
AccessMode: "self_service",
State: "completed",
})
job := batchImportResumeJob{sqliteDSN: appTestDSN(t, store)}
if err := job.Run(context.Background()); err != nil {
t.Fatalf("batchImportResumeJob.Run() error = %v", err)
}
if got := (reconcileSweepJob{}).Name(); got != "reconcile background scheduler" {
t.Fatalf("reconcileSweepJob.Name() = %q, want reconcile background scheduler", got)
}
}
func TestDefaultBackgroundSchedulersAndNewActionSet(t *testing.T) {
t.Parallel()
schedulers := defaultBackgroundSchedulers()
if schedulers.runBatchImport == nil || schedulers.runReconcile == nil {
t.Fatalf("defaultBackgroundSchedulers() = %+v, want non-nil functions", schedulers)
}
actions := NewActionSet("file:/tmp/nonexistent.db")
if actions.CreateBatchImportRun == nil || actions.ListBatchImportRuns == nil || actions.GetBatchImportRun == nil || actions.ListBatchImportRunItems == nil || actions.GetBatchImportRunItem == nil {
t.Fatalf("NewActionSet() returned nil batch actions: %+v", actions)
}
if actions.CreateHost == nil || actions.ListPacks == nil || actions.GetPack == nil || actions.ListPackProviders == nil {
t.Fatalf("NewActionSet() returned nil app actions: %+v", actions)
}
}
func TestCreateHostAuthFromLegacyFields(t *testing.T) {
t.Parallel()
if got := createHostAuthFromLegacyFields(" api-key ", " bearer-token "); got.Type != "bearer" || got.Token != "bearer-token" {
t.Fatalf("createHostAuthFromLegacyFields() = %+v, want bearer token", got)
}
if got := createHostAuthFromLegacyFields(" api-key ", ""); got.Type != "apikey" || got.Token != "api-key" {
t.Fatalf("createHostAuthFromLegacyFields() = %+v, want apikey", got)
}
}
func TestHostRecordToInfoAndPackRecordToInfo(t *testing.T) {
t.Parallel()
hostInfo := hostRecordToInfo(sqlite.Host{
HostID: "host-1",
BaseURL: "https://sub2api.example.com",
HostVersion: "0.1.126",
AuthType: "apikey",
CapabilityProbeJSON: `{"groups":true,"channels":true,"plans":true,"accounts":true,"account_test":true,"account_models":true,"subscriptions":true}`,
})
if hostInfo.Status != "supported" || hostInfo.Capabilities == nil || !hostInfo.Capabilities.Subscriptions {
t.Fatalf("hostRecordToInfo() = %+v, want supported capabilities", hostInfo)
}
hostInfo = hostRecordToInfo(sqlite.Host{
HostID: "host-2",
BaseURL: "https://bad.example.com",
CapabilityProbeJSON: "{",
})
if hostInfo.Capabilities != nil || hostInfo.Status != "" {
t.Fatalf("hostRecordToInfo(invalid json) = %+v, want nil capabilities and empty status", hostInfo)
}
packInfo := packRecordToInfo(sqlite.Pack{
PackID: "openai-cn-pack",
Version: "1.0.0",
Vendor: "OpenAI CN",
TargetHost: "sub2api",
MinHostVersion: "0.1.126",
MaxHostVersion: "0.2.x",
})
if packInfo.PackID != "openai-cn-pack" || packInfo.TargetHost != "sub2api" {
t.Fatalf("packRecordToInfo() = %+v, want projected pack info", packInfo)
}
}
func TestDeriveAccessStatusAndDefaultPositiveInt(t *testing.T) {
t.Parallel()
if got := deriveAccessStatus(sub2api.GatewayAccessResult{OK: true, HasExpectedModel: true, CompletionOK: true}); got != provision.AccessStatusSubscriptionReady {
t.Fatalf("deriveAccessStatus(ready) = %q, want %q", got, provision.AccessStatusSubscriptionReady)
}
if got := deriveAccessStatus(sub2api.GatewayAccessResult{OK: true, HasExpectedModel: true, CompletionOK: false}); got != provision.AccessStatusBroken {
t.Fatalf("deriveAccessStatus(broken) = %q, want %q", got, provision.AccessStatusBroken)
}
if got := defaultPositiveInt(3, 9); got != 3 {
t.Fatalf("defaultPositiveInt(3, 9) = %d, want 3", got)
}
if got := defaultPositiveInt(0, 9); got != 9 {
t.Fatalf("defaultPositiveInt(0, 9) = %d, want 9", got)
}
}
func TestMatchesItemFilters(t *testing.T) {
t.Parallel()
view := batch.ItemSummaryProjection{
ItemID: "item-1",
BaseURL: "https://kimi.example.com/v1",
ProviderID: "kimi-a7m",
CurrentStage: "done",
ConfirmationStatus: "advisory",
AccessStatus: "active",
MatchedAccountState: "active",
AccountResolution: "reused",
AdvisoryMessages: []string{"warning"},
}
hasWarning := true
if !matchesItemFilters(view, ListBatchImportRunItemsRequest{
CurrentStage: "done",
ConfirmationStatus: "advisory",
AccessStatus: "active",
ProviderID: "kimi-a7m",
MatchedAccountState: "active",
AccountResolution: "reused",
Query: "kimi",
HasWarning: &hasWarning,
}) {
t.Fatal("matchesItemFilters() = false, want match")
}
noWarning := false
if matchesItemFilters(view, ListBatchImportRunItemsRequest{HasWarning: &noWarning}) {
t.Fatal("matchesItemFilters(has_warning=false) = true, want false")
}
if matchesItemFilters(view, ListBatchImportRunItemsRequest{Query: "other"}) {
t.Fatal("matchesItemFilters(query=other) = true, want false")
}
}
func TestBuildGetBatchImportRunActionAndGetItemAction(t *testing.T) {
t.Parallel()
store := openAppTestStore(t)
defer closeAppTestStore(t, store)
mustCreateAppImportRun(t, store, sqlite.ImportRun{
RunID: "run-1",
HostID: "host-1",
Mode: "partial",
AccessMode: "self_service",
State: "running",
})
mustExecSQL(t, store, `UPDATE import_runs SET started_at = '2026-05-23 10:00:00' WHERE run_id = 'run-1'`)
mustCreateAppImportRunItem(t, store, sqlite.ImportRunItem{
ItemID: "item-1",
RunID: "run-1",
BaseURL: "https://kimi.example.com/v1",
ProviderID: "kimi-a7m",
APIKeyFingerprint: "sha256:1",
CurrentStage: "done",
ConfirmationStatus: "confirmed",
AccessStatus: "active",
MatchedAccountState: "active",
AccountResolution: "reused",
AdvisoryMessagesJSON: `["gateway_warmup_retry_succeeded"]`,
})
action := buildGetBatchImportRunAction(appTestDSN(t, store))
run, err := action(context.Background(), "run-1")
if err != nil {
t.Fatalf("buildGetBatchImportRunAction() error = %v", err)
}
if run.RunID != "run-1" {
t.Fatalf("run.RunID = %q, want run-1", run.RunID)
}
if _, err := action(context.Background(), "missing"); err == nil || err.Error() != "run not found: missing" {
t.Fatalf("missing run error = %v, want run not found", err)
}
itemAction := buildGetBatchImportRunItemAction(appTestDSN(t, store))
item, err := itemAction(context.Background(), GetBatchImportRunItemRequest{RunID: "run-1", ItemID: "item-1"})
if err != nil {
t.Fatalf("buildGetBatchImportRunItemAction() error = %v", err)
}
if item.ItemID != "item-1" || item.AccountResolution != "reused" {
t.Fatalf("item = %+v, want projected item", item)
}
if _, err := itemAction(context.Background(), GetBatchImportRunItemRequest{RunID: "run-2", ItemID: "item-1"}); err == nil || err.Error() != "item not found in run run-2" {
t.Fatalf("wrong run error = %v, want item not found in run", err)
}
}
func TestResolveProvidersForQueryAndLatestAccessStatus(t *testing.T) {
t.Parallel()
store := openAppTestStore(t)
defer closeAppTestStore(t, store)
host1, err := store.Hosts().Create(context.Background(), sqlite.Host{
HostID: "host-1",
BaseURL: "https://one.example.com",
HostVersion: "0.1.126",
AuthToken: "token-1",
})
if err != nil {
t.Fatalf("Hosts().Create(host1) error = %v", err)
}
host2, err := store.Hosts().Create(context.Background(), sqlite.Host{
HostID: "host-2",
BaseURL: "https://two.example.com",
HostVersion: "0.1.126",
AuthToken: "token-2",
})
if err != nil {
t.Fatalf("Hosts().Create(host2) error = %v", err)
}
packID, err := store.Packs().Create(context.Background(), sqlite.Pack{
PackID: "openai-cn-pack",
Version: "1.0.0",
Checksum: "checksum-1",
})
if err != nil {
t.Fatalf("Packs().Create() error = %v", err)
}
providerID, err := store.Providers().Create(context.Background(), sqlite.Provider{
PackID: packID,
ProviderID: "deepseek",
DisplayName: "DeepSeek",
BaseURL: "https://api.example.com",
Platform: "openai",
})
if err != nil {
t.Fatalf("Providers().Create() error = %v", err)
}
batch1, err := store.ImportBatches().Create(context.Background(), sqlite.ImportBatch{
HostID: host1,
PackID: packID,
ProviderID: providerID,
Mode: provision.ImportModePartial,
BatchStatus: provision.BatchStatusSucceeded,
AccessStatus: provision.AccessStatusSelfServiceReady,
})
if err != nil {
t.Fatalf("ImportBatches().Create(batch1) error = %v", err)
}
batch2, err := store.ImportBatches().Create(context.Background(), sqlite.ImportBatch{
HostID: host2,
PackID: packID,
ProviderID: providerID,
Mode: provision.ImportModeStrict,
BatchStatus: provision.BatchStatusSucceeded,
AccessStatus: provision.AccessStatusSubscriptionReady,
})
if err != nil {
t.Fatalf("ImportBatches().Create(batch2) error = %v", err)
}
if _, err := store.AccessClosures().Create(context.Background(), sqlite.AccessClosureRecord{
BatchID: batch1,
ClosureType: provision.AccessModeSelfService,
Status: provision.AccessStatusSelfServiceReady,
}); err != nil {
t.Fatalf("AccessClosures().Create(batch1) error = %v", err)
}
if _, err := store.AccessClosures().Create(context.Background(), sqlite.AccessClosureRecord{
BatchID: batch2,
ClosureType: provision.AccessModeSubscription,
Status: provision.AccessStatusSubscriptionReady,
}); err != nil {
t.Fatalf("AccessClosures().Create(batch2) error = %v", err)
}
if _, err := resolveProvidersForQuery(context.Background(), nil, ProviderQueryRequest{}); err == nil || err.Error() != "store is required" {
t.Fatalf("resolveProvidersForQuery(nil store) error = %v, want store is required", err)
}
if _, err := resolveProvidersForQuery(context.Background(), store, ProviderQueryRequest{}); err == nil || err.Error() != "provider_id is required" {
t.Fatalf("resolveProvidersForQuery(missing provider) error = %v, want provider_id is required", err)
}
providers, err := resolveProvidersForQuery(context.Background(), store, ProviderQueryRequest{ProviderID: "deepseek", PackID: "openai-cn-pack"})
if err != nil {
t.Fatalf("resolveProvidersForQuery() error = %v", err)
}
if len(providers) != 1 || providers[0].ProviderID != "deepseek" {
t.Fatalf("resolveProvidersForQuery() = %+v, want deepseek provider", providers)
}
if _, err := resolveLatestAccessStatus(context.Background(), nil, sqlite.Provider{}, ""); err == nil || err.Error() != "store is required" {
t.Fatalf("resolveLatestAccessStatus(nil store) error = %v, want store is required", err)
}
status, err := resolveLatestAccessStatus(context.Background(), store, providers[0], "host-1")
if err != nil {
t.Fatalf("resolveLatestAccessStatus(host-1) error = %v", err)
}
if status != provision.AccessStatusSelfServiceReady {
t.Fatalf("resolveLatestAccessStatus(host-1) = %q, want %q", status, provision.AccessStatusSelfServiceReady)
}
if _, err := resolveLatestAccessStatus(context.Background(), store, providers[0], ""); err == nil || err.Error() != "provider exists on multiple hosts; host_id is required" {
t.Fatalf("resolveLatestAccessStatus(multi-host) error = %v, want multi-host error", err)
}
}
func TestBuildListBatchImportRunsActionAndItemsAction(t *testing.T) {
t.Parallel()
store := openAppTestStore(t)
defer closeAppTestStore(t, store)
mustCreateAppImportRun(t, store, sqlite.ImportRun{
RunID: "run-1",
HostID: "host-1",
Mode: "partial",
AccessMode: "self_service",
State: "running",
TotalItems: 1,
WarningItems: 1,
ActiveItems: 1,
BrokenItems: 0,
DegradedItems: 0,
})
mustCreateAppImportRun(t, store, sqlite.ImportRun{
RunID: "run-2",
HostID: "host-1",
Mode: "strict",
AccessMode: "subscription",
State: "completed",
})
mustCreateAppImportRun(t, store, sqlite.ImportRun{
RunID: "run-3",
HostID: "host-1",
Mode: "strict",
AccessMode: "subscription",
State: "completed",
})
mustExecSQL(t, store, `UPDATE import_runs SET started_at = '2026-05-23 10:00:03' WHERE run_id = 'run-1'`)
mustExecSQL(t, store, `UPDATE import_runs SET started_at = '2026-05-23 10:00:02' WHERE run_id = 'run-2'`)
mustExecSQL(t, store, `UPDATE import_runs SET started_at = '2026-05-23 10:00:01' WHERE run_id = 'run-3'`)
mustCreateAppImportRunItem(t, store, sqlite.ImportRunItem{
ItemID: "item-1",
RunID: "run-1",
BaseURL: "https://kimi.example.com/v1",
ProviderID: "kimi-a7m",
APIKeyFingerprint: "sha256:1",
CurrentStage: "done",
ConfirmationStatus: "advisory",
AccessStatus: "active",
MatchedAccountState: "active",
AccountResolution: "reused",
AdvisoryMessagesJSON: `["warning"]`,
})
mustCreateAppImportRunItem(t, store, sqlite.ImportRunItem{
ItemID: "item-2",
RunID: "run-2",
BaseURL: "https://other.example.com/v1",
ProviderID: "deepseek",
APIKeyFingerprint: "sha256:2",
CurrentStage: "confirm",
ConfirmationStatus: "pending",
AccessStatus: "broken",
MatchedAccountState: "broken",
AccountResolution: "created",
AdvisoryMessagesJSON: `[]`,
})
mustCreateAppImportRunItem(t, store, sqlite.ImportRunItem{
ItemID: "item-3",
RunID: "run-3",
BaseURL: "https://other.example.com/v1",
ProviderID: "deepseek-backup",
APIKeyFingerprint: "sha256:3",
CurrentStage: "confirm",
ConfirmationStatus: "pending",
AccessStatus: "active",
MatchedAccountState: "active",
AccountResolution: "reused",
AdvisoryMessagesJSON: `[]`,
})
listRuns := buildListBatchImportRunsAction(appTestDSN(t, store))
runs, err := listRuns(context.Background(), ListBatchImportRunsRequest{
State: "completed",
Query: "other.example.com",
Limit: 1,
})
if err != nil {
t.Fatalf("buildListBatchImportRunsAction() error = %v", err)
}
if len(runs.Runs) != 1 || runs.Runs[0].RunID != "run-2" {
t.Fatalf("runs = %+v, want [run-2]", runs.Runs)
}
if runs.NextCursor == nil || *runs.NextCursor != "run-3" {
t.Fatalf("runs.NextCursor = %v, want run-3", runs.NextCursor)
}
listItems := buildListBatchImportRunItemsAction(appTestDSN(t, store))
hasWarning := true
items, err := listItems(context.Background(), ListBatchImportRunItemsRequest{
RunID: "run-1",
HasWarning: &hasWarning,
Query: "kimi",
Limit: 10,
})
if err != nil {
t.Fatalf("buildListBatchImportRunItemsAction() error = %v", err)
}
if len(items.Items) != 1 || items.Items[0].ItemID != "item-1" {
t.Fatalf("items = %+v, want [item-1]", items.Items)
}
if items.NextCursor != nil {
t.Fatalf("items.NextCursor = %v, want nil", items.NextCursor)
}
if _, err := listItems(context.Background(), ListBatchImportRunItemsRequest{RunID: "missing"}); err == nil || err.Error() != "run not found: missing" {
t.Fatalf("missing items run error = %v, want run not found", err)
}
}
func appTestDSN(t *testing.T, store *sqlite.DB) string {
t.Helper()
row := store.SQLDB().QueryRow(`PRAGMA database_list`)
var seq int
var name string
var file string
if err := row.Scan(&seq, &name, &file); err != nil {
t.Fatalf("PRAGMA database_list scan error = %v", err)
}
return "file:" + file + "?_busy_timeout=5000&_pragma=foreign_keys(0)"
}
func mustCreateAppImportRun(t *testing.T, store *sqlite.DB, run sqlite.ImportRun) {
t.Helper()
if err := store.ImportRuns().Create(context.Background(), run); err != nil {
t.Fatalf("ImportRuns().Create() error = %v", err)
}
}
func mustCreateAppImportRunItem(t *testing.T, store *sqlite.DB, item sqlite.ImportRunItem) {
t.Helper()
if err := store.ImportRunItems().Create(context.Background(), item); err != nil {
t.Fatalf("ImportRunItems().Create() error = %v", err)
}
}
func mustExecSQL(t *testing.T, store *sqlite.DB, query string, args ...any) {
t.Helper()
if _, err := store.SQLDB().Exec(query, args...); err != nil {
t.Fatalf("Exec(%q) error = %v", query, err)
}
}
func TestResolveManagedHostAndNewSub2APIClient(t *testing.T) {
t.Parallel()
store := openAppTestStore(t)
defer closeAppTestStore(t, store)
if _, err := store.Hosts().Create(context.Background(), sqlite.Host{
HostID: "host-1",
BaseURL: "https://sub2api.example.com",
HostVersion: "0.1.126",
AuthType: "",
AuthToken: "host-token",
}); err != nil {
t.Fatalf("Hosts().Create() error = %v", err)
}
hostRow, client, err := resolveManagedHost(context.Background(), store, "host-1", "", CreateHostAuth{})
if err != nil {
t.Fatalf("resolveManagedHost() error = %v", err)
}
if hostRow.HostID != "host-1" || client == nil {
t.Fatalf("resolveManagedHost() = (%+v, %v), want host-1 and client", hostRow, client)
}
if _, _, err := resolveManagedHost(context.Background(), store, "host-1", "https://other.example.com", CreateHostAuth{}); err == nil || !strings.Contains(err.Error(), `host "host-1" base_url mismatch`) {
t.Fatalf("resolveManagedHost(mismatch) error = %v, want mismatch", err)
}
if _, _, err := resolveManagedHost(context.Background(), store, "", "", CreateHostAuth{}); err == nil || err.Error() != "host_id is required" {
t.Fatalf("resolveManagedHost(empty) error = %v, want host_id is required", err)
}
if auth := authFromStoredHost(sqlite.Host{AuthType: "", AuthToken: " token "}); auth.Type != "apikey" || auth.Token != "token" {
t.Fatalf("authFromStoredHost(default) = %+v, want apikey/token", auth)
}
if _, err := newSub2APIClient("https://sub2api.example.com", CreateHostAuth{Type: "other", Token: "t"}); err == nil || !strings.Contains(err.Error(), `unsupported auth type "other"`) {
t.Fatalf("newSub2APIClient(unsupported) error = %v, want unsupported auth type", err)
}
if _, err := newSub2APIClient("https://sub2api.example.com", CreateHostAuth{Type: "apikey"}); err == nil || !strings.Contains(err.Error(), "auth.token is required") {
t.Fatalf("newSub2APIClient(missing token) error = %v, want auth.token is required", err)
}
}
func TestHandlerWrappersForPackAndHostRoutes(t *testing.T) {
t.Parallel()
t.Run("handleListPacks returns empty array", func(t *testing.T) {
t.Parallel()
req := httptestRequest(t, "GET", "/packs", map[string]any{}, "")
rec := &responseRecorder{header: map[string][]string{}}
handleListPacks(rec, req, func(context.Context) ([]PackInfo, error) { return nil, nil })
assertStatusCode(t, rec, 200)
packs, ok := decodeTopLevelArray(t, rec.Body().Bytes(), "packs")
if !ok || len(packs) != 0 {
t.Fatalf("packs = %#v, want empty array", packs)
}
})
t.Run("handleGetPack requires pack id", func(t *testing.T) {
t.Parallel()
req := httptestRequest(t, "GET", "/packs/", map[string]any{}, "")
rec := &responseRecorder{header: map[string][]string{}}
handleGetPack(rec, req, func(context.Context, string) (PackInfo, error) { return PackInfo{}, nil })
assertStatusCode(t, rec, 400)
assertJSONContains(t, rec.Body().Bytes(), "error.message", "pack_id is required")
})
t.Run("handleGetPack returns payload", func(t *testing.T) {
t.Parallel()
req := httptestRequest(t, "GET", "/packs/openai-cn-pack", map[string]any{}, "")
req.SetPathValue("packID", "openai-cn-pack")
rec := &responseRecorder{header: map[string][]string{}}
handleGetPack(rec, req, func(_ context.Context, packID string) (PackInfo, error) {
if packID != "openai-cn-pack" {
t.Fatalf("packID = %q, want openai-cn-pack", packID)
}
return PackInfo{PackID: "openai-cn-pack", Version: "1.0.0"}, nil
})
assertStatusCode(t, rec, 200)
assertJSONContains(t, rec.Body().Bytes(), "pack_id", "openai-cn-pack")
})
t.Run("handleListPackProviders returns array", func(t *testing.T) {
t.Parallel()
req := httptestRequest(t, "GET", "/packs/openai-cn-pack/providers", map[string]any{}, "")
req.SetPathValue("packID", "openai-cn-pack")
rec := &responseRecorder{header: map[string][]string{}}
handleListPackProviders(rec, req, func(_ context.Context, packID string) ([]PackProviderInfo, error) {
if packID != "openai-cn-pack" {
t.Fatalf("packID = %q, want openai-cn-pack", packID)
}
return nil, nil
})
assertStatusCode(t, rec, 200)
providers, ok := decodeTopLevelArray(t, rec.Body().Bytes(), "providers")
if !ok || len(providers) != 0 {
t.Fatalf("providers = %#v, want empty array", providers)
}
})
t.Run("handleCreateHost decodes request", func(t *testing.T) {
t.Parallel()
req := httptestRequest(t, "POST", "/hosts", map[string]any{
"name": "host-1",
"base_url": "https://sub2api.example.com",
"auth": map[string]any{"type": "apikey", "token": "host-token"},
}, "")
rec := &responseRecorder{header: map[string][]string{}}
handleCreateHost(rec, req, func(_ context.Context, req CreateHostRequest) (HostInfo, error) {
if req.BaseURL != "https://sub2api.example.com" || req.Auth.Token != "host-token" {
t.Fatalf("request = %+v, want decoded create host request", req)
}
return HostInfo{HostID: "host-1", BaseURL: req.BaseURL}, nil
})
assertStatusCode(t, rec, 200)
assertJSONContains(t, rec.Body().Bytes(), "host_id", "host-1")
})
t.Run("handleProbeHost requires host id", func(t *testing.T) {
t.Parallel()
req := httptestRequest(t, "POST", "/hosts/probe", map[string]any{
"auth": map[string]any{"type": "apikey", "token": "host-token"},
}, "")
rec := &responseRecorder{header: map[string][]string{}}
handleProbeHost(rec, req, func(context.Context, ProbeHostRequest) (HostInfo, error) { return HostInfo{}, nil })
assertStatusCode(t, rec, 400)
assertJSONContains(t, rec.Body().Bytes(), "error.message", "host_id is required")
})
}
func TestBuildListBatchImportRunItemsActionCursor(t *testing.T) {
t.Parallel()
store := openAppTestStore(t)
defer closeAppTestStore(t, store)
mustCreateAppImportRun(t, store, sqlite.ImportRun{
RunID: "run-1",
HostID: "host-1",
Mode: "partial",
AccessMode: "self_service",
State: "running",
})
mustExecSQL(t, store, `UPDATE import_runs SET started_at = '2026-05-23 10:00:00' WHERE run_id = 'run-1'`)
mustCreateAppImportRunItem(t, store, sqlite.ImportRunItem{
ItemID: "item-1",
RunID: "run-1",
BaseURL: "https://a.example.com/v1",
ProviderID: "a",
APIKeyFingerprint: "sha256:a",
CurrentStage: "done",
ConfirmationStatus: "confirmed",
AccessStatus: "active",
MatchedAccountState: "active",
AccountResolution: "created",
})
mustCreateAppImportRunItem(t, store, sqlite.ImportRunItem{
ItemID: "item-2",
RunID: "run-1",
BaseURL: "https://b.example.com/v1",
ProviderID: "b",
APIKeyFingerprint: "sha256:b",
CurrentStage: "done",
ConfirmationStatus: "confirmed",
AccessStatus: "active",
MatchedAccountState: "active",
AccountResolution: "created",
})
action := buildListBatchImportRunItemsAction(appTestDSN(t, store))
result, err := action(context.Background(), ListBatchImportRunItemsRequest{
RunID: "run-1",
Cursor: "item-1",
Limit: 1,
})
if err != nil {
t.Fatalf("buildListBatchImportRunItemsAction(cursor) error = %v", err)
}
if len(result.Items) != 1 || result.Items[0].ItemID != "item-2" {
t.Fatalf("result.Items = %+v, want [item-2]", result.Items)
}
}
func TestBuildListBatchImportRunsActionCursorAndDefaults(t *testing.T) {
t.Parallel()
store := openAppTestStore(t)
defer closeAppTestStore(t, store)
mustCreateAppImportRun(t, store, sqlite.ImportRun{RunID: "run-1", HostID: "host-1", Mode: "partial", AccessMode: "self_service", State: "running"})
mustCreateAppImportRun(t, store, sqlite.ImportRun{RunID: "run-2", HostID: "host-1", Mode: "partial", AccessMode: "self_service", State: "running"})
mustExecSQL(t, store, `UPDATE import_runs SET started_at = '2026-05-23 10:00:02' WHERE run_id = 'run-1'`)
mustExecSQL(t, store, `UPDATE import_runs SET started_at = '2026-05-23 10:00:01' WHERE run_id = 'run-2'`)
action := buildListBatchImportRunsAction(appTestDSN(t, store))
result, err := action(context.Background(), ListBatchImportRunsRequest{Cursor: "run-1", Limit: -1})
if err != nil {
t.Fatalf("buildListBatchImportRunsAction(cursor) error = %v", err)
}
if len(result.Runs) != 1 || result.Runs[0].RunID != "run-2" {
t.Fatalf("result.Runs = %+v, want [run-2]", result.Runs)
}
}
func TestBuildGetBatchImportRunActionPropagatesDBError(t *testing.T) {
t.Parallel()
action := buildGetBatchImportRunAction("file:/definitely-missing-path/does-not-exist.db?mode=ro")
if _, err := action(context.Background(), "run-1"); err == nil {
t.Fatal("buildGetBatchImportRunAction() error = nil, want open db error")
}
}
func TestBuildListBatchImportRunItemsActionPropagatesDBError(t *testing.T) {
t.Parallel()
action := buildListBatchImportRunItemsAction("file:/definitely-missing-path/does-not-exist.db?mode=ro")
if _, err := action(context.Background(), ListBatchImportRunItemsRequest{RunID: "run-1"}); err == nil {
t.Fatal("buildListBatchImportRunItemsAction() error = nil, want open db error")
}
}
func TestBuildListBatchImportRunsActionPropagatesDBError(t *testing.T) {
t.Parallel()
action := buildListBatchImportRunsAction("file:/definitely-missing-path/does-not-exist.db?mode=ro")
if _, err := action(context.Background(), ListBatchImportRunsRequest{}); err == nil {
t.Fatal("buildListBatchImportRunsAction() error = nil, want open db error")
}
}
func TestBuildGetBatchImportRunItemActionPropagatesMissingItem(t *testing.T) {
t.Parallel()
store := openAppTestStore(t)
defer closeAppTestStore(t, store)
action := buildGetBatchImportRunItemAction(appTestDSN(t, store))
if _, err := action(context.Background(), GetBatchImportRunItemRequest{RunID: "run-1", ItemID: "missing"}); err == nil || err.Error() != "item not found: missing" {
t.Fatalf("missing item error = %v, want item not found", err)
}
}
func TestResumePendingBatchImportRunsNoRunningRuns(t *testing.T) {
t.Parallel()
store := openAppTestStore(t)
defer closeAppTestStore(t, store)
mustCreateAppImportRun(t, store, sqlite.ImportRun{
RunID: "run-1",
HostID: "host-1",
Mode: "partial",
AccessMode: "self_service",
State: "completed",
})
if err := resumePendingBatchImportRuns(context.Background(), appTestDSN(t, store)); err != nil {
t.Fatalf("resumePendingBatchImportRuns() error = %v", err)
}
}
func TestNewBatchImportRuntimeRunnerFromStoredRun(t *testing.T) {
t.Parallel()
store := openAppTestStore(t)
defer closeAppTestStore(t, store)
if _, err := store.Hosts().Create(context.Background(), sqlite.Host{
HostID: "host-1",
BaseURL: "https://sub2api.example.com",
HostVersion: "0.1.126",
AuthToken: "host-token",
}); err != nil {
t.Fatalf("Hosts().Create() error = %v", err)
}
runner, err := newBatchImportRuntimeRunnerFromStoredRun(context.Background(), store, sqlite.ImportRun{
RunID: "run-1",
HostID: "host-1",
Mode: "partial",
AccessMode: "subscription",
SubscriptionUsersJSON: `["user-1","user-2"]`,
SubscriptionDays: 30,
ProbeAPIKey: "probe-key",
})
if err != nil {
t.Fatalf("newBatchImportRuntimeRunnerFromStoredRun() error = %v", err)
}
if runner.request.HostID != "host-1" || len(runner.request.SubscriptionUsers) != 2 || runner.request.ProbeAPIKey != "probe-key" {
t.Fatalf("runner.request = %+v, want parsed stored run request", runner.request)
}
}
func TestBuildGetBatchImportRunActionClassifiesNotFoundOnlyOnSQLNoRows(t *testing.T) {
t.Parallel()
store := openAppTestStore(t)
defer closeAppTestStore(t, store)
action := buildGetBatchImportRunAction(appTestDSN(t, store))
_, err := action(context.Background(), "missing")
if err == nil || err.Error() != "run not found: missing" {
t.Fatalf("missing run error = %v, want run not found", err)
}
}
func TestSQLNoRowsReference(t *testing.T) {
t.Parallel()
if sql.ErrNoRows == nil {
t.Fatal("sql.ErrNoRows = nil")
}
}