package cabana import ( "context" "encoding/json" "net/http" "net/http/httptest" "strings" "testing" "testing/fstest" "git.golem15.com/golem15/summercms/modules/backpack" "git.golem15.com/golem15/summercms/modules/bouncer" "git.golem15.com/golem15/summercms/modules/pact" "git.golem15.com/golem15/summercms/modules/party" "git.golem15.com/golem15/summercms/modules/phrasebook" "git.golem15.com/golem15/summercms/modules/towel" ) // TestRecordActionPrompt (D-34): a prompted record action's prompt is // localized in the show payload (label, placeholder and comment in the // request's language, the default cap of 255 when MaxLength is 0); its // value is trimmed and capped in runes; a non-string or empty payload is // 422 on payload; an action without a Prompt refuses any payload; and a // prompt-free value is the empty string. The Postgres run of the value // into Run, and the empty Value a plain action receives, are pinned by // TestRecordActionPromptRun. func TestRecordActionPrompt(t *testing.T) { cat := phrasebook.NewCatalog() if err := cat.Load("acme", fstest.MapFS{ "lang/en/lang.yaml": {Data: []byte("site_login: Site login\nsite_login_hint: The login on the public site.\nlogin_placeholder: login\n")}, "lang/pl/lang.yaml": {Data: []byte("site_login: Login na stronie\nsite_login_hint: Login w serwisie publicznym.\nlogin_placeholder: login\n")}, }); err != nil { t.Fatal(err) } tr := phrasebook.NewTranslator(cat, phrasebook.Options{Locale: "en", Fallback: "en"}) var got []string extender := screensExtender{extensions: []pact.AdminActionExtension{{ Controller: AdministratorsControllerID, RecordActions: []pact.AdminRecordAction{ promptedAction("acme_link", &pact.AdminActionPrompt{Label: "acme::lang.site_login", Placeholder: "acme::lang.login_placeholder", Comment: "acme::lang.site_login_hint", MaxLength: 5}, &got), promptedAction("acme_note", &pact.AdminActionPrompt{Label: "acme::lang.site_login"}, &got), pingAction(), }, }}} reg, err := compileWithScreens(t, []party.Plugin{extender}) if err != nil { t.Fatal(err) } cc, _ := reg.Get(AdministratorsControllerID) super := &bouncer.Principal{ID: 1, Backend: true, IsSuperuser: true} t.Run("prompt localized in the show payload", func(t *testing.T) { for locale, want := range map[string][2]string{ "en": {"Site login", "The login on the public site."}, "pl": {"Login na stronie", "Login w serwisie publicznym."}, } { ctx := towel.WithLocale(bouncer.WithUser(context.Background(), super), locale) offered, err := (CRUDService{tr: tr}).offeredRecordActions(ctx, cc, &BackendUser{ID: 5, IsActivated: true}) if err != nil { t.Fatal(err) } for _, a := range offered { switch a.Name { case "acme_link": if a.Prompt == nil || a.Prompt.Label != want[0] || a.Prompt.Comment != want[1] || a.Prompt.Placeholder != "login" || a.Prompt.MaxLength != 5 { t.Fatalf("%s acme_link prompt = %+v", locale, a.Prompt) } case "acme_note": if a.Prompt == nil || a.Prompt.Label != want[0] || a.Prompt.MaxLength != 255 { t.Fatalf("%s acme_note prompt = %+v", locale, a.Prompt) } case "acme_ping": if a.Prompt != nil { t.Fatalf("acme_ping carries a prompt: %+v", a.Prompt) } } } } }) t.Run("value trimmed and capped in runes", func(t *testing.T) { prompt := &pact.AdminActionPrompt{MaxLength: 5} for raw, want := range map[string]string{`" ąęś "`: "ąęś", `"abcde"`: "abcde", "\"\\tabc\\n\"": "abc"} { if v, err := promptValue(context.Background(), nil, prompt, json.RawMessage(raw)); err != nil || v != want { t.Fatalf("%s = %q, %v", raw, v, err) } } for _, raw := range []string{`"abcdef"`, `"ąęśćżź"`, `""`, `" "`, `7`, `null`, `["a"]`, `{"v":"a"}`} { if v, err := promptValue(context.Background(), nil, prompt, json.RawMessage(raw)); err == nil { t.Fatalf("%s accepted as %q", raw, v) } } if v, err := promptValue(context.Background(), nil, nil, json.RawMessage(`"x"`)); err != nil || v != "" { t.Fatalf("no prompt = %q, %v", v, err) } }) t.Run("route refusals", func(t *testing.T) { svc := &service{app: backpack.New(nil), reg: reg} post := func(action, body string) *httptest.ResponseRecorder { req := httptest.NewRequest(http.MethodPost, "/", strings.NewReader(body)) for k, v := range map[string]string{"vendor": "summercms", "plugin": "cabana", "controller": "users", "id": "5", "action": action} { req.SetPathValue(k, v) } req = req.WithContext(bouncer.WithUser(req.Context(), super)) rec := httptest.NewRecorder() svc.recordAction(rec, req) return rec } for _, body := range []string{`{"payload":42}`, `{"payload":true}`, `{"payload":"abcdefgh"}`, `{"payload":""}`, `{}`} { if rec := post("acme_link", body); rec.Code != http.StatusUnprocessableEntity || !strings.Contains(rec.Body.String(), `"payload"`) { t.Fatalf("%s = %d %s", body, rec.Code, rec.Body.String()) } } if rec := post("acme_ping", `{"payload":"x"}`); rec.Code != http.StatusUnprocessableEntity { t.Fatalf("payload on a plain action = %d %s", rec.Code, rec.Body.String()) } if len(got) != 0 { t.Fatalf("a refused request ran the action: %v", got) } }) }