lbcd/v2/btcjson/cmdparse_test.go
Dave Collins cb7c24141a Reimagine btcjson package with version 2.
This commit implements a reimagining of the way the btcjson package
functions based upon how the project has evolved and lessons learned while
using it since it was first written.  It therefore contains significant
changes to the API.  For now, it has been implemented in a v2 subdirectory
to prevent breaking existing callers, but the ultimate goal is to update
all callers to use the new version and then to replace the old API with
the new one.

This also removes the need for the btcws completely since those commands
have been rolled in.

The following is an overview of the changes and some reasoning behind why
they were made:

- The infrastructure has been completely changed to be reflection based instead
  of requiring thousands and thousands of lines of manual, and therefore error
  prone, marshal/unmarshal code
  - This makes it much easier to add new commands without making marshalling
    mistakes since it is simply a struct definition and a call to register that
    new struct (plus a trivial New<foo>Cmd function and tests, of course)
  - It also makes it much easier to gain a lot of information from simply
    looking at the struct definition which was previously not possible
    such as the order of the parameters, which parameters are required
    versus optional, and what the default values for optional parameters
    are
- Each command now has usage flags associated with them that can be
  queried which are intended to allow classification of the commands such
  as for chain server and wallet server and websocket-only
- The help infrastructure has been completely redone to provide automatic
  generation with caller provided description map and result types. This
  is in contrast to the previous method of providing the help directly
  which meant it would end up in the binary of anything that imported the
  package
- Many of the structs have been renamed to use the terminology from the
  JSON-RPC
  specification:
  - RawCmd/Message is now only a single struct named Request to reflect the fact
    it is a JSON-RPC request
  - Error is now called RPCError to reflect the fact it is specifically an RPC
    error as opposed to many of the other errors that are possible
    - All RPC error codes except the standard JSON-RPC 2.0 errors have been
      converted from full structs to only codes since an audit of the codebase
      has shown that the messages are overridden the vast majority of the time
      with specifics (as they should be) and removing them also avoids the
      temptation to return non-specific, and therefore not as helpful, error
      messages
  - There is now an Error which provides a type assertable error with
    error codes so callers can better ascertain failure reasons
    programatically
- The ID is no longer a part of the command and is instead specified at the time
  the command is marshalled into a JSON-RPC request.  This aligns better with
  the way JSON-RPC functions since it is the caller who manages the ID that is
  sent with any given _request_, not the package
- All <Foo>Cmd structs now treat non-pointers as required fields and pointers as
  optional fields
- All New<Foo>Cmd functions now accept the exact number of parameters, with
  pointers to the appropriate type for optional parameters
  - This is preferrable to the old vararg syntax since it means the code will
    fail to compile if the optional arguments are changed now which helps
    prevent errors creep in over time from missed modifications to optional args
- All of the connection related code has been completely eliminated since this
  package is not intended to used a client, rather it is intended to provide
  the infrastructure needed to marshal/unmarshal Bitcoin-specific JSON-RPC
  requests and replies from static types
  - The btcrpcclient package provides a robust client with connection management
    and higher-level types that in turn uses the primitives provided by this
    package
  - Even if the caller does not wish to use btcrpcclient for some reason, they
    should still be responsible for connection management since they might want
    to use any number of connection features which the package would not
    necessarily support
- Synced a few of the commands that have added new optional fields that
  have since been added to Bitcoin Core
- Includes all of the commands and notifications that were previously in
  btcws
- Now provides 100% test coverage with parallel tests
- The code is completely golint and go vet clean

This has the side effect of addressing nearly everything in, and therefore
closes #26.

Also fixes #18 and closes #19.
2015-02-19 00:41:07 -06:00

519 lines
12 KiB
Go

