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