Files
sm-user-plugin/updates/email_verified_at_test.go
Jakub Zych 35cf16689d test: email_verified_at writers, non-writers and the migration
- 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
2026-10-21 14:01:52 +02:00

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)
}
})
}