package controllers import ( "context" "errors" "testing" "git.golem15.com/golem15/sm-journal-plugin/models" "git.golem15.com/golem15/summercms/modules/backpack" "git.golem15.com/golem15/summercms/modules/bouncer" "git.golem15.com/golem15/summercms/modules/cabana" "gorm.io/gorm" ) // TestPostsEditorHooks pins the posts controller's Form hooks around the // Blog Editor (Phase 16.2 D-02, D-03): the default-locale content write is // copied to content_html before the save, each written non-default locale's // content is written as that locale's content_html after it, and content the // save does not write is never touched. func TestPostsEditorHooks(t *testing.T) { withWriter := func(t *testing.T) (postsAdminController, *recordingWriter) { t.Helper() writer := &recordingWriter{} app := backpack.New(nil) if err := app.Publish[cabana.TranslationWriter](writer); err != nil { t.Fatal(err) } return postsAdminController{app: func() *backpack.App { return app }}, writer } principal := func() context.Context { return bouncer.WithUser(context.Background(), editorPrincipal()) } writes := func(ctx context.Context, list ...cabana.FieldWrite) context.Context { return cabana.WithFieldWrites(ctx, map[string][]cabana.FieldWrite{"content": list}) } type hook func(postsAdminController, context.Context, any) error before := map[string]hook{ "create": postsAdminController.FormBeforeCreate, "update": postsAdminController.FormBeforeUpdate, } after := map[string]hook{ "create": postsAdminController.FormAfterCreate, "update": postsAdminController.FormAfterUpdate, } for _, op := range []string{"create", "update"} { t.Run(op+": default and non-default writes", func(t *testing.T) { ctl, writer := withWriter(t) ctx := writes(principal(), cabana.FieldWrite{Locale: "pl", Default: true, Value: "

PL

"}, cabana.FieldWrite{Locale: "en", Value: "

EN

"}, cabana.FieldWrite{Locale: "de", Value: "

DE

"}, ) post := &models.Post{Content: "

PL

", ContentHTML: "old"} if err := before[op](ctl, ctx, post); err != nil { t.Fatal(err) } if post.ContentHTML != "

PL

" { t.Fatalf("ContentHTML = %q, want the default write", post.ContentHTML) } if len(writer.writes) != 0 { t.Fatalf("translations written before the save: %#v", writer.writes) } if err := after[op](ctl, ctx, post); err != nil { t.Fatal(err) } want := []recordedWrite{{"content_html", "en", "

EN

"}, {"content_html", "de", "

DE

"}} if len(writer.writes) != 2 || writer.writes[0] != want[0] || writer.writes[1] != want[1] { t.Fatalf("writes = %#v, want %#v", writer.writes, want) } }) t.Run(op+": only the default locale written", func(t *testing.T) { ctl, writer := withWriter(t) ctx := writes(principal(), cabana.FieldWrite{Locale: "pl", Default: true, Value: "

PL

"}) post := &models.Post{ContentHTML: "old"} if err := before[op](ctl, ctx, post); err != nil { t.Fatal(err) } if err := after[op](ctl, ctx, post); err != nil { t.Fatal(err) } if post.ContentHTML != "

PL

" || len(writer.writes) != 0 { t.Fatalf("ContentHTML = %q, writes = %#v", post.ContentHTML, writer.writes) } }) t.Run(op+": only a non-default locale written", func(t *testing.T) { ctl, writer := withWriter(t) ctx := writes(principal(), cabana.FieldWrite{Locale: "en", Value: "

EN

"}) post := &models.Post{Content: "markdown", ContentHTML: "

imported

"} if err := before[op](ctl, ctx, post); err != nil { t.Fatal(err) } if err := after[op](ctl, ctx, post); err != nil { t.Fatal(err) } if post.ContentHTML != "

imported

" { t.Fatalf("ContentHTML = %q, want the untouched default kept", post.ContentHTML) } if len(writer.writes) != 1 || writer.writes[0] != (recordedWrite{"content_html", "en", "

