package controllers import ( "context" "errors" "slices" "strconv" "testing" "time" "git.golem15.com/golem15/summercms/modules/cabana" "git.golem15.com/jakub/sm-bm-plugin/models" "gorm.io/gorm" ) // recordingIndex records the after-commit calls of the materials admin. type recordingIndex struct { calls []string txs []*gorm.DB } func (r *recordingIndex) SyncAfterCommit(_ context.Context, tx *gorm.DB, id uint) { r.calls = append(r.calls, "sync "+itoa(id)) r.txs = append(r.txs, tx) } func (r *recordingIndex) RemoveAfterCommit(_ context.Context, tx *gorm.DB, id uint) { r.calls = append(r.calls, "remove "+itoa(id)) r.txs = append(r.txs, tx) } func (r *recordingIndex) QueueExtract(_ context.Context, tx *gorm.DB, id uint) { r.calls = append(r.calls, "queue "+itoa(id)) r.txs = append(r.txs, tx) } func itoa(id uint) string { return strconv.FormatUint(uint64(id), 10) } func TestMaterialsAdminHooks(t *testing.T) { t.Cleanup(func() { UseMaterialIndex(nil) }) var c materialsAdmin ctx := context.Background() m := &models.Material{ID: 7} // No index installed: nothing to do, and no error. UseMaterialIndex(nil) for name, fn := range map[string]func(context.Context, any) error{"create": c.FormAfterCreate, "update": c.FormAfterUpdate, "delete": c.FormAfterDelete} { if err := fn(ctx, m); err != nil { t.Errorf("%s without an index: %v", name, err) } } rec := &recordingIndex{} UseMaterialIndex(rec) if err := c.FormAfterCreate(ctx, m); err != nil { t.Fatal(err) } if err := c.FormAfterUpdate(ctx, m); err != nil { t.Fatal(err) } if err := c.FormAfterDelete(ctx, m); err != nil { t.Fatal(err) } // Create and update queue the file text job (in the save transaction) // before the after-commit sync; delete queues nothing. if want := []string{"queue 7", "sync 7", "queue 7", "sync 7", "remove 7"}; !slices.Equal(rec.calls, want) { t.Errorf("calls = %v, want %v", rec.calls, want) } // Outside a cabana write there is no transaction: the index then runs // at once (lagoon.AfterCommit with a nil handle). for i, tx := range rec.txs { if tx != nil { t.Errorf("call %d got a transaction outside a write", i) } } // A model of another type is an error and schedules nothing. rec.calls = nil for name, fn := range map[string]func(context.Context, any) error{"create": c.FormAfterCreate, "update": c.FormAfterUpdate, "delete": c.FormAfterDelete} { if err := fn(ctx, &models.Course{ID: 7}); err == nil { t.Errorf("%s with a course: no error", name) } } var nilMaterial *models.Material if err := c.FormAfterUpdate(ctx, nilMaterial); err == nil { t.Error("update with a nil material: no error") } if len(rec.calls) != 0 { t.Errorf("calls for a wrong model = %v", rec.calls) } } // TestMaterialsAdminRevision checks the revision hooks of the materials // admin outside a database write: without a transaction they do nothing and // store no snapshot, a model that is not a material is an error, and the // embedded date check still runs first. func TestMaterialsAdminRevision(t *testing.T) { t.Cleanup(func() { UseMaterialIndex(nil) }) UseMaterialIndex(nil) var c materialsAdmin ctx := context.Background() m := &models.Material{ID: 41, Title: "Notatka", Type: "notatka"} if err := c.FormBeforeUpdate(ctx, m); err != nil { t.Errorf("before update without a transaction: %v", err) } if _, found := pendingRevisions.Load(m.ID); found { t.Error("before update without a transaction stored a snapshot") } if err := c.FormAfterUpdate(ctx, m); err != nil { t.Errorf("after update without a transaction: %v", err) } if err := c.FormBeforeUpdate(ctx, &models.Course{ID: 41}); err == nil { t.Error("before update with a course: no error") } var nilMaterial *models.Material if err := c.FormBeforeUpdate(ctx, nilMaterial); err == nil { t.Error("before update with a nil material: no error") } at := func(h, m int) time.Time { return time.Date(2026, 10, 3, h, m, 0, 0, models.Location) } bad := &models.ClassSession{StartsAt: at(8, 0), EndsAt: at(7, 0)} var ve *cabana.ValidationError if err := c.FormBeforeUpdate(ctx, bad); !errors.As(err, &ve) { t.Errorf("before update with bad dates: err = %v, want a cabana.ValidationError", err) } else if _, ok := ve.Details["ends_at"]; !ok { t.Errorf("before update with bad dates: details %v name no ends_at", ve.Details) } }