test(32-01): add failing tests for form extension placement and save
- TestFormExtensionPlacement, TestFormExtensionBootErrors, TestFormExtensionTwoPluginsSameAnchor, TestFormExtensionOnePluginTwoControllers, TestFormExtensionUnextendedSchemaUnchanged, TestFormExtensionWidgetsAndPartials - TestFormExtensionSave (Postgres): rollback, 422 lines, notices, invisible extensions, locale maps, hidden owner fields - checkFormExtensionTabLabels declared (no-op) so the tests compile
This commit is contained in:
@@ -4,6 +4,7 @@ import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"io/fs"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
@@ -338,3 +339,197 @@ func TestFormExtensionTracer(t *testing.T) {
|
||||
t.Fatal("the owner table gained a c column")
|
||||
}
|
||||
}
|
||||
|
||||
// feMLType is a translatable field type of acme.extra: Decode trims and
|
||||
// reports the trim per locale.
|
||||
func feMLType() pact.FormFieldType {
|
||||
return pact.FormFieldType{
|
||||
Name: "extraml", Element: "acme-extra-ml", Translatable: true, Scripts: []string{"assets/js/extra.js"},
|
||||
Decode: func(_ context.Context, in pact.FormFieldValue) (pact.FormFieldDecoded, error) {
|
||||
text, _ := in.Value.(string)
|
||||
if text == "bad" {
|
||||
return pact.FormFieldDecoded{}, &pact.FormFieldError{Messages: []string{"bad in " + in.Locale}}
|
||||
}
|
||||
out := pact.FormFieldDecoded{Value: strings.TrimSpace(text)}
|
||||
if out.Value != text {
|
||||
out.Notices = []pact.FormFieldNotice{{Message: "trimmed " + in.Locale}}
|
||||
}
|
||||
return out, nil
|
||||
},
|
||||
Load: func(_ context.Context, in pact.FormFieldValue) (any, error) {
|
||||
text, _ := in.Value.(string)
|
||||
return "served:" + in.Locale + ":" + text, nil
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// TestFormExtensionSave (D-91, T-32-01, T-32-02): Save errors roll the whole
|
||||
// save back, AdminFormErrors answer 422 field lines, notices reach the save
|
||||
// response, an invisible extension is inert on save, translatable values
|
||||
// reach Save as locale maps without touching the owner's translations, and a
|
||||
// hidden owner field leaves show and save.
|
||||
func TestFormExtensionSave(t *testing.T) {
|
||||
store := &feStore{}
|
||||
var mlSaves []map[string]any
|
||||
extra := feContributor{
|
||||
id: "acme.extra",
|
||||
fsys: fstest.MapFS{"extensions/posts.yaml": &fstest.MapFile{Data: []byte("fields:\n c:\n type: text\n after: a\n")}, "assets/js/extra.js": &fstest.MapFile{Data: []byte("// extra\n")}},
|
||||
types: []pact.FormFieldType{feMLType()},
|
||||
extensions: []pact.AdminActionExtension{
|
||||
{Controller: "acme.blog.posts", Form: &pact.AdminFormExtension{Fields: "extensions/posts.yaml", Load: store.load, Save: store.save, Hide: []string{"b"}}},
|
||||
{Controller: "acme.blog.posts", Form: &pact.AdminFormExtension{
|
||||
FieldsYAML: []byte("tabs:\n - name: acme.extra::lang.tab\nfields:\n t:\n type: mltext\n tab: acme.extra::lang.tab\n n:\n type: extraml\n tab: acme.extra::lang.tab\n"),
|
||||
Permissions: []string{"acme.extra.translate"},
|
||||
Load: func(context.Context, pact.AdminFormLoad) (map[string]any, error) {
|
||||
return map[string]any{"t": map[string]string{"pl": "TP", "en": "TE"}, "n": map[string]string{"pl": "NP", "en": "NE"}}, nil
|
||||
},
|
||||
Save: func(_ context.Context, in pact.AdminFormSave) (pact.AdminFormSaveResult, error) {
|
||||
mlSaves = append(mlSaves, in.Values)
|
||||
return pact.AdminFormSaveResult{}, nil
|
||||
},
|
||||
}},
|
||||
},
|
||||
}
|
||||
env := newFEEnv(t, feOwner{fields: feOwnerFields}, extra)
|
||||
writer := ftWriter()
|
||||
if err := env.svc.app.Publish[TranslationWriter](writer); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
seed := fePost{A: "A0", B: "B0"}
|
||||
if err := env.db.Create(&seed).Error; err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
id := strconv.FormatUint(uint64(seed.ID), 10)
|
||||
put := func(principal *bouncer.Principal, body map[string]any) *httptest.ResponseRecorder {
|
||||
t.Helper()
|
||||
return feCall(t, env.svc, principal, "acme.blog.posts", http.MethodPut, "record", id, body)
|
||||
}
|
||||
|
||||
t.Run("AdminFormErrors answer 422 and roll back", func(t *testing.T) {
|
||||
store.fail = pact.AdminFormErrors{"c": {"The c field is required."}}
|
||||
defer func() { store.fail = nil }()
|
||||
rec := put(superUser(), map[string]any{"a": "A1", "c": "C1"})
|
||||
if rec.Code != http.StatusUnprocessableEntity || !strings.Contains(rec.Body.String(), "The c field is required.") {
|
||||
t.Fatalf("status=%d body=%s", rec.Code, rec.Body.String())
|
||||
}
|
||||
if got := env.post(t, seed.ID); got.A != "A0" {
|
||||
t.Fatalf("owner row changed: %+v", got)
|
||||
}
|
||||
if value, ok := env.extra(t, seed.ID); ok {
|
||||
t.Fatalf("contributor row kept after rollback: %q", value)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("a plain Save error answers 500 and rolls back", func(t *testing.T) {
|
||||
store.fail = errors.New("disk full")
|
||||
defer func() { store.fail = nil }()
|
||||
rec := put(superUser(), map[string]any{"a": "A1", "c": "C1"})
|
||||
if rec.Code != http.StatusInternalServerError || strings.Contains(rec.Body.String(), "disk full") {
|
||||
t.Fatalf("status=%d body=%s", rec.Code, rec.Body.String())
|
||||
}
|
||||
if got := env.post(t, seed.ID); got.A != "A0" {
|
||||
t.Fatalf("owner row changed: %+v", got)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("notices reach the save response, on owner fields too", func(t *testing.T) {
|
||||
store.notices = map[string][]pact.FormFieldNotice{"a": {{Message: "address changed"}}}
|
||||
defer func() { store.notices = nil }()
|
||||
rec := put(superUser(), map[string]any{"a": "A1", "c": "C1"})
|
||||
if rec.Code != http.StatusOK {
|
||||
t.Fatalf("status=%d body=%s", rec.Code, rec.Body.String())
|
||||
}
|
||||
notices := pgFTNotices(t, pgFTDecode(t, rec))
|
||||
if len(notices) != 1 || notices[0].Field != "a" || notices[0].Message != "address changed" {
|
||||
t.Fatalf("notices = %+v", notices)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("a hidden owner field leaves show and save", func(t *testing.T) {
|
||||
rec := put(superUser(), map[string]any{"a": "A2", "b": "B9", "c": "C2"})
|
||||
if rec.Code != http.StatusOK {
|
||||
t.Fatalf("status=%d body=%s", rec.Code, rec.Body.String())
|
||||
}
|
||||
if got := env.post(t, seed.ID); got.A != "A2" || got.B != "B0" {
|
||||
t.Fatalf("owner row=%+v, want a written and hidden b untouched", got)
|
||||
}
|
||||
shown := pgFTDecode(t, feCall(t, env.svc, superUser(), "acme.blog.posts", http.MethodGet, "record", id, nil))
|
||||
if _, ok := shown.Data["b"]; ok {
|
||||
t.Fatalf("show serves hidden b: %v", shown.Data)
|
||||
}
|
||||
names := feSchemaNames(t, feCall(t, env.svc, superUser(), "acme.blog.posts", http.MethodGet, "schema", "", nil))
|
||||
if feJoin(names) != "a c t n" {
|
||||
t.Fatalf("schema = %v", names)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("translatable values reach Save as locale maps", func(t *testing.T) {
|
||||
mlSaves = nil
|
||||
rec := put(superUser(), map[string]any{"a": "A3", "t": map[string]any{"pl": "P", "en": "E"}, "n": map[string]any{"pl": " X ", "en": "Y"}})
|
||||
if rec.Code != http.StatusOK {
|
||||
t.Fatalf("status=%d body=%s", rec.Code, rec.Body.String())
|
||||
}
|
||||
if len(mlSaves) != 1 {
|
||||
t.Fatalf("saves = %v", mlSaves)
|
||||
}
|
||||
tv, _ := mlSaves[0]["t"].(map[string]string)
|
||||
nv, _ := mlSaves[0]["n"].(map[string]string)
|
||||
if tv["pl"] != "P" || tv["en"] != "E" || nv["pl"] != "X" || nv["en"] != "Y" {
|
||||
t.Fatalf("Save values = %#v", mlSaves[0])
|
||||
}
|
||||
if len(writer.attrs) != 0 {
|
||||
t.Fatalf("the owner's translations were written: %v", writer.attrs)
|
||||
}
|
||||
body := pgFTDecode(t, rec)
|
||||
notices := pgFTNotices(t, body)
|
||||
if len(notices) != 1 || notices[0].Field != "n" || notices[0].Locale != "pl" {
|
||||
t.Fatalf("notices = %+v", notices)
|
||||
}
|
||||
served, _ := body.Data["n"].(map[string]any)
|
||||
if served["pl"] != "served:pl:NP" || served["en"] != "served:en:NE" {
|
||||
t.Fatalf("n served = %v, want the type's Load after the extension's", body.Data["n"])
|
||||
}
|
||||
refused := put(superUser(), map[string]any{"a": "A3", "n": map[string]any{"pl": "bad"}})
|
||||
if refused.Code != http.StatusUnprocessableEntity || !strings.Contains(refused.Body.String(), "bad in pl") {
|
||||
t.Fatalf("refused status=%d body=%s", refused.Code, refused.Body.String())
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("an invisible extension is inert on save", func(t *testing.T) {
|
||||
editor := &bouncer.Principal{ID: 2, Backend: true, PermissionGrants: map[string]bool{"acme.blog.access": true}}
|
||||
mlSaves = nil
|
||||
rec := put(editor, map[string]any{"a": "A4", "t": map[string]any{"pl": "Z"}, "c": "C4"})
|
||||
if rec.Code != http.StatusOK {
|
||||
t.Fatalf("status=%d body=%s", rec.Code, rec.Body.String())
|
||||
}
|
||||
if len(mlSaves) != 0 {
|
||||
t.Fatalf("Save of the invisible extension ran: %v", mlSaves)
|
||||
}
|
||||
if value, _ := env.extra(t, seed.ID); value != "C4" {
|
||||
t.Fatalf("the visible extension did not save: %q", value)
|
||||
}
|
||||
data := pgFTDecode(t, rec).Data
|
||||
if _, ok := data["t"]; ok {
|
||||
t.Fatalf("save response serves the invisible extension's field: %v", data)
|
||||
}
|
||||
shown := pgFTDecode(t, feCall(t, env.svc, editor, "acme.blog.posts", http.MethodGet, "record", id, nil))
|
||||
if _, ok := shown.Data["n"]; ok {
|
||||
t.Fatalf("show serves the invisible extension's field: %v", shown.Data)
|
||||
}
|
||||
if names := feSchemaNames(t, feCall(t, env.svc, editor, "acme.blog.posts", http.MethodGet, "schema", "", nil)); feJoin(names) != "a c" {
|
||||
t.Fatalf("schema = %v", names)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("create hands Save the new record", func(t *testing.T) {
|
||||
before := len(store.saves)
|
||||
rec := feCall(t, env.svc, superUser(), "acme.blog.posts", http.MethodPost, "record", "", map[string]any{"a": "N", "c": "NC"})
|
||||
if rec.Code != http.StatusCreated {
|
||||
t.Fatalf("status=%d body=%s", rec.Code, rec.Body.String())
|
||||
}
|
||||
last := store.saves[len(store.saves)-1]
|
||||
if len(store.saves) != before+1 || !last.Created || last.ID == "" || last.ID == id {
|
||||
t.Fatalf("create Save = %+v", last)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user