package cabana import ( "errors" "fmt" "strings" "testing" "testing/fstest" "git.golem15.com/golem15/summercms/modules/pact" "git.golem15.com/golem15/summercms/modules/seaglass" ) // TestRichEditorCompile pins D-10's Docker-free rules: the keys a richeditor // takes, the notices a removal report becomes, the value check before a // save, the projected sanitize step, and the boot refusal of the // controller-only features on settings forms. func TestRichEditorCompile(t *testing.T) { t.Run("keys", func(t *testing.T) { cc := ucMustCompile(t, ucFields+" description:\n type: richeditor\n label: Description\n size: large\n span: left\n comment: HTML\n required: true\n tab: Content\n default: \"
x
\"\n", nil) if d := ucField(t, cc, "description"); d.Type != richEditorType || d.Size != "large" || d.Tab != "Content" { t.Fatalf("description = %+v", d) } for _, key := range []string{"options: typeOptions", "attributes: {}", "placeholder: x", "toolbarButtons: bold", "prompt: x", "maxItems: 2"} { _, err := ucCompile(ucFields+" description:\n type: richeditor\n "+key+"\n", nil) if err == nil { t.Fatalf("richeditor accepted %s", key) } } _, err := ucCompile(ucFields+" description:\n type: richeditor\n options: typeOptions\n", nil) wantErr(t, err, "options is not valid on type: richeditor") }) t.Run("notices", func(t *testing.T) { for _, tc := range []struct { name string in seaglass.Removals want string }{ {"nothing removed", seaglass.Removals{}, "[]"}, {"elements", seaglass.Removals{Elements: []string{"script", "iframe"}, ElementCount: 3, Changed: true}, "[" + msgRichRemovedElements + " names=script, iframe count=3]"}, {"attributes", seaglass.Removals{Attributes: []string{"onclick"}, AttributeCount: 1, Changed: true}, "[" + msgRichRemovedAttributes + " names=onclick count=1]"}, {"comments", seaglass.Removals{Comments: 2, Changed: true}, "[" + msgRichRemovedComments + " names= count=2]"}, {"all three in order", seaglass.Removals{Elements: []string{"style"}, ElementCount: 1, Attributes: []string{"style", "rel"}, AttributeCount: 2, Comments: 1, Changed: true}, "[" + msgRichRemovedElements + " names=style count=1 " + msgRichRemovedAttributes + " names=style, rel count=2 " + msgRichRemovedComments + " names= count=1]"}, {"markup changed only", seaglass.Removals{Changed: true}, "[" + msgRichMarkupChanged + " names= count=0]"}, } { var parts []string for _, n := range richEditorNotices(tc.in) { parts = append(parts, fmt.Sprintf("%s names=%s count=%d", n.Message, n.Params["names"], n.Count)) } if got := "[" + strings.Join(parts, " ") + "]"; got != tc.want { t.Fatalf("%s: notices = %s\nwant %s", tc.name, got, tc.want) } } }) cc := ucMustCompile(t, ucFields+" description:\n type: richeditor\n"+ucRepeater(""), nil) t.Run("a value that is not a string or null is a 422 on the field", func(t *testing.T) { for _, raw := range []string{`{"description":"x
"}`, `{"description":null}`, `{"title":"no description"}`} { if err := checkRichEditorValues(cc, utJSON(t, raw).(map[string]any), "update"); err != nil { t.Fatalf("%s: %v", raw, err) } } for _, raw := range []string{`{"description":1}`, `{"description":true}`, `{"description":["x
"]}`, `{"description":{"pl":"x
"}}`} { err := checkRichEditorValues(cc, utJSON(t, raw).(map[string]any), "create") var invalid *ValidationError if !errors.As(err, &invalid) || invalid.Details["description"] == nil || len(invalid.Details) != 1 { t.Fatalf("%s: err = %v", raw, err) } } }) t.Run("projected values are sanitized, clean ones byte for byte", func(t *testing.T) { clean := "It's fine & \"quoted\"
" projected := map[string]any{"description": clean} if notices := sanitizeRichEditors(cc, projected); len(notices) != 0 || projected["description"] != clean { t.Fatalf("clean: %v %v", projected, notices) } projected = map[string]any{"description": `a
`} notices := sanitizeRichEditors(cc, projected) if projected["description"] != "a
" || len(notices) != 2 || notices[0].field != "description" || notices[0].notice.Message != msgRichRemovedAttributes || notices[1].notice.Message != msgRichRemovedComments { t.Fatalf("dirty: %v %+v", projected, notices) } projected = map[string]any{"description": nil, "title": ``} if notices := sanitizeRichEditors(cc, projected); len(notices) != 0 || projected["description"] != nil || projected["title"] != `` { t.Fatalf("null and other fields: %v %v", projected, notices) } if notices := sanitizeRichEditors(cc, map[string]any{}); len(notices) != 0 { t.Fatal("absent value reported") } }) t.Run("settings forms accept a richeditor (Phase 18 F3)", func(t *testing.T) { fsys := fstest.MapFS{"models/settings/fields.yaml": &fstest.MapFile{Data: []byte("fields:\n enabled:\n type: switch\n extra:\n type: richeditor\n")}} item := pact.SettingsItem{Code: "demo", Label: "Demo", Form: "models/settings/fields.yaml", Model: "DemoSettings", NewModel: func() any { return &ucSettings{} }} if _, err := compileSetting("acme.unit", item, fsys); err != nil { t.Fatalf("richeditor refused on a settings form: %v", err) } }) t.Run("settings forms refuse the controller-only features", func(t *testing.T) { for name, field := range map[string]string{ "repeater": " type: repeater\n form:\n fields:\n a:\n type: text\n", "dependsOn": " type: dropdown\n dependsOn: enabled\n", } { fsys := fstest.MapFS{"models/settings/fields.yaml": &fstest.MapFile{Data: []byte("fields:\n enabled:\n type: switch\n extra:\n" + field)}} item := pact.SettingsItem{Code: "demo", Label: "Demo", Form: "models/settings/fields.yaml", Model: "DemoSettings", NewModel: func() any { return &ucSettings{} }} _, err := compileSetting("acme.unit", item, fsys) wantErr(t, err, "setting demo field extra", "supported in controller forms only") if name != "dependsOn" { wantErr(t, err, name) } } }) t.Run("controllerFormOnly", func(t *testing.T) { for _, field := range []FormField{{Type: richEditorType}, {Type: repeaterType}, {Type: "dropdown", DependsOn: "type"}} { if err := controllerFormOnly(field); err == nil { t.Fatalf("%+v accepted", field) } } for _, field := range []FormField{{Type: "text"}, {Type: "dropdown"}, {Type: "markdown"}} { if err := controllerFormOnly(field); err != nil { t.Fatalf("%+v refused: %v", field, err) } } }) } // ucSettings is the settings model of the settings refusal case. type ucSettings struct { ID uint `gorm:"column:id;primaryKey"` Enabled bool `gorm:"column:enabled"` Extra string `gorm:"column:extra"` } func (*ucSettings) Fillable() []string { return []string{"enabled", "extra"} } func (*ucSettings) Rules() map[string]string { return map[string]string{} }