feat(32-02): record events from every admin write path
- Delete and each bulk-delete row fire RecordDeleted after commit - tree reorder fires RecordSaved per moved record (parent/sort in model types) - relation-manager child create/update/delete fire events naming the controller that edits the child's model - TestRecordEvents covers create, update, translated slug, no-op, rollback (validation, FormBeforeUpdate, extension Save), failing listener, reorder, child writes, delete and bulk delete; extension Save sees the changes - docs: Record events in extending.md, admin-controllers notes, cabana README
This commit is contained in:
506
modules/cabana/record_events_postgres_test.go
Normal file
506
modules/cabana/record_events_postgres_test.go
Normal file
@@ -0,0 +1,506 @@
|
||||
package cabana
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io/fs"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"reflect"
|
||||
"strconv"
|
||||
"sync"
|
||||
"testing"
|
||||
"testing/fstest"
|
||||
"time"
|
||||
|
||||
"git.golem15.com/golem15/summercms/modules/backpack"
|
||||
"git.golem15.com/golem15/summercms/modules/bouncer"
|
||||
"git.golem15.com/golem15/summercms/modules/pact"
|
||||
"git.golem15.com/golem15/summercms/modules/towel"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// The D-92 record events on a real Postgres database: an acme.events plugin
|
||||
// with a pages controller (translatable slug, a tree reorder page and a
|
||||
// notes relation manager), a notes controller, and a contributor whose form
|
||||
// extension sees the save's changes.
|
||||
|
||||
const rePageMorph = `Acme\Events\Page`
|
||||
|
||||
type rePage struct {
|
||||
ID uint `gorm:"column:id;primaryKey"`
|
||||
Title string `gorm:"column:title"`
|
||||
Slug string `gorm:"column:slug"`
|
||||
ParentID *uint `gorm:"column:parent_id"`
|
||||
SortOrder int `gorm:"column:sort_order"`
|
||||
UpdatedAt *time.Time `gorm:"column:updated_at"`
|
||||
Notes []reNote `gorm:"-"`
|
||||
}
|
||||
|
||||
func (rePage) TableName() string { return "cabana_re_pages" }
|
||||
func (rePage) MorphName() string { return rePageMorph }
|
||||
func (*rePage) Fillable() []string { return []string{"title", "slug"} }
|
||||
func (*rePage) Rules() map[string]string { return map[string]string{"title": "required"} }
|
||||
|
||||
type reNote struct {
|
||||
ID uint `gorm:"column:id;primaryKey"`
|
||||
PageID *uint `gorm:"column:page_id"`
|
||||
Body string `gorm:"column:body"`
|
||||
}
|
||||
|
||||
func (reNote) TableName() string { return "cabana_re_notes" }
|
||||
func (*reNote) Fillable() []string { return []string{"body"} }
|
||||
func (*reNote) Rules() map[string]string { return map[string]string{} }
|
||||
|
||||
// rePages is the pages controller; failUpdate makes FormBeforeUpdate refuse.
|
||||
type rePages struct{ failUpdate *bool }
|
||||
|
||||
func (rePages) ID() string { return "acme.events.pages" }
|
||||
func (rePages) ModelName() string { return "Page" }
|
||||
func (rePages) ConfigDir() string { return "controllers/pages" }
|
||||
func (rePages) NewRecord() any { return &rePage{} }
|
||||
func (rePages) FormExtendQuery(_ context.Context, q *gorm.DB) *gorm.DB { return q }
|
||||
func (c rePages) FormBeforeUpdate(context.Context, any) error {
|
||||
if c.failUpdate != nil && *c.failUpdate {
|
||||
return errors.New("refused")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
func (rePages) AdminRelationContracts() []RelationContract {
|
||||
return []RelationContract{{
|
||||
Name: "notes",
|
||||
Kind: RelationHasMany,
|
||||
NewRelated: func() any { return &reNote{} },
|
||||
ForeignKey: "page_id",
|
||||
Columns: map[string]string{"body": "body"},
|
||||
}}
|
||||
}
|
||||
|
||||
type reNotes struct{}
|
||||
|
||||
func (reNotes) ID() string { return "acme.events.notes" }
|
||||
func (reNotes) ModelName() string { return "Note" }
|
||||
func (reNotes) ConfigDir() string { return "controllers/notes" }
|
||||
func (reNotes) NewRecord() any { return &reNote{} }
|
||||
|
||||
type rePlugin struct{ failUpdate *bool }
|
||||
|
||||
func (rePlugin) ID() string { return "acme.events" }
|
||||
func (rePlugin) Requires() []string { return nil }
|
||||
func (rePlugin) Register(*backpack.App) error { return nil }
|
||||
func (rePlugin) Boot(*backpack.App) error { return nil }
|
||||
func (rePlugin) AdminFS() fs.FS { return reFS() }
|
||||
func (p rePlugin) AdminControllers() []pact.AdminController {
|
||||
return []pact.AdminController{rePages{failUpdate: p.failUpdate}, reNotes{}}
|
||||
}
|
||||
|
||||
var (
|
||||
_ pact.FormBeforeUpdate = rePages{}
|
||||
_ AdminRelationContractProvider = rePages{}
|
||||
)
|
||||
|
||||
func reFS() fstest.MapFS {
|
||||
file := func(s string) *fstest.MapFile { return &fstest.MapFile{Data: []byte(s)} }
|
||||
return fstest.MapFS{
|
||||
"controllers/pages/config_form.yaml": file("name: Page\nform: ~/plugins/acme/events/models/page/fields.yaml\nmodelClass: Page\n"),
|
||||
"controllers/pages/config_list.yaml": file("list: ~/plugins/acme/events/models/page/columns.yaml\nmodelClass: Page\nrecordsPerPage: 20\ntoolbar:\n buttons: [create, reorder]\n"),
|
||||
"controllers/pages/config_reorder.yaml": file("title: Reorder pages\nnameFrom: title\nparentColumn: parent_id\nsortColumn: sort_order\n"),
|
||||
"controllers/pages/config_relation.yaml": file("notes:\n label: Notes\n view:\n list:\n columns:\n body:\n label: Body\n toolbarButtons: create|update|delete\n manage:\n form: $/acme/events/models/note/fields.yaml\n"),
|
||||
"models/page/columns.yaml": file("columns:\n title:\n label: Title\n"),
|
||||
"models/page/fields.yaml": file("fields:\n title:\n type: text\n label: Title\n slug:\n type: mltext\n label: Slug\n notes:\n type: relation-manager\n relation: notes\n"),
|
||||
"controllers/notes/config_form.yaml": file("name: Note\nform: ~/plugins/acme/events/models/note/fields.yaml\nmodelClass: Note\n"),
|
||||
"controllers/notes/config_list.yaml": file("list: ~/plugins/acme/events/models/note/columns.yaml\nmodelClass: Note\nrecordsPerPage: 20\n"),
|
||||
"models/note/columns.yaml": file("columns:\n body:\n label: Body\n"),
|
||||
"models/note/fields.yaml": file("fields:\n body:\n type: text\n label: Body\n"),
|
||||
}
|
||||
}
|
||||
|
||||
// reWriter stores translations in memory per record, field and locale.
|
||||
type reWriter struct {
|
||||
mu sync.Mutex
|
||||
data map[string]string
|
||||
}
|
||||
|
||||
func reKey(model any, field, locale string) string {
|
||||
return fmt.Sprintf("%d/%s/%s", pkUint(model), field, locale)
|
||||
}
|
||||
|
||||
func (*reWriter) DefaultLocale(context.Context, *gorm.DB) (string, error) { return "en", nil }
|
||||
func (*reWriter) EnabledLocales(context.Context, *gorm.DB) ([]string, error) {
|
||||
return []string{"en", "pl"}, nil
|
||||
}
|
||||
func (w *reWriter) WriteTranslated(_ context.Context, _ *gorm.DB, model any, field, locale, value string) error {
|
||||
w.mu.Lock()
|
||||
defer w.mu.Unlock()
|
||||
if w.data == nil {
|
||||
w.data = map[string]string{}
|
||||
}
|
||||
w.data[reKey(model, field, locale)] = value
|
||||
return nil
|
||||
}
|
||||
func (w *reWriter) TranslatedExact(_ context.Context, _ *gorm.DB, model any, field, locale string) (string, bool, error) {
|
||||
w.mu.Lock()
|
||||
defer w.mu.Unlock()
|
||||
text, ok := w.data[reKey(model, field, locale)]
|
||||
return text, ok, nil
|
||||
}
|
||||
|
||||
// reListener records every record event the bus delivers.
|
||||
type reListener struct {
|
||||
mu sync.Mutex
|
||||
saved []pact.RecordSaved
|
||||
deleted []pact.RecordDeleted
|
||||
}
|
||||
|
||||
func (l *reListener) take() ([]pact.RecordSaved, []pact.RecordDeleted) {
|
||||
l.mu.Lock()
|
||||
defer l.mu.Unlock()
|
||||
saved, deleted := l.saved, l.deleted
|
||||
l.saved, l.deleted = nil, nil
|
||||
return saved, deleted
|
||||
}
|
||||
|
||||
type reEnv struct {
|
||||
db *gorm.DB
|
||||
svc *service
|
||||
listener *reListener
|
||||
extFail *error
|
||||
extSaves *[]pact.AdminFormSave
|
||||
}
|
||||
|
||||
func newREEnv(t *testing.T, failUpdate *bool) reEnv {
|
||||
t.Helper()
|
||||
_, db := newListService(t)
|
||||
if err := db.Migrator().DropTable(&rePage{}, &reNote{}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := db.AutoMigrate(&rePage{}, &reNote{}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
t.Cleanup(func() { _ = db.Migrator().DropTable(&rePage{}, &reNote{}) })
|
||||
var (
|
||||
extFail error
|
||||
extSaves []pact.AdminFormSave
|
||||
)
|
||||
extra := feContributor{
|
||||
id: "acme.extra",
|
||||
fsys: feExtraFS("fields:\n extra:\n type: text\n label: Extra\n"),
|
||||
extensions: []pact.AdminActionExtension{{
|
||||
Controller: "acme.events.pages",
|
||||
Form: &pact.AdminFormExtension{Fields: "extensions/posts.yaml", Save: func(_ context.Context, in pact.AdminFormSave) (pact.AdminFormSaveResult, error) {
|
||||
extSaves = append(extSaves, in)
|
||||
return pact.AdminFormSaveResult{}, extFail
|
||||
}},
|
||||
}},
|
||||
}
|
||||
reg, err := feCompile(rePlugin{failUpdate: failUpdate}, extra)
|
||||
if err != nil {
|
||||
t.Fatalf("compile: %v", err)
|
||||
}
|
||||
app := backpack.New(nil)
|
||||
if err := app.Publish(db); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := app.Publish[TranslationWriter](&reWriter{}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
l := &reListener{}
|
||||
app.Events.Listen("test.records", func(_ context.Context, ev *pact.RecordSaved) error {
|
||||
l.mu.Lock()
|
||||
defer l.mu.Unlock()
|
||||
l.saved = append(l.saved, *ev)
|
||||
return nil
|
||||
})
|
||||
app.Events.Listen("test.records", func(_ context.Context, ev *pact.RecordDeleted) error {
|
||||
l.mu.Lock()
|
||||
defer l.mu.Unlock()
|
||||
l.deleted = append(l.deleted, *ev)
|
||||
return nil
|
||||
})
|
||||
return reEnv{db: db, svc: &service{app: app, reg: reg}, listener: l, extFail: &extFail, extSaves: &extSaves}
|
||||
}
|
||||
|
||||
func (e reEnv) pages(t *testing.T) *CompiledController {
|
||||
t.Helper()
|
||||
cc, ok := e.svc.reg.Get("acme.events.pages")
|
||||
if !ok {
|
||||
t.Fatal("pages controller missing")
|
||||
}
|
||||
return cc
|
||||
}
|
||||
|
||||
func (e reEnv) crud(t *testing.T) CRUDService {
|
||||
t.Helper()
|
||||
svc, err := e.svc.crud()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
return svc
|
||||
}
|
||||
|
||||
func (e reEnv) relations(t *testing.T) RelationService {
|
||||
t.Helper()
|
||||
svc, err := e.svc.relations()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
return svc
|
||||
}
|
||||
|
||||
// reCtx is an administrator's request context with the content locale pl.
|
||||
func reCtx() context.Context {
|
||||
return towel.WithLocale(bouncer.WithUser(context.Background(), superUser()), "pl")
|
||||
}
|
||||
|
||||
func reID(t *testing.T, data map[string]any) uint {
|
||||
t.Helper()
|
||||
switch v := data["id"].(type) {
|
||||
case uint:
|
||||
return v
|
||||
case float64:
|
||||
return uint(v)
|
||||
case json.Number:
|
||||
n, _ := v.Int64()
|
||||
return uint(n)
|
||||
}
|
||||
n, err := strconv.ParseUint(fmt.Sprint(data["id"]), 10, 64)
|
||||
if err != nil {
|
||||
t.Fatalf("record id %v: %v", data["id"], err)
|
||||
}
|
||||
return uint(n)
|
||||
}
|
||||
|
||||
func oneSaved(t *testing.T, l *reListener) pact.RecordSaved {
|
||||
t.Helper()
|
||||
saved, deleted := l.take()
|
||||
if len(saved) != 1 || len(deleted) != 0 {
|
||||
t.Fatalf("events: saved %+v deleted %+v, want one RecordSaved", saved, deleted)
|
||||
}
|
||||
return saved[0]
|
||||
}
|
||||
|
||||
func noEvents(t *testing.T, l *reListener) {
|
||||
t.Helper()
|
||||
if saved, deleted := l.take(); len(saved) != 0 || len(deleted) != 0 {
|
||||
t.Fatalf("events after a refused write: saved %+v deleted %+v", saved, deleted)
|
||||
}
|
||||
}
|
||||
|
||||
// TestRecordEvents (D-92): every admin write path fires RecordSaved or
|
||||
// RecordDeleted after the commit, with the changes per locale, and a
|
||||
// rolled-back write fires nothing.
|
||||
func TestRecordEvents(t *testing.T) {
|
||||
failUpdate := false
|
||||
env := newREEnv(t, &failUpdate)
|
||||
cc := env.pages(t)
|
||||
crud := env.crud(t)
|
||||
ctx := reCtx()
|
||||
var homeID uint
|
||||
|
||||
t.Run("create", func(t *testing.T) {
|
||||
res, err := crud.Create(ctx, cc, RecordInput{Body: map[string]any{"title": "Home", "slug": map[string]any{"en": "home", "pl": "dom"}}})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
homeID = reID(t, res)
|
||||
ev := oneSaved(t, env.listener)
|
||||
want := pact.RecordSaved{
|
||||
Controller: "acme.events.pages", Model: rePageMorph, ID: strconv.FormatUint(uint64(homeID), 10), Locale: "pl", Created: true,
|
||||
Changes: []pact.RecordChange{
|
||||
{Attribute: "title", New: "Home"},
|
||||
{Attribute: "slug", New: "home"},
|
||||
{Attribute: "slug", Locale: "pl", New: "dom"},
|
||||
},
|
||||
}
|
||||
if !reflect.DeepEqual(ev, want) {
|
||||
t.Fatalf("event = %+v\nwant %+v", ev, want)
|
||||
}
|
||||
if saves := *env.extSaves; len(saves) != 1 || !reflect.DeepEqual(saves[0].Changes, want.Changes) {
|
||||
t.Fatalf("extension Save changes = %+v, want the event's", saves)
|
||||
}
|
||||
})
|
||||
|
||||
update := func(body map[string]any) (map[string]any, error) {
|
||||
return crud.Update(ctx, cc, homeID, RecordInput{Body: body})
|
||||
}
|
||||
|
||||
t.Run("update", func(t *testing.T) {
|
||||
*env.extSaves = nil
|
||||
if _, err := update(map[string]any{"title": "Start"}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
ev := oneSaved(t, env.listener)
|
||||
want := []pact.RecordChange{{Attribute: "title", Old: "Home", New: "Start"}}
|
||||
if ev.Created || !reflect.DeepEqual(ev.Changes, want) {
|
||||
t.Fatalf("event = %+v, want changes %+v", ev, want)
|
||||
}
|
||||
if saves := *env.extSaves; len(saves) != 1 || !reflect.DeepEqual(saves[0].Changes, want) {
|
||||
t.Fatalf("extension Save changes = %+v, want %+v", saves, want)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("translated slug", func(t *testing.T) {
|
||||
if _, err := update(map[string]any{"slug": map[string]any{"en": "home", "pl": "strona"}}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
ev := oneSaved(t, env.listener)
|
||||
want := []pact.RecordChange{{Attribute: "slug", Locale: "pl", Old: "dom", New: "strona"}}
|
||||
if !reflect.DeepEqual(ev.Changes, want) {
|
||||
t.Fatalf("changes = %+v, want %+v", ev.Changes, want)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("no-op update", func(t *testing.T) {
|
||||
if _, err := update(map[string]any{"title": "Start", "slug": map[string]any{"en": "home", "pl": "strona"}}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
ev := oneSaved(t, env.listener)
|
||||
if len(ev.Changes) != 0 || ev.ID != strconv.FormatUint(uint64(homeID), 10) {
|
||||
t.Fatalf("no-op event = %+v, want no changes", ev)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("rollback", func(t *testing.T) {
|
||||
if _, err := update(map[string]any{"title": ""}); err == nil {
|
||||
t.Fatal("empty title saved")
|
||||
}
|
||||
noEvents(t, env.listener)
|
||||
failUpdate = true
|
||||
if _, err := update(map[string]any{"title": "Refused"}); err == nil {
|
||||
t.Fatal("FormBeforeUpdate refusal saved")
|
||||
}
|
||||
failUpdate = false
|
||||
noEvents(t, env.listener)
|
||||
*env.extFail = pact.AdminFormErrors{"extra": {"bad"}}
|
||||
if _, err := update(map[string]any{"title": "Refused"}); err == nil {
|
||||
t.Fatal("extension Save refusal saved")
|
||||
}
|
||||
*env.extFail = nil
|
||||
noEvents(t, env.listener)
|
||||
var page rePage
|
||||
if err := env.db.First(&page, homeID).Error; err != nil || page.Title != "Start" {
|
||||
t.Fatalf("page after refusals = %+v err=%v", page, err)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("failing listener", func(t *testing.T) {
|
||||
env.svc.app.Events.Listen("test.failing", func(context.Context, *pact.RecordSaved) error {
|
||||
return errors.New("listener down")
|
||||
})
|
||||
if _, err := update(map[string]any{"title": "Again"}); err != nil {
|
||||
t.Fatalf("save failed with a failing listener: %v", err)
|
||||
}
|
||||
ev := oneSaved(t, env.listener)
|
||||
if len(ev.Changes) != 1 || ev.Changes[0].New != "Again" {
|
||||
t.Fatalf("event = %+v", ev)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("reorder", func(t *testing.T) {
|
||||
child := rePage{Title: "Child", Slug: "child", SortOrder: 2}
|
||||
if err := env.db.Create(&child).Error; err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := env.db.Model(&rePage{}).Where("id = ?", homeID).Update("sort_order", 1).Error; err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
body, _ := json.Marshal(map[string]any{"nodes": []map[string]any{
|
||||
{"id": homeID, "parent_id": nil, "sort_order": 1},
|
||||
{"id": child.ID, "parent_id": homeID, "sort_order": 1},
|
||||
}})
|
||||
req := httptest.NewRequest(http.MethodPost, "/", bytes.NewReader(body))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
req.SetPathValue("vendor", "acme")
|
||||
req.SetPathValue("plugin", "events")
|
||||
req.SetPathValue("controller", "pages")
|
||||
req = req.WithContext(bouncer.WithUser(req.Context(), superUser()))
|
||||
rec := httptest.NewRecorder()
|
||||
env.svc.reorderPost(rec, req)
|
||||
if rec.Code != http.StatusOK {
|
||||
t.Fatalf("reorder status=%d body=%s", rec.Code, rec.Body.String())
|
||||
}
|
||||
ev := oneSaved(t, env.listener)
|
||||
want := pact.RecordSaved{
|
||||
Controller: "acme.events.pages", Model: rePageMorph, ID: strconv.FormatUint(uint64(child.ID), 10),
|
||||
Changes: []pact.RecordChange{
|
||||
{Attribute: "parent_id", Old: nil, New: homeID},
|
||||
{Attribute: "sort_order", Old: 2, New: 1},
|
||||
},
|
||||
}
|
||||
ev.Locale = ""
|
||||
if !reflect.DeepEqual(ev, want) {
|
||||
t.Fatalf("reorder event = %+v\nwant %+v", ev, want)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("child write", func(t *testing.T) {
|
||||
rel := env.relations(t)
|
||||
created, err := rel.CreateChild(ctx, cc, "notes", homeID, RecordInput{Body: map[string]any{"body": "first"}})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
noteID := reID(t, created.Data)
|
||||
ev := oneSaved(t, env.listener)
|
||||
if ev.Controller != "acme.events.notes" || ev.Model != "cabana_re_notes" || !ev.Created || ev.ID != strconv.FormatUint(uint64(noteID), 10) {
|
||||
t.Fatalf("child create event = %+v", ev)
|
||||
}
|
||||
if _, err := rel.UpdateChild(ctx, cc, "notes", homeID, noteID, RecordInput{Body: map[string]any{"body": "second"}}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
ev = oneSaved(t, env.listener)
|
||||
want := []pact.RecordChange{{Attribute: "body", Old: "first", New: "second"}}
|
||||
if ev.Created || !reflect.DeepEqual(ev.Changes, want) {
|
||||
t.Fatalf("child update event = %+v, want changes %+v", ev, want)
|
||||
}
|
||||
if _, err := rel.DeleteChildren(ctx, cc, "notes", homeID, BulkDeleteInput{IDs: []any{float64(noteID)}}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
saved, deleted := env.listener.take()
|
||||
wantDel := []pact.RecordDeleted{{Controller: "acme.events.notes", Model: "cabana_re_notes", ID: strconv.FormatUint(uint64(noteID), 10)}}
|
||||
if len(saved) != 0 || !reflect.DeepEqual(deleted, wantDel) {
|
||||
t.Fatalf("child delete events: saved %+v deleted %+v", saved, deleted)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("delete", func(t *testing.T) {
|
||||
victim := rePage{Title: "Victim", Slug: "victim"}
|
||||
if err := env.db.Create(&victim).Error; err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, err := crud.Delete(ctx, cc, victim.ID); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
saved, deleted := env.listener.take()
|
||||
want := []pact.RecordDeleted{{Controller: "acme.events.pages", Model: rePageMorph, ID: strconv.FormatUint(uint64(victim.ID), 10)}}
|
||||
if len(saved) != 0 || !reflect.DeepEqual(deleted, want) {
|
||||
t.Fatalf("delete events: saved %+v deleted %+v", saved, deleted)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("bulk delete", func(t *testing.T) {
|
||||
a, b := rePage{Title: "A", Slug: "a"}, rePage{Title: "B", Slug: "b"}
|
||||
for _, p := range []*rePage{&a, &b} {
|
||||
if err := env.db.Create(p).Error; err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
if _, err := crud.BulkDelete(ctx, cc, BulkDeleteInput{IDs: []any{float64(a.ID), float64(999999)}}); err == nil {
|
||||
t.Fatal("partial selection deleted")
|
||||
}
|
||||
noEvents(t, env.listener)
|
||||
if _, err := crud.BulkDelete(ctx, cc, BulkDeleteInput{IDs: []any{float64(a.ID), float64(b.ID)}}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
saved, deleted := env.listener.take()
|
||||
if len(saved) != 0 || len(deleted) != 2 {
|
||||
t.Fatalf("bulk delete events: saved %+v deleted %+v", saved, deleted)
|
||||
}
|
||||
got := map[string]bool{deleted[0].ID: true, deleted[1].ID: true}
|
||||
if !got[strconv.FormatUint(uint64(a.ID), 10)] || !got[strconv.FormatUint(uint64(b.ID), 10)] || deleted[0].Model != rePageMorph {
|
||||
t.Fatalf("bulk delete events = %+v", deleted)
|
||||
}
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user