2022-02-06 07:06:32 +00:00

324 lines
9.8 KiB
Go

// Copyright (C) 2017 Michael J. Fromberger. All Rights Reserved.
package jrpc2
// This file contains tests that need to inspect the internal details of the
// implementation to verify that the results are correct.
import (
"context"
"encoding/json"
"reflect"
"testing"
"time"
"github.com/creachadair/jrpc2/channel"
"github.com/creachadair/jrpc2/code"
"github.com/fortytw2/leaktest"
"github.com/google/go-cmp/cmp"
"github.com/google/go-cmp/cmp/cmpopts"
)
func TestParseRequests(t *testing.T) {
tests := []struct {
input string
want []*Request
err error
}{
// An empty batch is valid and produces no results.
{`[]`, nil, nil},
// An empty single request is invalid but returned anyway.
{`{}`, []*Request{{}}, ErrInvalidVersion},
// A valid notification.
{`{"jsonrpc":"2.0", "method": "foo", "params":[1, 2, 3]}`, []*Request{{
method: "foo",
params: json.RawMessage(`[1, 2, 3]`),
}}, nil},
// A valid request, with nil parameters.
{`{"jsonrpc":"2.0", "method": "foo", "id":10332, "params":null}`, []*Request{{
id: json.RawMessage("10332"), method: "foo",
}}, nil},
// A valid mixed batch.
{`[ {"jsonrpc": "2.0", "id": 1, "method": "A", "params": {}},
{"jsonrpc": "2.0", "params": [5], "method": "B"} ]`, []*Request{
{method: "A", id: json.RawMessage(`1`), params: json.RawMessage(`{}`)},
{method: "B", params: json.RawMessage(`[5]`)},
}, nil},
// An invalid batch.
{`[{"id": 37, "method": "complain", "params":[]}]`, []*Request{
{method: "complain", id: json.RawMessage(`37`), params: json.RawMessage(`[]`)},
}, ErrInvalidVersion},
// A broken request.
{`{`, nil, Errorf(code.ParseError, "invalid request value")},
// A broken batch.
{`["bad"{]`, nil, Errorf(code.ParseError, "invalid request value")},
}
for _, test := range tests {
got, err := ParseRequests([]byte(test.input))
if !errEQ(err, test.err) {
t.Errorf("ParseRequests(%#q): got error %v, want %v", test.input, err, test.err)
continue
}
diff := cmp.Diff(test.want, got, cmp.AllowUnexported(Request{}), cmpopts.EquateEmpty())
if diff != "" {
t.Errorf("ParseRequests(%#q): wrong result (-want, +got):\n%s", test.input, diff)
}
}
}
func errEQ(x, y error) bool {
if x == nil {
return y == nil
} else if y == nil {
return false
}
return code.FromError(x) == code.FromError(y) && x.Error() == y.Error()
}
func TestRequest_UnmarshalParams(t *testing.T) {
type xy struct {
X int `json:"x"`
Y bool `json:"y"`
}
tests := []struct {
input string
want interface{}
pstring string
code code.Code
}{
// If parameters are set, the target should be updated.
{`{"jsonrpc":"2.0", "id":1, "method":"X", "params":[1,2]}`, []int{1, 2}, "[1,2]", code.NoError},
// If parameters are null, the target should not be modified.
{`{"jsonrpc":"2.0", "id":2, "method":"Y", "params":null}`, "", "", code.NoError},
// If parameters are not set, the target should not be modified.
{`{"jsonrpc":"2.0", "id":2, "method":"Y"}`, 0, "", code.NoError},
// Unmarshaling should work into a struct as long as the fields match.
{`{"jsonrpc":"2.0", "id":3, "method":"Z", "params":{}}`, xy{}, "{}", code.NoError},
{`{"jsonrpc":"2.0", "id":4, "method":"Z", "params":{"x":17}}`, xy{X: 17}, `{"x":17}`, code.NoError},
{`{"jsonrpc":"2.0", "id":5, "method":"Z", "params":{"x":23, "y":true}}`,
xy{X: 23, Y: true}, `{"x":23, "y":true}`, code.NoError},
{`{"jsonrpc":"2.0", "id":6, "method":"Z", "params":{"x":23, "z":"wat"}}`,
xy{X: 23}, `{"x":23, "z":"wat"}`, code.NoError},
}
for _, test := range tests {
req, err := ParseRequests([]byte(test.input))
if err != nil {
t.Errorf("Parsing request %#q failed: %v", test.input, err)
} else if len(req) != 1 {
t.Fatalf("Wrong number of requests: got %d, want 1", len(req))
}
// Allocate a zero of the expected type to unmarshal into.
target := reflect.New(reflect.TypeOf(test.want)).Interface()
{
err := req[0].UnmarshalParams(target)
if got := code.FromError(err); got != test.code {
t.Errorf("UnmarshalParams error: got code %d, want %d [%v]", got, test.code, err)
}
if err != nil {
continue
}
}
// Dereference the target to get the value to compare.
got := reflect.ValueOf(target).Elem().Interface()
if diff := cmp.Diff(test.want, got); diff != "" {
t.Errorf("Parameters(%#q): wrong result (-want, +got):\n%s", test.input, diff)
}
// Check that the parameter string matches.
if got := req[0].ParamString(); got != test.pstring {
t.Errorf("ParamString(%#q): got %q, want %q", test.input, got, test.pstring)
}
}
}
type hmap map[string]Handler
func (h hmap) Assign(_ context.Context, method string) Handler { return h[method] }
func (h hmap) Names() []string { return nil }
// Verify that if the client context terminates during a request, the client
// will terminate and report failure.
func TestClient_contextCancellation(t *testing.T) {
defer leaktest.Check(t)()
started := make(chan struct{})
stopped := make(chan struct{})
cpipe, spipe := channel.Direct()
srv := NewServer(hmap{
"Hang": methodFunc(func(ctx context.Context, _ *Request) (interface{}, error) {
close(started) // signal that the method handler is running
select {
case <-stopped:
t.Log("Server unblocked by client completing")
return true, nil
case <-time.After(10 * time.Second):
t.Error("Server timed out before completion")
return false, nil
}
}),
}, nil).Start(spipe)
c := NewClient(cpipe, nil)
defer func() {
c.Close()
srv.Wait()
}()
// Start a call that will hang around until a timer expires or an explicit
// cancellation is received.
ctx, cancel := context.WithCancel(context.Background())
req, err := c.req(ctx, "Hang", nil)
if err != nil {
t.Fatalf("c.req(Hang) failed: %v", err)
}
rsps, err := c.send(ctx, jmessages{req})
if err != nil {
t.Fatalf("c.send(Hang) failed: %v", err)
}
// Wait for the handler to start so that we don't race with calling the
// handler on the server side, then cancel the context client-side.
<-started
cancel()
// The call should fail client side, in the usual way for a cancellation.
rsp := rsps[0]
rsp.wait()
close(stopped)
if err := rsp.Error(); err != nil {
if err.Code != code.Cancelled {
t.Errorf("Response error for %q: got %v, want %v", rsp.ID(), err, code.Cancelled)
}
} else {
t.Errorf("Response for %q: unexpectedly succeeded", rsp.ID())
}
}
func TestServer_specialMethods(t *testing.T) {
defer leaktest.Check(t)()
s := NewServer(hmap{
"rpc.nonesuch": methodFunc(func(context.Context, *Request) (interface{}, error) {
return "OK", nil
}),
"donkeybait": methodFunc(func(context.Context, *Request) (interface{}, error) {
return true, nil
}),
}, nil)
ctx := context.Background()
for _, name := range []string{rpcServerInfo, "donkeybait"} {
if got := s.assign(ctx, name); got == nil {
t.Errorf("s.assign(%s): no method assigned", name)
}
}
if got := s.assign(ctx, "rpc.nonesuch"); got != nil {
t.Errorf("s.assign(rpc.nonesuch): got %v, want nil", got)
}
}
// Verify that the option to remove the special behaviour of rpc.* methods can
// be correctly disabled by the server options.
func TestServer_disableBuiltinHook(t *testing.T) {
defer leaktest.Check(t)()
s := NewServer(hmap{
"rpc.nonesuch": methodFunc(func(context.Context, *Request) (interface{}, error) {
return "OK", nil
}),
}, &ServerOptions{DisableBuiltin: true})
ctx := context.Background()
// With builtins disabled, the default rpc.* methods should not get assigned.
for _, name := range []string{rpcServerInfo} {
if got := s.assign(ctx, name); got != nil {
t.Errorf("s.assign(%s): got %+v, wanted nil", name, got)
}
}
// However, user-assigned methods with this prefix should now work.
if got := s.assign(ctx, "rpc.nonesuch"); got == nil {
t.Error("s.assign(rpc.nonesuch): missing assignment")
}
}
// Verify that a batch request gets a batch reply, even if it is only a single
// request. The Client never sends requests like that, but the server needs to
// cope with it correctly.
func TestBatchReply(t *testing.T) {
defer leaktest.Check(t)()
cpipe, spipe := channel.Direct()
srv := NewServer(hmap{
"test": methodFunc(func(_ context.Context, req *Request) (interface{}, error) {
return req.Method() + " OK", nil
}),
}, nil).Start(spipe)
defer func() { cpipe.Close(); srv.Wait() }()
tests := []struct {
input, want string
}{
// A single-element batch gets returned as a batch.
{`[{"jsonrpc":"2.0", "id":1, "method":"test"}]`,
`[{"jsonrpc":"2.0","id":1,"result":"test OK"}]`},
// A single-element non-batch gets returned as a single reply.
{`{"jsonrpc":"2.0", "id":2, "method":"test"}`,
`{"jsonrpc":"2.0","id":2,"result":"test OK"}`},
}
for _, test := range tests {
if err := cpipe.Send([]byte(test.input)); err != nil {
t.Errorf("Send failed: %v", err)
}
rsp, err := cpipe.Recv()
if err != nil {
t.Errorf("Recv failed: %v", err)
}
if got := string(rsp); got != test.want {
t.Errorf("Batch reply:\n got %#q\nwant %#q", got, test.want)
}
}
}
func TestMarshalResponse(t *testing.T) {
tests := []struct {
id string
err *Error
result string
want string
}{
{"", nil, "", `{"jsonrpc":"2.0"}`},
{"null", nil, "", `{"jsonrpc":"2.0","id":null}`},
{"123", Errorf(code.ParseError, "failed"), "",
`{"jsonrpc":"2.0","id":123,"error":{"code":-32700,"message":"failed"}}`},
{"456", nil, `{"ok":true,"values":[4,5,6]}`,
`{"jsonrpc":"2.0","id":456,"result":{"ok":true,"values":[4,5,6]}}`},
}
for _, test := range tests {
rsp := &Response{id: test.id, err: test.err}
if test.err == nil {
rsp.result = json.RawMessage(test.result)
}
got, err := json.Marshal(rsp)
if err != nil {
t.Errorf("Marshaling %+v: unexpected error: %v", rsp, err)
} else if s := string(got); s != test.want {
t.Errorf("Marshaling %+v: got %#q, want %#q", rsp, s, test.want)
}
}
}