package sqlite import ( "context" "database/sql" "errors" "strings" "testing" ) type resultStub struct { lastInsertID int64 lastInsertErr error } func (r resultStub) LastInsertId() (int64, error) { if r.lastInsertErr != nil { return 0, r.lastInsertErr } return r.lastInsertID, nil } func (r resultStub) RowsAffected() (int64, error) { return 1, nil } type execQuerierStub struct { execFn func(context.Context, string, ...any) (sql.Result, error) queryFn func(context.Context, string, ...any) (*sql.Rows, error) queryRowFn func(context.Context, string, ...any) *sql.Row } func (s execQuerierStub) ExecContext(ctx context.Context, query string, args ...any) (sql.Result, error) { return s.execFn(ctx, query, args...) } func (s execQuerierStub) QueryContext(ctx context.Context, query string, args ...any) (*sql.Rows, error) { return s.queryFn(ctx, query, args...) } func (s execQuerierStub) QueryRowContext(ctx context.Context, query string, args ...any) *sql.Row { return s.queryRowFn(ctx, query, args...) } func TestProvidersRepoCreateAndGet(t *testing.T) { store := openTestDB(t) packID := createTestPack(t, store) providerID, err := store.Providers().Create(context.Background(), Provider{ PackID: packID, ProviderID: "deepseek", DisplayName: "DeepSeek", BaseURL: "https://api.deepseek.com", Platform: "openai", AccountType: "apikey", SmokeTestModel: "deepseek-chat", ManifestJSON: `{"models":["deepseek-chat"]}`, }) if err != nil { t.Fatalf("Create() error = %v", err) } if providerID <= 0 { t.Fatalf("Create() id = %d, want positive", providerID) } got, err := store.Providers().GetByPackIDAndProviderID(context.Background(), packID, "deepseek") if err != nil { t.Fatalf("GetByPackIDAndProviderID() error = %v", err) } if got.ProviderID != "deepseek" || got.DisplayName != "DeepSeek" { t.Fatalf("GetByPackIDAndProviderID() = %+v, want deepseek", got) } } func TestProvidersRepoListByProviderID(t *testing.T) { store := openTestDB(t) packID1 := createTestPackWithSuffix(t, store, "a") packID2 := createTestPackWithSuffix(t, store, "b") store.Providers().Create(context.Background(), Provider{PackID: packID1, ProviderID: "deepseek", DisplayName: "DS1", BaseURL: "https://a.com", Platform: "openai"}) store.Providers().Create(context.Background(), Provider{PackID: packID2, ProviderID: "deepseek", DisplayName: "DS2", BaseURL: "https://b.com", Platform: "openai"}) providers, err := store.Providers().ListByProviderID(context.Background(), "deepseek") if err != nil { t.Fatalf("ListByProviderID() error = %v", err) } if len(providers) != 2 { t.Fatalf("ListByProviderID() count = %d, want 2", len(providers)) } } func TestProvidersRepoListByBaseURL(t *testing.T) { store := openTestDB(t) packID1 := createTestPackWithSuffix(t, store, "base-a") packID2 := createTestPackWithSuffix(t, store, "base-b") store.Providers().Create(context.Background(), Provider{PackID: packID1, ProviderID: "minimax-53hk", DisplayName: "MM1", BaseURL: "https://api.53hk.cn/v1", Platform: "openai"}) store.Providers().Create(context.Background(), Provider{PackID: packID2, ProviderID: "api-53hk-42797c06", DisplayName: "MM2", BaseURL: "https://api.53hk.cn/v1", Platform: "openai"}) providers, err := store.Providers().ListByBaseURL(context.Background(), "https://api.53hk.cn/v1") if err != nil { t.Fatalf("ListByBaseURL() error = %v", err) } if len(providers) != 2 { t.Fatalf("ListByBaseURL() count = %d, want 2", len(providers)) } } func TestProvidersRepoListByPackID(t *testing.T) { store := openTestDB(t) packID := createTestPack(t, store) otherPackID := createTestPackWithSuffix(t, store, "other") if _, err := store.Providers().Create(context.Background(), Provider{PackID: packID, ProviderID: "provider-a", DisplayName: "A", BaseURL: "https://a.example.com", Platform: "openai"}); err != nil { t.Fatalf("Create(provider-a) error = %v", err) } if _, err := store.Providers().Create(context.Background(), Provider{PackID: packID, ProviderID: "provider-b", DisplayName: "B", BaseURL: "https://b.example.com", Platform: "openai"}); err != nil { t.Fatalf("Create(provider-b) error = %v", err) } if _, err := store.Providers().Create(context.Background(), Provider{PackID: otherPackID, ProviderID: "provider-c", DisplayName: "C", BaseURL: "https://c.example.com", Platform: "openai"}); err != nil { t.Fatalf("Create(provider-c) error = %v", err) } providers, err := store.Providers().ListByPackID(context.Background(), packID) if err != nil { t.Fatalf("ListByPackID() error = %v", err) } if len(providers) != 2 { t.Fatalf("ListByPackID() count = %d, want 2", len(providers)) } if providers[0].ProviderID != "provider-a" || providers[1].ProviderID != "provider-b" { t.Fatalf("ListByPackID() provider ids = [%q %q], want [provider-a provider-b]", providers[0].ProviderID, providers[1].ProviderID) } } func createTestPackWithSuffix(t *testing.T, store *DB, suffix string) int64 { t.Helper() id, err := store.Packs().Create(context.Background(), Pack{ PackID: "pack-" + sanitizeTestName(t.Name()) + "-" + suffix, Version: "1.0.0", Checksum: "chk", }) if err != nil { t.Fatalf("createTestPackWithSuffix error = %v", err) } return id } func TestProvidersRepoListByProviderIDEmpty(t *testing.T) { store := openTestDB(t) providers, err := store.Providers().ListByProviderID(context.Background(), "nonexistent") if err != nil { t.Fatalf("ListByProviderID() error = %v", err) } if len(providers) != 0 { t.Fatalf("ListByProviderID() count = %d, want 0", len(providers)) } } func TestProvidersRepoListByBaseURLEmpty(t *testing.T) { store := openTestDB(t) providers, err := store.Providers().ListByBaseURL(context.Background(), "https://missing.example.com/v1") if err != nil { t.Fatalf("ListByBaseURL() error = %v", err) } if len(providers) != 0 { t.Fatalf("ListByBaseURL() count = %d, want 0", len(providers)) } } func TestProvidersRepoUpsertCreatesNew(t *testing.T) { store := openTestDB(t) packID := createTestPack(t, store) id, err := store.Providers().Upsert(context.Background(), Provider{ PackID: packID, ProviderID: "upsert-p", DisplayName: "P", BaseURL: "https://u.com", Platform: "openai", }) if err != nil { t.Fatalf("Upsert() error = %v", err) } if id <= 0 { t.Fatalf("Upsert() id = %d, want positive", id) } } func TestProvidersRepoUpsertUpdatesExisting(t *testing.T) { store := openTestDB(t) packID := createTestPack(t, store) id1, _ := store.Providers().Upsert(context.Background(), Provider{ PackID: packID, ProviderID: "upsert-p", DisplayName: "P1", BaseURL: "https://u1.com", Platform: "openai", }) id2, _ := store.Providers().Upsert(context.Background(), Provider{ PackID: packID, ProviderID: "upsert-p", DisplayName: "P2", BaseURL: "https://u2.com", Platform: "openai", }) if id2 != id1 { t.Fatalf("Upsert update id = %d, want %d", id2, id1) } got, _ := store.Providers().GetByPackIDAndProviderID(context.Background(), packID, "upsert-p") if got.DisplayName != "P2" { t.Fatalf("DisplayName after upsert = %q, want P2", got.DisplayName) } } func TestProvidersRepoUpsertTrimsAndDefaultsOptionalJSON(t *testing.T) { store := openTestDB(t) packID := createTestPack(t, store) id, err := store.Providers().Upsert(context.Background(), Provider{ PackID: packID, ProviderID: " upsert-json ", DisplayName: " Upsert JSON ", BaseURL: " https://json.example.com/v1 ", Platform: " openai ", AccountType: " apikey ", SmokeTestModel: " gpt-5.4 ", }) if err != nil { t.Fatalf("Upsert() error = %v", err) } got, err := store.Providers().GetByID(context.Background(), id) if err != nil { t.Fatalf("GetByID() error = %v", err) } if got.ProviderID != "upsert-json" { t.Fatalf("ProviderID = %q, want upsert-json", got.ProviderID) } if got.DisplayName != "Upsert JSON" { t.Fatalf("DisplayName = %q, want Upsert JSON", got.DisplayName) } if got.BaseURL != "https://json.example.com/v1" { t.Fatalf("BaseURL = %q, want trimmed base url", got.BaseURL) } if got.Platform != "openai" { t.Fatalf("Platform = %q, want openai", got.Platform) } if got.AccountType != "apikey" { t.Fatalf("AccountType = %q, want apikey", got.AccountType) } if got.SmokeTestModel != "gpt-5.4" { t.Fatalf("SmokeTestModel = %q, want gpt-5.4", got.SmokeTestModel) } if got.DefaultModelsJSON != "[]" { t.Fatalf("DefaultModelsJSON = %q, want []", got.DefaultModelsJSON) } if got.GroupTemplateJSON != "{}" { t.Fatalf("GroupTemplateJSON = %q, want {}", got.GroupTemplateJSON) } if got.ChannelTemplateJSON != "{}" { t.Fatalf("ChannelTemplateJSON = %q, want {}", got.ChannelTemplateJSON) } if got.PlanTemplateJSON != "{}" { t.Fatalf("PlanTemplateJSON = %q, want {}", got.PlanTemplateJSON) } if got.ImportOptionsJSON != "{}" { t.Fatalf("ImportOptionsJSON = %q, want {}", got.ImportOptionsJSON) } if got.ManifestJSON != "{}" { t.Fatalf("ManifestJSON = %q, want {}", got.ManifestJSON) } } func TestProvidersRepoUpsertFallsBackWhenLastInsertIDUnavailable(t *testing.T) { store := openTestDB(t) packID := createTestPack(t, store) ctx := context.Background() db := store.SQLDB() repo := newProvidersRepo(execQuerierStub{ execFn: func(ctx context.Context, query string, args ...any) (sql.Result, error) { if _, err := db.ExecContext(ctx, query, args...); err != nil { return nil, err } return resultStub{lastInsertErr: errors.New("last insert unavailable")}, nil }, queryFn: db.QueryContext, queryRowFn: db.QueryRowContext, }) id, err := repo.Upsert(ctx, Provider{ PackID: packID, ProviderID: "fallback-provider", DisplayName: "Fallback Provider", BaseURL: "https://fallback.example.com/v1", Platform: "openai", }) if err != nil { t.Fatalf("Upsert() error = %v", err) } if id <= 0 { t.Fatalf("Upsert() id = %d, want positive", id) } got, err := store.Providers().GetByPackIDAndProviderID(ctx, packID, "fallback-provider") if err != nil { t.Fatalf("GetByPackIDAndProviderID() error = %v", err) } if got.ID != id { t.Fatalf("fallback id = %d, want persisted id %d", id, got.ID) } } func TestProvidersRepoUpsertReturnsExecError(t *testing.T) { ctx := context.Background() repo := newProvidersRepo(execQuerierStub{ execFn: func(context.Context, string, ...any) (sql.Result, error) { return nil, errors.New("exec boom") }, queryFn: func(context.Context, string, ...any) (*sql.Rows, error) { return nil, errors.New("unexpected query") }, queryRowFn: func(context.Context, string, ...any) *sql.Row { panic("unexpected QueryRowContext") }, }) _, err := repo.Upsert(ctx, Provider{ PackID: 1, ProviderID: "provider-exec-error", DisplayName: "Provider Exec Error", BaseURL: "https://exec-error.example.com/v1", Platform: "openai", }) if err == nil || !strings.Contains(err.Error(), "exec boom") { t.Fatalf("Upsert() error = %v, want exec boom", err) } } func TestProvidersRepoUpsertReturnsFallbackReadError(t *testing.T) { store := openTestDB(t) ctx := context.Background() db := store.SQLDB() repo := newProvidersRepo(execQuerierStub{ execFn: func(context.Context, string, ...any) (sql.Result, error) { return resultStub{lastInsertErr: errors.New("last insert unavailable")}, nil }, queryFn: db.QueryContext, queryRowFn: db.QueryRowContext, }) _, err := repo.Upsert(ctx, Provider{ PackID: createTestPack(t, store), ProviderID: "provider-missing-after-upsert", DisplayName: "Provider Missing After Upsert", BaseURL: "https://missing.example.com/v1", Platform: "openai", }) if !errors.Is(err, sql.ErrNoRows) { t.Fatalf("Upsert() error = %v, want sql.ErrNoRows fallback read error", err) } } func TestProvidersRepoValidationErrors(t *testing.T) { store := openTestDB(t) packID := createTestPack(t, store) tests := []struct { name string provider Provider }{ {"pack_id zero", Provider{ProviderID: "p", DisplayName: "d", BaseURL: "b", Platform: "openai"}}, {"empty provider_id", Provider{PackID: packID, DisplayName: "d", BaseURL: "b", Platform: "openai"}}, {"empty display_name", Provider{PackID: packID, ProviderID: "p", BaseURL: "b", Platform: "openai"}}, {"empty base_url", Provider{PackID: packID, ProviderID: "p", DisplayName: "d", Platform: "openai"}}, {"empty platform", Provider{PackID: packID, ProviderID: "p", DisplayName: "d", BaseURL: "b"}}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { _, err := store.Providers().Create(context.Background(), tt.provider) if err == nil { t.Fatal("Create() error = nil, want validation error") } }) } } func TestProvidersRepoUpsertValidationErrors(t *testing.T) { store := openTestDB(t) packID := createTestPack(t, store) tests := []struct { name string provider Provider }{ {"pack_id zero", Provider{ProviderID: "p", DisplayName: "d", BaseURL: "b", Platform: "openai"}}, {"empty provider_id", Provider{PackID: packID, DisplayName: "d", BaseURL: "b", Platform: "openai"}}, {"empty display_name", Provider{PackID: packID, ProviderID: "p", BaseURL: "b", Platform: "openai"}}, {"empty base_url", Provider{PackID: packID, ProviderID: "p", DisplayName: "d", Platform: "openai"}}, {"empty platform", Provider{PackID: packID, ProviderID: "p", DisplayName: "d", BaseURL: "b"}}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { _, err := store.Providers().Upsert(context.Background(), tt.provider) if err == nil { t.Fatal("Upsert() error = nil, want validation error") } }) } } func TestProvidersRepoGetByPackIDAndProviderIDNotFound(t *testing.T) { store := openTestDB(t) _, err := store.Providers().GetByPackIDAndProviderID(context.Background(), 999, "p") if !errors.Is(err, sql.ErrNoRows) { t.Fatalf("GetByPackIDAndProviderID() error = %v, want sql.ErrNoRows", err) } } func TestProvidersRepoGetByID(t *testing.T) { store := openTestDB(t) packID := createTestPack(t, store) providerID, err := store.Providers().Create(context.Background(), Provider{ PackID: packID, ProviderID: "provider-id-lookup", DisplayName: "Lookup", BaseURL: "https://lookup.example.com", Platform: "openai", }) if err != nil { t.Fatalf("Create() error = %v", err) } got, err := store.Providers().GetByID(context.Background(), providerID) if err != nil { t.Fatalf("GetByID() error = %v", err) } if got.ProviderID != "provider-id-lookup" { t.Fatalf("GetByID() provider_id = %q, want provider-id-lookup", got.ProviderID) } } func TestProvidersRepoGetByIDErrors(t *testing.T) { store := openTestDB(t) if _, err := store.Providers().GetByID(context.Background(), 0); err == nil { t.Fatal("GetByID(0) error = nil, want error") } if _, err := store.Providers().ListByPackID(context.Background(), 0); err == nil { t.Fatal("ListByPackID(0) error = nil, want error") } if _, err := store.Providers().GetByID(context.Background(), 999); !errors.Is(err, sql.ErrNoRows) { t.Fatalf("GetByID(999) error = %v, want sql.ErrNoRows", err) } } func TestProvidersRepoGetByPackIDEmptyError(t *testing.T) { store := openTestDB(t) _, err := store.Providers().GetByPackIDAndProviderID(context.Background(), 0, "p") if err == nil { t.Fatal("GetByPackIDAndProviderID with packID=0 error = nil, want error") } } func TestDefaultJSONObject(t *testing.T) { if got := defaultJSONObject(""); got != "{}" { t.Fatalf("defaultJSONObject('') = %q, want {}", got) } if got := defaultJSONObject(`{"a":1}`); got != `{"a":1}` { t.Fatalf("defaultJSONObject() = %q, want input", got) } } func TestDefaultJSONArray(t *testing.T) { if got := defaultJSONArray(""); got != "[]" { t.Fatalf("defaultJSONArray('') = %q, want []", got) } if got := defaultJSONArray(`["a"]`); got != `["a"]` { t.Fatalf("defaultJSONArray() = %q, want input", got) } }