test(16.2-06): pin the untouched-value skip in cabana and PluginField
- TestFieldTypeUntouched: served values skip Decode, writes and notices; one changed locale writes and reports only that locale; canonical JSON comparison; a create never skips - PluginField: an element that echoes its value on connect is never written a second time
This commit is contained in:
299
modules/cabana/field_type_test.go
Normal file
299
modules/cabana/field_type_test.go
Normal file
@@ -0,0 +1,299 @@
|
||||
package cabana
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"git.golem15.com/golem15/summercms/modules/pact"
|
||||
)
|
||||
|
||||
// Unit tests of the plugin field type framework (Phase 16.2 plan 06). They
|
||||
// use no database: the compile path runs on fstest trees, saves run the
|
||||
// decode half of CRUDService.save (ftSave) with the recording translation
|
||||
// writer, and routes run through httptest.
|
||||
|
||||
// utCounter counts Decode calls per field and locale.
|
||||
type utCounter map[string]int
|
||||
|
||||
// utTypes are the untouched-skip stub types: blob (plain, Load parses a JSON
|
||||
// object and serves anything else as stored) and notesml (translatable, Load
|
||||
// prefixes "served:"). Both Decodes always report a notice, so a skipped
|
||||
// value is visible as a missing notice.
|
||||
func utTypes(calls utCounter) []pact.FormFieldType {
|
||||
return []pact.FormFieldType{
|
||||
{
|
||||
Name: "blob",
|
||||
Element: "acme-demo-blob",
|
||||
Decode: func(_ context.Context, in pact.FormFieldValue) (pact.FormFieldDecoded, error) {
|
||||
calls[in.Field+":"+in.Locale]++
|
||||
return pact.FormFieldDecoded{Value: in.Value, Notices: []pact.FormFieldNotice{{Message: "decoded"}}}, nil
|
||||
},
|
||||
Load: func(_ context.Context, in pact.FormFieldValue) (any, error) {
|
||||
text, _ := in.Value.(string)
|
||||
if strings.HasPrefix(text, "{") {
|
||||
var out any
|
||||
if err := json.Unmarshal([]byte(text), &out); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
return text, nil
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "notesml",
|
||||
Element: "acme-demo-notesml",
|
||||
Translatable: true,
|
||||
Decode: func(_ context.Context, in pact.FormFieldValue) (pact.FormFieldDecoded, error) {
|
||||
calls[in.Field+":"+in.Locale]++
|
||||
text, _ := in.Value.(string)
|
||||
return pact.FormFieldDecoded{Value: text, Notices: []pact.FormFieldNotice{{Message: "decoded " + in.Locale}}}, nil
|
||||
},
|
||||
Load: func(_ context.Context, in pact.FormFieldValue) (any, error) {
|
||||
text, _ := in.Value.(string)
|
||||
return "served:" + text, nil
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// utController compiles the notes controller with notes as blob and body as
|
||||
// notesml.
|
||||
func utController(t *testing.T, calls utCounter) *CompiledController {
|
||||
t.Helper()
|
||||
plugin := ftPlugin{
|
||||
id: "acme.demo",
|
||||
fsys: ftFS(" type: blob\n label: Notes\n", ftBodyField),
|
||||
types: utTypes(calls),
|
||||
}
|
||||
reg, err := ftCompile(plugin)
|
||||
if err != nil {
|
||||
t.Fatalf("compile: %v", err)
|
||||
}
|
||||
cc, ok := reg.Get("acme.demo.notes")
|
||||
if !ok {
|
||||
t.Fatal("no notes controller")
|
||||
}
|
||||
return cc
|
||||
}
|
||||
|
||||
// utJSON decodes a JSON save body value the way the request decoder does.
|
||||
func utJSON(t *testing.T, raw string) any {
|
||||
t.Helper()
|
||||
var out any
|
||||
if err := json.Unmarshal([]byte(raw), &out); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func TestFieldTypeUntouched(t *testing.T) {
|
||||
stored := func() *ftNote {
|
||||
return &ftNote{ID: 1, Body: "P", Notes: `{"a":2,"b":1}`}
|
||||
}
|
||||
storedWriter := func() *recordingWriter {
|
||||
w := ftWriter()
|
||||
w.attrs = map[string]map[string]string{"en": {"body": "E"}}
|
||||
return w
|
||||
}
|
||||
|
||||
t.Run("an update equal to the served values writes and reports nothing", func(t *testing.T) {
|
||||
calls := utCounter{}
|
||||
cc := utController(t, calls)
|
||||
writer := storedWriter()
|
||||
target := stored()
|
||||
body := map[string]any{
|
||||
"body": map[string]any{"pl": "served:P", "en": "served:E"},
|
||||
"notes": utJSON(t, `{"b":1,"a":2}`),
|
||||
}
|
||||
res, projected, translations, err := ftSave(t, cc, writer, target, body, true)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, ok := projected["body"]; ok {
|
||||
t.Fatalf("untouched host value projected: %v", projected)
|
||||
}
|
||||
if _, ok := projected["notes"]; ok {
|
||||
t.Fatalf("untouched plain value projected: %v", projected)
|
||||
}
|
||||
writer.attrs = nil
|
||||
if err := applyMLTranslations(context.Background(), nil, writer, target, translations); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(writer.ids) != 0 || len(writer.attrs) != 0 {
|
||||
t.Fatalf("WriteTranslated called: ids=%v attrs=%v", writer.ids, writer.attrs)
|
||||
}
|
||||
if len(res.writes) != 0 {
|
||||
t.Fatalf("writes=%v", res.writes)
|
||||
}
|
||||
if len(res.notices) != 0 || localizeNotices(context.Background(), nil, res.notices) != nil {
|
||||
t.Fatalf("notices=%v", res.notices)
|
||||
}
|
||||
if len(calls) != 0 {
|
||||
t.Fatalf("Decode ran for untouched values: %v", calls)
|
||||
}
|
||||
raw, _ := json.Marshal(RecordMeta{Labels: map[string][]RelationOption{}, Notices: localizeNotices(context.Background(), nil, res.notices)})
|
||||
if strings.Contains(string(raw), "notices") {
|
||||
t.Fatalf("meta=%s", raw)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("the stored value is not the served value", func(t *testing.T) {
|
||||
calls := utCounter{}
|
||||
cc := utController(t, calls)
|
||||
writer := storedWriter()
|
||||
// "P" is the stored host text; the element received "served:P".
|
||||
res, projected, _, err := ftSave(t, cc, writer, stored(), map[string]any{"body": map[string]any{"pl": "P", "en": "served:E"}}, true)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if projected["body"] != "P" || calls["body:pl"] != 1 || calls["body:en"] != 0 {
|
||||
t.Fatalf("projected=%v calls=%v", projected, calls)
|
||||
}
|
||||
if len(res.writes["body"]) != 1 || res.writes["body"][0] != (FieldWrite{Locale: "pl", Default: true, Value: "P"}) {
|
||||
t.Fatalf("writes=%v", res.writes)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("one changed non-default locale writes and reports only that locale", func(t *testing.T) {
|
||||
calls := utCounter{}
|
||||
cc := utController(t, calls)
|
||||
writer := storedWriter()
|
||||
target := stored()
|
||||
res, projected, translations, err := ftSave(t, cc, writer, target, map[string]any{"body": map[string]any{"pl": "served:P", "en": "E2"}}, true)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, ok := projected["body"]; ok {
|
||||
t.Fatalf("untouched host value projected: %v", projected)
|
||||
}
|
||||
writer.attrs = nil
|
||||
if err := applyMLTranslations(context.Background(), nil, writer, target, translations); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if fmt.Sprint(writer.attrs) != fmt.Sprint(map[string]map[string]string{"en": {"body": "E2"}}) || len(writer.ids) != 1 {
|
||||
t.Fatalf("attrs=%v ids=%v", writer.attrs, writer.ids)
|
||||
}
|
||||
if fmt.Sprint(res.writes) != fmt.Sprint(map[string][]FieldWrite{"body": {{Locale: "en", Value: "E2"}}}) {
|
||||
t.Fatalf("writes=%v", res.writes)
|
||||
}
|
||||
got := localizeNotices(context.Background(), nil, res.notices)
|
||||
if len(got) != 1 || got[0] != (FieldNotice{Field: "body", Locale: "en", Message: "decoded en"}) {
|
||||
t.Fatalf("notices=%+v", got)
|
||||
}
|
||||
if fmt.Sprint(calls) != fmt.Sprint(utCounter{"body:en": 1}) {
|
||||
t.Fatalf("calls=%v", calls)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("one changed default locale writes the host value only", func(t *testing.T) {
|
||||
calls := utCounter{}
|
||||
cc := utController(t, calls)
|
||||
writer := storedWriter()
|
||||
target := stored()
|
||||
res, projected, translations, err := ftSave(t, cc, writer, target, map[string]any{"body": map[string]any{"pl": "P2", "en": "served:E"}}, true)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if projected["body"] != "P2" {
|
||||
t.Fatalf("projected=%v", projected)
|
||||
}
|
||||
writer.attrs = nil
|
||||
if err := applyMLTranslations(context.Background(), nil, writer, target, translations); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(writer.ids) != 0 {
|
||||
t.Fatalf("translation written for an untouched locale: %v", writer.attrs)
|
||||
}
|
||||
got := localizeNotices(context.Background(), nil, res.notices)
|
||||
if len(got) != 1 || got[0].Locale != "pl" {
|
||||
t.Fatalf("notices=%+v", got)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("an untouched missing translation is the empty string", func(t *testing.T) {
|
||||
calls := utCounter{}
|
||||
cc := utController(t, calls)
|
||||
writer := ftWriter() // no en translation stored
|
||||
res, _, translations, err := ftSave(t, cc, writer, stored(), map[string]any{"body": map[string]any{"pl": "served:P", "en": "served:"}}, true)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(res.writes) != 0 || len(res.notices) != 0 {
|
||||
t.Fatalf("writes=%v notices=%v", res.writes, res.notices)
|
||||
}
|
||||
for _, item := range translations {
|
||||
if _, ok := item.values["en"]; ok && item.field == "body" {
|
||||
t.Fatalf("an untouched empty en is still written: %v", translations)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("canonical comparison: key order is equal, a string is not a number", func(t *testing.T) {
|
||||
if !sameJSON(utJSON(t, `{"b":1,"a":{"y":[1,2],"x":null}}`), utJSON(t, `{"a":{"x":null,"y":[1,2]},"b":1}`)) {
|
||||
t.Fatal("equal objects with different key order compare different")
|
||||
}
|
||||
if sameJSON("1", float64(1)) || sameJSON(float64(1), "1") {
|
||||
t.Fatal(`"1" and 1 compare equal`)
|
||||
}
|
||||
if sameJSON(utJSON(t, `[1,2]`), utJSON(t, `[2,1]`)) {
|
||||
t.Fatal("array order ignored")
|
||||
}
|
||||
if sameJSON(func() {}, func() {}) {
|
||||
t.Fatal("unencodable values compare equal")
|
||||
}
|
||||
|
||||
calls := utCounter{}
|
||||
cc := utController(t, calls)
|
||||
target := stored()
|
||||
target.Notes = "1"
|
||||
res, projected, _, err := ftSave(t, cc, ftWriter(), target, map[string]any{"notes": utJSON(t, `1`)}, true)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if projected["notes"] != float64(1) || calls["notes:"] != 1 || len(res.notices) != 1 {
|
||||
t.Fatalf("number against a stored string was skipped: projected=%v calls=%v", projected, calls)
|
||||
}
|
||||
_, projected, _, err = ftSave(t, cc, ftWriter(), target, map[string]any{"notes": "1"}, true)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, ok := projected["notes"]; ok {
|
||||
t.Fatalf("an equal string was decoded: %v", projected)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("a create never skips", func(t *testing.T) {
|
||||
calls := utCounter{}
|
||||
cc := utController(t, calls)
|
||||
writer := storedWriter()
|
||||
target := stored()
|
||||
body := map[string]any{
|
||||
"body": map[string]any{"pl": "served:P", "en": "served:E"},
|
||||
"notes": utJSON(t, `{"b":1,"a":2}`),
|
||||
}
|
||||
res, projected, translations, err := ftSave(t, cc, writer, target, body, false)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if projected["body"] != "served:P" || projected["notes"] == nil {
|
||||
t.Fatalf("projected=%v", projected)
|
||||
}
|
||||
writer.attrs = nil
|
||||
if err := applyMLTranslations(context.Background(), nil, writer, target, translations); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if writer.attrs["en"]["body"] != "served:E" {
|
||||
t.Fatalf("attrs=%v", writer.attrs)
|
||||
}
|
||||
if len(res.writes["body"]) != 2 || len(res.writes["notes"]) != 1 || len(res.notices) != 3 {
|
||||
t.Fatalf("writes=%v notices=%v", res.writes, res.notices)
|
||||
}
|
||||
if fmt.Sprint(calls) != fmt.Sprint(utCounter{"body:pl": 1, "body:en": 1, "notes:": 1}) {
|
||||
t.Fatalf("calls=%v", calls)
|
||||
}
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user