github.com/evdatsion/aphelion-dpos-bft@v0.32.1/rpc/lib/server/handlers_test.go (about)

     1  package rpcserver_test
     2  
     3  import (
     4  	"bytes"
     5  	"encoding/json"
     6  	"io/ioutil"
     7  	"net/http"
     8  	"net/http/httptest"
     9  	"strings"
    10  	"testing"
    11  
    12  	"github.com/gorilla/websocket"
    13  	"github.com/stretchr/testify/assert"
    14  	"github.com/stretchr/testify/require"
    15  
    16  	amino "github.com/evdatsion/go-amino"
    17  	"github.com/evdatsion/aphelion-dpos-bft/libs/log"
    18  	rs "github.com/evdatsion/aphelion-dpos-bft/rpc/lib/server"
    19  	types "github.com/evdatsion/aphelion-dpos-bft/rpc/lib/types"
    20  )
    21  
    22  //////////////////////////////////////////////////////////////////////////////
    23  // HTTP REST API
    24  // TODO
    25  
    26  //////////////////////////////////////////////////////////////////////////////
    27  // JSON-RPC over HTTP
    28  
    29  func testMux() *http.ServeMux {
    30  	funcMap := map[string]*rs.RPCFunc{
    31  		"c": rs.NewRPCFunc(func(ctx *types.Context, s string, i int) (string, error) { return "foo", nil }, "s,i"),
    32  	}
    33  	cdc := amino.NewCodec()
    34  	mux := http.NewServeMux()
    35  	buf := new(bytes.Buffer)
    36  	logger := log.NewTMLogger(buf)
    37  	rs.RegisterRPCFuncs(mux, funcMap, cdc, logger)
    38  
    39  	return mux
    40  }
    41  
    42  func statusOK(code int) bool { return code >= 200 && code <= 299 }
    43  
    44  // Ensure that nefarious/unintended inputs to `params`
    45  // do not crash our RPC handlers.
    46  // See Issue https://github.com/evdatsion/aphelion-dpos-bft/issues/708.
    47  func TestRPCParams(t *testing.T) {
    48  	mux := testMux()
    49  	tests := []struct {
    50  		payload    string
    51  		wantErr    string
    52  		expectedId interface{}
    53  	}{
    54  		// bad
    55  		{`{"jsonrpc": "2.0", "id": "0"}`, "Method not found", types.JSONRPCStringID("0")},
    56  		{`{"jsonrpc": "2.0", "method": "y", "id": "0"}`, "Method not found", types.JSONRPCStringID("0")},
    57  		{`{"method": "c", "id": "0", "params": a}`, "invalid character", types.JSONRPCStringID("")}, // id not captured in JSON parsing failures
    58  		{`{"method": "c", "id": "0", "params": ["a"]}`, "got 1", types.JSONRPCStringID("0")},
    59  		{`{"method": "c", "id": "0", "params": ["a", "b"]}`, "invalid character", types.JSONRPCStringID("0")},
    60  		{`{"method": "c", "id": "0", "params": [1, 1]}`, "of type string", types.JSONRPCStringID("0")},
    61  
    62  		// good
    63  		{`{"jsonrpc": "2.0", "method": "c", "id": "0", "params": null}`, "", types.JSONRPCStringID("0")},
    64  		{`{"method": "c", "id": "0", "params": {}}`, "", types.JSONRPCStringID("0")},
    65  		{`{"method": "c", "id": "0", "params": ["a", "10"]}`, "", types.JSONRPCStringID("0")},
    66  	}
    67  
    68  	for i, tt := range tests {
    69  		req, _ := http.NewRequest("POST", "http://localhost/", strings.NewReader(tt.payload))
    70  		rec := httptest.NewRecorder()
    71  		mux.ServeHTTP(rec, req)
    72  		res := rec.Result()
    73  		// Always expecting back a JSONRPCResponse
    74  		assert.True(t, statusOK(res.StatusCode), "#%d: should always return 2XX", i)
    75  		blob, err := ioutil.ReadAll(res.Body)
    76  		if err != nil {
    77  			t.Errorf("#%d: err reading body: %v", i, err)
    78  			continue
    79  		}
    80  
    81  		recv := new(types.RPCResponse)
    82  		assert.Nil(t, json.Unmarshal(blob, recv), "#%d: expecting successful parsing of an RPCResponse:\nblob: %s", i, blob)
    83  		assert.NotEqual(t, recv, new(types.RPCResponse), "#%d: not expecting a blank RPCResponse", i)
    84  		assert.Equal(t, tt.expectedId, recv.ID, "#%d: expected ID not matched in RPCResponse", i)
    85  		if tt.wantErr == "" {
    86  			assert.Nil(t, recv.Error, "#%d: not expecting an error", i)
    87  		} else {
    88  			assert.True(t, recv.Error.Code < 0, "#%d: not expecting a positive JSONRPC code", i)
    89  			// The wanted error is either in the message or the data
    90  			assert.Contains(t, recv.Error.Message+recv.Error.Data, tt.wantErr, "#%d: expected substring", i)
    91  		}
    92  	}
    93  }
    94  
    95  func TestJSONRPCID(t *testing.T) {
    96  	mux := testMux()
    97  	tests := []struct {
    98  		payload    string
    99  		wantErr    bool
   100  		expectedId interface{}
   101  	}{
   102  		// good id
   103  		{`{"jsonrpc": "2.0", "method": "c", "id": "0", "params": ["a", "10"]}`, false, types.JSONRPCStringID("0")},
   104  		{`{"jsonrpc": "2.0", "method": "c", "id": "abc", "params": ["a", "10"]}`, false, types.JSONRPCStringID("abc")},
   105  		{`{"jsonrpc": "2.0", "method": "c", "id": 0, "params": ["a", "10"]}`, false, types.JSONRPCIntID(0)},
   106  		{`{"jsonrpc": "2.0", "method": "c", "id": 1, "params": ["a", "10"]}`, false, types.JSONRPCIntID(1)},
   107  		{`{"jsonrpc": "2.0", "method": "c", "id": 1.3, "params": ["a", "10"]}`, false, types.JSONRPCIntID(1)},
   108  		{`{"jsonrpc": "2.0", "method": "c", "id": -1, "params": ["a", "10"]}`, false, types.JSONRPCIntID(-1)},
   109  		{`{"jsonrpc": "2.0", "method": "c", "id": null, "params": ["a", "10"]}`, false, nil},
   110  
   111  		// bad id
   112  		{`{"jsonrpc": "2.0", "method": "c", "id": {}, "params": ["a", "10"]}`, true, nil},
   113  		{`{"jsonrpc": "2.0", "method": "c", "id": [], "params": ["a", "10"]}`, true, nil},
   114  	}
   115  
   116  	for i, tt := range tests {
   117  		req, _ := http.NewRequest("POST", "http://localhost/", strings.NewReader(tt.payload))
   118  		rec := httptest.NewRecorder()
   119  		mux.ServeHTTP(rec, req)
   120  		res := rec.Result()
   121  		// Always expecting back a JSONRPCResponse
   122  		assert.True(t, statusOK(res.StatusCode), "#%d: should always return 2XX", i)
   123  		blob, err := ioutil.ReadAll(res.Body)
   124  		if err != nil {
   125  			t.Errorf("#%d: err reading body: %v", i, err)
   126  			continue
   127  		}
   128  
   129  		recv := new(types.RPCResponse)
   130  		err = json.Unmarshal(blob, recv)
   131  		assert.Nil(t, err, "#%d: expecting successful parsing of an RPCResponse:\nblob: %s", i, blob)
   132  		if !tt.wantErr {
   133  			assert.NotEqual(t, recv, new(types.RPCResponse), "#%d: not expecting a blank RPCResponse", i)
   134  			assert.Equal(t, tt.expectedId, recv.ID, "#%d: expected ID not matched in RPCResponse", i)
   135  			assert.Nil(t, recv.Error, "#%d: not expecting an error", i)
   136  		} else {
   137  			assert.True(t, recv.Error.Code < 0, "#%d: not expecting a positive JSONRPC code", i)
   138  		}
   139  	}
   140  }
   141  
   142  func TestRPCNotification(t *testing.T) {
   143  	mux := testMux()
   144  	body := strings.NewReader(`{"jsonrpc": "2.0", "id": ""}`)
   145  	req, _ := http.NewRequest("POST", "http://localhost/", body)
   146  	rec := httptest.NewRecorder()
   147  	mux.ServeHTTP(rec, req)
   148  	res := rec.Result()
   149  
   150  	// Always expecting back a JSONRPCResponse
   151  	require.True(t, statusOK(res.StatusCode), "should always return 2XX")
   152  	blob, err := ioutil.ReadAll(res.Body)
   153  	require.Nil(t, err, "reading from the body should not give back an error")
   154  	require.Equal(t, len(blob), 0, "a notification SHOULD NOT be responded to by the server")
   155  }
   156  
   157  func TestRPCNotificationInBatch(t *testing.T) {
   158  	mux := testMux()
   159  	tests := []struct {
   160  		payload     string
   161  		expectCount int
   162  	}{
   163  		{
   164  			`[
   165  				{"jsonrpc": "2.0","id": ""},
   166  				{"jsonrpc": "2.0","method":"c","id":"abc","params":["a","10"]}
   167  			 ]`,
   168  			1,
   169  		},
   170  		{
   171  			`[
   172  				{"jsonrpc": "2.0","id": ""},
   173  				{"jsonrpc": "2.0","method":"c","id":"abc","params":["a","10"]},
   174  				{"jsonrpc": "2.0","id": ""},
   175  				{"jsonrpc": "2.0","method":"c","id":"abc","params":["a","10"]}
   176  			 ]`,
   177  			2,
   178  		},
   179  	}
   180  	for i, tt := range tests {
   181  		req, _ := http.NewRequest("POST", "http://localhost/", strings.NewReader(tt.payload))
   182  		rec := httptest.NewRecorder()
   183  		mux.ServeHTTP(rec, req)
   184  		res := rec.Result()
   185  		// Always expecting back a JSONRPCResponse
   186  		assert.True(t, statusOK(res.StatusCode), "#%d: should always return 2XX", i)
   187  		blob, err := ioutil.ReadAll(res.Body)
   188  		if err != nil {
   189  			t.Errorf("#%d: err reading body: %v", i, err)
   190  			continue
   191  		}
   192  
   193  		var responses []types.RPCResponse
   194  		// try to unmarshal an array first
   195  		err = json.Unmarshal(blob, &responses)
   196  		if err != nil {
   197  			// if we were actually expecting an array, but got an error
   198  			if tt.expectCount > 1 {
   199  				t.Errorf("#%d: expected an array, couldn't unmarshal it\nblob: %s", i, blob)
   200  				continue
   201  			} else {
   202  				// we were expecting an error here, so let's unmarshal a single response
   203  				var response types.RPCResponse
   204  				err = json.Unmarshal(blob, &response)
   205  				if err != nil {
   206  					t.Errorf("#%d: expected successful parsing of an RPCResponse\nblob: %s", i, blob)
   207  					continue
   208  				}
   209  				// have a single-element result
   210  				responses = []types.RPCResponse{response}
   211  			}
   212  		}
   213  		if tt.expectCount != len(responses) {
   214  			t.Errorf("#%d: expected %d response(s), but got %d\nblob: %s", i, tt.expectCount, len(responses), blob)
   215  			continue
   216  		}
   217  		for _, response := range responses {
   218  			assert.NotEqual(t, response, new(types.RPCResponse), "#%d: not expecting a blank RPCResponse", i)
   219  		}
   220  	}
   221  }
   222  
   223  func TestUnknownRPCPath(t *testing.T) {
   224  	mux := testMux()
   225  	req, _ := http.NewRequest("GET", "http://localhost/unknownrpcpath", nil)
   226  	rec := httptest.NewRecorder()
   227  	mux.ServeHTTP(rec, req)
   228  	res := rec.Result()
   229  
   230  	// Always expecting back a 404 error
   231  	require.Equal(t, http.StatusNotFound, res.StatusCode, "should always return 404")
   232  }
   233  
   234  //////////////////////////////////////////////////////////////////////////////
   235  // JSON-RPC over WEBSOCKETS
   236  
   237  func TestWebsocketManagerHandler(t *testing.T) {
   238  	s := newWSServer()
   239  	defer s.Close()
   240  
   241  	// check upgrader works
   242  	d := websocket.Dialer{}
   243  	c, dialResp, err := d.Dial("ws://"+s.Listener.Addr().String()+"/websocket", nil)
   244  	require.NoError(t, err)
   245  
   246  	if got, want := dialResp.StatusCode, http.StatusSwitchingProtocols; got != want {
   247  		t.Errorf("dialResp.StatusCode = %q, want %q", got, want)
   248  	}
   249  
   250  	// check basic functionality works
   251  	req, err := types.MapToRequest(amino.NewCodec(), types.JSONRPCStringID("TestWebsocketManager"), "c", map[string]interface{}{"s": "a", "i": 10})
   252  	require.NoError(t, err)
   253  	err = c.WriteJSON(req)
   254  	require.NoError(t, err)
   255  
   256  	var resp types.RPCResponse
   257  	err = c.ReadJSON(&resp)
   258  	require.NoError(t, err)
   259  	require.Nil(t, resp.Error)
   260  }
   261  
   262  func newWSServer() *httptest.Server {
   263  	funcMap := map[string]*rs.RPCFunc{
   264  		"c": rs.NewWSRPCFunc(func(ctx *types.Context, s string, i int) (string, error) { return "foo", nil }, "s,i"),
   265  	}
   266  	wm := rs.NewWebsocketManager(funcMap, amino.NewCodec())
   267  	wm.SetLogger(log.TestingLogger())
   268  
   269  	mux := http.NewServeMux()
   270  	mux.HandleFunc("/websocket", wm.WebsocketHandler)
   271  
   272  	return httptest.NewServer(mux)
   273  }