// Package testutil provides shared helpers for Silo tests. package testutil import ( "context" "fmt" "os" "path/filepath" "sort" "testing" "github.com/jackc/pgx/v5/pgxpool" ) // MustConnectTestPool connects to a test database using TEST_DATABASE_URL. // If the env var is unset the test is skipped. Migrations are applied and // all data tables are truncated before returning. The pool is closed // automatically when the test finishes. func MustConnectTestPool(t *testing.T) *pgxpool.Pool { t.Helper() dsn := os.Getenv("TEST_DATABASE_URL") if dsn == "" { t.Skip("TEST_DATABASE_URL not set, skipping integration test") } pool, err := pgxpool.New(context.Background(), dsn) if err != nil { t.Fatalf("connecting to test database: %v", err) } if err := pool.Ping(context.Background()); err != nil { pool.Close() t.Fatalf("pinging test database: %v", err) } t.Cleanup(func() { pool.Close() }) RunMigrations(t, pool) TruncateAll(t, pool) return pool } // RunMigrations applies all SQL migration files from the migrations/ // directory. It walks upward from the current working directory to find // the project root (containing go.mod). func RunMigrations(t *testing.T, pool *pgxpool.Pool) { t.Helper() root := findProjectRoot(t) migDir := filepath.Join(root, "migrations") entries, err := os.ReadDir(migDir) if err != nil { t.Fatalf("reading migrations directory: %v", err) } // Sort by filename to apply in order. sort.Slice(entries, func(i, j int) bool { return entries[i].Name() < entries[j].Name() }) for _, e := range entries { if e.IsDir() || filepath.Ext(e.Name()) != ".sql" { continue } sql, err := os.ReadFile(filepath.Join(migDir, e.Name())) if err != nil { t.Fatalf("reading migration %s: %v", e.Name(), err) } if _, err := pool.Exec(context.Background(), string(sql)); err != nil { // Migrations may contain IF NOT EXISTS / CREATE OR REPLACE, // so most "already exists" errors are fine. Log and continue. t.Logf("migration %s: %v (may be OK if already applied)", e.Name(), err) } } } // TruncateAll removes all data from tables, leaving schema intact. func TruncateAll(t *testing.T, pool *pgxpool.Pool) { t.Helper() _, err := pool.Exec(context.Background(), ` TRUNCATE settings_overrides, module_state, job_log, jobs, job_definitions, runners, dag_cross_edges, dag_edges, dag_nodes, audit_log, sync_log, api_tokens, sessions, item_files, item_projects, relationships, revisions, inventory, items, projects, sequences_by_name, users, property_migrations CASCADE `) if err != nil { t.Fatalf("truncating tables: %v", err) } } // findProjectRoot walks upward from cwd to find the directory containing go.mod. func findProjectRoot(t *testing.T) string { t.Helper() dir, err := os.Getwd() if err != nil { t.Fatalf("getting working directory: %v", err) } for { if _, err := os.Stat(filepath.Join(dir, "go.mod")); err == nil { return dir } parent := filepath.Dir(dir) if parent == dir { t.Fatalf("could not find project root (go.mod)") } dir = parent } panic(fmt.Sprintf("unreachable")) }