github.com/theQRL/go-zond@v0.1.1/node/rpcstack_test.go (about)

     1  // Copyright 2020 The go-ethereum Authors
     2  // This file is part of the go-ethereum library.
     3  //
     4  // The go-ethereum library is free software: you can redistribute it and/or modify
     5  // it under the terms of the GNU Lesser General Public License as published by
     6  // the Free Software Foundation, either version 3 of the License, or
     7  // (at your option) any later version.
     8  //
     9  // The go-ethereum library is distributed in the hope that it will be useful,
    10  // but WITHOUT ANY WARRANTY; without even the implied warranty of
    11  // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
    12  // GNU Lesser General Public License for more details.
    13  //
    14  // You should have received a copy of the GNU Lesser General Public License
    15  // along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
    16  
    17  package node
    18  
    19  import (
    20  	"bytes"
    21  	"fmt"
    22  	"io"
    23  	"net/http"
    24  	"net/http/httptest"
    25  	"net/url"
    26  	"strconv"
    27  	"strings"
    28  	"testing"
    29  	"time"
    30  
    31  	"github.com/golang-jwt/jwt/v4"
    32  	"github.com/gorilla/websocket"
    33  	"github.com/stretchr/testify/assert"
    34  	"github.com/theQRL/go-zond/internal/testlog"
    35  	"github.com/theQRL/go-zond/log"
    36  	"github.com/theQRL/go-zond/rpc"
    37  )
    38  
    39  const testMethod = "rpc_modules"
    40  
    41  // TestCorsHandler makes sure CORS are properly handled on the http server.
    42  func TestCorsHandler(t *testing.T) {
    43  	srv := createAndStartServer(t, &httpConfig{CorsAllowedOrigins: []string{"test", "test.com"}}, false, &wsConfig{}, nil)
    44  	defer srv.stop()
    45  	url := "http://" + srv.listenAddr()
    46  
    47  	resp := rpcRequest(t, url, testMethod, "origin", "test.com")
    48  	assert.Equal(t, "test.com", resp.Header.Get("Access-Control-Allow-Origin"))
    49  
    50  	resp2 := rpcRequest(t, url, testMethod, "origin", "bad")
    51  	assert.Equal(t, "", resp2.Header.Get("Access-Control-Allow-Origin"))
    52  }
    53  
    54  // TestVhosts makes sure vhosts are properly handled on the http server.
    55  func TestVhosts(t *testing.T) {
    56  	srv := createAndStartServer(t, &httpConfig{Vhosts: []string{"test"}}, false, &wsConfig{}, nil)
    57  	defer srv.stop()
    58  	url := "http://" + srv.listenAddr()
    59  
    60  	resp := rpcRequest(t, url, testMethod, "host", "test")
    61  	assert.Equal(t, resp.StatusCode, http.StatusOK)
    62  
    63  	resp2 := rpcRequest(t, url, testMethod, "host", "bad")
    64  	assert.Equal(t, resp2.StatusCode, http.StatusForbidden)
    65  }
    66  
    67  type originTest struct {
    68  	spec    string
    69  	expOk   []string
    70  	expFail []string
    71  }
    72  
    73  // splitAndTrim splits input separated by a comma
    74  // and trims excessive white space from the substrings.
    75  // Copied over from flags.go
    76  func splitAndTrim(input string) (ret []string) {
    77  	l := strings.Split(input, ",")
    78  	for _, r := range l {
    79  		r = strings.TrimSpace(r)
    80  		if len(r) > 0 {
    81  			ret = append(ret, r)
    82  		}
    83  	}
    84  	return ret
    85  }
    86  
    87  // TestWebsocketOrigins makes sure the websocket origins are properly handled on the websocket server.
    88  func TestWebsocketOrigins(t *testing.T) {
    89  	tests := []originTest{
    90  		{
    91  			spec: "*", // allow all
    92  			expOk: []string{"", "http://test", "https://test", "http://test:8540", "https://test:8540",
    93  				"http://test.com", "https://foo.test", "http://testa", "http://atestb:8540", "https://atestb:8540"},
    94  		},
    95  		{
    96  			spec:    "test",
    97  			expOk:   []string{"http://test", "https://test", "http://test:8540", "https://test:8540"},
    98  			expFail: []string{"http://test.com", "https://foo.test", "http://testa", "http://atestb:8540", "https://atestb:8540"},
    99  		},
   100  		// scheme tests
   101  		{
   102  			spec:  "https://test",
   103  			expOk: []string{"https://test", "https://test:9999"},
   104  			expFail: []string{
   105  				"test",                                // no scheme, required by spec
   106  				"http://test",                         // wrong scheme
   107  				"http://test.foo", "https://a.test.x", // subdomain variations
   108  				"http://testx:8540", "https://xtest:8540"},
   109  		},
   110  		// ip tests
   111  		{
   112  			spec:  "https://12.34.56.78",
   113  			expOk: []string{"https://12.34.56.78", "https://12.34.56.78:8540"},
   114  			expFail: []string{
   115  				"http://12.34.56.78",     // wrong scheme
   116  				"http://12.34.56.78:443", // wrong scheme
   117  				"http://1.12.34.56.78",   // wrong 'domain name'
   118  				"http://12.34.56.78.a",   // wrong 'domain name'
   119  				"https://87.65.43.21", "http://87.65.43.21:8540", "https://87.65.43.21:8540"},
   120  		},
   121  		// port tests
   122  		{
   123  			spec:  "test:8540",
   124  			expOk: []string{"http://test:8540", "https://test:8540"},
   125  			expFail: []string{
   126  				"http://test", "https://test", // spec says port required
   127  				"http://test:8541", "https://test:8541", // wrong port
   128  				"http://bad", "https://bad", "http://bad:8540", "https://bad:8540"},
   129  		},
   130  		// scheme and port
   131  		{
   132  			spec:  "https://test:8540",
   133  			expOk: []string{"https://test:8540"},
   134  			expFail: []string{
   135  				"https://test",                          // missing port
   136  				"http://test",                           // missing port, + wrong scheme
   137  				"http://test:8540",                      // wrong scheme
   138  				"http://test:8541", "https://test:8541", // wrong port
   139  				"http://bad", "https://bad", "http://bad:8540", "https://bad:8540"},
   140  		},
   141  		// several allowed origins
   142  		{
   143  			spec: "localhost,http://127.0.0.1",
   144  			expOk: []string{"localhost", "http://localhost", "https://localhost:8443",
   145  				"http://127.0.0.1", "http://127.0.0.1:8080"},
   146  			expFail: []string{
   147  				"https://127.0.0.1", // wrong scheme
   148  				"http://bad", "https://bad", "http://bad:8540", "https://bad:8540"},
   149  		},
   150  	}
   151  	for _, tc := range tests {
   152  		srv := createAndStartServer(t, &httpConfig{}, true, &wsConfig{Origins: splitAndTrim(tc.spec)}, nil)
   153  		url := fmt.Sprintf("ws://%v", srv.listenAddr())
   154  		for _, origin := range tc.expOk {
   155  			if err := wsRequest(t, url, "Origin", origin); err != nil {
   156  				t.Errorf("spec '%v', origin '%v': expected ok, got %v", tc.spec, origin, err)
   157  			}
   158  		}
   159  		for _, origin := range tc.expFail {
   160  			if err := wsRequest(t, url, "Origin", origin); err == nil {
   161  				t.Errorf("spec '%v', origin '%v': expected not to allow,  got ok", tc.spec, origin)
   162  			}
   163  		}
   164  		srv.stop()
   165  	}
   166  }
   167  
   168  // TestIsWebsocket tests if an incoming websocket upgrade request is handled properly.
   169  func TestIsWebsocket(t *testing.T) {
   170  	r, _ := http.NewRequest(http.MethodGet, "/", nil)
   171  
   172  	assert.False(t, isWebsocket(r))
   173  	r.Header.Set("upgrade", "websocket")
   174  	assert.False(t, isWebsocket(r))
   175  	r.Header.Set("connection", "upgrade")
   176  	assert.True(t, isWebsocket(r))
   177  	r.Header.Set("connection", "upgrade,keep-alive")
   178  	assert.True(t, isWebsocket(r))
   179  	r.Header.Set("connection", " UPGRADE,keep-alive")
   180  	assert.True(t, isWebsocket(r))
   181  }
   182  
   183  func Test_checkPath(t *testing.T) {
   184  	tests := []struct {
   185  		req      *http.Request
   186  		prefix   string
   187  		expected bool
   188  	}{
   189  		{
   190  			req:      &http.Request{URL: &url.URL{Path: "/test"}},
   191  			prefix:   "/test",
   192  			expected: true,
   193  		},
   194  		{
   195  			req:      &http.Request{URL: &url.URL{Path: "/testing"}},
   196  			prefix:   "/test",
   197  			expected: true,
   198  		},
   199  		{
   200  			req:      &http.Request{URL: &url.URL{Path: "/"}},
   201  			prefix:   "/test",
   202  			expected: false,
   203  		},
   204  		{
   205  			req:      &http.Request{URL: &url.URL{Path: "/fail"}},
   206  			prefix:   "/test",
   207  			expected: false,
   208  		},
   209  		{
   210  			req:      &http.Request{URL: &url.URL{Path: "/"}},
   211  			prefix:   "",
   212  			expected: true,
   213  		},
   214  		{
   215  			req:      &http.Request{URL: &url.URL{Path: "/fail"}},
   216  			prefix:   "",
   217  			expected: false,
   218  		},
   219  		{
   220  			req:      &http.Request{URL: &url.URL{Path: "/"}},
   221  			prefix:   "/",
   222  			expected: true,
   223  		},
   224  		{
   225  			req:      &http.Request{URL: &url.URL{Path: "/testing"}},
   226  			prefix:   "/",
   227  			expected: true,
   228  		},
   229  	}
   230  
   231  	for i, tt := range tests {
   232  		t.Run(strconv.Itoa(i), func(t *testing.T) {
   233  			assert.Equal(t, tt.expected, checkPath(tt.req, tt.prefix))
   234  		})
   235  	}
   236  }
   237  
   238  func createAndStartServer(t *testing.T, conf *httpConfig, ws bool, wsConf *wsConfig, timeouts *rpc.HTTPTimeouts) *httpServer {
   239  	t.Helper()
   240  
   241  	if timeouts == nil {
   242  		timeouts = &rpc.DefaultHTTPTimeouts
   243  	}
   244  	srv := newHTTPServer(testlog.Logger(t, log.LvlDebug), *timeouts)
   245  	assert.NoError(t, srv.enableRPC(apis(), *conf))
   246  	if ws {
   247  		assert.NoError(t, srv.enableWS(nil, *wsConf))
   248  	}
   249  	assert.NoError(t, srv.setListenAddr("localhost", 0))
   250  	assert.NoError(t, srv.start())
   251  	return srv
   252  }
   253  
   254  // wsRequest attempts to open a WebSocket connection to the given URL.
   255  func wsRequest(t *testing.T, url string, extraHeaders ...string) error {
   256  	t.Helper()
   257  	//t.Logf("checking WebSocket on %s (origin %q)", url, browserOrigin)
   258  
   259  	headers := make(http.Header)
   260  	// Apply extra headers.
   261  	if len(extraHeaders)%2 != 0 {
   262  		panic("odd extraHeaders length")
   263  	}
   264  	for i := 0; i < len(extraHeaders); i += 2 {
   265  		key, value := extraHeaders[i], extraHeaders[i+1]
   266  		headers.Set(key, value)
   267  	}
   268  	conn, _, err := websocket.DefaultDialer.Dial(url, headers)
   269  	if conn != nil {
   270  		conn.Close()
   271  	}
   272  	return err
   273  }
   274  
   275  // rpcRequest performs a JSON-RPC request to the given URL.
   276  func rpcRequest(t *testing.T, url, method string, extraHeaders ...string) *http.Response {
   277  	t.Helper()
   278  
   279  	body := fmt.Sprintf(`{"jsonrpc":"2.0","id":1,"method":"%s","params":[]}`, method)
   280  	return baseRpcRequest(t, url, body, extraHeaders...)
   281  }
   282  
   283  func batchRpcRequest(t *testing.T, url string, methods []string, extraHeaders ...string) *http.Response {
   284  	reqs := make([]string, len(methods))
   285  	for i, m := range methods {
   286  		reqs[i] = fmt.Sprintf(`{"jsonrpc":"2.0","id":1,"method":"%s","params":[]}`, m)
   287  	}
   288  	body := fmt.Sprintf(`[%s]`, strings.Join(reqs, ","))
   289  	return baseRpcRequest(t, url, body, extraHeaders...)
   290  }
   291  
   292  func baseRpcRequest(t *testing.T, url, bodyStr string, extraHeaders ...string) *http.Response {
   293  	t.Helper()
   294  
   295  	// Create the request.
   296  	body := bytes.NewReader([]byte(bodyStr))
   297  	req, err := http.NewRequest(http.MethodPost, url, body)
   298  	if err != nil {
   299  		t.Fatal("could not create http request:", err)
   300  	}
   301  	req.Header.Set("content-type", "application/json")
   302  	req.Header.Set("accept-encoding", "identity")
   303  
   304  	// Apply extra headers.
   305  	if len(extraHeaders)%2 != 0 {
   306  		panic("odd extraHeaders length")
   307  	}
   308  	for i := 0; i < len(extraHeaders); i += 2 {
   309  		key, value := extraHeaders[i], extraHeaders[i+1]
   310  		if strings.EqualFold(key, "host") {
   311  			req.Host = value
   312  		} else {
   313  			req.Header.Set(key, value)
   314  		}
   315  	}
   316  
   317  	// Perform the request.
   318  	t.Logf("checking RPC/HTTP on %s %v", url, extraHeaders)
   319  	resp, err := http.DefaultClient.Do(req)
   320  	if err != nil {
   321  		t.Fatal(err)
   322  	}
   323  	t.Cleanup(func() { resp.Body.Close() })
   324  	return resp
   325  }
   326  
   327  type testClaim map[string]interface{}
   328  
   329  func (testClaim) Valid() error {
   330  	return nil
   331  }
   332  
   333  func TestJWT(t *testing.T) {
   334  	var secret = []byte("secret")
   335  	issueToken := func(secret []byte, method jwt.SigningMethod, input map[string]interface{}) string {
   336  		if method == nil {
   337  			method = jwt.SigningMethodHS256
   338  		}
   339  		ss, _ := jwt.NewWithClaims(method, testClaim(input)).SignedString(secret)
   340  		return ss
   341  	}
   342  	cfg := rpcEndpointConfig{jwtSecret: []byte("secret")}
   343  	httpcfg := &httpConfig{rpcEndpointConfig: cfg}
   344  	wscfg := &wsConfig{Origins: []string{"*"}, rpcEndpointConfig: cfg}
   345  	srv := createAndStartServer(t, httpcfg, true, wscfg, nil)
   346  	wsUrl := fmt.Sprintf("ws://%v", srv.listenAddr())
   347  	htUrl := fmt.Sprintf("http://%v", srv.listenAddr())
   348  
   349  	expOk := []func() string{
   350  		func() string {
   351  			return fmt.Sprintf("Bearer %v", issueToken(secret, nil, testClaim{"iat": time.Now().Unix()}))
   352  		},
   353  		func() string {
   354  			return fmt.Sprintf("Bearer %v", issueToken(secret, nil, testClaim{"iat": time.Now().Unix() + 4}))
   355  		},
   356  		func() string {
   357  			return fmt.Sprintf("Bearer %v", issueToken(secret, nil, testClaim{"iat": time.Now().Unix() - 4}))
   358  		},
   359  		func() string {
   360  			return fmt.Sprintf("Bearer %v", issueToken(secret, nil, testClaim{
   361  				"iat": time.Now().Unix(),
   362  				"exp": time.Now().Unix() + 2,
   363  			}))
   364  		},
   365  		func() string {
   366  			return fmt.Sprintf("Bearer %v", issueToken(secret, nil, testClaim{
   367  				"iat": time.Now().Unix(),
   368  				"bar": "baz",
   369  			}))
   370  		},
   371  	}
   372  	for i, tokenFn := range expOk {
   373  		token := tokenFn()
   374  		if err := wsRequest(t, wsUrl, "Authorization", token); err != nil {
   375  			t.Errorf("test %d-ws, token '%v': expected ok, got %v", i, token, err)
   376  		}
   377  		token = tokenFn()
   378  		if resp := rpcRequest(t, htUrl, testMethod, "Authorization", token); resp.StatusCode != 200 {
   379  			t.Errorf("test %d-http, token '%v': expected ok, got %v", i, token, resp.StatusCode)
   380  		}
   381  	}
   382  
   383  	expFail := []func() string{
   384  		// future
   385  		func() string {
   386  			return fmt.Sprintf("Bearer %v", issueToken(secret, nil, testClaim{"iat": time.Now().Unix() + int64(jwtExpiryTimeout.Seconds()) + 1}))
   387  		},
   388  		// stale
   389  		func() string {
   390  			return fmt.Sprintf("Bearer %v", issueToken(secret, nil, testClaim{"iat": time.Now().Unix() - int64(jwtExpiryTimeout.Seconds()) - 1}))
   391  		},
   392  		// wrong algo
   393  		func() string {
   394  			return fmt.Sprintf("Bearer %v", issueToken(secret, jwt.SigningMethodHS512, testClaim{"iat": time.Now().Unix() + 4}))
   395  		},
   396  		// expired
   397  		func() string {
   398  			return fmt.Sprintf("Bearer %v", issueToken(secret, nil, testClaim{"iat": time.Now().Unix(), "exp": time.Now().Unix()}))
   399  		},
   400  		// missing mandatory iat
   401  		func() string {
   402  			return fmt.Sprintf("Bearer %v", issueToken(secret, nil, testClaim{}))
   403  		},
   404  		//  wrong secret
   405  		func() string {
   406  			return fmt.Sprintf("Bearer %v", issueToken([]byte("wrong"), nil, testClaim{"iat": time.Now().Unix()}))
   407  		},
   408  		func() string {
   409  			return fmt.Sprintf("Bearer %v", issueToken([]byte{}, nil, testClaim{"iat": time.Now().Unix()}))
   410  		},
   411  		func() string {
   412  			return fmt.Sprintf("Bearer %v", issueToken(nil, nil, testClaim{"iat": time.Now().Unix()}))
   413  		},
   414  		// Various malformed syntax
   415  		func() string {
   416  			return fmt.Sprintf("%v", issueToken(secret, nil, testClaim{"iat": time.Now().Unix()}))
   417  		},
   418  		func() string {
   419  			return fmt.Sprintf("Bearer  %v", issueToken(secret, nil, testClaim{"iat": time.Now().Unix()}))
   420  		},
   421  		func() string {
   422  			return fmt.Sprintf("bearer %v", issueToken(secret, nil, testClaim{"iat": time.Now().Unix()}))
   423  		},
   424  		func() string {
   425  			return fmt.Sprintf("Bearer: %v", issueToken(secret, nil, testClaim{"iat": time.Now().Unix()}))
   426  		},
   427  		func() string {
   428  			return fmt.Sprintf("Bearer:%v", issueToken(secret, nil, testClaim{"iat": time.Now().Unix()}))
   429  		},
   430  		func() string {
   431  			return fmt.Sprintf("Bearer\t%v", issueToken(secret, nil, testClaim{"iat": time.Now().Unix()}))
   432  		},
   433  		func() string {
   434  			return fmt.Sprintf("Bearer \t%v", issueToken(secret, nil, testClaim{"iat": time.Now().Unix()}))
   435  		},
   436  	}
   437  	for i, tokenFn := range expFail {
   438  		token := tokenFn()
   439  		if err := wsRequest(t, wsUrl, "Authorization", token); err == nil {
   440  			t.Errorf("tc %d-ws, token '%v': expected not to allow,  got ok", i, token)
   441  		}
   442  
   443  		token = tokenFn()
   444  		resp := rpcRequest(t, htUrl, testMethod, "Authorization", token)
   445  		if resp.StatusCode != http.StatusUnauthorized {
   446  			t.Errorf("tc %d-http, token '%v': expected not to allow,  got %v", i, token, resp.StatusCode)
   447  		}
   448  	}
   449  	srv.stop()
   450  }
   451  
   452  func TestGzipHandler(t *testing.T) {
   453  	type gzipTest struct {
   454  		name    string
   455  		handler http.HandlerFunc
   456  		status  int
   457  		isGzip  bool
   458  		header  map[string]string
   459  	}
   460  	tests := []gzipTest{
   461  		{
   462  			name: "Write",
   463  			handler: func(w http.ResponseWriter, r *http.Request) {
   464  				w.Write([]byte("response"))
   465  			},
   466  			isGzip: true,
   467  			status: 200,
   468  		},
   469  		{
   470  			name: "WriteHeader",
   471  			handler: func(w http.ResponseWriter, r *http.Request) {
   472  				w.Header().Set("x-foo", "bar")
   473  				w.WriteHeader(205)
   474  				w.Write([]byte("response"))
   475  			},
   476  			isGzip: true,
   477  			status: 205,
   478  			header: map[string]string{"x-foo": "bar"},
   479  		},
   480  		{
   481  			name: "WriteContentLength",
   482  			handler: func(w http.ResponseWriter, r *http.Request) {
   483  				w.Header().Set("content-length", "8")
   484  				w.Write([]byte("response"))
   485  			},
   486  			isGzip: true,
   487  			status: 200,
   488  		},
   489  		{
   490  			name: "Flush",
   491  			handler: func(w http.ResponseWriter, r *http.Request) {
   492  				w.Write([]byte("res"))
   493  				w.(http.Flusher).Flush()
   494  				w.Write([]byte("ponse"))
   495  			},
   496  			isGzip: true,
   497  			status: 200,
   498  		},
   499  		{
   500  			name: "disable",
   501  			handler: func(w http.ResponseWriter, r *http.Request) {
   502  				w.Header().Set("transfer-encoding", "identity")
   503  				w.Header().Set("x-foo", "bar")
   504  				w.Write([]byte("response"))
   505  			},
   506  			isGzip: false,
   507  			status: 200,
   508  			header: map[string]string{"x-foo": "bar"},
   509  		},
   510  		{
   511  			name: "disable-WriteHeader",
   512  			handler: func(w http.ResponseWriter, r *http.Request) {
   513  				w.Header().Set("transfer-encoding", "identity")
   514  				w.Header().Set("x-foo", "bar")
   515  				w.WriteHeader(205)
   516  				w.Write([]byte("response"))
   517  			},
   518  			isGzip: false,
   519  			status: 205,
   520  			header: map[string]string{"x-foo": "bar"},
   521  		},
   522  	}
   523  
   524  	for _, test := range tests {
   525  		test := test
   526  		t.Run(test.name, func(t *testing.T) {
   527  			srv := httptest.NewServer(newGzipHandler(test.handler))
   528  			defer srv.Close()
   529  
   530  			resp, err := http.Get(srv.URL)
   531  			if err != nil {
   532  				t.Fatal(err)
   533  			}
   534  			defer resp.Body.Close()
   535  
   536  			content, err := io.ReadAll(resp.Body)
   537  			if err != nil {
   538  				t.Fatal(err)
   539  			}
   540  			wasGzip := resp.Uncompressed
   541  
   542  			if string(content) != "response" {
   543  				t.Fatalf("wrong response content %q", content)
   544  			}
   545  			if wasGzip != test.isGzip {
   546  				t.Fatalf("response gzipped == %t, want %t", wasGzip, test.isGzip)
   547  			}
   548  			if resp.StatusCode != test.status {
   549  				t.Fatalf("response status == %d, want %d", resp.StatusCode, test.status)
   550  			}
   551  			for name, expectedValue := range test.header {
   552  				if v := resp.Header.Get(name); v != expectedValue {
   553  					t.Fatalf("response header %s == %s, want %s", name, v, expectedValue)
   554  				}
   555  			}
   556  		})
   557  	}
   558  }
   559  
   560  func TestHTTPWriteTimeout(t *testing.T) {
   561  	const (
   562  		timeoutRes = `{"jsonrpc":"2.0","id":1,"error":{"code":-32002,"message":"request timed out"}}`
   563  		greetRes   = `{"jsonrpc":"2.0","id":1,"result":"Hello"}`
   564  	)
   565  	// Set-up server
   566  	timeouts := rpc.DefaultHTTPTimeouts
   567  	timeouts.WriteTimeout = time.Second
   568  	srv := createAndStartServer(t, &httpConfig{Modules: []string{"test"}}, false, &wsConfig{}, &timeouts)
   569  	url := fmt.Sprintf("http://%v", srv.listenAddr())
   570  
   571  	// Send normal request
   572  	t.Run("message", func(t *testing.T) {
   573  		resp := rpcRequest(t, url, "test_sleep")
   574  		defer resp.Body.Close()
   575  		body, err := io.ReadAll(resp.Body)
   576  		if err != nil {
   577  			t.Fatal(err)
   578  		}
   579  		if string(body) != timeoutRes {
   580  			t.Errorf("wrong response. have %s, want %s", string(body), timeoutRes)
   581  		}
   582  	})
   583  
   584  	// Batch request
   585  	t.Run("batch", func(t *testing.T) {
   586  		want := fmt.Sprintf("[%s,%s,%s]", greetRes, timeoutRes, timeoutRes)
   587  		resp := batchRpcRequest(t, url, []string{"test_greet", "test_sleep", "test_greet"})
   588  		defer resp.Body.Close()
   589  		body, err := io.ReadAll(resp.Body)
   590  		if err != nil {
   591  			t.Fatal(err)
   592  		}
   593  		if string(body) != want {
   594  			t.Errorf("wrong response. have %s, want %s", string(body), want)
   595  		}
   596  	})
   597  }
   598  
   599  func apis() []rpc.API {
   600  	return []rpc.API{
   601  		{
   602  			Namespace: "test",
   603  			Service:   &testService{},
   604  		},
   605  	}
   606  }
   607  
   608  type testService struct{}
   609  
   610  func (s *testService) Greet() string {
   611  	return "Hello"
   612  }
   613  
   614  func (s *testService) Sleep() {
   615  	time.Sleep(1500 * time.Millisecond)
   616  }