Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
26 changes: 15 additions & 11 deletions internal/controllers/account.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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
Expand Down Expand Up @@ -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
}
Expand All @@ -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
}
Expand All @@ -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
}
Expand All @@ -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{
Expand All @@ -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
Expand All @@ -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,
Expand Down
153 changes: 68 additions & 85 deletions internal/controllers/account_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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)
Expand Down Expand Up @@ -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, "")
}
Loading