Skip to content
quarry Artifact [718a76a9a7]
Login

Artifact [718a76a9a7]

Artifact 718a76a9a766f533c975b0344b2406a252e2c242dd0aaa7bc1d6632ef23ecf9d:


package api

import (
	"context"
	"net/http"
	"net/http/httptest"
	"strings"
	"testing"

	"craftdesign.group/quarry/internal/oltp"
	"craftdesign.group/quarry/internal/ops"
)

// A JSON field of the wrong type is refused in the spec's words, never
// the decoder's.
//
// forum.moderation took `enabled` as a boolean and a caller sending the
// old "on" was told `cannot unmarshal string into Go struct field
// moderationSetIn.enabled of type bool` (jva, #1085): a struct and a
// Go type the caller has never seen, where every other refusal on the
// body says which field and what it takes. One rewording at the decode,
// so every op's bodies read the same.
func TestAJSONFieldOfTheWrongTypeIsRefusedInTheSpecsWords(t *testing.T) {
	type in struct {
		Flag  bool     `json:"flag"`
		Count int      `json:"count"`
		Names []string `json:"names"`
	}
	r := ops.NewRegistry()
	if err := ops.Register(r, ops.Op[in, struct{}]{
		Name: "typed.post", Kind: ops.Command, Auth: ops.Anonymous,
		Route: "POST /api/v1/typed", Doc: "test double with a boolean, a number and a list",
		Run: func(context.Context, in) (struct{}, error) { return struct{}{}, nil },
	}); err != nil {
		t.Fatal(err)
	}
	h, err := RouterWithCaps(r, "test", func(_, _ string) CapSet { return oltp.CapSet("s") }, nil)
	if err != nil {
		t.Fatal(err)
	}
	for _, c := range []struct{ body, want string }{
		{`{"flag": "on"}`, `field "flag" must be a boolean, not a string`},
		{`{"count": "3"}`, `field "count" must be a number, not a string`},
		{`{"names": "x"}`, `field "names" must be a list, not a string`},
		{`{"flag": 1}`, `field "flag" must be a boolean, not a number`},
	} {
		req := httptest.NewRequest("POST", "/api/v1/typed", strings.NewReader(c.body))
		req.Header.Set("Content-Type", "application/json")
		w := httptest.NewRecorder()
		h.ServeHTTP(w, req)
		if w.Code != http.StatusBadRequest {
			t.Errorf("%s: %d %s", c.body, w.Code, w.Body.String())
			continue
		}
		got := errorOf(t, w.Body.Bytes())
		if !strings.Contains(got, c.want) || strings.Contains(got, "Go struct") || strings.Contains(got, "unmarshal") {
			t.Errorf("%s: %q, want it to say %q and nothing about Go", c.body, got, c.want)
		}
	}
	// The right types still land.
	req := httptest.NewRequest("POST", "/api/v1/typed", strings.NewReader(`{"flag": true, "count": 3, "names": ["a"]}`))
	req.Header.Set("Content-Type", "application/json")
	w := httptest.NewRecorder()
	h.ServeHTTP(w, req)
	if w.Code != http.StatusOK {
		t.Errorf("well-typed body: %d %s", w.Code, w.Body.String())
	}
}