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
This commit is contained in:
197
controllers/posts_editor_test.go
Normal file
197
controllers/posts_editor_test.go
Normal file
@@ -0,0 +1,197 @@
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user