package rpc import ( "bytes" "encoding/json" "fmt" "io/ioutil" "net/http" "net/http/httptest" "reflect" "strings" "testing" "github.com/CityOfZion/neo-go/pkg/core" "github.com/CityOfZion/neo-go/pkg/core/transaction" "github.com/CityOfZion/neo-go/pkg/rpc/wrappers" "github.com/CityOfZion/neo-go/pkg/util" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) type executor struct { chain *core.Blockchain handler http.HandlerFunc } const ( defaultJSONRPC = "2.0" defaultID = 1 ) type rpcTestCase struct { name string params string fail bool result func(e *executor) interface{} check func(t *testing.T, e *executor, result interface{}) } var rpcTestCases = map[string][]rpcTestCase{ "getaccountstate": { { name: "positive", params: `["AZ81H31DMWzbSnFDLFkzh9vHwaDLayV7fU"]`, result: func(e *executor) interface{} { return &GetAccountStateResponse{} }, check: func(t *testing.T, e *executor, result interface{}) { res, ok := result.(*GetAccountStateResponse) require.True(t, ok) assert.Equal(t, 1, len(res.Result.Balances)) assert.Equal(t, false, res.Result.Frozen) }, }, { name: "positive null", params: `["AK2nJJpJr6o664CWJKi1QRXjqeic2zRp8y"]`, result: func(e *executor) interface{} { return &GetAccountStateResponse{} }, check: func(t *testing.T, e *executor, result interface{}) { res, ok := result.(*GetAccountStateResponse) require.True(t, ok) assert.Equal(t, 0, len(res.Result.Balances)) assert.Equal(t, false, res.Result.Frozen) }, }, { name: "no params", params: `[]`, fail: true, }, { name: "invalid address", params: `["notabase58"]`, fail: true, }, }, "getassetstate": { { name: "positive", params: `["602c79718b16e442de58778e148d0b1084e3b2dffd5de6b7b16cee7969282de7"]`, result: func(e *executor) interface{} { return &GetAssetResponse{} }, check: func(t *testing.T, e *executor, result interface{}) { res, ok := result.(*GetAssetResponse) require.True(t, ok) assert.Equal(t, "00", res.Result.Owner) assert.Equal(t, "AWKECj9RD8rS8RPcpCgYVjk1DeYyHwxZm3", res.Result.Admin) }, }, { name: "negative", params: `["602c79718b16e442de58778e148d0b1084e3b2dffd5de6b7b16cee7969282de2"]`, result: func(e *executor) interface{} { return "Invalid assetid" }, }, { name: "no params", params: `[]`, fail: true, }, { name: "invalid hash", params: `["notahex"]`, fail: true, }, }, "getbestblockhash": { { params: "[]", result: func(e *executor) interface{} { return "0x" + e.chain.CurrentBlockHash().StringLE() }, }, { params: "1", fail: true, }, }, "getblock": { { name: "positive", params: "[1, 1]", result: func(e *executor) interface{} { return &GetBlockResponse{} }, check: func(t *testing.T, e *executor, result interface{}) { res, ok := result.(*GetBlockResponse) require.True(t, ok) block, err := e.chain.GetBlock(e.chain.GetHeaderHash(1)) require.NoErrorf(t, err, "could not get block") assert.Equal(t, block.Hash(), res.Result.Hash) for i := range res.Result.Tx { tx := res.Result.Tx[i] require.Equal(t, transaction.MinerType, tx.Type) miner, ok := block.Transactions[i].Data.(*transaction.MinerTX) require.True(t, ok) require.Equal(t, miner.Nonce, tx.Nonce) require.Equal(t, block.Transactions[i].Hash(), tx.TxID) } }, }, { name: "no params", params: `[]`, fail: true, }, { name: "bad params", params: `[[]]`, fail: true, }, { name: "invalid height", params: `[-1]`, fail: true, }, { name: "invalid hash", params: `["notahex"]`, fail: true, }, { name: "missing hash", params: `["` + util.Uint256{}.String() + `"]`, fail: true, }, }, "getblockcount": { { params: "[]", result: func(e *executor) interface{} { return int(e.chain.BlockHeight() + 1) }, }, }, "getblockhash": { { params: "[1]", result: func(e *executor) interface{} { return "" }, check: func(t *testing.T, e *executor, result interface{}) { res, ok := result.(*StringResultResponse) require.True(t, ok) block, err := e.chain.GetBlock(e.chain.GetHeaderHash(1)) require.NoErrorf(t, err, "could not get block") expectedHash := "0x" + block.Hash().StringLE() assert.Equal(t, expectedHash, res.Result) }, }, { name: "string height", params: `["first"]`, fail: true, }, { name: "invalid number height", params: `[-2]`, fail: true, }, }, "getconnectioncount": { { params: "[]", result: func(*executor) interface{} { return 0 }, }, }, "getpeers": { { params: "[]", result: func(*executor) interface{} { return &GetPeersResponse{ Jsonrpc: defaultJSONRPC, Result: struct { Unconnected []int `json:"unconnected"` Connected []int `json:"connected"` Bad []int `json:"bad"` }{ Unconnected: []int{}, Connected: []int{}, Bad: []int{}, }, ID: defaultID, } }, }, }, "getrawtransaction": { { name: "no params", params: `[]`, fail: true, }, { name: "invalid hash", params: `["notahex"]`, fail: true, }, { name: "missing hash", params: `["` + util.Uint256{}.String() + `"]`, fail: true, }, }, "getunspents": { { name: "positive", params: `["AZ81H31DMWzbSnFDLFkzh9vHwaDLayV7fU"]`, result: func(e *executor) interface{} { return &GetUnspents{} }, check: func(t *testing.T, e *executor, result interface{}) { res, ok := result.(*GetUnspents) require.True(t, ok) require.Equal(t, 1, len(res.Result.Balance)) assert.Equal(t, 1, len(res.Result.Balance[0].Unspents)) }, }, { name: "positive null", params: `["AK2nJJpJr6o664CWJKi1QRXjqeic2zRp8y"]`, result: func(e *executor) interface{} { return &GetUnspents{} }, check: func(t *testing.T, e *executor, result interface{}) { res, ok := result.(*GetUnspents) require.True(t, ok) require.Equal(t, 0, len(res.Result.Balance)) }, }, }, "getversion": { { params: "[]", result: func(*executor) interface{} { return &GetVersionResponse{} }, check: func(t *testing.T, e *executor, result interface{}) { resp, ok := result.(*GetVersionResponse) require.True(t, ok) require.Equal(t, "/NEO-GO:/", resp.Result.UserAgent) }, }, }, "invoke": { { name: "positive", params: `["50befd26fdf6e4d957c11e078b24ebce6291456f", [{"type": "String", "value": "qwerty"}]]`, result: func(e *executor) interface{} { return &InvokeFunctionResponse{} }, check: func(t *testing.T, e *executor, result interface{}) { res, ok := result.(*InvokeFunctionResponse) require.True(t, ok) assert.Equal(t, "06717765727479676f459162ceeb248b071ec157d9e4f6fd26fdbe50", res.Result.Script) assert.NotEqual(t, "", res.Result.State) assert.NotEqual(t, 0, res.Result.GasConsumed) }, }, { name: "no params", params: `[]`, fail: true, }, { name: "not a string", params: `[42, []]`, fail: true, }, { name: "not a scripthash", params: `["qwerty", []]`, fail: true, }, { name: "not an array", params: `["50befd26fdf6e4d957c11e078b24ebce6291456f", 42]`, fail: true, }, { name: "bad params", params: `["50befd26fdf6e4d957c11e078b24ebce6291456f", [{"type": "Integer", "value": "qwerty"}]]`, fail: true, }, }, "invokefunction": { { name: "positive", params: `["50befd26fdf6e4d957c11e078b24ebce6291456f", "test", []]`, result: func(e *executor) interface{} { return &InvokeFunctionResponse{} }, check: func(t *testing.T, e *executor, result interface{}) { res, ok := result.(*InvokeFunctionResponse) require.True(t, ok) assert.NotEqual(t, "", res.Result.Script) assert.NotEqual(t, "", res.Result.State) assert.NotEqual(t, 0, res.Result.GasConsumed) }, }, { name: "no params", params: `[]`, fail: true, }, { name: "not a string", params: `[42, "test", []]`, fail: true, }, { name: "not a scripthash", params: `["qwerty", "test", []]`, fail: true, }, { name: "bad params", params: `["50befd26fdf6e4d957c11e078b24ebce6291456f", "test", [{"type": "Integer", "value": "qwerty"}]]`, fail: true, }, }, "invokescript": { { name: "positive", params: `["51c56b0d48656c6c6f2c20776f726c6421680f4e656f2e52756e74696d652e4c6f67616c7566"]`, result: func(e *executor) interface{} { return &InvokeFunctionResponse{} }, check: func(t *testing.T, e *executor, result interface{}) { res, ok := result.(*InvokeFunctionResponse) require.True(t, ok) assert.NotEqual(t, "", res.Result.Script) assert.NotEqual(t, "", res.Result.State) assert.NotEqual(t, 0, res.Result.GasConsumed) }, }, { name: "no params", params: `[]`, fail: true, }, { name: "not a string", params: `[42]`, fail: true, }, { name: "bas string", params: `["qwerty"]`, fail: true, }, }, "sendrawtransaction": { { name: "positive", params: `["d1001b00046e616d6567d3d8602814a429a91afdbaa3914884a1c90c733101201cc9c05cefffe6cdd7b182816a9152ec218d2ec000000141403387ef7940a5764259621e655b3c621a6aafd869a611ad64adcc364d8dd1edf84e00a7f8b11b630a377eaef02791d1c289d711c08b7ad04ff0d6c9caca22cfe6232103cbb45da6072c14761c9da545749d9cfd863f860c351066d16df480602a2024c6ac"]`, result: func(e *executor) interface{} { return &SendTXResponse{} }, check: func(t *testing.T, e *executor, result interface{}) { res, ok := result.(*SendTXResponse) require.True(t, ok) assert.True(t, res.Result) }, }, { name: "negative", params: `["0274d792072617720636f6e7472616374207472616e73616374696f6e206465736372697074696f6e01949354ea0a8b57dfee1e257a1aedd1e0eea2e5837de145e8da9c0f101bfccc8e0100029b7cffdaa674beae0f930ebe6085af9093e5fe56b34a5c220ccdcf6efc336fc500a3e11100000000ea610aa6db39bd8c8556c9569d94b5e5a5d0ad199b7cffdaa674beae0f930ebe6085af9093e5fe56b34a5c220ccdcf6efc336fc5004f2418010000001cc9c05cefffe6cdd7b182816a9152ec218d2ec0014140dbd3cddac5cb2bd9bf6d93701f1a6f1c9dbe2d1b480c54628bbb2a4d536158c747a6af82698edf9f8af1cac3850bcb772bd9c8e4ac38f80704751cc4e0bd0e67232103cbb45da6072c14761c9da545749d9cfd863f860c351066d16df480602a2024c6ac"]`, fail: true, }, { name: "no params", params: `[]`, fail: true, }, { name: "invalid string", params: `["notahex"]`, fail: true, }, { name: "invalid tx", params: `["0274d792072617720636f6e747261637"]`, fail: true, }, }, "validateaddress": { { name: "positive", params: `["AQVh2pG732YvtNaxEGkQUei3YA4cvo7d2i"]`, result: func(*executor) interface{} { return &ValidateAddrResponse{} }, check: func(t *testing.T, e *executor, result interface{}) { res, ok := result.(*ValidateAddrResponse) require.True(t, ok) assert.Equal(t, "AQVh2pG732YvtNaxEGkQUei3YA4cvo7d2i", res.Result.Address) assert.True(t, res.Result.IsValid) }, }, { name: "negative", params: "[1]", result: func(*executor) interface{} { return &ValidateAddrResponse{ Jsonrpc: defaultJSONRPC, Result: wrappers.ValidateAddressResponse{ Address: float64(1), IsValid: false, }, ID: defaultID, } }, }, }, } func TestRPC(t *testing.T) { chain, handler := initServerWithInMemoryChain(t) defer chain.Close() e := &executor{chain: chain, handler: handler} for method, cases := range rpcTestCases { t.Run(method, func(t *testing.T) { rpc := `{"jsonrpc": "2.0", "id": 1, "method": "%s", "params": %s}` for _, tc := range cases { t.Run(tc.name, func(t *testing.T) { body := doRPCCall(fmt.Sprintf(rpc, method, tc.params), handler, t) checkErrResponse(t, body, tc.fail) if tc.fail { return } expected, res := tc.getResultPair(e) err := json.Unmarshal(body, res) require.NoErrorf(t, err, "could not parse response: %s", body) if tc.check == nil { assert.Equal(t, expected, res) } else { tc.check(t, e, res) } }) } }) } t.Run("getrawtransaction", func(t *testing.T) { block, _ := chain.GetBlock(chain.GetHeaderHash(0)) TXHash := block.Transactions[1].Hash() rpc := fmt.Sprintf(`{"jsonrpc": "2.0", "id": 1, "method": "getrawtransaction", "params": ["%s"]}"`, TXHash.StringLE()) body := doRPCCall(rpc, handler, t) checkErrResponse(t, body, false) var res StringResultResponse err := json.Unmarshal(body, &res) require.NoErrorf(t, err, "could not parse response: %s", body) assert.Equal(t, "400000455b7b226c616e67223a227a682d434e222c226e616d65223a22e5b08fe89a81e882a1227d2c7b226c616e67223a22656e222c226e616d65223a22416e745368617265227d5d0000c16ff28623000000da1745e9b549bd0bfa1a569971c77eba30cd5a4b00000000", res.Result) }) t.Run("getrawtransaction 2 arguments", func(t *testing.T) { block, _ := chain.GetBlock(chain.GetHeaderHash(0)) TXHash := block.Transactions[1].Hash() rpc := fmt.Sprintf(`{"jsonrpc": "2.0", "id": 1, "method": "getrawtransaction", "params": ["%s", 0]}"`, TXHash.StringLE()) body := doRPCCall(rpc, handler, t) checkErrResponse(t, body, false) var res StringResultResponse err := json.Unmarshal(body, &res) require.NoErrorf(t, err, "could not parse response: %s", body) assert.Equal(t, "400000455b7b226c616e67223a227a682d434e222c226e616d65223a22e5b08fe89a81e882a1227d2c7b226c616e67223a22656e222c226e616d65223a22416e745368617265227d5d0000c16ff28623000000da1745e9b549bd0bfa1a569971c77eba30cd5a4b00000000", res.Result) }) } func (tc rpcTestCase) getResultPair(e *executor) (expected interface{}, res interface{}) { expected = tc.result(e) switch exp := expected.(type) { case string: res = new(StringResultResponse) expected = &StringResultResponse{ Jsonrpc: defaultJSONRPC, Result: exp, ID: defaultID, } case int: res = new(IntResultResponse) expected = &IntResultResponse{ Jsonrpc: defaultJSONRPC, Result: exp, ID: defaultID, } default: resVal := reflect.New(reflect.TypeOf(expected).Elem()) res = resVal.Interface() } return } func checkErrResponse(t *testing.T, body []byte, expectingFail bool) { var errresp ErrorResponse err := json.Unmarshal(body, &errresp) require.Nil(t, err) if expectingFail { assert.NotEqual(t, 0, errresp.Error.Code) assert.NotEqual(t, "", errresp.Error.Message) } else { assert.Equal(t, 0, errresp.Error.Code) assert.Equal(t, "", errresp.Error.Message) } } func doRPCCall(rpcCall string, handler http.HandlerFunc, t *testing.T) []byte { req := httptest.NewRequest("POST", "http://0.0.0.0:20333/", strings.NewReader(rpcCall)) req.Header.Set("Content-Type", "application/json") w := httptest.NewRecorder() handler(w, req) resp := w.Result() body, err := ioutil.ReadAll(resp.Body) assert.NoErrorf(t, err, "could not read response from the request: %s", rpcCall) return bytes.TrimSpace(body) }