package lagoon import ( "bytes" "errors" "io" "os" "strings" "testing" "git.golem15.com/golem15/summercms/modules/backpack" "git.golem15.com/golem15/summercms/modules/bonfire" "git.golem15.com/golem15/summercms/modules/pact" "git.golem15.com/golem15/summercms/modules/party" "github.com/go-gormigrate/gormigrate/v2" "gorm.io/gorm" ) func TestHistoryTableName(t *testing.T) { got, err := HistoryTableName("golem15.user") if err != nil { t.Fatal(err) } if got != "summer_migrations_golem15_user" { t.Fatalf("got %q", got) } got, err = HistoryTableName("golem15.fonoteka") if err != nil { t.Fatal(err) } if got != "summer_migrations_golem15_fonoteka" { t.Fatalf("got %q", got) } if _, err := HistoryTableName("golem15.user;drop"); err == nil { t.Fatal("want invalid plugin id error") } if _, err := HistoryTableName(""); err == nil { t.Fatal("want empty plugin id error") } } func TestRuntimeCommandsRegisterBareAndColonNames(t *testing.T) { cmds := RuntimeCommands(nil, nil) names := map[string]bool{} for _, c := range cmds { names[c.Name] = true } for _, want := range []string{"migrate", "migrate:rollback", "migrate:status", "key:generate"} { if !names[want] { t.Fatalf("missing %s", want) } } root, err := bonfire.NewRoot("app", cmds, io.Discard) if err != nil { t.Fatal(err) } root.SetArgs([]string{"migrate", "--help"}) if err := root.Execute(); err != nil { t.Fatal(err) } } func TestNoAutoMigrate(t *testing.T) { entries, err := os.ReadDir(".") if err != nil { t.Fatal(err) } for _, entry := range entries { if entry.IsDir() || !strings.HasSuffix(entry.Name(), ".go") || strings.HasSuffix(entry.Name(), "_test.go") { continue } body, err := os.ReadFile(entry.Name()) if err != nil { t.Fatal(err) } if bytes.Contains(body, []byte("AutoMigrate")) { t.Fatalf("%s must not call AutoMigrate", entry.Name()) } } } func TestOpenRequiresDSN(t *testing.T) { _, _, err := Open(t.Context(), " ") if err == nil || !strings.Contains(err.Error(), "database.dsn") { t.Fatalf("got %v", err) } } type stubPlugin struct{ id string } func (s stubPlugin) ID() string { return s.id } func (s stubPlugin) Requires() []string { return nil } func (s stubPlugin) Register(*backpack.App) error { return nil } func (s stubPlugin) Boot(*backpack.App) error { return nil } func TestRollbackUnknownPluginIsNamedError(t *testing.T) { err := RollbackLast(&gorm.DB{}, []party.Plugin{stubPlugin{id: "golem15.user"}}, "golem15.missing") if !errors.Is(err, ErrUnknownPlugin) { t.Fatalf("got %v", err) } if !strings.Contains(err.Error(), "golem15.missing") { t.Fatalf("error should name plugin: %v", err) } } func TestRollbackMissingPluginIsNamedError(t *testing.T) { err := RollbackLast(&gorm.DB{}, nil, "") if !errors.Is(err, ErrNoMigrations) { t.Fatalf("got %v", err) } } type migPlugin struct { id string migrations []*gormigrate.Migration } func (p migPlugin) ID() string { return p.id } func (p migPlugin) Requires() []string { return nil } func (p migPlugin) Register(*backpack.App) error { return nil } func (p migPlugin) Boot(*backpack.App) error { return nil } func (p migPlugin) Migrations() []*gormigrate.Migration { return p.migrations } func TestMigrateRunsSystemFilesFirst(t *testing.T) { db, _ := dedicatedDB(t, "lagoon_attach_first") gdb, err := Use(t.Context(), db) if err != nil { t.Fatal(err) } if err := Migrate(gdb, nil); err != nil { t.Fatal(err) } if !gdb.Migrator().HasTable("system_files") { t.Fatal("system_files must exist after Migrate with an empty plugin list") } } func TestTwoPluginMigrationSetsIsolated(t *testing.T) { db, _ := dedicatedDB(t, "lagoon_mig_iso") gdb, err := Use(t.Context(), db) if err != nil { t.Fatal(err) } alpha := migPlugin{ id: "demo.alpha", migrations: []*gormigrate.Migration{{ ID: "202609170001_create_alpha", Migrate: func(tx *gorm.DB) error { return tx.Exec(`CREATE TABLE lagoon_alpha (id BIGSERIAL PRIMARY KEY, name TEXT NOT NULL)`).Error }, Rollback: func(tx *gorm.DB) error { return tx.Exec(`DROP TABLE IF EXISTS lagoon_alpha`).Error }, }}, } beta := migPlugin{ id: "demo.beta", migrations: []*gormigrate.Migration{ { ID: "202609170001_create_beta", Migrate: func(tx *gorm.DB) error { return tx.Exec(`CREATE TABLE lagoon_beta (id BIGSERIAL PRIMARY KEY, name TEXT NOT NULL)`).Error }, Rollback: func(tx *gorm.DB) error { return tx.Exec(`DROP TABLE IF EXISTS lagoon_beta`).Error }, }, { ID: "202609170002_seed_beta", Migrate: func(tx *gorm.DB) error { return tx.Exec(`INSERT INTO lagoon_beta (name) VALUES ('seed')`).Error }, Rollback: func(tx *gorm.DB) error { return tx.Exec(`DELETE FROM lagoon_beta WHERE name = 'seed'`).Error }, }, }, } plugins := []party.Plugin{alpha, beta} if err := Migrate(gdb, plugins); err != nil { t.Fatal(err) } if err := Migrate(gdb, plugins); err != nil { t.Fatalf("idempotent migrate: %v", err) } status, err := Status(gdb, plugins) if err != nil { t.Fatal(err) } if len(status) != 2 { t.Fatalf("status rows = %+v", status) } if status[0].Plugin != "demo.alpha" || status[0].Table != "summer_migrations_demo_alpha" || strings.Join(status[0].IDs, ",") != "202609170001_create_alpha" { t.Fatalf("alpha status = %+v", status[0]) } if status[1].Plugin != "demo.beta" || status[1].Table != "summer_migrations_demo_beta" || strings.Join(status[1].IDs, ",") != "202609170001_create_beta,202609170002_seed_beta" { t.Fatalf("beta status = %+v", status[1]) } if !gdb.Migrator().HasTable("lagoon_alpha") || !gdb.Migrator().HasTable("lagoon_beta") { t.Fatal("both plugin tables must exist after migrate") } var seedName string if err := gdb.Raw(`SELECT name FROM lagoon_beta`).Scan(&seedName).Error; err != nil { t.Fatal(err) } if seedName != "seed" { t.Fatalf("seed = %q", seedName) } if err := RollbackLast(gdb, plugins, "demo.beta"); err != nil { t.Fatal(err) } var n int64 if err := gdb.Table("lagoon_beta").Count(&n).Error; err != nil { t.Fatal(err) } if n != 0 { t.Fatalf("beta seed rollback left %d rows", n) } if !gdb.Migrator().HasTable("lagoon_alpha") || !gdb.Migrator().HasTable("lagoon_beta") { t.Fatal("schema rollback must not run on the first beta RollbackLast") } status, err = Status(gdb, plugins) if err != nil { t.Fatal(err) } if strings.Join(status[0].IDs, ",") != "202609170001_create_alpha" { t.Fatalf("alpha history changed: %+v", status[0]) } if strings.Join(status[1].IDs, ",") != "202609170001_create_beta" { t.Fatalf("beta history = %+v", status[1]) } if err := RollbackLast(gdb, plugins, "demo.beta"); err != nil { t.Fatal(err) } if gdb.Migrator().HasTable("lagoon_beta") { t.Fatal("second beta rollback must drop lagoon_beta") } if !gdb.Migrator().HasTable("lagoon_alpha") { t.Fatal("alpha table must survive beta rollback") } status, err = Status(gdb, plugins) if err != nil { t.Fatal(err) } if len(status[0].IDs) != 1 { t.Fatalf("alpha history after beta down = %+v", status[0]) } if len(status[1].IDs) != 0 { t.Fatalf("beta history should be empty, got %+v", status[1]) } } var _ pact.HasMigrations = migPlugin{}