mirror of
https://github.com/tendermint/tendermint.git
synced 2026-01-08 14:21:14 +00:00
Responses are constructed from requests using MakeResponse, MakeError, and MakeErrorf. This ensures the response is always paired with the correct ID, makes cases where there is no ID more explicit at the usage site, and consolidates the handling of error introspection across transports. The logic for unpacking errors and assigning JSON-RPC response types was previously duplicated in three places. Consolidate it in the types package for the RPC subsystem. * update test cases
173 lines
4.5 KiB
Go
173 lines
4.5 KiB
Go
package server
|
|
|
|
import (
|
|
"context"
|
|
"crypto/tls"
|
|
"fmt"
|
|
"io"
|
|
"net"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"sync"
|
|
"sync/atomic"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/fortytw2/leaktest"
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
|
|
"github.com/tendermint/tendermint/libs/log"
|
|
rpctypes "github.com/tendermint/tendermint/rpc/jsonrpc/types"
|
|
)
|
|
|
|
type sampleResult struct {
|
|
Value string `json:"value"`
|
|
}
|
|
|
|
func TestMaxOpenConnections(t *testing.T) {
|
|
const max = 5 // max simultaneous connections
|
|
|
|
t.Cleanup(leaktest.Check(t))
|
|
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
defer cancel()
|
|
|
|
logger := log.NewNopLogger()
|
|
|
|
// Start the server.
|
|
var open int32
|
|
mux := http.NewServeMux()
|
|
mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
|
|
if n := atomic.AddInt32(&open, 1); n > int32(max) {
|
|
t.Errorf("%d open connections, want <= %d", n, max)
|
|
}
|
|
defer atomic.AddInt32(&open, -1)
|
|
time.Sleep(10 * time.Millisecond)
|
|
fmt.Fprint(w, "some body")
|
|
})
|
|
config := DefaultConfig()
|
|
l, err := Listen("tcp://127.0.0.1:0", max)
|
|
require.NoError(t, err)
|
|
defer l.Close()
|
|
|
|
go Serve(ctx, l, mux, logger, config) //nolint:errcheck // ignore for tests
|
|
|
|
// Make N GET calls to the server.
|
|
attempts := max * 2
|
|
var wg sync.WaitGroup
|
|
var failed int32
|
|
for i := 0; i < attempts; i++ {
|
|
wg.Add(1)
|
|
go func() {
|
|
defer wg.Done()
|
|
c := http.Client{Timeout: 3 * time.Second}
|
|
r, err := c.Get("http://" + l.Addr().String())
|
|
if err != nil {
|
|
atomic.AddInt32(&failed, 1)
|
|
return
|
|
}
|
|
defer r.Body.Close()
|
|
}()
|
|
}
|
|
wg.Wait()
|
|
|
|
// We expect some Gets to fail as the server's accept queue is filled,
|
|
// but most should succeed.
|
|
if int(failed) >= attempts/2 {
|
|
t.Errorf("%d requests failed within %d attempts", failed, attempts)
|
|
}
|
|
}
|
|
|
|
func TestServeTLS(t *testing.T) {
|
|
t.Cleanup(leaktest.Check(t))
|
|
|
|
ln, err := net.Listen("tcp", "localhost:0")
|
|
require.NoError(t, err)
|
|
defer ln.Close()
|
|
|
|
mux := http.NewServeMux()
|
|
mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
|
|
fmt.Fprint(w, "some body")
|
|
})
|
|
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
defer cancel()
|
|
|
|
logger := log.NewNopLogger()
|
|
|
|
chErr := make(chan error, 1)
|
|
go func() {
|
|
select {
|
|
case chErr <- ServeTLS(ctx, ln, mux, "test.crt", "test.key", logger, DefaultConfig()):
|
|
case <-ctx.Done():
|
|
}
|
|
}()
|
|
|
|
select {
|
|
case err := <-chErr:
|
|
require.NoError(t, err)
|
|
case <-time.After(100 * time.Millisecond):
|
|
}
|
|
|
|
tr := &http.Transport{
|
|
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
|
|
}
|
|
c := &http.Client{Transport: tr}
|
|
res, err := c.Get("https://" + ln.Addr().String())
|
|
require.NoError(t, err)
|
|
defer res.Body.Close()
|
|
assert.Equal(t, http.StatusOK, res.StatusCode)
|
|
|
|
body, err := io.ReadAll(res.Body)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, []byte("some body"), body)
|
|
}
|
|
|
|
func TestWriteRPCResponse(t *testing.T) {
|
|
req := rpctypes.RPCRequest{ID: rpctypes.JSONRPCIntID(-1)}
|
|
|
|
// one argument
|
|
w := httptest.NewRecorder()
|
|
logger := log.NewNopLogger()
|
|
writeRPCResponse(w, logger, req.MakeResponse(&sampleResult{"hello"}))
|
|
resp := w.Result()
|
|
body, err := io.ReadAll(resp.Body)
|
|
require.NoError(t, resp.Body.Close())
|
|
require.NoError(t, err)
|
|
assert.Equal(t, 200, resp.StatusCode)
|
|
assert.Equal(t, "application/json", resp.Header.Get("Content-Type"))
|
|
assert.Equal(t, "", resp.Header.Get("Cache-control"))
|
|
assert.Equal(t, `{"jsonrpc":"2.0","id":-1,"result":{"value":"hello"}}`, string(body))
|
|
|
|
// multiple arguments
|
|
w = httptest.NewRecorder()
|
|
writeRPCResponse(w, logger,
|
|
req.MakeResponse(&sampleResult{"hello"}),
|
|
req.MakeResponse(&sampleResult{"world"}),
|
|
)
|
|
resp = w.Result()
|
|
body, err = io.ReadAll(resp.Body)
|
|
require.NoError(t, resp.Body.Close())
|
|
require.NoError(t, err)
|
|
|
|
assert.Equal(t, 200, resp.StatusCode)
|
|
assert.Equal(t, "application/json", resp.Header.Get("Content-Type"))
|
|
assert.Equal(t, `[{"jsonrpc":"2.0","id":-1,"result":{"value":"hello"}},`+
|
|
`{"jsonrpc":"2.0","id":-1,"result":{"value":"world"}}]`, string(body))
|
|
}
|
|
|
|
func TestWriteHTTPResponse(t *testing.T) {
|
|
w := httptest.NewRecorder()
|
|
logger := log.NewNopLogger()
|
|
req := rpctypes.RPCRequest{ID: rpctypes.JSONRPCIntID(-1)}
|
|
writeHTTPResponse(w, logger, req.MakeErrorf(rpctypes.CodeInternalError, "foo"))
|
|
resp := w.Result()
|
|
body, err := io.ReadAll(resp.Body)
|
|
require.NoError(t, resp.Body.Close())
|
|
require.NoError(t, err)
|
|
assert.Equal(t, http.StatusOK, resp.StatusCode)
|
|
assert.Equal(t, "application/json", resp.Header.Get("Content-Type"))
|
|
assert.Equal(t, `{"code":-32603,"message":"Internal error","data":"foo"}`, string(body))
|
|
}
|