github.com/vipernet-xyz/tm@v0.34.24/rpc/jsonrpc/server/http_json_handler_test.go (about)

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