//go:build integration package repository import ( "context" "database/sql" "testing" "github.com/stretchr/testify/require" ) func TestMigrationsRunner_IsIdempotent_AndSchemaIsUpToDate(t *testing.T) { tx := testTx(t) // Re-apply migrations to verify idempotency (no errors, no duplicate rows). require.NoError(t, ApplyMigrations(context.Background(), integrationDB)) // schema_migrations should have at least the current migration set. var applied int require.NoError(t, tx.QueryRowContext(context.Background(), "SELECT COUNT(*) FROM schema_migrations").Scan(&applied)) require.GreaterOrEqual(t, applied, 7, "expected schema_migrations to contain applied migrations") // users: columns required by repository queries requireColumn(t, tx, "users", "username", "character varying", 100, false) requireColumn(t, tx, "users", "notes", "text", 0, false) // accounts: schedulable and rate-limit fields requireColumn(t, tx, "accounts", "notes", "text", 0, true) requireColumn(t, tx, "accounts", "schedulable", "boolean", 0, false) requireColumn(t, tx, "accounts", "rate_limited_at", "timestamp with time zone", 0, true) requireColumn(t, tx, "accounts", "rate_limit_reset_at", "timestamp with time zone", 0, true) requireColumn(t, tx, "accounts", "overload_until", "timestamp with time zone", 0, true) requireColumn(t, tx, "accounts", "session_window_status", "character varying", 20, true) // api_keys: key length should be 128 requireColumn(t, tx, "api_keys", "key", "character varying", 128, false) // redeem_codes: subscription fields requireColumn(t, tx, "redeem_codes", "group_id", "bigint", 0, true) requireColumn(t, tx, "redeem_codes", "validity_days", "integer", 0, false) // usage_logs: billing_type used by filters/stats requireColumn(t, tx, "usage_logs", "billing_type", "smallint", 0, false) // settings table should exist var settingsRegclass sql.NullString require.NoError(t, tx.QueryRowContext(context.Background(), "SELECT to_regclass('public.settings')").Scan(&settingsRegclass)) require.True(t, settingsRegclass.Valid, "expected settings table to exist") // user_allowed_groups table should exist var uagRegclass sql.NullString require.NoError(t, tx.QueryRowContext(context.Background(), "SELECT to_regclass('public.user_allowed_groups')").Scan(&uagRegclass)) require.True(t, uagRegclass.Valid, "expected user_allowed_groups table to exist") // user_subscriptions: deleted_at for soft delete support (migration 012) requireColumn(t, tx, "user_subscriptions", "deleted_at", "timestamp with time zone", 0, true) // orphan_allowed_groups_audit table should exist (migration 013) var orphanAuditRegclass sql.NullString require.NoError(t, tx.QueryRowContext(context.Background(), "SELECT to_regclass('public.orphan_allowed_groups_audit')").Scan(&orphanAuditRegclass)) require.True(t, orphanAuditRegclass.Valid, "expected orphan_allowed_groups_audit table to exist") // account_groups: created_at should be timestamptz requireColumn(t, tx, "account_groups", "created_at", "timestamp with time zone", 0, false) // user_allowed_groups: created_at should be timestamptz requireColumn(t, tx, "user_allowed_groups", "created_at", "timestamp with time zone", 0, false) } func requireColumn(t *testing.T, tx *sql.Tx, table, column, dataType string, maxLen int, nullable bool) { t.Helper() var row struct { DataType string MaxLen sql.NullInt64 Nullable string } err := tx.QueryRowContext(context.Background(), ` SELECT data_type, character_maximum_length, is_nullable FROM information_schema.columns WHERE table_schema = 'public' AND table_name = $1 AND column_name = $2 `, table, column).Scan(&row.DataType, &row.MaxLen, &row.Nullable) require.NoError(t, err, "query information_schema.columns for %s.%s", table, column) require.Equal(t, dataType, row.DataType, "data_type mismatch for %s.%s", table, column) if maxLen > 0 { require.True(t, row.MaxLen.Valid, "expected maxLen for %s.%s", table, column) require.Equal(t, int64(maxLen), row.MaxLen.Int64, "maxLen mismatch for %s.%s", table, column) } if nullable { require.Equal(t, "YES", row.Nullable, "nullable mismatch for %s.%s", table, column) } else { require.Equal(t, "NO", row.Nullable, "nullable mismatch for %s.%s", table, column) } }