package main import ( "context" "testing" "bookhoard/internal/config" "bookhoard/internal/database" "github.com/jackc/pgx/v5/pgxpool" "github.com/stretchr/testify/require" ) // TestSchemaInitializationIsIdempotent replays the application's production // schema initializer twice against the same database. schema.sql runs on // EVERY app startup, so it must apply cleanly to a database where a previous // startup already applied it. A regression here crash-loops the production // container on its second boot (e.g. SQLSTATE 2BP01: DROP FUNCTION refused // while a trigger created by the previous run still references it). // // Uses a dedicated pool: production Initialize holds the advisory lock on one // connection while executing the schema on another, but the shared test pool // is capped at MaxConns=1 and would deadlock. func TestSchemaInitializationIsIdempotent(t *testing.T) { if testing.Short() { t.Skip("skipping schema re-initialization in -short mode") } // Bring up the standard test environment (seeds users, starts server). setup := setupTestServer(t) defer func() { _ = setup.Close() }() // Dedicated pool with default connection limits for the initializer. poolCfg, err := pgxpool.ParseConfig(config.LoadConfig().DatabaseURL()) require.NoError(t, err, "failed to parse database URL") pool, err := pgxpool.NewWithConfig(context.Background(), poolCfg) require.NoError(t, err, "failed to connect for schema initialization test") defer func() { pool.Close() }() ctx := context.Background() // First boot: applies the schema to the (possibly fresh) database. if err := database.Initialize(ctx, pool); err != nil { t.Fatalf("first schema initialization failed: %v", err) } // Second boot against the now-initialized database: this is exactly // where non-idempotent statements (bare CREATE TRIGGER, DROP FUNCTION // with dependent objects, etc.) blow up. if err := database.Initialize(ctx, pool); err != nil { t.Fatalf("second schema initialization failed: %v — schema.sql must stay idempotent across restarts", err) } }