// Copyright (c) 2014 Conformal Systems LLC.
// Use of this source code is governed by an ISC
// license that can be found in the LICENSE file.
package btcjson_test
import (
"encoding/json"
"math"
"reflect"
"testing"
"github.com/btcsuite/btcjson/v2/btcjson"
)
// TestAssignField tests the assignField function handles supported combinations
// properly.
func TestAssignField(t *testing.T) {
t.Parallel()
tests := []struct {
name string
dest interface{}
src interface{}
expected interface{}
}{
{
name: "same types",
dest: int8(0),
src: int8(100),
expected: int8(100),
},
{
name: "same types - more source pointers",
dest: int8(0),
src: func() interface{} {
i := int8(100)
return &i
}(),
expected: int8(100),
},
{
name: "same types - more dest pointers",
dest: func() interface{} {
i := int8(0)
return &i
}(),
src: int8(100),
expected: int8(100),
},
{
name: "convertible types - more source pointers",
dest: int16(0),
src: func() interface{} {
i := int8(100)
return &i
}(),
expected: int16(100),
},
{
name: "convertible types - both pointers",
dest: func() interface{} {
i := int8(0)
return &i
}(),
src: func() interface{} {
i := int16(100)
return &i
}(),
expected: int8(100),
},
{
name: "convertible types - int16 -> int8",
dest: int8(0),
src: int16(100),
expected: int8(100),
},
{
name: "convertible types - int16 -> uint8",
dest: uint8(0),
src: int16(100),
expected: uint8(100),
},
{
name: "convertible types - uint16 -> int8",
dest: int8(0),
src: uint16(100),
expected: int8(100),
},
{
name: "convertible types - uint16 -> uint8",
dest: uint8(0),
src: uint16(100),
expected: uint8(100),
},
{
name: "convertible types - float32 -> float64",
dest: float64(0),
src: float32(1.5),
expected: float64(1.5),
},
{
name: "convertible types - float64 -> float32",
dest: float32(0),
src: float64(1.5),
expected: float32(1.5),
},
{
name: "convertible types - string -> bool",
dest: false,
src: "true",
expected: true,
},
{
name: "convertible types - string -> int8",
dest: int8(0),
src: "100",
expected: int8(100),
},
{
name: "convertible types - string -> uint8",
dest: uint8(0),
src: "100",
expected: uint8(100),
},
{
name: "convertible types - string -> float32",
dest: float32(0),
src: "1.5",
expected: float32(1.5),
},
{
name: "convertible types - typecase string -> string",
dest: "",
src: func() interface{} {
type foo string
return foo("foo")
}(),
expected: "foo",
},
{
name: "convertible types - string -> array",
dest: [2]string{},
src: `["test","test2"]`,
expected: [2]string{"test", "test2"},
},
{
name: "convertible types - string -> slice",
dest: []string{},
src: `["test","test2"]`,
expected: []string{"test", "test2"},
},
{
name: "convertible types - string -> struct",
dest: struct{ A int }{},
src: `{"A":100}`,
expected: struct{ A int }{100},
},
{
name: "convertible types - string -> map",
dest: map[string]float64{},
src: `{"1Address":1.5}`,
expected: map[string]float64{"1Address": 1.5},
},
}
t.Logf("Running %d tests", len(tests))
for i, test := range tests {
dst := reflect.New(reflect.TypeOf(test.dest)).Elem()
src := reflect.ValueOf(test.src)
err := btcjson.TstAssignField(1, "testField", dst, src)
if err != nil {
t.Errorf("Test #%d (%s) unexpected error: %v", i,
test.name, err)
continue
}
// Inidirect through to the base types to ensure their values
// are the same.
for dst.Kind() == reflect.Ptr {
dst = dst.Elem()
}
if !reflect.DeepEqual(dst.Interface(), test.expected) {
t.Errorf("Test #%d (%s) unexpected value - got %v, "+
"want %v", i, test.name, dst.Interface(),
test.expected)
continue
}
}
}
// TestAssignFieldErrors tests the assignField function error paths.
func TestAssignFieldErrors(t *testing.T) {
t.Parallel()
tests := []struct {
name string
dest interface{}
src interface{}
err btcjson.Error
}{
{
name: "general incompatible int -> string",
dest: string(0),
src: int(0),
err: btcjson.Error{ErrorCode: btcjson.ErrInvalidType},
},
{
name: "overflow source int -> dest int",
dest: int8(0),
src: int(128),
err: btcjson.Error{ErrorCode: btcjson.ErrInvalidType},
},
{
name: "overflow source int -> dest uint",
dest: uint8(0),
src: int(256),
err: btcjson.Error{ErrorCode: btcjson.ErrInvalidType},
},
{
name: "int -> float",
dest: float32(0),
src: int(256),
err: btcjson.Error{ErrorCode: btcjson.ErrInvalidType},
},
{
name: "overflow source uint64 -> dest int64",
dest: int64(0),
src: uint64(1 << 63),
err: btcjson.Error{ErrorCode: btcjson.ErrInvalidType},
},
{
name: "overflow source uint -> dest int",
dest: int8(0),
src: uint(128),
err: btcjson.Error{ErrorCode: btcjson.ErrInvalidType},
},
{
name: "overflow source uint -> dest uint",
dest: uint8(0),
src: uint(256),
err: btcjson.Error{ErrorCode: btcjson.ErrInvalidType},
},
{
name: "uint -> float",
dest: float32(0),
src: uint(256),
err: btcjson.Error{ErrorCode: btcjson.ErrInvalidType},
},
{
name: "float -> int",
dest: int(0),
src: float32(1.0),
err: btcjson.Error{ErrorCode: btcjson.ErrInvalidType},
},
{
name: "overflow float64 -> float32",
dest: float32(0),
src: float64(math.MaxFloat64),
err: btcjson.Error{ErrorCode: btcjson.ErrInvalidType},
},
{
name: "invalid string -> bool",
dest: true,
src: "foo",
err: btcjson.Error{ErrorCode: btcjson.ErrInvalidType},
},
{
name: "invalid string -> int",
dest: int8(0),
src: "foo",
err: btcjson.Error{ErrorCode: btcjson.ErrInvalidType},
},
{
name: "overflow string -> int",
dest: int8(0),
src: "128",
err: btcjson.Error{ErrorCode: btcjson.ErrInvalidType},
},
{
name: "invalid string -> uint",
dest: uint8(0),
src: "foo",
err: btcjson.Error{ErrorCode: btcjson.ErrInvalidType},
},
{
name: "overflow string -> uint",
dest: uint8(0),
src: "256",
err: btcjson.Error{ErrorCode: btcjson.ErrInvalidType},
},
{
name: "invalid string -> float",
dest: float32(0),
src: "foo",
err: btcjson.Error{ErrorCode: btcjson.ErrInvalidType},
},
{
name: "overflow string -> float",
dest: float32(0),
src: "1.7976931348623157e+308",
err: btcjson.Error{ErrorCode: btcjson.ErrInvalidType},
},
{
name: "invalid string -> array",
dest: [3]int{},
src: "foo",
err: btcjson.Error{ErrorCode: btcjson.ErrInvalidType},
},
{
name: "invalid string -> slice",
dest: []int{},
src: "foo",
err: btcjson.Error{ErrorCode: btcjson.ErrInvalidType},
},
{
name: "invalid string -> struct",
dest: struct{ A int }{},
src: "foo",
err: btcjson.Error{ErrorCode: btcjson.ErrInvalidType},
},
{
name: "invalid string -> map",
dest: map[string]int{},
src: "foo",
err: btcjson.Error{ErrorCode: btcjson.ErrInvalidType},
},
}
t.Logf("Running %d tests", len(tests))
for i, test := range tests {
dst := reflect.New(reflect.TypeOf(test.dest)).Elem()
src := reflect.ValueOf(test.src)
err := btcjson.TstAssignField(1, "testField", dst, src)
if reflect.TypeOf(err) != reflect.TypeOf(test.err) {
t.Errorf("Test #%d (%s) wrong error - got %T (%[3]v), "+
"want %T", i, test.name, err, test.err)
continue
}
gotErrorCode := err.(btcjson.Error).ErrorCode
if gotErrorCode != test.err.ErrorCode {
t.Errorf("Test #%d (%s) mismatched error code - got "+
"%v (%v), want %v", i, test.name, gotErrorCode,
err, test.err.ErrorCode)
continue
}
}
}
// TestNewCmdErrors ensures the error paths of NewCmd behave as expected.
func TestNewCmdErrors(t *testing.T) {
t.Parallel()
tests := []struct {
name string
method string
args []interface{}
err btcjson.Error
}{
{
name: "unregistered command",
method: "boguscommand",
args: []interface{}{},
err: btcjson.Error{ErrorCode: btcjson.ErrUnregisteredMethod},
},
{
name: "too few parameters to command with required + optional",
method: "getblock",
args: []interface{}{},
err: btcjson.Error{ErrorCode: btcjson.ErrNumParams},
},
{
name: "too many parameters to command with no optional",
method: "getblockcount",
args: []interface{}{"123"},
err: btcjson.Error{ErrorCode: btcjson.ErrNumParams},
},
{
name: "incorrect parameter type",
method: "getblock",
args: []interface{}{1},
err: btcjson.Error{ErrorCode: btcjson.ErrInvalidType},
},
}
t.Logf("Running %d tests", len(tests))
for i, test := range tests {
_, err := btcjson.NewCmd(test.method, test.args...)
if reflect.TypeOf(err) != reflect.TypeOf(test.err) {
t.Errorf("Test #%d (%s) wrong error - got %T (%[2]v), "+
"want %T", i, test.name, err, test.err)
continue
}
gotErrorCode := err.(btcjson.Error).ErrorCode
if gotErrorCode != test.err.ErrorCode {
t.Errorf("Test #%d (%s) mismatched error code - got "+
"%v (%v), want %v", i, test.name, gotErrorCode,
err, test.err.ErrorCode)
continue
}
}
}
// TestMarshalCmdErrors tests the error paths of the MarshalCmd function.
func TestMarshalCmdErrors(t *testing.T) {
t.Parallel()
tests := []struct {
name string
id interface{}
cmd interface{}
err btcjson.Error
}{
{
name: "unregistered type",
id: 1,
cmd: (*int)(nil),
err: btcjson.Error{ErrorCode: btcjson.ErrUnregisteredMethod},
},
{
name: "nil instance of registered type",
id: 1,
cmd: (*btcjson.GetBlockCmd)(nil),
err: btcjson.Error{ErrorCode: btcjson.ErrInvalidType},
},
{
name: "nil instance of registered type",
id: []int{0, 1},
cmd: &btcjson.GetBlockCountCmd{},
err: btcjson.Error{ErrorCode: btcjson.ErrInvalidType},
},
}
t.Logf("Running %d tests", len(tests))
for i, test := range tests {
_, err := btcjson.MarshalCmd(test.id, test.cmd)
if reflect.TypeOf(err) != reflect.TypeOf(test.err) {
t.Errorf("Test #%d (%s) wrong error - got %T (%[2]v), "+
"want %T", i, test.name, err, test.err)
continue
}
gotErrorCode := err.(btcjson.Error).ErrorCode
if gotErrorCode != test.err.ErrorCode {
t.Errorf("Test #%d (%s) mismatched error code - got "+
"%v (%v), want %v", i, test.name, gotErrorCode,
err, test.err.ErrorCode)
continue
}
}
}
// TestUnmarshalCmdErrors tests the error paths of the UnmarshalCmd function.
func TestUnmarshalCmdErrors(t *testing.T) {
t.Parallel()
tests := []struct {
name string
request btcjson.Request
err btcjson.Error
}{
{
name: "unregistered type",
request: btcjson.Request{
Jsonrpc: "1.0",
Method: "bogusmethod",
Params: nil,
ID: nil,
},
err: btcjson.Error{ErrorCode: btcjson.ErrUnregisteredMethod},
},
{
name: "incorrect number of params",
request: btcjson.Request{
Jsonrpc: "1.0",
Method: "getblockcount",
Params: []json.RawMessage{[]byte(`"bogusparam"`)},
ID: nil,
},
err: btcjson.Error{ErrorCode: btcjson.ErrNumParams},
},
{
name: "invalid type for a parameter",
request: btcjson.Request{
Jsonrpc: "1.0",
Method: "getblock",
Params: []json.RawMessage{[]byte("1")},
ID: nil,
},
err: btcjson.Error{ErrorCode: btcjson.ErrInvalidType},
},
{
name: "invalid JSON for a parameter",
request: btcjson.Request{
Jsonrpc: "1.0",
Method: "getblock",
Params: []json.RawMessage{[]byte(`"1`)},
ID: nil,
},
err: btcjson.Error{ErrorCode: btcjson.ErrInvalidType},
},
}
t.Logf("Running %d tests", len(tests))
for i, test := range tests {
_, err := btcjson.UnmarshalCmd(&test.request)
if reflect.TypeOf(err) != reflect.TypeOf(test.err) {
t.Errorf("Test #%d (%s) wrong error - got %T (%[2]v), "+
"want %T", i, test.name, err, test.err)
continue
}
gotErrorCode := err.(btcjson.Error).ErrorCode
if gotErrorCode != test.err.ErrorCode {
t.Errorf("Test #%d (%s) mismatched error code - got "+
"%v (%v), want %v", i, test.name, gotErrorCode,
err, test.err.ErrorCode)
continue
}
}
}