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:
2026-10-10 15:42:31 +02:00
parent c07da3912b
commit 08a20021e3
2 changed files with 374 additions and 0 deletions

View 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)
}
})
}