Files
summercms/modules/cabana/settings_tabs_test.go
Jakub Zych 0f35c9a0ca test(18-07): framework unit coverage of money, settings secrets, tabs, richeditor and danger actions (task 2)
- money_field_test.go: compile gates (moved from the smoke file and
  extended), model column gate, save-time value check, list projection
- settings secret projection and put without a database; tabs, settings
  richeditor sanitize and the danger flag as internal unit tests
- Postgres: TestMoneyFieldSave, TestSettingsSecretPostgres and the Phase 18
  payload cases of TestPhase10OpenAPIConformance
- vitest: money.ts helpers spec, MoneyField blur-only and server-error cases
- gate arrays and the cabana README Testing section name every test
2026-10-13 16:58:03 +02:00

152 lines
5.0 KiB
Go

package cabana
import (
"context"
"encoding/json"
"errors"
"strings"
"testing"
)
// TestSettingsTabs checks UI-SPEC F4 without a database: a settings form
// accepts `tab`, the compiled and localized schema carries each field's tab
// (untabbed fields form the default tab and omit the key), and an untabbed
// form's schema carries no tab key at all, byte for byte as before the
// feature. A repeater and a dependsOn dropdown stay refused on settings.
func TestSettingsTabs(t *testing.T) {
item, fsys := secretSettingsItem(`fields:
token:
label: Token
type: text
api_key:
label: API key
type: password
tab: Keys
notes:
label: Notes
type: richeditor
tab: Texts
`)
setting, err := compileSetting("acme.unit", item, fsys)
if err != nil {
t.Fatal(err)
}
view, err := setting.Form.Localize(context.Background(), nil, nil)
if err != nil {
t.Fatal(err)
}
tabs := map[string]string{}
var order []string
for _, field := range view.Fields {
tabs[field.Name] = field.Tab
order = append(order, field.Name)
}
if strings.Join(order, ",") != "token,api_key,notes" {
t.Fatalf("field order = %v", order)
}
if tabs["token"] != "" || tabs["api_key"] != "Keys" || tabs["notes"] != "Texts" {
t.Fatalf("tabs = %v", tabs)
}
raw, err := json.Marshal(view)
if err != nil {
t.Fatal(err)
}
if strings.Count(string(raw), `"tab":`) != 2 {
t.Fatalf("tabbed schema JSON = %s", raw)
}
item, fsys = secretSettingsItem("fields:\n token:\n type: text\n notes:\n type: richeditor\n")
plain, err := compileSetting("acme.unit", item, fsys)
if err != nil {
t.Fatal(err)
}
plainView, err := plain.Form.Localize(context.Background(), nil, nil)
if err != nil {
t.Fatal(err)
}
raw, err = json.Marshal(plainView)
if err != nil {
t.Fatal(err)
}
if strings.Contains(string(raw), `"tab"`) {
t.Fatalf("untabbed schema carries a tab key: %s", raw)
}
for name, fields := range map[string]string{
"repeater": "fields:\n notes:\n type: repeater\n form:\n fields:\n a:\n type: text\n",
"dependsOn": "fields:\n token:\n type: dropdown\n dependsOn: notes\n options: {a: A}\n notes:\n type: text\n",
} {
t.Run(name+" refused", func(t *testing.T) {
item, fsys := secretSettingsItem(fields)
if _, err := compileSetting("acme.unit", item, fsys); err == nil {
t.Fatalf("%s compiled on a settings form", name)
}
})
}
}
// TestSettingsRichEditor checks UI-SPEC F3 without a database: a settings
// richeditor value is sanitized through seaglass before Fill sees it and
// what was removed comes back as notices on the field (one per kind); clean
// HTML passes byte for byte with no notice; null is accepted and a value
// that is neither a string nor null is a 422 on the field before anything
// is written.
func TestSettingsRichEditor(t *testing.T) {
setting := secretTwoFields(t)
dirty := `<p onclick="steal()">Bank</p><script>alert(1)</script>`
projected := projectSettingBody(setting, map[string]any{"notes": dirty, "token": "t"})
notices := sanitizeSettingRichEditors(setting, projected)
if projected["notes"] != "<p>Bank</p>" {
t.Fatalf("sanitized notes = %#v", projected["notes"])
}
if projected["token"] != "t" {
t.Fatalf("a text field was touched: %#v", projected["token"])
}
if len(notices) != 2 {
t.Fatalf("notices = %+v, want one for the element and one for the attribute", notices)
}
messages := map[string]bool{}
for _, n := range notices {
if n.field != "notes" {
t.Fatalf("notice on %q", n.field)
}
messages[n.notice.Message] = true
}
if !messages[msgRichRemovedElements] || !messages[msgRichRemovedAttributes] {
t.Fatalf("notice keys = %v", messages)
}
meta := settingsMeta(context.Background(), nil, nil, notices)
if meta == nil || len(meta.Notices) != 2 || meta.Notices[0].Field != "notes" || meta.Secrets != nil {
t.Fatalf("meta = %+v", meta)
}
clean := "<p>IBAN <strong>PL00</strong> &amp; \"swift\"</p>\n<ul><li>one</li></ul>"
projected = projectSettingBody(setting, map[string]any{"notes": clean})
if notices := sanitizeSettingRichEditors(setting, projected); len(notices) != 0 {
t.Fatalf("clean HTML reported notices: %+v", notices)
}
if projected["notes"] != clean {
t.Fatalf("clean HTML rewritten: %q", projected["notes"])
}
if meta := settingsMeta(context.Background(), nil, nil, nil); meta != nil {
t.Fatalf("meta without secrets or notices = %+v", meta)
}
for _, ok := range []any{nil, "", "<p>x</p>"} {
if err := checkSettingRichEditors(setting, map[string]any{"notes": ok}); err != nil {
t.Fatalf("%#v refused: %v", ok, err)
}
}
for _, bad := range []any{[]any{"<p>x</p>"}, 5, true, map[string]any{"html": "x"}} {
err := checkSettingRichEditors(setting, map[string]any{"notes": bad})
var verr *ValidationError
if !errors.As(err, &verr) {
t.Fatalf("%#v: err = %v", bad, err)
}
if _, ok := verr.Details["notes"]; !ok {
t.Fatalf("details = %#v", verr.Details)
}
}
}