Migration 011's UPDATE ran without tapir.current_user_id set, so FORCE RLS blocked all rows and 0 users were updated (skipped_manual=607 in scheduler). Migration 012 temporarily drops FORCE so the table owner can run the UPDATE, then restores it.
109 lines
4.7 KiB
Go
109 lines
4.7 KiB
Go
package store_test
|
|
|
|
import (
|
|
"context"
|
|
"database/sql"
|
|
"os"
|
|
"testing"
|
|
|
|
"github.com/golang-migrate/migrate/v4"
|
|
migratepgx "github.com/golang-migrate/migrate/v4/database/pgx/v5"
|
|
"github.com/golang-migrate/migrate/v4/source/iofs"
|
|
"github.com/stretchr/testify/require"
|
|
|
|
_ "github.com/jackc/pgx/v5/stdlib" // register the "pgx" database/sql driver
|
|
)
|
|
|
|
// fileMigrator builds a golang-migrate instance from the on-disk migration files
|
|
// (not the embedded FS the production Migrate uses), so a test can step the schema
|
|
// up and down. os.DirFS(".") is rooted at the package dir; the SQL lives under
|
|
// "migrations". Mirrors store.Migrate's construction otherwise.
|
|
func fileMigrator(t *testing.T) *migrate.Migrate {
|
|
t.Helper()
|
|
db, err := sql.Open("pgx", dsn)
|
|
require.NoError(t, err)
|
|
t.Cleanup(func() { _ = db.Close() })
|
|
|
|
drv, err := migratepgx.WithInstance(db, &migratepgx.Config{})
|
|
require.NoError(t, err)
|
|
src, err := iofs.New(os.DirFS("."), "migrations")
|
|
require.NoError(t, err)
|
|
m, err := migrate.NewWithInstance("iofs", src, "pgx", drv)
|
|
require.NoError(t, err)
|
|
t.Cleanup(func() { _, _ = m.Close() })
|
|
return m
|
|
}
|
|
|
|
// loginEventsExists reports whether the login_events relation is present.
|
|
func loginEventsExists(t *testing.T) bool {
|
|
t.Helper()
|
|
var reg *string
|
|
require.NoError(t, rawPool(t).QueryRow(context.Background(),
|
|
`SELECT to_regclass('public.login_events')::text`).Scan(®))
|
|
return reg != nil
|
|
}
|
|
|
|
// TestMigration010LoginEventsUpDown proves migration 010 is reversible: the down
|
|
// migration drops login_events cleanly and the up migration recreates it. A rotten
|
|
// down migration (forgotten DROP, dangling policy) would fail here rather than in
|
|
// production during a rollback. The test restores the schema to latest before
|
|
// returning so the shared embedded-postgres stays at HEAD for sibling tests.
|
|
func TestMigration010LoginEventsUpDown(t *testing.T) {
|
|
newStore(t) // ensure the schema is migrated to latest (011 applied)
|
|
require.True(t, loginEventsExists(t), "login_events must exist at latest migration")
|
|
|
|
m := fileMigrator(t)
|
|
// 011 and 012 sit above 010; step them down first so 010 is exercised in isolation.
|
|
require.NoError(t, m.Steps(-1), "down 012 is a no-op, login_events intact")
|
|
require.True(t, loginEventsExists(t), "012 down leaves login_events intact")
|
|
require.NoError(t, m.Steps(-1), "down 011 must not touch login_events")
|
|
require.True(t, loginEventsExists(t), "011 down leaves login_events intact")
|
|
|
|
require.NoError(t, m.Steps(-1), "down 010 must drop login_events")
|
|
require.False(t, loginEventsExists(t), "login_events must be gone after the down migration")
|
|
|
|
require.NoError(t, m.Steps(3), "up must recreate 010 then re-apply 011 and 012")
|
|
require.True(t, loginEventsExists(t), "login_events must be restored after the up migration")
|
|
}
|
|
|
|
// autoSummarizeDefault reads the users.auto_summarize column default as text
|
|
// ("true"/"false"), so the migration's default flip is verifiable directly.
|
|
func autoSummarizeDefault(t *testing.T) string {
|
|
t.Helper()
|
|
var def string
|
|
require.NoError(t, rawPool(t).QueryRow(context.Background(),
|
|
`SELECT column_default FROM information_schema.columns
|
|
WHERE table_name = 'users' AND column_name = 'auto_summarize'`).Scan(&def))
|
|
return def
|
|
}
|
|
|
|
// TestMigration011AutoSummarizeDefaultUpDown proves migration 011 is reversible:
|
|
// up sets the auto_summarize column default to TRUE (ADR-018), down restores
|
|
// FALSE. The down intentionally does not revert existing rows — only the default.
|
|
func TestMigration011AutoSummarizeDefaultUpDown(t *testing.T) {
|
|
newStore(t) // latest (012 applied)
|
|
require.Equal(t, "true", autoSummarizeDefault(t), "011 sets the default to TRUE")
|
|
|
|
m := fileMigrator(t)
|
|
require.NoError(t, m.Steps(-1), "down 012 is a no-op")
|
|
require.NoError(t, m.Steps(-1), "down 011 reverts the column default")
|
|
require.Equal(t, "false", autoSummarizeDefault(t), "default is FALSE after the down migration")
|
|
|
|
require.NoError(t, m.Steps(1), "up 011 re-applies the TRUE default")
|
|
require.Equal(t, "true", autoSummarizeDefault(t))
|
|
require.NoError(t, m.Steps(1), "up 012 runs clean (no FORCE RLS on fresh schema)")
|
|
}
|
|
|
|
// TestMigration012FixAutoSummarizeRLS proves 012 runs cleanly and flips any
|
|
// remaining auto_summarize=FALSE rows to TRUE (the back-fill blocked by RLS in 011).
|
|
func TestMigration012FixAutoSummarizeRLS(t *testing.T) {
|
|
newStore(t) // apply all migrations including 012
|
|
require.Equal(t, "true", autoSummarizeDefault(t), "column default is TRUE after 012")
|
|
|
|
// Round-trip: down 012, then up 012 — must be idempotent.
|
|
m := fileMigrator(t)
|
|
require.NoError(t, m.Steps(-1), "down 012 must not error")
|
|
require.NoError(t, m.Steps(1), "up 012 must re-apply cleanly")
|
|
require.Equal(t, "true", autoSummarizeDefault(t), "default still TRUE after 012 re-applied")
|
|
}
|