Skip to content

Commit c0dc37c

Browse files
committed
feat: add cleanup for all resources
This endpoint allows you to delete all resources permanently. It can be used to reset an instance to “factory defaults“
1 parent 3815a8c commit c0dc37c

File tree

11 files changed

+190
-35
lines changed

11 files changed

+190
-35
lines changed

api/docs.go

Lines changed: 21 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ const docTemplate = `{
4949
"get": {
5050
"description": "Returns general information about the v1 API",
5151
"tags": [
52-
"General"
52+
"v1"
5353
],
5454
"summary": "v1 API",
5555
"responses": {
@@ -61,10 +61,28 @@ const docTemplate = `{
6161
}
6262
}
6363
},
64+
"delete": {
65+
"description": "Permanently deletes all resources",
66+
"tags": [
67+
"v1"
68+
],
69+
"summary": "Delete everything",
70+
"responses": {
71+
"204": {
72+
"description": "No Content"
73+
},
74+
"500": {
75+
"description": "Internal Server Error",
76+
"schema": {
77+
"$ref": "#/definitions/httperrors.HTTPError"
78+
}
79+
}
80+
}
81+
},
6482
"options": {
6583
"description": "Returns an empty response with the HTTP Header \"allow\" set to the allowed HTTP verbs",
6684
"tags": [
67-
"General"
85+
"v1"
6886
],
6987
"summary": "Allowed HTTP verbs",
7088
"responses": {
@@ -2831,7 +2849,7 @@ const docTemplate = `{
28312849
},
28322850
"allocations": {
28332851
"type": "string",
2834-
"example": "https://example.com/api/v1/allocations3"
2852+
"example": "https://example.com/api/v1/allocations"
28352853
},
28362854
"budgets": {
28372855
"type": "string",

api/swagger.json

Lines changed: 21 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@
3737
"get": {
3838
"description": "Returns general information about the v1 API",
3939
"tags": [
40-
"General"
40+
"v1"
4141
],
4242
"summary": "v1 API",
4343
"responses": {
@@ -49,10 +49,28 @@
4949
}
5050
}
5151
},
52+
"delete": {
53+
"description": "Permanently deletes all resources",
54+
"tags": [
55+
"v1"
56+
],
57+
"summary": "Delete everything",
58+
"responses": {
59+
"204": {
60+
"description": "No Content"
61+
},
62+
"500": {
63+
"description": "Internal Server Error",
64+
"schema": {
65+
"$ref": "#/definitions/httperrors.HTTPError"
66+
}
67+
}
68+
}
69+
},
5270
"options": {
5371
"description": "Returns an empty response with the HTTP Header \"allow\" set to the allowed HTTP verbs",
5472
"tags": [
55-
"General"
73+
"v1"
5674
],
5775
"summary": "Allowed HTTP verbs",
5876
"responses": {
@@ -2819,7 +2837,7 @@
28192837
},
28202838
"allocations": {
28212839
"type": "string",
2822-
"example": "https://example.com/api/v1/allocations3"
2840+
"example": "https://example.com/api/v1/allocations"
28232841
},
28242842
"budgets": {
28252843
"type": "string",

api/swagger.yaml

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -557,7 +557,7 @@ definitions:
557557
example: https://example.com/api/v1/accounts
558558
type: string
559559
allocations:
560-
example: https://example.com/api/v1/allocations3
560+
example: https://example.com/api/v1/allocations
561561
type: string
562562
budgets:
563563
example: https://example.com/api/v1/budgets
@@ -612,6 +612,18 @@ paths:
612612
tags:
613613
- General
614614
/v1:
615+
delete:
616+
description: Permanently deletes all resources
617+
responses:
618+
"204":
619+
description: No Content
620+
"500":
621+
description: Internal Server Error
622+
schema:
623+
$ref: '#/definitions/httperrors.HTTPError'
624+
summary: Delete everything
625+
tags:
626+
- v1
615627
get:
616628
description: Returns general information about the v1 API
617629
responses:
@@ -621,7 +633,7 @@ paths:
621633
$ref: '#/definitions/router.V1Response'
622634
summary: v1 API
623635
tags:
624-
- General
636+
- v1
625637
options:
626638
description: Returns an empty response with the HTTP Header "allow" set to the
627639
allowed HTTP verbs
@@ -630,7 +642,7 @@ paths:
630642
description: No Content
631643
summary: Allowed HTTP verbs
632644
tags:
633-
- General
645+
- v1
634646
/v1/accounts:
635647
get:
636648
description: Returns a list of accounts

internal/router/router.go

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,7 @@ func Router() (*gin.Engine, error) {
111111
v1 := r.Group("/v1")
112112
{
113113
v1.GET("", GetV1)
114+
v1.DELETE("", controllers.DeleteAll)
114115
v1.OPTIONS("", OptionsV1)
115116
}
116117

@@ -197,12 +198,12 @@ type V1Links struct {
197198
Categories string `json:"categories" example:"https://example.com/api/v1/categories"`
198199
Transactions string `json:"transactions" example:"https://example.com/api/v1/transactions"`
199200
Envelopes string `json:"envelopes" example:"https://example.com/api/v1/envelopes"`
200-
Allocations string `json:"allocations" example:"https://example.com/api/v1/allocations3"`
201+
Allocations string `json:"allocations" example:"https://example.com/api/v1/allocations"`
201202
}
202203

203204
// @Summary v1 API
204205
// @Description Returns general information about the v1 API
205-
// @Tags General
206+
// @Tags v1
206207
// @Success 200 {object} V1Response
207208
// @Router /v1 [get]
208209
func GetV1(c *gin.Context) {
@@ -220,9 +221,9 @@ func GetV1(c *gin.Context) {
220221

221222
// @Summary Allowed HTTP verbs
222223
// @Description Returns an empty response with the HTTP Header "allow" set to the allowed HTTP verbs
223-
// @Tags General
224+
// @Tags v1
224225
// @Success 204
225226
// @Router /v1 [options]
226227
func OptionsV1(c *gin.Context) {
227-
httputil.OptionsGet(c)
228+
httputil.OptionsGetDelete(c)
228229
}

internal/router/router_test.go

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -142,12 +142,13 @@ func TestOptions(t *testing.T) {
142142
t.Parallel()
143143

144144
tests := []struct {
145-
path string
146-
f func(*gin.Context)
145+
path string
146+
f func(*gin.Context)
147+
expected string
147148
}{
148-
{"/", router.OptionsRoot},
149-
{"/version", router.OptionsVersion},
150-
{"/v1", router.OptionsV1},
149+
{"/", router.OptionsRoot, "GET"},
150+
{"/version", router.OptionsVersion, "GET"},
151+
{"/v1", router.OptionsV1, "GET, DELETE"},
151152
}
152153

153154
for _, tt := range tests {
@@ -164,7 +165,7 @@ func TestOptions(t *testing.T) {
164165
r.ServeHTTP(w, c.Request)
165166

166167
assert.Equal(t, http.StatusNoContent, w.Code)
167-
assert.Equal(t, http.MethodGet, w.Header().Get("allow"))
168+
assert.Equal(t, tt.expected, w.Header().Get("allow"))
168169
})
169170
}
170171
}

pkg/controllers/cleanup.go

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
package controllers
2+
3+
import (
4+
"net/http"
5+
6+
"github.com/envelope-zero/backend/internal/database"
7+
"github.com/envelope-zero/backend/pkg/httperrors"
8+
"github.com/envelope-zero/backend/pkg/models"
9+
"github.com/gin-gonic/gin"
10+
)
11+
12+
// @Summary Delete everything
13+
// @Description Permanently deletes all resources
14+
// @Tags v1
15+
// @Success 204
16+
// @Failure 500 {object} httperrors.HTTPError
17+
// @Router /v1 [delete]
18+
func DeleteAll(c *gin.Context) {
19+
err := database.DB.Unscoped().Where("true").Delete(&models.Transaction{}).Error
20+
if err != nil {
21+
httperrors.Handler(c, err)
22+
return
23+
}
24+
25+
err = database.DB.Unscoped().Where("true").Delete(&models.Allocation{}).Error
26+
if err != nil {
27+
httperrors.Handler(c, err)
28+
return
29+
}
30+
31+
err = database.DB.Unscoped().Where("true").Delete(&models.Envelope{}).Error
32+
if err != nil {
33+
httperrors.Handler(c, err)
34+
return
35+
}
36+
37+
err = database.DB.Unscoped().Where("true").Delete(&models.Category{}).Error
38+
if err != nil {
39+
httperrors.Handler(c, err)
40+
return
41+
}
42+
43+
err = database.DB.Unscoped().Where("true").Delete(&models.Account{}).Error
44+
if err != nil {
45+
httperrors.Handler(c, err)
46+
return
47+
}
48+
49+
err = database.DB.Unscoped().Where("true").Delete(&models.Budget{}).Error
50+
if err != nil {
51+
httperrors.Handler(c, err)
52+
return
53+
}
54+
55+
c.JSON(http.StatusNoContent, gin.H{})
56+
}

pkg/controllers/cleanup_test.go

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
package controllers_test
2+
3+
import (
4+
"net/http"
5+
6+
"github.com/envelope-zero/backend/pkg/models"
7+
"github.com/envelope-zero/backend/pkg/test"
8+
"github.com/shopspring/decimal"
9+
)
10+
11+
func (suite *TestSuiteEnv) TestCleanup() {
12+
_ = createTestBudget(suite.T(), models.BudgetCreate{})
13+
_ = createTestAccount(suite.T(), models.AccountCreate{})
14+
_ = createTestCategory(suite.T(), models.CategoryCreate{})
15+
_ = createTestEnvelope(suite.T(), models.EnvelopeCreate{})
16+
_ = createTestAllocation(suite.T(), models.AllocationCreate{})
17+
_ = createTestTransaction(suite.T(), models.TransactionCreate{Amount: decimal.NewFromFloat(17.32)})
18+
19+
tests := []string{
20+
"http://example.com/v1/budgets",
21+
"http://example.com/v1/accounts",
22+
"http://example.com/v1/categories",
23+
"http://example.com/v1/transactions",
24+
"http://example.com/v1/envelopes",
25+
"http://example.com/v1/allocations",
26+
}
27+
28+
// Delete
29+
recorder := test.Request(suite.T(), http.MethodDelete, "http://example.com/v1", "")
30+
test.AssertHTTPStatus(suite.T(), http.StatusNoContent, &recorder)
31+
32+
// Verify
33+
for _, tt := range tests {
34+
suite.Run(tt, func() {
35+
recorder := test.Request(suite.T(), http.MethodGet, tt, "")
36+
test.AssertHTTPStatus(suite.T(), http.StatusOK, &recorder)
37+
38+
var response struct {
39+
Data []any `json:"data"`
40+
}
41+
42+
test.DecodeResponse(suite.T(), &recorder, &response)
43+
suite.Assert().Len(response.Data, 0, "There are resources left for type %s", tt)
44+
})
45+
}
46+
}

pkg/controllers/main_list_test.go

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,6 @@ var methodNotAllowedTests = []struct {
1313
{"/", http.MethodPost},
1414
{"/", http.MethodDelete},
1515
{"http://example.com/v1", http.MethodPost},
16-
{"http://example.com/v1", http.MethodDelete},
1716
{"http://example.com/v1/budgets", "HEAD"},
1817
{"http://example.com/v1/budgets", "PUT"},
1918
}

pkg/controllers/main_options_test.go

Lines changed: 0 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -7,21 +7,6 @@ import (
77
"github.com/stretchr/testify/assert"
88
)
99

10-
func (suite *TestSuiteEnv) TestOptionsHeaderGeneral() {
11-
optionsHeaderTests := []string{
12-
"/",
13-
"/version",
14-
"http://example.com/v1",
15-
}
16-
17-
for _, path := range optionsHeaderTests {
18-
recorder := test.Request(suite.T(), http.MethodOptions, path, "")
19-
20-
assert.Equal(suite.T(), http.StatusNoContent, recorder.Code)
21-
assert.Equal(suite.T(), recorder.Header().Get("allow"), "GET")
22-
}
23-
}
24-
2510
func (suite *TestSuiteEnv) TestOptionsHeaderResources() {
2611
optionsHeaderTests := []string{
2712
"http://example.com/v1/budgets",

pkg/httputil/options.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,11 @@ func OptionsGetPost(c *gin.Context) {
1717
c.Render(http.StatusNoContent, render.JSON{})
1818
}
1919

20+
func OptionsGetDelete(c *gin.Context) {
21+
c.Header("allow", "GET, DELETE")
22+
c.Render(http.StatusNoContent, render.JSON{})
23+
}
24+
2025
func OptionsGetPatchDelete(c *gin.Context) {
2126
c.Header("allow", "GET, PATCH, DELETE")
2227
c.Render(http.StatusNoContent, render.JSON{})

0 commit comments

Comments
 (0)