- 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
152 lines
5.0 KiB
Go
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> & \"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)
|
|
}
|
|
}
|
|
}
|