github.com/jimmyx0x/go-ethereum@v1.10.28/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/ethereum/go-ethereum/internal/testlog"
    32  	"github.com/ethereum/go-ethereum/log"
    33  	"github.com/ethereum/go-ethereum/rpc"
    34  	"github.com/golang-jwt/jwt/v4"
    35  	"github.com/gorilla/websocket"
    36  	"github.com/stretchr/testify/assert"
    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("GET", "/", 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("POST", 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  	return resp
   324  }
   325  
   326  type testClaim map[string]interface{}
   327  
   328  func (testClaim) Valid() error {
   329  	return nil
   330  }
   331  
   332  func TestJWT(t *testing.T) {
   333  	var secret = []byte("secret")
   334  	issueToken := func(secret []byte, method jwt.SigningMethod, input map[string]interface{}) string {
   335  		if method == nil {
   336  			method = jwt.SigningMethodHS256
   337  		}
   338  		ss, _ := jwt.NewWithClaims(method, testClaim(input)).SignedString(secret)
   339  		return ss
   340  	}
   341  	srv := createAndStartServer(t, &httpConfig{jwtSecret: []byte("secret")},
   342  		true, &wsConfig{Origins: []string{"*"}, jwtSecret: []byte("secret")}, nil)
   343  	wsUrl := fmt.Sprintf("ws://%v", srv.listenAddr())
   344  	htUrl := fmt.Sprintf("http://%v", srv.listenAddr())
   345  
   346  	expOk := []func() string{
   347  		func() string {
   348  			return fmt.Sprintf("Bearer %v", issueToken(secret, nil, testClaim{"iat": time.Now().Unix()}))
   349  		},
   350  		func() string {
   351  			return fmt.Sprintf("Bearer %v", issueToken(secret, nil, testClaim{"iat": time.Now().Unix() + 4}))
   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{
   358  				"iat": time.Now().Unix(),
   359  				"exp": time.Now().Unix() + 2,
   360  			}))
   361  		},
   362  		func() string {
   363  			return fmt.Sprintf("Bearer %v", issueToken(secret, nil, testClaim{
   364  				"iat": time.Now().Unix(),
   365  				"bar": "baz",
   366  			}))
   367  		},
   368  	}
   369  	for i, tokenFn := range expOk {
   370  		token := tokenFn()
   371  		if err := wsRequest(t, wsUrl, "Authorization", token); err != nil {
   372  			t.Errorf("test %d-ws, token '%v': expected ok, got %v", i, token, err)
   373  		}
   374  		token = tokenFn()
   375  		if resp := rpcRequest(t, htUrl, testMethod, "Authorization", token); resp.StatusCode != 200 {
   376  			t.Errorf("test %d-http, token '%v': expected ok, got %v", i, token, resp.StatusCode)
   377  		}
   378  	}
   379  
   380  	expFail := []func() string{
   381  		// future
   382  		func() string {
   383  			return fmt.Sprintf("Bearer %v", issueToken(secret, nil, testClaim{"iat": time.Now().Unix() + int64(jwtExpiryTimeout.Seconds()) + 1}))
   384  		},
   385  		// stale
   386  		func() string {
   387  			return fmt.Sprintf("Bearer %v", issueToken(secret, nil, testClaim{"iat": time.Now().Unix() - int64(jwtExpiryTimeout.Seconds()) - 1}))
   388  		},
   389  		// wrong algo
   390  		func() string {
   391  			return fmt.Sprintf("Bearer %v", issueToken(secret, jwt.SigningMethodHS512, testClaim{"iat": time.Now().Unix() + 4}))
   392  		},
   393  		// expired
   394  		func() string {
   395  			return fmt.Sprintf("Bearer %v", issueToken(secret, nil, testClaim{"iat": time.Now().Unix(), "exp": time.Now().Unix()}))
   396  		},
   397  		// missing mandatory iat
   398  		func() string {
   399  			return fmt.Sprintf("Bearer %v", issueToken(secret, nil, testClaim{}))
   400  		},
   401  		//  wrong secret
   402  		func() string {
   403  			return fmt.Sprintf("Bearer %v", issueToken([]byte("wrong"), nil, testClaim{"iat": time.Now().Unix()}))
   404  		},
   405  		func() string {
   406  			return fmt.Sprintf("Bearer %v", issueToken([]byte{}, nil, testClaim{"iat": time.Now().Unix()}))
   407  		},
   408  		func() string {
   409  			return fmt.Sprintf("Bearer %v", issueToken(nil, nil, testClaim{"iat": time.Now().Unix()}))
   410  		},
   411  		// Various malformed syntax
   412  		func() string {
   413  			return fmt.Sprintf("%v", issueToken(secret, nil, testClaim{"iat": time.Now().Unix()}))
   414  		},
   415  		func() string {
   416  			return fmt.Sprintf("Bearer  %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\t%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  	}
   434  	for i, tokenFn := range expFail {
   435  		token := tokenFn()
   436  		if err := wsRequest(t, wsUrl, "Authorization", token); err == nil {
   437  			t.Errorf("tc %d-ws, token '%v': expected not to allow,  got ok", i, token)
   438  		}
   439  
   440  		token = tokenFn()
   441  		resp := rpcRequest(t, htUrl, testMethod, "Authorization", token)
   442  		if resp.StatusCode != http.StatusUnauthorized {
   443  			t.Errorf("tc %d-http, token '%v': expected not to allow,  got %v", i, token, resp.StatusCode)
   444  		}
   445  	}
   446  	srv.stop()
   447  }
   448  
   449  func TestGzipHandler(t *testing.T) {
   450  	type gzipTest struct {
   451  		name    string
   452  		handler http.HandlerFunc
   453  		status  int
   454  		isGzip  bool
   455  		header  map[string]string
   456  	}
   457  	tests := []gzipTest{
   458  		{
   459  			name: "Write",
   460  			handler: func(w http.ResponseWriter, r *http.Request) {
   461  				w.Write([]byte("response"))
   462  			},
   463  			isGzip: true,
   464  			status: 200,
   465  		},
   466  		{
   467  			name: "WriteHeader",
   468  			handler: func(w http.ResponseWriter, r *http.Request) {
   469  				w.Header().Set("x-foo", "bar")
   470  				w.WriteHeader(205)
   471  				w.Write([]byte("response"))
   472  			},
   473  			isGzip: true,
   474  			status: 205,
   475  			header: map[string]string{"x-foo": "bar"},
   476  		},
   477  		{
   478  			name: "WriteContentLength",
   479  			handler: func(w http.ResponseWriter, r *http.Request) {
   480  				w.Header().Set("content-length", "8")
   481  				w.Write([]byte("response"))
   482  			},
   483  			isGzip: true,
   484  			status: 200,
   485  		},
   486  		{
   487  			name: "Flush",
   488  			handler: func(w http.ResponseWriter, r *http.Request) {
   489  				w.Write([]byte("res"))
   490  				w.(http.Flusher).Flush()
   491  				w.Write([]byte("ponse"))
   492  			},
   493  			isGzip: true,
   494  			status: 200,
   495  		},
   496  		{
   497  			name: "disable",
   498  			handler: func(w http.ResponseWriter, r *http.Request) {
   499  				w.Header().Set("transfer-encoding", "identity")
   500  				w.Header().Set("x-foo", "bar")
   501  				w.Write([]byte("response"))
   502  			},
   503  			isGzip: false,
   504  			status: 200,
   505  			header: map[string]string{"x-foo": "bar"},
   506  		},
   507  		{
   508  			name: "disable-WriteHeader",
   509  			handler: func(w http.ResponseWriter, r *http.Request) {
   510  				w.Header().Set("transfer-encoding", "identity")
   511  				w.Header().Set("x-foo", "bar")
   512  				w.WriteHeader(205)
   513  				w.Write([]byte("response"))
   514  			},
   515  			isGzip: false,
   516  			status: 205,
   517  			header: map[string]string{"x-foo": "bar"},
   518  		},
   519  	}
   520  
   521  	for _, test := range tests {
   522  		test := test
   523  		t.Run(test.name, func(t *testing.T) {
   524  			srv := httptest.NewServer(newGzipHandler(test.handler))
   525  			defer srv.Close()
   526  
   527  			resp, err := http.Get(srv.URL)
   528  			if err != nil {
   529  				t.Fatal(err)
   530  			}
   531  			defer resp.Body.Close()
   532  
   533  			content, err := io.ReadAll(resp.Body)
   534  			if err != nil {
   535  				t.Fatal(err)
   536  			}
   537  			wasGzip := resp.Uncompressed
   538  
   539  			if string(content) != "response" {
   540  				t.Fatalf("wrong response content %q", content)
   541  			}
   542  			if wasGzip != test.isGzip {
   543  				t.Fatalf("response gzipped == %t, want %t", wasGzip, test.isGzip)
   544  			}
   545  			if resp.StatusCode != test.status {
   546  				t.Fatalf("response status == %d, want %d", resp.StatusCode, test.status)
   547  			}
   548  			for name, expectedValue := range test.header {
   549  				if v := resp.Header.Get(name); v != expectedValue {
   550  					t.Fatalf("response header %s == %s, want %s", name, v, expectedValue)
   551  				}
   552  			}
   553  		})
   554  	}
   555  }
   556  
   557  func TestHTTPWriteTimeout(t *testing.T) {
   558  	const (
   559  		timeoutRes = `{"jsonrpc":"2.0","id":1,"error":{"code":-32002,"message":"request timed out"}}`
   560  		greetRes   = `{"jsonrpc":"2.0","id":1,"result":"Hello"}`
   561  	)
   562  	// Set-up server
   563  	timeouts := rpc.DefaultHTTPTimeouts
   564  	timeouts.WriteTimeout = time.Second
   565  	srv := createAndStartServer(t, &httpConfig{Modules: []string{"test"}}, false, &wsConfig{}, &timeouts)
   566  	url := fmt.Sprintf("http://%v", srv.listenAddr())
   567  
   568  	// Send normal request
   569  	t.Run("message", func(t *testing.T) {
   570  		resp := rpcRequest(t, url, "test_sleep")
   571  		defer resp.Body.Close()
   572  		body, err := io.ReadAll(resp.Body)
   573  		if err != nil {
   574  			t.Fatal(err)
   575  		}
   576  		if string(body) != timeoutRes {
   577  			t.Errorf("wrong response. have %s, want %s", string(body), timeoutRes)
   578  		}
   579  	})
   580  
   581  	// Batch request
   582  	t.Run("batch", func(t *testing.T) {
   583  		want := fmt.Sprintf("[%s,%s,%s]", greetRes, timeoutRes, timeoutRes)
   584  		resp := batchRpcRequest(t, url, []string{"test_greet", "test_sleep", "test_greet"})
   585  		defer resp.Body.Close()
   586  		body, err := io.ReadAll(resp.Body)
   587  		if err != nil {
   588  			t.Fatal(err)
   589  		}
   590  		if string(body) != want {
   591  			t.Errorf("wrong response. have %s, want %s", string(body), want)
   592  		}
   593  	})
   594  }
   595  
   596  func apis() []rpc.API {
   597  	return []rpc.API{
   598  		{
   599  			Namespace: "test",
   600  			Service:   &testService{},
   601  		},
   602  	}
   603  }
   604  
   605  type testService struct{}
   606  
   607  func (s *testService) Greet() string {
   608  	return "Hello"
   609  }
   610  
   611  func (s *testService) Sleep() {
   612  	time.Sleep(1500 * time.Millisecond)
   613  }