From 94b4a4b9ec5929c318273d28aa5cc44cb3ff4baa Mon Sep 17 00:00:00 2001 From: Damian-4chain Date: Wed, 23 Oct 2024 18:26:05 +0200 Subject: [PATCH] feat(SPV-1106): add headers errors --- bhserrors/definitions.go | 38 +++++++-- bhserrors/errors.go | 4 +- bhserrors/http_response.go | 2 +- database/sql/headers.go | 23 ++---- .../testrepository/header_testrepository.go | 6 +- service/chain_service.go | 2 +- service/header_service.go | 9 ++- .../http/endpoints/api/headers/endpoints.go | 19 +++-- .../api/headers/header_endpoints_test.go | 81 +++++++++---------- 9 files changed, 98 insertions(+), 86 deletions(-) diff --git a/bhserrors/definitions.go b/bhserrors/definitions.go index bfbd6faa..c0d35dec 100644 --- a/bhserrors/definitions.go +++ b/bhserrors/definitions.go @@ -1,9 +1,12 @@ package bhserrors -// ////////////////////////////////// AUTH ERRORS +// ////////////////////////////////// GENERIC ERRORS // ErrGeneric is a generic error that something went wrong -var ErrGeneric = BHSError{Message: "Something went wrong. Internal server error", StatusCode: 500, Code: "ErrGeneric"} +var ErrGeneric = BHSError{Message: "Internal server error", StatusCode: 500, Code: "ErrGeneric"} + +// ErrBindBody is an error when it fails to bind JSON body +var ErrBindBody = BHSError{Message: "Error during bind JSON body", StatusCode: 400, Code: "ErrBindBody"} // ////////////////////////////////// AUTH ERRORS @@ -19,6 +22,9 @@ var ErrInvalidAccessToken = BHSError{Message: "Invalid access token", StatusCode // ErrUnauthorized is a generic error when user is unauthorized to make a request var ErrUnauthorized = BHSError{Message: "Not authorized", StatusCode: 401, Code: "ErrUnauthorized"} +// ErrAdminTokenNotFound is when admin token was not found in Block Header Service +var ErrAdminTokenNotFound = BHSError{Message: "Admin token not found", StatusCode: 401, Code: "ErrAdminTokenNotFound"} + // ////////////////////////////////// MERKLE ROOTS ERRORS // ErrMerklerootNotFound is when provided merkleroot from user was not found in Block Header Service's database @@ -30,10 +36,7 @@ var ErrMerklerootNotInLongestChain = BHSError{Message: "Provided merkleroot is n // ErrInvalidBatchSize is when user provided incorrect batchSize var ErrInvalidBatchSize = BHSError{Message: "batchSize must be 0 or a positive integer", Code: "ErrInvalidBatchSize", StatusCode: 400} -// ////////////////////////////////// TOKEN ERRORS - -// ErrAdminTokenNotFound is when admin token was not found in Block Header Service -var ErrAdminTokenNotFound = BHSError{Message: "Admin token not found", StatusCode: 401, Code: "ErrAdminTokenNotFound"} +// ////////////////////////////////// ACCESS ERRORS // ErrTokenNotFound is when token was not found in Block Header Service var ErrTokenNotFound = BHSError{Message: "Token not found", StatusCode: 404, Code: "ErrTokenNotFound"} @@ -43,3 +46,26 @@ var ErrCreateToken = BHSError{Message: "Failed to create new token", StatusCode: // ErrDeleteToken is when delete token fails var ErrDeleteToken = BHSError{Message: "Failed to delete token", StatusCode: 400, Code: "ErrDeleteToken"} + +// ////////////////////////////////// HEADERS ERRORS + +// ErrAncestorHashHigher is when ancestor hash height is heigher than requested header +var ErrAncestorHashHigher = BHSError{Message: "Ancestor header height can not be higher than requested header height", StatusCode: 400, Code: "ErrAncestorHashHigher"} + +// ErrAncestorNotFound is when ancestor for a given hash was not found +var ErrAncestorNotFound = BHSError{Message: "Failed to get ancestor with given hash ", StatusCode: 400, Code: "ErrAncestorNotFound"} + +// ErrHeadersNotPartOfTheSameChain is when provided headers are not part of the same chain +var ErrHeadersNotPartOfTheSameChain = BHSError{Message: "the headers provided are not part of the same chain", StatusCode: 400, Code: "ErrHeadersNotPartOfTheSameChain"} + +// ErrHeaderWithGivenHashes is when getting header with given hashes fails +var ErrHeaderWithGivenHashes = BHSError{Message: "Error during getting headers with given hashes", StatusCode: 400, Code: "ErrHeaderWithGivenHashes"} + +// ErrHeaderNotFound is when hash could not be found +var ErrHeaderNotFound = BHSError{Message: "Header not found", StatusCode: 404, Code: "ErrHeaderNotFound"} + +// ErrHeaderNotFound is when hash could not be found for given range +var ErrHeadersForGivenRangeNotFound = BHSError{Message: "Could not find headers in given range", StatusCode: 404, Code: "ErrHeadersForGivenRangeNotFound"} + +// ErrHeaderStopHeightNotFound is when stop height for given heade was not found +var ErrHeaderStopHeightNotFound = BHSError{Message: "Could not find stop height for given header", StatusCode: 404, Code: "ErrHeaderStopHeightNotFound"} diff --git a/bhserrors/errors.go b/bhserrors/errors.go index 2838f074..bce4da45 100644 --- a/bhserrors/errors.go +++ b/bhserrors/errors.go @@ -25,8 +25,8 @@ type BHSError struct { cause error } -// responseError is an error which will be returned in HTTP response -type responseError struct { +// ResponseError is an error which will be returned in HTTP response +type ResponseError struct { Code string `json:"code"` Message string `json:"message"` } diff --git a/bhserrors/http_response.go b/bhserrors/http_response.go index 7382428f..fc073f46 100644 --- a/bhserrors/http_response.go +++ b/bhserrors/http_response.go @@ -21,7 +21,7 @@ func AbortWithErrorResponse(c *gin.Context, err error, log *zerolog.Logger) { c.AbortWithStatusJSON(statusCode, response) } -func mapAndLog(err error, log *zerolog.Logger) (model responseError, statusCode int) { +func mapAndLog(err error, log *zerolog.Logger) (model ResponseError, statusCode int) { model.Code = UnknownErrorCode model.Message = "Internal server error" statusCode = 500 diff --git a/database/sql/headers.go b/database/sql/headers.go index 18833e7e..b6436dd5 100644 --- a/database/sql/headers.go +++ b/database/sql/headers.go @@ -279,10 +279,7 @@ func (h *HeadersDb) Count(ctx context.Context) (int, error) { func (h *HeadersDb) GetHeaderByHash(ctx context.Context, hash string) (*dto.DbBlockHeader, error) { var bh dto.DbBlockHeader if err := h.db.GetContext(ctx, &bh, h.db.Rebind(sqlHeader), hash); err != nil { - if errors.Is(err, sql.ErrNoRows) { - return nil, errors.New("could not find hash") - } - return nil, errors.Wrapf(err, "failed to get blockhash using hash %s", hash) + return nil, bhserrors.ErrHeaderNotFound.Wrap(err) } return &bh, nil } @@ -303,10 +300,7 @@ func (h *HeadersDb) GetHeaderByHeight(ctx context.Context, height int32, state s func (h *HeadersDb) GetHeaderByHeightRange(from int, to int) ([]*dto.DbBlockHeader, error) { var bh []*dto.DbBlockHeader if err := h.db.Select(&bh, h.db.Rebind(sqlHeaderByHeightRange), from, to); err != nil { - if errors.Is(err, sql.ErrNoRows) { - return nil, errors.New("could not find headers in given range") - } - return nil, errors.Wrapf(err, "failed to get headers using given range from: %d to: %d", from, to) + return nil, bhserrors.ErrHeadersForGivenRangeNotFound.Wrap(err) } return bh, nil } @@ -345,10 +339,7 @@ func (h *HeadersDb) GenesisExists(_ context.Context) bool { func (h *HeadersDb) GetPreviousHeader(ctx context.Context, hash string) (*dto.DbBlockHeader, error) { var bh dto.DbBlockHeader if err := h.db.GetContext(ctx, &bh, h.db.Rebind(sqlSelectPreviousBlock), hash); err != nil { - if errors.Is(err, sql.ErrNoRows) { - return nil, errors.New("could not find header") - } - return nil, errors.Wrapf(err, "failed to get prev header using hash %s", hash) + return nil, bhserrors.ErrHeaderNotFound.Wrap(err) } return &bh, nil } @@ -371,10 +362,10 @@ func (h *HeadersDb) GetTip(_ context.Context) (*dto.DbBlockHeader, error) { func (h *HeadersDb) GetAncestorOnHeight(hash string, height int32) (*dto.DbBlockHeader, error) { var bh []*dto.DbBlockHeader if err := h.db.Select(&bh, h.db.Rebind(sqlSelectAncestorOnHeight), hash, int(height), int(height)); err != nil { - return nil, errors.Wrapf(err, "failed to get ancestors using given hash: %s ", hash) + return nil, bhserrors.ErrAncestorNotFound.Wrap(err) } if len(bh) == 0 { - return nil, errors.New("could not find ancestors for a providen hash") + return nil, bhserrors.ErrAncestorNotFound } return bh[0], nil } @@ -392,10 +383,10 @@ func (h *HeadersDb) GetAllTips() ([]*dto.DbBlockHeader, error) { func (h *HeadersDb) GetChainBetweenTwoHashes(low string, high string) ([]*dto.DbBlockHeader, error) { var bh []*dto.DbBlockHeader if err := h.db.Select(&bh, h.db.Rebind(sqlChainBetweenTwoHashes), high, low, low); err != nil { - return nil, errors.Wrapf(err, "failed to get headers using given range from: %s to: %s", low, high) + return nil, bhserrors.ErrHeadersForGivenRangeNotFound.Wrap(err) } if len(bh) == 0 { - return nil, errors.New("could not find headers in given range") + return nil, bhserrors.ErrHeadersForGivenRangeNotFound } return bh, nil } diff --git a/internal/tests/testrepository/header_testrepository.go b/internal/tests/testrepository/header_testrepository.go index 98f5414c..713f4c61 100644 --- a/internal/tests/testrepository/header_testrepository.go +++ b/internal/tests/testrepository/header_testrepository.go @@ -70,7 +70,7 @@ func (r *HeaderTestRepository) GetHeaderByHeightRange(from int, to int) ([]*doma return filteredHeaders, nil } - return nil, errors.New("could not find headers in given range") + return nil, bhserrors.ErrHeadersForGivenRangeNotFound } // GetLongestChainHeadersFromHeight returns from db the headers from "longest chain" starting from given height. @@ -133,7 +133,7 @@ func (r *HeaderTestRepository) GetHeaderByHash(hash string) (*domains.BlockHeade if header != nil { return header, nil } - return nil, errors.New("could not find hash") + return nil, bhserrors.ErrHeaderNotFound } // GenesisExists check if genesis header is in db. @@ -363,7 +363,7 @@ func (r *HeaderTestRepository) GetHeadersStopHeight(hashStop string) (int, error return int(header.Height), nil } } - return 0, errors.New("could not find stop height") + return 0, bhserrors.ErrHeaderStopHeightNotFound } // FillWithLongestChain fills the test header repository diff --git a/service/chain_service.go b/service/chain_service.go index 5fbcfe15..513ea67a 100644 --- a/service/chain_service.go +++ b/service/chain_service.go @@ -186,7 +186,7 @@ func (cs *chainService) createHeader(hash *domains.BlockHash, bs *domains.BlockH func (cs *chainService) previousHeader(bs *domains.BlockHeaderSource) (*domains.BlockHeader, error) { h, err := cs.Repositories.Headers.GetHeaderByHash(bs.PrevBlock.String()) - if h == nil && err != nil && err.Error() == "could not find hash" { + if h == nil && err != nil && err.Error() == "Header not found" { return domains.NewOrphanPreviousBlockHeader(), nil } return h, err diff --git a/service/header_service.go b/service/header_service.go index 2ff7d939..6ad8d928 100644 --- a/service/header_service.go +++ b/service/header_service.go @@ -6,6 +6,7 @@ import ( "math" "time" + "github.com/bitcoin-sv/block-headers-service/bhserrors" "github.com/bitcoin-sv/block-headers-service/config" "github.com/bitcoin-sv/block-headers-service/domains" "github.com/bitcoin-sv/block-headers-service/internal/chaincfg" @@ -128,20 +129,20 @@ func (hs *HeaderService) GetHeaderAncestorsByHash(hash string, ancestorHash stri // Check possible errors if err != nil || err2 != nil { - return nil, errors.New("error during getting headers with given hashes") + return nil, bhserrors.ErrHeaderWithGivenHashes } else if ancestorHeader.Height > reqHeader.Height { - return nil, errors.New("ancestor header height can not be higher than requested header heght") + return nil, bhserrors.ErrAncestorHashHigher } else if ancestorHeader.Height == reqHeader.Height { return make([]*domains.BlockHeader, 0), nil } a, err := hs.repo.Headers.GetAncestorOnHeight(reqHeader.Hash.String(), ancestorHeader.Height) if err != nil { - return nil, errors.New("the headers provided are not part of the same chain") + return nil, bhserrors.ErrHeadersNotPartOfTheSameChain.Wrap(err) } if a.Hash != ancestorHeader.Hash { - return nil, errors.New("the headers provided are not part of the same chain") + return nil, bhserrors.ErrHeadersNotPartOfTheSameChain } // Get headers from db diff --git a/transports/http/endpoints/api/headers/endpoints.go b/transports/http/endpoints/api/headers/endpoints.go index 55cbad47..a45b162e 100644 --- a/transports/http/endpoints/api/headers/endpoints.go +++ b/transports/http/endpoints/api/headers/endpoints.go @@ -4,19 +4,22 @@ import ( "net/http" "strconv" + "github.com/bitcoin-sv/block-headers-service/bhserrors" "github.com/bitcoin-sv/block-headers-service/config" "github.com/bitcoin-sv/block-headers-service/service" router "github.com/bitcoin-sv/block-headers-service/transports/http/endpoints/routes" "github.com/gin-gonic/gin" + "github.com/rs/zerolog" ) type handler struct { service service.Headers + log *zerolog.Logger } // NewHandler creates new endpoint handler. func NewHandler(s *service.Services) router.APIEndpoints { - return &handler{service: s.Headers} + return &handler{service: s.Headers, log: s.Logger} } // RegisterAPIEndpoints registers routes that are part of service API. @@ -48,7 +51,7 @@ func (h *handler) getHeaderByHash(c *gin.Context) { if err == nil { c.JSON(http.StatusOK, newBlockHeaderResponse(bh)) } else { - c.JSON(http.StatusBadRequest, err.Error()) + bhserrors.ErrorResponse(c, err, h.log) } } @@ -77,10 +80,10 @@ func (h *handler) getHeaderByHeight(c *gin.Context) { if err == nil { c.JSON(http.StatusOK, mapToBlockHeadersResponses(bh)) } else { - c.JSON(http.StatusBadRequest, err.Error()) + bhserrors.ErrorResponse(c, err, h.log) } } else { - c.JSON(http.StatusBadRequest, err.Error()) + bhserrors.ErrorResponse(c, err, h.log) } } @@ -103,7 +106,7 @@ func (h *handler) getHeaderAncestorsByHash(c *gin.Context) { if err == nil { c.JSON(http.StatusOK, mapToBlockHeadersResponses(ancestors)) } else { - c.JSON(http.StatusBadRequest, err.Error()) + bhserrors.ErrorResponse(c, err, h.log) } } @@ -120,14 +123,14 @@ func (h *handler) getHeaderAncestorsByHash(c *gin.Context) { func (h *handler) getCommonAncestor(c *gin.Context) { var body []string if err := c.BindJSON(&body); err != nil { - c.JSON(http.StatusBadRequest, err.Error()) + bhserrors.ErrorResponse(c, bhserrors.ErrBindBody.Wrap(err), h.log) } else { ancestor, err := h.service.GetCommonAncestor(body) if err == nil { c.JSON(http.StatusOK, newBlockHeaderResponse(ancestor)) } else { - c.JSON(http.StatusBadRequest, err.Error()) + bhserrors.ErrorResponse(c, err, h.log) } } } @@ -150,6 +153,6 @@ func (h *handler) getHeadersState(c *gin.Context) { headerStateResponse := newBlockHeaderStateResponse(bh) c.JSON(http.StatusOK, headerStateResponse) } else { - c.JSON(http.StatusBadRequest, err.Error()) + bhserrors.ErrorResponse(c, err, h.log) } } diff --git a/transports/http/endpoints/api/headers/header_endpoints_test.go b/transports/http/endpoints/api/headers/header_endpoints_test.go index c683a4ff..5b38bf27 100644 --- a/transports/http/endpoints/api/headers/header_endpoints_test.go +++ b/transports/http/endpoints/api/headers/header_endpoints_test.go @@ -5,7 +5,6 @@ import ( "context" "encoding/json" "fmt" - "io" "net/http" "strconv" "testing" @@ -17,6 +16,7 @@ import ( "github.com/bitcoin-sv/block-headers-service/internal/tests/fixtures" "github.com/bitcoin-sv/block-headers-service/internal/tests/testapp" "github.com/bitcoin-sv/block-headers-service/transports/http/endpoints/api/headers" + "github.com/stretchr/testify/require" ) var expectedObj = headers.BlockHeaderResponse{ @@ -37,10 +37,10 @@ func TestGetHeaderByHash(t *testing.T) { defer cleanup() expectedResult := struct { code int - body []byte + body string }{ code: http.StatusUnauthorized, - body: []byte("\"empty auth header\""), + body: "{\"message\": \"Empty auth header\", \"code\": \"ErrMissingAuthHeader\"}", } // when @@ -48,8 +48,8 @@ func TestGetHeaderByHash(t *testing.T) { // then assert.Equal(t, res.Code, expectedResult.code) - body, _ := io.ReadAll(res.Body) - assert.EqualBytes(t, body, expectedResult.body) + // exp, _ := json.MarshalIndent(.body, "", " ") + require.JSONEq(t, expectedResult.body, res.Body.String()) }) t.Run("success", func(t *testing.T) { @@ -82,10 +82,10 @@ func TestGetHeaderByHash(t *testing.T) { defer cleanup() expectedResult := struct { code int - body []byte + body string }{ - code: http.StatusBadRequest, - body: []byte("\"could not find hash\""), + code: http.StatusNotFound, + body: "{\"code\":\"ErrHeaderNotFound\",\"message\":\"Header not found\"}", } // when @@ -93,8 +93,7 @@ func TestGetHeaderByHash(t *testing.T) { // then assert.Equal(t, res.Code, expectedResult.code) - body, _ := io.ReadAll(res.Body) - assert.EqualBytes(t, body, expectedResult.body) + require.JSONEq(t, expectedResult.body, res.Body.String()) }) } @@ -105,10 +104,10 @@ func TestGetHeaderByHeight(t *testing.T) { defer cleanup() expectedResult := struct { code int - body []byte + body string }{ code: http.StatusUnauthorized, - body: []byte("\"empty auth header\""), + body: "{\"code\":\"ErrMissingAuthHeader\",\"message\":\"Empty auth header\"}", } // when @@ -116,8 +115,7 @@ func TestGetHeaderByHeight(t *testing.T) { // then assert.Equal(t, res.Code, expectedResult.code) - body, _ := io.ReadAll(res.Body) - assert.EqualBytes(t, body, expectedResult.body) + require.JSONEq(t, expectedResult.body, res.Body.String()) }) t.Run("success", func(t *testing.T) { @@ -150,10 +148,10 @@ func TestGetHeaderByHeight(t *testing.T) { defer cleanup() expectedResult := struct { code int - body []byte + body string }{ - code: http.StatusBadRequest, - body: []byte("\"could not find headers in given range\""), + code: http.StatusNotFound, + body: "{\"code\":\"ErrHeadersForGivenRangeNotFound\",\"message\":\"Could not find headers in given range\"}", } // when @@ -161,8 +159,7 @@ func TestGetHeaderByHeight(t *testing.T) { // then assert.Equal(t, res.Code, expectedResult.code) - body, _ := io.ReadAll(res.Body) - assert.EqualBytes(t, body, expectedResult.body) + require.JSONEq(t, expectedResult.body, res.Body.String()) }) } @@ -173,10 +170,10 @@ func TestGetHeaderAncestorsByHash(t *testing.T) { defer cleanup() expectedResult := struct { code int - body []byte + body string }{ code: http.StatusUnauthorized, - body: []byte("\"empty auth header\""), + body: "{\"message\": \"Empty auth header\", \"code\": \"ErrMissingAuthHeader\"}", } // when @@ -184,8 +181,7 @@ func TestGetHeaderAncestorsByHash(t *testing.T) { // then assert.Equal(t, res.Code, expectedResult.code) - body, _ := io.ReadAll(res.Body) - assert.EqualBytes(t, body, expectedResult.body) + require.JSONEq(t, expectedResult.body, res.Body.String()) }) t.Run("success", func(t *testing.T) { @@ -218,10 +214,10 @@ func TestGetHeaderAncestorsByHash(t *testing.T) { defer cleanup() expectedResult := struct { code int - body []byte + body string }{ code: http.StatusBadRequest, - body: []byte("\"error during getting headers with given hashes\""), + body: "{\"code\":\"ErrHeaderWithGivenHashes\",\"message\":\"Error during getting headers with given hashes\"}", } // when @@ -229,8 +225,7 @@ func TestGetHeaderAncestorsByHash(t *testing.T) { // then assert.Equal(t, res.Code, expectedResult.code) - body, _ := io.ReadAll(res.Body) - assert.EqualBytes(t, body, expectedResult.body) + require.JSONEq(t, expectedResult.body, res.Body.String()) }) } @@ -241,10 +236,10 @@ func TestGetCommonAncestor(t *testing.T) { defer cleanup() expectedResult := struct { code int - body []byte + body string }{ code: http.StatusUnauthorized, - body: []byte("\"empty auth header\""), + body: "{\"message\": \"Empty auth header\", \"code\": \"ErrMissingAuthHeader\"}", } // when @@ -252,8 +247,7 @@ func TestGetCommonAncestor(t *testing.T) { // then assert.Equal(t, res.Code, expectedResult.code) - body, _ := io.ReadAll(res.Body) - assert.EqualBytes(t, body, expectedResult.body) + require.JSONEq(t, expectedResult.body, res.Body.String()) }) t.Run("success", func(t *testing.T) { @@ -297,10 +291,10 @@ func TestGetCommonAncestor(t *testing.T) { defer cleanup() expectedResult := struct { code int - body []byte + body string }{ - code: http.StatusBadRequest, - body: []byte("\"could not find hash\""), + code: http.StatusNotFound, + body: "{\"code\":\"ErrHeaderNotFound\",\"message\":\"Header not found\"}", } // when @@ -308,8 +302,7 @@ func TestGetCommonAncestor(t *testing.T) { // then assert.Equal(t, res.Code, expectedResult.code) - body, _ := io.ReadAll(res.Body) - assert.EqualBytes(t, body, expectedResult.body) + require.JSONEq(t, expectedResult.body, res.Body.String()) }) } @@ -320,10 +313,10 @@ func TestGetHeadersState(t *testing.T) { defer cleanup() expectedResult := struct { code int - body []byte + body string }{ code: http.StatusUnauthorized, - body: []byte("\"empty auth header\""), + body: "{\"message\": \"Empty auth header\", \"code\": \"ErrMissingAuthHeader\"}", } // when @@ -331,8 +324,7 @@ func TestGetHeadersState(t *testing.T) { // then assert.Equal(t, res.Code, expectedResult.code) - body, _ := io.ReadAll(res.Body) - assert.EqualBytes(t, body, expectedResult.body) + require.JSONEq(t, expectedResult.body, res.Body.String()) }) t.Run("success", func(t *testing.T) { @@ -371,10 +363,10 @@ func TestGetHeadersState(t *testing.T) { defer cleanup() expectedResult := struct { code int - body []byte + body string }{ - code: http.StatusBadRequest, - body: []byte("\"could not find hash\""), + code: http.StatusNotFound, + body: "{\"code\":\"ErrHeaderNotFound\",\"message\":\"Header not found\"}", } // when @@ -382,8 +374,7 @@ func TestGetHeadersState(t *testing.T) { // then assert.Equal(t, res.Code, expectedResult.code) - body, _ := io.ReadAll(res.Body) - assert.EqualBytes(t, body, expectedResult.body) + require.JSONEq(t, expectedResult.body, res.Body.String()) }) }