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 != 6 { t.Fatalf("schema_migrations row count after first open = %d, want 6", 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 != 6 { t.Fatalf("schema_migrations row count after second open = %d, want 6", 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 != 6 { t.Fatalf("schema_migrations row count after backfill = %d, want 6", 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 }