Expand app runtime coverage
This commit is contained in:
785
internal/app/coverage_helpers_test.go
Normal file
785
internal/app/coverage_helpers_test.go
Normal 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")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user