Files
sm-journal-plugin/controllers/posts_editor_test.go
Jakub Zych da7a0732c3 test(16.2-06): full unit coverage of the Journal Blog Editor field types
- Decode: clean values byte for byte, nil, non-string refusal, the 35
  unsafe payloads stripped with notices exactly when changed
- notices order, ten-name cut, plural forms in lang/en and lang/pl
- LooksLikeHTML, options, ListValue, Upload sniffing and safe names
- StoreMedia layout, suffixes, no overwrite, size and folder rules
- editor corpus and policy cross-check extended in place: more negatives,
  notices only when stripped, stable second pass
- TestPostsEditorHooks: create and update, default and non-default
  writes, no writes, no writer
- TestCategoriesAdminBlogEditor (Docker host): untouched saves keep
  imported markup and NULL, clean edit, strip and report, blank to NULL,
  editor upload
2026-10-10 15:55:00 +02:00

198 lines
7.3 KiB
Go

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: "<p>PL</p>"},
cabana.FieldWrite{Locale: "en", Value: "<p>EN</p>"},
cabana.FieldWrite{Locale: "de", Value: "<p>DE</p>"},
)
post := &models.Post{Content: "<p>PL</p>", ContentHTML: "old"}
if err := before[op](ctl, ctx, post); err != nil {
t.Fatal(err)
}
if post.ContentHTML != "<p>PL</p>" {
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", "<p>EN</p>"}, {"content_html", "de", "<p>DE</p>"}}
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: "<p>PL</p>"})
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 != "<p>PL</p>" || 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: "<p>EN</p>"})
post := &models.Post{Content: "markdown", ContentHTML: "<p>imported</p>"}
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 != "<p>imported</p>" {
t.Fatalf("ContentHTML = %q, want the untouched default kept", post.ContentHTML)
}
if len(writer.writes) != 1 || writer.writes[0] != (recordedWrite{"content_html", "en", "<p>EN</p>"}) {
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: "<p>imported</p>"}
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 != "<p>imported</p>" || 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: "<p>EN</p>"})
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: "<p>EN</p>"})
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: "<p>EN</p>"})
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
}