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 }