You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

167 lines
4.8 KiB
Go

package testutil
import (
"context"
"database/sql"
"fmt"
"net/url"
"os"
"testing"
"time"
"git.runcible.io/learning/pulley/internal/config"
"git.runcible.io/learning/pulley/internal/data"
"git.runcible.io/learning/pulley/internal/database"
"git.runcible.io/learning/pulley/migrations"
"github.com/jackc/pgx/v5"
"github.com/jackc/pgx/v5/pgxpool"
)
// SetupTestDB provides a connection pool to a freshly created temporary database.
//
// Environment Variables:
// - PULLEY_INTEGRATION_DATABASE_URI: The connection string for the admin/maintenance DB.
// Defaults to: postgres://pulley:pulley@localhost:5432/postgres?sslmode=disable
// - PULLEY_ALLOW_REMOTE_INTEGRATION_TEST: Must be 'true' if the URI host is not 'localhost' or '127.0.0.1'.
//
// Safety:
//
// If the resolved URI points to a remote host and the safety flag is not enabled, the test
// will fail immediately to prevent accidental execution against production or staging environments.
//
// Lifecycle:
//
// The function uses t.Cleanup to automatically close the pool and drop the temporary
// database, ensuring no resource leaks between test runs.
func SetupTestDB(t *testing.T) *pgxpool.Pool {
t.Helper()
ctxTimeout, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
intDBUri, ok := os.LookupEnv("PULLEY_INTEGRATION_DATABASE_URI")
if !ok {
intDBUri = "postgres://pulley:pulley@localhost:5434/postgres?sslmode=disable"
t.Log("PULLEY_INTEGRATION_DATABASE_URI not set. Using localhost default")
}
// check for non-local hostname
parsed, err := url.Parse(intDBUri)
if err != nil {
t.Fatal(err)
}
if parsed.Hostname() != "localhost" && parsed.Hostname() != "127.0.0.1" {
if os.Getenv("PULLEY_ALLOW_REMOTE_INTEGRATION_TEST") != "true" {
t.Fatalf("Attempting to run integration tests against a non-local DB (%s). "+
"Set PULLEY_ALLOW_REMOTE_INTEGRATION_TEST=true to bypass.", parsed.Hostname())
}
}
// verify connection
dbAdmin, err := sql.Open("pgx", intDBUri)
if err != nil {
t.Log("Hint: check if `make start-local` is running a postgres container")
t.Fatalf("Failed to connect to database: %s", err.Error())
}
t.Cleanup(func() {
dbAdmin.Close()
})
err = dbAdmin.PingContext(ctxTimeout)
if err != nil {
t.Log("Hint: check if `make start-local` is running a postgres container")
t.Fatalf("Failed to connect to database: %s", err)
}
// create and migrate new db
testDbName := fmt.Sprintf("test_db_%d", time.Now().UnixNano())
t.Logf("Creating integration test database: %s", testDbName)
query := fmt.Sprintf("CREATE DATABASE %s", testDbName)
_, err = dbAdmin.ExecContext(ctxTimeout, query)
if err != nil {
t.Fatalf("Created to create database %s: %s", testDbName, err)
}
testCreds := *parsed
testCreds.Path = fmt.Sprintf("/%s", testDbName)
// migrations
migrateDb, err := sql.Open("pgx", testCreds.String())
if err != nil {
t.Fatalf("Failed to open migrations connection to %s: %s", testDbName, err)
}
defer migrateDb.Close()
err = migrateDb.PingContext(ctxTimeout)
if err != nil {
t.Fatalf("Failed to connect to database: %s", err)
}
t.Logf("Migrating database %s", testDbName)
err = migrations.Migrate(migrateDb)
if err != nil {
t.Fatalf("Failed to migrate %s: %s", testDbName, err)
}
migrateDb.Close()
testPool, err := database.OpenPgPool(ctxTimeout, config.ServiceConfig{DatabaseUri: testCreds.String()})
t.Cleanup(func() {
t.Logf("Ensuring test pool is closed")
testPool.Close()
t.Logf("Cleaning up database %s", testDbName)
if t.Failed() && os.Getenv("PULLEY_PRESERVE_FAILED_DB") == "true" {
t.Logf("Test failed; skipping db cleanup for %s", testDbName)
return
}
ctxCleanup, cleanupCancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cleanupCancel()
query := fmt.Sprintf("DROP DATABASE IF EXISTS %s WITH (FORCE)", testDbName)
_, err = dbAdmin.ExecContext(ctxCleanup, query)
if err != nil {
t.Errorf("Error in dropping database %s", testDbName)
}
})
return testPool
}
// SeedMovie inserts movies into the movies database table
func SeedMovies(t *testing.T, pool *pgxpool.Pool, movies []data.Movie) {
t.Helper()
ctxTimeout, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
tx, err := pool.BeginTx(ctxTimeout, pgx.TxOptions{})
if err != nil {
t.Fatalf("Failed to begin seed transaction: %v", err)
}
defer tx.Rollback(ctxTimeout)
query := `
INSERT INTO movies (id, title, year, runtime, genres, version)
VALUES ($1, $2, $3, $4, $5, $6)`
for _, m := range movies {
_, err := tx.Exec(ctxTimeout, query, m.ID, m.Title, m.Year, m.Runtime, m.Genres, m.Version)
if err != nil {
t.Fatalf("failed to insert movie %q: %v", m.Title, err)
}
}
if err = tx.Commit(ctxTimeout); err != nil {
t.Fatalf("Failed to commit seed transaction: %v", err)
}
}