diff --git a/jhttp/getter_test.go b/jhttp/getter_test.go index 6046db8..98ceb7d 100644 --- a/jhttp/getter_test.go +++ b/jhttp/getter_test.go @@ -4,7 +4,6 @@ package jhttp_test import ( "context" - "encoding/hex" "fmt" "io" "net/http" @@ -17,6 +16,7 @@ import ( "github.com/creachadair/jrpc2/handler" "github.com/creachadair/jrpc2/jhttp" "github.com/fortytw2/leaktest" + "github.com/google/go-cmp/cmp" ) func TestGetter(t *testing.T) { @@ -70,6 +70,98 @@ func TestGetter(t *testing.T) { }) } +func TestParseQuery(t *testing.T) { + tests := []struct { + url string + body string + method string + want interface{} + errText string + }{ + // Error: Missing method name. + {"http://localhost:2112/", "", "", nil, "empty URL path"}, + + // No parameters. + {"http://localhost/foo", "", "foo", nil, ""}, + + // Unbalanced double-quoted string. + {"https://fuzz.ball/foo?bad=%22xyz", "", "foo", nil, "missing string quote"}, + {"https://fuzz.ball/bar?bad=xyz%22", "", "bar", nil, "missing string quote"}, + + // Unbalanced single-quoted string. + {`http://stripe/sister?bad='invalid`, "", "sister", nil, "missing bytes quote"}, + {`http://stripe/sister?bad=invalid'`, "", "sister", nil, "missing bytes quote"}, + + // Invalid byte string. + {`http://green.as/balls?bad='NOT%20VALID'`, "", "balls", nil, "decoding bytes"}, + + // Invalid double-quoted string. + {`http://black.as/sin?bad=%22a%5Cx25%22`, "", "sin", nil, "invalid character"}, + + // Valid: Single-quoted byte string (base64). + {`http://fast.as.hell/and?twice='YXMgcHJldHR5IGFzIHlvdQ=='`, + "", "and", map[string]interface{}{ + "twice": []byte("as pretty as you"), + }, ""}, + + // Valid: Unquoted strings and null. + {`http://head.like/a-hole?black=as&your=null&soul`, + "", "a-hole", map[string]interface{}{ + "black": "as", + "your": nil, + "soul": "", + }, ""}, + + // Valid: Quoted strings, numbers, Booleans. + {`http://foo.com:1999/go/west/?alpha=%22xyz%22&bravo=3&charlie=true&delta=false&echo=3.2`, + "", "go/west", map[string]interface{}{ + "alpha": "xyz", + "bravo": int64(3), + "charlie": true, + "delta": false, + "echo": 3.2, + }, ""}, + + // Valid: Form-encoded query in the request body. + {`http://buz.org:2013/bodyblow`, + "alpha=%22pdq%22&bravo=-19.4&charlie=false", "bodyblow", map[string]interface{}{ + "alpha": "pdq", + "bravo": float64(-19.4), + "charlie": false, + }, ""}, + } + for _, test := range tests { + t.Run("ParseQuery", func(t *testing.T) { + req, err := http.NewRequest("PUT", test.url, strings.NewReader(test.body)) + if err != nil { + t.Fatalf("New request for %q failed: %v", test.url, err) + } + if test.body != "" { + req.Header.Set("Content-Type", "application/x-www-form-urlencoded") + } + + method, params, err := jhttp.ParseQuery(req) + if err != nil { + if test.errText == "" { + t.Fatalf("ParseQuery failed: %v", err) + } else if !strings.Contains(err.Error(), test.errText) { + t.Fatalf("ParseQuery: got error %v, want %q", err, test.errText) + } + } else if test.errText != "" { + t.Fatalf("ParseQuery: got method %q, params %+v, wanted error %q", + method, params, test.errText) + } else { + if method != test.method { + t.Errorf("ParseQuery method: got %q, want %q", method, test.method) + } + if diff := cmp.Diff(test.want, params); diff != "" { + t.Errorf("Wrong params: (-want, +got)\n%s", diff) + } + } + }) + } +} + func TestGetter_parseRequest(t *testing.T) { defer leaktest.Check(t)() @@ -84,22 +176,15 @@ func TestGetter_parseRequest(t *testing.T) { if err := req.ParseForm(); err != nil { return "", nil, err } - params := make(map[string]interface{}) - for key := range req.Form { - val := req.Form.Get(key) - v, err := strconv.Atoi(val) - if err == nil { - params[key] = v - continue - } - b, err := hex.DecodeString(val) - if err == nil { - params[key] = b - continue - } - params[key] = val + tag := req.Form.Get("tag") + val, err := strconv.ParseInt(req.Form.Get("value"), 10, 64) + if err != nil && req.Form.Get("value") != "" { + return "", nil, fmt.Errorf("invalid number: %w", err) } - return strings.TrimPrefix(req.URL.Path, "/x/"), params, nil + return strings.TrimPrefix(req.URL.Path, "/x/"), map[string]interface{}{ + "tag": tag, + "value": val, + }, nil }, }) defer checkClose(t, g) @@ -127,8 +212,8 @@ func TestGetter_parseRequest(t *testing.T) { }) t.Run("InternalError", func(t *testing.T) { // N.B. Parameter type does not match on the server side. - got := mustGet(t, url("x/format?tag=foo&value=bar"), http.StatusInternalServerError) - const want = `"code":-32602` // InvalidParams + got := mustGet(t, url("x/format?tag=foo&value=bar"), http.StatusBadRequest) + const want = `"code":-32700` // ParseError if !strings.Contains(got, want) { t.Errorf("Response body: got %#q, want %#q", got, want) }