From cc3cd2f7753fd1dfee0847521ec962a7135486b1 Mon Sep 17 00:00:00 2001 From: crypto-services Date: Tue, 20 Feb 2024 14:35:41 +0800 Subject: [PATCH] Add testing --- health/check_block.go | 7 +- health/check_peers.go | 4 +- health/check_synced.go | 3 +- health/check_time.go | 4 +- health/health.go | 45 +-- health/health_test.go | 742 +++++++++++++++++++++++++++++++++++++++++ health/interface.go | 15 + health/service.go | 4 +- 8 files changed, 790 insertions(+), 34 deletions(-) create mode 100644 health/health_test.go create mode 100644 health/interface.go diff --git a/health/check_block.go b/health/check_block.go index f8e95ddcae..a68b560100 100644 --- a/health/check_block.go +++ b/health/check_block.go @@ -2,16 +2,13 @@ package health import ( "context" - "fmt" "math/big" - - "github.com/ethereum/go-ethereum/ethclient" ) -func checkBlockNumber(ec *ethclient.Client, blockNumber *big.Int) error { +func checkBlockNumber(ec ethClient, blockNumber *big.Int) error { _, err := ec.BlockByNumber(context.TODO(), blockNumber) if err != nil { - return fmt.Errorf("no known block with number %v (%x hex)", blockNumber.Int64(), blockNumber.Int64()) + return err } return nil } diff --git a/health/check_peers.go b/health/check_peers.go index d4b47cbe6d..e20973348c 100644 --- a/health/check_peers.go +++ b/health/check_peers.go @@ -4,15 +4,13 @@ import ( "context" "errors" "fmt" - - "github.com/ethereum/go-ethereum/ethclient" ) var ( errNotEnoughPeers = errors.New("not enough peers") ) -func checkMinPeers(ec *ethclient.Client, minPeerCount uint) error { +func checkMinPeers(ec ethClient, minPeerCount uint) error { peerCount, err := ec.PeerCount(context.TODO()) if err != nil { return err diff --git a/health/check_synced.go b/health/check_synced.go index 78d3d025cf..580b6a28fb 100644 --- a/health/check_synced.go +++ b/health/check_synced.go @@ -4,7 +4,6 @@ import ( "errors" "net/http" - "github.com/ethereum/go-ethereum/ethclient" "github.com/ethereum/go-ethereum/log" ) @@ -12,7 +11,7 @@ var ( errNotSynced = errors.New("not synced") ) -func checkSynced(ec *ethclient.Client, r *http.Request) error { +func checkSynced(ec ethClient, r *http.Request) error { i, err := ec.SyncProgress(r.Context()) if err != nil { log.Root().Warn("Unable to check sync status for healthcheck", "err", err.Error()) diff --git a/health/check_time.go b/health/check_time.go index a1a7d820a7..ae0df8dad4 100644 --- a/health/check_time.go +++ b/health/check_time.go @@ -5,8 +5,6 @@ import ( "errors" "fmt" "net/http" - - "github.com/ethereum/go-ethereum/ethclient" ) var ( @@ -14,7 +12,7 @@ var ( ) func checkTime( - ec *ethclient.Client, + ec ethClient, r *http.Request, seconds int, ) error { diff --git a/health/health.go b/health/health.go index 78ff97bfe5..402c02686a 100644 --- a/health/health.go +++ b/health/health.go @@ -16,6 +16,7 @@ import ( const ( healthHeader = "X-GETH-HEALTHCHECK" + query = "query" synced = "synced" minPeerCount = "min_peer_count" checkBlock = "check_block" @@ -23,8 +24,8 @@ const ( ) var ( - errCheckDisabled = errors.New("error check disabled") - errBadHeaderValue = errors.New("bad header value") + errCheckDisabled = errors.New("error check disabled") + errInvalidValue = errors.New("invalid value provided") ) type requestBody struct { @@ -34,7 +35,7 @@ type requestBody struct { MaxSecondsBehind *int `json:"max_seconds_behind"` } -func (h *handler) processFromHeaders(headers []string, w http.ResponseWriter, r *http.Request) { +func processFromHeaders(ec ethClient, headers []string, w http.ResponseWriter, r *http.Request) { var ( errCheckSynced = errCheckDisabled errCheckPeer = errCheckDisabled @@ -45,7 +46,7 @@ func (h *handler) processFromHeaders(headers []string, w http.ResponseWriter, r for _, header := range headers { lHeader := strings.ToLower(header) if lHeader == synced { - errCheckSynced = checkSynced(h.ec, r) + errCheckSynced = checkSynced(ec, r) } if strings.HasPrefix(lHeader, minPeerCount) { peers, err := strconv.Atoi(strings.TrimPrefix(lHeader, minPeerCount)) @@ -53,7 +54,7 @@ func (h *handler) processFromHeaders(headers []string, w http.ResponseWriter, r errCheckPeer = err break } - errCheckPeer = checkMinPeers(h.ec, uint(peers)) + errCheckPeer = checkMinPeers(ec, uint(peers)) } if strings.HasPrefix(lHeader, checkBlock) { block, err := strconv.Atoi(strings.TrimPrefix(lHeader, checkBlock)) @@ -61,7 +62,7 @@ func (h *handler) processFromHeaders(headers []string, w http.ResponseWriter, r errCheckBlock = err break } - errCheckBlock = checkBlockNumber(h.ec, big.NewInt(int64(block))) + errCheckBlock = checkBlockNumber(ec, big.NewInt(int64(block))) } if strings.HasPrefix(lHeader, maxSecondsBehind) { seconds, err := strconv.Atoi(strings.TrimPrefix(lHeader, maxSecondsBehind)) @@ -70,18 +71,18 @@ func (h *handler) processFromHeaders(headers []string, w http.ResponseWriter, r break } if seconds < 0 { - errCheckSeconds = errBadHeaderValue + errCheckSeconds = errInvalidValue break } now := time.Now().Unix() - errCheckSeconds = checkTime(h.ec, r, int(now)-seconds) + errCheckSeconds = checkTime(ec, r, int(now)-seconds) } } - reportHealth(errCheckSynced, errCheckPeer, errCheckBlock, errCheckSeconds, w) + reportHealth(nil, errCheckSynced, errCheckPeer, errCheckBlock, errCheckSeconds, w) } -func (h *handler) processFromBody(w http.ResponseWriter, r *http.Request) { +func processFromBody(ec ethClient, w http.ResponseWriter, r *http.Request) { body, errParse := parseHealthCheckBody(r.Body) defer r.Body.Close() @@ -96,37 +97,43 @@ func (h *handler) processFromBody(w http.ResponseWriter, r *http.Request) { log.Root().Warn("Unable to process healthcheck request", "err", errParse) } else { if body.Synced != nil { - errCheckSynced = checkSynced(h.ec, r) + errCheckSynced = checkSynced(ec, r) } if body.MinPeerCount != nil { - errCheckPeer = checkMinPeers(h.ec, *body.MinPeerCount) + errCheckPeer = checkMinPeers(ec, *body.MinPeerCount) } if body.CheckBlock != nil { - errCheckBlock = checkBlockNumber(h.ec, big.NewInt(int64(*body.CheckBlock))) + errCheckBlock = checkBlockNumber(ec, big.NewInt(int64(*body.CheckBlock))) } if body.MaxSecondsBehind != nil { seconds := *body.MaxSecondsBehind if seconds < 0 { - errCheckSeconds = errBadHeaderValue + errCheckSeconds = errInvalidValue + } else { + now := time.Now().Unix() + errCheckSeconds = checkTime(ec, r, int(now)-seconds) } - now := time.Now().Unix() - errCheckSeconds = checkTime(h.ec, r, int(now)-seconds) } } - err := reportHealth(errCheckSynced, errCheckPeer, errCheckBlock, errCheckSeconds, w) + err := reportHealth(errParse, errCheckSynced, errCheckPeer, errCheckBlock, errCheckSeconds, w) if err != nil { log.Root().Warn("Unable to process healthcheck request", "err", err) } } -func reportHealth(errCheckSynced, errCheckPeer, errCheckBlock, errCheckSeconds error, w http.ResponseWriter) error { +func reportHealth(errParse, errCheckSynced, errCheckPeer, errCheckBlock, errCheckSeconds error, w http.ResponseWriter) error { statusCode := http.StatusOK errs := make(map[string]string) + if shouldChangeStatusCode(errParse) { + statusCode = http.StatusInternalServerError + } + errs[query] = errorStringOrOK(errParse) + if shouldChangeStatusCode(errCheckSynced) { statusCode = http.StatusInternalServerError } @@ -188,7 +195,7 @@ func shouldChangeStatusCode(err error) bool { func errorStringOrOK(err error) string { if err == nil { - return "HEALTHY" + return "OK" } if errors.Is(err, errCheckDisabled) { diff --git a/health/health_test.go b/health/health_test.go new file mode 100644 index 0000000000..d66edc2d99 --- /dev/null +++ b/health/health_test.go @@ -0,0 +1,742 @@ +package health + +import ( + "context" + "encoding/json" + "errors" + "io" + "math/big" + "net/http" + "net/http/httptest" + "strings" + "testing" + "time" + + "github.com/ethereum/go-ethereum" + "github.com/ethereum/go-ethereum/core/types" +) + +type ethClientStub struct { + peersResult uint64 + peersError error + blockResult *types.Block + blockError error + syncingResult *ethereum.SyncProgress + syncingError error +} + +func (e *ethClientStub) PeerCount(_ context.Context) (uint64, error) { + return e.peersResult, e.peersError +} + +func (e *ethClientStub) BlockByNumber(_ context.Context, _ *big.Int) (*types.Block, error) { + return e.blockResult, e.blockError +} + +func (e *ethClientStub) SyncProgress(_ context.Context) (*ethereum.SyncProgress, error) { + return e.syncingResult, e.syncingError +} + +func TestProcessFromHeaders(t *testing.T) { + cases := []struct { + headers []string + clientPeerResult uint64 + clientPeerError error + clientBlockResult *types.Block + clientBlockError error + clientSyncingResult *ethereum.SyncProgress + clientSyncingError error + expectedStatusCode int + expectedBody map[string]string + }{ + // 0 - sync check enabled - syncing + { + headers: []string{"synced"}, + clientPeerResult: uint64(1), + clientPeerError: nil, + clientBlockResult: &types.Block{}, + clientBlockError: nil, + clientSyncingResult: nil, + clientSyncingError: nil, + expectedStatusCode: http.StatusOK, + expectedBody: map[string]string{ + synced: "OK", + minPeerCount: "DISABLED", + checkBlock: "DISABLED", + maxSecondsBehind: "DISABLED", + }, + }, + // 1 - sync check enabled - not syncing + { + headers: []string{"synced"}, + clientPeerResult: uint64(1), + clientPeerError: nil, + clientBlockResult: nil, + clientBlockError: nil, + clientSyncingResult: ðereum.SyncProgress{}, + clientSyncingError: nil, + expectedStatusCode: http.StatusInternalServerError, + expectedBody: map[string]string{ + synced: "ERROR: not synced", + minPeerCount: "DISABLED", + checkBlock: "DISABLED", + maxSecondsBehind: "DISABLED", + }, + }, + // 2 - sync check enabled - error checking sync + { + headers: []string{"synced"}, + clientPeerResult: uint64(1), + clientPeerError: nil, + clientBlockResult: nil, + clientBlockError: nil, + clientSyncingResult: ðereum.SyncProgress{}, + clientSyncingError: errors.New("problem checking sync"), + expectedStatusCode: http.StatusInternalServerError, + expectedBody: map[string]string{ + synced: "ERROR: problem checking sync", + minPeerCount: "DISABLED", + checkBlock: "DISABLED", + maxSecondsBehind: "DISABLED", + }, + }, + // 3 - peer count enabled - good request + { + headers: []string{"min_peer_count1"}, + clientPeerResult: uint64(1), + clientPeerError: nil, + clientBlockResult: nil, + clientBlockError: nil, + clientSyncingResult: nil, + clientSyncingError: nil, + expectedStatusCode: http.StatusOK, + expectedBody: map[string]string{ + synced: "DISABLED", + minPeerCount: "OK", + checkBlock: "DISABLED", + maxSecondsBehind: "DISABLED", + }, + }, + // 4 - peer count enabled - not enough peers + { + headers: []string{"min_peer_count10"}, + clientPeerResult: uint64(1), + clientPeerError: nil, + clientBlockResult: nil, + clientBlockError: nil, + clientSyncingResult: nil, + clientSyncingError: nil, + expectedStatusCode: http.StatusInternalServerError, + expectedBody: map[string]string{ + synced: "DISABLED", + minPeerCount: "ERROR: not enough peers: 1 (minimum 10)", + checkBlock: "DISABLED", + maxSecondsBehind: "DISABLED", + }, + }, + // 5 - peer count enabled - error checking peers + { + headers: []string{"min_peer_count10"}, + clientPeerResult: uint64(1), + clientPeerError: errors.New("problem checking peers"), + clientBlockResult: nil, + clientBlockError: nil, + clientSyncingResult: nil, + clientSyncingError: nil, + expectedStatusCode: http.StatusInternalServerError, + expectedBody: map[string]string{ + synced: "DISABLED", + minPeerCount: "ERROR: problem checking peers", + checkBlock: "DISABLED", + maxSecondsBehind: "DISABLED", + }, + }, + // 6 - peer count enabled - badly formed request + { + headers: []string{"min_peer_countABC"}, + clientPeerResult: uint64(1), + clientPeerError: nil, + clientBlockResult: nil, + clientBlockError: nil, + clientSyncingResult: nil, + clientSyncingError: nil, + expectedStatusCode: http.StatusInternalServerError, + expectedBody: map[string]string{ + synced: "DISABLED", + minPeerCount: "ERROR: strconv.Atoi: parsing \"abc\": invalid syntax", + checkBlock: "DISABLED", + maxSecondsBehind: "DISABLED", + }, + }, + // 7 - block check - all ok + { + headers: []string{"check_block10"}, + clientPeerResult: uint64(1), + clientPeerError: nil, + clientBlockResult: &types.Block{}, + clientBlockError: nil, + clientSyncingResult: nil, + clientSyncingError: nil, + expectedStatusCode: http.StatusOK, + expectedBody: map[string]string{ + synced: "DISABLED", + minPeerCount: "DISABLED", + checkBlock: "OK", + maxSecondsBehind: "DISABLED", + }, + }, + // 8 - block check - no block found + { + headers: []string{"check_block10"}, + clientPeerResult: uint64(1), + clientPeerError: nil, + clientBlockResult: nil, + clientBlockError: errors.New("not found"), + clientSyncingResult: nil, + clientSyncingError: nil, + expectedStatusCode: http.StatusInternalServerError, + expectedBody: map[string]string{ + synced: "DISABLED", + minPeerCount: "DISABLED", + checkBlock: "ERROR: not found", + maxSecondsBehind: "DISABLED", + }, + }, + // 9 - block check - error checking block + { + headers: []string{"check_block10"}, + clientPeerResult: uint64(1), + clientPeerError: nil, + clientBlockResult: &types.Block{}, + clientBlockError: errors.New("problem checking block"), + clientSyncingResult: nil, + clientSyncingError: nil, + expectedStatusCode: http.StatusInternalServerError, + expectedBody: map[string]string{ + synced: "DISABLED", + minPeerCount: "DISABLED", + checkBlock: "ERROR: problem checking block", + maxSecondsBehind: "DISABLED", + }, + }, + // 10 - block check - badly formed request + { + headers: []string{"check_blockABC"}, + clientPeerResult: uint64(1), + clientPeerError: nil, + clientBlockResult: nil, + clientBlockError: nil, + clientSyncingResult: nil, + clientSyncingError: nil, + expectedStatusCode: http.StatusInternalServerError, + expectedBody: map[string]string{ + synced: "DISABLED", + minPeerCount: "DISABLED", + checkBlock: "ERROR: strconv.Atoi: parsing \"abc\": invalid syntax", + maxSecondsBehind: "DISABLED", + }, + }, + // 11 - seconds check - all ok + { + headers: []string{"max_seconds_behind60"}, + clientPeerResult: uint64(1), + clientPeerError: nil, + clientBlockResult: types.NewBlockWithHeader(&types.Header{ + Time: uint64(time.Now().Add(-10 * time.Second).Unix()), + }), + clientBlockError: nil, + clientSyncingResult: nil, + clientSyncingError: nil, + expectedStatusCode: http.StatusOK, + expectedBody: map[string]string{ + synced: "DISABLED", + minPeerCount: "DISABLED", + checkBlock: "DISABLED", + maxSecondsBehind: "OK", + }, + }, + // 12 - seconds check - too old + { + headers: []string{"max_seconds_behind60"}, + clientPeerResult: uint64(1), + clientPeerError: nil, + clientBlockResult: types.NewBlockWithHeader(&types.Header{ + Time: uint64(time.Now().Add(-1 * time.Hour).Unix()), + }), + clientBlockError: nil, + clientSyncingResult: nil, + clientSyncingError: nil, + expectedStatusCode: http.StatusInternalServerError, + expectedBody: map[string]string{ + synced: "DISABLED", + minPeerCount: "DISABLED", + checkBlock: "DISABLED", + maxSecondsBehind: "ERROR: timestamp too old: got ts:", + }, + }, + // 13 - seconds check - less than 0 seconds + { + headers: []string{"max_seconds_behind-1"}, + clientPeerResult: uint64(1), + clientPeerError: nil, + clientBlockResult: types.NewBlockWithHeader(&types.Header{ + Time: uint64(time.Now().Add(1 * time.Hour).Unix()), + }), + clientBlockError: nil, + clientSyncingResult: nil, + clientSyncingError: nil, + expectedStatusCode: http.StatusInternalServerError, + expectedBody: map[string]string{ + synced: "DISABLED", + minPeerCount: "DISABLED", + checkBlock: "DISABLED", + maxSecondsBehind: "ERROR: invalid value provided", + }, + }, + // 14 - seconds check - badly formed request + { + headers: []string{"max_seconds_behindABC"}, + clientPeerResult: uint64(1), + clientPeerError: nil, + clientBlockResult: &types.Block{}, + clientBlockError: nil, + clientSyncingResult: nil, + clientSyncingError: nil, + expectedStatusCode: http.StatusInternalServerError, + expectedBody: map[string]string{ + synced: "DISABLED", + minPeerCount: "DISABLED", + checkBlock: "DISABLED", + maxSecondsBehind: "ERROR: strconv.Atoi: parsing \"abc\": invalid syntax", + }, + }, + // 15 - all checks - report ok + { + headers: []string{"synced", "check_block10", "min_peer_count1", "max_seconds_behind60"}, + clientPeerResult: uint64(10), + clientPeerError: nil, + clientBlockResult: types.NewBlockWithHeader(&types.Header{ + Time: uint64(time.Now().Add(1 * time.Second).Unix()), + }), + clientBlockError: nil, + clientSyncingResult: nil, + clientSyncingError: nil, + expectedStatusCode: http.StatusOK, + expectedBody: map[string]string{ + synced: "OK", + minPeerCount: "OK", + checkBlock: "OK", + maxSecondsBehind: "OK", + }, + }, + } + + for idx, c := range cases { + w := httptest.NewRecorder() + r, err := http.NewRequest(http.MethodGet, "http://localhost:9090/health", nil) + if err != nil { + t.Errorf("%v: creating request: %v", idx, err) + } + + for _, header := range c.headers { + r.Header.Add("X-GETH-HEALTHCHECK", header) + } + + ethClient := ðClientStub{ + peersResult: c.clientPeerResult, + peersError: c.clientPeerError, + blockResult: c.clientBlockResult, + blockError: c.clientBlockError, + syncingResult: c.clientSyncingResult, + syncingError: c.clientSyncingError, + } + + processFromHeaders(ethClient, r.Header.Values(healthHeader), w, r) + + result := w.Result() + if result.StatusCode != c.expectedStatusCode { + t.Errorf("%v: expected status code: %v, but got: %v", idx, c.expectedStatusCode, result.StatusCode) + } + + bodyBytes, err := io.ReadAll(result.Body) + if err != nil { + t.Errorf("%v: reading response body: %s", idx, err) + } + + var body map[string]string + err = json.Unmarshal(bodyBytes, &body) + if err != nil { + t.Errorf("%v: unmarshalling the response body: %s", idx, err) + } + result.Body.Close() + + for k, v := range c.expectedBody { + val, found := body[k] + if !found { + t.Errorf("%v: expected the key: %s to be in the response body but it wasn't there", idx, k) + } + if !strings.Contains(val, v) { + t.Errorf("%v: expected the response body key: %s to contain: %s, but it contained: %s", idx, k, v, val) + } + } + } +} + +func TestProcessHealthcheckIfNeeded_RequestBody(t *testing.T) { + cases := []struct { + body string + clientPeerResult uint64 + clientPeerError error + clientBlockResult *types.Block + clientBlockError error + clientSyncingResult *ethereum.SyncProgress + clientSyncingError error + expectedStatusCode int + expectedBody map[string]string + }{ + // 0 - sync check enabled - syncing + { + body: "{\"synced\": true}", + clientPeerResult: uint64(1), + clientPeerError: nil, + clientBlockResult: nil, + clientBlockError: nil, + clientSyncingResult: nil, + clientSyncingError: nil, + expectedStatusCode: http.StatusOK, + expectedBody: map[string]string{ + query: "OK", + synced: "OK", + minPeerCount: "DISABLED", + checkBlock: "DISABLED", + maxSecondsBehind: "DISABLED", + }, + }, + // 1 - sync check enabled - not syncing + { + body: "{\"synced\": true}", + clientPeerResult: uint64(1), + clientPeerError: nil, + clientBlockResult: nil, + clientBlockError: nil, + clientSyncingResult: ðereum.SyncProgress{}, + clientSyncingError: nil, + expectedStatusCode: http.StatusInternalServerError, + expectedBody: map[string]string{ + query: "OK", + synced: "ERROR: not synced", + minPeerCount: "DISABLED", + checkBlock: "DISABLED", + maxSecondsBehind: "DISABLED", + }, + }, + // 2 - sync check enabled - error checking sync + { + body: "{\"synced\": true}", + clientPeerResult: uint64(1), + clientPeerError: nil, + clientBlockResult: nil, + clientBlockError: nil, + clientSyncingResult: ðereum.SyncProgress{}, + clientSyncingError: errors.New("problem checking sync"), + expectedStatusCode: http.StatusInternalServerError, + expectedBody: map[string]string{ + query: "OK", + synced: "ERROR: problem checking sync", + minPeerCount: "DISABLED", + checkBlock: "DISABLED", + maxSecondsBehind: "DISABLED", + }, + }, + // 3 - peer count enabled - good request + { + body: "{\"min_peer_count\": 1}", + clientPeerResult: uint64(1), + clientPeerError: nil, + clientBlockResult: nil, + clientBlockError: nil, + clientSyncingResult: nil, + clientSyncingError: nil, + expectedStatusCode: http.StatusOK, + expectedBody: map[string]string{ + query: "OK", + synced: "DISABLED", + minPeerCount: "OK", + checkBlock: "DISABLED", + maxSecondsBehind: "DISABLED", + }, + }, + // 4 - peer count enabled - not enough peers + { + body: "{\"min_peer_count\": 10}", + clientPeerResult: uint64(1), + clientPeerError: nil, + clientBlockResult: nil, + clientBlockError: nil, + clientSyncingResult: nil, + clientSyncingError: nil, + expectedStatusCode: http.StatusInternalServerError, + expectedBody: map[string]string{ + query: "OK", + synced: "DISABLED", + minPeerCount: "ERROR: not enough peers: 1 (minimum 10)", + checkBlock: "DISABLED", + maxSecondsBehind: "DISABLED", + }, + }, + // 5 - peer count enabled - error checking peers + { + body: "{\"min_peer_count\": 10}", + clientPeerResult: uint64(1), + clientPeerError: errors.New("problem checking peers"), + clientBlockResult: nil, + clientBlockError: nil, + clientSyncingResult: nil, + clientSyncingError: nil, + expectedStatusCode: http.StatusInternalServerError, + expectedBody: map[string]string{ + query: "OK", + synced: "DISABLED", + minPeerCount: "ERROR: problem checking peers", + checkBlock: "DISABLED", + maxSecondsBehind: "DISABLED", + }, + }, + // 6 - peer count enabled - badly formed request + { + body: "{\"min_peer_count\": \"ABC\"}", + clientPeerResult: uint64(1), + clientPeerError: nil, + clientBlockResult: nil, + clientBlockError: nil, + clientSyncingResult: nil, + clientSyncingError: nil, + expectedStatusCode: http.StatusInternalServerError, + expectedBody: map[string]string{ + query: "ERROR: json: cannot unmarshal string into Go struct field requestBody.min_peer_count of type uint", + synced: "DISABLED", + minPeerCount: "DISABLED", + checkBlock: "DISABLED", + maxSecondsBehind: "DISABLED", + }, + }, + // 7 - block check - all ok + { + body: "{\"check_block\": 10}", + clientPeerResult: uint64(1), + clientPeerError: nil, + clientBlockResult: &types.Block{}, + clientBlockError: nil, + clientSyncingResult: nil, + clientSyncingError: nil, + expectedStatusCode: http.StatusOK, + expectedBody: map[string]string{ + query: "OK", + synced: "DISABLED", + minPeerCount: "DISABLED", + checkBlock: "OK", + maxSecondsBehind: "DISABLED", + }, + }, + // 8 - block check - no block found + { + body: "{\"check_block\": 10}", + clientPeerResult: uint64(1), + clientPeerError: nil, + clientBlockResult: nil, + clientBlockError: errors.New("not found"), + clientSyncingResult: nil, + clientSyncingError: nil, + expectedStatusCode: http.StatusInternalServerError, + expectedBody: map[string]string{ + query: "OK", + synced: "DISABLED", + minPeerCount: "DISABLED", + checkBlock: "ERROR: not found", + maxSecondsBehind: "DISABLED", + }, + }, + // 9 - block check - error checking block + { + body: "{\"check_block\": 10}", + clientPeerResult: uint64(1), + clientPeerError: nil, + clientBlockResult: &types.Block{}, + clientBlockError: errors.New("problem checking block"), + clientSyncingResult: nil, + clientSyncingError: nil, + expectedStatusCode: http.StatusInternalServerError, + expectedBody: map[string]string{ + query: "OK", + synced: "DISABLED", + minPeerCount: "DISABLED", + checkBlock: "ERROR: problem checking block", + maxSecondsBehind: "DISABLED", + }, + }, + // 10 - block check - badly formed request + { + body: "{\"check_block\": \"ABC\"}", + clientPeerResult: uint64(1), + clientPeerError: nil, + clientBlockResult: nil, + clientBlockError: nil, + clientSyncingResult: nil, + clientSyncingError: nil, + expectedStatusCode: http.StatusInternalServerError, + expectedBody: map[string]string{ + query: "ERROR: json: cannot unmarshal string into Go struct field requestBody.check_block of type uint64", + synced: "DISABLED", + minPeerCount: "DISABLED", + checkBlock: "DISABLED", + maxSecondsBehind: "DISABLED", + }, + }, + // 11 - seconds check - all ok + { + body: "{\"max_seconds_behind\": 60}", + clientPeerResult: uint64(1), + clientPeerError: nil, + clientBlockResult: types.NewBlockWithHeader(&types.Header{ + Time: uint64(time.Now().Add(-10 * time.Second).Unix()), + }), + clientBlockError: nil, + clientSyncingResult: nil, + clientSyncingError: nil, + expectedStatusCode: http.StatusOK, + expectedBody: map[string]string{ + query: "OK", + synced: "DISABLED", + minPeerCount: "DISABLED", + checkBlock: "DISABLED", + maxSecondsBehind: "OK", + }, + }, + // 12 - seconds check - too old + { + body: "{\"max_seconds_behind\": 60}", + clientPeerResult: uint64(1), + clientPeerError: nil, + clientBlockResult: types.NewBlockWithHeader(&types.Header{ + Time: uint64(time.Now().Add(-1 * time.Hour).Unix()), + }), + clientBlockError: nil, + clientSyncingResult: nil, + clientSyncingError: nil, + expectedStatusCode: http.StatusInternalServerError, + expectedBody: map[string]string{ + query: "OK", + synced: "DISABLED", + minPeerCount: "DISABLED", + checkBlock: "DISABLED", + maxSecondsBehind: "ERROR: timestamp too old: got ts:", + }, + }, + // 13 - seconds check - less than 0 seconds + { + body: "{\"max_seconds_behind\": -1}", + clientPeerResult: uint64(1), + clientPeerError: nil, + clientBlockResult: types.NewBlockWithHeader(&types.Header{ + Time: uint64(time.Now().Add(1 * time.Hour).Unix()), + }), + clientBlockError: nil, + clientSyncingResult: nil, + clientSyncingError: nil, + expectedStatusCode: http.StatusInternalServerError, + expectedBody: map[string]string{ + query: "OK", + synced: "DISABLED", + minPeerCount: "DISABLED", + checkBlock: "DISABLED", + maxSecondsBehind: "ERROR: invalid value provided", + }, + }, + // 14 - seconds check - badly formed request + { + body: "{\"max_seconds_behind\": \"ABC\"}", + clientPeerResult: uint64(1), + clientPeerError: nil, + clientBlockResult: &types.Block{}, + clientBlockError: nil, + clientSyncingResult: nil, + clientSyncingError: nil, + expectedStatusCode: http.StatusInternalServerError, + expectedBody: map[string]string{ + query: "ERROR: json: cannot unmarshal string into Go struct field requestBody.max_seconds_behind of type int", + synced: "DISABLED", + minPeerCount: "DISABLED", + checkBlock: "DISABLED", + maxSecondsBehind: "DISABLED", + }, + }, + // 15 - all checks - report ok + { + body: "{\"synced\": true, \"min_peer_count\": 1, \"check_block\": 10, \"max_seconds_behind\": 60}", + clientPeerResult: uint64(10), + clientPeerError: nil, + clientBlockResult: types.NewBlockWithHeader(&types.Header{ + Time: uint64(time.Now().Add(1 * time.Second).Unix()), + }), + clientBlockError: nil, + clientSyncingResult: nil, + clientSyncingError: nil, + expectedStatusCode: http.StatusOK, + expectedBody: map[string]string{ + query: "OK", + synced: "OK", + minPeerCount: "OK", + checkBlock: "OK", + maxSecondsBehind: "OK", + }, + }, + } + + for idx, c := range cases { + w := httptest.NewRecorder() + r, err := http.NewRequest(http.MethodGet, "http://localhost:9090/health", nil) + if err != nil { + t.Errorf("%v: creating request: %v", idx, err) + } + + r.Body = io.NopCloser(strings.NewReader(c.body)) + + ethClient := ðClientStub{ + peersResult: c.clientPeerResult, + peersError: c.clientPeerError, + blockResult: c.clientBlockResult, + blockError: c.clientBlockError, + syncingResult: c.clientSyncingResult, + syncingError: c.clientSyncingError, + } + + processFromBody(ethClient, w, r) + + result := w.Result() + if result.StatusCode != c.expectedStatusCode { + t.Errorf("%v: expected status code: %v, but got: %v", idx, c.expectedStatusCode, result.StatusCode) + } + + bodyBytes, err := io.ReadAll(result.Body) + if err != nil { + t.Errorf("%v: reading response body: %s", idx, err) + } + + var body map[string]string + err = json.Unmarshal(bodyBytes, &body) + if err != nil { + t.Errorf("%v: unmarshalling the response body: %s", idx, err) + } + result.Body.Close() + + for k, v := range c.expectedBody { + val, found := body[k] + if !found { + t.Errorf("%v: expected the key: %s to be in the response body but it wasn't there", idx, k) + } + if !strings.Contains(val, v) { + t.Errorf("%v: expected the response body key: %s to contain: %s, but it contained: %s", idx, k, v, val) + } + } + } +} diff --git a/health/interface.go b/health/interface.go new file mode 100644 index 0000000000..7ddbecd047 --- /dev/null +++ b/health/interface.go @@ -0,0 +1,15 @@ +package health + +import ( + "context" + "math/big" + + "github.com/ethereum/go-ethereum" + "github.com/ethereum/go-ethereum/core/types" +) + +type ethClient interface { + PeerCount(context.Context) (uint64, error) + BlockByNumber(context.Context, *big.Int) (*types.Block, error) + SyncProgress(context.Context) (*ethereum.SyncProgress, error) +} diff --git a/health/service.go b/health/service.go index f1aba69042..6279dfd2a8 100644 --- a/health/service.go +++ b/health/service.go @@ -14,9 +14,9 @@ type handler struct { func (h handler) ServeHTTP(w http.ResponseWriter, r *http.Request) { headers := r.Header.Values(healthHeader) if len(headers) != 0 { - h.processFromHeaders(headers, w, r) + processFromHeaders(h.ec, headers, w, r) } else { - h.processFromBody(w, r) + processFromBody(h.ec, w, r) } }