package updates import ( "testing" "git.golem15.com/golem15/summercms/modules/lagoon" "github.com/go-gormigrate/gormigrate/v2" ) // TestAdminColumnsMigrations applies the three admin migrations on a real // Postgres, checks what each one adds, rolls each one back on its own and // applies them again. func TestAdminColumnsMigrations(t *testing.T) { sqlDB := dedicatedDB(t, "user_admin_columns") gdb, err := lagoon.Use(t.Context(), sqlDB) if err != nil { t.Fatal(err) } m := gormigrate.New(gdb, &gormigrate.Options{ TableName: "summer_migrations_golem15_user", UseTransaction: true, }, All()) if err := m.Migrate(); err != nil { t.Fatal(err) } columnType := func(table, column string) (string, string) { t.Helper() var row struct { DataType string IsNullable string } if err := gdb.Raw(`SELECT data_type, is_nullable FROM information_schema.columns WHERE table_name = ? AND column_name = ?`, table, column).Scan(&row).Error; err != nil { t.Fatal(err) } return row.DataType, row.IsNullable } if typ, null := columnType("users", "permissions"); typ != "text" || null != "YES" { t.Fatalf("users.permissions = %q nullable %q", typ, null) } if typ, null := columnType("users", "last_seen"); typ != "timestamp with time zone" || null != "YES" { t.Fatalf("users.last_seen = %q nullable %q", typ, null) } const table = "golem15_user_frontend_permissions" want := map[string][2]string{ "id": {"integer", "NO"}, "code": {"character varying", "NO"}, "label": {"character varying", "NO"}, "tab": {"character varying", "NO"}, "comment": {"character varying", "YES"}, "created_at": {"timestamp with time zone", "YES"}, "updated_at": {"timestamp with time zone", "YES"}, } for column, shape := range want { if typ, null := columnType(table, column); typ != shape[0] || null != shape[1] { t.Fatalf("%s.%s = %q nullable %q", table, column, typ, null) } } insert := `INSERT INTO golem15_user_frontend_permissions (code, label, tab) VALUES (?, ?, ?)` if err := gdb.Exec(insert, "blog.post", "Post", "Blog").Error; err != nil { t.Fatal(err) } if err := gdb.Exec(insert, "blog.post", "Post again", "Blog").Error; err == nil { t.Fatal("a duplicate permission code was accepted") } // An existing user keeps working with both columns empty. if err := gdb.Exec(`INSERT INTO users (email) VALUES ('columns@example.test')`).Error; err != nil { t.Fatal(err) } var empty int64 if err := gdb.Raw(`SELECT COUNT(*) FROM users WHERE permissions IS NULL AND last_seen IS NULL`).Scan(&empty).Error; err != nil || empty != 1 { t.Fatalf("users with empty admin columns = %d err = %v", empty, err) } if err := m.RollbackMigration(frontendPermissionsMigrations[0]); err != nil { t.Fatal(err) } if gdb.Migrator().HasTable(table) { t.Fatalf("%s survived rollback", table) } if err := m.RollbackMigration(usersLastSeenMigrations[0]); err != nil { t.Fatal(err) } if gdb.Migrator().HasColumn("users", "last_seen") || !gdb.Migrator().HasColumn("users", "permissions") { t.Fatal("last_seen rollback touched the wrong column") } if err := m.RollbackMigration(usersPermissionsMigrations[0]); err != nil { t.Fatal(err) } if gdb.Migrator().HasColumn("users", "permissions") { t.Fatal("users.permissions survived rollback") } for _, kept := range []string{"users", "user_groups", "user_throttle", "user_api_tokens"} { if !gdb.Migrator().HasTable(kept) { t.Fatalf("rollback dropped %s", kept) } } var users int64 if err := gdb.Raw(`SELECT COUNT(*) FROM users`).Scan(&users).Error; err != nil || users != 1 { t.Fatalf("users after rollback = %d err = %v", users, err) } if err := m.Migrate(); err != nil { t.Fatalf("re-up: %v", err) } if !gdb.Migrator().HasTable(table) || !gdb.Migrator().HasColumn("users", "permissions") || !gdb.Migrator().HasColumn("users", "last_seen") { t.Fatal("re-up did not restore the admin columns") } } // TestAdminColumnsHistory: the three admin migrations are part of the // plugin's history under their own ids, each id once, and applying the whole // history twice changes nothing. func TestAdminColumnsHistory(t *testing.T) { sqlDB := dedicatedDB(t, "user_admin_history") gdb, err := lagoon.Use(t.Context(), sqlDB) if err != nil { t.Fatal(err) } seen := map[string]int{} for _, migration := range All() { seen[migration.ID]++ if migration.Migrate == nil || migration.Rollback == nil { t.Fatalf("migration %s lacks an up or a down", migration.ID) } } admin := []string{usersPermissionsMigrations[0].ID, usersLastSeenMigrations[0].ID, frontendPermissionsMigrations[0].ID} for _, id := range admin { if seen[id] != 1 { t.Fatalf("migration %s is in the history %d times", id, seen[id]) } } for id, n := range seen { if n != 1 { t.Fatalf("migration id %s is used %d times", id, n) } } m := gormigrate.New(gdb, &gormigrate.Options{ TableName: "summer_migrations_golem15_user", UseTransaction: true, }, All()) for range 2 { if err := m.Migrate(); err != nil { t.Fatal(err) } } var applied int64 if err := gdb.Raw(`SELECT COUNT(*) FROM summer_migrations_golem15_user WHERE id IN ?`, admin).Scan(&applied).Error; err != nil || applied != 3 { t.Fatalf("applied admin migrations = %d err = %v", applied, err) } // The permission code stays unique after a down and an up of its table. if err := m.RollbackMigration(frontendPermissionsMigrations[0]); err != nil { t.Fatal(err) } if err := m.Migrate(); err != nil { t.Fatal(err) } insert := `INSERT INTO golem15_user_frontend_permissions (code, label, tab) VALUES (?, ?, ?)` if err := gdb.Exec(insert, "history.code", "One", "Tab").Error; err != nil { t.Fatal(err) } if err := gdb.Exec(insert, "history.code", "Two", "Other").Error; err == nil { t.Fatal("a duplicate permission code was accepted after the re-up") } if err := gdb.Exec(insert, "History.Code", "Three", "Tab").Error; err != nil { t.Fatalf("codes differing by case are distinct: %v", err) } }