Skip to content

Commit 4de528b

Browse files
committed
fix: check parent for account
1 parent 9accf35 commit 4de528b

File tree

6 files changed

+126
-9
lines changed

6 files changed

+126
-9
lines changed

internal/controllers/account.go

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -96,10 +96,8 @@ func GetAccounts(c *gin.Context) {
9696

9797
// GetAccount retrieves an account by its ID.
9898
func GetAccount(c *gin.Context) {
99-
var account models.Account
100-
err := models.DB.First(&account, c.Param("accountId")).Error
99+
account, err := getAccount(c)
101100
if err != nil {
102-
FetchErrorHandler(c, err)
103101
return
104102
}
105103

internal/controllers/account_test.go

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,22 @@ func TestNoAccountNotFound(t *testing.T) {
8282
test.AssertHTTPStatus(t, http.StatusNotFound, &recorder)
8383
}
8484

85+
// TestAccountInvalidIDs verifies that on non-number requests for account IDs,
86+
// the API returs a Bad Request status code.
87+
func TestAccountInvalidIDs(t *testing.T) {
88+
r := test.Request(t, "GET", "/v1/budgets/1/accounts/-56", "")
89+
test.AssertHTTPStatus(t, http.StatusBadRequest, &r)
90+
91+
r = test.Request(t, "GET", "/v1/budgets/1/accounts/notANumber", "")
92+
test.AssertHTTPStatus(t, http.StatusBadRequest, &r)
93+
94+
r = test.Request(t, "GET", "/v1/budgets/-61/accounts/56", "")
95+
test.AssertHTTPStatus(t, http.StatusBadRequest, &r)
96+
97+
r = test.Request(t, "GET", "/v1/budgets/RandomStringThatIsNotAUint64/accounts/1", "")
98+
test.AssertHTTPStatus(t, http.StatusBadRequest, &r)
99+
}
100+
85101
// TestNonexistingBudgetAccounts404 is a regression test for https://github.com/envelope-zero/backend/issues/89.
86102
//
87103
// It verifies that for a non-existing budget, the accounts endpoint raises a 404
@@ -91,6 +107,25 @@ func TestNonexistingBudgetAccounts404(t *testing.T) {
91107
test.AssertHTTPStatus(t, http.StatusNotFound, &recorder)
92108
}
93109

110+
// TestAccountParentChecked is a regression test for https://github.com/envelope-zero/backend/issues/90.
111+
//
112+
// It verifies that the account details endpoint for a budget only returns accounts that belong to the
113+
// budget.
114+
func TestAccountParentChecked(t *testing.T) {
115+
r := test.Request(t, "POST", "/v1/budgets", `{ "name": "New Budget", "note": "More tests something something" }`)
116+
test.AssertHTTPStatus(t, http.StatusCreated, &r)
117+
118+
var budget BudgetDetailResponse
119+
test.DecodeResponse(t, &r, &budget)
120+
121+
path := fmt.Sprintf("/v1/budgets/%v", budget.Data.ID)
122+
r = test.Request(t, "GET", path+"/accounts/1", "")
123+
test.AssertHTTPStatus(t, http.StatusNotFound, &r)
124+
125+
r = test.Request(t, "DELETE", path, "")
126+
test.AssertHTTPStatus(t, http.StatusNoContent, &r)
127+
}
128+
94129
func TestCreateAccount(t *testing.T) {
95130
recorder := test.Request(t, "POST", "/v1/budgets/1/accounts", `{ "name": "New Account", "note": "More tests something something" }`)
96131
test.AssertHTTPStatus(t, http.StatusCreated, &recorder)

internal/controllers/budget_test.go

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,16 @@ func TestNoBudgetNotFound(t *testing.T) {
4848
test.AssertHTTPStatus(t, http.StatusNotFound, &recorder)
4949
}
5050

51+
// TestBudgetInvalidIDs verifies that on non-number requests for budget IDs,
52+
// the API returs a Bad Request status code.
53+
func TestBudgetInvalidIDs(t *testing.T) {
54+
r := test.Request(t, "GET", "/v1/budgets/-17", "")
55+
test.AssertHTTPStatus(t, http.StatusBadRequest, &r)
56+
57+
r = test.Request(t, "GET", "/v1/budgets/DefinitelyNotAUint64", "")
58+
test.AssertHTTPStatus(t, http.StatusBadRequest, &r)
59+
}
60+
5161
func TestCreateBudget(t *testing.T) {
5262
recorder := test.Request(t, "POST", "/v1/budgets", `{ "name": "New Budget", "note": "More tests something something" }`)
5363
test.AssertHTTPStatus(t, http.StatusCreated, &recorder)

internal/controllers/category_test.go

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,16 @@ func TestNoCategoryNotFound(t *testing.T) {
4949
test.AssertHTTPStatus(t, http.StatusNotFound, &recorder)
5050
}
5151

52+
// TestCategoryInvalidIDs verifies that on non-number requests for category IDs,
53+
// the API returs a Bad Request status code.
54+
func TestCategoryInvalidIDs(t *testing.T) {
55+
r := test.Request(t, "GET", "/v1/budgets/1/categories/-557", "")
56+
test.AssertHTTPStatus(t, http.StatusBadRequest, &r)
57+
58+
r = test.Request(t, "GET", "/v1/budgets/1/categories/HanShotFirst", "")
59+
test.AssertHTTPStatus(t, http.StatusBadRequest, &r)
60+
}
61+
5262
// TestNonexistingBudgetCategories404 is a regression test for https://github.com/envelope-zero/backend/issues/89.
5363
//
5464
// It verifies that for a non-existing budget, the accounts endpoint raises a 404

internal/controllers/envelope_test.go

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,16 @@ func TestNoEnvelopeNotFound(t *testing.T) {
4949
test.AssertHTTPStatus(t, http.StatusNotFound, &recorder)
5050
}
5151

52+
// TestEnvelopeInvalidIDs verifies that on non-number requests for envelope IDs,
53+
// the API returs a Bad Request status code.
54+
func TestEnvelopeInvalidIDs(t *testing.T) {
55+
r := test.Request(t, "GET", "/v1/budgets/1/categories/1/envelopes/-1985", "")
56+
test.AssertHTTPStatus(t, http.StatusBadRequest, &r)
57+
58+
r = test.Request(t, "GET", "/v1/budgets/1/categories/1/envelopes/OhNoOurTable", "")
59+
test.AssertHTTPStatus(t, http.StatusBadRequest, &r)
60+
}
61+
5262
// TestNonexistingCategoryEnvelopes404 is a regression test for https://github.com/envelope-zero/backend/issues/89.
5363
//
5464
// It verifies that for a non-existing category, the envelopes endpoint raises a 404

internal/controllers/helper.go

Lines changed: 60 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import (
55
"fmt"
66
"io"
77
"net/http"
8+
"reflect"
89
"strconv"
910

1011
"github.com/envelope-zero/backend/internal/models"
@@ -43,14 +44,29 @@ func requestURL(c *gin.Context) string {
4344
return scheme + "://" + c.Request.Host + forwardedPrefix + c.Request.URL.Path
4445
}
4546

47+
func parseID(c *gin.Context, param string) (uint64, error) {
48+
var parsed uint64
49+
50+
parsed, err := strconv.ParseUint(c.Param(param), 10, 64)
51+
if err != nil {
52+
FetchErrorHandler(c, err)
53+
return 0, err
54+
}
55+
56+
return parsed, nil
57+
}
58+
4659
// getBudget verifies that the budget from the URL parameters exists and returns it.
4760
func getBudget(c *gin.Context) (models.Budget, error) {
4861
var budget models.Budget
4962

50-
budgetID, _ := strconv.ParseUint(c.Param("budgetId"), 10, 64)
63+
budgetID, err := parseID(c, "budgetId")
64+
if err != nil {
65+
return models.Budget{}, err
66+
}
5167

5268
// Check that the budget exists. If not, return a 404
53-
err := models.DB.Where(&models.Budget{
69+
err = models.DB.Where(&models.Budget{
5470
Model: models.Model{
5571
ID: budgetID,
5672
},
@@ -63,15 +79,46 @@ func getBudget(c *gin.Context) (models.Budget, error) {
6379
return budget, nil
6480
}
6581

82+
// getAccount verifies that the request URI is valid for the account and returns it.
83+
func getAccount(c *gin.Context) (models.Account, error) {
84+
var account models.Account
85+
86+
budget, err := getBudget(c)
87+
if err != nil {
88+
return models.Account{}, err
89+
}
90+
91+
accountID, err := parseID(c, "accountId")
92+
if err != nil {
93+
return models.Account{}, err
94+
}
95+
96+
err = models.DB.First(&account, &models.Account{
97+
BudgetID: budget.ID,
98+
Model: models.Model{
99+
ID: accountID,
100+
},
101+
}).Error
102+
if err != nil {
103+
FetchErrorHandler(c, err)
104+
return models.Account{}, err
105+
}
106+
107+
return account, nil
108+
}
109+
66110
// getCategory verifies that the category from the URL parameters exists and returns it
67111
//
68112
// It also verifies that the budget that is referred to exists.
69113
func getCategory(c *gin.Context) (models.Category, error) {
70114
var category models.Category
71115

72-
categoryID, _ := strconv.ParseUint(c.Param("categoryId"), 10, 64)
116+
categoryID, err := parseID(c, "categoryId")
117+
if err != nil {
118+
return models.Category{}, err
119+
}
73120

74-
_, err := getBudget(c)
121+
_, err = getBudget(c)
75122
if err != nil {
76123
return models.Category{}, err
77124
}
@@ -95,9 +142,12 @@ func getCategory(c *gin.Context) (models.Category, error) {
95142
func getEnvelope(c *gin.Context) (models.Envelope, error) {
96143
var envelope models.Envelope
97144

98-
envelopeID, _ := strconv.ParseUint(c.Param("envelopeId"), 10, 64)
145+
envelopeID, err := parseID(c, "envelopeId")
146+
if err != nil {
147+
return models.Envelope{}, err
148+
}
99149

100-
_, err := getCategory(c)
150+
_, err = getCategory(c)
101151
if err != nil {
102152
return models.Envelope{}, err
103153
}
@@ -119,6 +169,10 @@ func getEnvelope(c *gin.Context) (models.Envelope, error) {
119169
func FetchErrorHandler(c *gin.Context, err error) {
120170
if errors.Is(err, gorm.ErrRecordNotFound) {
121171
c.AbortWithStatus(http.StatusNotFound)
172+
} else if reflect.TypeOf(err) == reflect.TypeOf(&strconv.NumError{}) {
173+
c.JSON(http.StatusBadRequest, gin.H{
174+
"error": "An ID specified in the query string was not a valid uint64",
175+
})
122176
} else {
123177
log.Error().Str("request-id", requestid.Get(c)).Msgf("%T: %v", err, err.Error())
124178
c.JSON(http.StatusInternalServerError, gin.H{

0 commit comments

Comments
 (0)