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 }