Skip to content

Commit

Permalink
api: paginated endpoints now return ErrAccountNotFound, ErrElectionNo…
Browse files Browse the repository at this point in the history
…tFound, ErrOrgNotFound

 * add tests to avoid regressions
  • Loading branch information
altergui committed Aug 7, 2024
1 parent 03b3863 commit ca3f738
Show file tree
Hide file tree
Showing 7 changed files with 204 additions and 0 deletions.
4 changes: 4 additions & 0 deletions api/accounts.go
Original file line number Diff line number Diff line change
Expand Up @@ -571,6 +571,10 @@ func (a *API) accountListHandler(_ *apirest.APIdata, ctx *httprouter.HTTPContext
//
// Errors returned are always of type APIerror.
func (a *API) sendAccountList(ctx *httprouter.HTTPContext, params *AccountParams) error {
if params.AccountID != "" && !a.indexer.AccountExists(params.AccountID) {
return ErrAccountNotFound
}

accounts, total, err := a.indexer.AccountList(
params.Limit,
params.Page*params.Limit,
Expand Down
14 changes: 14 additions & 0 deletions api/chain.go
Original file line number Diff line number Diff line change
Expand Up @@ -332,6 +332,10 @@ func (a *API) organizationListByFilterAndPageHandler(msg *apirest.APIdata, ctx *
//
// Errors returned are always of type APIerror.
func (a *API) sendOrganizationList(ctx *httprouter.HTTPContext, params *OrganizationParams) error {
if params.OrganizationID != "" && !a.indexer.EntityExists(params.OrganizationID) {
return ErrOrgNotFound
}

orgs, total, err := a.indexer.EntityList(
params.Limit,
params.Page*params.Limit,
Expand Down Expand Up @@ -1061,6 +1065,10 @@ func (a *API) chainFeesListByTypeAndPageHandler(_ *apirest.APIdata, ctx *httprou
//
// Errors returned are always of type APIerror.
func (a *API) sendFeesList(ctx *httprouter.HTTPContext, params *FeesParams) error {
if params.AccountID != "" && !a.indexer.AccountExists(params.AccountID) {
return ErrAccountNotFound
}

fees, total, err := a.indexer.TokenFeesList(
params.Limit,
params.Page*params.Limit,
Expand Down Expand Up @@ -1118,6 +1126,12 @@ func (a *API) chainTransfersListHandler(_ *apirest.APIdata, ctx *httprouter.HTTP
//
// Errors returned are always of type APIerror.
func (a *API) sendTransfersList(ctx *httprouter.HTTPContext, params *TransfersParams) error {
for _, param := range []string{params.AccountID, params.AccountIDFrom, params.AccountIDTo} {
if param != "" && !a.indexer.AccountExists(param) {
return ErrAccountNotFound.With(param)
}
}

transfers, total, err := a.indexer.TokenTransfersList(
params.Limit,
params.Page*params.Limit,
Expand Down
8 changes: 8 additions & 0 deletions api/elections.go
Original file line number Diff line number Diff line change
Expand Up @@ -262,6 +262,14 @@ func (a *API) electionListHandler(_ *apirest.APIdata, ctx *httprouter.HTTPContex
//
// Errors returned are always of type APIerror.
func (a *API) sendElectionList(ctx *httprouter.HTTPContext, params *ElectionParams) error {
if params.ElectionID != "" && !a.indexer.ProcessExists(params.ElectionID) {
return ErrElectionNotFound
}

if params.OrganizationID != "" && !a.indexer.EntityExists(params.OrganizationID) {
return ErrOrgNotFound
}

status, err := parseStatus(params.Status)
if err != nil {
return err
Expand Down
4 changes: 4 additions & 0 deletions api/vote.go
Original file line number Diff line number Diff line change
Expand Up @@ -226,6 +226,10 @@ func (a *API) votesListHandler(_ *apirest.APIdata, ctx *httprouter.HTTPContext)
//
// Errors returned are always of type APIerror.
func (a *API) sendVotesList(ctx *httprouter.HTTPContext, params *VoteParams) error {
if params.ElectionID != "" && !a.indexer.ProcessExists(params.ElectionID) {
return ErrElectionNotFound
}

votes, total, err := a.indexer.VoteList(
params.Limit,
params.Page*params.Limit,
Expand Down
144 changes: 144 additions & 0 deletions test/apierror_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,18 @@ func TestAPIerror(t *testing.T) {
args: args{"GET", nil, []string{"accounts", "0123456789012345678901234567890123456789", "elections", "count"}},
want: api.ErrOrgNotFound,
},
{
args: args{"GET", nil, []string{"accounts", "0123456789012345678901234567890123456789", "elections", "page", "0"}},
want: api.ErrOrgNotFound,
},
{
args: args{"GET", nil, []string{"accounts", "0123456789012345678901234567890123456789", "transfers", "page", "0"}},
want: api.ErrAccountNotFound,
},
{
args: args{"GET", nil, []string{"accounts", "0123456789012345678901234567890123456789", "fees", "page", "0"}},
want: api.ErrAccountNotFound,
},
{
args: args{"GET", nil, []string{"chain", "blocks", "1234"}},
want: api.ErrBlockNotFound,
Expand Down Expand Up @@ -116,6 +128,10 @@ func TestAPIerror(t *testing.T) {
args: args{"GET", nil, []string{"elections", "page", "-1"}},
want: api.ErrPageNotFound,
},
{
args: args{"GET", nil, []string{"elections", "0123456789012345678901234567890123456789", "votes", "page", "0"}},
want: api.ErrElectionNotFound,
},
}
for _, tt := range tests {
t.Run(tt.want.Error(), func(t *testing.T) {
Expand All @@ -128,3 +144,131 @@ func TestAPIerror(t *testing.T) {
})
}
}

func TestAPIerrorWithQuery(t *testing.T) {
server := testcommon.APIserver{}
server.Start(t,
api.ChainHandler,
api.CensusHandler,
api.VoteHandler,
api.AccountHandler,
api.ElectionHandler,
api.WalletHandler,
)
// Block 1
server.VochainAPP.AdvanceTestBlock()

token1 := uuid.New()
c := testutil.NewTestHTTPclient(t, server.ListenAddr, &token1)

type args struct {
method string
jsonBody any
urlPath []string
query string
}
tests := []struct {
name string
args args
want apirest.APIerror
}{
{
args: args{"GET", nil, []string{"accounts"}, "page=1234"},
want: api.ErrPageNotFound,
},
{
args: args{"GET", nil, []string{"accounts"}, "accountId=0123456789"},
want: api.ErrAccountNotFound,
},
{
args: args{"GET", nil, []string{"accounts"}, "accountId=0123456789&page=1234"},
want: api.ErrAccountNotFound,
},
{
args: args{"GET", nil, []string{"elections"}, "electionId=0123456789"},
want: api.ErrElectionNotFound,
},
{
args: args{"GET", nil, []string{"elections"}, "electionId=0123456789&page=1234"},
want: api.ErrElectionNotFound,
},
{
args: args{"GET", nil, []string{"elections"}, "organizationId=0123456789"},
want: api.ErrOrgNotFound,
},
{
args: args{"GET", nil, []string{"elections"}, "organizationId=0123456789&page=1234"},
want: api.ErrOrgNotFound,
},
{
args: args{"GET", nil, []string{"elections"}, "status=FOOBAR"},
want: api.ErrParamStatusInvalid,
},
{
args: args{"GET", nil, []string{"elections"}, "manuallyEnded=FOOBAR"},
want: api.ErrCantParseBoolean,
},
{
args: args{"GET", nil, []string{"chain", "transactions"}, "page=1234"},
want: api.ErrPageNotFound,
},
// TODO: not yet implemented
// {
// args: args{"GET", nil, []string{"chain", "transactions"}, "height=1234"},
// want: api.ErrBlockNotFound,
// },
{
args: args{"GET", nil, []string{"chain", "transactions"}, "height=FOOBAR"},
want: api.ErrCantParseNumber,
},
// TODO: should this endpoint check `type` is a sane value?
// {
// args: args{"GET", nil, []string{"chain", "transactions"}, "type=FOOBAR"},
// want: api.ErrParamTypeInvalid,
// },
{
args: args{"GET", nil, []string{"chain", "organizations"}, "organizationId=0123456789"},
want: api.ErrOrgNotFound,
},
{
args: args{"GET", nil, []string{"chain", "fees"}, "accountId=0123456789"},
want: api.ErrAccountNotFound,
},
// TODO: should this endpoint check `reference` matches something?
// {
// args: args{"GET", nil, []string{"chain", "fees"}, "reference=0123456789"},
// want: api.ErrTransactionNotFound,
// },
// TODO: should this endpoint check `type` is a sane value?
// {
// args: args{"GET", nil, []string{"chain", "fees"}, "type=FOOBAR"},
// want: api.ErrParamTypeInvalid,
// },
{
args: args{"GET", nil, []string{"votes"}, "electionId=0123456789"},
want: api.ErrElectionNotFound,
},
{
args: args{"GET", nil, []string{"chain", "transfers"}, "accountId=0123456789"},
want: api.ErrAccountNotFound,
},
{
args: args{"GET", nil, []string{"chain", "transfers"}, "accountIdFrom=0123456789"},
want: api.ErrAccountNotFound,
},
{
args: args{"GET", nil, []string{"chain", "transfers"}, "accountIdTo=0123456789"},
want: api.ErrAccountNotFound,
},
}
for _, tt := range tests {
t.Run(tt.want.Error(), func(t *testing.T) {
resp, code := c.RequestWithQuery(tt.args.method, tt.args.jsonBody, tt.args.query, tt.args.urlPath...)
t.Logf("httpstatus=%d body=%s", code, resp)
qt.Assert(t, code, qt.Equals, tt.want.HTTPstatus)
apierr := &apirest.APIerror{}
qt.Assert(t, json.Unmarshal(resp, apierr), qt.IsNil)
qt.Assert(t, apierr.Code, qt.Equals, tt.want.Code)
})
}
}
10 changes: 10 additions & 0 deletions vochain/indexer/indexer.go
Original file line number Diff line number Diff line change
Expand Up @@ -844,3 +844,13 @@ func (idx *Indexer) AccountList(limit, offset int, accountID string) ([]*indexer
}
return list, uint64(results[0].TotalCount), nil
}

// AccountExists returns whether the passed accountID matches at least one row in the db.
// accountID is a partial or full hex string.
func (idx *Indexer) AccountExists(accountID string) bool {
_, count, err := idx.AccountList(1, 0, accountID)
if err != nil {
log.Errorw(err, "indexer query failed")
}
return count > 0
}
20 changes: 20 additions & 0 deletions vochain/indexer/process.go
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,16 @@ func (idx *Indexer) ProcessList(limit, offset int, entityID string, processID st
return list, uint64(results[0].TotalCount), nil
}

// ProcessExists returns whether the passed processID matches at least one row in the db.
// processID is a partial or full hex string.
func (idx *Indexer) ProcessExists(processID string) bool {
_, count, err := idx.ProcessList(1, 0, "", processID, 0, 0, 0, nil, nil, nil)
if err != nil {
log.Errorw(err, "indexer query failed")
}
return count > 0
}

// CountTotalProcesses returns the total number of processes indexed.
func (idx *Indexer) CountTotalProcesses() uint64 {
count, err := idx.readOnlyQuery.GetProcessCount(context.TODO())
Expand Down Expand Up @@ -128,6 +138,16 @@ func (idx *Indexer) EntityList(limit, offset int, entityID string) ([]indexertyp
return list, uint64(results[0].TotalCount), nil
}

// EntityExists returns whether the passed entityID matches at least one row in the db.
// entityID is a partial or full hex string.
func (idx *Indexer) EntityExists(entityID string) bool {
_, count, err := idx.EntityList(1, 0, entityID)
if err != nil {
log.Errorw(err, "indexer query failed")
}
return count > 0
}

// CountTotalEntities return the total number of entities indexed by the indexer
func (idx *Indexer) CountTotalEntities() uint64 {
count, err := idx.readOnlyQuery.GetEntityCount(context.TODO())
Expand Down

0 comments on commit ca3f738

Please sign in to comment.