EN

"}) { t.Fatalf("writes = %#v", writer.writes) } }) t.Run(op+": no content written", func(t *testing.T) { ctl, writer := withWriter(t) for name, ctx := range map[string]context.Context{ "outside a save": principal(), "no plugin field": cabana.WithFieldWrites(principal(), nil), "other fields": cabana.WithFieldWrites(principal(), map[string][]cabana.FieldWrite{"excerpt": {{Locale: "en", Value: "x"}}}), } { post := &models.Post{Content: "markdown", ContentHTML: "

imported

"} if err := before[op](ctl, ctx, post); err != nil { t.Fatalf("%s: %v", name, err) } if err := after[op](ctl, ctx, post); err != nil { t.Fatalf("%s: %v", name, err) } if post.ContentHTML != "

imported

" || len(writer.writes) != 0 { t.Fatalf("%s: ContentHTML = %q, writes = %#v", name, post.ContentHTML, writer.writes) } } }) t.Run(op+": no translation writer or no application", func(t *testing.T) { ctx := writes(principal(), cabana.FieldWrite{Locale: "en", Value: "

EN

"}) for name, ctl := range map[string]postsAdminController{ "no writer": {app: func() *backpack.App { return backpack.New(nil) }}, "nil app": {app: func() *backpack.App { return nil }}, "no app": {}, } { if err := after[op](ctl, ctx, &models.Post{}); err != nil { t.Fatalf("%s: %v", name, err) } } }) } t.Run("a non-string value is not copied and fails the translation write", func(t *testing.T) { ctl, writer := withWriter(t) post := &models.Post{ContentHTML: "old"} ctx := writes(principal(), cabana.FieldWrite{Locale: "pl", Default: true, Value: 42}) if err := ctl.FormBeforeUpdate(ctx, post); err != nil { t.Fatal(err) } if post.ContentHTML != "old" { t.Fatalf("ContentHTML = %q", post.ContentHTML) } ctx = writes(principal(), cabana.FieldWrite{Locale: "en", Value: 42}) if err := ctl.FormAfterUpdate(ctx, post); err == nil || len(writer.writes) != 0 { t.Fatalf("err = %v, writes = %#v", err, writer.writes) } }) t.Run("a model that is not a post is an error", func(t *testing.T) { ctl, _ := withWriter(t) ctx := writes(principal(), cabana.FieldWrite{Locale: "en", Value: "

EN

"}) for name, fn := range map[string]hook{ "FormBeforeCreate": postsAdminController.FormBeforeCreate, "FormBeforeUpdate": postsAdminController.FormBeforeUpdate, "FormAfterCreate": postsAdminController.FormAfterCreate, "FormAfterUpdate": postsAdminController.FormAfterUpdate, } { if err := fn(ctl, ctx, &models.Category{}); err == nil { t.Fatalf("%s accepted a category", name) } if err := fn(ctl, ctx, (*models.Post)(nil)); err == nil { t.Fatalf("%s accepted a nil post", name) } } }) t.Run("a translation writer error fails the save", func(t *testing.T) { boom := errors.New("write failed") app := backpack.New(nil) if err := app.Publish[cabana.TranslationWriter](failingWriter{recordingWriter: &recordingWriter{}, err: boom}); err != nil { t.Fatal(err) } ctl := postsAdminController{app: func() *backpack.App { return app }} ctx := writes(principal(), cabana.FieldWrite{Locale: "en", Value: "

EN

"}) if err := ctl.FormAfterCreate(ctx, &models.Post{}); !errors.Is(err, boom) { t.Fatalf("err = %v, want %v", err, boom) } }) } // failingWriter is a cabana.TranslationWriter whose writes fail. type failingWriter struct { *recordingWriter err error } func (w failingWriter) WriteTranslated(context.Context, *gorm.DB, any, string, string, string) error { return w.err }