- 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
198 lines
7.3 KiB
Go
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
|
|
}
|