- pact.AdminActionInput.Payload (json.RawMessage) carries the widget's own JSON value untouched; pact.AdminActionResult.Data is passed through as data - cabana decodes payload with a 64 KiB cap (422 on body), refuses it on the toolbar and record routes, and embeds Data once encoded with a 256 KiB cap (opaque 500 when larger or unencodable); fill stays filtered - root .swaggo overrides json.RawMessage so swag keeps record_id and values; admin.json and schema.d.ts regenerated (payload?: unknown, data?: unknown) - TestWidgetPayloadAndData covers pass-through, cap, refusal and data 500 - cabana and pact READMEs, partials-and-widgets and admin-spa docs updated
127 lines
4.9 KiB
Go
127 lines
4.9 KiB
Go
package cabana_test
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"net/http"
|
|
"reflect"
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
// TestWidgetPayloadAndData drives the widget payload and data channel through
|
|
// the assembled router on PostgreSQL (quick-261006-eyj; T-Q261006-01, -02,
|
|
// -04): the payload reaches the action byte for byte, data comes back
|
|
// unfiltered while fill is still filtered, the 64 KiB payload cap, the
|
|
// toolbar and record refusal, and the 256 KiB or unencodable data as an
|
|
// opaque 500.
|
|
func TestWidgetPayloadAndData(t *testing.T) {
|
|
env, gdb := newActEnv(t)
|
|
mine := actInsert(t, gdb, "mine", "acme")
|
|
const widget = "/acme/demo/gadgets/widgets/lookup"
|
|
const toolbar = "/acme/demo/gadgets/toolbar/recount"
|
|
|
|
t.Run("payload reaches the action and data comes back untouched", func(t *testing.T) {
|
|
const payload = `{"order":[3,1,2],"note":"x"}`
|
|
rec := env.expect(t, http.StatusOK, http.MethodPost, widget,
|
|
fmt.Sprintf(`{"record_id":%d,"values":{"name":"typed","tenant":"other"},"payload":%s}`, mine, payload), "bearer")
|
|
calls := env.spy.take()
|
|
if len(calls) != 1 {
|
|
t.Fatalf("calls = %+v", calls)
|
|
}
|
|
if string(calls[0].Payload) != payload {
|
|
t.Fatalf("payload = %s, want %s", calls[0].Payload, payload)
|
|
}
|
|
if !reflect.DeepEqual(calls[0].Values, map[string]any{"name": "typed"}) {
|
|
t.Fatalf("values = %#v", calls[0].Values)
|
|
}
|
|
result := actResult(t, rec)
|
|
if !reflect.DeepEqual(result.Fill, map[string]any{"name": "reordered"}) {
|
|
t.Fatalf("fill = %#v", result.Fill)
|
|
}
|
|
want := map[string]any{
|
|
"echo": map[string]any{"order": []any{float64(3), float64(1), float64(2)}, "note": "x"},
|
|
"items": []any{map[string]any{"id": float64(1), "title": "a"}},
|
|
}
|
|
if !reflect.DeepEqual(result.Data, want) {
|
|
t.Fatalf("data = %#v, want %#v", result.Data, want)
|
|
}
|
|
})
|
|
|
|
t.Run("payload may be any JSON value and is nil when absent", func(t *testing.T) {
|
|
for _, literal := range []string{`[1,2]`, `"s"`, `0`, `false`, `null`} {
|
|
env.expect(t, http.StatusOK, http.MethodPost, widget, `{"payload":`+literal+`}`, "bearer")
|
|
calls := env.spy.take()
|
|
if len(calls) != 1 || string(calls[0].Payload) != literal {
|
|
t.Fatalf("payload %s: calls = %+v", literal, calls)
|
|
}
|
|
}
|
|
env.expect(t, http.StatusOK, http.MethodPost, widget, `{}`, "bearer")
|
|
calls := env.spy.take()
|
|
if len(calls) != 1 || len(calls[0].Payload) != 0 || calls[0].Payload != nil {
|
|
t.Fatalf("absent payload: calls = %+v", calls)
|
|
}
|
|
})
|
|
|
|
t.Run("no data key when the action returns none", func(t *testing.T) {
|
|
for _, path := range []string{widget, toolbar} {
|
|
rec := env.expect(t, http.StatusOK, http.MethodPost, path, `{}`, "bearer")
|
|
var body struct {
|
|
Data map[string]any `json:"data"`
|
|
}
|
|
if err := json.Unmarshal(rec.Body.Bytes(), &body); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, ok := body.Data["data"]; ok {
|
|
t.Fatalf("%s carried a data key: %s", path, rec.Body.String())
|
|
}
|
|
}
|
|
env.spy.take()
|
|
})
|
|
|
|
t.Run("payload cap", func(t *testing.T) {
|
|
fits := `{"payload":"` + strings.Repeat("a", 65534) + `"}`
|
|
env.expect(t, http.StatusOK, http.MethodPost, widget, fits, "bearer")
|
|
if calls := env.spy.take(); len(calls) != 1 || len(calls[0].Payload) != 65536 {
|
|
t.Fatalf("64 KiB payload: calls = %d", len(calls))
|
|
}
|
|
over := `{"payload":"` + strings.Repeat("a", 65535) + `"}`
|
|
rec := env.expect(t, http.StatusUnprocessableEntity, http.MethodPost, widget, over, "bearer")
|
|
actErrorCode(t, rec.Body.Bytes(), "validation_failed")
|
|
if !strings.Contains(rec.Body.String(), `"body"`) {
|
|
t.Fatalf("422 without a body detail: %s", rec.Body.String())
|
|
}
|
|
if calls := env.spy.take(); len(calls) != 0 {
|
|
t.Fatalf("action ran for an oversized payload: %d calls", len(calls))
|
|
}
|
|
})
|
|
|
|
t.Run("toolbar and record routes refuse a payload", func(t *testing.T) {
|
|
for _, body := range []string{`{"payload":{}}`, `{"payload":null}`, `{"payload":1}`} {
|
|
rec := env.expect(t, http.StatusUnprocessableEntity, http.MethodPost, toolbar, body, "bearer")
|
|
actErrorCode(t, rec.Body.Bytes(), "validation_failed")
|
|
}
|
|
if calls := env.spy.take(); len(calls) != 0 {
|
|
t.Fatalf("toolbar ran with a payload: %+v", calls)
|
|
}
|
|
roster, rdb := newRosterEnv(t)
|
|
person := rosterInsert(t, rdb, rosterPerson{Tenant: "acme", Name: "Pat", Email: "pat@example.test"})
|
|
rec := roster.expect(t, http.StatusUnprocessableEntity, http.MethodPost, rosterPath(person, "/actions/activate"), `{"payload":1}`, "bearer")
|
|
actErrorCode(t, rec.Body.Bytes(), "validation_failed")
|
|
if rosterLoad(t, rdb, person).Active {
|
|
t.Fatal("a record action ran with a payload")
|
|
}
|
|
})
|
|
|
|
t.Run("data over 256 KiB or unencodable is an opaque 500", func(t *testing.T) {
|
|
for _, literal := range []string{`"big"`, `"nan"`} {
|
|
rec := env.expect(t, http.StatusInternalServerError, http.MethodPost, widget, `{"payload":`+literal+`}`, "bearer")
|
|
actErrorCode(t, rec.Body.Bytes(), "error")
|
|
if strings.Contains(rec.Body.String(), "xxxx") {
|
|
t.Fatalf("500 leaked the data: %d bytes", rec.Body.Len())
|
|
}
|
|
}
|
|
env.spy.take()
|
|
})
|
|
}
|