Skip to content

Commit 7f87d3b

Browse files
authored
fix: improve foreign key and not found messages in API v3 (#896)
1 parent 18f1262 commit 7f87d3b

File tree

11 files changed

+279
-164
lines changed

11 files changed

+279
-164
lines changed

pkg/controllers/account_v3.go

Lines changed: 24 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,18 @@ type AccountCreateResponseV3 struct {
5555
Data []AccountResponseV3 `json:"data"` // List of created Accounts
5656
}
5757

58+
func (a *AccountCreateResponseV3) appendError(err httperrors.Error, status int) int {
59+
s := err.Error()
60+
a.Data = append(a.Data, AccountResponseV3{Error: &s})
61+
62+
// The final status code is the highest HTTP status code number
63+
if err.Status > status {
64+
status = err.Status
65+
}
66+
67+
return status
68+
}
69+
5870
type AccountResponseV3 struct {
5971
Data *AccountV3 `json:"data"` // Data for the account
6072
Error *string `json:"error" example:"the specified resource ID is not a valid UUID"` // The error, if any occurred for this transaction
@@ -214,7 +226,7 @@ func (co Controller) OptionsAccountDetailV3(c *gin.Context) {
214226
// @Failure 404 {object} AccountCreateResponseV3
215227
// @Failure 500 {object} AccountCreateResponseV3
216228
// @Param accounts body []AccountCreateV3 true "Accounts"
217-
// @Router /v3/accounts [post].
229+
// @Router /v3/accounts [post]
218230
func (co Controller) CreateAccountsV3(c *gin.Context) {
219231
var accounts []AccountCreateV3
220232

@@ -236,36 +248,25 @@ func (co Controller) CreateAccountsV3(c *gin.Context) {
236248
AccountCreate: create.ToCreate(),
237249
}
238250

251+
// Verify that budget exists. If not, append the error
252+
// and move to the next account
253+
_, err := getResourceByID[models.Budget](c, co, create.BudgetID)
254+
if !err.Nil() {
255+
status = r.appendError(err, status)
256+
continue
257+
}
258+
239259
dbErr := co.DB.Create(&a).Error
240260
if dbErr != nil {
241261
err := httperrors.GenericDBError[models.Account](a, c, dbErr)
242-
s := err.Error()
243-
c.JSON(err.Status, AccountCreateResponseV3{
244-
Error: &s,
245-
})
246-
return
247-
}
248-
249-
// Append the error
250-
if !err.Nil() {
251-
e := err.Error()
252-
r.Data = append(r.Data, AccountResponseV3{Error: &e})
253-
254-
// The final status code is the highest HTTP status code number since this also
255-
// represents the priority we
256-
if err.Status > status {
257-
status = err.Status
258-
}
262+
status = r.appendError(err, status)
259263
continue
260264
}
261265

262266
aObject, err := co.getAccountV3(c, a.ID)
263267
if !err.Nil() {
264-
e := err.Error()
265-
c.JSON(err.Status, AccountCreateResponseV3{
266-
Error: &e,
267-
})
268-
return
268+
status = r.appendError(err, status)
269+
continue
269270
}
270271
r.Data = append(r.Data, AccountResponseV3{Data: &aObject})
271272
}

pkg/controllers/account_v3_test.go

Lines changed: 46 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -239,29 +239,68 @@ func (suite *TestSuiteStandard) TestAccountsV3GetFilter() {
239239
}
240240

241241
func (suite *TestSuiteStandard) TestAccountsV3CreateFails() {
242+
// Test account for uniqueness
243+
a := suite.createTestAccountV3(suite.T(), controllers.AccountCreateV3{
244+
Name: "Unique Account Name for Budget",
245+
})
246+
242247
tests := []struct {
243-
name string
244-
body any
245-
status int // expected HTTP status
248+
name string
249+
body any
250+
status int // expected HTTP status
251+
testFunc func(t *testing.T, a controllers.AccountCreateResponseV3) // tests to perform against the updated account resource
246252
}{
247-
{"Broken Body", `[{ "note": 2 }]`, http.StatusBadRequest},
248-
{"No body", "", http.StatusBadRequest},
253+
{"Broken Body", `[{ "note": 2 }]`, http.StatusBadRequest, func(t *testing.T, a controllers.AccountCreateResponseV3) {
254+
assert.Equal(t, "json: cannot unmarshal number into Go struct field AccountCreateV3.note of type string", *a.Error)
255+
}},
256+
{
257+
"No body", "", http.StatusBadRequest,
258+
func(t *testing.T, a controllers.AccountCreateResponseV3) {
259+
assert.Equal(t, "the request body must not be empty", *a.Error)
260+
},
261+
},
249262
{
250263
"No Budget",
251264
`[{ "note": "Some text" }]`,
252265
http.StatusBadRequest,
266+
func(t *testing.T, a controllers.AccountCreateResponseV3) {
267+
assert.Equal(t, "no Budget ID specified", *a.Data[0].Error)
268+
},
253269
},
254270
{
255271
"Non-existing Budget",
256272
`[{ "budgetId": "ea85ad1a-3679-4ced-b83b-89566c12ece9" }]`,
273+
http.StatusNotFound,
274+
func(t *testing.T, a controllers.AccountCreateResponseV3) {
275+
assert.Equal(t, "there is no Budget with this ID", *a.Data[0].Error)
276+
},
277+
},
278+
{
279+
"Duplicate name for budget",
280+
[]controllers.AccountCreateV3{
281+
{
282+
Name: a.Data.Name,
283+
BudgetID: a.Data.BudgetID,
284+
},
285+
},
257286
http.StatusBadRequest,
287+
func(t *testing.T, a controllers.AccountCreateResponseV3) {
288+
assert.Equal(t, "the account name must be unique for the budget", *a.Data[0].Error)
289+
},
258290
},
259291
}
260292

261293
for _, tt := range tests {
262294
suite.T().Run(tt.name, func(t *testing.T) {
263-
recorder := test.Request(suite.controller, t, http.MethodPost, "http://example.com/v3/accounts", tt.body)
264-
assertHTTPStatus(t, &recorder, tt.status)
295+
r := test.Request(suite.controller, t, http.MethodPost, "http://example.com/v3/accounts", tt.body)
296+
assertHTTPStatus(t, &r, tt.status)
297+
298+
var a controllers.AccountCreateResponseV3
299+
decodeResponse(t, &r, &a)
300+
301+
if tt.testFunc != nil {
302+
tt.testFunc(t, a)
303+
}
265304
})
266305
}
267306
}

pkg/controllers/budget_v3.go

Lines changed: 15 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,18 @@ type BudgetCreateResponseV3 struct {
7474
Data []BudgetResponseV3 `json:"data"` // List of created Budgets
7575
}
7676

77+
func (b *BudgetCreateResponseV3) appendError(err httperrors.Error, status int) int {
78+
s := err.Error()
79+
b.Data = append(b.Data, BudgetResponseV3{Error: &s})
80+
81+
// The final status code is the highest HTTP status code number
82+
if err.Status > status {
83+
status = err.Status
84+
}
85+
86+
return status
87+
}
88+
7789
type BudgetResponseV3 struct {
7890
Data *BudgetV3 `json:"data"` // Data for the budget
7991
Error *string `json:"error" example:"the specified resource ID is not a valid UUID"` // The error, if any occurred
@@ -176,34 +188,15 @@ func (co Controller) CreateBudgetsV3(c *gin.Context) {
176188
dbErr := co.DB.Create(&b).Error
177189
if dbErr != nil {
178190
err := httperrors.GenericDBError[models.Budget](b, c, dbErr)
179-
s := err.Error()
180-
c.JSON(err.Status, BudgetCreateResponseV3{
181-
Error: &s,
182-
})
183-
return
184-
}
185-
186-
// Append the error
187-
if !err.Nil() {
188-
e := err.Error()
189-
r.Data = append(r.Data, BudgetResponseV3{Error: &e})
190-
191-
// The final status code is the highest HTTP status code number since this also
192-
// represents the priority we
193-
if err.Status > status {
194-
status = err.Status
195-
}
191+
status = r.appendError(err, status)
196192
continue
197193
}
198194

199195
// Append the budget
200196
bObject, err := co.getBudgetV3(c, b.ID)
201197
if !err.Nil() {
202-
e := err.Error()
203-
c.JSON(err.Status, BudgetCreateResponseV3{
204-
Error: &e,
205-
})
206-
return
198+
status = r.appendError(err, status)
199+
continue
207200
}
208201
r.Data = append(r.Data, BudgetResponseV3{Data: &bObject})
209202
}

pkg/controllers/budget_v3_test.go

Lines changed: 18 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -180,17 +180,29 @@ func (suite *TestSuiteStandard) TestBudgetsV3GetFilter() {
180180

181181
func (suite *TestSuiteStandard) TestBudgetsV3CreateFails() {
182182
tests := []struct {
183-
name string
184-
body string
183+
name string
184+
body string
185+
testFunc func(t *testing.T, b controllers.BudgetCreateResponseV3) // tests to perform against the updated budget resource
185186
}{
186-
{"Broken Body", `{ "note": 2 }`},
187-
{"No body", ""},
187+
{"Broken Body", `{ "note": 2 }`, func(t *testing.T, b controllers.BudgetCreateResponseV3) {
188+
assert.Equal(t, "json: cannot unmarshal object into Go value of type []models.BudgetCreate", *b.Error)
189+
}},
190+
{"No body", "", func(t *testing.T, b controllers.BudgetCreateResponseV3) {
191+
assert.Equal(t, "the request body must not be empty", *b.Error)
192+
}},
188193
}
189194

190195
for _, tt := range tests {
191196
suite.T().Run(tt.name, func(t *testing.T) {
192-
recorder := test.Request(suite.controller, t, http.MethodPost, "http://example.com/v3/budgets", tt.body)
193-
assertHTTPStatus(t, &recorder, http.StatusBadRequest)
197+
r := test.Request(suite.controller, t, http.MethodPost, "http://example.com/v3/budgets", tt.body)
198+
assertHTTPStatus(t, &r, http.StatusBadRequest)
199+
200+
var b controllers.BudgetCreateResponseV3
201+
suite.decodeResponse(&r, &b)
202+
203+
if tt.testFunc != nil {
204+
tt.testFunc(t, b)
205+
}
194206
})
195207
}
196208
}

pkg/controllers/category_v3.go

Lines changed: 23 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,18 @@ type CategoryCreateResponseV3 struct {
9090
Error *string `json:"error" example:"the specified resource ID is not a valid UUID"` // The error, if any occurred
9191
}
9292

93+
func (c *CategoryCreateResponseV3) appendError(err httperrors.Error, status int) int {
94+
s := err.Error()
95+
c.Data = append(c.Data, CategoryResponseV3{Error: &s})
96+
97+
// The final status code is the highest HTTP status code number
98+
if err.Status > status {
99+
status = err.Status
100+
}
101+
102+
return status
103+
}
104+
93105
type CategoryResponseV3 struct {
94106
Data *CategoryV3 `json:"data"` // Data for the Category
95107
Error *string `json:"error" example:"the specified resource ID is not a valid UUID"` // The error, if any occurred
@@ -206,36 +218,25 @@ func (co Controller) CreateCategoriesV3(c *gin.Context) {
206218
CategoryCreate: create.ToCreate(),
207219
}
208220

221+
// Verify that the budget exists. If not, append the error
222+
// and move to the next one.
223+
_, err := getResourceByID[models.Budget](c, co, create.BudgetID)
224+
if !err.Nil() {
225+
status = r.appendError(err, status)
226+
continue
227+
}
228+
209229
dbErr := co.DB.Create(&category).Error
210230
if dbErr != nil {
211231
err := httperrors.GenericDBError[models.Category](category, c, dbErr)
212-
s := err.Error()
213-
c.JSON(err.Status, CategoryCreateResponseV3{
214-
Error: &s,
215-
})
216-
return
217-
}
218-
219-
// Append the error
220-
if !err.Nil() {
221-
e := err.Error()
222-
r.Data = append(r.Data, CategoryResponseV3{Error: &e})
223-
224-
// The final status code is the highest HTTP status code number since this also
225-
// represents the priority we
226-
if err.Status > status {
227-
status = err.Status
228-
}
232+
status = r.appendError(err, status)
229233
continue
230234
}
231235

232236
eObject, err := co.getCategoryV3(c, category.ID)
233237
if !err.Nil() {
234-
e := err.Error()
235-
c.JSON(err.Status, CategoryCreateResponseV3{
236-
Error: &e,
237-
})
238-
return
238+
status = r.appendError(err, status)
239+
continue
239240
}
240241
r.Data = append(r.Data, CategoryResponseV3{Data: &eObject})
241242
}

pkg/controllers/category_v3_test.go

Lines changed: 40 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -213,36 +213,65 @@ func (suite *TestSuiteStandard) TestCategoriesV3CreateFails() {
213213
})
214214

215215
tests := []struct {
216-
name string
217-
body any
218-
status int // expected HTTP status
216+
name string
217+
body any
218+
status int // expected HTTP status
219+
testFunc func(t *testing.T, c controllers.CategoryCreateResponseV3) // tests to perform against the updated category resource
219220
}{
220-
{"Broken Body", `[{ "note": 2 }]`, http.StatusBadRequest},
221-
{"No body", "", http.StatusBadRequest},
221+
{
222+
"Broken Body", `[{ "note": 2 }]`, http.StatusBadRequest,
223+
func(t *testing.T, c controllers.CategoryCreateResponseV3) {
224+
assert.Equal(t, "json: cannot unmarshal number into Go struct field CategoryCreateV3.note of type string", *c.Error)
225+
},
226+
},
227+
{
228+
"No body", "", http.StatusBadRequest,
229+
func(t *testing.T, c controllers.CategoryCreateResponseV3) {
230+
assert.Equal(t, "the request body must not be empty", *c.Error)
231+
},
232+
},
222233
{
223234
"No Budget",
224235
`[{ "note": "Some text" }]`,
225236
http.StatusBadRequest,
237+
func(t *testing.T, c controllers.CategoryCreateResponseV3) {
238+
assert.Equal(t, "no Budget ID specified", *c.Data[0].Error)
239+
},
226240
},
227241
{
228242
"Non-existing Budget",
229243
`[{ "budgetId": "ea85ad1a-3679-4ced-b83b-89566c12ece9" }]`,
230-
http.StatusBadRequest,
244+
http.StatusNotFound,
245+
func(t *testing.T, c controllers.CategoryCreateResponseV3) {
246+
assert.Equal(t, "there is no Budget with this ID", *c.Data[0].Error)
247+
},
231248
},
232249
{
233250
"Duplicate name in Budget",
234-
controllers.CategoryCreateV3{
235-
BudgetID: c.Data.BudgetID,
236-
Name: c.Data.Name,
251+
[]controllers.CategoryCreateV3{
252+
{
253+
BudgetID: c.Data.BudgetID,
254+
Name: c.Data.Name,
255+
},
237256
},
238257
http.StatusBadRequest,
258+
func(t *testing.T, c controllers.CategoryCreateResponseV3) {
259+
assert.Equal(t, "the category name must be unique for the budget", *c.Data[0].Error)
260+
},
239261
},
240262
}
241263

242264
for _, tt := range tests {
243265
suite.T().Run(tt.name, func(t *testing.T) {
244-
recorder := test.Request(suite.controller, t, http.MethodPost, "http://example.com/v3/categories", tt.body)
245-
assertHTTPStatus(t, &recorder, tt.status)
266+
r := test.Request(suite.controller, t, http.MethodPost, "http://example.com/v3/categories", tt.body)
267+
assertHTTPStatus(t, &r, tt.status)
268+
269+
var c controllers.CategoryCreateResponseV3
270+
decodeResponse(t, &r, &c)
271+
272+
if tt.testFunc != nil {
273+
tt.testFunc(t, c)
274+
}
246275
})
247276
}
248277
}

0 commit comments

Comments
 (0)