package lagoon import ( "database/sql" "errors" "testing" "git.golem15.com/golem15/summercms/modules/backpack" "gorm.io/gorm" ) // TestOnDatabaseAfterActivate covers the production boot order: plugins // register database hooks at Boot, before serve publishes the database. func TestOnDatabaseAfterActivate(t *testing.T) { db, _ := dedicatedDB(t, "lagoon_ondatabase") gdb, err := Use(t.Context(), db) if err != nil { t.Fatal(err) } t.Run("before_publish_runs_on_publish", func(t *testing.T) { app := backpack.New(nil) calls := 0 var gotSQL *sql.DB var gotGDB *gorm.DB if err := OnDatabase(app, func(s *sql.DB, g *gorm.DB) error { calls++ gotSQL, gotGDB = s, g return nil }); err != nil { t.Fatal(err) } if calls != 0 { t.Fatalf("hook ran %d times before Publish", calls) } if err := Publish(app, db, gdb); err != nil { t.Fatal(err) } if calls != 1 || gotSQL != db || gotGDB != gdb { t.Fatalf("after Publish: calls=%d sql=%v gdb=%v", calls, gotSQL == db, gotGDB == gdb) } }) t.Run("after_publish_runs_now", func(t *testing.T) { app := backpack.New(nil) if err := Publish(app, db, gdb); err != nil { t.Fatal(err) } calls := 0 if err := OnDatabase(app, func(*sql.DB, *gorm.DB) error { calls++; return nil }); err != nil { t.Fatal(err) } if calls != 1 { t.Fatalf("hook ran %d times, want 1", calls) } }) t.Run("gorm_only_published_runs_now", func(t *testing.T) { app := backpack.New(nil) if err := app.Publish(gdb); err != nil { t.Fatal(err) } var gotSQL *sql.DB if err := OnDatabase(app, func(s *sql.DB, _ *gorm.DB) error { gotSQL = s; return nil }); err != nil { t.Fatal(err) } if gotSQL != db { t.Fatal("hook did not receive the pool behind the published *gorm.DB") } }) t.Run("errors_surface", func(t *testing.T) { boom := errors.New("boom") app := backpack.New(nil) if err := OnDatabase(app, func(*sql.DB, *gorm.DB) error { return boom }); err != nil { t.Fatal(err) } if err := Publish(app, db, gdb); !errors.Is(err, boom) { t.Fatalf("Publish err = %v, want the hook error", err) } if err := OnDatabase(app, func(*sql.DB, *gorm.DB) error { return boom }); !errors.Is(err, boom) { t.Fatalf("OnDatabase err = %v, want the hook error", err) } }) } // TestOnDatabaseIsolationAndErrors covers the per-app hook queue: two apps // keep separate queues, a published app runs later hooks at once, and nil // arguments are refused. func TestOnDatabaseIsolationAndErrors(t *testing.T) { db, _ := dedicatedDB(t, "lagoon_ondatabase_isolation") gdb, err := Use(t.Context(), db) if err != nil { t.Fatal(err) } a, b := backpack.New(nil), backpack.New(nil) var ranA, ranB int if err := OnDatabase(a, func(*sql.DB, *gorm.DB) error { ranA++; return nil }); err != nil { t.Fatal(err) } if err := OnDatabase(b, func(*sql.DB, *gorm.DB) error { ranB++; return nil }); err != nil { t.Fatal(err) } if err := Publish(a, db, gdb); err != nil { t.Fatal(err) } if ranA != 1 || ranB != 0 { t.Fatalf("after publishing app a: a ran %d, b ran %d; want 1 and 0", ranA, ranB) } if err := Publish(b, db, gdb); err != nil { t.Fatal(err) } if ranA != 1 || ranB != 1 { t.Fatalf("after publishing app b: a ran %d, b ran %d; want 1 and 1", ranA, ranB) } if err := OnDatabase(nil, func(*sql.DB, *gorm.DB) error { return nil }); err == nil { t.Fatal("nil app accepted") } if err := OnDatabase(a, nil); err == nil { t.Fatal("nil hook accepted") } // A second failing queued hook is never reached: Publish returns the // first error. c := backpack.New(nil) boom := errors.New("first") second := false _ = OnDatabase(c, func(*sql.DB, *gorm.DB) error { return boom }) _ = OnDatabase(c, func(*sql.DB, *gorm.DB) error { second = true; return nil }) if err := Publish(c, db, gdb); !errors.Is(err, boom) || second { t.Fatalf("Publish = %v (second ran %v), want the first error only", err, second) } }