Files
sub2api-cn-relay-manager/tests/integration/store_init_test.go
2026-05-12 23:25:02 +08:00

292 lines
7.5 KiB
Go

package integration_test
import (
"context"
"database/sql"
"errors"
"fmt"
"path/filepath"
"testing"
_ "modernc.org/sqlite"
"sub2api-cn-relay-manager/internal/store/sqlite"
)
func TestStoreInitCreatesRequiredTables(t *testing.T) {
store := openTestStore(t)
defer closeTestStore(t, store)
for _, table := range []string{"hosts", "packs", "providers"} {
if !tableExists(t, store.SQLDB(), table) {
t.Fatalf("table %q does not exist after store initialization", table)
}
}
}
func TestStoreInitEnforcesUniqueConstraints(t *testing.T) {
ctx := context.Background()
store := openTestStore(t)
defer closeTestStore(t, store)
packID, err := store.Packs().Create(ctx, sqlite.Pack{
PackID: "openai-cn-pack",
Version: "1.0.0",
Checksum: "checksum-1",
})
if err != nil {
t.Fatalf("Packs().Create() error = %v", err)
}
provider := sqlite.Provider{
PackID: packID,
ProviderID: "deepseek",
DisplayName: "DeepSeek",
BaseURL: "https://api.deepseek.com",
Platform: "openai",
}
if _, err := store.Providers().Create(ctx, provider); err != nil {
t.Fatalf("Providers().Create() first call error = %v", err)
}
if _, err := store.Providers().Create(ctx, provider); err == nil {
t.Fatal("Providers().Create() second call error = nil, want unique constraint failure")
}
}
func TestStoreInitEnforcesProviderForeignKey(t *testing.T) {
ctx := context.Background()
store := openTestStore(t)
defer closeTestStore(t, store)
_, err := store.Providers().Create(ctx, sqlite.Provider{
PackID: 9999,
ProviderID: "ghost",
DisplayName: "Ghost",
BaseURL: "https://ghost.example.com",
Platform: "openai",
})
if err == nil {
t.Fatal("Providers().Create() error = nil, want foreign key failure")
}
}
func TestStoreInitRollsBackTransaction(t *testing.T) {
ctx := context.Background()
store := openTestStore(t)
defer closeTestStore(t, store)
wantErr := errors.New("force rollback")
err := store.WithTx(ctx, func(queries *sqlite.Queries) error {
_, err := queries.Hosts.Create(ctx, sqlite.Host{
HostID: "host-1",
BaseURL: "https://host.example.com",
HostVersion: "0.1.126",
CapabilityProbeJSON: `{"supports_batch_accounts":true}`,
})
if err != nil {
return err
}
return wantErr
})
if !errors.Is(err, wantErr) {
t.Fatalf("WithTx() error = %v, want %v", err, wantErr)
}
if got := countRows(t, store.SQLDB(), "hosts"); got != 0 {
t.Fatalf("hosts row count after rollback = %d, want 0", got)
}
}
func TestStoreInitRecordsMigrationLedgerOnce(t *testing.T) {
dbPath := filepath.Join(t.TempDir(), "state.db")
dsn := fmt.Sprintf("file:%s?_busy_timeout=5000", filepath.ToSlash(dbPath))
store1, err := sqlite.Open(context.Background(), dsn)
if err != nil {
t.Fatalf("first sqlite.Open() error = %v", err)
}
if got := countRows(t, store1.SQLDB(), "schema_migrations"); got != 1 {
t.Fatalf("schema_migrations row count after first open = %d, want 1", got)
}
if err := store1.Close(); err != nil {
t.Fatalf("first store.Close() error = %v", err)
}
store2, err := sqlite.Open(context.Background(), dsn)
if err != nil {
t.Fatalf("second sqlite.Open() error = %v", err)
}
defer closeTestStore(t, store2)
if got := countRows(t, store2.SQLDB(), "schema_migrations"); got != 1 {
t.Fatalf("schema_migrations row count after second open = %d, want 1", got)
}
}
func TestStoreInitBackfillsLedgerForCompletePreLedgerSchema(t *testing.T) {
dbPath := filepath.Join(t.TempDir(), "state.db")
dsn := fmt.Sprintf("file:%s?_busy_timeout=5000", filepath.ToSlash(dbPath))
rawDB := openRawSQLiteDB(t, dsn)
createLegacy0001Schema(t, rawDB)
closeRawSQLiteDB(t, rawDB)
store, err := sqlite.Open(context.Background(), dsn)
if err != nil {
t.Fatalf("sqlite.Open() on complete pre-ledger schema error = %v", err)
}
defer closeTestStore(t, store)
if got := countRows(t, store.SQLDB(), "schema_migrations"); got != 1 {
t.Fatalf("schema_migrations row count after backfill = %d, want 1", got)
}
}
func TestStoreInitFailsWhenPreLedgerSchemaIsPartial(t *testing.T) {
dbPath := filepath.Join(t.TempDir(), "state.db")
dsn := fmt.Sprintf("file:%s?_busy_timeout=5000", filepath.ToSlash(dbPath))
rawDB := openRawSQLiteDB(t, dsn)
mustExec(t, rawDB, `
CREATE TABLE hosts (
id INTEGER PRIMARY KEY AUTOINCREMENT,
host_id TEXT NOT NULL UNIQUE,
base_url TEXT NOT NULL,
host_version TEXT NOT NULL,
capability_probe_json TEXT NOT NULL DEFAULT '{}',
created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP
)`)
closeRawSQLiteDB(t, rawDB)
store, err := sqlite.Open(context.Background(), dsn)
if err == nil {
closeTestStore(t, store)
t.Fatal("sqlite.Open() error = nil, want partial pre-ledger schema failure")
}
}
func openTestStore(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 openRawSQLiteDB(t *testing.T, dsn string) *sql.DB {
t.Helper()
db, err := sql.Open("sqlite", dsn)
if err != nil {
t.Fatalf("sql.Open() error = %v", err)
}
if err := db.PingContext(context.Background()); err != nil {
t.Fatalf("raw db PingContext() error = %v", err)
}
return db
}
func closeRawSQLiteDB(t *testing.T, db *sql.DB) {
t.Helper()
if err := db.Close(); err != nil {
t.Fatalf("raw db Close() error = %v", err)
}
}
func createLegacy0001Schema(t *testing.T, db *sql.DB) {
t.Helper()
mustExec(t, db, `
CREATE TABLE hosts (
id INTEGER PRIMARY KEY AUTOINCREMENT,
host_id TEXT NOT NULL UNIQUE,
base_url TEXT NOT NULL,
host_version TEXT NOT NULL,
capability_probe_json TEXT NOT NULL DEFAULT '{}',
created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP
)`)
mustExec(t, db, `
CREATE TABLE packs (
id INTEGER PRIMARY KEY AUTOINCREMENT,
pack_id TEXT NOT NULL UNIQUE,
version TEXT NOT NULL,
checksum TEXT NOT NULL,
installed_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP
)`)
mustExec(t, db, `
CREATE TABLE providers (
id INTEGER PRIMARY KEY AUTOINCREMENT,
pack_id INTEGER NOT NULL,
provider_id TEXT NOT NULL,
display_name TEXT NOT NULL,
base_url TEXT NOT NULL,
platform TEXT NOT NULL,
created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT fk_providers_pack
FOREIGN KEY (pack_id)
REFERENCES packs(id)
ON DELETE CASCADE,
CONSTRAINT uq_providers_pack_provider
UNIQUE (pack_id, provider_id)
)`)
}
func mustExec(t *testing.T, db *sql.DB, statement string) {
t.Helper()
if _, err := db.ExecContext(context.Background(), statement); err != nil {
t.Fatalf("ExecContext() error = %v", err)
}
}
func closeTestStore(t *testing.T, store *sqlite.DB) {
t.Helper()
if err := store.Close(); err != nil {
t.Fatalf("store.Close() error = %v", err)
}
}
func tableExists(t *testing.T, db *sql.DB, table string) bool {
t.Helper()
var name string
err := db.QueryRowContext(
context.Background(),
"SELECT name FROM sqlite_master WHERE type = 'table' AND name = ?",
table,
).Scan(&name)
if errors.Is(err, sql.ErrNoRows) {
return false
}
if err != nil {
t.Fatalf("tableExists(%q) query error = %v", table, err)
}
return name == table
}
func countRows(t *testing.T, db *sql.DB, table string) int {
t.Helper()
var count int
query := fmt.Sprintf("SELECT COUNT(*) FROM %s", table)
if err := db.QueryRowContext(context.Background(), query).Scan(&count); err != nil {
t.Fatalf("countRows(%q) query error = %v", table, err)
}
return count
}