github.com/palisadeinc/bor@v0.0.0-20230615125219-ab7196213d15/consensus/bor/heimdall/client_test.go (about)

     1  package heimdall
     2  
     3  import (
     4  	"context"
     5  	"encoding/json"
     6  	"errors"
     7  	"fmt"
     8  	"math/big"
     9  	"net"
    10  	"net/http"
    11  	"sync"
    12  	"testing"
    13  	"time"
    14  
    15  	"github.com/ethereum/go-ethereum/common"
    16  	"github.com/ethereum/go-ethereum/common/network"
    17  	"github.com/ethereum/go-ethereum/consensus/bor/heimdall/checkpoint"
    18  
    19  	"github.com/stretchr/testify/require"
    20  )
    21  
    22  // HttpHandlerFake defines the handler functions required to serve
    23  // requests to the mock heimdal server for specific functions. Add more handlers
    24  // according to requirements.
    25  type HttpHandlerFake struct {
    26  	handleFetchCheckpoint http.HandlerFunc
    27  }
    28  
    29  func (h *HttpHandlerFake) GetCheckpointHandler() http.HandlerFunc {
    30  	return func(w http.ResponseWriter, r *http.Request) {
    31  		h.handleFetchCheckpoint.ServeHTTP(w, r)
    32  	}
    33  }
    34  
    35  func CreateMockHeimdallServer(wg *sync.WaitGroup, port int, listener net.Listener, handler *HttpHandlerFake) (*http.Server, error) {
    36  	// Create a new server mux
    37  	mux := http.NewServeMux()
    38  
    39  	// Create a route for fetching latest checkpoint
    40  	mux.HandleFunc("/checkpoints/latest", func(w http.ResponseWriter, r *http.Request) {
    41  		handler.GetCheckpointHandler()(w, r)
    42  	})
    43  
    44  	// Add other routes as per requirement
    45  
    46  	// Create the server with given port and mux
    47  	srv := &http.Server{
    48  		Addr:    fmt.Sprintf("localhost:%d", port),
    49  		Handler: mux,
    50  	}
    51  
    52  	// Close the listener using the port and immediately consume it below
    53  	err := listener.Close()
    54  	if err != nil {
    55  		return nil, err
    56  	}
    57  
    58  	go func() {
    59  		defer wg.Done()
    60  
    61  		// always returns error. ErrServerClosed on graceful close
    62  		if err := srv.ListenAndServe(); err != http.ErrServerClosed {
    63  			fmt.Printf("error in server.ListenAndServe(): %v", err)
    64  		}
    65  	}()
    66  
    67  	return srv, nil
    68  }
    69  
    70  // TestFetchCheckpointFromMockHeimdall tests the heimdall client side logic
    71  // to fetch checkpoints (latest for the scope of test) from a mock heimdall server.
    72  // It can be used for debugging purpose (like response fields, marshalling/unmarshalling, etc).
    73  func TestFetchCheckpointFromMockHeimdall(t *testing.T) {
    74  	t.Parallel()
    75  
    76  	// Create a wait group for sending across the mock server
    77  	wg := &sync.WaitGroup{}
    78  	wg.Add(1)
    79  
    80  	// Initialize the fake handler and add a fake checkpoint handler function
    81  	handler := &HttpHandlerFake{}
    82  	handler.handleFetchCheckpoint = func(w http.ResponseWriter, _ *http.Request) {
    83  		err := json.NewEncoder(w).Encode(checkpoint.CheckpointResponse{
    84  			Height: "0",
    85  			Result: checkpoint.Checkpoint{
    86  				Proposer:   common.Address{},
    87  				StartBlock: big.NewInt(0),
    88  				EndBlock:   big.NewInt(512),
    89  				RootHash:   common.Hash{},
    90  				BorChainID: "15001",
    91  				Timestamp:  0,
    92  			},
    93  		})
    94  
    95  		if err != nil {
    96  			w.WriteHeader(500) // Return 500 Internal Server Error.
    97  		}
    98  	}
    99  
   100  	// Fetch available port
   101  	port, listener, err := network.FindAvailablePort()
   102  	require.NoError(t, err, "expect no error in finding available port")
   103  
   104  	// Create mock heimdall server and pass handler instance for setting up the routes
   105  	srv, err := CreateMockHeimdallServer(wg, port, listener, handler)
   106  	require.NoError(t, err, "expect no error in starting mock heimdall server")
   107  
   108  	// Create a new heimdall client and use same port for connection
   109  	client := NewHeimdallClient(fmt.Sprintf("http://localhost:%d", port))
   110  	_, err = client.FetchCheckpoint(context.Background(), -1)
   111  	require.NoError(t, err, "expect no error in fetching checkpoint")
   112  
   113  	// Shutdown the server
   114  	err = srv.Shutdown(context.TODO())
   115  	require.NoError(t, err, "expect no error in shutting down mock heimdall server")
   116  
   117  	// Wait for `wg.Done()` to be called in the mock server's routine.
   118  	wg.Wait()
   119  }
   120  
   121  // TestFetchShutdown tests the heimdall client side logic for context timeout and
   122  // interrupt handling while fetching checkpoints (latest for the scope of test)
   123  // from a mock heimdall server.
   124  func TestFetchShutdown(t *testing.T) {
   125  	t.Parallel()
   126  
   127  	// Create a wait group for sending across the mock server
   128  	wg := &sync.WaitGroup{}
   129  	wg.Add(1)
   130  
   131  	// Initialize the fake handler and add a fake checkpoint handler function
   132  	handler := &HttpHandlerFake{}
   133  
   134  	// Case1 - Testing context timeout: Create delay in serving requests for simulating timeout. Add delay slightly
   135  	// greater than `retryDelay`. This should cause the request to timeout and trigger shutdown
   136  	// due to `ctx.Done()`. Expect context timeout error.
   137  	handler.handleFetchCheckpoint = func(w http.ResponseWriter, _ *http.Request) {
   138  		time.Sleep(6 * time.Second)
   139  
   140  		err := json.NewEncoder(w).Encode(checkpoint.CheckpointResponse{
   141  			Height: "0",
   142  			Result: checkpoint.Checkpoint{
   143  				Proposer:   common.Address{},
   144  				StartBlock: big.NewInt(0),
   145  				EndBlock:   big.NewInt(512),
   146  				RootHash:   common.Hash{},
   147  				BorChainID: "15001",
   148  				Timestamp:  0,
   149  			},
   150  		})
   151  
   152  		if err != nil {
   153  			w.WriteHeader(500) // Return 500 Internal Server Error.
   154  		}
   155  	}
   156  
   157  	// Fetch available port
   158  	port, listener, err := network.FindAvailablePort()
   159  	require.NoError(t, err, "expect no error in finding available port")
   160  
   161  	// Create mock heimdall server and pass handler instance for setting up the routes
   162  	srv, err := CreateMockHeimdallServer(wg, port, listener, handler)
   163  	require.NoError(t, err, "expect no error in starting mock heimdall server")
   164  
   165  	// Create a new heimdall client and use same port for connection
   166  	client := NewHeimdallClient(fmt.Sprintf("http://localhost:%d", port))
   167  
   168  	ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
   169  
   170  	// Expect this to fail due to timeout
   171  	_, err = client.FetchCheckpoint(ctx, -1)
   172  	require.Equal(t, "context deadline exceeded", err.Error(), "expect the function error to be a context deadline exeeded error")
   173  	require.Equal(t, "context deadline exceeded", ctx.Err().Error(), "expect the ctx error to be a context deadline exeeded error")
   174  
   175  	cancel()
   176  
   177  	// Case2 - Testing context cancellation. Pass a context with timeout to the request and
   178  	// cancel it before timeout. This should cause the request to timeout and trigger shutdown
   179  	// due to `ctx.Done()`. Expect context cancellation error.
   180  	handler.handleFetchCheckpoint = func(w http.ResponseWriter, _ *http.Request) {
   181  		time.Sleep(10 * time.Millisecond)
   182  		w.WriteHeader(500) // Return 500 Internal Server Error.
   183  	}
   184  
   185  	ctx, cancel = context.WithTimeout(context.Background(), 10*time.Second) // Use some high value for timeout
   186  
   187  	// Cancel the context after a delay until we make request
   188  	go func(cancel context.CancelFunc) {
   189  		time.Sleep(10 * time.Millisecond)
   190  		cancel()
   191  	}(cancel)
   192  
   193  	// Expect this to fail due to cancellation
   194  	_, err = client.FetchCheckpoint(ctx, -1)
   195  	require.Equal(t, "context canceled", err.Error(), "expect the function error to be a context cancelled error")
   196  	require.Equal(t, "context canceled", ctx.Err().Error(), "expect the ctx error to be a context cancelled error")
   197  
   198  	// Case3 - Testing interrupt: Closing the `closeCh` in heimdall client simulating interrupt. This
   199  	// should cause the request to fail and throw an error due to `<-closeCh` in fetchWithRetry.
   200  	// Expect shutdown detected error.
   201  	handler.handleFetchCheckpoint = func(w http.ResponseWriter, _ *http.Request) {
   202  		w.WriteHeader(500) // Return 500 Internal Server Error.
   203  	}
   204  
   205  	// Close the channel after a delay until we make request
   206  	go func() {
   207  		time.Sleep(1 * time.Second)
   208  		close(client.closeCh)
   209  	}()
   210  
   211  	// Expect this to fail due to shutdown
   212  	_, err = client.FetchCheckpoint(context.Background(), -1)
   213  	require.Equal(t, ErrShutdownDetected.Error(), err.Error(), "expect the function error to be a shutdown detected error")
   214  
   215  	// Shutdown the server
   216  	err = srv.Shutdown(context.TODO())
   217  	require.NoError(t, err, "expect no error in shutting down mock heimdall server")
   218  
   219  	// Wait for `wg.Done()` to be called in the mock server's routine.
   220  	wg.Wait()
   221  }
   222  
   223  // TestContext includes bunch of simple tests to verify the working of timeout
   224  // based context and cancellation.
   225  func TestContext(t *testing.T) {
   226  	t.Parallel()
   227  
   228  	ctx, cancel1 := context.WithTimeout(context.Background(), 1*time.Second)
   229  
   230  	// Case1: Done is not yet closed, so Err returns nil.
   231  	require.NoError(t, ctx.Err(), "expect nil error")
   232  
   233  	wg := &sync.WaitGroup{}
   234  
   235  	// Case2: Check if timeout is being handled
   236  	wg.Add(1)
   237  
   238  	go func(ctx context.Context, wg *sync.WaitGroup) {
   239  		defer wg.Done()
   240  		select {
   241  		case <-ctx.Done():
   242  			// Expect context deadline exceeded error
   243  			require.Equal(t, "context deadline exceeded", ctx.Err().Error(), "expect the ctx error to be a context deadline exceeded error")
   244  		case <-time.After(2 * time.Second):
   245  			// Case for safely exiting the tests
   246  			return
   247  		}
   248  	}(ctx, wg)
   249  
   250  	// Case3: Check normal case
   251  	ctx, cancel2 := context.WithTimeout(context.Background(), 3*time.Second)
   252  
   253  	wg.Add(1)
   254  
   255  	errCh := make(chan error, 1)
   256  
   257  	go func(ctx context.Context, wg *sync.WaitGroup) {
   258  		defer wg.Done()
   259  		select {
   260  		case <-ctx.Done():
   261  			// Expect this to never occur, throw explicit error
   262  			errCh <- errors.New("unexpectecd call to `ctx.Done()`")
   263  		case <-time.After(2 * time.Second):
   264  			// Case for safely exiting the tests
   265  			errCh <- nil
   266  			return
   267  		}
   268  	}(ctx, wg)
   269  
   270  	if err := <-errCh; err != nil {
   271  		t.Fatalf("err: %v", err)
   272  	}
   273  
   274  	// Case4: Check if cancellation is being handled
   275  	ctx, cancel3 := context.WithTimeout(context.Background(), 1*time.Second)
   276  
   277  	wg.Add(1)
   278  
   279  	go func(cancel context.CancelFunc) {
   280  		time.Sleep(500 * time.Millisecond)
   281  		cancel()
   282  	}(cancel3)
   283  
   284  	go func(ctx context.Context, wg *sync.WaitGroup) {
   285  		defer wg.Done()
   286  		select {
   287  		case <-ctx.Done():
   288  			// Expect context canceled error
   289  			require.Equal(t, "context canceled", ctx.Err().Error(), "expect the ctx error to be a context canceled error")
   290  		case <-time.After(2 * time.Second):
   291  			// Case for safely exiting the tests
   292  			return
   293  		}
   294  	}(ctx, wg)
   295  
   296  	// Wait for all tests to pass
   297  	wg.Wait()
   298  
   299  	// Cancel all remaining contexts
   300  	cancel1()
   301  	cancel2()
   302  }
   303  
   304  func TestSpanURL(t *testing.T) {
   305  	t.Parallel()
   306  
   307  	url, err := spanURL("http://bor0", 1)
   308  	if err != nil {
   309  		t.Fatal("got an error", err)
   310  	}
   311  
   312  	const expected = "http://bor0/bor/span/1"
   313  
   314  	if url.String() != expected {
   315  		t.Fatalf("expected URL %q, got %q", url.String(), expected)
   316  	}
   317  }
   318  
   319  func TestStateSyncURL(t *testing.T) {
   320  	t.Parallel()
   321  
   322  	url, err := stateSyncURL("http://bor0", 10, 100)
   323  	if err != nil {
   324  		t.Fatal("got an error", err)
   325  	}
   326  
   327  	const expected = "http://bor0/clerk/event-record/list?from-id=10&to-time=100&limit=50"
   328  
   329  	if url.String() != expected {
   330  		t.Fatalf("expected URL %q, got %q", url.String(), expected)
   331  	}
   332  }