diff --git a/rpc/client/interface.go b/rpc/client/interface.go index 33f5dace..0683d469 100644 --- a/rpc/client/interface.go +++ b/rpc/client/interface.go @@ -24,12 +24,10 @@ import ( "github.com/tendermint/tendermint/types" ) -type Client interface { - // general chain info - Status() (*ctypes.ResultStatus, error) - NetInfo() (*ctypes.ResultNetInfo, error) - Genesis() (*ctypes.ResultGenesis, error) - +// ABCIClient groups together the functionality that principally +// affects the ABCI app. In many cases this will be all we want, +// so we can accept an interface which is easier to mock +type ABCIClient interface { // reading from abci app ABCIInfo() (*ctypes.ResultABCIInfo, error) ABCIQuery(path string, data []byte, prove bool) (*ctypes.ResultABCIQuery, error) @@ -38,15 +36,44 @@ type Client interface { BroadcastTxCommit(tx types.Tx) (*ctypes.ResultBroadcastTxCommit, error) BroadcastTxAsync(tx types.Tx) (*ctypes.ResultBroadcastTx, error) BroadcastTxSync(tx types.Tx) (*ctypes.ResultBroadcastTx, error) +} - // validating block info - BlockchainInfo(minHeight, maxHeight int) (*ctypes.ResultBlockchainInfo, error) +// SignClient groups together the interfaces need to get valid +// signatures and prove anything about the chain +type SignClient interface { Block(height int) (*ctypes.ResultBlock, error) Commit(height int) (*ctypes.ResultCommit, error) Validators() (*ctypes.ResultValidators, error) +} + +// NetworkClient is general info about the network state. May not +// be needed usually. +// +// Not included in the Client interface, but generally implemented +// by concrete implementations. +type NetworkClient interface { + NetInfo() (*ctypes.ResultNetInfo, error) + // remove this??? + DialSeeds(seeds []string) (*ctypes.ResultDialSeeds, error) +} + +// HistoryClient shows us data from genesis to now in large chunks. +type HistoryClient interface { + Genesis() (*ctypes.ResultGenesis, error) + BlockchainInfo(minHeight, maxHeight int) (*ctypes.ResultBlockchainInfo, error) +} + +type StatusClient interface { + // general chain info + Status() (*ctypes.ResultStatus, error) +} + +type Client interface { + ABCIClient + SignClient + HistoryClient + StatusClient + // Note: doesn't include NetworkClient, is it important?? // TODO: add some sort of generic subscription mechanism... - - // remove this??? - // DialSeeds(seeds []string) (*ctypes.ResultDialSeeds, error) } diff --git a/rpc/client/mock/abci.go b/rpc/client/mock/abci.go new file mode 100644 index 00000000..95d6fc0a --- /dev/null +++ b/rpc/client/mock/abci.go @@ -0,0 +1,186 @@ +package mock + +import ( + abci "github.com/tendermint/abci/types" + "github.com/tendermint/tendermint/rpc/client" + ctypes "github.com/tendermint/tendermint/rpc/core/types" + "github.com/tendermint/tendermint/types" +) + +// ABCIApp will send all abci related request to the named app, +// so you can test app behavior from a client without needing +// an entire tendermint node +type ABCIApp struct { + App abci.Application +} + +func (a ABCIApp) _assertABCIClient() client.ABCIClient { + return a +} + +func (a ABCIApp) ABCIInfo() (*ctypes.ResultABCIInfo, error) { + return &ctypes.ResultABCIInfo{a.App.Info()}, nil +} + +func (a ABCIApp) ABCIQuery(path string, data []byte, prove bool) (*ctypes.ResultABCIQuery, error) { + q := a.App.Query(abci.RequestQuery{data, path, 0, prove}) + return &ctypes.ResultABCIQuery{q}, nil +} + +func (a ABCIApp) BroadcastTxCommit(tx types.Tx) (*ctypes.ResultBroadcastTxCommit, error) { + res := ctypes.ResultBroadcastTxCommit{} + c := a.App.CheckTx(tx) + res.CheckTx = &abci.ResponseCheckTx{c.Code, c.Data, c.Log} + if !c.IsOK() { + return &res, nil + } + d := a.App.DeliverTx(tx) + res.DeliverTx = &abci.ResponseDeliverTx{d.Code, d.Data, d.Log} + return &res, nil +} + +func (a ABCIApp) BroadcastTxAsync(tx types.Tx) (*ctypes.ResultBroadcastTx, error) { + d := a.App.DeliverTx(tx) + return &ctypes.ResultBroadcastTx{d.Code, d.Data, d.Log}, nil +} + +func (a ABCIApp) BroadcastTxSync(tx types.Tx) (*ctypes.ResultBroadcastTx, error) { + d := a.App.DeliverTx(tx) + return &ctypes.ResultBroadcastTx{d.Code, d.Data, d.Log}, nil +} + +// ABCIMock will send all abci related request to the named app, +// so you can test app behavior from a client without needing +// an entire tendermint node +type ABCIMock struct { + Info Call + Query Call + BroadcastCommit Call + Broadcast Call +} + +func (m ABCIMock) _assertABCIClient() client.ABCIClient { + return m +} + +func (m ABCIMock) ABCIInfo() (*ctypes.ResultABCIInfo, error) { + res, err := m.Info.GetResponse(nil) + if err != nil { + return nil, err + } + return &ctypes.ResultABCIInfo{res.(abci.ResponseInfo)}, nil +} + +func (m ABCIMock) ABCIQuery(path string, data []byte, prove bool) (*ctypes.ResultABCIQuery, error) { + res, err := m.Query.GetResponse(QueryArgs{path, data, prove}) + if err != nil { + return nil, err + } + return &ctypes.ResultABCIQuery{res.(abci.ResponseQuery)}, nil +} + +func (m ABCIMock) BroadcastTxCommit(tx types.Tx) (*ctypes.ResultBroadcastTxCommit, error) { + res, err := m.BroadcastCommit.GetResponse(tx) + if err != nil { + return nil, err + } + return res.(*ctypes.ResultBroadcastTxCommit), nil +} + +func (m ABCIMock) BroadcastTxAsync(tx types.Tx) (*ctypes.ResultBroadcastTx, error) { + res, err := m.Broadcast.GetResponse(tx) + if err != nil { + return nil, err + } + return res.(*ctypes.ResultBroadcastTx), nil +} + +func (m ABCIMock) BroadcastTxSync(tx types.Tx) (*ctypes.ResultBroadcastTx, error) { + res, err := m.Broadcast.GetResponse(tx) + if err != nil { + return nil, err + } + return res.(*ctypes.ResultBroadcastTx), nil +} + +// ABCIRecorder can wrap another type (ABCIApp, ABCIMock, or Client) +// and record all ABCI related calls. +type ABCIRecorder struct { + Client client.ABCIClient + Calls []Call +} + +func NewABCIRecorder(client client.ABCIClient) *ABCIRecorder { + return &ABCIRecorder{ + Client: client, + Calls: []Call{}, + } +} + +func (r *ABCIRecorder) _assertABCIClient() client.ABCIClient { + return r +} + +type QueryArgs struct { + Path string + Data []byte + Prove bool +} + +func (r *ABCIRecorder) addCall(call Call) { + r.Calls = append(r.Calls, call) +} + +func (r *ABCIRecorder) ABCIInfo() (*ctypes.ResultABCIInfo, error) { + res, err := r.Client.ABCIInfo() + r.addCall(Call{ + Name: "abci_info", + Response: res, + Error: err, + }) + return res, err +} + +func (r *ABCIRecorder) ABCIQuery(path string, data []byte, prove bool) (*ctypes.ResultABCIQuery, error) { + res, err := r.Client.ABCIQuery(path, data, prove) + r.addCall(Call{ + Name: "abci_query", + Args: QueryArgs{path, data, prove}, + Response: res, + Error: err, + }) + return res, err +} + +func (r *ABCIRecorder) BroadcastTxCommit(tx types.Tx) (*ctypes.ResultBroadcastTxCommit, error) { + res, err := r.Client.BroadcastTxCommit(tx) + r.addCall(Call{ + Name: "broadcast_tx_commit", + Args: tx, + Response: res, + Error: err, + }) + return res, err +} + +func (r *ABCIRecorder) BroadcastTxAsync(tx types.Tx) (*ctypes.ResultBroadcastTx, error) { + res, err := r.Client.BroadcastTxAsync(tx) + r.addCall(Call{ + Name: "broadcast_tx_async", + Args: tx, + Response: res, + Error: err, + }) + return res, err +} + +func (r *ABCIRecorder) BroadcastTxSync(tx types.Tx) (*ctypes.ResultBroadcastTx, error) { + res, err := r.Client.BroadcastTxSync(tx) + r.addCall(Call{ + Name: "broadcast_tx_sync", + Args: tx, + Response: res, + Error: err, + }) + return res, err +} diff --git a/rpc/client/mock/abci_test.go b/rpc/client/mock/abci_test.go new file mode 100644 index 00000000..50e767ed --- /dev/null +++ b/rpc/client/mock/abci_test.go @@ -0,0 +1,142 @@ +package mock_test + +import ( + "errors" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + abci "github.com/tendermint/abci/types" + ctypes "github.com/tendermint/tendermint/rpc/core/types" + "github.com/tendermint/tendermint/types" + + "github.com/tendermint/tendermint/rpc/client/mock" +) + +func TestABCIMock(t *testing.T) { + assert, require := assert.New(t), require.New(t) + + key, value := []byte("foo"), []byte("bar") + height := uint64(10) + goodTx := types.Tx{0x01, 0xff} + badTx := types.Tx{0x12, 0x21} + + m := mock.ABCIMock{ + Info: mock.Call{Error: errors.New("foobar")}, + Query: mock.Call{Response: abci.ResponseQuery{ + Key: key, + Value: value, + Height: height, + }}, + // Broadcast commit depends on call + BroadcastCommit: mock.Call{ + Args: goodTx, + Response: &ctypes.ResultBroadcastTxCommit{ + CheckTx: &abci.ResponseCheckTx{Data: []byte("stand")}, + DeliverTx: &abci.ResponseDeliverTx{Data: []byte("deliver")}, + }, + Error: errors.New("bad tx"), + }, + Broadcast: mock.Call{Error: errors.New("must commit")}, + } + + // now, let's try to make some calls + _, err := m.ABCIInfo() + require.NotNil(err) + assert.Equal("foobar", err.Error()) + + // query always returns the response + query, err := m.ABCIQuery("/", nil, false) + require.Nil(err) + require.NotNil(query) + assert.Equal(key, query.Response.GetKey()) + assert.Equal(value, query.Response.GetValue()) + assert.Equal(height, query.Response.GetHeight()) + + // non-commit calls always return errors + _, err = m.BroadcastTxSync(goodTx) + require.NotNil(err) + assert.Equal("must commit", err.Error()) + _, err = m.BroadcastTxAsync(goodTx) + require.NotNil(err) + assert.Equal("must commit", err.Error()) + + // commit depends on the input + _, err = m.BroadcastTxCommit(badTx) + require.NotNil(err) + assert.Equal("bad tx", err.Error()) + bres, err := m.BroadcastTxCommit(goodTx) + require.Nil(err, "%+v", err) + assert.EqualValues(0, bres.CheckTx.Code) + assert.EqualValues("stand", bres.CheckTx.Data) + assert.EqualValues("deliver", bres.DeliverTx.Data) +} + +func TestABCIRecorder(t *testing.T) { + assert, require := assert.New(t), require.New(t) + m := mock.ABCIMock{ + Info: mock.Call{Response: abci.ResponseInfo{ + Data: "data", + Version: "v0.9.9", + }}, + Query: mock.Call{Error: errors.New("query")}, + Broadcast: mock.Call{Error: errors.New("broadcast")}, + BroadcastCommit: mock.Call{Error: errors.New("broadcast_commit")}, + } + r := mock.NewABCIRecorder(m) + + require.Equal(0, len(r.Calls)) + + r.ABCIInfo() + r.ABCIQuery("path", []byte("data"), true) + require.Equal(2, len(r.Calls)) + + info := r.Calls[0] + assert.Equal("abci_info", info.Name) + assert.Nil(info.Error) + assert.Nil(info.Args) + require.NotNil(info.Response) + ir, ok := info.Response.(*ctypes.ResultABCIInfo) + require.True(ok) + assert.Equal("data", ir.Response.Data) + assert.Equal("v0.9.9", ir.Response.Version) + + query := r.Calls[1] + assert.Equal("abci_query", query.Name) + assert.Nil(query.Response) + require.NotNil(query.Error) + assert.Equal("query", query.Error.Error()) + require.NotNil(query.Args) + qa, ok := query.Args.(mock.QueryArgs) + require.True(ok) + assert.Equal("path", qa.Path) + assert.EqualValues("data", qa.Data) + assert.True(qa.Prove) + + // now add some broadcasts + txs := []types.Tx{{1}, {2}, {3}} + r.BroadcastTxCommit(txs[0]) + r.BroadcastTxSync(txs[1]) + r.BroadcastTxAsync(txs[2]) + + require.Equal(5, len(r.Calls)) + + bc := r.Calls[2] + assert.Equal("broadcast_tx_commit", bc.Name) + assert.Nil(bc.Response) + require.NotNil(bc.Error) + assert.EqualValues(bc.Args, txs[0]) + + bs := r.Calls[3] + assert.Equal("broadcast_tx_sync", bs.Name) + assert.Nil(bs.Response) + require.NotNil(bs.Error) + assert.EqualValues(bs.Args, txs[1]) + + ba := r.Calls[4] + assert.Equal("broadcast_tx_async", ba.Name) + assert.Nil(ba.Response) + require.NotNil(ba.Error) + assert.EqualValues(ba.Args, txs[2]) + +} diff --git a/rpc/client/mock/client.go b/rpc/client/mock/client.go new file mode 100644 index 00000000..ddeb1219 --- /dev/null +++ b/rpc/client/mock/client.go @@ -0,0 +1,169 @@ +/* +package mock returns a Client implementation that +accepts various (mock) implementations of the various methods. + +This implementation is useful for using in tests, when you don't +need a real server, but want a high-level of control about +the server response you want to mock (eg. error handling), +or if you just want to record the calls to verify in your tests. + +For real clients, you probably want the "http" package. If you +want to directly call a tendermint node in process, you can use the +"local" package. +*/ +package mock + +import ( + "reflect" + + "github.com/tendermint/tendermint/rpc/client" + "github.com/tendermint/tendermint/rpc/core" + ctypes "github.com/tendermint/tendermint/rpc/core/types" + "github.com/tendermint/tendermint/types" +) + +// Client wraps arbitrary implementations of the various interfaces. +// +// We provide a few choices to mock out each one in this package. +// Nothing hidden here, so no New function, just construct it from +// some parts, and swap them out them during the tests. +type Client struct { + client.ABCIClient + client.SignClient + client.HistoryClient + client.StatusClient +} + +func (c Client) _assertIsClient() client.Client { + return c +} + +// Call is used by recorders to save a call and response. +// It can also be used to configure mock responses. +// +type Call struct { + Name string + Args interface{} + Response interface{} + Error error +} + +// GetResponse will generate the apporiate response for us, when +// using the Call struct to configure a Mock handler. +// +// When configuring a response, if only one of Response or Error is +// set then that will always be returned. If both are set, then +// we return Response if the Args match the set args, Error otherwise. +func (c Call) GetResponse(args interface{}) (interface{}, error) { + // handle the case with no response + if c.Response == nil { + if c.Error == nil { + panic("Misconfigured call, you must set either Response or Error") + } + return nil, c.Error + } + // response without error + if c.Error == nil { + return c.Response, nil + } + // have both, we must check args.... + if reflect.DeepEqual(args, c.Args) { + return c.Response, nil + } + return nil, c.Error +} + +func (c Client) Status() (*ctypes.ResultStatus, error) { + return core.Status() +} + +func (c Client) ABCIInfo() (*ctypes.ResultABCIInfo, error) { + return core.ABCIInfo() +} + +func (c Client) ABCIQuery(path string, data []byte, prove bool) (*ctypes.ResultABCIQuery, error) { + return core.ABCIQuery(path, data, prove) +} + +func (c Client) BroadcastTxCommit(tx types.Tx) (*ctypes.ResultBroadcastTxCommit, error) { + return core.BroadcastTxCommit(tx) +} + +func (c Client) BroadcastTxAsync(tx types.Tx) (*ctypes.ResultBroadcastTx, error) { + return core.BroadcastTxAsync(tx) +} + +func (c Client) BroadcastTxSync(tx types.Tx) (*ctypes.ResultBroadcastTx, error) { + return core.BroadcastTxSync(tx) +} + +func (c Client) NetInfo() (*ctypes.ResultNetInfo, error) { + return core.NetInfo() +} + +func (c Client) DialSeeds(seeds []string) (*ctypes.ResultDialSeeds, error) { + return core.UnsafeDialSeeds(seeds) +} + +func (c Client) BlockchainInfo(minHeight, maxHeight int) (*ctypes.ResultBlockchainInfo, error) { + return core.BlockchainInfo(minHeight, maxHeight) +} + +func (c Client) Genesis() (*ctypes.ResultGenesis, error) { + return core.Genesis() +} + +func (c Client) Block(height int) (*ctypes.ResultBlock, error) { + return core.Block(height) +} + +func (c Client) Commit(height int) (*ctypes.ResultCommit, error) { + return core.Commit(height) +} + +func (c Client) Validators() (*ctypes.ResultValidators, error) { + return core.Validators() +} + +/** websocket event stuff here... **/ + +/* +// StartWebsocket starts up a websocket and a listener goroutine +// if already started, do nothing +func (c Client) StartWebsocket() error { + var err error + if c.ws == nil { + ws := rpcclient.NewWSClient(c.remote, c.endpoint) + _, err = ws.Start() + if err == nil { + c.ws = ws + } + } + return errors.Wrap(err, "StartWebsocket") +} + +// StopWebsocket stops the websocket connection +func (c Client) StopWebsocket() { + if c.ws != nil { + c.ws.Stop() + c.ws = nil + } +} + +// GetEventChannels returns the results and error channel from the websocket +func (c Client) GetEventChannels() (chan json.RawMessage, chan error) { + if c.ws == nil { + return nil, nil + } + return c.ws.ResultsCh, c.ws.ErrorsCh +} + +func (c Client) Subscribe(event string) error { + return errors.Wrap(c.ws.Subscribe(event), "Subscribe") +} + +func (c Client) Unsubscribe(event string) error { + return errors.Wrap(c.ws.Unsubscribe(event), "Unsubscribe") +} + +*/