diff --git a/go.mod b/go.mod index c57400dc..f01f429e 100644 --- a/go.mod +++ b/go.mod @@ -39,7 +39,7 @@ require ( github.com/go-playground/universal-translator v0.18.0 // indirect github.com/go-playground/validator/v10 v10.10.1 // indirect github.com/golang/protobuf v1.5.2 // indirect - github.com/google/uuid v1.3.0 // indirect + github.com/google/uuid v1.3.0 github.com/jackc/chunkreader/v2 v2.0.1 // indirect github.com/jackc/pgconn v1.12.0 // indirect github.com/jackc/pgio v1.0.0 // indirect diff --git a/internal/controllers/account.go b/internal/controllers/account.go index eaa8d805..24d28b0e 100644 --- a/internal/controllers/account.go +++ b/internal/controllers/account.go @@ -7,6 +7,7 @@ import ( "github.com/envelope-zero/backend/internal/httputil" "github.com/envelope-zero/backend/internal/models" "github.com/gin-gonic/gin" + "github.com/google/uuid" ) type AccountListResponse struct { @@ -23,7 +24,7 @@ type Account struct { } type AccountLinks struct { - Self string `json:"self" example:"https://example.com/api/v1/accounts/17"` + Self string `json:"self" example:"https://example.com/api/v1/accounts/af892e10-7e0a-4fb8-b1bc-4b6d88401ed2"` } // RegisterAccountRoutes registers the routes for accounts with @@ -135,12 +136,13 @@ func GetAccounts(c *gin.Context) { // @Param accountId path uint64 true "ID of the account" // @Router /v1/accounts/{accountId} [get] func GetAccount(c *gin.Context) { - id, err := httputil.ParseID(c, "accountId") + p, err := uuid.Parse(c.Param("accountId")) if err != nil { + httputil.ErrorInvalidUUID(c) return } - accountObject, err := getAccountObject(c, id) + accountObject, err := getAccountObject(c, p) if err != nil { return } @@ -160,12 +162,13 @@ func GetAccount(c *gin.Context) { // @Param account body models.AccountCreate true "Account" // @Router /v1/accounts/{accountId} [patch] func UpdateAccount(c *gin.Context) { - id, err := httputil.ParseID(c, "accountId") + p, err := uuid.Parse(c.Param("accountId")) if err != nil { + httputil.ErrorInvalidUUID(c) return } - account, err := getAccountResource(c, id) + account, err := getAccountResource(c, p) if err != nil { return } @@ -191,12 +194,13 @@ func UpdateAccount(c *gin.Context) { // @Param accountId path uint64 true "ID of the account" // @Router /v1/accounts/{accountId} [delete] func DeleteAccount(c *gin.Context) { - id, err := httputil.ParseID(c, "accountId") + p, err := uuid.Parse(c.Param("accountId")) if err != nil { + httputil.ErrorInvalidUUID(c) return } - account, err := getAccountResource(c, id) + account, err := getAccountResource(c, p) if err != nil { return } @@ -207,7 +211,7 @@ func DeleteAccount(c *gin.Context) { } // getAccountResource is the internal helper to verify permissions and return an account. -func getAccountResource(c *gin.Context, id uint64) (models.Account, error) { +func getAccountResource(c *gin.Context, id uuid.UUID) (models.Account, error) { var account models.Account err := models.DB.Where(&models.Account{ @@ -223,7 +227,7 @@ func getAccountResource(c *gin.Context, id uint64) (models.Account, error) { return account, nil } -func getAccountObject(c *gin.Context, id uint64) (Account, error) { +func getAccountObject(c *gin.Context, id uuid.UUID) (Account, error) { resource, err := getAccountResource(c, id) if err != nil { return Account{}, err @@ -239,8 +243,8 @@ func getAccountObject(c *gin.Context, id uint64) (Account, error) { // // This function is only needed for getAccountObject as we cannot create an instance of Account // with mixed named and unnamed parameters. -func getAccountLinks(c *gin.Context, id uint64) AccountLinks { - url := httputil.RequestPathV1(c) + fmt.Sprintf("/accounts/%d", id) +func getAccountLinks(c *gin.Context, id uuid.UUID) AccountLinks { + url := httputil.RequestPathV1(c) + fmt.Sprintf("/accounts/%s", id) return AccountLinks{ Self: url, diff --git a/internal/controllers/account_test.go b/internal/controllers/account_test.go index 8711d2fb..280b79f6 100644 --- a/internal/controllers/account_test.go +++ b/internal/controllers/account_test.go @@ -8,12 +8,23 @@ import ( "github.com/envelope-zero/backend/internal/controllers" "github.com/envelope-zero/backend/internal/models" "github.com/envelope-zero/backend/internal/test" + "github.com/google/uuid" "github.com/shopspring/decimal" "github.com/stretchr/testify/assert" ) +func createTestAccount(t *testing.T, c models.AccountCreate) controllers.AccountResponse { + r := test.Request(t, http.MethodPost, "/v1/accounts", c) + test.AssertHTTPStatus(t, http.StatusCreated, &r) + + var a controllers.AccountResponse + test.DecodeResponse(t, &r, &a) + + return a +} + func TestGetAccounts(t *testing.T) { - recorder := test.Request(t, "GET", "/v1/accounts", "") + recorder := test.Request(t, http.MethodGet, "/v1/accounts", "") var response controllers.AccountListResponse test.DecodeResponse(t, &recorder, &response) @@ -24,19 +35,16 @@ func TestGetAccounts(t *testing.T) { } bankAccount := response.Data[0] - assert.Equal(t, uint64(1), bankAccount.BudgetID) assert.Equal(t, "Bank Account", bankAccount.Name) assert.Equal(t, true, bankAccount.OnBudget) assert.Equal(t, false, bankAccount.External) cashAccount := response.Data[1] - assert.Equal(t, uint64(1), cashAccount.BudgetID) assert.Equal(t, "Cash Account", cashAccount.Name) assert.Equal(t, false, cashAccount.OnBudget) assert.Equal(t, false, cashAccount.External) externalAccount := response.Data[2] - assert.Equal(t, uint64(1), externalAccount.BudgetID) assert.Equal(t, "External Account", externalAccount.Name) assert.Equal(t, false, externalAccount.OnBudget) assert.Equal(t, true, externalAccount.External) @@ -64,155 +72,130 @@ func TestGetAccounts(t *testing.T) { } func TestNoAccountNotFound(t *testing.T) { - recorder := test.Request(t, "GET", "/v1/accounts/37", "") + recorder := test.Request(t, http.MethodGet, "/v1/accounts/39633f90-3d9f-4b1e-ac24-c341c432a6e3", "") test.AssertHTTPStatus(t, http.StatusNotFound, &recorder) } -// TestAccountInvalidIDs verifies that on non-number requests for account IDs, -// the API returs a Bad Request status code. func TestAccountInvalidIDs(t *testing.T) { - r := test.Request(t, "GET", "/v1/accounts/-56", "") + /* + * GET + */ + r := test.Request(t, http.MethodGet, "/v1/accounts/-56", "") test.AssertHTTPStatus(t, http.StatusBadRequest, &r) - r = test.Request(t, "GET", "/v1/accounts/notANumber", "") + r = test.Request(t, http.MethodGet, "/v1/accounts/notANumber", "") test.AssertHTTPStatus(t, http.StatusBadRequest, &r) - r = test.Request(t, "GET", "/v1/accounts/56", "") - test.AssertHTTPStatus(t, http.StatusNotFound, &r) - - r = test.Request(t, "GET", "/v1/accounts/1", "") - test.AssertHTTPStatus(t, http.StatusOK, &r) + r = test.Request(t, http.MethodGet, "/v1/accounts/23", "") + test.AssertHTTPStatus(t, http.StatusBadRequest, &r) - r = test.Request(t, "PATCH", "/v1/accounts/-274", "") + /* + * PATCH + */ + r = test.Request(t, http.MethodPatch, "/v1/accounts/-274", "") test.AssertHTTPStatus(t, http.StatusBadRequest, &r) - r = test.Request(t, "PATCH", "/v1/accounts/stringRandom", "") + r = test.Request(t, http.MethodPatch, "/v1/accounts/stringRandom", "") test.AssertHTTPStatus(t, http.StatusBadRequest, &r) - r = test.Request(t, "DELETE", "/v1/accounts/-274", "") + /* + * DELETE + */ + r = test.Request(t, http.MethodDelete, "/v1/accounts/-274", "") test.AssertHTTPStatus(t, http.StatusBadRequest, &r) - r = test.Request(t, "DELETE", "/v1/accounts/stringRandom", "") + r = test.Request(t, http.MethodDelete, "/v1/accounts/stringRandom", "") test.AssertHTTPStatus(t, http.StatusBadRequest, &r) } func TestCreateAccount(t *testing.T) { - recorder := test.Request(t, "POST", "/v1/accounts", `{ "name": "New Account", "note": "More tests something something" }`) - test.AssertHTTPStatus(t, http.StatusCreated, &recorder) - - var apiAccount controllers.AccountResponse - test.DecodeResponse(t, &recorder, &apiAccount) + _ = createTestAccount(t, models.AccountCreate{Name: "Test account for creation"}) } func TestCreateBrokenAccount(t *testing.T) { - recorder := test.Request(t, "POST", "/v1/accounts", `{ "createdAt": "New Account", "note": "More tests for accounts to ensure less brokenness something" }`) + recorder := test.Request(t, http.MethodPost, "/v1/accounts", `{ "createdAt": "New Account", "note": "More tests for accounts to ensure less brokenness something" }`) test.AssertHTTPStatus(t, http.StatusBadRequest, &recorder) } func TestCreateAccountNoBody(t *testing.T) { - recorder := test.Request(t, "POST", "/v1/accounts", "") + recorder := test.Request(t, http.MethodPost, "/v1/accounts", "") test.AssertHTTPStatus(t, http.StatusBadRequest, &recorder) } func TestCreateAccountNoBudget(t *testing.T) { - recorder := test.Request(t, "POST", "/v1/accounts", `{ "budgetId": 5476 }`) + recorder := test.Request(t, http.MethodPost, "/v1/accounts", models.AccountCreate{BudgetID: uuid.New()}) test.AssertHTTPStatus(t, http.StatusNotFound, &recorder) } func TestGetAccount(t *testing.T) { - recorder := test.Request(t, "GET", "/v1/accounts/1", "") - test.AssertHTTPStatus(t, http.StatusOK, &recorder) - - var account controllers.AccountResponse - test.DecodeResponse(t, &recorder, &account) - - var dbAccount models.Account - models.DB.First(&dbAccount, account.Data.ID) + a := createTestAccount(t, models.AccountCreate{}) - // The test transactions have a sum of -30 - if !decimal.NewFromFloat(-30).Equals(account.Data.Balance) { - assert.Fail(t, "Account balance does not equal -30", account.Data.Balance) - } + r := test.Request(t, http.MethodGet, a.Data.Links.Self, "") + assert.Equal(t, http.StatusOK, r.Code) } func TestGetAccountTransactionsNonExistingAccount(t *testing.T) { - recorder := test.Request(t, "GET", "/v1/accounts/57372/transactions", "") - assert.Equal(t, 404, recorder.Code) + recorder := test.Request(t, http.MethodGet, "/v1/accounts/57372/transactions", "") + assert.Equal(t, http.StatusNotFound, recorder.Code) } func TestUpdateAccount(t *testing.T) { - recorder := test.Request(t, "POST", "/v1/accounts", `{ "name": "New Account", "note": "More tests something something" }`) - test.AssertHTTPStatus(t, http.StatusCreated, &recorder) - - var account controllers.AccountResponse - test.DecodeResponse(t, &recorder, &account) + a := createTestAccount(t, models.AccountCreate{Name: "Original name"}) - recorder = test.Request(t, "PATCH", account.Data.Links.Self, `{ "name": "Updated new account for testing" }`) - test.AssertHTTPStatus(t, http.StatusOK, &recorder) + r := test.Request(t, http.MethodPatch, a.Data.Links.Self, models.AccountCreate{Name: "Updated new account for testing"}) + test.AssertHTTPStatus(t, http.StatusOK, &r) - var updatedAccount controllers.AccountResponse - test.DecodeResponse(t, &recorder, &updatedAccount) + var u controllers.AccountResponse + test.DecodeResponse(t, &r, &u) - assert.Equal(t, "Updated new account for testing", updatedAccount.Data.Name) + assert.Equal(t, "Updated new account for testing", u.Data.Name) } func TestUpdateAccountBroken(t *testing.T) { - recorder := test.Request(t, "POST", "/v1/accounts", `{ "name": "New Account", "note": "More tests something something" }`) - test.AssertHTTPStatus(t, http.StatusCreated, &recorder) + a := createTestAccount(t, models.AccountCreate{ + Name: "New Account", + Note: "More tests something something", + }) - var account controllers.AccountResponse - test.DecodeResponse(t, &recorder, &account) - - recorder = test.Request(t, "PATCH", account.Data.Links.Self, `{ "name": 2" }`) - test.AssertHTTPStatus(t, http.StatusBadRequest, &recorder) + r := test.Request(t, http.MethodPatch, a.Data.Links.Self, `{ "name": 2" }`) + test.AssertHTTPStatus(t, http.StatusBadRequest, &r) } func TestUpdateNonExistingAccount(t *testing.T) { - recorder := test.Request(t, "PATCH", "/v1/accounts/48902805", `{ "name": "2" }`) + recorder := test.Request(t, http.MethodPatch, "/v1/accounts/9b81de41-eead-451d-bc6b-31fceedd236c", models.AccountCreate{Name: "This account does not exist"}) test.AssertHTTPStatus(t, http.StatusNotFound, &recorder) } func TestDeleteAccountsAndEmptyList(t *testing.T) { - recorder := test.Request(t, "DELETE", "/v1/accounts/1", "") - test.AssertHTTPStatus(t, http.StatusNoContent, &recorder) - - recorder = test.Request(t, "DELETE", "/v1/accounts/2", "") - test.AssertHTTPStatus(t, http.StatusNoContent, &recorder) - - recorder = test.Request(t, "DELETE", "/v1/accounts/3", "") - test.AssertHTTPStatus(t, http.StatusNoContent, &recorder) + r := test.Request(t, http.MethodGet, "/v1/accounts", "") - recorder = test.Request(t, "DELETE", "/v1/accounts/4", "") - test.AssertHTTPStatus(t, http.StatusNoContent, &recorder) + var l controllers.AccountListResponse + test.DecodeResponse(t, &r, &l) - recorder = test.Request(t, "DELETE", "/v1/accounts/5", "") - test.AssertHTTPStatus(t, http.StatusNoContent, &recorder) - - recorder = test.Request(t, "DELETE", "/v1/accounts/6", "") - test.AssertHTTPStatus(t, http.StatusNoContent, &recorder) + for _, a := range l.Data { + r = test.Request(t, http.MethodDelete, a.Links.Self, "") + test.AssertHTTPStatus(t, http.StatusNoContent, &r) + } - recorder = test.Request(t, "GET", "/v1/accounts", "") - var apiResponse controllers.AccountListResponse - test.DecodeResponse(t, &recorder, &apiResponse) + r = test.Request(t, http.MethodGet, "/v1/accounts", "") + test.DecodeResponse(t, &r, &l) // Verify that the account list is an empty list, not null - assert.NotNil(t, apiResponse.Data) - assert.Empty(t, apiResponse.Data) + assert.NotNil(t, l.Data) + assert.Empty(t, l.Data) } func TestDeleteNonExistingAccount(t *testing.T) { - recorder := test.Request(t, "DELETE", "/v1/accounts/48902805", "") + recorder := test.Request(t, http.MethodDelete, "/v1/accounts/77b70a75-4bb3-4d1d-90cf-5b7a61f452f5", "") test.AssertHTTPStatus(t, http.StatusNotFound, &recorder) } func TestDeleteAccountWithBody(t *testing.T) { - recorder := test.Request(t, "POST", "/v1/accounts", `{ "name": "Delete me now!" }`) - test.AssertHTTPStatus(t, http.StatusCreated, &recorder) + a := createTestAccount(t, models.AccountCreate{Name: "Delete me now!"}) - var account controllers.AccountResponse - test.DecodeResponse(t, &recorder, &account) + r := test.Request(t, http.MethodDelete, a.Data.Links.Self, models.AccountCreate{Name: "Some other account"}) + test.AssertHTTPStatus(t, http.StatusNoContent, &r) - recorder = test.Request(t, "DELETE", account.Data.Links.Self, `{ "name": "test name 23" }`) - test.AssertHTTPStatus(t, http.StatusNoContent, &recorder) + r = test.Request(t, http.MethodGet, a.Data.Links.Self, "") } diff --git a/internal/controllers/allocation.go b/internal/controllers/allocation.go index ccb1116c..c3685744 100644 --- a/internal/controllers/allocation.go +++ b/internal/controllers/allocation.go @@ -7,6 +7,7 @@ import ( "strings" "github.com/gin-contrib/requestid" + "github.com/google/uuid" "github.com/rs/zerolog/log" "github.com/envelope-zero/backend/internal/httputil" @@ -28,7 +29,7 @@ type Allocation struct { } type AllocationLinks struct { - Self string `json:"self" example:"https://example.com/api/v1/allocations/47"` + Self string `json:"self" example:"https://example.com/api/v1/allocations/902cd93c-3724-4e46-8540-d014131282fc"` } // RegisterAllocationRoutes registers the routes for allocations with @@ -159,12 +160,13 @@ func GetAllocations(c *gin.Context) { // @Param allocationId path uint64 true "ID of the allocation" // @Router /v1/allocations/{allocationId} [get] func GetAllocation(c *gin.Context) { - id, err := httputil.ParseID(c, "allocationId") + p, err := uuid.Parse(c.Param("allocationId")) if err != nil { + httputil.ErrorInvalidUUID(c) return } - allocationObject, err := getAllocationObject(c, id) + allocationObject, err := getAllocationObject(c, p) if err != nil { return } @@ -185,12 +187,13 @@ func GetAllocation(c *gin.Context) { // @Param allocation body models.AllocationCreate true "Allocation" // @Router /v1/allocations/{allocationId} [patch] func UpdateAllocation(c *gin.Context) { - id, err := httputil.ParseID(c, "allocationId") + p, err := uuid.Parse(c.Param("allocationId")) if err != nil { + httputil.ErrorInvalidUUID(c) return } - allocation, err := getAllocationResource(c, id) + allocation, err := getAllocationResource(c, p) if err != nil { return } @@ -216,12 +219,13 @@ func UpdateAllocation(c *gin.Context) { // @Param allocationId path uint64 true "ID of the allocation" // @Router /v1/allocations/{allocationId} [delete] func DeleteAllocation(c *gin.Context) { - id, err := httputil.ParseID(c, "allocationId") + p, err := uuid.Parse(c.Param("allocationId")) if err != nil { + httputil.ErrorInvalidUUID(c) return } - allocation, err := getAllocationResource(c, id) + allocation, err := getAllocationResource(c, p) if err != nil { return } @@ -232,7 +236,7 @@ func DeleteAllocation(c *gin.Context) { } // getAllocationResource verifies that the request URI is valid for the transaction and returns it. -func getAllocationResource(c *gin.Context, id uint64) (models.Allocation, error) { +func getAllocationResource(c *gin.Context, id uuid.UUID) (models.Allocation, error) { var allocation models.Allocation err := models.DB.First(&allocation, &models.Allocation{ @@ -248,7 +252,7 @@ func getAllocationResource(c *gin.Context, id uint64) (models.Allocation, error) return allocation, nil } -func getAllocationObject(c *gin.Context, id uint64) (Allocation, error) { +func getAllocationObject(c *gin.Context, id uuid.UUID) (Allocation, error) { resource, err := getAllocationResource(c, id) if err != nil { return Allocation{}, err @@ -264,8 +268,8 @@ func getAllocationObject(c *gin.Context, id uint64) (Allocation, error) { // // This function is only needed for getAllocationObject as we cannot create an instance of Allocation // with mixed named and unnamed parameters. -func getAllocationLinks(c *gin.Context, id uint64) AllocationLinks { - url := httputil.RequestPathV1(c) + fmt.Sprintf("/allocations/%d", id) +func getAllocationLinks(c *gin.Context, id uuid.UUID) AllocationLinks { + url := httputil.RequestPathV1(c) + fmt.Sprintf("/allocations/%s", id) return AllocationLinks{ Self: url, diff --git a/internal/controllers/allocation_test.go b/internal/controllers/allocation_test.go index 15464246..f6e05ad8 100644 --- a/internal/controllers/allocation_test.go +++ b/internal/controllers/allocation_test.go @@ -1,7 +1,6 @@ package controllers_test import ( - "fmt" "net/http" "testing" "time" @@ -9,10 +8,21 @@ import ( "github.com/envelope-zero/backend/internal/controllers" "github.com/envelope-zero/backend/internal/models" "github.com/envelope-zero/backend/internal/test" + "github.com/google/uuid" "github.com/shopspring/decimal" "github.com/stretchr/testify/assert" ) +func createTestAllocation(t *testing.T, c models.AllocationCreate) controllers.AllocationResponse { + r := test.Request(t, "POST", "/v1/allocations", c) + test.AssertHTTPStatus(t, http.StatusCreated, &r) + + var a controllers.AllocationResponse + test.DecodeResponse(t, &r, &a) + + return a +} + func TestGetAllocations(t *testing.T) { recorder := test.Request(t, "GET", "/v1/allocations", "") @@ -24,7 +34,6 @@ func TestGetAllocations(t *testing.T) { assert.FailNow(t, "Response does not have exactly 3 items") } - assert.Equal(t, uint64(1), response.Data[0].EnvelopeID) assert.Equal(t, uint8(1), response.Data[0].Month) assert.Equal(t, uint(2022), response.Data[0].Year) @@ -37,39 +46,52 @@ func TestGetAllocations(t *testing.T) { } func TestNoAllocationNotFound(t *testing.T) { - recorder := test.Request(t, "GET", "/v1/allocations/60", "") + recorder := test.Request(t, "GET", "/v1/allocations/f8b93ce2-309f-4e99-8886-6ab960df99c3", "") test.AssertHTTPStatus(t, http.StatusNotFound, &recorder) } -// TestAllocationInvalidIDs verifies that on non-number requests for allocation IDs, -// the API returs a Bad Request status code. func TestAllocationInvalidIDs(t *testing.T) { - r := test.Request(t, "GET", "/v1/allocations/-2", "") + /* + * GET + */ + r := test.Request(t, http.MethodGet, "/v1/allocations/-56", "") test.AssertHTTPStatus(t, http.StatusBadRequest, &r) - r = test.Request(t, "GET", "/v1/allocations/RoadWorkAhead", "") + r = test.Request(t, http.MethodGet, "/v1/allocations/notANumber", "") test.AssertHTTPStatus(t, http.StatusBadRequest, &r) - r = test.Request(t, "PATCH", "/v1/allocations/SneezingBecauseAllergies", "") + r = test.Request(t, http.MethodGet, "/v1/allocations/23", "") test.AssertHTTPStatus(t, http.StatusBadRequest, &r) - r = test.Request(t, "DELETE", "/v1/allocations/;!", "") + /* + * PATCH + */ + r = test.Request(t, http.MethodPatch, "/v1/allocations/-274", "") test.AssertHTTPStatus(t, http.StatusBadRequest, &r) -} -func TestCreateAllocation(t *testing.T) { - recorder := test.Request(t, "POST", "/v1/allocations", `{ "month": 10, "year": 2022, "amount": 15.42 }`) - test.AssertHTTPStatus(t, http.StatusCreated, &recorder) + r = test.Request(t, http.MethodPatch, "/v1/allocations/stringRandom", "") + test.AssertHTTPStatus(t, http.StatusBadRequest, &r) - var apiAllocation controllers.AllocationResponse - test.DecodeResponse(t, &recorder, &apiAllocation) + /* + * DELETE + */ + r = test.Request(t, http.MethodDelete, "/v1/allocations/-274", "") + test.AssertHTTPStatus(t, http.StatusBadRequest, &r) - var dbAllocation models.Allocation - models.DB.First(&dbAllocation, apiAllocation.Data.ID) + r = test.Request(t, http.MethodDelete, "/v1/allocations/stringRandom", "") + test.AssertHTTPStatus(t, http.StatusBadRequest, &r) +} - if !decimal.NewFromFloat(15.42).Equal(apiAllocation.Data.Amount) { - assert.Fail(t, "Allocation amount does not equal 15.42", apiAllocation.Data.Amount) +func TestCreateAllocation(t *testing.T) { + a := createTestAllocation(t, models.AllocationCreate{ + Month: 10, + Year: 2022, + Amount: decimal.NewFromFloat(15.42), + }) + + if !decimal.NewFromFloat(15.42).Equal(a.Data.Amount) { + assert.Fail(t, "Allocation amount does not equal 15.42", a.Data.Amount) } } @@ -79,17 +101,17 @@ func TestCreateBrokenAllocation(t *testing.T) { } func TestCreateAllocationNonExistingEnvelope(t *testing.T) { - recorder := test.Request(t, "POST", "/v1/allocations", `{ "envelopeId": 2581 }`) + recorder := test.Request(t, "POST", "/v1/allocations", models.AllocationCreate{EnvelopeID: uuid.New()}) test.AssertHTTPStatus(t, http.StatusNotFound, &recorder) } func TestCreateDuplicateAllocation(t *testing.T) { - recorder := test.Request(t, "POST", "/v1/allocations", `{ "year": 2022, "month": 2 }`) + recorder := test.Request(t, "POST", "/v1/allocations", models.AllocationCreate{Year: 2022, Month: 2}) test.AssertHTTPStatus(t, http.StatusBadRequest, &recorder) } func TestCreateAllocationNoMonth(t *testing.T) { - recorder := test.Request(t, "POST", "/v1/allocations", `{ "year": 2022, "month": 17 }`) + recorder := test.Request(t, "POST", "/v1/allocations", models.AllocationCreate{Year: 2022, Month: 17}) test.AssertHTTPStatus(t, http.StatusBadRequest, &recorder) } @@ -99,78 +121,54 @@ func TestCreateAllocationNoBody(t *testing.T) { } func TestGetAllocation(t *testing.T) { - recorder := test.Request(t, "GET", "/v1/allocations/1", "") - test.AssertHTTPStatus(t, http.StatusOK, &recorder) - - var allocationObject, savedAllocation controllers.AllocationResponse - test.DecodeResponse(t, &recorder, &allocationObject) - - recorder = test.Request(t, "GET", allocationObject.Data.Links.Self, "") - test.DecodeResponse(t, &recorder, &savedAllocation) + a := createTestAllocation(t, models.AllocationCreate{ + Year: 2022, + Month: 8, + }) - assert.Equal(t, savedAllocation, allocationObject) + r := test.Request(t, http.MethodGet, a.Data.Links.Self, "") + assert.Equal(t, http.StatusOK, r.Code) } func TestUpdateAllocation(t *testing.T) { - recorder := test.Request(t, "POST", "/v1/allocations", `{ "year": 2100, "month": 6 }`) - test.AssertHTTPStatus(t, http.StatusCreated, &recorder) + a := createTestAllocation(t, models.AllocationCreate{Year: 2100, Month: 6}) - var allocation controllers.AllocationResponse - test.DecodeResponse(t, &recorder, &allocation) - - path := fmt.Sprintf("/v1/allocations/%v", allocation.Data.ID) - recorder = test.Request(t, "PATCH", path, `{ "year": 2022 }`) - test.AssertHTTPStatus(t, http.StatusOK, &recorder) + r := test.Request(t, "PATCH", a.Data.Links.Self, models.AllocationCreate{Year: 2022}) + test.AssertHTTPStatus(t, http.StatusOK, &r) var updatedAllocation controllers.AllocationResponse - test.DecodeResponse(t, &recorder, &updatedAllocation) + test.DecodeResponse(t, &r, &updatedAllocation) assert.Equal(t, uint(2022), updatedAllocation.Data.Year) } func TestUpdateAllocationBroken(t *testing.T) { - recorder := test.Request(t, "POST", "/v1/allocations", `{ "year": 2017, "month": 11 }`) - test.AssertHTTPStatus(t, http.StatusCreated, &recorder) - - var allocation controllers.AllocationResponse - test.DecodeResponse(t, &recorder, &allocation) + a := createTestAllocation(t, models.AllocationCreate{Year: 2100, Month: 6}) - path := fmt.Sprintf("/v1/allocations/%v", allocation.Data.ID) - recorder = test.Request(t, "PATCH", path, `{ "name": 2" }`) - test.AssertHTTPStatus(t, http.StatusBadRequest, &recorder) + r := test.Request(t, "PATCH", a.Data.Links.Self, `{ "name": 2" }`) + test.AssertHTTPStatus(t, http.StatusBadRequest, &r) } func TestUpdateNonExistingAllocation(t *testing.T) { - recorder := test.Request(t, "PATCH", "/v1/allocations/48902805", `{ "name": "2" }`) + recorder := test.Request(t, "PATCH", "/v1/allocations/df684988-31df-444c-8aaa-b53195d55d6e", models.AllocationCreate{Month: 2}) test.AssertHTTPStatus(t, http.StatusNotFound, &recorder) } func TestDeleteAllocation(t *testing.T) { - recorder := test.Request(t, "POST", "/v1/allocations", `{ "year": 2033, "month": 11 }`) - test.AssertHTTPStatus(t, http.StatusCreated, &recorder) - - var allocation controllers.AllocationResponse - test.DecodeResponse(t, &recorder, &allocation) + a := createTestAllocation(t, models.AllocationCreate{Year: 2033, Month: 11}) + r := test.Request(t, "DELETE", a.Data.Links.Self, "") - path := fmt.Sprintf("/v1/allocations/%v", allocation.Data.ID) - recorder = test.Request(t, "DELETE", path, "") - - test.AssertHTTPStatus(t, http.StatusNoContent, &recorder) + test.AssertHTTPStatus(t, http.StatusNoContent, &r) } func TestDeleteNonExistingAllocation(t *testing.T) { - recorder := test.Request(t, "DELETE", "/v1/allocations/48902805", "") + recorder := test.Request(t, "DELETE", "/v1/allocations/34ac51a7-431c-454b-ba29-feaefeae70d5", "") test.AssertHTTPStatus(t, http.StatusNotFound, &recorder) } func TestDeleteAllocationWithBody(t *testing.T) { - recorder := test.Request(t, "POST", "/v1/allocations", `{ "year": 2070, "month": 12}`) - test.AssertHTTPStatus(t, http.StatusCreated, &recorder) - - var allocation controllers.AllocationResponse - test.DecodeResponse(t, &recorder, &allocation) + a := createTestAllocation(t, models.AllocationCreate{Year: 2070, Month: 12}) - path := fmt.Sprintf("/v1/allocations/%v", allocation.Data.ID) - recorder = test.Request(t, "DELETE", path, `{ "name": "test name 23" }`) - test.AssertHTTPStatus(t, http.StatusNoContent, &recorder) + r := test.Request(t, "DELETE", a.Data.Links.Self, models.AllocationCreate{Year: 2011, Month: 3}) + test.AssertHTTPStatus(t, http.StatusNoContent, &r) } diff --git a/internal/controllers/budget.go b/internal/controllers/budget.go index dd091004..e510f17b 100644 --- a/internal/controllers/budget.go +++ b/internal/controllers/budget.go @@ -9,6 +9,7 @@ import ( "github.com/envelope-zero/backend/internal/httputil" "github.com/envelope-zero/backend/internal/models" "github.com/gin-gonic/gin" + "github.com/google/uuid" ) type BudgetListResponse struct { @@ -25,11 +26,11 @@ type Budget struct { } type BudgetLinks struct { - Self string `json:"self" example:"https://example.com/api/v1/budgets/4"` - Accounts string `json:"accounts" example:"https://example.com/api/v1/accounts?budget=2"` - Categories string `json:"categories" example:"https://example.com/api/v1/categories?budget=2"` - Transactions string `json:"transactions" example:"https://example.com/api/v1/budgets/2/transactions"` - Month string `json:"month" example:"https://example.com/api/v1/budgets/2/2022-03"` + Self string `json:"self" example:"https://example.com/api/v1/budgets/550dc009-cea6-4c12-b2a5-03446eb7b7cf"` + Accounts string `json:"accounts" example:"https://example.com/api/v1/accounts?budget=550dc009-cea6-4c12-b2a5-03446eb7b7cf"` + Categories string `json:"categories" example:"https://example.com/api/v1/categories?budget=550dc009-cea6-4c12-b2a5-03446eb7b7cf"` + Transactions string `json:"transactions" example:"https://example.com/api/v1/transactions?budget=550dc009-cea6-4c12-b2a5-03446eb7b7cf"` + Month string `json:"month" example:"https://example.com/api/v1/budgets/550dc009-cea6-4c12-b2a5-03446eb7b7cf/2022-03"` } type BudgetMonthResponse struct { @@ -137,12 +138,13 @@ func GetBudgets(c *gin.Context) { // @Param budgetId path uint64 true "ID of the budget" // @Router /v1/budgets/{budgetId} [get] func GetBudget(c *gin.Context) { - id, err := httputil.ParseID(c, "budgetId") + p, err := uuid.Parse(c.Param("budgetId")) if err != nil { + httputil.ErrorInvalidUUID(c) return } - budgetObject, err := getBudgetObject(c, id) + budgetObject, err := getBudgetObject(c, p) if err != nil { return } @@ -162,12 +164,13 @@ func GetBudget(c *gin.Context) { // @Param month path string true "The month in YYYY-MM format" // @Router /v1/budgets/{budgetId}/{month} [get] func GetBudgetMonth(c *gin.Context) { - id, err := httputil.ParseID(c, "budgetId") + p, err := uuid.Parse(c.Param("budgetId")) if err != nil { + httputil.ErrorInvalidUUID(c) return } - budget, err := getBudgetResource(c, id) + budget, err := getBudgetResource(c, p) if err != nil { return } @@ -225,12 +228,13 @@ func GetBudgetMonth(c *gin.Context) { // @Param budget body models.BudgetCreate true "Budget" // @Router /v1/budgets/{budgetId} [patch] func UpdateBudget(c *gin.Context) { - id, err := httputil.ParseID(c, "budgetId") + p, err := uuid.Parse(c.Param("budgetId")) if err != nil { + httputil.ErrorInvalidUUID(c) return } - budget, err := getBudgetResource(c, id) + budget, err := getBudgetResource(c, p) if err != nil { return } @@ -255,12 +259,13 @@ func UpdateBudget(c *gin.Context) { // @Param budgetId path uint64 true "ID of the budget" // @Router /v1/budgets/{budgetId} [delete] func DeleteBudget(c *gin.Context) { - id, err := httputil.ParseID(c, "budgetId") + p, err := uuid.Parse(c.Param("budgetId")) if err != nil { + httputil.ErrorInvalidUUID(c) return } - budget, err := getBudgetResource(c, id) + budget, err := getBudgetResource(c, p) if err != nil { return } @@ -271,7 +276,7 @@ func DeleteBudget(c *gin.Context) { } // getBudgetResource is the internal helper to verify permissions and return a budget. -func getBudgetResource(c *gin.Context, id uint64) (models.Budget, error) { +func getBudgetResource(c *gin.Context, id uuid.UUID) (models.Budget, error) { var budget models.Budget err := models.DB.Where(&models.Budget{ @@ -287,7 +292,7 @@ func getBudgetResource(c *gin.Context, id uint64) (models.Budget, error) { return budget, nil } -func getBudgetObject(c *gin.Context, id uint64) (Budget, error) { +func getBudgetObject(c *gin.Context, id uuid.UUID) (Budget, error) { resource, err := getBudgetResource(c, id) if err != nil { return Budget{}, err @@ -303,14 +308,14 @@ func getBudgetObject(c *gin.Context, id uint64) (Budget, error) { // // This function is only needed for getBudgetObject as we cannot create an instance of Budget // with mixed named and unnamed parameters. -func getBudgetLinks(c *gin.Context, id uint64) BudgetLinks { - url := httputil.RequestPathV1(c) + fmt.Sprintf("/budgets/%d", id) +func getBudgetLinks(c *gin.Context, id uuid.UUID) BudgetLinks { + url := httputil.RequestPathV1(c) + fmt.Sprintf("/budgets/%s", id) return BudgetLinks{ Self: url, - Accounts: httputil.RequestPathV1(c) + fmt.Sprintf("/accounts?budget=%d", id), - Categories: httputil.RequestPathV1(c) + fmt.Sprintf("/categories?budget=%d", id), - Transactions: httputil.RequestPathV1(c) + fmt.Sprintf("/transactions?budget=%d", id), + Accounts: httputil.RequestPathV1(c) + fmt.Sprintf("/accounts?budget=%s", id), + Categories: httputil.RequestPathV1(c) + fmt.Sprintf("/categories?budget=%s", id), + Transactions: httputil.RequestPathV1(c) + fmt.Sprintf("/transactions?budget=%s", id), Month: url + "/YYYY-MM", } } diff --git a/internal/controllers/budget_test.go b/internal/controllers/budget_test.go index 27bdf0aa..7f3831be 100644 --- a/internal/controllers/budget_test.go +++ b/internal/controllers/budget_test.go @@ -1,6 +1,7 @@ package controllers_test import ( + "fmt" "net/http" "testing" "time" @@ -34,30 +35,43 @@ func TestGetBudgets(t *testing.T) { } func TestNoBudgetNotFound(t *testing.T) { - recorder := test.Request(t, "GET", "/v1/budgets/2", "") + recorder := test.Request(t, "GET", "/v1/budgets/65064e6f-04b4-46e0-8bbc-88c96c6b21bd", "") test.AssertHTTPStatus(t, http.StatusNotFound, &recorder) } -// TestBudgetInvalidIDs verifies that on non-number requests for budget IDs, -// the API returs a Bad Request status code. func TestBudgetInvalidIDs(t *testing.T) { - r := test.Request(t, "GET", "/v1/budgets/-17", "") + /* + * GET + */ + r := test.Request(t, http.MethodGet, "/v1/budgets/-56", "") test.AssertHTTPStatus(t, http.StatusBadRequest, &r) - r = test.Request(t, "GET", "/v1/budgets/DefinitelyNotAUint64", "") + r = test.Request(t, http.MethodGet, "/v1/budgets/notANumber", "") test.AssertHTTPStatus(t, http.StatusBadRequest, &r) - r = test.Request(t, "GET", "/v1/budgets/DefinitelyNotAUint64/2022-07", "") + r = test.Request(t, http.MethodGet, "/v1/budgets/23", "") test.AssertHTTPStatus(t, http.StatusBadRequest, &r) - r = test.Request(t, "GET", "/v1/budgets/-17/1969-07", "") + r = test.Request(t, http.MethodGet, "/v1/budgets/d19a622f-broken-uuid/2022-01", "") test.AssertHTTPStatus(t, http.StatusBadRequest, &r) - r = test.Request(t, "PATCH", "/v1/budgets/-17", "") + /* + * PATCH + */ + r = test.Request(t, http.MethodPatch, "/v1/budgets/-274", "") test.AssertHTTPStatus(t, http.StatusBadRequest, &r) - r = test.Request(t, "DELETE", "/v1/budgets/-17", "") + r = test.Request(t, http.MethodPatch, "/v1/budgets/stringRandom", "") + test.AssertHTTPStatus(t, http.StatusBadRequest, &r) + + /* + * DELETE + */ + r = test.Request(t, http.MethodDelete, "/v1/budgets/-274", "") + test.AssertHTTPStatus(t, http.StatusBadRequest, &r) + + r = test.Request(t, http.MethodDelete, "/v1/budgets/stringRandom", "") test.AssertHTTPStatus(t, http.StatusBadRequest, &r) } @@ -86,6 +100,10 @@ func TestCreateBudgetNoBody(t *testing.T) { // TestBudgetMonth verifies that the monthly calculations are correct. func TestBudgetMonth(t *testing.T) { + var budgetList controllers.BudgetListResponse + r := test.Request(t, http.MethodGet, "/v1/budgets", "") + test.DecodeResponse(t, &r, &budgetList) + var budgetMonth controllers.BudgetMonthResponse tests := []struct { @@ -93,13 +111,12 @@ func TestBudgetMonth(t *testing.T) { response controllers.BudgetMonthResponse }{ { - "/v1/budgets/1/2022-01", + fmt.Sprintf("/v1/budgets/%s/2022-01", budgetList.Data[0].ID), controllers.BudgetMonthResponse{ Data: models.BudgetMonth{ Month: time.Date(2022, 1, 1, 0, 0, 0, 0, time.UTC), Envelopes: []models.EnvelopeMonth{ { - ID: 1, Name: "Utilities", Month: time.Date(2022, 1, 1, 0, 0, 0, 0, time.UTC), Spent: decimal.NewFromFloat(-10), @@ -111,13 +128,12 @@ func TestBudgetMonth(t *testing.T) { }, }, { - "/v1/budgets/1/2022-02", + fmt.Sprintf("/v1/budgets/%s/2022-02", budgetList.Data[0].ID), controllers.BudgetMonthResponse{ Data: models.BudgetMonth{ Month: time.Date(2022, 2, 1, 0, 0, 0, 0, time.UTC), Envelopes: []models.EnvelopeMonth{ { - ID: 1, Name: "Utilities", Month: time.Date(2022, 2, 1, 0, 0, 0, 0, time.UTC), Spent: decimal.NewFromFloat(-5), @@ -129,13 +145,12 @@ func TestBudgetMonth(t *testing.T) { }, }, { - "/v1/budgets/1/2022-03", + fmt.Sprintf("/v1/budgets/%s/2022-03", budgetList.Data[0].ID), controllers.BudgetMonthResponse{ Data: models.BudgetMonth{ Month: time.Date(2022, 3, 1, 0, 0, 0, 0, time.UTC), Envelopes: []models.EnvelopeMonth{ { - ID: 1, Name: "Utilities", Month: time.Date(2022, 3, 1, 0, 0, 0, 0, time.UTC), Spent: decimal.NewFromFloat(-15), @@ -165,21 +180,29 @@ func TestBudgetMonth(t *testing.T) { } } -// TestBudgetMonthInvalid verifies that non-parseable requests return a HTTP 400 Bad Request. -func TestBudgetMonthInvalid(t *testing.T) { - r := test.Request(t, "GET", "/v1/budgets/1/Stonks!", "") - test.AssertHTTPStatus(t, http.StatusBadRequest, &r) -} - // TestBudgetMonthNonExistent verifies that month requests for non-existing budgets return a HTTP 404 Not Found. func TestBudgetMonthNonExistent(t *testing.T) { - r := test.Request(t, "GET", "/v1/budgets/831/2022-01", "") + r := test.Request(t, "GET", "/v1/budgets/65064e6f-04b4-46e0-8bbc-88c96c6b21bd/2022-01", "") test.AssertHTTPStatus(t, http.StatusNotFound, &r) } // TestBudgetMonthZero tests that we return a HTTP Bad Request when requesting data for the zero timestamp. func TestBudgetMonthZero(t *testing.T) { - r := test.Request(t, "GET", "/v1/budgets/1/0001-01", "") + var budgetList controllers.BudgetListResponse + r := test.Request(t, http.MethodGet, "/v1/budgets", "") + test.DecodeResponse(t, &r, &budgetList) + + r = test.Request(t, "GET", fmt.Sprintf("/v1/budgets/%s/0001-01", budgetList.Data[0].ID), "") + test.AssertHTTPStatus(t, http.StatusBadRequest, &r) +} + +// TestBudgetMonthInvalid tests that we return a HTTP Bad Request when requesting data for the zero timestamp. +func TestBudgetMonthInvalid(t *testing.T) { + var budgetList controllers.BudgetListResponse + r := test.Request(t, http.MethodGet, "/v1/budgets", "") + test.DecodeResponse(t, &r, &budgetList) + + r = test.Request(t, "GET", fmt.Sprintf("/v1/budgets/%s/December-2020", budgetList.Data[0].ID), "") test.AssertHTTPStatus(t, http.StatusBadRequest, &r) } @@ -212,7 +235,7 @@ func TestUpdateBudgetBroken(t *testing.T) { } func TestUpdateNonExistingBudget(t *testing.T) { - recorder := test.Request(t, "PATCH", "/v1/budgets/48902805", `{ "name": "2" }`) + recorder := test.Request(t, "PATCH", "/v1/budgets/a29bd123-beec-47de-a9cd-b6f7483fe00f", `{ "name": "2" }`) test.AssertHTTPStatus(t, http.StatusNotFound, &recorder) } @@ -228,7 +251,7 @@ func TestDeleteBudget(t *testing.T) { } func TestDeleteNonExistingBudget(t *testing.T) { - recorder := test.Request(t, "DELETE", "/v1/budgets/48902805", "") + recorder := test.Request(t, "DELETE", "/v1/budgets/c3d34346-609a-4734-9364-98f5b0100150", "") test.AssertHTTPStatus(t, http.StatusNotFound, &recorder) } diff --git a/internal/controllers/category.go b/internal/controllers/category.go index 83a7c122..bc23249a 100644 --- a/internal/controllers/category.go +++ b/internal/controllers/category.go @@ -7,6 +7,7 @@ import ( "github.com/envelope-zero/backend/internal/httputil" "github.com/envelope-zero/backend/internal/models" "github.com/gin-gonic/gin" + "github.com/google/uuid" ) type CategoryListResponse struct { @@ -23,8 +24,8 @@ type Category struct { } type CategoryLinks struct { - Self string `json:"self" example:"https://example.com/api/v1/categories/7"` - Envelopes string `json:"envelopes" example:"https://example.com/api/v1/envelopes?category=7"` + Self string `json:"self" example:"https://example.com/api/v1/categories/3b1ea324-d438-4419-882a-2fc91d71772f"` + Envelopes string `json:"envelopes" example:"https://example.com/api/v1/envelopes?category=3b1ea324-d438-4419-882a-2fc91d71772f"` } // RegisterCategoryRoutes registers the routes for categories with @@ -136,12 +137,13 @@ func GetCategories(c *gin.Context) { // @Param categoryId path uint64 true "ID of the category" // @Router /v1/categories/{categoryId} [get] func GetCategory(c *gin.Context) { - id, err := httputil.ParseID(c, "categoryId") + p, err := uuid.Parse(c.Param("categoryId")) if err != nil { + httputil.ErrorInvalidUUID(c) return } - categoryObject, err := getCategoryObject(c, id) + categoryObject, err := getCategoryObject(c, p) if err != nil { return } @@ -162,12 +164,13 @@ func GetCategory(c *gin.Context) { // @Param category body models.CategoryCreate true "Category" // @Router /v1/categories/{categoryId} [patch] func UpdateCategory(c *gin.Context) { - id, err := httputil.ParseID(c, "categoryId") + p, err := uuid.Parse(c.Param("categoryId")) if err != nil { + httputil.ErrorInvalidUUID(c) return } - category, err := getCategoryResource(c, id) + category, err := getCategoryResource(c, p) if err != nil { return } @@ -192,12 +195,13 @@ func UpdateCategory(c *gin.Context) { // @Param categoryId path uint64 true "ID of the category" // @Router /v1/categories/{categoryId} [delete] func DeleteCategory(c *gin.Context) { - id, err := httputil.ParseID(c, "categoryId") + p, err := uuid.Parse(c.Param("categoryId")) if err != nil { + httputil.ErrorInvalidUUID(c) return } - category, err := getCategoryResource(c, id) + category, err := getCategoryResource(c, p) if err != nil { return } @@ -207,7 +211,7 @@ func DeleteCategory(c *gin.Context) { c.JSON(http.StatusNoContent, gin.H{}) } -func getCategoryResource(c *gin.Context, id uint64) (models.Category, error) { +func getCategoryResource(c *gin.Context, id uuid.UUID) (models.Category, error) { var category models.Category err := models.DB.Where(&models.Category{ @@ -224,7 +228,7 @@ func getCategoryResource(c *gin.Context, id uint64) (models.Category, error) { } // getCategoryResources returns all categories for the requested budget. -func getCategoryResources(c *gin.Context, id uint64) ([]models.Category, error) { +func getCategoryResources(c *gin.Context, id uuid.UUID) ([]models.Category, error) { var categories []models.Category models.DB.Where(&models.Category{ @@ -236,7 +240,7 @@ func getCategoryResources(c *gin.Context, id uint64) ([]models.Category, error) return categories, nil } -func getCategoryObject(c *gin.Context, id uint64) (Category, error) { +func getCategoryObject(c *gin.Context, id uuid.UUID) (Category, error) { resource, err := getCategoryResource(c, id) if err != nil { return Category{}, err @@ -252,11 +256,11 @@ func getCategoryObject(c *gin.Context, id uint64) (Category, error) { // // This function is only needed for getCategoryObject as we cannot create an instance of Category // with mixed named and unnamed parameters. -func getCategoryLinks(c *gin.Context, id uint64) CategoryLinks { - url := httputil.RequestPathV1(c) + fmt.Sprintf("/categories/%d", id) +func getCategoryLinks(c *gin.Context, id uuid.UUID) CategoryLinks { + url := httputil.RequestPathV1(c) + fmt.Sprintf("/categories/%s", id) return CategoryLinks{ Self: url, - Envelopes: httputil.RequestPathV1(c) + fmt.Sprintf("/envelopes?category=%d", id), + Envelopes: httputil.RequestPathV1(c) + fmt.Sprintf("/envelopes?category=%s", id), } } diff --git a/internal/controllers/category_test.go b/internal/controllers/category_test.go index e7daebc6..52e0e3eb 100644 --- a/internal/controllers/category_test.go +++ b/internal/controllers/category_test.go @@ -21,7 +21,6 @@ func TestGetCategories(t *testing.T) { assert.FailNow(t, "Response does not have exactly 1 item") } - assert.Equal(t, uint64(1), response.Data[0].BudgetID) assert.Equal(t, "Running costs", response.Data[0].Name) assert.Equal(t, "For e.g. groceries and energy bills", response.Data[0].Note) @@ -33,24 +32,40 @@ func TestGetCategories(t *testing.T) { } func TestNoCategoryNotFound(t *testing.T) { - recorder := test.Request(t, "GET", "/v1/categories/2", "") + recorder := test.Request(t, "GET", "/v1/categories/4e743e94-6a4b-44d6-aba5-d77c87103ff7", "") test.AssertHTTPStatus(t, http.StatusNotFound, &recorder) } -// TestCategoryInvalidIDs verifies that on non-number requests for category IDs, -// the API returs a Bad Request status code. func TestCategoryInvalidIDs(t *testing.T) { - r := test.Request(t, "GET", "/v1/categories/-557", "") + /* + * GET + */ + r := test.Request(t, http.MethodGet, "/v1/categories/-56", "") test.AssertHTTPStatus(t, http.StatusBadRequest, &r) - r = test.Request(t, "GET", "/v1/categories/NFTsAreAScam", "") + r = test.Request(t, http.MethodGet, "/v1/categories/notANumber", "") test.AssertHTTPStatus(t, http.StatusBadRequest, &r) - r = test.Request(t, "PATCH", "/v1/categories/CatsAreCute", "") + r = test.Request(t, http.MethodGet, "/v1/categories/23", "") test.AssertHTTPStatus(t, http.StatusBadRequest, &r) - r = test.Request(t, "DELETE", "/v1/categories/-!", "") + /* + * PATCH + */ + r = test.Request(t, http.MethodPatch, "/v1/categories/-274", "") + test.AssertHTTPStatus(t, http.StatusBadRequest, &r) + + r = test.Request(t, http.MethodPatch, "/v1/categories/stringRandom", "") + test.AssertHTTPStatus(t, http.StatusBadRequest, &r) + + /* + * DELETE + */ + r = test.Request(t, http.MethodDelete, "/v1/categories/-274", "") + test.AssertHTTPStatus(t, http.StatusBadRequest, &r) + + r = test.Request(t, http.MethodDelete, "/v1/categories/stringRandom", "") test.AssertHTTPStatus(t, http.StatusBadRequest, &r) } @@ -73,7 +88,7 @@ func TestCreateBrokenCategory(t *testing.T) { } func TestCreateBrokenNoBudget(t *testing.T) { - recorder := test.Request(t, "POST", "/v1/categories", `{ "budgetId": 674 }`) + recorder := test.Request(t, "POST", "/v1/categories", `{ "budgetId": "f8c74664-9b79-4e15-8d3d-4618f3e3c230" }`) test.AssertHTTPStatus(t, http.StatusNotFound, &recorder) } @@ -111,7 +126,7 @@ func TestUpdateCategoryBroken(t *testing.T) { } func TestUpdateNonExistingCategory(t *testing.T) { - recorder := test.Request(t, "PATCH", "/v1/categories/48902805", `{ "name": "2" }`) + recorder := test.Request(t, "PATCH", "/v1/categories/f9288848-517e-4b8c-9f14-b3d849ca275b", `{ "name": "2" }`) test.AssertHTTPStatus(t, http.StatusNotFound, &recorder) } @@ -127,7 +142,7 @@ func TestDeleteCategory(t *testing.T) { } func TestDeleteNonExistingCategory(t *testing.T) { - recorder := test.Request(t, "DELETE", "/v1/categories/48902805", "") + recorder := test.Request(t, "DELETE", "/v1/categories/a2aa0569-5ac5-42e1-8563-7c61194cc7d9", "") test.AssertHTTPStatus(t, http.StatusNotFound, &recorder) } diff --git a/internal/controllers/envelope.go b/internal/controllers/envelope.go index 7987d4c5..b7c70b2a 100644 --- a/internal/controllers/envelope.go +++ b/internal/controllers/envelope.go @@ -8,6 +8,7 @@ import ( "github.com/envelope-zero/backend/internal/httputil" "github.com/envelope-zero/backend/internal/models" "github.com/gin-gonic/gin" + "github.com/google/uuid" ) type EnvelopeListResponse struct { @@ -28,9 +29,9 @@ type EnvelopeMonthResponse struct { } type EnvelopeLinks struct { - Self string `json:"self" example:"https://example.com/api/v1/envelopes/87"` - Allocations string `json:"allocations" example:"https://example.com/api/v1/allocations?envelope=87"` - Month string `json:"month" example:"https://example.com/api/v1/envelopes/87/2019-03"` + Self string `json:"self" example:"https://example.com/api/v1/envelopes/45b6b5b9-f746-4ae9-b77b-7688b91f8166"` + Allocations string `json:"allocations" example:"https://example.com/api/v1/allocations?envelope=45b6b5b9-f746-4ae9-b77b-7688b91f8166"` + Month string `json:"month" example:"https://example.com/api/v1/envelopes/45b6b5b9-f746-4ae9-b77b-7688b91f8166/2019-03"` } // RegisterEnvelopeRoutes registers the routes for envelopes with @@ -139,12 +140,13 @@ func GetEnvelopes(c *gin.Context) { // @Param envelopeId path uint64 true "ID of the envelope" // @Router /v1/envelopes/{envelopeId} [get] func GetEnvelope(c *gin.Context) { - id, err := httputil.ParseID(c, "envelopeId") + p, err := uuid.Parse(c.Param("envelopeId")) if err != nil { + httputil.ErrorInvalidUUID(c) return } - envelopeObject, err := getEnvelopeObject(c, id) + envelopeObject, err := getEnvelopeObject(c, p) if err != nil { return } @@ -164,17 +166,18 @@ func GetEnvelope(c *gin.Context) { // @Param month path string true "The month in YYYY-MM format" // @Router /v1/envelopes/{envelopeId}/{month} [get] func GetEnvelopeMonth(c *gin.Context) { - var month URIMonth - if err := c.BindUri(&month); err != nil { + p, err := uuid.Parse(c.Param("envelopeId")) + if err != nil { + httputil.ErrorInvalidUUID(c) return } - id, err := httputil.ParseID(c, "envelopeId") - if err != nil { + var month URIMonth + if err := c.BindUri(&month); err != nil { return } - envelope, _ := getEnvelopeResource(c, id) + envelope, _ := getEnvelopeResource(c, p) if month.Month.IsZero() { httputil.NewError(c, http.StatusBadRequest, errors.New("You cannot request data for no month")) @@ -197,12 +200,13 @@ func GetEnvelopeMonth(c *gin.Context) { // @Param envelope body models.EnvelopeCreate true "Envelope" // @Router /v1/envelopes/{envelopeId} [patch] func UpdateEnvelope(c *gin.Context) { - id, err := httputil.ParseID(c, "envelopeId") + p, err := uuid.Parse(c.Param("envelopeId")) if err != nil { + httputil.ErrorInvalidUUID(c) return } - envelope, err := getEnvelopeResource(c, id) + envelope, err := getEnvelopeResource(c, p) if err != nil { return } @@ -227,12 +231,13 @@ func UpdateEnvelope(c *gin.Context) { // @Param envelopeId path uint64 true "ID of the envelope" // @Router /v1/envelopes/{envelopeId} [delete] func DeleteEnvelope(c *gin.Context) { - id, err := httputil.ParseID(c, "envelopeId") + p, err := uuid.Parse(c.Param("envelopeId")) if err != nil { + httputil.ErrorInvalidUUID(c) return } - envelope, err := getEnvelopeResource(c, id) + envelope, err := getEnvelopeResource(c, p) if err != nil { return } @@ -243,7 +248,7 @@ func DeleteEnvelope(c *gin.Context) { } // getEnvelopeResource verifies that the envelope from the URL parameters exists and returns it. -func getEnvelopeResource(c *gin.Context, id uint64) (models.Envelope, error) { +func getEnvelopeResource(c *gin.Context, id uuid.UUID) (models.Envelope, error) { var envelope models.Envelope err := models.DB.Where(&models.Envelope{ @@ -259,7 +264,7 @@ func getEnvelopeResource(c *gin.Context, id uint64) (models.Envelope, error) { return envelope, nil } -func getEnvelopeObject(c *gin.Context, id uint64) (Envelope, error) { +func getEnvelopeObject(c *gin.Context, id uuid.UUID) (Envelope, error) { resource, err := getEnvelopeResource(c, id) if err != nil { return Envelope{}, err @@ -275,8 +280,8 @@ func getEnvelopeObject(c *gin.Context, id uint64) (Envelope, error) { // // This function is only needed for getEnvelopeObject as we cannot create an instance of Envelope // with mixed named and unnamed parameters. -func getEnvelopeLinks(c *gin.Context, id uint64) EnvelopeLinks { - url := httputil.RequestPathV1(c) + fmt.Sprintf("/envelopes/%d", id) +func getEnvelopeLinks(c *gin.Context, id uuid.UUID) EnvelopeLinks { + url := httputil.RequestPathV1(c) + fmt.Sprintf("/envelopes/%s", id) return EnvelopeLinks{ Self: url, diff --git a/internal/controllers/envelope_test.go b/internal/controllers/envelope_test.go index 2d9db9df..61b83c7e 100644 --- a/internal/controllers/envelope_test.go +++ b/internal/controllers/envelope_test.go @@ -13,6 +13,16 @@ import ( "github.com/stretchr/testify/assert" ) +func createTestEnvelope(t *testing.T, c models.EnvelopeCreate) controllers.EnvelopeResponse { + r := test.Request(t, "POST", "/v1/envelopes", c) + test.AssertHTTPStatus(t, http.StatusCreated, &r) + + var e controllers.EnvelopeResponse + test.DecodeResponse(t, &r, &e) + + return e +} + func TestGetEnvelopes(t *testing.T) { recorder := test.Request(t, "GET", "/v1/envelopes", "") @@ -24,7 +34,6 @@ func TestGetEnvelopes(t *testing.T) { assert.FailNow(t, "Response does not have exactly 1 item") } - assert.Equal(t, uint64(1), response.Data[0].CategoryID) assert.Equal(t, "Utilities", response.Data[0].Name) assert.Equal(t, "Energy & Water", response.Data[0].Note) @@ -36,24 +45,43 @@ func TestGetEnvelopes(t *testing.T) { } func TestNoEnvelopeNotFound(t *testing.T) { - recorder := test.Request(t, "GET", "/v1/envelopes/2", "") + recorder := test.Request(t, "GET", "/v1/envelopes/828f2483-dabd-4267-a223-e34b5f171978", "") test.AssertHTTPStatus(t, http.StatusNotFound, &recorder) } -// TestEnvelopeInvalidIDs verifies that on non-number requests for envelope IDs, -// the API returs a Bad Request status code. func TestEnvelopeInvalidIDs(t *testing.T) { - r := test.Request(t, "GET", "/v1/envelopes/-1985", "") + /* + * GET + */ + r := test.Request(t, http.MethodGet, "/v1/envelopes/-56", "") test.AssertHTTPStatus(t, http.StatusBadRequest, &r) - r = test.Request(t, "GET", "/v1/envelopes/OhNoOurTable", "") + r = test.Request(t, http.MethodGet, "/v1/envelopes/notANumber", "") test.AssertHTTPStatus(t, http.StatusBadRequest, &r) - r = test.Request(t, "PATCH", "/v1/envelopes/StupidLittleWalk", "") + r = test.Request(t, http.MethodGet, "/v1/envelopes/23", "") test.AssertHTTPStatus(t, http.StatusBadRequest, &r) - r = test.Request(t, "DELETE", "/v1/envelopes/25640ly", "") + r = test.Request(t, http.MethodGet, "/v1/envelopes/d19a622f-broken-uuid/2017-09", "") + test.AssertHTTPStatus(t, http.StatusBadRequest, &r) + + /* + * PATCH + */ + r = test.Request(t, http.MethodPatch, "/v1/envelopes/-274", "") + test.AssertHTTPStatus(t, http.StatusBadRequest, &r) + + r = test.Request(t, http.MethodPatch, "/v1/envelopes/stringRandom", "") + test.AssertHTTPStatus(t, http.StatusBadRequest, &r) + + /* + * DELETE + */ + r = test.Request(t, http.MethodDelete, "/v1/envelopes/-274", "") + test.AssertHTTPStatus(t, http.StatusBadRequest, &r) + + r = test.Request(t, http.MethodDelete, "/v1/envelopes/stringRandom", "") test.AssertHTTPStatus(t, http.StatusBadRequest, &r) } @@ -76,7 +104,7 @@ func TestCreateBrokenEnvelope(t *testing.T) { } func TestCreateEnvelopeNoCategory(t *testing.T) { - recorder := test.Request(t, "POST", "/v1/envelopes", `{ "categoryId": 5967 }`) + recorder := test.Request(t, "POST", "/v1/envelopes", `{ "categoryId": "5f0cd7b9-9788-4871-96f8-c816c9ae338a" }`) test.AssertHTTPStatus(t, http.StatusNotFound, &recorder) } @@ -86,20 +114,15 @@ func TestCreateEnvelopeNoBody(t *testing.T) { } func TestGetEnvelope(t *testing.T) { - recorder := test.Request(t, "GET", "/v1/envelopes/1", "") - test.AssertHTTPStatus(t, http.StatusOK, &recorder) - - var envelopeObject, savedEnvelope controllers.EnvelopeResponse - test.DecodeResponse(t, &recorder, &envelopeObject) - - recorder = test.Request(t, "GET", envelopeObject.Data.Links.Self, "") - test.DecodeResponse(t, &recorder, &savedEnvelope) - - assert.Equal(t, savedEnvelope, envelopeObject) + _ = createTestEnvelope(t, models.EnvelopeCreate{Name: "This only tests creation"}) } // TestEnvelopeMonth verifies that the monthly calculations are correct. func TestEnvelopeMonth(t *testing.T) { + var envelopeList controllers.EnvelopeListResponse + r := test.Request(t, http.MethodGet, "/v1/envelopes", "") + test.DecodeResponse(t, &r, &envelopeList) + var envelopeMonth controllers.EnvelopeMonthResponse tests := []struct { @@ -107,9 +130,8 @@ func TestEnvelopeMonth(t *testing.T) { envelopeMonth models.EnvelopeMonth }{ { - "/v1/envelopes/1/2022-01", + fmt.Sprintf("/v1/envelopes/%s/2022-01", envelopeList.Data[0].ID), models.EnvelopeMonth{ - ID: 1, Name: "Utilities", Month: time.Date(2022, 1, 1, 0, 0, 0, 0, time.UTC), Spent: decimal.NewFromFloat(-10), @@ -118,9 +140,8 @@ func TestEnvelopeMonth(t *testing.T) { }, }, { - "/v1/envelopes/1/2022-02", + fmt.Sprintf("/v1/envelopes/%s/2022-02", envelopeList.Data[0].ID), models.EnvelopeMonth{ - ID: 1, Name: "Utilities", Month: time.Date(2022, 2, 1, 0, 0, 0, 0, time.UTC), Spent: decimal.NewFromFloat(-5), @@ -129,9 +150,8 @@ func TestEnvelopeMonth(t *testing.T) { }, }, { - "/v1/envelopes/1/2022-03", + fmt.Sprintf("/v1/envelopes/%s/2022-03", envelopeList.Data[0].ID), models.EnvelopeMonth{ - ID: 1, Name: "Utilities", Month: time.Date(2022, 3, 1, 0, 0, 0, 0, time.UTC), Spent: decimal.NewFromFloat(-15), @@ -146,7 +166,6 @@ func TestEnvelopeMonth(t *testing.T) { test.AssertHTTPStatus(t, http.StatusOK, &r) test.DecodeResponse(t, &r, &envelopeMonth) - assert.Equal(t, envelopeMonth.Data.ID, tt.envelopeMonth.ID) assert.Equal(t, envelopeMonth.Data.Name, tt.envelopeMonth.Name) assert.Equal(t, envelopeMonth.Data.Month, tt.envelopeMonth.Month) assert.True(t, envelopeMonth.Data.Spent.Equal(tt.envelopeMonth.Spent), "Monthly spent calculation for %v is wrong: should be %v, but is %v: %#v", envelopeMonth.Data.Month, tt.envelopeMonth.Spent, envelopeMonth.Data.Spent, envelopeMonth.Data) @@ -156,17 +175,19 @@ func TestEnvelopeMonth(t *testing.T) { } func TestEnvelopeMonthInvalid(t *testing.T) { - // Test that non-parseable requests produce an error - r := test.Request(t, "GET", "/v1/envelopes/1/Stonks!", "") - test.AssertHTTPStatus(t, http.StatusBadRequest, &r) + var envelopeList controllers.EnvelopeListResponse + r := test.Request(t, http.MethodGet, "/v1/envelopes", "") + test.DecodeResponse(t, &r, &envelopeList) - r = test.Request(t, "GET", "/v1/envelopes/-17/2022-03", "") + // Test that non-parseable requests produce an error + r = test.Request(t, "GET", fmt.Sprintf("/v1/envelopes/%s/Stonks!", envelopeList.Data[0].ID), "") test.AssertHTTPStatus(t, http.StatusBadRequest, &r) } // TestEnvelopeMonthZero tests that we return a HTTP Bad Request when requesting data for the zero timestamp. func TestEnvelopeMonthZero(t *testing.T) { - r := test.Request(t, "GET", "/v1/envelopes/1/0001-01", "") + e := createTestEnvelope(t, models.EnvelopeCreate{}) + r := test.Request(t, http.MethodGet, fmt.Sprintf("%s/0001-01", e.Data.Links.Self), "") test.AssertHTTPStatus(t, http.StatusBadRequest, &r) } @@ -201,17 +222,18 @@ func TestUpdateEnvelopeBroken(t *testing.T) { } func TestUpdateNonExistingEnvelope(t *testing.T) { - recorder := test.Request(t, "PATCH", "/v1/envelopes/48902805", `{ "name": "2" }`) + recorder := test.Request(t, "PATCH", "/v1/envelopes/dcf472ba-a64e-4f0f-900e-a789319e432c", `{ "name": "2" }`) test.AssertHTTPStatus(t, http.StatusNotFound, &recorder) } func TestDeleteEnvelope(t *testing.T) { - recorder := test.Request(t, "DELETE", "/v1/envelopes/1", "") - test.AssertHTTPStatus(t, http.StatusNoContent, &recorder) + e := createTestEnvelope(t, models.EnvelopeCreate{Name: "Delete me!"}) + r := test.Request(t, http.MethodDelete, e.Data.Links.Self, "") + test.AssertHTTPStatus(t, http.StatusNoContent, &r) } func TestDeleteNonExistingEnvelope(t *testing.T) { - recorder := test.Request(t, "DELETE", "/v1/envelopes/48902805", "") + recorder := test.Request(t, "DELETE", "/v1/envelopes/21a300da-d8b4-478d-8e85-95cb7982cbca", "") test.AssertHTTPStatus(t, http.StatusNotFound, &recorder) } diff --git a/internal/controllers/transaction.go b/internal/controllers/transaction.go index 2f18c675..8c3bcc61 100644 --- a/internal/controllers/transaction.go +++ b/internal/controllers/transaction.go @@ -8,6 +8,7 @@ import ( "github.com/envelope-zero/backend/internal/httputil" "github.com/envelope-zero/backend/internal/models" "github.com/gin-gonic/gin" + "github.com/google/uuid" "github.com/shopspring/decimal" ) @@ -25,7 +26,7 @@ type Transaction struct { } type TransactionLinks struct { - Self string `json:"self" example:"https://example.com/api/v1/transactions/1741"` + Self string `json:"self" example:"https://example.com/api/v1/transactions/d430d7c3-d14c-4712-9336-ee56965a6673"` } // RegisterTransactionRoutes registers the routes for transactions with @@ -137,12 +138,13 @@ func GetTransactions(c *gin.Context) { // @Param transactionId path uint64 true "ID of the transaction" // @Router /v1/transactions/{transactionId} [get] func GetTransaction(c *gin.Context) { - id, err := httputil.ParseID(c, "transactionId") + p, err := uuid.Parse(c.Param("transactionId")) if err != nil { + httputil.ErrorInvalidUUID(c) return } - transactionObject, err := getTransactionObject(c, id) + transactionObject, err := getTransactionObject(c, p) if err != nil { return } @@ -163,12 +165,13 @@ func GetTransaction(c *gin.Context) { // @Param transaction body models.TransactionCreate true "Transaction" // @Router /v1/transactions/{transactionId} [patch] func UpdateTransaction(c *gin.Context) { - id, err := httputil.ParseID(c, "transactionId") + p, err := uuid.Parse(c.Param("transactionId")) if err != nil { + httputil.ErrorInvalidUUID(c) return } - transaction, err := getTransactionResource(c, id) + transaction, err := getTransactionResource(c, p) if err != nil { return } @@ -190,7 +193,7 @@ func UpdateTransaction(c *gin.Context) { } models.DB.Model(&transaction).Updates(data) - transactionObject, _ := getTransactionObject(c, id) + transactionObject, _ := getTransactionObject(c, p) c.JSON(http.StatusOK, TransactionResponse{Data: transactionObject}) } @@ -204,12 +207,13 @@ func UpdateTransaction(c *gin.Context) { // @Param transactionId path uint64 true "ID of the transaction" // @Router /v1/transactions/{transactionId} [delete] func DeleteTransaction(c *gin.Context) { - id, err := httputil.ParseID(c, "transactionId") + p, err := uuid.Parse(c.Param("transactionId")) if err != nil { + httputil.ErrorInvalidUUID(c) return } - transaction, err := getTransactionResource(c, id) + transaction, err := getTransactionResource(c, p) if err != nil { return } @@ -220,7 +224,7 @@ func DeleteTransaction(c *gin.Context) { } // getTransactionResource verifies that the request URI is valid for the transaction and returns it. -func getTransactionResource(c *gin.Context, id uint64) (models.Transaction, error) { +func getTransactionResource(c *gin.Context, id uuid.UUID) (models.Transaction, error) { var transaction models.Transaction err := models.DB.First(&transaction, &models.Transaction{ @@ -236,7 +240,7 @@ func getTransactionResource(c *gin.Context, id uint64) (models.Transaction, erro return transaction, nil } -func getTransactionObject(c *gin.Context, id uint64) (Transaction, error) { +func getTransactionObject(c *gin.Context, id uuid.UUID) (Transaction, error) { resource, err := getTransactionResource(c, id) if err != nil { return Transaction{}, err @@ -252,8 +256,8 @@ func getTransactionObject(c *gin.Context, id uint64) (Transaction, error) { // // This function is only needed for getTransactionObject as we cannot create an instance of Transaction // with mixed named and unnamed parameters. -func getTransactionLinks(c *gin.Context, id uint64) TransactionLinks { - url := httputil.RequestPathV1(c) + fmt.Sprintf("/transactions/%d", id) +func getTransactionLinks(c *gin.Context, id uuid.UUID) TransactionLinks { + url := httputil.RequestPathV1(c) + fmt.Sprintf("/transactions/%s", id) return TransactionLinks{ Self: url, diff --git a/internal/controllers/transaction_test.go b/internal/controllers/transaction_test.go index fb45c273..81f15ba1 100644 --- a/internal/controllers/transaction_test.go +++ b/internal/controllers/transaction_test.go @@ -12,6 +12,16 @@ import ( "github.com/stretchr/testify/assert" ) +func createTestTransaction(t *testing.T, c models.TransactionCreate) controllers.TransactionResponse { + r := test.Request(t, "POST", "/v1/transactions", c) + test.AssertHTTPStatus(t, http.StatusCreated, &r) + + var tr controllers.TransactionResponse + test.DecodeResponse(t, &r, &tr) + + return tr +} + func TestGetTransactions(t *testing.T) { recorder := test.Request(t, "GET", "/v1/transactions", "") @@ -24,34 +34,22 @@ func TestGetTransactions(t *testing.T) { } januaryTransaction := response.Data[0] - assert.Equal(t, uint64(1), januaryTransaction.BudgetID) assert.Equal(t, "Water bill for January", januaryTransaction.Note) assert.Equal(t, true, januaryTransaction.Reconciled) - assert.Equal(t, uint64(1), januaryTransaction.SourceAccountID) - assert.Equal(t, uint64(3), januaryTransaction.DestinationAccountID) - assert.Equal(t, uint64(1), januaryTransaction.EnvelopeID) if !decimal.NewFromFloat(10).Equal(januaryTransaction.Amount) { assert.Fail(t, "Transaction amount does not equal 10", januaryTransaction.Amount) } februaryTransaction := response.Data[1] - assert.Equal(t, uint64(1), februaryTransaction.BudgetID) assert.Equal(t, "Water bill for February", februaryTransaction.Note) assert.Equal(t, false, februaryTransaction.Reconciled) - assert.Equal(t, uint64(1), februaryTransaction.SourceAccountID) - assert.Equal(t, uint64(3), februaryTransaction.DestinationAccountID) - assert.Equal(t, uint64(1), februaryTransaction.EnvelopeID) if !decimal.NewFromFloat(5).Equal(februaryTransaction.Amount) { assert.Fail(t, "Transaction amount does not equal 5", februaryTransaction.Amount) } marchTransaction := response.Data[2] - assert.Equal(t, uint64(1), marchTransaction.BudgetID) assert.Equal(t, "Water bill for March", marchTransaction.Note) assert.Equal(t, false, marchTransaction.Reconciled) - assert.Equal(t, uint64(1), marchTransaction.SourceAccountID) - assert.Equal(t, uint64(3), marchTransaction.DestinationAccountID) - assert.Equal(t, uint64(1), marchTransaction.EnvelopeID) if !decimal.NewFromFloat(15).Equal(marchTransaction.Amount) { assert.Fail(t, "Transaction amount does not equal 15", marchTransaction.Amount) } @@ -66,24 +64,40 @@ func TestGetTransactions(t *testing.T) { } func TestNoTransactionNotFound(t *testing.T) { - recorder := test.Request(t, "GET", "/v1/transactions/37", "") + recorder := test.Request(t, "GET", "/v1/transactions/048b061f-3b6b-45ab-b0e9-0f38d2fff0c8", "") test.AssertHTTPStatus(t, http.StatusNotFound, &recorder) } -// TestTransactionInvalidIDs verifies that on non-number requests for transaction IDs, -// the API returs a Bad Request status code. func TestTransactionInvalidIDs(t *testing.T) { - r := test.Request(t, "GET", "/v1/transactions/-56", "") + /* + * GET + */ + r := test.Request(t, http.MethodGet, "/v1/transactions/-56", "") + test.AssertHTTPStatus(t, http.StatusBadRequest, &r) + + r = test.Request(t, http.MethodGet, "/v1/transactions/notANumber", "") + test.AssertHTTPStatus(t, http.StatusBadRequest, &r) + + r = test.Request(t, http.MethodGet, "/v1/transactions/23", "") + test.AssertHTTPStatus(t, http.StatusBadRequest, &r) + + /* + * PATCH + */ + r = test.Request(t, http.MethodPatch, "/v1/transactions/-274", "") test.AssertHTTPStatus(t, http.StatusBadRequest, &r) - r = test.Request(t, "GET", "/v1/transactions/notANumber", "") + r = test.Request(t, http.MethodPatch, "/v1/transactions/stringRandom", "") test.AssertHTTPStatus(t, http.StatusBadRequest, &r) - r = test.Request(t, "PATCH", "/v1/transactions/TreesAreNice", "") + /* + * DELETE + */ + r = test.Request(t, http.MethodDelete, "/v1/transactions/-274", "") test.AssertHTTPStatus(t, http.StatusBadRequest, &r) - r = test.Request(t, "DELETE", "/v1/transactions/-15", "") + r = test.Request(t, http.MethodDelete, "/v1/transactions/stringRandom", "") test.AssertHTTPStatus(t, http.StatusBadRequest, &r) } @@ -116,7 +130,7 @@ func TestCreateNegativeAmountTransaction(t *testing.T) { } func TestCreateNonExistingBudgetTransaction(t *testing.T) { - recorder := test.Request(t, "POST", "/v1/transactions", `{ "budgetId": 5, "amount": 32.12, "note": "The budget with this id must exist, so this must fail" }`) + recorder := test.Request(t, "POST", "/v1/transactions", `{ "budgetId": "978e95a0-90f2-4dee-91fd-ee708c30301c", "amount": 32.12, "note": "The budget with this id must exist, so this must fail" }`) test.AssertHTTPStatus(t, http.StatusNotFound, &recorder) } @@ -126,18 +140,10 @@ func TestCreateTransactionNoBody(t *testing.T) { } func TestGetTransaction(t *testing.T) { - recorder := test.Request(t, "GET", "/v1/transactions/1", "") - test.AssertHTTPStatus(t, http.StatusOK, &recorder) - - var transaction controllers.TransactionResponse - test.DecodeResponse(t, &recorder, &transaction) + tr := createTestTransaction(t, models.TransactionCreate{Amount: decimal.NewFromFloat(13.71)}) - var dbTransaction models.Transaction - models.DB.First(&dbTransaction, transaction.Data.ID) - - if !decimal.NewFromFloat(10).Equals(transaction.Data.Amount) { - assert.Fail(t, "Transaction amount does not equal 10", transaction.Data.Amount) - } + r := test.Request(t, http.MethodGet, tr.Data.Links.Self, "") + assert.Equal(t, http.StatusOK, r.Code) } func TestUpdateTransaction(t *testing.T) { @@ -179,17 +185,19 @@ func TestUpdateTransactionNegativeAmount(t *testing.T) { } func TestUpdateNonExistingTransaction(t *testing.T) { - recorder := test.Request(t, "PATCH", "/v1/transactions/48902805", `{ "note": "2" }`) + recorder := test.Request(t, "PATCH", "/v1/transactions/6ae3312c-23cf-4225-9a81-4f218ba41b00", `{ "note": "2" }`) test.AssertHTTPStatus(t, http.StatusNotFound, &recorder) } func TestDeleteTransaction(t *testing.T) { - recorder := test.Request(t, "DELETE", "/v1/transactions/1", "") + tr := createTestTransaction(t, models.TransactionCreate{Amount: decimal.NewFromFloat(123.12)}) + + recorder := test.Request(t, "DELETE", tr.Data.Links.Self, "") test.AssertHTTPStatus(t, http.StatusNoContent, &recorder) } func TestDeleteNonExistingTransaction(t *testing.T) { - recorder := test.Request(t, "DELETE", "/v1/transactions/48902805", "") + recorder := test.Request(t, "DELETE", "/v1/transactions/4bcb6d09-ced1-41e8-a3fe-bf4f16c5e501", "") test.AssertHTTPStatus(t, http.StatusNotFound, &recorder) } diff --git a/internal/httputil/error.go b/internal/httputil/error.go index 1d51b044..bad3b396 100644 --- a/internal/httputil/error.go +++ b/internal/httputil/error.go @@ -47,3 +47,7 @@ func NewError(c *gin.Context, status int, err error) { type HTTPError struct { Error string `json:"error" example:"An ID specified in the query string was not a valid uint64"` } + +func ErrorInvalidUUID(c *gin.Context) { + NewError(c, http.StatusBadRequest, errors.New("The specified resource ID is not a valid UUID")) +} diff --git a/internal/httputil/error_test.go b/internal/httputil/error_test.go index ac32ad9a..789c988c 100644 --- a/internal/httputil/error_test.go +++ b/internal/httputil/error_test.go @@ -23,7 +23,6 @@ func TestFetchErrorHandlerErrRecordNotFound(t *testing.T) { httputil.FetchErrorHandler(c, gorm.ErrRecordNotFound) }) - // Check without reverse proxy headers c.Request, _ = http.NewRequest(http.MethodGet, "/", nil) r.ServeHTTP(w, c.Request) assert.Equal(t, http.StatusNotFound, w.Code) @@ -37,7 +36,6 @@ func TestFetchErrorHandlerStrconvNumError(t *testing.T) { httputil.FetchErrorHandler(c, &strconv.NumError{}) }) - // Check without reverse proxy headers c.Request, _ = http.NewRequest(http.MethodGet, "/", nil) r.ServeHTTP(w, c.Request) assert.Equal(t, http.StatusBadRequest, w.Code) @@ -52,7 +50,6 @@ func TestFetchErrorHandlerTimeParseError(t *testing.T) { httputil.FetchErrorHandler(c, &time.ParseError{}) }) - // Check without reverse proxy headers c.Request, _ = http.NewRequest(http.MethodGet, "/", nil) r.ServeHTTP(w, c.Request) assert.Equal(t, http.StatusBadRequest, w.Code) @@ -67,9 +64,22 @@ func TestFetchErrorHandlerInternalServerError(t *testing.T) { httputil.FetchErrorHandler(c, errors.New("Some random error")) }) - // Check without reverse proxy headers c.Request, _ = http.NewRequest(http.MethodGet, "/", nil) r.ServeHTTP(w, c.Request) assert.Equal(t, http.StatusInternalServerError, w.Code) assert.Contains(t, test.DecodeError(t, w.Body.Bytes()), "an error occured on the server during your request") } + +func TestErrorInvalidUUID(t *testing.T) { + w := httptest.NewRecorder() + c, r := gin.CreateTestContext(w) + + r.GET("/", func(ctx *gin.Context) { + httputil.ErrorInvalidUUID(c) + }) + + c.Request, _ = http.NewRequest(http.MethodGet, "/", nil) + r.ServeHTTP(w, c.Request) + assert.Equal(t, http.StatusBadRequest, w.Code) + assert.Contains(t, test.DecodeError(t, w.Body.Bytes()), "not a valid UUID") +} diff --git a/internal/httputil/request.go b/internal/httputil/request.go index 15187085..7c92f6bf 100644 --- a/internal/httputil/request.go +++ b/internal/httputil/request.go @@ -2,10 +2,8 @@ package httputil import ( "errors" - "fmt" "io" "net/http" - "strconv" "github.com/gin-contrib/requestid" "github.com/gin-gonic/gin" @@ -31,8 +29,6 @@ func RequestHost(c *gin.Context) string { host := c.Request.Host var forwardedPrefix string - fmt.Println(c.Request.Header) - xForwardedHost := c.Request.Header.Get("x-forwarded-host") if xForwardedHost != "" { host = xForwardedHost @@ -57,19 +53,6 @@ func RequestURL(c *gin.Context) string { return RequestHost(c) + c.Request.URL.Path } -// ParseID parses the ID. -func ParseID(c *gin.Context, param string) (uint64, error) { - var parsed uint64 - - parsed, err := strconv.ParseUint(c.Param(param), 10, 64) - if err != nil { - FetchErrorHandler(c, err) - return 0, err - } - - return parsed, nil -} - // BindData binds the data from the request to the struct passed in the interface. func BindData(c *gin.Context, data interface{}) error { if err := c.ShouldBindJSON(&data); err != nil { diff --git a/internal/httputil/request_test.go b/internal/httputil/request_test.go index e65d465e..2efc853a 100644 --- a/internal/httputil/request_test.go +++ b/internal/httputil/request_test.go @@ -2,7 +2,6 @@ package httputil_test import ( "bytes" - "fmt" "net/http" "net/http/httptest" "testing" @@ -106,48 +105,6 @@ func TestRequestPathURI(t *testing.T) { assert.Equal(t, "http://example.com/seenotrettung/ist-kein-verbrechen", w.Body.String()) } -func TestParseIDInvalid(t *testing.T) { - w := httptest.NewRecorder() - c, r := gin.CreateTestContext(w) - - r.GET("/", func(ctx *gin.Context) { - id, err := httputil.ParseID(c, "NotAValidUint") - if err != nil { - return - } - - c.String(http.StatusOK, fmt.Sprintf("%d", id)) - }) - - c.Request, _ = http.NewRequest(http.MethodGet, "/", nil) - c.Request.Host = "example.com" - r.ServeHTTP(w, c.Request) - - // Not checking the error messages here as this is done in error_test.go - assert.Equal(t, http.StatusBadRequest, w.Code) -} - -func TestParseID(t *testing.T) { - w := httptest.NewRecorder() - c, r := gin.CreateTestContext(w) - c.Params = append(c.Params, gin.Param{Key: "id", Value: "171"}) - - r.GET("/", func(ctx *gin.Context) { - id, err := httputil.ParseID(c, "id") - if err != nil { - return - } - - c.String(http.StatusOK, fmt.Sprintf("%d", id)) - }) - - c.Request, _ = http.NewRequest(http.MethodGet, "/", nil) - c.Request.Host = "example.com" - r.ServeHTTP(w, c.Request) - - assert.Equal(t, "171", w.Body.String()) -} - func TestBindData(t *testing.T) { w := httptest.NewRecorder() c, r := gin.CreateTestContext(w) diff --git a/internal/models/account.go b/internal/models/account.go index db3e9c72..21162fbc 100644 --- a/internal/models/account.go +++ b/internal/models/account.go @@ -1,6 +1,7 @@ package models import ( + "github.com/google/uuid" "github.com/shopspring/decimal" "gorm.io/gorm" ) @@ -15,11 +16,11 @@ type Account struct { } type AccountCreate struct { - Name string `json:"name,omitempty"` - Note string `json:"note,omitempty"` - BudgetID uint64 `json:"budgetId"` - OnBudget bool `json:"onBudget"` // Always false when external: true - External bool `json:"external"` + Name string `json:"name,omitempty"` + Note string `json:"note,omitempty"` + BudgetID uuid.UUID `json:"budgetId"` + OnBudget bool `json:"onBudget"` // Always false when external: true + External bool `json:"external"` } func (a Account) WithCalculations() Account { diff --git a/internal/models/allocation.go b/internal/models/allocation.go index 5aa377af..0c64f941 100644 --- a/internal/models/allocation.go +++ b/internal/models/allocation.go @@ -1,6 +1,7 @@ package models import ( + "github.com/google/uuid" "github.com/shopspring/decimal" ) @@ -15,5 +16,5 @@ type AllocationCreate struct { Month uint8 `json:"month" gorm:"uniqueIndex:year_month;check:month_valid,month >= 1 AND month <= 12"` Year uint `json:"year" gorm:"uniqueIndex:year_month"` Amount decimal.Decimal `json:"amount" gorm:"type:DECIMAL(20,8)"` - EnvelopeID uint64 `json:"envelopeId,omitempty"` + EnvelopeID uuid.UUID `json:"envelopeId,omitempty"` } diff --git a/internal/models/budget.go b/internal/models/budget.go index 326d665a..e2221eb2 100644 --- a/internal/models/budget.go +++ b/internal/models/budget.go @@ -2,6 +2,8 @@ package models import ( "time" + + "github.com/google/uuid" ) // Budget represents a budget @@ -20,7 +22,7 @@ type BudgetCreate struct { } type BudgetMonth struct { - ID uint64 `json:"id" example:"23"` + ID uuid.UUID `json:"id" example:"23"` Name string `json:"name" example:"A test envelope"` Month time.Time `json:"month" example:"2006-05-04T15:02:01.000000Z"` Envelopes []EnvelopeMonth `json:"envelopes,omitempty"` diff --git a/internal/models/category.go b/internal/models/category.go index d6307b77..3323c4c1 100644 --- a/internal/models/category.go +++ b/internal/models/category.go @@ -1,5 +1,7 @@ package models +import "github.com/google/uuid" + // Category represents a category of envelopes. type Category struct { Model @@ -8,7 +10,7 @@ type Category struct { } type CategoryCreate struct { - Name string `json:"name,omitempty"` - BudgetID uint64 `json:"budgetId"` - Note string `json:"note,omitempty"` + Name string `json:"name,omitempty"` + BudgetID uuid.UUID `json:"budgetId"` + Note string `json:"note,omitempty"` } diff --git a/internal/models/envelope.go b/internal/models/envelope.go index 51d31343..e72e618d 100644 --- a/internal/models/envelope.go +++ b/internal/models/envelope.go @@ -4,6 +4,7 @@ import ( "fmt" "time" + "github.com/google/uuid" "github.com/shopspring/decimal" ) @@ -15,14 +16,14 @@ type Envelope struct { } type EnvelopeCreate struct { - Name string `json:"name,omitempty"` - CategoryID uint64 `json:"categoryId"` - Note string `json:"note,omitempty"` + Name string `json:"name,omitempty"` + CategoryID uuid.UUID `json:"categoryId"` + Note string `json:"note,omitempty"` } // EnvelopeMonth contains data about an Envelope for a specific month. type EnvelopeMonth struct { - ID uint64 `json:"id"` + ID uuid.UUID `json:"id"` Name string `json:"name"` Month time.Time `json:"month"` Spent decimal.Decimal `json:"spent"` @@ -34,7 +35,7 @@ type EnvelopeMonth struct { func (e Envelope) Spent(t time.Time) decimal.Decimal { // All transactions where the Envelope ID matches and that have an external account as source and an internal account as destination incoming, _ := RawTransactions( - fmt.Sprintf("SELECT transactions.* FROM transactions, accounts AS source_accounts, accounts AS destination_accounts WHERE transactions.source_account_id = source_accounts.id AND source_accounts.external AND transactions.destination_account_id = destination_accounts.id AND NOT destination_accounts.external AND transactions.envelope_id = %v", e.ID), + fmt.Sprintf("SELECT transactions.* FROM transactions, accounts AS source_accounts, accounts AS destination_accounts WHERE transactions.source_account_id = source_accounts.id AND source_accounts.external AND transactions.destination_account_id = destination_accounts.id AND NOT destination_accounts.external AND transactions.envelope_id = \"%v\"", e.ID), ) // Add all incoming transactions that are in the correct month @@ -47,7 +48,7 @@ func (e Envelope) Spent(t time.Time) decimal.Decimal { outgoing, _ := RawTransactions( // All transactions where the envelope ID matches that have an internal account as source and an external account as destination - fmt.Sprintf("SELECT transactions.* FROM transactions, accounts AS source_accounts, accounts AS destination_accounts WHERE transactions.source_account_id = source_accounts.id AND NOT source_accounts.external AND transactions.destination_account_id = destination_accounts.id AND destination_accounts.external AND transactions.envelope_id = %v", e.ID), + fmt.Sprintf("SELECT transactions.* FROM transactions, accounts AS source_accounts, accounts AS destination_accounts WHERE transactions.source_account_id = source_accounts.id AND NOT source_accounts.external AND transactions.destination_account_id = destination_accounts.id AND destination_accounts.external AND transactions.envelope_id = \"%v\"", e.ID), ) // Add all outgoing transactions that are in the correct month diff --git a/internal/models/model.go b/internal/models/model.go index 790985d6..dfd39846 100644 --- a/internal/models/model.go +++ b/internal/models/model.go @@ -3,12 +3,13 @@ package models import ( "time" + "github.com/google/uuid" "gorm.io/gorm" ) // Model is the base model for all other models in Envelope Zero. type Model struct { - ID uint64 `json:"id" example:"42" format:"uint64"` + ID uuid.UUID `json:"id" example:"65392deb-5e92-4268-b114-297faad6cdce"` CreatedAt time.Time `json:"createdAt" example:"2022-04-02T19:28:44.491514Z"` UpdatedAt time.Time `json:"updatedAt" example:"2022-04-17T20:14:01.048145Z"` DeletedAt *gorm.DeletedAt `json:"deletedAt,omitempty" gorm:"index"` @@ -29,3 +30,9 @@ func (m *Model) AfterFind(tx *gorm.DB) (err error) { return nil } + +// BeforeCreate is set to generate a UUID for the resource. +func (m *Model) BeforeCreate(tx *gorm.DB) (err error) { + m.ID = uuid.New() + return nil +} diff --git a/internal/models/transaction.go b/internal/models/transaction.go index b303471c..4409a3e9 100644 --- a/internal/models/transaction.go +++ b/internal/models/transaction.go @@ -3,6 +3,7 @@ package models import ( "time" + "github.com/google/uuid" "github.com/shopspring/decimal" "gorm.io/gorm" ) @@ -21,10 +22,10 @@ type TransactionCreate struct { Date time.Time `json:"date,omitempty"` Amount decimal.Decimal `json:"amount" gorm:"type:DECIMAL(20,8)"` Note string `json:"note,omitempty"` - BudgetID uint64 `json:"budgetId,omitempty"` - SourceAccountID uint64 `json:"sourceAccountId,omitempty"` - DestinationAccountID uint64 `json:"destinationAccountId,omitempty"` - EnvelopeID uint64 `json:"envelopeId,omitempty"` + BudgetID uuid.UUID `json:"budgetId,omitempty"` + SourceAccountID uuid.UUID `json:"sourceAccountId,omitempty"` + DestinationAccountID uuid.UUID `json:"destinationAccountId,omitempty"` + EnvelopeID uuid.UUID `json:"envelopeId,omitempty"` Reconciled bool `json:"reconciled"` } diff --git a/internal/test/helpers.go b/internal/test/helpers.go index 27486a8b..63316473 100644 --- a/internal/test/helpers.go +++ b/internal/test/helpers.go @@ -27,8 +27,19 @@ type APIResponse struct { } // Request is a helper method to simplify making a HTTP request for tests. -func Request(t *testing.T, method, url, body string, headers ...map[string]string) httptest.ResponseRecorder { - byteStr := []byte(body) +func Request(t *testing.T, method, url string, body any, headers ...map[string]string) httptest.ResponseRecorder { + var byteStr []byte + var err error + + // If the body is a string, convert it to bytes + if reflect.TypeOf(body).Kind() == reflect.String { + byteStr = []byte(body.(string)) + } else { + byteStr, err = json.Marshal(body) + if err != nil { + assert.FailNow(t, "Request body could not be marshalled from object input", err) + } + } os.Setenv("LOG_FORMAT", "human") router, err := controllers.Router() diff --git a/internal/test/helpers_test.go b/internal/test/helpers_test.go deleted file mode 100644 index 5601ec0a..00000000 --- a/internal/test/helpers_test.go +++ /dev/null @@ -1,26 +0,0 @@ -package test_test - -import ( - "net/http" - "testing" - - "github.com/envelope-zero/backend/internal/test" - "github.com/stretchr/testify/assert" -) - -func TestRequest(t *testing.T) { - recorder := test.Request(t, "GET", "/v1", "", map[string]string{"x-helper-id": "17481"}) - test.AssertHTTPStatus(t, http.StatusOK, &recorder) -} - -func TestDecodeResponse(t *testing.T) { - var budgets test.APIResponse - - r := test.Request(t, "GET", "/v1/budgets", "") - test.DecodeResponse(t, &r, &budgets) -} - -func TestDecodeError(t *testing.T) { - m := test.DecodeError(t, []byte(`{"error":"some string"}`)) - assert.Equal(t, "some string", m) -} diff --git a/swag/docs.go b/swag/docs.go index b4916827..768d22a0 100644 --- a/swag/docs.go +++ b/swag/docs.go @@ -1736,7 +1736,7 @@ const docTemplate = `{ "type": "number" }, "budgetId": { - "type": "integer" + "type": "string" }, "createdAt": { "type": "string", @@ -1749,9 +1749,8 @@ const docTemplate = `{ "type": "boolean" }, "id": { - "type": "integer", - "format": "uint64", - "example": 42 + "type": "string", + "example": "65392deb-5e92-4268-b114-297faad6cdce" }, "links": { "$ref": "#/definitions/controllers.AccountLinks" @@ -1780,7 +1779,7 @@ const docTemplate = `{ "properties": { "self": { "type": "string", - "example": "https://example.com/api/v1/accounts/17" + "example": "https://example.com/api/v1/accounts/af892e10-7e0a-4fb8-b1bc-4b6d88401ed2" } } }, @@ -1817,12 +1816,11 @@ const docTemplate = `{ "$ref": "#/definitions/gorm.DeletedAt" }, "envelopeId": { - "type": "integer" + "type": "string" }, "id": { - "type": "integer", - "format": "uint64", - "example": 42 + "type": "string", + "example": "65392deb-5e92-4268-b114-297faad6cdce" }, "links": { "$ref": "#/definitions/controllers.AllocationLinks" @@ -1844,7 +1842,7 @@ const docTemplate = `{ "properties": { "self": { "type": "string", - "example": "https://example.com/api/v1/allocations/47" + "example": "https://example.com/api/v1/allocations/902cd93c-3724-4e46-8540-d014131282fc" } } }, @@ -1882,9 +1880,8 @@ const docTemplate = `{ "$ref": "#/definitions/gorm.DeletedAt" }, "id": { - "type": "integer", - "format": "uint64", - "example": 42 + "type": "string", + "example": "65392deb-5e92-4268-b114-297faad6cdce" }, "links": { "$ref": "#/definitions/controllers.BudgetLinks" @@ -1908,23 +1905,23 @@ const docTemplate = `{ "properties": { "accounts": { "type": "string", - "example": "https://example.com/api/v1/accounts?budget=2" + "example": "https://example.com/api/v1/accounts?budget=550dc009-cea6-4c12-b2a5-03446eb7b7cf" }, "categories": { "type": "string", - "example": "https://example.com/api/v1/categories?budget=2" + "example": "https://example.com/api/v1/categories?budget=550dc009-cea6-4c12-b2a5-03446eb7b7cf" }, "month": { "type": "string", - "example": "https://example.com/api/v1/budgets/2/2022-03" + "example": "https://example.com/api/v1/budgets/550dc009-cea6-4c12-b2a5-03446eb7b7cf/2022-03" }, "self": { "type": "string", - "example": "https://example.com/api/v1/budgets/4" + "example": "https://example.com/api/v1/budgets/550dc009-cea6-4c12-b2a5-03446eb7b7cf" }, "transactions": { "type": "string", - "example": "https://example.com/api/v1/budgets/2/transactions" + "example": "https://example.com/api/v1/transactions?budget=550dc009-cea6-4c12-b2a5-03446eb7b7cf" } } }, @@ -1959,7 +1956,7 @@ const docTemplate = `{ "type": "object", "properties": { "budgetId": { - "type": "integer" + "type": "string" }, "createdAt": { "type": "string", @@ -1969,9 +1966,8 @@ const docTemplate = `{ "$ref": "#/definitions/gorm.DeletedAt" }, "id": { - "type": "integer", - "format": "uint64", - "example": 42 + "type": "string", + "example": "65392deb-5e92-4268-b114-297faad6cdce" }, "links": { "$ref": "#/definitions/controllers.CategoryLinks" @@ -1993,11 +1989,11 @@ const docTemplate = `{ "properties": { "envelopes": { "type": "string", - "example": "https://example.com/api/v1/envelopes?category=7" + "example": "https://example.com/api/v1/envelopes?category=3b1ea324-d438-4419-882a-2fc91d71772f" }, "self": { "type": "string", - "example": "https://example.com/api/v1/categories/7" + "example": "https://example.com/api/v1/categories/3b1ea324-d438-4419-882a-2fc91d71772f" } } }, @@ -2024,7 +2020,7 @@ const docTemplate = `{ "type": "object", "properties": { "categoryId": { - "type": "integer" + "type": "string" }, "createdAt": { "type": "string", @@ -2034,9 +2030,8 @@ const docTemplate = `{ "$ref": "#/definitions/gorm.DeletedAt" }, "id": { - "type": "integer", - "format": "uint64", - "example": 42 + "type": "string", + "example": "65392deb-5e92-4268-b114-297faad6cdce" }, "links": { "$ref": "#/definitions/controllers.EnvelopeLinks" @@ -2058,15 +2053,15 @@ const docTemplate = `{ "properties": { "allocations": { "type": "string", - "example": "https://example.com/api/v1/allocations?envelope=87" + "example": "https://example.com/api/v1/allocations?envelope=45b6b5b9-f746-4ae9-b77b-7688b91f8166" }, "month": { "type": "string", - "example": "https://example.com/api/v1/envelopes/87/2019-03" + "example": "https://example.com/api/v1/envelopes/45b6b5b9-f746-4ae9-b77b-7688b91f8166/2019-03" }, "self": { "type": "string", - "example": "https://example.com/api/v1/envelopes/87" + "example": "https://example.com/api/v1/envelopes/45b6b5b9-f746-4ae9-b77b-7688b91f8166" } } }, @@ -2129,7 +2124,7 @@ const docTemplate = `{ "type": "number" }, "budgetId": { - "type": "integer" + "type": "string" }, "createdAt": { "type": "string", @@ -2142,15 +2137,14 @@ const docTemplate = `{ "$ref": "#/definitions/gorm.DeletedAt" }, "destinationAccountId": { - "type": "integer" + "type": "string" }, "envelopeId": { - "type": "integer" + "type": "string" }, "id": { - "type": "integer", - "format": "uint64", - "example": 42 + "type": "string", + "example": "65392deb-5e92-4268-b114-297faad6cdce" }, "links": { "$ref": "#/definitions/controllers.TransactionLinks" @@ -2162,7 +2156,7 @@ const docTemplate = `{ "type": "boolean" }, "sourceAccountId": { - "type": "integer" + "type": "string" }, "updatedAt": { "type": "string", @@ -2175,7 +2169,7 @@ const docTemplate = `{ "properties": { "self": { "type": "string", - "example": "https://example.com/api/v1/transactions/1741" + "example": "https://example.com/api/v1/transactions/d430d7c3-d14c-4712-9336-ee56965a6673" } } }, @@ -2277,7 +2271,7 @@ const docTemplate = `{ "type": "object", "properties": { "budgetId": { - "type": "integer" + "type": "string" }, "external": { "type": "boolean" @@ -2301,7 +2295,7 @@ const docTemplate = `{ "type": "number" }, "envelopeId": { - "type": "integer" + "type": "string" }, "month": { "type": "integer" @@ -2338,8 +2332,8 @@ const docTemplate = `{ } }, "id": { - "type": "integer", - "example": 23 + "type": "string", + "example": "23" }, "month": { "type": "string", @@ -2355,7 +2349,7 @@ const docTemplate = `{ "type": "object", "properties": { "budgetId": { - "type": "integer" + "type": "string" }, "name": { "type": "string" @@ -2369,7 +2363,7 @@ const docTemplate = `{ "type": "object", "properties": { "categoryId": { - "type": "integer" + "type": "string" }, "name": { "type": "string" @@ -2389,7 +2383,7 @@ const docTemplate = `{ "type": "number" }, "id": { - "type": "integer" + "type": "string" }, "month": { "type": "string" @@ -2409,16 +2403,16 @@ const docTemplate = `{ "type": "number" }, "budgetId": { - "type": "integer" + "type": "string" }, "date": { "type": "string" }, "destinationAccountId": { - "type": "integer" + "type": "string" }, "envelopeId": { - "type": "integer" + "type": "string" }, "note": { "type": "string" @@ -2427,7 +2421,7 @@ const docTemplate = `{ "type": "boolean" }, "sourceAccountId": { - "type": "integer" + "type": "string" } } } diff --git a/swag/swagger.json b/swag/swagger.json index 72fb8aeb..3782656d 100644 --- a/swag/swagger.json +++ b/swag/swagger.json @@ -1724,7 +1724,7 @@ "type": "number" }, "budgetId": { - "type": "integer" + "type": "string" }, "createdAt": { "type": "string", @@ -1737,9 +1737,8 @@ "type": "boolean" }, "id": { - "type": "integer", - "format": "uint64", - "example": 42 + "type": "string", + "example": "65392deb-5e92-4268-b114-297faad6cdce" }, "links": { "$ref": "#/definitions/controllers.AccountLinks" @@ -1768,7 +1767,7 @@ "properties": { "self": { "type": "string", - "example": "https://example.com/api/v1/accounts/17" + "example": "https://example.com/api/v1/accounts/af892e10-7e0a-4fb8-b1bc-4b6d88401ed2" } } }, @@ -1805,12 +1804,11 @@ "$ref": "#/definitions/gorm.DeletedAt" }, "envelopeId": { - "type": "integer" + "type": "string" }, "id": { - "type": "integer", - "format": "uint64", - "example": 42 + "type": "string", + "example": "65392deb-5e92-4268-b114-297faad6cdce" }, "links": { "$ref": "#/definitions/controllers.AllocationLinks" @@ -1832,7 +1830,7 @@ "properties": { "self": { "type": "string", - "example": "https://example.com/api/v1/allocations/47" + "example": "https://example.com/api/v1/allocations/902cd93c-3724-4e46-8540-d014131282fc" } } }, @@ -1870,9 +1868,8 @@ "$ref": "#/definitions/gorm.DeletedAt" }, "id": { - "type": "integer", - "format": "uint64", - "example": 42 + "type": "string", + "example": "65392deb-5e92-4268-b114-297faad6cdce" }, "links": { "$ref": "#/definitions/controllers.BudgetLinks" @@ -1896,23 +1893,23 @@ "properties": { "accounts": { "type": "string", - "example": "https://example.com/api/v1/accounts?budget=2" + "example": "https://example.com/api/v1/accounts?budget=550dc009-cea6-4c12-b2a5-03446eb7b7cf" }, "categories": { "type": "string", - "example": "https://example.com/api/v1/categories?budget=2" + "example": "https://example.com/api/v1/categories?budget=550dc009-cea6-4c12-b2a5-03446eb7b7cf" }, "month": { "type": "string", - "example": "https://example.com/api/v1/budgets/2/2022-03" + "example": "https://example.com/api/v1/budgets/550dc009-cea6-4c12-b2a5-03446eb7b7cf/2022-03" }, "self": { "type": "string", - "example": "https://example.com/api/v1/budgets/4" + "example": "https://example.com/api/v1/budgets/550dc009-cea6-4c12-b2a5-03446eb7b7cf" }, "transactions": { "type": "string", - "example": "https://example.com/api/v1/budgets/2/transactions" + "example": "https://example.com/api/v1/transactions?budget=550dc009-cea6-4c12-b2a5-03446eb7b7cf" } } }, @@ -1947,7 +1944,7 @@ "type": "object", "properties": { "budgetId": { - "type": "integer" + "type": "string" }, "createdAt": { "type": "string", @@ -1957,9 +1954,8 @@ "$ref": "#/definitions/gorm.DeletedAt" }, "id": { - "type": "integer", - "format": "uint64", - "example": 42 + "type": "string", + "example": "65392deb-5e92-4268-b114-297faad6cdce" }, "links": { "$ref": "#/definitions/controllers.CategoryLinks" @@ -1981,11 +1977,11 @@ "properties": { "envelopes": { "type": "string", - "example": "https://example.com/api/v1/envelopes?category=7" + "example": "https://example.com/api/v1/envelopes?category=3b1ea324-d438-4419-882a-2fc91d71772f" }, "self": { "type": "string", - "example": "https://example.com/api/v1/categories/7" + "example": "https://example.com/api/v1/categories/3b1ea324-d438-4419-882a-2fc91d71772f" } } }, @@ -2012,7 +2008,7 @@ "type": "object", "properties": { "categoryId": { - "type": "integer" + "type": "string" }, "createdAt": { "type": "string", @@ -2022,9 +2018,8 @@ "$ref": "#/definitions/gorm.DeletedAt" }, "id": { - "type": "integer", - "format": "uint64", - "example": 42 + "type": "string", + "example": "65392deb-5e92-4268-b114-297faad6cdce" }, "links": { "$ref": "#/definitions/controllers.EnvelopeLinks" @@ -2046,15 +2041,15 @@ "properties": { "allocations": { "type": "string", - "example": "https://example.com/api/v1/allocations?envelope=87" + "example": "https://example.com/api/v1/allocations?envelope=45b6b5b9-f746-4ae9-b77b-7688b91f8166" }, "month": { "type": "string", - "example": "https://example.com/api/v1/envelopes/87/2019-03" + "example": "https://example.com/api/v1/envelopes/45b6b5b9-f746-4ae9-b77b-7688b91f8166/2019-03" }, "self": { "type": "string", - "example": "https://example.com/api/v1/envelopes/87" + "example": "https://example.com/api/v1/envelopes/45b6b5b9-f746-4ae9-b77b-7688b91f8166" } } }, @@ -2117,7 +2112,7 @@ "type": "number" }, "budgetId": { - "type": "integer" + "type": "string" }, "createdAt": { "type": "string", @@ -2130,15 +2125,14 @@ "$ref": "#/definitions/gorm.DeletedAt" }, "destinationAccountId": { - "type": "integer" + "type": "string" }, "envelopeId": { - "type": "integer" + "type": "string" }, "id": { - "type": "integer", - "format": "uint64", - "example": 42 + "type": "string", + "example": "65392deb-5e92-4268-b114-297faad6cdce" }, "links": { "$ref": "#/definitions/controllers.TransactionLinks" @@ -2150,7 +2144,7 @@ "type": "boolean" }, "sourceAccountId": { - "type": "integer" + "type": "string" }, "updatedAt": { "type": "string", @@ -2163,7 +2157,7 @@ "properties": { "self": { "type": "string", - "example": "https://example.com/api/v1/transactions/1741" + "example": "https://example.com/api/v1/transactions/d430d7c3-d14c-4712-9336-ee56965a6673" } } }, @@ -2265,7 +2259,7 @@ "type": "object", "properties": { "budgetId": { - "type": "integer" + "type": "string" }, "external": { "type": "boolean" @@ -2289,7 +2283,7 @@ "type": "number" }, "envelopeId": { - "type": "integer" + "type": "string" }, "month": { "type": "integer" @@ -2326,8 +2320,8 @@ } }, "id": { - "type": "integer", - "example": 23 + "type": "string", + "example": "23" }, "month": { "type": "string", @@ -2343,7 +2337,7 @@ "type": "object", "properties": { "budgetId": { - "type": "integer" + "type": "string" }, "name": { "type": "string" @@ -2357,7 +2351,7 @@ "type": "object", "properties": { "categoryId": { - "type": "integer" + "type": "string" }, "name": { "type": "string" @@ -2377,7 +2371,7 @@ "type": "number" }, "id": { - "type": "integer" + "type": "string" }, "month": { "type": "string" @@ -2397,16 +2391,16 @@ "type": "number" }, "budgetId": { - "type": "integer" + "type": "string" }, "date": { "type": "string" }, "destinationAccountId": { - "type": "integer" + "type": "string" }, "envelopeId": { - "type": "integer" + "type": "string" }, "note": { "type": "string" @@ -2415,7 +2409,7 @@ "type": "boolean" }, "sourceAccountId": { - "type": "integer" + "type": "string" } } } diff --git a/swag/swagger.yaml b/swag/swagger.yaml index 3380d0e7..acedd8ae 100644 --- a/swag/swagger.yaml +++ b/swag/swagger.yaml @@ -4,7 +4,7 @@ definitions: balance: type: number budgetId: - type: integer + type: string createdAt: example: "2022-04-02T19:28:44.491514Z" type: string @@ -13,9 +13,8 @@ definitions: external: type: boolean id: - example: 42 - format: uint64 - type: integer + example: 65392deb-5e92-4268-b114-297faad6cdce + type: string links: $ref: '#/definitions/controllers.AccountLinks' name: @@ -34,7 +33,7 @@ definitions: controllers.AccountLinks: properties: self: - example: https://example.com/api/v1/accounts/17 + example: https://example.com/api/v1/accounts/af892e10-7e0a-4fb8-b1bc-4b6d88401ed2 type: string type: object controllers.AccountListResponse: @@ -59,11 +58,10 @@ definitions: deletedAt: $ref: '#/definitions/gorm.DeletedAt' envelopeId: - type: integer + type: string id: - example: 42 - format: uint64 - type: integer + example: 65392deb-5e92-4268-b114-297faad6cdce + type: string links: $ref: '#/definitions/controllers.AllocationLinks' month: @@ -77,7 +75,7 @@ definitions: controllers.AllocationLinks: properties: self: - example: https://example.com/api/v1/allocations/47 + example: https://example.com/api/v1/allocations/902cd93c-3724-4e46-8540-d014131282fc type: string type: object controllers.AllocationListResponse: @@ -103,9 +101,8 @@ definitions: deletedAt: $ref: '#/definitions/gorm.DeletedAt' id: - example: 42 - format: uint64 - type: integer + example: 65392deb-5e92-4268-b114-297faad6cdce + type: string links: $ref: '#/definitions/controllers.BudgetLinks' name: @@ -121,19 +118,19 @@ definitions: controllers.BudgetLinks: properties: accounts: - example: https://example.com/api/v1/accounts?budget=2 + example: https://example.com/api/v1/accounts?budget=550dc009-cea6-4c12-b2a5-03446eb7b7cf type: string categories: - example: https://example.com/api/v1/categories?budget=2 + example: https://example.com/api/v1/categories?budget=550dc009-cea6-4c12-b2a5-03446eb7b7cf type: string month: - example: https://example.com/api/v1/budgets/2/2022-03 + example: https://example.com/api/v1/budgets/550dc009-cea6-4c12-b2a5-03446eb7b7cf/2022-03 type: string self: - example: https://example.com/api/v1/budgets/4 + example: https://example.com/api/v1/budgets/550dc009-cea6-4c12-b2a5-03446eb7b7cf type: string transactions: - example: https://example.com/api/v1/budgets/2/transactions + example: https://example.com/api/v1/transactions?budget=550dc009-cea6-4c12-b2a5-03446eb7b7cf type: string type: object controllers.BudgetListResponse: @@ -156,16 +153,15 @@ definitions: controllers.Category: properties: budgetId: - type: integer + type: string createdAt: example: "2022-04-02T19:28:44.491514Z" type: string deletedAt: $ref: '#/definitions/gorm.DeletedAt' id: - example: 42 - format: uint64 - type: integer + example: 65392deb-5e92-4268-b114-297faad6cdce + type: string links: $ref: '#/definitions/controllers.CategoryLinks' name: @@ -179,10 +175,10 @@ definitions: controllers.CategoryLinks: properties: envelopes: - example: https://example.com/api/v1/envelopes?category=7 + example: https://example.com/api/v1/envelopes?category=3b1ea324-d438-4419-882a-2fc91d71772f type: string self: - example: https://example.com/api/v1/categories/7 + example: https://example.com/api/v1/categories/3b1ea324-d438-4419-882a-2fc91d71772f type: string type: object controllers.CategoryListResponse: @@ -200,16 +196,15 @@ definitions: controllers.Envelope: properties: categoryId: - type: integer + type: string createdAt: example: "2022-04-02T19:28:44.491514Z" type: string deletedAt: $ref: '#/definitions/gorm.DeletedAt' id: - example: 42 - format: uint64 - type: integer + example: 65392deb-5e92-4268-b114-297faad6cdce + type: string links: $ref: '#/definitions/controllers.EnvelopeLinks' name: @@ -223,13 +218,13 @@ definitions: controllers.EnvelopeLinks: properties: allocations: - example: https://example.com/api/v1/allocations?envelope=87 + example: https://example.com/api/v1/allocations?envelope=45b6b5b9-f746-4ae9-b77b-7688b91f8166 type: string month: - example: https://example.com/api/v1/envelopes/87/2019-03 + example: https://example.com/api/v1/envelopes/45b6b5b9-f746-4ae9-b77b-7688b91f8166/2019-03 type: string self: - example: https://example.com/api/v1/envelopes/87 + example: https://example.com/api/v1/envelopes/45b6b5b9-f746-4ae9-b77b-7688b91f8166 type: string type: object controllers.EnvelopeListResponse: @@ -271,7 +266,7 @@ definitions: amount: type: number budgetId: - type: integer + type: string createdAt: example: "2022-04-02T19:28:44.491514Z" type: string @@ -280,13 +275,12 @@ definitions: deletedAt: $ref: '#/definitions/gorm.DeletedAt' destinationAccountId: - type: integer + type: string envelopeId: - type: integer + type: string id: - example: 42 - format: uint64 - type: integer + example: 65392deb-5e92-4268-b114-297faad6cdce + type: string links: $ref: '#/definitions/controllers.TransactionLinks' note: @@ -294,7 +288,7 @@ definitions: reconciled: type: boolean sourceAccountId: - type: integer + type: string updatedAt: example: "2022-04-17T20:14:01.048145Z" type: string @@ -302,7 +296,7 @@ definitions: controllers.TransactionLinks: properties: self: - example: https://example.com/api/v1/transactions/1741 + example: https://example.com/api/v1/transactions/d430d7c3-d14c-4712-9336-ee56965a6673 type: string type: object controllers.TransactionListResponse: @@ -371,7 +365,7 @@ definitions: models.AccountCreate: properties: budgetId: - type: integer + type: string external: type: boolean name: @@ -387,7 +381,7 @@ definitions: amount: type: number envelopeId: - type: integer + type: string month: type: integer year: @@ -412,8 +406,8 @@ definitions: $ref: '#/definitions/models.EnvelopeMonth' type: array id: - example: 23 - type: integer + example: "23" + type: string month: example: "2006-05-04T15:02:01.000000Z" type: string @@ -424,7 +418,7 @@ definitions: models.CategoryCreate: properties: budgetId: - type: integer + type: string name: type: string note: @@ -433,7 +427,7 @@ definitions: models.EnvelopeCreate: properties: categoryId: - type: integer + type: string name: type: string note: @@ -446,7 +440,7 @@ definitions: balance: type: number id: - type: integer + type: string month: type: string name: @@ -459,19 +453,19 @@ definitions: amount: type: number budgetId: - type: integer + type: string date: type: string destinationAccountId: - type: integer + type: string envelopeId: - type: integer + type: string note: type: string reconciled: type: boolean sourceAccountId: - type: integer + type: string type: object info: contact: {}