Skip to content

Commit 02b6f0f

Browse files
authored
feat: enable foreign keys for SQLite (#221)
BREAKING CHANGE: Foreign keys are now enforced, meaning that resources can't be created without referencing all the other resources they can reference, e.g. a budget for an account.
1 parent 0ffbd11 commit 02b6f0f

18 files changed

+325
-130
lines changed

.vscode/launch.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
"cwd": "${workspaceFolder}",
1010
"program": "${workspaceFolder}/main.go",
1111
"env": {
12-
"LOG_LEVEL": "debug"
12+
"GIN_MODE": "debug"
1313
},
1414
"console": "integratedTerminal"
1515
},

main.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ func main() {
3333
panic("Could not create data directory")
3434
}
3535

36-
dsn = "data/gorm.db"
36+
dsn = "data/gorm.db?_pragma=foreign_keys(1)"
3737
dialector = sqlite.Open
3838
}
3939

pkg/controllers/account.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package controllers
22

33
import (
4+
"errors"
45
"fmt"
56
"net/http"
67

@@ -214,6 +215,12 @@ func DeleteAccount(c *gin.Context) {
214215

215216
// getAccountResource is the internal helper to verify permissions and return an account.
216217
func getAccountResource(c *gin.Context, id uuid.UUID) (models.Account, error) {
218+
if id == uuid.Nil {
219+
err := errors.New("No account ID specified")
220+
httputil.NewError(c, http.StatusBadRequest, err)
221+
return models.Account{}, err
222+
}
223+
217224
var account models.Account
218225

219226
err := database.DB.Where(&models.Account{

pkg/controllers/account_test.go

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,10 @@ import (
1414
)
1515

1616
func createTestAccount(t *testing.T, c models.AccountCreate) controllers.AccountResponse {
17+
if c.BudgetID == uuid.Nil {
18+
c.BudgetID = createTestBudget(t, models.BudgetCreate{Name: "Testing budget"}).Data.ID
19+
}
20+
1721
r := test.Request(t, http.MethodPost, "http://example.com/v1/accounts", c)
1822
test.AssertHTTPStatus(t, http.StatusCreated, &r)
1923

@@ -113,6 +117,11 @@ func (suite *TestSuiteEnv) TestCreateAccount() {
113117
_ = createTestAccount(suite.T(), models.AccountCreate{Name: "Test account for creation"})
114118
}
115119

120+
func (suite *TestSuiteEnv) TestCreateAccountNoBudget() {
121+
r := test.Request(suite.T(), http.MethodPost, "http://example.com/v1/accounts", models.Account{})
122+
test.AssertHTTPStatus(suite.T(), http.StatusBadRequest, &r)
123+
}
124+
116125
func (suite *TestSuiteEnv) TestCreateBrokenAccount() {
117126
recorder := test.Request(suite.T(), http.MethodPost, "http://example.com/v1/accounts", `{ "createdAt": "New Account", "note": "More tests for accounts to ensure less brokenness something" }`)
118127
test.AssertHTTPStatus(suite.T(), http.StatusBadRequest, &recorder)
@@ -123,7 +132,7 @@ func (suite *TestSuiteEnv) TestCreateAccountNoBody() {
123132
test.AssertHTTPStatus(suite.T(), http.StatusBadRequest, &recorder)
124133
}
125134

126-
func (suite *TestSuiteEnv) TestCreateAccountNoBudget() {
135+
func (suite *TestSuiteEnv) TestCreateAccountNonExistingBudget() {
127136
recorder := test.Request(suite.T(), http.MethodPost, "http://example.com/v1/accounts", models.AccountCreate{BudgetID: uuid.New()})
128137
test.AssertHTTPStatus(suite.T(), http.StatusNotFound, &recorder)
129138
}

pkg/controllers/allocation.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -238,6 +238,12 @@ func DeleteAllocation(c *gin.Context) {
238238

239239
// getAllocationResource verifies that the request URI is valid for the transaction and returns it.
240240
func getAllocationResource(c *gin.Context, id uuid.UUID) (models.Allocation, error) {
241+
if id == uuid.Nil {
242+
err := errors.New("No allocation ID specified")
243+
httputil.NewError(c, http.StatusBadRequest, err)
244+
return models.Allocation{}, err
245+
}
246+
241247
var allocation models.Allocation
242248

243249
err := database.DB.First(&allocation, &models.Allocation{

pkg/controllers/allocation_test.go

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,10 @@ import (
1414
)
1515

1616
func createTestAllocation(t *testing.T, c models.AllocationCreate) controllers.AllocationResponse {
17+
if c.EnvelopeID == uuid.Nil {
18+
c.EnvelopeID = createTestEnvelope(t, models.EnvelopeCreate{Name: "Transaction Test Envelope"}).Data.ID
19+
}
20+
1721
r := test.Request(t, "POST", "http://example.com/v1/allocations", c)
1822
test.AssertHTTPStatus(t, http.StatusCreated, &r)
1923

@@ -95,6 +99,11 @@ func (suite *TestSuiteEnv) TestCreateAllocation() {
9599
}
96100
}
97101

102+
func (suite *TestSuiteEnv) TestCreateAllocationNoEnvelope() {
103+
r := test.Request(suite.T(), http.MethodPost, "http://example.com/v1/allocations", models.Allocation{})
104+
test.AssertHTTPStatus(suite.T(), http.StatusBadRequest, &r)
105+
}
106+
98107
func (suite *TestSuiteEnv) TestCreateBrokenAllocation() {
99108
recorder := test.Request(suite.T(), "POST", "http://example.com/v1/allocations", `{ "createdAt": "New Allocation" }`)
100109
test.AssertHTTPStatus(suite.T(), http.StatusBadRequest, &recorder)
@@ -172,3 +181,8 @@ func (suite *TestSuiteEnv) TestDeleteAllocationWithBody() {
172181
r := test.Request(suite.T(), "DELETE", a.Data.Links.Self, models.AllocationCreate{Year: 2011, Month: 3})
173182
test.AssertHTTPStatus(suite.T(), http.StatusNoContent, &r)
174183
}
184+
185+
func (suite *TestSuiteEnv) TestDeleteNullAllocation() {
186+
r := test.Request(suite.T(), "DELETE", "http://example.com/v1/allocations/00000000-0000-0000-0000-000000000000", "")
187+
test.AssertHTTPStatus(suite.T(), http.StatusBadRequest, &r)
188+
}

pkg/controllers/budget.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -300,6 +300,12 @@ func DeleteBudget(c *gin.Context) {
300300

301301
// getBudgetResource is the internal helper to verify permissions and return a budget.
302302
func getBudgetResource(c *gin.Context, id uuid.UUID) (models.Budget, error) {
303+
if id == uuid.Nil {
304+
err := errors.New("No budget ID specified")
305+
httputil.NewError(c, http.StatusBadRequest, err)
306+
return models.Budget{}, err
307+
}
308+
303309
var budget models.Budget
304310

305311
err := database.DB.Where(&models.Budget{

pkg/controllers/category.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package controllers
22

33
import (
4+
"errors"
45
"fmt"
56
"net/http"
67

@@ -213,6 +214,12 @@ func DeleteCategory(c *gin.Context) {
213214
}
214215

215216
func getCategoryResource(c *gin.Context, id uuid.UUID) (models.Category, error) {
217+
if id == uuid.Nil {
218+
err := errors.New("No category ID specified")
219+
httputil.NewError(c, http.StatusBadRequest, err)
220+
return models.Category{}, err
221+
}
222+
216223
var category models.Category
217224

218225
err := database.DB.Where(&models.Category{

pkg/controllers/category_test.go

Lines changed: 34 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,30 @@ package controllers_test
22

33
import (
44
"net/http"
5+
"testing"
56
"time"
67

78
"github.com/envelope-zero/backend/pkg/controllers"
9+
"github.com/envelope-zero/backend/pkg/models"
810
"github.com/envelope-zero/backend/pkg/test"
11+
"github.com/google/uuid"
912
"github.com/stretchr/testify/assert"
1013
)
1114

15+
func createTestCategory(t *testing.T, c models.CategoryCreate) controllers.CategoryResponse {
16+
if c.BudgetID == uuid.Nil {
17+
c.BudgetID = createTestBudget(t, models.BudgetCreate{Name: "Testing budget"}).Data.ID
18+
}
19+
20+
r := test.Request(t, http.MethodPost, "http://example.com/v1/categories", c)
21+
test.AssertHTTPStatus(t, http.StatusCreated, &r)
22+
23+
var category controllers.CategoryResponse
24+
test.DecodeResponse(t, &r, &category)
25+
26+
return category
27+
}
28+
1229
func (suite *TestSuiteEnv) TestGetCategories() {
1330
recorder := test.Request(suite.T(), "GET", "http://example.com/v1/categories", "")
1431

@@ -30,6 +47,13 @@ func (suite *TestSuiteEnv) TestGetCategories() {
3047
assert.LessOrEqual(suite.T(), diff, test.TOLERANCE)
3148
}
3249

50+
func (suite *TestSuiteEnv) TestGetCategory() {
51+
category := createTestCategory(suite.T(), models.CategoryCreate{Name: "Catch me if you can!"})
52+
recorder := test.Request(suite.T(), http.MethodGet, category.Data.Links.Self, "")
53+
54+
test.AssertHTTPStatus(suite.T(), http.StatusOK, &recorder)
55+
}
56+
3357
func (suite *TestSuiteEnv) TestNoCategoryNotFound() {
3458
recorder := test.Request(suite.T(), "GET", "http://example.com/v1/categories/4e743e94-6a4b-44d6-aba5-d77c87103ff7", "")
3559

@@ -69,24 +93,15 @@ func (suite *TestSuiteEnv) TestCategoryInvalidIDs() {
6993
}
7094

7195
func (suite *TestSuiteEnv) TestCreateCategory() {
72-
recorder := test.Request(suite.T(), "POST", "http://example.com/v1/categories", `{ "name": "New Category", "note": "More tests something something" }`)
73-
test.AssertHTTPStatus(suite.T(), http.StatusCreated, &recorder)
74-
75-
var categoryObject, savedCategory controllers.CategoryResponse
76-
test.DecodeResponse(suite.T(), &recorder, &categoryObject)
77-
78-
recorder = test.Request(suite.T(), "GET", categoryObject.Data.Links.Self, "")
79-
test.DecodeResponse(suite.T(), &recorder, &savedCategory)
80-
81-
assert.Equal(suite.T(), savedCategory, categoryObject)
96+
_ = createTestCategory(suite.T(), models.CategoryCreate{Name: "New Category", Note: "Something to test creation"})
8297
}
8398

8499
func (suite *TestSuiteEnv) TestCreateBrokenCategory() {
85100
recorder := test.Request(suite.T(), "POST", "http://example.com/v1/categories", `{ "createdAt": "New Category", "note": "More tests for categories to ensure less brokenness something" }`)
86101
test.AssertHTTPStatus(suite.T(), http.StatusBadRequest, &recorder)
87102
}
88103

89-
func (suite *TestSuiteEnv) TestCreateBrokenNoBudget() {
104+
func (suite *TestSuiteEnv) TestCreateBudgetDoesNotExist() {
90105
recorder := test.Request(suite.T(), "POST", "http://example.com/v1/categories", `{ "budgetId": "f8c74664-9b79-4e15-8d3d-4618f3e3c230" }`)
91106
test.AssertHTTPStatus(suite.T(), http.StatusNotFound, &recorder)
92107
}
@@ -97,13 +112,9 @@ func (suite *TestSuiteEnv) TestCreateCategoryNoBody() {
97112
}
98113

99114
func (suite *TestSuiteEnv) TestUpdateCategory() {
100-
recorder := test.Request(suite.T(), "POST", "http://example.com/v1/categories", `{ "name": "New Category", "note": "More tests something something" }`)
101-
test.AssertHTTPStatus(suite.T(), http.StatusCreated, &recorder)
115+
category := createTestCategory(suite.T(), models.CategoryCreate{Name: "New category", Note: "Mor(r)e tests"})
102116

103-
var category controllers.CategoryResponse
104-
test.DecodeResponse(suite.T(), &recorder, &category)
105-
106-
recorder = test.Request(suite.T(), "PATCH", category.Data.Links.Self, `{ "name": "Updated new category for testing" }`)
117+
recorder := test.Request(suite.T(), "PATCH", category.Data.Links.Self, `{ "name": "Updated new category for testing" }`)
107118
test.AssertHTTPStatus(suite.T(), http.StatusOK, &recorder)
108119

109120
var updatedCategory controllers.CategoryResponse
@@ -114,13 +125,9 @@ func (suite *TestSuiteEnv) TestUpdateCategory() {
114125
}
115126

116127
func (suite *TestSuiteEnv) TestUpdateCategoryBroken() {
117-
recorder := test.Request(suite.T(), "POST", "http://example.com/v1/categories", `{ "name": "New Category", "note": "More tests something something" }`)
118-
test.AssertHTTPStatus(suite.T(), http.StatusCreated, &recorder)
128+
category := createTestCategory(suite.T(), models.CategoryCreate{Name: "New category", Note: "Mor(r)e tests"})
119129

120-
var category controllers.CategoryResponse
121-
test.DecodeResponse(suite.T(), &recorder, &category)
122-
123-
recorder = test.Request(suite.T(), "PATCH", category.Data.Links.Self, `{ "name": 2" }`)
130+
recorder := test.Request(suite.T(), "PATCH", category.Data.Links.Self, `{ "name": 2" }`)
124131
test.AssertHTTPStatus(suite.T(), http.StatusBadRequest, &recorder)
125132
}
126133

@@ -130,13 +137,9 @@ func (suite *TestSuiteEnv) TestUpdateNonExistingCategory() {
130137
}
131138

132139
func (suite *TestSuiteEnv) TestDeleteCategory() {
133-
recorder := test.Request(suite.T(), "POST", "http://example.com/v1/categories", `{ "name": "Delete me now!" }`)
134-
test.AssertHTTPStatus(suite.T(), http.StatusCreated, &recorder)
135-
136-
var category controllers.CategoryResponse
137-
test.DecodeResponse(suite.T(), &recorder, &category)
140+
category := createTestCategory(suite.T(), models.CategoryCreate{Name: "Delete me now!"})
138141

139-
recorder = test.Request(suite.T(), "DELETE", category.Data.Links.Self, "")
142+
recorder := test.Request(suite.T(), "DELETE", category.Data.Links.Self, "")
140143
test.AssertHTTPStatus(suite.T(), http.StatusNoContent, &recorder)
141144
}
142145

@@ -146,12 +149,8 @@ func (suite *TestSuiteEnv) TestDeleteNonExistingCategory() {
146149
}
147150

148151
func (suite *TestSuiteEnv) TestDeleteCategoryWithBody() {
149-
recorder := test.Request(suite.T(), "POST", "http://example.com/v1/categories", `{ "name": "Delete me now!" }`)
150-
test.AssertHTTPStatus(suite.T(), http.StatusCreated, &recorder)
151-
152-
var category controllers.CategoryResponse
153-
test.DecodeResponse(suite.T(), &recorder, &category)
152+
category := createTestCategory(suite.T(), models.CategoryCreate{Name: "Delete me now!"})
154153

155-
recorder = test.Request(suite.T(), "DELETE", category.Data.Links.Self, `{ "name": "test name 23" }`)
154+
recorder := test.Request(suite.T(), "DELETE", category.Data.Links.Self, `{ "name": "test name 23" }`)
156155
test.AssertHTTPStatus(suite.T(), http.StatusNoContent, &recorder)
157156
}

pkg/controllers/envelope.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -250,6 +250,12 @@ func DeleteEnvelope(c *gin.Context) {
250250

251251
// getEnvelopeResource verifies that the envelope from the URL parameters exists and returns it.
252252
func getEnvelopeResource(c *gin.Context, id uuid.UUID) (models.Envelope, error) {
253+
if id == uuid.Nil {
254+
err := errors.New("No envelope ID specified")
255+
httputil.NewError(c, http.StatusBadRequest, err)
256+
return models.Envelope{}, err
257+
}
258+
253259
var envelope models.Envelope
254260

255261
err := database.DB.Where(&models.Envelope{

0 commit comments

Comments
 (0)