- TestEmailVerifiedAt: register (auto, admin), activate, activate-by-code, reset-password, change-password, update - TestAdminEmailVerifiedAt: admin create, activate action, invitation link, email change, JSON - TestEmailVerifiedAtMigration: column shape, no backfill, existing column kept, rollback keeps column
144 lines
4.7 KiB
Go
144 lines
4.7 KiB
Go
package updates
|
|
|
|
import (
|
|
"testing"
|
|
"time"
|
|
|
|
"git.golem15.com/golem15/summercms/modules/lagoon"
|
|
"github.com/go-gormigrate/gormigrate/v2"
|
|
"gorm.io/gorm"
|
|
)
|
|
|
|
const emailVerifiedAtID = "202610210001_add_users_email_verified_at"
|
|
|
|
// emailVerifiedDB opens a dedicated database and a migrator over every
|
|
// registered migration.
|
|
func emailVerifiedDB(t *testing.T, name string) (*gorm.DB, *gormigrate.Gormigrate) {
|
|
t.Helper()
|
|
gdb, err := lagoon.Use(t.Context(), dedicatedDB(t, name))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
return gdb, gormigrate.New(gdb, &gormigrate.Options{
|
|
TableName: "summer_migrations_golem15_user",
|
|
UseTransaction: true,
|
|
}, All())
|
|
}
|
|
|
|
// TestEmailVerifiedAtMigration pins D-27: users.email_verified_at is a
|
|
// nullable TIMESTAMPTZ added only when missing, the migration writes no
|
|
// rows (no backfill, D-26), a column that already exists keeps its values
|
|
// and the rollback leaves it in place.
|
|
func TestEmailVerifiedAtMigration(t *testing.T) {
|
|
t.Run("column_shape", func(t *testing.T) {
|
|
gdb, m := emailVerifiedDB(t, "user_email_verified_shape")
|
|
if err := m.Migrate(); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
var row struct {
|
|
DataType string
|
|
IsNullable string
|
|
}
|
|
if err := gdb.Raw(`SELECT data_type, is_nullable FROM information_schema.columns
|
|
WHERE table_name = 'users' AND column_name = 'email_verified_at'`).Scan(&row).Error; err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if row.DataType != "timestamp with time zone" || row.IsNullable != "YES" {
|
|
t.Fatalf("email_verified_at = %+v", row)
|
|
}
|
|
})
|
|
|
|
t.Run("no_backfill", func(t *testing.T) {
|
|
gdb, m := emailVerifiedDB(t, "user_email_verified_backfill")
|
|
if err := m.MigrateTo("202610160002_add_handle_to_users"); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if gdb.Migrator().HasColumn("users", "email_verified_at") {
|
|
t.Fatal("setup: the column exists before the migration")
|
|
}
|
|
for _, email := range []string{"legacy-a@example.test", "legacy-b@example.test"} {
|
|
if err := gdb.Exec(`INSERT INTO users (email, password, is_activated, has_self_set_password, activated_at)
|
|
VALUES (?, 'hash', TRUE, TRUE, NOW())`, email).Error; err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
if err := m.Migrate(); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
var total, proven int64
|
|
if err := gdb.Raw(`SELECT COUNT(*), COUNT(email_verified_at) FROM users`).Row().Scan(&total, &proven); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if total != 2 || proven != 0 {
|
|
t.Fatalf("users %d, with proof %d: the migration must write no rows", total, proven)
|
|
}
|
|
})
|
|
|
|
t.Run("existing_column_kept", func(t *testing.T) {
|
|
gdb, m := emailVerifiedDB(t, "user_email_verified_existing")
|
|
if err := m.MigrateTo("202610160002_add_handle_to_users"); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
// A Laravel-derived schema already carries the column and its values.
|
|
if err := gdb.Exec(`ALTER TABLE users ADD COLUMN email_verified_at TIMESTAMPTZ NULL`).Error; err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
want := time.Date(2024, 1, 2, 3, 4, 5, 0, time.UTC)
|
|
if err := gdb.Exec(`INSERT INTO users (email, email_verified_at) VALUES ('imported@example.test', ?)`, want).Error; err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := gdb.Exec(`INSERT INTO users (email) VALUES ('imported-unproven@example.test')`).Error; err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := m.Migrate(); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
var ran int64
|
|
if err := gdb.Raw(`SELECT COUNT(*) FROM summer_migrations_golem15_user WHERE id = ?`, emailVerifiedAtID).Scan(&ran).Error; err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if ran != 1 {
|
|
t.Fatal("the migration did not run over the existing column")
|
|
}
|
|
var got *time.Time
|
|
if err := gdb.Raw(`SELECT email_verified_at FROM users WHERE email = 'imported@example.test'`).Row().Scan(&got); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if got == nil || !got.Equal(want) {
|
|
t.Fatalf("existing value = %v, want %v", got, want)
|
|
}
|
|
var proven int64
|
|
if err := gdb.Raw(`SELECT COUNT(email_verified_at) FROM users`).Row().Scan(&proven); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if proven != 1 {
|
|
t.Fatalf("rows with proof = %d, want only the imported one", proven)
|
|
}
|
|
})
|
|
|
|
t.Run("rollback_keeps_column", func(t *testing.T) {
|
|
gdb, m := emailVerifiedDB(t, "user_email_verified_rollback")
|
|
if err := m.Migrate(); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := m.RollbackMigration(usersEmailVerifiedAtMigrations[0]); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if !gdb.Migrator().HasColumn("users", "email_verified_at") {
|
|
t.Fatal("the rollback dropped the shared users column")
|
|
}
|
|
})
|
|
|
|
t.Run("history_once", func(t *testing.T) {
|
|
n := 0
|
|
for _, mig := range All() {
|
|
if mig.ID == emailVerifiedAtID {
|
|
n++
|
|
}
|
|
}
|
|
if n != 1 || len(usersEmailVerifiedAtMigrations) != 1 || usersEmailVerifiedAtMigrations[0].ID != emailVerifiedAtID {
|
|
t.Fatalf("%s registered %d times", emailVerifiedAtID, n)
|
|
}
|
|
})
|
|
}
|