318 lines
8.9 KiB
Go
318 lines
8.9 KiB
Go
package handler
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"log"
|
|
"strings"
|
|
"testing"
|
|
|
|
"github.com/creachadair/jrpc2"
|
|
"github.com/google/go-cmp/cmp"
|
|
)
|
|
|
|
// Verify that the New function correctly handles the various type signatures
|
|
// it's advertised to support, and not others.
|
|
func TestNew(t *testing.T) {
|
|
tests := []struct {
|
|
v interface{}
|
|
bad bool
|
|
}{
|
|
{v: nil, bad: true}, // nil value
|
|
{v: "not a function", bad: true}, // not a function
|
|
|
|
// All the legal kinds...
|
|
{v: func(context.Context) error { return nil }},
|
|
{v: func(context.Context, *jrpc2.Request) (interface{}, error) { return nil, nil }},
|
|
{v: func(context.Context) (int, error) { return 0, nil }},
|
|
{v: func(context.Context, []int) error { return nil }},
|
|
{v: func(context.Context, []bool) (float64, error) { return 0, nil }},
|
|
{v: func(context.Context, ...string) (bool, error) { return false, nil }},
|
|
{v: func(context.Context, *jrpc2.Request) (byte, error) { return '0', nil }},
|
|
{v: func(context.Context) bool { return true }},
|
|
{v: func(context.Context, int) bool { return true }},
|
|
|
|
// Things that aren't supposed to work.
|
|
{v: func() error { return nil }, bad: true}, // wrong # of params
|
|
{v: func(a, b, c int) bool { return false }, bad: true}, // ...
|
|
{v: func(byte) {}, bad: true}, // wrong # of results
|
|
{v: func(byte) (int, bool, error) { return 0, true, nil }, bad: true}, // ...
|
|
{v: func(string) error { return nil }, bad: true}, // missing context
|
|
{v: func(a, b string) error { return nil }, bad: true}, // P1 is not context
|
|
{v: func(context.Context) (int, bool) { return 1, true }, bad: true}, // R2 is not error
|
|
|
|
//lint:ignore ST1008 verify permuted error position does not match
|
|
{v: func(context.Context) (error, float64) { return nil, 0 }, bad: true}, // ...
|
|
}
|
|
for _, test := range tests {
|
|
got, err := newHandler(test.v)
|
|
if !test.bad && err != nil {
|
|
t.Errorf("newHandler(%T): unexpected error: %v", test.v, err)
|
|
} else if test.bad && err == nil {
|
|
t.Errorf("newHandler(%T): got %+v, want error", test.v, got)
|
|
}
|
|
}
|
|
}
|
|
|
|
type dummy struct{}
|
|
|
|
func (dummy) Y1(context.Context) (int, error) { return 0, nil }
|
|
|
|
func (dummy) N1(string) {}
|
|
|
|
func (dummy) Y2(_ context.Context, vs ...int) (int, error) { return len(vs), nil }
|
|
|
|
func (dummy) N2() bool { return false }
|
|
|
|
func (dummy) Y3(context.Context) error { return errors.New("blah") }
|
|
|
|
//lint:ignore U1000 verify unexported methods are not assigned
|
|
func (dummy) n3(context.Context, []string) error { return nil }
|
|
|
|
// Verify that the NewService function obtains the correct functions.
|
|
func TestNewService(t *testing.T) {
|
|
var stub dummy
|
|
ctx := context.Background()
|
|
m := NewService(stub)
|
|
for _, test := range []string{"Y1", "Y2", "Y3", "N1", "N2", "n3", "foo"} {
|
|
got := m.Assign(ctx, test) != nil
|
|
want := strings.HasPrefix(test, "Y")
|
|
if got != want {
|
|
t.Errorf("Assign %q: got %v, want %v", test, got, want)
|
|
}
|
|
}
|
|
}
|
|
|
|
// Verify that a stub with no usable methods panics.
|
|
func TestEmptyService(t *testing.T) {
|
|
type empty struct{}
|
|
|
|
defer func() {
|
|
if x := recover(); x != nil {
|
|
t.Logf("Received expected panic: %v", x)
|
|
}
|
|
}()
|
|
m := NewService(empty{})
|
|
t.Fatalf("NewService(empty): got %v, want panic", m)
|
|
}
|
|
|
|
// Verify that a ServiceMap assigns names correctly.
|
|
func TestServiceMap(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
want bool
|
|
}{
|
|
{"nothing", false}, // not a known service
|
|
{"Test", false}, // no method in the service
|
|
{"Test.", false}, // empty method name in service
|
|
{"Test.Y1", true}, // OK
|
|
{"Test.Y2", true},
|
|
{"Test.Y3", true},
|
|
{"Test.Y4", false},
|
|
{"Test.N1", false},
|
|
{"Test.N2", false},
|
|
}
|
|
ctx := context.Background()
|
|
m := ServiceMap{"Test": NewService(dummy{})}
|
|
for _, test := range tests {
|
|
got := m.Assign(ctx, test.name) != nil
|
|
if got != test.want {
|
|
t.Errorf("Assign(%q): got %v, want %v", test.name, got, test.want)
|
|
}
|
|
}
|
|
|
|
got, want := m.Names(), []string{"Test.Y1", "Test.Y2", "Test.Y3"} // sorted
|
|
if diff := cmp.Diff(want, got); diff != "" {
|
|
t.Errorf("Wrong method names: (-want, +got)\n%s", diff)
|
|
}
|
|
}
|
|
|
|
// Verify that argument decoding works.
|
|
func TestArgs(t *testing.T) {
|
|
type stuff struct {
|
|
S string
|
|
Z int
|
|
F float64
|
|
B bool
|
|
}
|
|
var tmp stuff
|
|
tests := []struct {
|
|
json string
|
|
args Args
|
|
want stuff
|
|
ok bool
|
|
}{
|
|
{``, nil, stuff{}, false}, // incomplete
|
|
{`{}`, nil, stuff{}, false}, // wrong type (object)
|
|
{`true`, nil, stuff{}, false}, // wrong type (bool)
|
|
|
|
{`[]`, nil, stuff{}, true},
|
|
{`[ ]`, nil, stuff{}, true},
|
|
{`null`, nil, stuff{}, true},
|
|
|
|
// Respect order of arguments and values.
|
|
{`["foo", 25]`, Args{&tmp.S, &tmp.Z}, stuff{S: "foo", Z: 25}, true},
|
|
{`[25, "foo"]`, Args{&tmp.Z, &tmp.S}, stuff{S: "foo", Z: 25}, true},
|
|
|
|
{`[true, 3.5, "blah"]`, Args{&tmp.B, &tmp.F, &tmp.S},
|
|
stuff{S: "blah", B: true, F: 3.5}, true},
|
|
|
|
// Skip values with a nil corresponding argument.
|
|
{`[true, 101, "ignored"]`, Args{&tmp.B, &tmp.Z, nil},
|
|
stuff{B: true, Z: 101}, true},
|
|
{`[true, 101, "observed"]`, Args{&tmp.B, nil, &tmp.S},
|
|
stuff{B: true, S: "observed"}, true},
|
|
|
|
// Mismatched argument/value count.
|
|
{`["wrong"]`, Args{&tmp.S, &tmp.Z}, stuff{}, false}, // too few values
|
|
{`["really", "wrong"]`, Args{&tmp.S}, stuff{}, false}, // too many values
|
|
|
|
// Mismatched argument/value types.
|
|
{`["nope"]`, Args{&tmp.B}, stuff{}, false}, // wrong value type
|
|
{`[{}]`, Args{&tmp.F}, stuff{}, false}, // "
|
|
}
|
|
for _, test := range tests {
|
|
tmp = stuff{} // reset
|
|
if err := json.Unmarshal([]byte(test.json), &test.args); err != nil {
|
|
if test.ok {
|
|
t.Errorf("Unmarshal %#q: unexpected error: %v", test.json, err)
|
|
} else {
|
|
t.Logf("Unmarshal %#q: got expected error: %v", test.json, err)
|
|
}
|
|
continue
|
|
}
|
|
|
|
if diff := cmp.Diff(test.want, tmp); diff != "" {
|
|
t.Errorf("Unmarshal %#q: (-want, +got)\n%s", test.json, diff)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestArgsMarshal(t *testing.T) {
|
|
tests := []struct {
|
|
input []interface{}
|
|
want string
|
|
}{
|
|
{nil, "[]"},
|
|
{[]interface{}{}, "[]"},
|
|
{[]interface{}{12345}, "[12345]"},
|
|
{[]interface{}{"hey you"}, `["hey you"]`},
|
|
{[]interface{}{true, false}, "[true,false]"},
|
|
{[]interface{}{nil, 3.5}, "[null,3.5]"},
|
|
{[]interface{}{[]string{"a", "b"}, 33}, `[["a","b"],33]`},
|
|
{[]interface{}{1, map[string]string{
|
|
"ok": "yes",
|
|
}, 3}, `[1,{"ok":"yes"},3]`},
|
|
}
|
|
for _, test := range tests {
|
|
got, err := json.Marshal(Args(test.input))
|
|
if err != nil {
|
|
t.Errorf("Marshal %+v: unexpected error: %v", test.input, err)
|
|
} else if s := string(got); s != test.want {
|
|
t.Errorf("Marshal %+v: got %#q, want %#q", test.input, s, test.want)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestObjUnmarshal(t *testing.T) {
|
|
// N.B. Exported field names here to satisfy cmp.Diff.
|
|
type sub struct {
|
|
Foo string `json:"foo"`
|
|
}
|
|
type values struct {
|
|
Z int
|
|
S string
|
|
T sub
|
|
L []int
|
|
}
|
|
var v values
|
|
|
|
tests := []struct {
|
|
input string
|
|
obj Obj
|
|
want *values
|
|
}{
|
|
{"", nil, nil}, // error: empty text
|
|
{"true", nil, nil}, // error: not an object
|
|
{"[]", nil, nil}, // error: not an object
|
|
{`{"x":true}`, Obj{"x": &v.S}, nil}, // error: wrong type
|
|
|
|
// Nothing to unpack, no place to put it.
|
|
{"{}", nil, &values{}},
|
|
|
|
// Ignore non-matching keys but keep matching ones.
|
|
{`{"apple":true, "laser":"sauce"}`, Obj{"laser": &v.S}, &values{S: "sauce"}},
|
|
|
|
// Assign to matching fields including compound types.
|
|
{`{"x": 25, "q": "snark", "sub": {"foo":"bark"}, "yawp": false, "#":[5,3,2,4,7]}`, Obj{
|
|
"x": &v.Z,
|
|
"q": &v.S,
|
|
"sub": &v.T,
|
|
"#": &v.L,
|
|
}, &values{
|
|
Z: 25,
|
|
S: "snark",
|
|
T: sub{Foo: "bark"},
|
|
L: []int{5, 3, 2, 4, 7},
|
|
}},
|
|
}
|
|
for _, test := range tests {
|
|
v = values{} // reset
|
|
|
|
if err := json.Unmarshal([]byte(test.input), &test.obj); err != nil {
|
|
if test.want == nil {
|
|
t.Logf("Unmarshal: got expected error: %v", err)
|
|
} else {
|
|
t.Errorf("Unmarshal %q: %v", test.input, err)
|
|
}
|
|
continue
|
|
}
|
|
if diff := cmp.Diff(*test.want, v); diff != "" {
|
|
t.Errorf("Wrong values: (-want, +got)\n%s", diff)
|
|
}
|
|
}
|
|
}
|
|
|
|
func ExampleArgs_unmarshal() {
|
|
const input = `[25, false, "apple"]`
|
|
|
|
var count int
|
|
var item string
|
|
|
|
if err := json.Unmarshal([]byte(input), &Args{&count, nil, &item}); err != nil {
|
|
log.Fatalf("Decoding failed: %v", err)
|
|
}
|
|
fmt.Printf("count=%d, item=%q\n", count, item)
|
|
// Output:
|
|
// count=25, item="apple"
|
|
}
|
|
|
|
func ExampleArgs_marshal() {
|
|
bits, err := json.Marshal(Args{1, "foo", false, nil})
|
|
if err != nil {
|
|
log.Fatalf("Encoding failed: %v", err)
|
|
}
|
|
fmt.Println(string(bits))
|
|
// Output:
|
|
// [1,"foo",false,null]
|
|
}
|
|
|
|
func ExampleObj_unmarshal() {
|
|
const input = `{"uid": 501, "name": "P. T. Barnum", "tags": [1, 3]}`
|
|
|
|
var uid int
|
|
var name string
|
|
|
|
if err := json.Unmarshal([]byte(input), &Obj{
|
|
"uid": &uid,
|
|
"name": &name,
|
|
}); err != nil {
|
|
log.Fatalf("Decoding failed: %v", err)
|
|
}
|
|
fmt.Printf("uid=%d, name=%q\n", uid, name)
|
|
// Output:
|
|
// uid=501, name="P. T. Barnum"
|
|
}
|