diff --git a/example-project/cmd/web-api/handlers/account.go b/example-project/cmd/web-api/handlers/account.go index 9f9bda6..b23944f 100644 --- a/example-project/cmd/web-api/handlers/account.go +++ b/example-project/cmd/web-api/handlers/account.go @@ -93,7 +93,7 @@ func (a *Account) Update(ctx context.Context, w http.ResponseWriter, r *http.Req if _, ok := errors.Cause(err).(*web.Error); !ok { err = web.NewRequestError(err, http.StatusBadRequest) } - return web.RespondJsonError(ctx, w, err) + return web.RespondJsonError(ctx, w, err) } err := account.Update(ctx, claims, a.MasterDB, req, v.Now) diff --git a/example-project/cmd/web-api/handlers/project.go b/example-project/cmd/web-api/handlers/project.go index bdd66ea..ccae3e9 100644 --- a/example-project/cmd/web-api/handlers/project.go +++ b/example-project/cmd/web-api/handlers/project.go @@ -22,6 +22,7 @@ type Project struct { } // Find godoc +// TODO: Need to implement unittests on projects/find endpoint. There are none. // @Summary List projects // @Description Find returns the existing projects in the system. // @Tags project @@ -192,7 +193,7 @@ func (p *Project) Create(ctx context.Context, w http.ResponseWriter, r *http.Req if _, ok := errors.Cause(err).(*web.Error); !ok { err = web.NewRequestError(err, http.StatusBadRequest) } - return web.RespondJsonError(ctx, w, err) + return web.RespondJsonError(ctx, w, err) } res, err := project.Create(ctx, claims, p.MasterDB, req, v.Now) @@ -242,7 +243,7 @@ func (p *Project) Update(ctx context.Context, w http.ResponseWriter, r *http.Req if _, ok := errors.Cause(err).(*web.Error); !ok { err = web.NewRequestError(err, http.StatusBadRequest) } - return web.RespondJsonError(ctx, w, err) + return web.RespondJsonError(ctx, w, err) } err := project.Update(ctx, claims, p.MasterDB, req, v.Now) @@ -275,7 +276,6 @@ func (p *Project) Update(ctx context.Context, w http.ResponseWriter, r *http.Req // @Success 204 // @Failure 400 {object} web.ErrorResponse // @Failure 403 {object} web.ErrorResponse -// @Failure 404 {object} web.ErrorResponse // @Failure 500 {object} web.ErrorResponse // @Router /projects/archive [patch] func (p *Project) Archive(ctx context.Context, w http.ResponseWriter, r *http.Request, params map[string]string) error { @@ -294,15 +294,13 @@ func (p *Project) Archive(ctx context.Context, w http.ResponseWriter, r *http.Re if _, ok := errors.Cause(err).(*web.Error); !ok { err = web.NewRequestError(err, http.StatusBadRequest) } - return web.RespondJsonError(ctx, w, err) + return web.RespondJsonError(ctx, w, err) } err := project.Archive(ctx, claims, p.MasterDB, req, v.Now) if err != nil { cause := errors.Cause(err) switch cause { - case project.ErrNotFound: - return web.RespondJsonError(ctx, w, web.NewRequestError(err, http.StatusNotFound)) case project.ErrForbidden: return web.RespondJsonError(ctx, w, web.NewRequestError(err, http.StatusForbidden)) default: @@ -329,7 +327,6 @@ func (p *Project) Archive(ctx context.Context, w http.ResponseWriter, r *http.Re // @Success 204 // @Failure 400 {object} web.ErrorResponse // @Failure 403 {object} web.ErrorResponse -// @Failure 404 {object} web.ErrorResponse // @Failure 500 {object} web.ErrorResponse // @Router /projects/{id} [delete] func (p *Project) Delete(ctx context.Context, w http.ResponseWriter, r *http.Request, params map[string]string) error { @@ -342,11 +339,14 @@ func (p *Project) Delete(ctx context.Context, w http.ResponseWriter, r *http.Req if err != nil { cause := errors.Cause(err) switch cause { - case project.ErrNotFound: - return web.RespondJsonError(ctx, w, web.NewRequestError(err, http.StatusNotFound)) case project.ErrForbidden: return web.RespondJsonError(ctx, w, web.NewRequestError(err, http.StatusForbidden)) default: + _, ok := cause.(validator.ValidationErrors) + if ok { + return web.RespondJsonError(ctx, w, web.NewRequestError(err, http.StatusBadRequest)) + } + return errors.Wrapf(err, "Id: %s", params["id"]) } } diff --git a/example-project/cmd/web-api/handlers/routes.go b/example-project/cmd/web-api/handlers/routes.go index a9119da..f492986 100644 --- a/example-project/cmd/web-api/handlers/routes.go +++ b/example-project/cmd/web-api/handlers/routes.go @@ -42,10 +42,9 @@ func API(shutdown chan os.Signal, log *log.Logger, masterDB *sqlx.DB, redis *red // This route is not authenticated app.Handle("POST", "/v1/oauth/token", u.Token) - // Register user account management endpoints. ua := UserAccount{ - MasterDB: masterDB, + MasterDB: masterDB, } app.Handle("GET", "/v1/user_accounts", ua.Find, mid.Authenticate(authenticator)) app.Handle("POST", "/v1/user_accounts", ua.Create, mid.Authenticate(authenticator), mid.HasRole(auth.RoleAdmin)) diff --git a/example-project/cmd/web-api/handlers/signup.go b/example-project/cmd/web-api/handlers/signup.go index 5719f73..74eaca5 100644 --- a/example-project/cmd/web-api/handlers/signup.go +++ b/example-project/cmd/web-api/handlers/signup.go @@ -45,7 +45,7 @@ func (c *Signup) Signup(ctx context.Context, w http.ResponseWriter, r *http.Requ if _, ok := errors.Cause(err).(*web.Error); !ok { err = web.NewRequestError(err, http.StatusBadRequest) } - return web.RespondJsonError(ctx, w, err) + return web.RespondJsonError(ctx, w, err) } res, err := signup.Signup(ctx, claims, c.MasterDB, req, v.Now) diff --git a/example-project/cmd/web-api/handlers/user.go b/example-project/cmd/web-api/handlers/user.go index 7ca8e86..945c701 100644 --- a/example-project/cmd/web-api/handlers/user.go +++ b/example-project/cmd/web-api/handlers/user.go @@ -27,6 +27,7 @@ type User struct { } // Find godoc +// TODO: Need to implement unittests on users/find endpoint. There are none. // @Summary List users // @Description Find returns the existing users in the system. // @Tags user @@ -40,7 +41,6 @@ type User struct { // @Param included-archived query boolean false "Included Archived, example: false" // @Success 200 {array} user.UserResponse // @Failure 400 {object} web.ErrorResponse -// @Failure 403 {object} web.ErrorResponse // @Failure 500 {object} web.ErrorResponse // @Router /users [get] func (u *User) Find(ctx context.Context, w http.ResponseWriter, r *http.Request, params map[string]string) error { @@ -196,7 +196,7 @@ func (u *User) Create(ctx context.Context, w http.ResponseWriter, r *http.Reques if _, ok := errors.Cause(err).(*web.Error); !ok { err = web.NewRequestError(err, http.StatusBadRequest) } - return web.RespondJsonError(ctx, w, err) + return web.RespondJsonError(ctx, w, err) } res, err := user.Create(ctx, claims, u.MasterDB, req, v.Now) @@ -247,7 +247,7 @@ func (u *User) Update(ctx context.Context, w http.ResponseWriter, r *http.Reques if _, ok := errors.Cause(err).(*web.Error); !ok { err = web.NewRequestError(err, http.StatusBadRequest) } - return web.RespondJsonError(ctx, w, err) + return web.RespondJsonError(ctx, w, err) } err := user.Update(ctx, claims, u.MasterDB, req, v.Now) @@ -298,7 +298,7 @@ func (u *User) UpdatePassword(ctx context.Context, w http.ResponseWriter, r *htt if _, ok := errors.Cause(err).(*web.Error); !ok { err = web.NewRequestError(err, http.StatusBadRequest) } - return web.RespondJsonError(ctx, w, err) + return web.RespondJsonError(ctx, w, err) } err := user.UpdatePassword(ctx, claims, u.MasterDB, req, v.Now) @@ -308,7 +308,7 @@ func (u *User) UpdatePassword(ctx context.Context, w http.ResponseWriter, r *htt case user.ErrNotFound: return web.RespondJsonError(ctx, w, web.NewRequestError(err, http.StatusNotFound)) case user.ErrForbidden: - return web.RespondJsonError(ctx, w, web.NewRequestError(err, http.StatusForbidden)) + return web.RespondJsonError(ctx, w, web.NewRequestError(err, http.StatusForbidden)) default: _, ok := cause.(validator.ValidationErrors) if ok { @@ -333,7 +333,6 @@ func (u *User) UpdatePassword(ctx context.Context, w http.ResponseWriter, r *htt // @Success 204 // @Failure 400 {object} web.ErrorResponse // @Failure 403 {object} web.ErrorResponse -// @Failure 404 {object} web.ErrorResponse // @Failure 500 {object} web.ErrorResponse // @Router /users/archive [patch] func (u *User) Archive(ctx context.Context, w http.ResponseWriter, r *http.Request, params map[string]string) error { @@ -352,15 +351,13 @@ func (u *User) Archive(ctx context.Context, w http.ResponseWriter, r *http.Reque if _, ok := errors.Cause(err).(*web.Error); !ok { err = web.NewRequestError(err, http.StatusBadRequest) } - return web.RespondJsonError(ctx, w, err) + return web.RespondJsonError(ctx, w, err) } err := user.Archive(ctx, claims, u.MasterDB, req, v.Now) if err != nil { cause := errors.Cause(err) switch cause { - case user.ErrNotFound: - return web.RespondJsonError(ctx, w, web.NewRequestError(err, http.StatusNotFound)) case user.ErrForbidden: return web.RespondJsonError(ctx, w, web.NewRequestError(err, http.StatusForbidden)) default: @@ -387,7 +384,6 @@ func (u *User) Archive(ctx context.Context, w http.ResponseWriter, r *http.Reque // @Success 204 // @Failure 400 {object} web.ErrorResponse // @Failure 403 {object} web.ErrorResponse -// @Failure 404 {object} web.ErrorResponse // @Failure 500 {object} web.ErrorResponse // @Router /users/{id} [delete] func (u *User) Delete(ctx context.Context, w http.ResponseWriter, r *http.Request, params map[string]string) error { @@ -400,12 +396,15 @@ func (u *User) Delete(ctx context.Context, w http.ResponseWriter, r *http.Reques if err != nil { cause := errors.Cause(err) switch cause { - case user.ErrNotFound: - return web.RespondJsonError(ctx, w, web.NewRequestError(err, http.StatusNotFound)) case user.ErrForbidden: return web.RespondJsonError(ctx, w, web.NewRequestError(err, http.StatusForbidden)) default: - return errors.Wrapf(cause, "Id: %s", params["id"]) + _, ok := cause.(validator.ValidationErrors) + if ok { + return web.RespondJsonError(ctx, w, web.NewRequestError(err, http.StatusBadRequest)) + } + + return errors.Wrapf(err, "Id: %s", params["id"]) } } @@ -420,10 +419,9 @@ func (u *User) Delete(ctx context.Context, w http.ResponseWriter, r *http.Reques // @Produce json // @Security OAuth2Password // @Param account_id path int true "Account ID" -// @Success 201 +// @Success 200 // @Failure 400 {object} web.ErrorResponse -// @Failure 403 {object} web.ErrorResponse -// @Failure 404 {object} web.ErrorResponse +// @Failure 401 {object} web.ErrorResponse // @Failure 500 {object} web.ErrorResponse // @Router /users/switch-account/{account_id} [patch] func (u *User) SwitchAccount(ctx context.Context, w http.ResponseWriter, r *http.Request, params map[string]string) error { @@ -453,7 +451,7 @@ func (u *User) SwitchAccount(ctx context.Context, w http.ResponseWriter, r *http } } - return web.RespondJson(ctx, w, tkn, http.StatusNoContent) + return web.RespondJson(ctx, w, tkn, http.StatusOK) } // Token godoc @@ -464,10 +462,10 @@ func (u *User) SwitchAccount(ctx context.Context, w http.ResponseWriter, r *http // @Produce json // @Security BasicAuth // @Param scope query string false "Scope" Enums(user, admin) -// @Success 200 {object} user.Token -// @Header 200 {string} Token "qwerty" -// @Failure 400 {object} web.Error -// @Failure 403 {object} web.Error +// @Success 200 +// @Failure 400 {object} web.ErrorResponse +// @Failure 401 {object} web.ErrorResponse +// @Failure 500 {object} web.ErrorResponse // @Router /oauth/token [post] func (u *User) Token(ctx context.Context, w http.ResponseWriter, r *http.Request, params map[string]string) error { v, ok := ctx.Value(web.KeyValues).(*web.Values) @@ -491,6 +489,11 @@ func (u *User) Token(ctx context.Context, w http.ResponseWriter, r *http.Request case user.ErrAuthenticationFailure: return web.RespondJsonError(ctx, w, web.NewRequestError(err, http.StatusUnauthorized)) default: + _, ok := cause.(validator.ValidationErrors) + if ok { + return web.RespondJsonError(ctx, w, web.NewRequestError(err, http.StatusBadRequest)) + } + return errors.Wrap(err, "authenticating") } } diff --git a/example-project/cmd/web-api/handlers/user_account.go b/example-project/cmd/web-api/handlers/user_account.go index 3fb182b..05c36ef 100644 --- a/example-project/cmd/web-api/handlers/user_account.go +++ b/example-project/cmd/web-api/handlers/user_account.go @@ -15,12 +15,13 @@ import ( // UserAccount represents the UserAccount API method handler set. type UserAccount struct { - MasterDB *sqlx.DB + MasterDB *sqlx.DB // ADD OTHER STATE LIKE THE LOGGER AND CONFIG HERE. } // Find godoc +// TODO: Need to implement unittests on user_accounts/find endpoint. There are none. // @Summary List user accounts // @Description Find returns the existing user accounts in the system. // @Tags user_account @@ -191,7 +192,7 @@ func (u *UserAccount) Create(ctx context.Context, w http.ResponseWriter, r *http if _, ok := errors.Cause(err).(*web.Error); !ok { err = web.NewRequestError(err, http.StatusBadRequest) } - return web.RespondJsonError(ctx, w, err) + return web.RespondJsonError(ctx, w, err) } res, err := user_account.Create(ctx, claims, u.MasterDB, req, v.Now) @@ -242,7 +243,7 @@ func (u *UserAccount) Update(ctx context.Context, w http.ResponseWriter, r *http if _, ok := errors.Cause(err).(*web.Error); !ok { err = web.NewRequestError(err, http.StatusBadRequest) } - return web.RespondJsonError(ctx, w, err) + return web.RespondJsonError(ctx, w, err) } err := user_account.Update(ctx, claims, u.MasterDB, req, v.Now) @@ -275,7 +276,6 @@ func (u *UserAccount) Update(ctx context.Context, w http.ResponseWriter, r *http // @Success 204 // @Failure 400 {object} web.ErrorResponse // @Failure 403 {object} web.ErrorResponse -// @Failure 404 {object} web.ErrorResponse // @Failure 500 {object} web.ErrorResponse // @Router /user_accounts/archive [patch] func (u *UserAccount) Archive(ctx context.Context, w http.ResponseWriter, r *http.Request, params map[string]string) error { @@ -294,15 +294,13 @@ func (u *UserAccount) Archive(ctx context.Context, w http.ResponseWriter, r *htt if _, ok := errors.Cause(err).(*web.Error); !ok { err = web.NewRequestError(err, http.StatusBadRequest) } - return web.RespondJsonError(ctx, w, err) + return web.RespondJsonError(ctx, w, err) } err := user_account.Archive(ctx, claims, u.MasterDB, req, v.Now) if err != nil { cause := errors.Cause(err) switch cause { - case user_account.ErrNotFound: - return web.RespondJsonError(ctx, w, web.NewRequestError(err, http.StatusNotFound)) case user_account.ErrForbidden: return web.RespondJsonError(ctx, w, web.NewRequestError(err, http.StatusForbidden)) default: @@ -329,7 +327,6 @@ func (u *UserAccount) Archive(ctx context.Context, w http.ResponseWriter, r *htt // @Success 204 // @Failure 400 {object} web.ErrorResponse // @Failure 403 {object} web.ErrorResponse -// @Failure 404 {object} web.ErrorResponse // @Failure 500 {object} web.ErrorResponse // @Router /user_accounts [delete] func (u *UserAccount) Delete(ctx context.Context, w http.ResponseWriter, r *http.Request, params map[string]string) error { @@ -343,15 +340,13 @@ func (u *UserAccount) Delete(ctx context.Context, w http.ResponseWriter, r *http if _, ok := errors.Cause(err).(*web.Error); !ok { err = web.NewRequestError(err, http.StatusBadRequest) } - return web.RespondJsonError(ctx, w, err) + return web.RespondJsonError(ctx, w, err) } err := user_account.Delete(ctx, claims, u.MasterDB, req) if err != nil { cause := errors.Cause(err) switch cause { - case user_account.ErrNotFound: - return web.RespondJsonError(ctx, w, web.NewRequestError(err, http.StatusNotFound)) case user_account.ErrForbidden: return web.RespondJsonError(ctx, w, web.NewRequestError(err, http.StatusForbidden)) default: @@ -360,7 +355,7 @@ func (u *UserAccount) Delete(ctx context.Context, w http.ResponseWriter, r *http return web.RespondJsonError(ctx, w, web.NewRequestError(err, http.StatusBadRequest)) } - return errors.Wrapf(err, "UserID: %s AccountID: %s User Account: %+v", req.UserID, req.AccountID, &req) + return errors.Wrapf(err, "UserID: %s, AccountID: %s", req.UserID, req.AccountID) } } diff --git a/example-project/cmd/web-api/tests/account_test.go b/example-project/cmd/web-api/tests/account_test.go index 7209902..cf50197 100644 --- a/example-project/cmd/web-api/tests/account_test.go +++ b/example-project/cmd/web-api/tests/account_test.go @@ -5,335 +5,507 @@ import ( "encoding/json" "fmt" "net/http" - "strconv" "testing" - "time" - "geeks-accelerator/oss/saas-starter-kit/example-project/internal/mid" "geeks-accelerator/oss/saas-starter-kit/example-project/internal/account" + "geeks-accelerator/oss/saas-starter-kit/example-project/internal/mid" "geeks-accelerator/oss/saas-starter-kit/example-project/internal/platform/auth" "geeks-accelerator/oss/saas-starter-kit/example-project/internal/platform/tests" "geeks-accelerator/oss/saas-starter-kit/example-project/internal/platform/web" - "github.com/google/go-cmp/cmp" "github.com/pborman/uuid" ) -func mockAccount() *account.Account { - req := account.AccountCreateRequest{ - Name: uuid.NewRandom().String(), - Address1: "103 East Main St", - Address2: "Unit 546", - City: "Valdez", - Region: "AK", - Country: "USA", - Zipcode: "99686", - } - - a, err := account.Create(tests.Context(), auth.Claims{}, test.MasterDB, req, time.Now().UTC().AddDate(-1, -1, -1)) - if err != nil { - panic(err) - } - return a -} - -// TestAccount is the entry point for the account endpoints. -func TestAccount(t *testing.T) { +// TestAccountCRUDAdmin tests all the account CRUD endpoints using an user with role admin. +func TestAccountCRUDAdmin(t *testing.T) { defer tests.Recover(t) - t.Run("getAccount", getAccount) - t.Run("patchAccount", patchAccount) -} + tr := roleTests[auth.RoleAdmin] -// getAccount validates get account by ID endpoint. -func getAccount(t *testing.T) { + s := newMockSignup() + tr.Account = s.account + tr.User = s.user + tr.Token = s.token + tr.Claims = s.claims + ctx := s.context - var rtests []requestTest + // Test create. + { + expectedStatus := http.StatusMethodNotAllowed - forbiddenAccount := mockAccount() - - // Both roles should be able to read the account. - for rn, tr := range roleTests { - acc := tr.SignupResult.Account - - // Test 200. - rtests = append(rtests, requestTest{ - fmt.Sprintf("Role %s 200", rn), - http.MethodGet, - fmt.Sprintf("/v1/accounts/%s", acc.ID), - nil, + req := mockUserCreateRequest() + rt := requestTest{ + fmt.Sprintf("Create %d w/role %s", expectedStatus, tr.Role), + http.MethodPost, + "/v1/accounts", + req, tr.Token, tr.Claims, - http.StatusOK, + expectedStatus, nil, - func(treq requestTest, body []byte) bool { - var actual account.AccountResponse - if err := json.Unmarshal(body, &actual); err != nil { - t.Logf("\t\tGot error : %+v", err) - return false - } + } + t.Logf("\tTest: %s - %s %s", rt.name, rt.method, rt.url) - // Add claims to the context so they can be retrieved later. - ctx := context.WithValue(tests.Context(), auth.Key, tr.Claims) + w, ok := executeRequestTest(t, rt, ctx) + if !ok { + t.Fatalf("\t%s\tExecute request failed.", tests.Failed) + } + t.Logf("\t%s\tReceived valid status code of %d.", tests.Success, w.Code) - expectedMap := map[string]interface{}{ - "updated_at": web.NewTimeResponse(ctx, acc.UpdatedAt), - "id": acc.ID, - "address2": acc.Address2, - "region": acc.Region, - "zipcode": acc.Zipcode, - "timezone": acc.Timezone, - "created_at": web.NewTimeResponse(ctx, acc.CreatedAt), - "country": acc.Country, - "billing_user_id": &acc.BillingUserID, - "name": acc.Name, - "address1": acc.Address1, - "city": acc.City, - "status": map[string]interface{}{ - "value": "active", - "title": "Active", - "options": []map[string]interface{}{{"selected": false, "title": "[Active Pending Disabled]", "value": "[active pending disabled]"}}, - }, - "signup_user_id": &acc.SignupUserID, - } - expectedJson, err := json.Marshal(expectedMap) - if err != nil { - t.Logf("\t\tGot error : %+v", err) - return false - } - - var expected account.AccountResponse - if err := json.Unmarshal([]byte(expectedJson), &expected); err != nil { - t.Logf("\t\tGot error : %+v", err) - printResultMap(ctx, body) - return false - } - - if diff := cmp.Diff(actual, expected); diff != "" { - actualJSON, err := json.MarshalIndent(actual, "", " ") - if err != nil { - t.Logf("\t\tGot error : %+v", err) - return false - } - t.Logf("\t\tGot : %s\n", actualJSON) - - expectedJSON, err := json.MarshalIndent(expected, "", " ") - if err != nil { - t.Logf("\t\tGot error : %+v", err) - return false - } - t.Logf("\t\tExpected : %s\n", expectedJSON) - - t.Logf("\t\tDiff : %s\n", diff) - - if len(expectedMap) == 0 { - printResultMap(ctx, body) - } - - return false - } - - return true - }, - }) - - // Test 404. - invalidID := uuid.NewRandom().String() - rtests = append(rtests, requestTest{ - fmt.Sprintf("Role %s 404 w/invalid ID", rn), - http.MethodGet, - fmt.Sprintf("/v1/accounts/%s", invalidID), - nil, - tr.Token, - tr.Claims, - http.StatusNotFound, - web.ErrorResponse{ - Error: fmt.Sprintf("account %s not found: Entity not found", invalidID), - }, - func(treq requestTest, body []byte) bool { - return true - }, - }) - - // Test 404 - Account exists but not allowed. - rtests = append(rtests, requestTest{ - fmt.Sprintf("Role %s 404 w/random account ID", rn), - http.MethodGet, - fmt.Sprintf("/v1/accounts/%s", forbiddenAccount.ID), - nil, - tr.Token, - tr.Claims, - http.StatusNotFound, - web.ErrorResponse{ - Error: fmt.Sprintf("account %s not found: Entity not found", forbiddenAccount.ID), - }, - func(treq requestTest, body []byte) bool { - return true - }, - }) - } - - runRequestTests(t, rtests) -} - -// patchAccount validates update account by ID endpoint. -func patchAccount(t *testing.T) { - - var rtests []requestTest - - // Test update an account - // Admin role: 204 - // User role 403 - for rn, tr := range roleTests { - var expectedStatus int - var expectedErr interface{} - - // Test 204. - if rn == auth.RoleAdmin { - expectedStatus = http.StatusNoContent - } else { - expectedStatus = http.StatusForbidden - expectedErr = web.ErrorResponse{ - Error: mid.ErrForbidden.Error(), + if len(w.Body.String()) != 0 { + if diff := cmpDiff(t, w.Body.Bytes(), nil); diff { + t.Fatalf("\t%s\tReceived expected empty.", tests.Failed) } } + t.Logf("\t%s\tReceived expected empty.", tests.Success) + } - newName := rn + uuid.NewRandom().String() + strconv.Itoa(len(rtests)) - rtests = append(rtests, requestTest{ - fmt.Sprintf("Role %s %d", rn, expectedStatus), + // Test read. + { + expectedStatus := http.StatusOK + + rt := requestTest{ + fmt.Sprintf("Read %d w/role %s", expectedStatus, tr.Role), + http.MethodGet, + fmt.Sprintf("/v1/accounts/%s", tr.Account.ID), + nil, + tr.Token, + tr.Claims, + expectedStatus, + nil, + } + t.Logf("\tTest: %s - %s %s", rt.name, rt.method, rt.url) + + w, ok := executeRequestTest(t, rt, ctx) + if !ok { + t.Fatalf("\t%s\tExecute request failed.", tests.Failed) + } + t.Logf("\t%s\tReceived valid status code of %d.", tests.Success, w.Code) + + var actual account.AccountResponse + if err := json.Unmarshal(w.Body.Bytes(), &actual); err != nil { + t.Logf("\t\tGot error : %+v", err) + t.Fatalf("\t%s\tDecode response body failed.", tests.Failed) + } + + expectedMap := map[string]interface{}{ + "updated_at": web.NewTimeResponse(ctx, tr.Account.UpdatedAt), + "id": tr.Account.ID, + "address2": tr.Account.Address2, + "region": tr.Account.Region, + "zipcode": tr.Account.Zipcode, + "timezone": tr.Account.Timezone, + "created_at": web.NewTimeResponse(ctx, tr.Account.CreatedAt), + "country": tr.Account.Country, + "billing_user_id": &tr.Account.BillingUserID.String, + "name": tr.Account.Name, + "address1": tr.Account.Address1, + "city": tr.Account.City, + "status": map[string]interface{}{ + "value": "active", + "title": "Active", + "options": []map[string]interface{}{{"selected": false, "title": "[Active Pending Disabled]", "value": "[active pending disabled]"}}, + }, + "signup_user_id": &tr.Account.SignupUserID.String, + } + + var expected account.AccountResponse + if err := decodeMapToStruct(expectedMap, &expected); err != nil { + t.Logf("\t\tGot error : %+v\nActual results to format expected : \n", err) + printResultMap(ctx, w.Body.Bytes()) // used to help format expectedMap + t.Fatalf("\t%s\tDecode expected failed.", tests.Failed) + } + + if diff := cmpDiff(t, actual, expected); diff { + t.Fatalf("\t%s\tReceived expected result.", tests.Failed) + } + t.Logf("\t%s\tReceived expected result.", tests.Success) + } + + // Test Read with random ID. + { + expectedStatus := http.StatusNotFound + + randID := uuid.NewRandom().String() + rt := requestTest{ + fmt.Sprintf("Read %d w/role %s using random ID", expectedStatus, tr.Role), + http.MethodGet, + fmt.Sprintf("/v1/accounts/%s", randID), + nil, + tr.Token, + tr.Claims, + expectedStatus, + nil, + } + t.Logf("\tTest: %s - %s %s", rt.name, rt.method, rt.url) + + w, ok := executeRequestTest(t, rt, ctx) + if !ok { + t.Fatalf("\t%s\tExecute request failed.", tests.Failed) + } + t.Logf("\t%s\tReceived valid status code of %d.", tests.Success, w.Code) + + var actual web.ErrorResponse + if err := json.Unmarshal(w.Body.Bytes(), &actual); err != nil { + t.Logf("\t\tGot error : %+v", err) + t.Fatalf("\t%s\tDecode response body failed.", tests.Failed) + } + + expected := web.ErrorResponse{ + Error: fmt.Sprintf("account %s not found: Entity not found", randID), + } + + if diff := cmpDiff(t, actual, expected); diff { + t.Fatalf("\t%s\tReceived expected error.", tests.Failed) + } + t.Logf("\t%s\tReceived expected error.", tests.Success) + } + + // Test Read with forbidden ID. + { + expectedStatus := http.StatusNotFound + + rt := requestTest{ + fmt.Sprintf("Read %d w/role %s using forbidden ID", expectedStatus, tr.Role), + http.MethodGet, + fmt.Sprintf("/v1/accounts/%s", tr.ForbiddenAccount.ID), + nil, + tr.Token, + tr.Claims, + expectedStatus, + nil, + } + t.Logf("\tTest: %s - %s %s", rt.name, rt.method, rt.url) + + w, ok := executeRequestTest(t, rt, ctx) + if !ok { + t.Fatalf("\t%s\tExecute request failed.", tests.Failed) + } + t.Logf("\t%s\tReceived valid status code of %d.", tests.Success, w.Code) + + var actual web.ErrorResponse + if err := json.Unmarshal(w.Body.Bytes(), &actual); err != nil { + t.Logf("\t\tGot error : %+v", err) + t.Fatalf("\t%s\tDecode response body failed.", tests.Failed) + } + + expected := web.ErrorResponse{ + Error: fmt.Sprintf("account %s not found: Entity not found", tr.ForbiddenAccount.ID), + } + + if diff := cmpDiff(t, actual, expected); diff { + t.Fatalf("\t%s\tReceived expected error.", tests.Failed) + } + t.Logf("\t%s\tReceived expected error.", tests.Success) + } + + // Test update. + { + expectedStatus := http.StatusNoContent + + newName := uuid.NewRandom().String() + rt := requestTest{ + fmt.Sprintf("Update %d w/role %s", expectedStatus, tr.Role), http.MethodPatch, "/v1/accounts", account.AccountUpdateRequest{ - ID: tr.SignupResult.Account.ID, + ID: tr.Account.ID, Name: &newName, }, tr.Token, tr.Claims, expectedStatus, - expectedErr, - func(treq requestTest, body []byte) bool { - return true - }, - }) - } + nil, + } + t.Logf("\tTest: %s - %s %s", rt.name, rt.method, rt.url) - // Test update an account with invalid data. - // Admin role: 400 - // User role 400 - for rn, tr := range roleTests { - var expectedStatus int - var expectedErr interface{} + w, ok := executeRequestTest(t, rt, ctx) + if !ok { + t.Fatalf("\t%s\tExecute request failed.", tests.Failed) + } + t.Logf("\t%s\tReceived valid status code of %d.", tests.Success, w.Code) - if rn == auth.RoleAdmin { - expectedStatus = http.StatusBadRequest - expectedErr = web.ErrorResponse{ - Error: "field validation error", - Fields: []web.FieldError{ - {Field: "status", Error: "Key: 'AccountUpdateRequest.status' Error:Field validation for 'status' failed on the 'oneof' tag"}, - }, - } - } else { - expectedStatus = http.StatusForbidden - expectedErr = web.ErrorResponse{ - Error: mid.ErrForbidden.Error(), + if len(w.Body.String()) != 0 { + if diff := cmpDiff(t, w.Body.Bytes(), nil); diff { + t.Fatalf("\t%s\tReceived expected empty.", tests.Failed) } } + t.Logf("\t%s\tReceived expected empty.", tests.Success) + } +} + +// TestAccountCRUDUser tests all the account CRUD endpoints using an user with role user. +func TestAccountCRUDUser(t *testing.T) { + defer tests.Recover(t) + + tr := roleTests[auth.RoleUser] + + // Add claims to the context for the user. + ctx := context.WithValue(tests.Context(), auth.Key, tr.Claims) + + // Test create. + { + expectedStatus := http.StatusMethodNotAllowed + + req := mockUserCreateRequest() + rt := requestTest{ + fmt.Sprintf("Create %d w/role %s", expectedStatus, tr.Role), + http.MethodPost, + "/v1/accounts", + req, + tr.Token, + tr.Claims, + expectedStatus, + nil, + } + t.Logf("\tTest: %s - %s %s", rt.name, rt.method, rt.url) + + w, ok := executeRequestTest(t, rt, ctx) + if !ok { + t.Fatalf("\t%s\tExecute request failed.", tests.Failed) + } + t.Logf("\t%s\tReceived valid status code of %d.", tests.Success, w.Code) + + if len(w.Body.String()) != 0 { + if diff := cmpDiff(t, w.Body.Bytes(), nil); diff { + t.Fatalf("\t%s\tReceived expected empty.", tests.Failed) + } + } + t.Logf("\t%s\tReceived expected empty.", tests.Success) + } + + // Test read. + { + expectedStatus := http.StatusOK + + rt := requestTest{ + fmt.Sprintf("Read %d w/role %s", expectedStatus, tr.Role), + http.MethodGet, + fmt.Sprintf("/v1/accounts/%s", tr.Account.ID), + nil, + tr.Token, + tr.Claims, + expectedStatus, + nil, + } + t.Logf("\tTest: %s - %s %s", rt.name, rt.method, rt.url) + + w, ok := executeRequestTest(t, rt, ctx) + if !ok { + t.Fatalf("\t%s\tExecute request failed.", tests.Failed) + } + t.Logf("\t%s\tReceived valid status code of %d.", tests.Success, w.Code) + + var actual account.AccountResponse + if err := json.Unmarshal(w.Body.Bytes(), &actual); err != nil { + t.Logf("\t\tGot error : %+v", err) + t.Fatalf("\t%s\tDecode response body failed.", tests.Failed) + } + + expectedMap := map[string]interface{}{ + "updated_at": web.NewTimeResponse(ctx, tr.Account.UpdatedAt), + "id": tr.Account.ID, + "address2": tr.Account.Address2, + "region": tr.Account.Region, + "zipcode": tr.Account.Zipcode, + "timezone": tr.Account.Timezone, + "created_at": web.NewTimeResponse(ctx, tr.Account.CreatedAt), + "country": tr.Account.Country, + "billing_user_id": &tr.Account.BillingUserID.String, + "name": tr.Account.Name, + "address1": tr.Account.Address1, + "city": tr.Account.City, + "status": map[string]interface{}{ + "value": "active", + "title": "Active", + "options": []map[string]interface{}{{"selected": false, "title": "[Active Pending Disabled]", "value": "[active pending disabled]"}}, + }, + "signup_user_id": &tr.Account.SignupUserID.String, + } + + var expected account.AccountResponse + if err := decodeMapToStruct(expectedMap, &expected); err != nil { + t.Logf("\t\tGot error : %+v\nActual results to format expected : \n", err) + printResultMap(ctx, w.Body.Bytes()) // used to help format expectedMap + t.Fatalf("\t%s\tDecode expected failed.", tests.Failed) + } + + if diff := cmpDiff(t, actual, expected); diff { + t.Fatalf("\t%s\tReceived expected result.", tests.Failed) + } + t.Logf("\t%s\tReceived expected result.", tests.Success) + } + + // Test Read with random ID. + { + expectedStatus := http.StatusNotFound + + randID := uuid.NewRandom().String() + rt := requestTest{ + fmt.Sprintf("Read %d w/role %s using random ID", expectedStatus, tr.Role), + http.MethodGet, + fmt.Sprintf("/v1/accounts/%s", randID), + nil, + tr.Token, + tr.Claims, + expectedStatus, + nil, + } + t.Logf("\tTest: %s - %s %s", rt.name, rt.method, rt.url) + + w, ok := executeRequestTest(t, rt, ctx) + if !ok { + t.Fatalf("\t%s\tExecute request failed.", tests.Failed) + } + t.Logf("\t%s\tReceived valid status code of %d.", tests.Success, w.Code) + + var actual web.ErrorResponse + if err := json.Unmarshal(w.Body.Bytes(), &actual); err != nil { + t.Logf("\t\tGot error : %+v", err) + t.Fatalf("\t%s\tDecode response body failed.", tests.Failed) + } + + expected := web.ErrorResponse{ + Error: fmt.Sprintf("account %s not found: Entity not found", randID), + } + + if diff := cmpDiff(t, actual, expected); diff { + t.Fatalf("\t%s\tReceived expected error.", tests.Failed) + } + t.Logf("\t%s\tReceived expected error.", tests.Success) + } + + // Test Read with forbidden ID. + { + expectedStatus := http.StatusNotFound + + rt := requestTest{ + fmt.Sprintf("Read %d w/role %s using forbidden ID", expectedStatus, tr.Role), + http.MethodGet, + fmt.Sprintf("/v1/accounts/%s", tr.ForbiddenAccount.ID), + nil, + tr.Token, + tr.Claims, + expectedStatus, + nil, + } + t.Logf("\tTest: %s - %s %s", rt.name, rt.method, rt.url) + + w, ok := executeRequestTest(t, rt, ctx) + if !ok { + t.Fatalf("\t%s\tExecute request failed.", tests.Failed) + } + t.Logf("\t%s\tReceived valid status code of %d.", tests.Success, w.Code) + + var actual web.ErrorResponse + if err := json.Unmarshal(w.Body.Bytes(), &actual); err != nil { + t.Logf("\t\tGot error : %+v", err) + t.Fatalf("\t%s\tDecode response body failed.", tests.Failed) + } + + expected := web.ErrorResponse{ + Error: fmt.Sprintf("account %s not found: Entity not found", tr.ForbiddenAccount.ID), + } + + if diff := cmpDiff(t, actual, expected); diff { + t.Fatalf("\t%s\tReceived expected error.", tests.Failed) + } + t.Logf("\t%s\tReceived expected error.", tests.Success) + } + + // Test update. + { + expectedStatus := http.StatusForbidden + + newName := uuid.NewRandom().String() + rt := requestTest{ + fmt.Sprintf("Update %d w/role %s", expectedStatus, tr.Role), + http.MethodPatch, + "/v1/accounts", + account.AccountUpdateRequest{ + ID: tr.Account.ID, + Name: &newName, + }, + tr.Token, + tr.Claims, + expectedStatus, + nil, + } + t.Logf("\tTest: %s - %s %s", rt.name, rt.method, rt.url) + + w, ok := executeRequestTest(t, rt, ctx) + if !ok { + t.Fatalf("\t%s\tExecute request failed.", tests.Failed) + } + t.Logf("\t%s\tReceived valid status code of %d.", tests.Success, w.Code) + + var actual web.ErrorResponse + if err := json.Unmarshal(w.Body.Bytes(), &actual); err != nil { + t.Logf("\t\tGot error : %+v", err) + t.Fatalf("\t%s\tDecode response body failed.", tests.Failed) + } + + expected := web.ErrorResponse{ + Error: mid.ErrForbidden.Error(), + } + + if diff := cmpDiff(t, actual, expected); diff { + t.Fatalf("\t%s\tReceived expected error.", tests.Failed) + } + t.Logf("\t%s\tReceived expected error.", tests.Success) + } +} + +// TestAccountUpdate validates update account by ID endpoint. +func TestAccountUpdate(t *testing.T) { + defer tests.Recover(t) + + tr := roleTests[auth.RoleAdmin] + + // Add claims to the context for the user. + ctx := context.WithValue(tests.Context(), auth.Key, tr.Claims) + + // Test create with invalid data. + { + expectedStatus := http.StatusBadRequest invalidStatus := account.AccountStatus("invalid status") - rtests = append(rtests, requestTest{ - fmt.Sprintf("Role %s %d w/invalid data", rn, expectedStatus), + rt := requestTest{ + fmt.Sprintf("Update %d w/role %s using invalid data", expectedStatus, tr.Role), http.MethodPatch, "/v1/accounts", account.AccountUpdateRequest{ - ID: tr.SignupResult.User.ID, - Status: &invalidStatus, + ID: tr.Account.ID, + Status: &invalidStatus, }, tr.Token, tr.Claims, expectedStatus, - expectedErr, - func(treq requestTest, body []byte) bool { - return true - }, - }) - } - - // Test update an account for with an invalid ID. - // Admin role: 403 - // User role 403 - for rn, tr := range roleTests { - var expectedStatus int - var expectedErr interface{} - - // Test 403. - if rn == auth.RoleAdmin { - expectedStatus = http.StatusForbidden - expectedErr = web.ErrorResponse{ - Error: account.ErrForbidden.Error(), - } - } else { - expectedStatus = http.StatusForbidden - expectedErr = web.ErrorResponse{ - Error: mid.ErrForbidden.Error(), - } + nil, } - newName := rn + uuid.NewRandom().String() + strconv.Itoa(len(rtests)) - invalidID := uuid.NewRandom().String() - rtests = append(rtests, requestTest{ - fmt.Sprintf("Role %s %d w/invalid ID", rn, expectedStatus), - http.MethodPatch, - "/v1/accounts", - account.AccountUpdateRequest{ - ID: invalidID, - Name: &newName, - }, - tr.Token, - tr.Claims, - expectedStatus, - expectedErr, - func(treq requestTest, body []byte) bool { - return true - }, - }) - } + t.Logf("\tTest: %s - %s %s", rt.name, rt.method, rt.url) - // Test update an account for with random account ID. - // Admin role: 403 - // User role 403 - forbiddenAccount := mockAccount() - for rn, tr := range roleTests { - var expectedStatus int - var expectedErr interface{} - - // Test 403. - if rn == auth.RoleAdmin { - expectedStatus = http.StatusForbidden - expectedErr = web.ErrorResponse{ - Error: account.ErrForbidden.Error(), - } - } else { - expectedStatus = http.StatusForbidden - expectedErr = web.ErrorResponse{ - Error: mid.ErrForbidden.Error(), - } + w, ok := executeRequestTest(t, rt, ctx) + if !ok { + t.Fatalf("\t%s\tExecute request failed.", tests.Failed) } - newName := rn+uuid.NewRandom().String()+strconv.Itoa(len(rtests)) - rtests = append(rtests, requestTest{ - fmt.Sprintf("Role %s %d w/random account ID", rn, expectedStatus), - http.MethodPatch, - "/v1/accounts", - account.AccountUpdateRequest{ - ID: forbiddenAccount.ID, - Name: &newName, - }, - tr.Token, - tr.Claims, - expectedStatus, - expectedErr, - func(treq requestTest, body []byte) bool { - return true - }, - }) - } + t.Logf("\t%s\tReceived valid status code of %d.", tests.Success, w.Code) - runRequestTests(t, rtests) + var actual web.ErrorResponse + if err := json.Unmarshal(w.Body.Bytes(), &actual); err != nil { + t.Logf("\t\tGot error : %+v", err) + t.Fatalf("\t%s\tDecode response body failed.", tests.Failed) + } + + expected := web.ErrorResponse{ + Error: "field validation error", + Fields: []web.FieldError{ + {Field: "status", Error: "Key: 'AccountUpdateRequest.status' Error:Field validation for 'status' failed on the 'oneof' tag"}, + }, + } + + if diff := cmpDiff(t, actual, expected); diff { + t.Fatalf("\t%s\tReceived expected error.", tests.Failed) + } + t.Logf("\t%s\tReceived expected error.", tests.Success) + } } diff --git a/example-project/cmd/web-api/tests/project_test.go b/example-project/cmd/web-api/tests/project_test.go index 780ee94..aeb16fd 100644 --- a/example-project/cmd/web-api/tests/project_test.go +++ b/example-project/cmd/web-api/tests/project_test.go @@ -1,452 +1,888 @@ package tests -/* import ( - "bytes" + "context" "encoding/json" + "fmt" "net/http" - "net/http/httptest" - "strings" "testing" + "time" + "geeks-accelerator/oss/saas-starter-kit/example-project/internal/mid" + "geeks-accelerator/oss/saas-starter-kit/example-project/internal/platform/auth" "geeks-accelerator/oss/saas-starter-kit/example-project/internal/platform/tests" "geeks-accelerator/oss/saas-starter-kit/example-project/internal/platform/web" "geeks-accelerator/oss/saas-starter-kit/example-project/internal/project" - "github.com/google/go-cmp/cmp" - "github.com/google/go-cmp/cmp/cmpopts" - "gopkg.in/mgo.v2/bson" + "github.com/pborman/uuid" ) - -// TestProjects is the entry point for the projects -func TestProjects(t *testing.T) { - defer tests.Recover(t) - - t.Run("getProjects200Empty", getProjects200Empty) - t.Run("postProject400", postProject400) - t.Run("postProject401", postProject401) - t.Run("getProject404", getProject404) - t.Run("getProject400", getProject400) - t.Run("deleteProject404", deleteProject404) - t.Run("putProject404", putProject404) - t.Run("crudProjects", crudProject) -} - -// TODO: need to test Archive - -// getProjects200Empty validates an empty projects list can be retrieved with the endpoint. -func getProjects200Empty(t *testing.T) { - r := httptest.NewRequest("GET", "/v1/projects", nil) - w := httptest.NewRecorder() - - r.Header.Set("Authorization", userAuthorization) - - a.ServeHTTP(w, r) - - t.Log("Given the need to fetch an empty list of projects with the projects endpoint.") - { - t.Log("\tTest 0:\tWhen fetching an empty project list.") - { - if w.Code != http.StatusOK { - t.Fatalf("\t%s\tShould receive a status code of 200 for the response : %v", tests.Failed, w.Code) - } - t.Logf("\t%s\tShould receive a status code of 200 for the response.", tests.Success) - - recv := w.Body.String() - resp := `[]` - if resp != recv { - t.Log("Got :", recv) - t.Log("Want:", resp) - t.Fatalf("\t%s\tShould get the expected result.", tests.Failed) - } - t.Logf("\t%s\tShould get the expected result.", tests.Success) - } +func mockProjectCreateRequest(accountID string) project.ProjectCreateRequest { + return project.ProjectCreateRequest{ + Name: fmt.Sprintf("Moon Launch %s", uuid.NewRandom().String()), + AccountID: accountID, } } -// postProject400 validates a project can't be created with the endpoint -// unless a valid project document is submitted. -func postProject400(t *testing.T) { - r := httptest.NewRequest("POST", "/v1/projects", strings.NewReader(`{}`)) - w := httptest.NewRecorder() - - r.Header.Set("Authorization", userAuthorization) - - a.ServeHTTP(w, r) - - t.Log("Given the need to validate a new project can't be created with an invalid document.") - { - t.Log("\tTest 0:\tWhen using an incomplete project value.") - { - if w.Code != http.StatusBadRequest { - t.Fatalf("\t%s\tShould receive a status code of 400 for the response : %v", tests.Failed, w.Code) - } - t.Logf("\t%s\tShould receive a status code of 400 for the response.", tests.Success) - - // Inspect the response. - var got web.ErrorResponse - if err := json.NewDecoder(w.Body).Decode(&got); err != nil { - t.Fatalf("\t%s\tShould be able to unmarshal the response to an error type : %v", tests.Failed, err) - } - t.Logf("\t%s\tShould be able to unmarshal the response to an error type.", tests.Success) - - // Define what we want to see. - want := web.ErrorResponse{ - Error: "field validation error", - Fields: []web.FieldError{ - {Field: "name", Error: "name is a required field"}, - {Field: "cost", Error: "cost is a required field"}, - {Field: "quantity", Error: "quantity is a required field"}, - }, - } - - // We can't rely on the order of the field errors so they have to be - // sorted. Tell the cmp package how to sort them. - sorter := cmpopts.SortSlices(func(a, b web.FieldError) bool { - return a.Field < b.Field - }) - - if diff := cmp.Diff(want, got, sorter); diff != "" { - t.Fatalf("\t%s\tShould get the expected result. Diff:\n%s", tests.Failed, diff) - } - t.Logf("\t%s\tShould get the expected result.", tests.Success) - } - } -} - -// postProject401 validates a project can't be created with the endpoint -// unless the user is authenticated -func postProject401(t *testing.T) { - np := project.NewProject{ - Name: "Comic Books", - Cost: 25, - Quantity: 60, - } - - body, err := json.Marshal(&np) +// mockProject creates a new project for testing and associates it with the supplied account ID. +func newMockProject(accountID string) *project.Project { + req := mockProjectCreateRequest(accountID) + p, err := project.Create(tests.Context(), auth.Claims{}, test.MasterDB, req, time.Now().UTC().AddDate(-1, -1, -1)) if err != nil { - t.Fatal(err) - } - - r := httptest.NewRequest("POST", "/v1/projects", bytes.NewBuffer(body)) - w := httptest.NewRecorder() - - // Not setting an authorization header - - a.ServeHTTP(w, r) - - t.Log("Given the need to validate a new project can't be created with an invalid document.") - { - t.Log("\tTest 0:\tWhen using an incomplete project value.") - { - if w.Code != http.StatusUnauthorized { - t.Fatalf("\t%s\tShould receive a status code of 401 for the response : %v", tests.Failed, w.Code) - } - t.Logf("\t%s\tShould receive a status code of 401 for the response.", tests.Success) - } - } -} - -// getProject400 validates a project request for a malformed id. -func getProject400(t *testing.T) { - id := "12345" - - r := httptest.NewRequest("GET", "/v1/projects/"+id, nil) - w := httptest.NewRecorder() - - r.Header.Set("Authorization", userAuthorization) - - a.ServeHTTP(w, r) - - t.Log("Given the need to validate getting a project with a malformed id.") - { - t.Logf("\tTest 0:\tWhen using the new project %s.", id) - { - if w.Code != http.StatusBadRequest { - t.Fatalf("\t%s\tShould receive a status code of 400 for the response : %v", tests.Failed, w.Code) - } - t.Logf("\t%s\tShould receive a status code of 400 for the response.", tests.Success) - - recv := w.Body.String() - resp := `{"error":"ID is not in its proper form"}` - if resp != recv { - t.Log("Got :", recv) - t.Log("Want:", resp) - t.Fatalf("\t%s\tShould get the expected result.", tests.Failed) - } - t.Logf("\t%s\tShould get the expected result.", tests.Success) - } - } -} - -// getProject404 validates a project request for a project that does not exist with the endpoint. -func getProject404(t *testing.T) { - id := bson.NewObjectId().Hex() - - r := httptest.NewRequest("GET", "/v1/projects/"+id, nil) - w := httptest.NewRecorder() - - r.Header.Set("Authorization", userAuthorization) - - a.ServeHTTP(w, r) - - t.Log("Given the need to validate getting a project with an unknown id.") - { - t.Logf("\tTest 0:\tWhen using the new project %s.", id) - { - if w.Code != http.StatusNotFound { - t.Fatalf("\t%s\tShould receive a status code of 404 for the response : %v", tests.Failed, w.Code) - } - t.Logf("\t%s\tShould receive a status code of 404 for the response.", tests.Success) - - recv := w.Body.String() - resp := "Entity not found" - if !strings.Contains(recv, resp) { - t.Log("Got :", recv) - t.Log("Want:", resp) - t.Fatalf("\t%s\tShould get the expected result.", tests.Failed) - } - t.Logf("\t%s\tShould get the expected result.", tests.Success) - } - } -} - -// deleteProject404 validates deleting a project that does not exist. -func deleteProject404(t *testing.T) { - id := bson.NewObjectId().Hex() - - r := httptest.NewRequest("DELETE", "/v1/projects/"+id, nil) - w := httptest.NewRecorder() - - r.Header.Set("Authorization", userAuthorization) - - a.ServeHTTP(w, r) - - t.Log("Given the need to validate deleting a project that does not exist.") - { - t.Logf("\tTest 0:\tWhen using the new project %s.", id) - { - if w.Code != http.StatusNotFound { - t.Fatalf("\t%s\tShould receive a status code of 404 for the response : %v", tests.Failed, w.Code) - } - t.Logf("\t%s\tShould receive a status code of 404 for the response.", tests.Success) - - recv := w.Body.String() - resp := "Entity not found" - if !strings.Contains(recv, resp) { - t.Log("Got :", recv) - t.Log("Want:", resp) - t.Fatalf("\t%s\tShould get the expected result.", tests.Failed) - } - t.Logf("\t%s\tShould get the expected result.", tests.Success) - } - } -} - -// putProject404 validates updating a project that does not exist. -func putProject404(t *testing.T) { - up := project.UpdateProject{ - Name: tests.StringPointer("Nonexistent"), - } - - id := bson.NewObjectId().Hex() - - body, err := json.Marshal(&up) - if err != nil { - t.Fatal(err) - } - - r := httptest.NewRequest("PUT", "/v1/projects/"+id, bytes.NewBuffer(body)) - w := httptest.NewRecorder() - - r.Header.Set("Authorization", userAuthorization) - - a.ServeHTTP(w, r) - - t.Log("Given the need to validate updating a project that does not exist.") - { - t.Logf("\tTest 0:\tWhen using the new project %s.", id) - { - if w.Code != http.StatusNotFound { - t.Fatalf("\t%s\tShould receive a status code of 404 for the response : %v", tests.Failed, w.Code) - } - t.Logf("\t%s\tShould receive a status code of 404 for the response.", tests.Success) - - recv := w.Body.String() - resp := "Entity not found" - if !strings.Contains(recv, resp) { - t.Log("Got :", recv) - t.Log("Want:", resp) - t.Fatalf("\t%s\tShould get the expected result.", tests.Failed) - } - t.Logf("\t%s\tShould get the expected result.", tests.Success) - } - } -} - -// crudProject performs a complete test of CRUD against the api. -func crudProject(t *testing.T) { - p := postProject201(t) - defer deleteProject204(t, p.ID.Hex()) - - getProject200(t, p.ID.Hex()) - putProject204(t, p.ID.Hex()) -} - -// postProject201 validates a project can be created with the endpoint. -func postProject201(t *testing.T) project.Project { - np := project.NewProject{ - Name: "Comic Books", - Cost: 25, - Quantity: 60, - } - - body, err := json.Marshal(&np) - if err != nil { - t.Fatal(err) - } - - r := httptest.NewRequest("POST", "/v1/projects", bytes.NewBuffer(body)) - w := httptest.NewRecorder() - - r.Header.Set("Authorization", userAuthorization) - - a.ServeHTTP(w, r) - - // p is the value we will return. - var p project.Project - - t.Log("Given the need to create a new project with the projects endpoint.") - { - t.Log("\tTest 0:\tWhen using the declared project value.") - { - if w.Code != http.StatusCreated { - t.Fatalf("\t%s\tShould receive a status code of 201 for the response : %v", tests.Failed, w.Code) - } - t.Logf("\t%s\tShould receive a status code of 201 for the response.", tests.Success) - - if err := json.NewDecoder(w.Body).Decode(&p); err != nil { - t.Fatalf("\t%s\tShould be able to unmarshal the response : %v", tests.Failed, err) - } - - // Define what we wanted to receive. We will just trust the generated - // fields like ID and Dates so we copy p. - want := p - want.Name = "Comic Books" - want.Cost = 25 - want.Quantity = 60 - - if diff := cmp.Diff(want, p); diff != "" { - t.Fatalf("\t%s\tShould get the expected result. Diff:\n%s", tests.Failed, diff) - } - t.Logf("\t%s\tShould get the expected result.", tests.Success) - } + panic(err) } return p } -// deleteProject200 validates deleting a project that does exist. -func deleteProject204(t *testing.T, id string) { - r := httptest.NewRequest("DELETE", "/v1/projects/"+id, nil) - w := httptest.NewRecorder() +// TestProjectCRUDAdmin tests all the project CRUD endpoints using an user with role admin. +func TestProjectCRUDAdmin(t *testing.T) { + defer tests.Recover(t) - r.Header.Set("Authorization", userAuthorization) + tr := roleTests[auth.RoleAdmin] - a.ServeHTTP(w, r) + // Add claims to the context for the project. + ctx := context.WithValue(tests.Context(), auth.Key, tr.Claims) - t.Log("Given the need to validate deleting a project that does exist.") + // Test create. + var created project.ProjectResponse { - t.Logf("\tTest 0:\tWhen using the new project %s.", id) - { - if w.Code != http.StatusNoContent { - t.Fatalf("\t%s\tShould receive a status code of 204 for the response : %v", tests.Failed, w.Code) - } - t.Logf("\t%s\tShould receive a status code of 204 for the response.", tests.Success) + expectedStatus := http.StatusCreated + + req := mockProjectCreateRequest(tr.Account.ID) + rt := requestTest{ + fmt.Sprintf("Create %d w/role %s", expectedStatus, tr.Role), + http.MethodPost, + "/v1/projects", + req, + tr.Token, + tr.Claims, + expectedStatus, + nil, } + t.Logf("\tTest: %s - %s %s", rt.name, rt.method, rt.url) + + w, ok := executeRequestTest(t, rt, ctx) + if !ok { + t.Fatalf("\t%s\tExecute request failed.", tests.Failed) + } + t.Logf("\t%s\tReceived valid status code of %d.", tests.Success, w.Code) + + var actual project.ProjectResponse + if err := json.Unmarshal(w.Body.Bytes(), &actual); err != nil { + t.Logf("\t\tGot error : %+v", err) + t.Fatalf("\t%s\tDecode response body failed.", tests.Failed) + } + created = actual + + expectedMap := map[string]interface{}{ + "updated_at": web.NewTimeResponse(ctx, actual.UpdatedAt.Value), + "id": actual.ID, + "account_id": req.AccountID, + "status": web.NewEnumResponse(ctx, "active", project.ProjectStatus_Values), + "created_at": web.NewTimeResponse(ctx, actual.CreatedAt.Value), + "name": req.Name, + } + + var expected project.ProjectResponse + if err := decodeMapToStruct(expectedMap, &expected); err != nil { + t.Logf("\t\tGot error : %+v\nActual results to format expected : \n", err) + printResultMap(ctx, w.Body.Bytes()) // used to help format expectedMap + t.Fatalf("\t%s\tDecode expected failed.", tests.Failed) + } + + if diff := cmpDiff(t, actual, expected); diff { + if len(expectedMap) == 0 { + printResultMap(ctx, w.Body.Bytes()) // used to help format expectedMap + } + t.Fatalf("\t%s\tReceived expected result.", tests.Failed) + } + t.Logf("\t%s\tReceived expected result.", tests.Success) + } + + // Test read. + { + expectedStatus := http.StatusOK + + rt := requestTest{ + fmt.Sprintf("Read %d w/role %s", expectedStatus, tr.Role), + http.MethodGet, + fmt.Sprintf("/v1/projects/%s", created.ID), + nil, + tr.Token, + tr.Claims, + expectedStatus, + nil, + } + t.Logf("\tTest: %s - %s %s", rt.name, rt.method, rt.url) + + w, ok := executeRequestTest(t, rt, ctx) + if !ok { + t.Fatalf("\t%s\tExecute request failed.", tests.Failed) + } + t.Logf("\t%s\tReceived valid status code of %d.", tests.Success, w.Code) + + var actual project.ProjectResponse + if err := json.Unmarshal(w.Body.Bytes(), &actual); err != nil { + t.Logf("\t\tGot error : %+v", err) + t.Fatalf("\t%s\tDecode response body failed.", tests.Failed) + } + + if diff := cmpDiff(t, actual, created); diff { + t.Fatalf("\t%s\tReceived expected result.", tests.Failed) + } + t.Logf("\t%s\tReceived expected result.", tests.Success) + } + + // Test Read with random ID. + { + expectedStatus := http.StatusNotFound + + randID := uuid.NewRandom().String() + rt := requestTest{ + fmt.Sprintf("Read %d w/role %s using random ID", expectedStatus, tr.Role), + http.MethodGet, + fmt.Sprintf("/v1/projects/%s", randID), + nil, + tr.Token, + tr.Claims, + expectedStatus, + nil, + } + t.Logf("\tTest: %s - %s %s", rt.name, rt.method, rt.url) + + w, ok := executeRequestTest(t, rt, ctx) + if !ok { + t.Fatalf("\t%s\tExecute request failed.", tests.Failed) + } + t.Logf("\t%s\tReceived valid status code of %d.", tests.Success, w.Code) + + var actual web.ErrorResponse + if err := json.Unmarshal(w.Body.Bytes(), &actual); err != nil { + t.Logf("\t\tGot error : %+v", err) + t.Fatalf("\t%s\tDecode response body failed.", tests.Failed) + } + + expected := web.ErrorResponse{ + Error: fmt.Sprintf("project %s not found: Entity not found", randID), + } + + if diff := cmpDiff(t, actual, expected); diff { + t.Fatalf("\t%s\tReceived expected error.", tests.Failed) + } + t.Logf("\t%s\tReceived expected error.", tests.Success) + } + + // Test Read with forbidden ID. + forbiddenProject := newMockProject(newMockSignup().account.ID) + { + expectedStatus := http.StatusNotFound + + rt := requestTest{ + fmt.Sprintf("Read %d w/role %s using forbidden ID", expectedStatus, tr.Role), + http.MethodGet, + fmt.Sprintf("/v1/projects/%s", forbiddenProject.ID), + nil, + tr.Token, + tr.Claims, + expectedStatus, + nil, + } + t.Logf("\tTest: %s - %s %s", rt.name, rt.method, rt.url) + + w, ok := executeRequestTest(t, rt, ctx) + if !ok { + t.Fatalf("\t%s\tExecute request failed.", tests.Failed) + } + t.Logf("\t%s\tReceived valid status code of %d.", tests.Success, w.Code) + + var actual web.ErrorResponse + if err := json.Unmarshal(w.Body.Bytes(), &actual); err != nil { + t.Logf("\t\tGot error : %+v", err) + t.Fatalf("\t%s\tDecode response body failed.", tests.Failed) + } + + expected := web.ErrorResponse{ + Error: fmt.Sprintf("project %s not found: Entity not found", forbiddenProject.ID), + } + + if diff := cmpDiff(t, actual, expected); diff { + t.Fatalf("\t%s\tReceived expected error.", tests.Failed) + } + t.Logf("\t%s\tReceived expected error.", tests.Success) + } + + // Test update. + { + expectedStatus := http.StatusNoContent + + newName := uuid.NewRandom().String() + rt := requestTest{ + fmt.Sprintf("Update %d w/role %s", expectedStatus, tr.Role), + http.MethodPatch, + "/v1/projects", + project.ProjectUpdateRequest{ + ID: created.ID, + Name: &newName, + }, + tr.Token, + tr.Claims, + expectedStatus, + nil, + } + t.Logf("\tTest: %s - %s %s", rt.name, rt.method, rt.url) + + w, ok := executeRequestTest(t, rt, ctx) + if !ok { + t.Fatalf("\t%s\tExecute request failed.", tests.Failed) + } + t.Logf("\t%s\tReceived valid status code of %d.", tests.Success, w.Code) + + if len(w.Body.String()) != 0 { + if diff := cmpDiff(t, w.Body.Bytes(), nil); diff { + t.Fatalf("\t%s\tReceived expected empty.", tests.Failed) + } + } + t.Logf("\t%s\tReceived expected empty.", tests.Success) + } + + // Test archive. + { + expectedStatus := http.StatusNoContent + + rt := requestTest{ + fmt.Sprintf("Archive %d w/role %s", expectedStatus, tr.Role), + http.MethodPatch, + "/v1/projects/archive", + project.ProjectArchiveRequest{ + ID: created.ID, + }, + tr.Token, + tr.Claims, + expectedStatus, + nil, + } + t.Logf("\tTest: %s - %s %s", rt.name, rt.method, rt.url) + + w, ok := executeRequestTest(t, rt, ctx) + if !ok { + t.Fatalf("\t%s\tExecute request failed.", tests.Failed) + } + t.Logf("\t%s\tReceived valid status code of %d.", tests.Success, w.Code) + + if len(w.Body.String()) != 0 { + if diff := cmpDiff(t, w.Body.Bytes(), nil); diff { + t.Fatalf("\t%s\tReceived expected empty.", tests.Failed) + } + } + t.Logf("\t%s\tReceived expected empty.", tests.Success) + } + + // Test delete. + { + expectedStatus := http.StatusNoContent + + rt := requestTest{ + fmt.Sprintf("Delete %d w/role %s", expectedStatus, tr.Role), + http.MethodDelete, + fmt.Sprintf("/v1/projects/%s", created.ID), + nil, + tr.Token, + tr.Claims, + expectedStatus, + nil, + } + t.Logf("\tTest: %s - %s %s", rt.name, rt.method, rt.url) + + w, ok := executeRequestTest(t, rt, ctx) + if !ok { + t.Fatalf("\t%s\tExecute request failed.", tests.Failed) + } + t.Logf("\t%s\tReceived valid status code of %d.", tests.Success, w.Code) + + if len(w.Body.String()) != 0 { + if diff := cmpDiff(t, w.Body.Bytes(), nil); diff { + t.Fatalf("\t%s\tReceived expected empty.", tests.Failed) + } + } + t.Logf("\t%s\tReceived expected empty.", tests.Success) } } -// getProject200 validates a project request for an existing id. -func getProject200(t *testing.T, id string) { - r := httptest.NewRequest("GET", "/v1/projects/"+id, nil) - w := httptest.NewRecorder() +// TestProjectCRUDUser tests all the project CRUD endpoints using an user with role project. +func TestProjectCRUDUser(t *testing.T) { + defer tests.Recover(t) - r.Header.Set("Authorization", userAuthorization) + tr := roleTests[auth.RoleUser] - a.ServeHTTP(w, r) + // Add claims to the context for the project. + ctx := context.WithValue(tests.Context(), auth.Key, tr.Claims) - t.Log("Given the need to validate getting a project that exists.") + // Test create. { - t.Logf("\tTest 0:\tWhen using the new project %s.", id) - { - if w.Code != http.StatusOK { - t.Fatalf("\t%s\tShould receive a status code of 200 for the response : %v", tests.Failed, w.Code) - } - t.Logf("\t%s\tShould receive a status code of 200 for the response.", tests.Success) + expectedStatus := http.StatusForbidden - var p project.Project - if err := json.NewDecoder(w.Body).Decode(&p); err != nil { - t.Fatalf("\t%s\tShould be able to unmarshal the response : %v", tests.Failed, err) - } - - // Define what we wanted to receive. We will just trust the generated - // fields like Dates so we copy p. - want := p - want.ID = bson.ObjectIdHex(id) - want.Name = "Comic Books" - want.Cost = 25 - want.Quantity = 60 - - if diff := cmp.Diff(want, p); diff != "" { - t.Fatalf("\t%s\tShould get the expected result. Diff:\n%s", tests.Failed, diff) - } - t.Logf("\t%s\tShould get the expected result.", tests.Success) + req := mockProjectCreateRequest(tr.Account.ID) + rt := requestTest{ + fmt.Sprintf("Create %d w/role %s", expectedStatus, tr.Role), + http.MethodPost, + "/v1/projects", + req, + tr.Token, + tr.Claims, + expectedStatus, + nil, } + t.Logf("\tTest: %s - %s %s", rt.name, rt.method, rt.url) + + w, ok := executeRequestTest(t, rt, ctx) + if !ok { + t.Fatalf("\t%s\tExecute request failed.", tests.Failed) + } + t.Logf("\t%s\tReceived valid status code of %d.", tests.Success, w.Code) + + var actual web.ErrorResponse + if err := json.Unmarshal(w.Body.Bytes(), &actual); err != nil { + t.Logf("\t\tGot error : %+v", err) + t.Fatalf("\t%s\tDecode response body failed.", tests.Failed) + } + + expected := web.ErrorResponse{ + Error: mid.ErrForbidden.Error(), + } + + if diff := cmpDiff(t, actual, expected); diff { + t.Fatalf("\t%s\tReceived expected error.", tests.Failed) + } + t.Logf("\t%s\tReceived expected error.", tests.Success) + } + + // Since role doesn't support create, bypass auth to test other endpoints. + created := newMockProject(tr.Account.ID).Response(ctx) + + // Test read. + { + expectedStatus := http.StatusOK + + rt := requestTest{ + fmt.Sprintf("Read %d w/role %s", expectedStatus, tr.Role), + http.MethodGet, + fmt.Sprintf("/v1/projects/%s", created.ID), + nil, + tr.Token, + tr.Claims, + expectedStatus, + nil, + } + t.Logf("\tTest: %s - %s %s", rt.name, rt.method, rt.url) + + w, ok := executeRequestTest(t, rt, ctx) + if !ok { + t.Fatalf("\t%s\tExecute request failed.", tests.Failed) + } + t.Logf("\t%s\tReceived valid status code of %d.", tests.Success, w.Code) + + var actual *project.ProjectResponse + if err := json.Unmarshal(w.Body.Bytes(), &actual); err != nil { + t.Logf("\t\tGot error : %+v", err) + t.Fatalf("\t%s\tDecode response body failed.", tests.Failed) + } + + if diff := cmpDiff(t, actual, created); diff { + t.Fatalf("\t%s\tReceived expected result.", tests.Failed) + } + t.Logf("\t%s\tReceived expected result.", tests.Success) + } + + // Test Read with random ID. + { + expectedStatus := http.StatusNotFound + + randID := uuid.NewRandom().String() + rt := requestTest{ + fmt.Sprintf("Read %d w/role %s using random ID", expectedStatus, tr.Role), + http.MethodGet, + fmt.Sprintf("/v1/projects/%s", randID), + nil, + tr.Token, + tr.Claims, + expectedStatus, + nil, + } + t.Logf("\tTest: %s - %s %s", rt.name, rt.method, rt.url) + + w, ok := executeRequestTest(t, rt, ctx) + if !ok { + t.Fatalf("\t%s\tExecute request failed.", tests.Failed) + } + t.Logf("\t%s\tReceived valid status code of %d.", tests.Success, w.Code) + + var actual web.ErrorResponse + if err := json.Unmarshal(w.Body.Bytes(), &actual); err != nil { + t.Logf("\t\tGot error : %+v", err) + t.Fatalf("\t%s\tDecode response body failed.", tests.Failed) + } + + expected := web.ErrorResponse{ + Error: fmt.Sprintf("project %s not found: Entity not found", randID), + } + + if diff := cmpDiff(t, actual, expected); diff { + t.Fatalf("\t%s\tReceived expected error.", tests.Failed) + } + t.Logf("\t%s\tReceived expected error.", tests.Success) + } + + // Test Read with forbidden ID. + forbiddenProject := newMockProject(newMockSignup().account.ID) + { + expectedStatus := http.StatusNotFound + + rt := requestTest{ + fmt.Sprintf("Read %d w/role %s using forbidden ID", expectedStatus, tr.Role), + http.MethodGet, + fmt.Sprintf("/v1/projects/%s", forbiddenProject.ID), + nil, + tr.Token, + tr.Claims, + expectedStatus, + nil, + } + t.Logf("\tTest: %s - %s %s", rt.name, rt.method, rt.url) + + w, ok := executeRequestTest(t, rt, ctx) + if !ok { + t.Fatalf("\t%s\tExecute request failed.", tests.Failed) + } + t.Logf("\t%s\tReceived valid status code of %d.", tests.Success, w.Code) + + var actual web.ErrorResponse + if err := json.Unmarshal(w.Body.Bytes(), &actual); err != nil { + t.Logf("\t\tGot error : %+v", err) + t.Fatalf("\t%s\tDecode response body failed.", tests.Failed) + } + + expected := web.ErrorResponse{ + Error: fmt.Sprintf("project %s not found: Entity not found", forbiddenProject.ID), + } + + if diff := cmpDiff(t, actual, expected); diff { + t.Fatalf("\t%s\tReceived expected error.", tests.Failed) + } + t.Logf("\t%s\tReceived expected error.", tests.Success) + } + + // Test update. + { + expectedStatus := http.StatusForbidden + + newName := uuid.NewRandom().String() + rt := requestTest{ + fmt.Sprintf("Update %d w/role %s", expectedStatus, tr.Role), + http.MethodPatch, + "/v1/projects", + project.ProjectUpdateRequest{ + ID: created.ID, + Name: &newName, + }, + tr.Token, + tr.Claims, + expectedStatus, + nil, + } + t.Logf("\tTest: %s - %s %s", rt.name, rt.method, rt.url) + + w, ok := executeRequestTest(t, rt, ctx) + if !ok { + t.Fatalf("\t%s\tExecute request failed.", tests.Failed) + } + t.Logf("\t%s\tReceived valid status code of %d.", tests.Success, w.Code) + + var actual web.ErrorResponse + if err := json.Unmarshal(w.Body.Bytes(), &actual); err != nil { + t.Logf("\t\tGot error : %+v", err) + t.Fatalf("\t%s\tDecode response body failed.", tests.Failed) + } + + expected := web.ErrorResponse{ + Error: mid.ErrForbidden.Error(), + } + + if diff := cmpDiff(t, actual, expected); diff { + t.Fatalf("\t%s\tReceived expected error.", tests.Failed) + } + t.Logf("\t%s\tReceived expected error.", tests.Success) + } + + // Test archive. + { + expectedStatus := http.StatusForbidden + + rt := requestTest{ + fmt.Sprintf("Archive %d w/role %s", expectedStatus, tr.Role), + http.MethodPatch, + "/v1/projects/archive", + project.ProjectArchiveRequest{ + ID: created.ID, + }, + tr.Token, + tr.Claims, + expectedStatus, + nil, + } + t.Logf("\tTest: %s - %s %s", rt.name, rt.method, rt.url) + + w, ok := executeRequestTest(t, rt, ctx) + if !ok { + t.Fatalf("\t%s\tExecute request failed.", tests.Failed) + } + t.Logf("\t%s\tReceived valid status code of %d.", tests.Success, w.Code) + + var actual web.ErrorResponse + if err := json.Unmarshal(w.Body.Bytes(), &actual); err != nil { + t.Logf("\t\tGot error : %+v", err) + t.Fatalf("\t%s\tDecode response body failed.", tests.Failed) + } + + expected := web.ErrorResponse{ + Error: mid.ErrForbidden.Error(), + } + + if diff := cmpDiff(t, actual, expected); diff { + t.Fatalf("\t%s\tReceived expected error.", tests.Failed) + } + t.Logf("\t%s\tReceived expected error.", tests.Success) + } + + // Test delete. + { + expectedStatus := http.StatusForbidden + + rt := requestTest{ + fmt.Sprintf("Delete %d w/role %s", expectedStatus, tr.Role), + http.MethodDelete, + fmt.Sprintf("/v1/projects/%s", created.ID), + nil, + tr.Token, + tr.Claims, + expectedStatus, + nil, + } + t.Logf("\tTest: %s - %s %s", rt.name, rt.method, rt.url) + + w, ok := executeRequestTest(t, rt, ctx) + if !ok { + t.Fatalf("\t%s\tExecute request failed.", tests.Failed) + } + t.Logf("\t%s\tReceived valid status code of %d.", tests.Success, w.Code) + + var actual web.ErrorResponse + if err := json.Unmarshal(w.Body.Bytes(), &actual); err != nil { + t.Logf("\t\tGot error : %+v", err) + t.Fatalf("\t%s\tDecode response body failed.", tests.Failed) + } + + expected := web.ErrorResponse{ + Error: mid.ErrForbidden.Error(), + } + + if diff := cmpDiff(t, actual, expected); diff { + t.Fatalf("\t%s\tReceived expected error.", tests.Failed) + } + t.Logf("\t%s\tReceived expected error.", tests.Success) } } -// putProject204 validates updating a project that does exist. -func putProject204(t *testing.T, id string) { - body := `{"name": "Graphic Novels", "cost": 100}` - r := httptest.NewRequest("PUT", "/v1/projects/"+id, strings.NewReader(body)) - w := httptest.NewRecorder() +// TestProjectCreate validates create project endpoint. +func TestProjectCreate(t *testing.T) { + defer tests.Recover(t) - r.Header.Set("Authorization", userAuthorization) + tr := roleTests[auth.RoleAdmin] - a.ServeHTTP(w, r) + // Add claims to the context for the project. + ctx := context.WithValue(tests.Context(), auth.Key, tr.Claims) - t.Log("Given the need to update a project with the projects endpoint.") + // Test create with invalid data. { - t.Log("\tTest 0:\tWhen using the modified project value.") - { - if w.Code != http.StatusNoContent { - t.Fatalf("\t%s\tShould receive a status code of 204 for the response : %v", tests.Failed, w.Code) - } - t.Logf("\t%s\tShould receive a status code of 204 for the response.", tests.Success) + expectedStatus := http.StatusBadRequest - r = httptest.NewRequest("GET", "/v1/projects/"+id, nil) - w = httptest.NewRecorder() - - r.Header.Set("Authorization", userAuthorization) - - a.ServeHTTP(w, r) - - if w.Code != http.StatusOK { - t.Fatalf("\t%s\tShould receive a status code of 200 for the retrieve : %v", tests.Failed, w.Code) - } - t.Logf("\t%s\tShould receive a status code of 200 for the retrieve.", tests.Success) - - var ru project.Project - if err := json.NewDecoder(w.Body).Decode(&ru); err != nil { - t.Fatalf("\t%s\tShould be able to unmarshal the response : %v", tests.Failed, err) - } - - if ru.Name != "Graphic Novels" { - t.Fatalf("\t%s\tShould see an updated Name : got %q want %q", tests.Failed, ru.Name, "Graphic Novels") - } - t.Logf("\t%s\tShould see an updated Name.", tests.Success) + req := mockProjectCreateRequest(tr.Account.ID) + invalidStatus := project.ProjectStatus("invalid status") + req.Status = &invalidStatus + rt := requestTest{ + fmt.Sprintf("Create %d w/role %s using invalid data", expectedStatus, tr.Role), + http.MethodPost, + "/v1/projects", + req, + tr.Token, + tr.Claims, + expectedStatus, + nil, } + t.Logf("\tTest: %s - %s %s", rt.name, rt.method, rt.url) + + w, ok := executeRequestTest(t, rt, ctx) + if !ok { + t.Fatalf("\t%s\tExecute request failed.", tests.Failed) + } + t.Logf("\t%s\tReceived valid status code of %d.", tests.Success, w.Code) + + var actual web.ErrorResponse + if err := json.Unmarshal(w.Body.Bytes(), &actual); err != nil { + t.Logf("\t\tGot error : %+v", err) + t.Fatalf("\t%s\tDecode response body failed.", tests.Failed) + } + + expected := web.ErrorResponse{ + Error: "field validation error", + Fields: []web.FieldError{ + {Field: "status", Error: "Key: 'ProjectCreateRequest.status' Error:Field validation for 'status' failed on the 'oneof' tag"}, + }, + } + + if diff := cmpDiff(t, actual, expected); diff { + t.Fatalf("\t%s\tReceived expected error.", tests.Failed) + } + t.Logf("\t%s\tReceived expected error.", tests.Success) + } +} + +// TestProjectUpdate validates update project endpoint. +func TestProjectUpdate(t *testing.T) { + defer tests.Recover(t) + + tr := roleTests[auth.RoleAdmin] + + // Add claims to the context for the project. + ctx := context.WithValue(tests.Context(), auth.Key, tr.Claims) + + // Test update with invalid data. + { + expectedStatus := http.StatusBadRequest + + invalidStatus := project.ProjectStatus("invalid status") + rt := requestTest{ + fmt.Sprintf("Update %d w/role %s using invalid data", expectedStatus, tr.Role), + http.MethodPatch, + "/v1/projects", + project.ProjectUpdateRequest{ + ID: uuid.NewRandom().String(), + Status: &invalidStatus, + }, + tr.Token, + tr.Claims, + expectedStatus, + nil, + } + t.Logf("\tTest: %s - %s %s", rt.name, rt.method, rt.url) + + w, ok := executeRequestTest(t, rt, ctx) + if !ok { + t.Fatalf("\t%s\tExecute request failed.", tests.Failed) + } + t.Logf("\t%s\tReceived valid status code of %d.", tests.Success, w.Code) + + var actual web.ErrorResponse + if err := json.Unmarshal(w.Body.Bytes(), &actual); err != nil { + t.Logf("\t\tGot error : %+v", err) + t.Fatalf("\t%s\tDecode response body failed.", tests.Failed) + } + + expected := web.ErrorResponse{ + Error: "field validation error", + Fields: []web.FieldError{ + {Field: "status", Error: "Key: 'ProjectUpdateRequest.status' Error:Field validation for 'status' failed on the 'oneof' tag"}, + }, + } + + if diff := cmpDiff(t, actual, expected); diff { + t.Fatalf("\t%s\tReceived expected error.", tests.Failed) + } + t.Logf("\t%s\tReceived expected error.", tests.Success) + } +} + +// TestProjectArchive validates archive project endpoint. +func TestProjectArchive(t *testing.T) { + defer tests.Recover(t) + + tr := roleTests[auth.RoleAdmin] + + // Add claims to the context for the project. + ctx := context.WithValue(tests.Context(), auth.Key, tr.Claims) + + forbiddenProject := newMockProject(newMockSignup().account.ID) + + // Test archive with invalid data. + { + expectedStatus := http.StatusBadRequest + + rt := requestTest{ + fmt.Sprintf("Archive %d w/role %s using invalid data", expectedStatus, tr.Role), + http.MethodPatch, + "/v1/projects/archive", + project.ProjectArchiveRequest{ + ID: "a", + }, + tr.Token, + tr.Claims, + expectedStatus, + nil, + } + t.Logf("\tTest: %s - %s %s", rt.name, rt.method, rt.url) + + w, ok := executeRequestTest(t, rt, ctx) + if !ok { + t.Fatalf("\t%s\tExecute request failed.", tests.Failed) + } + t.Logf("\t%s\tReceived valid status code of %d.", tests.Success, w.Code) + + var actual web.ErrorResponse + if err := json.Unmarshal(w.Body.Bytes(), &actual); err != nil { + t.Logf("\t\tGot error : %+v", err) + t.Fatalf("\t%s\tDecode response body failed.", tests.Failed) + } + + expected := web.ErrorResponse{ + Error: "field validation error", + Fields: []web.FieldError{ + {Field: "id", Error: "Key: 'ProjectArchiveRequest.id' Error:Field validation for 'id' failed on the 'uuid' tag"}, + }, + } + + if diff := cmpDiff(t, actual, expected); diff { + t.Fatalf("\t%s\tReceived expected error.", tests.Failed) + } + t.Logf("\t%s\tReceived expected error.", tests.Success) + } + + // Test archive with forbidden ID. + { + expectedStatus := http.StatusForbidden + + rt := requestTest{ + fmt.Sprintf("Archive %d w/role %s using forbidden ID", expectedStatus, tr.Role), + http.MethodPatch, + "/v1/projects/archive", + project.ProjectArchiveRequest{ + ID: forbiddenProject.ID, + }, + tr.Token, + tr.Claims, + expectedStatus, + nil, + } + t.Logf("\tTest: %s - %s %s", rt.name, rt.method, rt.url) + + w, ok := executeRequestTest(t, rt, ctx) + if !ok { + t.Fatalf("\t%s\tExecute request failed.", tests.Failed) + } + t.Logf("\t%s\tReceived valid status code of %d.", tests.Success, w.Code) + + var actual web.ErrorResponse + if err := json.Unmarshal(w.Body.Bytes(), &actual); err != nil { + t.Logf("\t\tGot error : %+v", err) + t.Fatalf("\t%s\tDecode response body failed.", tests.Failed) + } + + expected := web.ErrorResponse{ + Error: project.ErrForbidden.Error(), + } + + if diff := cmpDiff(t, actual, expected); diff { + t.Fatalf("\t%s\tReceived expected error.", tests.Failed) + } + t.Logf("\t%s\tReceived expected error.", tests.Success) + } +} + +// TestProjectDelete validates delete project endpoint. +func TestProjectDelete(t *testing.T) { + defer tests.Recover(t) + + tr := roleTests[auth.RoleAdmin] + + // Add claims to the context for the project. + ctx := context.WithValue(tests.Context(), auth.Key, tr.Claims) + + forbiddenProject := newMockProject(newMockSignup().account.ID) + + // Test delete with invalid data. + { + expectedStatus := http.StatusBadRequest + + rt := requestTest{ + fmt.Sprintf("Delete %d w/role %s using invalid data", expectedStatus, tr.Role), + http.MethodDelete, + "/v1/projects/a", + nil, + tr.Token, + tr.Claims, + expectedStatus, + nil, + } + t.Logf("\tTest: %s - %s %s", rt.name, rt.method, rt.url) + + w, ok := executeRequestTest(t, rt, ctx) + if !ok { + t.Fatalf("\t%s\tExecute request failed.", tests.Failed) + } + t.Logf("\t%s\tReceived valid status code of %d.", tests.Success, w.Code) + + var actual web.ErrorResponse + if err := json.Unmarshal(w.Body.Bytes(), &actual); err != nil { + t.Logf("\t\tGot error : %+v", err) + t.Fatalf("\t%s\tDecode response body failed.", tests.Failed) + } + + expected := web.ErrorResponse{ + Error: "field validation error", + Fields: []web.FieldError{ + {Field: "id", Error: "Key: 'id' Error:Field validation for 'id' failed on the 'uuid' tag"}, + }, + } + + if diff := cmpDiff(t, actual, expected); diff { + t.Fatalf("\t%s\tReceived expected error.", tests.Failed) + } + t.Logf("\t%s\tReceived expected error.", tests.Success) + } + + // Test delete with forbidden ID. + { + expectedStatus := http.StatusForbidden + + rt := requestTest{ + fmt.Sprintf("Delete %d w/role %s using forbidden ID", expectedStatus, tr.Role), + http.MethodDelete, + fmt.Sprintf("/v1/projects/%s", forbiddenProject.ID), + nil, + tr.Token, + tr.Claims, + expectedStatus, + nil, + } + t.Logf("\tTest: %s - %s %s", rt.name, rt.method, rt.url) + + w, ok := executeRequestTest(t, rt, ctx) + if !ok { + t.Fatalf("\t%s\tExecute request failed.", tests.Failed) + } + t.Logf("\t%s\tReceived valid status code of %d.", tests.Success, w.Code) + + var actual web.ErrorResponse + if err := json.Unmarshal(w.Body.Bytes(), &actual); err != nil { + t.Logf("\t\tGot error : %+v", err) + t.Fatalf("\t%s\tDecode response body failed.", tests.Failed) + } + + expected := web.ErrorResponse{ + Error: project.ErrForbidden.Error(), + } + + if diff := cmpDiff(t, actual, expected); diff { + t.Fatalf("\t%s\tReceived expected error.", tests.Failed) + } + t.Logf("\t%s\tReceived expected error.", tests.Success) } } -*/ diff --git a/example-project/cmd/web-api/tests/signup_test.go b/example-project/cmd/web-api/tests/signup_test.go index 1977057..90b5895 100644 --- a/example-project/cmd/web-api/tests/signup_test.go +++ b/example-project/cmd/web-api/tests/signup_test.go @@ -1,19 +1,30 @@ package tests import ( + "context" "encoding/json" + "fmt" "net/http" "testing" + "time" + "geeks-accelerator/oss/saas-starter-kit/example-project/internal/account" "geeks-accelerator/oss/saas-starter-kit/example-project/internal/platform/auth" "geeks-accelerator/oss/saas-starter-kit/example-project/internal/platform/tests" "geeks-accelerator/oss/saas-starter-kit/example-project/internal/platform/web" "geeks-accelerator/oss/saas-starter-kit/example-project/internal/signup" "geeks-accelerator/oss/saas-starter-kit/example-project/internal/user" - "github.com/google/go-cmp/cmp" "github.com/pborman/uuid" ) +type mockSignup struct { + account *account.Account + user mockUser + token user.Token + claims auth.Claims + context context.Context +} + func mockSignupRequest() signup.SignupRequest { return signup.SignupRequest{ Account: signup.SignupAccount{ @@ -34,152 +45,199 @@ func mockSignupRequest() signup.SignupRequest { } } -// TestSignup is the entry point for the signup +func newMockSignup() mockSignup { + req := mockSignupRequest() + now := time.Now().UTC().AddDate(-1, -1, -1) + s, err := signup.Signup(tests.Context(), auth.Claims{}, test.MasterDB, req, now) + if err != nil { + panic(err) + } + + expires := time.Now().UTC().Sub(s.User.CreatedAt) + time.Hour + tkn, err := user.Authenticate(tests.Context(), test.MasterDB, authenticator, req.User.Email, req.User.Password, expires, now) + if err != nil { + panic(err) + } + + claims, err := authenticator.ParseClaims(tkn.AccessToken) + if err != nil { + panic(err) + } + + // Add claims to the context for the user. + ctx := context.WithValue(tests.Context(), auth.Key, claims) + + return mockSignup{ + account: s.Account, + user: mockUser{s.User, req.User.Password}, + claims: claims, + token: tkn, + context: ctx, + } +} + +// TestSignup validates the signup endpoint. func TestSignup(t *testing.T) { defer tests.Recover(t) - t.Run("postSigup", postSigup) -} + ctx := tests.Context() -// postSigup validates the signup endpoint. -func postSigup(t *testing.T) { + // Test signup. + { + expectedStatus := http.StatusCreated - var rtests []requestTest + req := mockSignupRequest() + rt := requestTest{ + fmt.Sprintf("Signup %d w/no authorization", expectedStatus), + http.MethodPost, + "/v1/signup", + req, + user.Token{}, + auth.Claims{}, + expectedStatus, + nil, + } + t.Logf("\tTest: %s - %s %s", rt.name, rt.method, rt.url) - // Test 201. - // Signup does not require auth, so empty token and claims should result in success. - req1 := mockSignupRequest() - rtests = append(rtests, requestTest{ - "No Authorization Valid", - http.MethodPost, - "/v1/signup", - req1, - user.Token{}, - auth.Claims{}, - http.StatusCreated, - nil, - func(treq requestTest, body []byte) bool { - var actual signup.SignupResponse - if err := json.Unmarshal(body, &actual); err != nil { - t.Logf("\t\tGot error : %+v", err) - return false - } + w, ok := executeRequestTest(t, rt, ctx) + if !ok { + t.Fatalf("\t%s\tExecute request failed.", tests.Failed) + } + t.Logf("\t%s\tReceived valid status code of %d.", tests.Success, w.Code) - ctx := tests.Context() + var actual signup.SignupResponse + if err := json.Unmarshal(w.Body.Bytes(), &actual); err != nil { + t.Logf("\t\tGot error : %+v", err) + t.Fatalf("\t%s\tDecode response body failed.", tests.Failed) + } - req := treq.request.(signup.SignupRequest ) - - expectedMap := map[string]interface{}{ - "user": map[string]interface{}{ - "id": actual.User.ID, - "name": req.User.Name, - "email": req.User.Email, - "timezone": actual.User.Timezone, - "created_at": web.NewTimeResponse(ctx, actual.User.CreatedAt.Value), - "updated_at": web.NewTimeResponse(ctx, actual.User.UpdatedAt.Value), - }, - "account": map[string]interface{}{ - "updated_at": web.NewTimeResponse(ctx, actual.Account.UpdatedAt.Value), - "id": actual.Account.ID, - "address2": req.Account.Address2, - "region": req.Account.Region, - "zipcode": req.Account.Zipcode, - "timezone": actual.Account.Timezone, - "created_at": web.NewTimeResponse(ctx, actual.Account.CreatedAt.Value), - "country": req.Account.Country, - "billing_user_id": &actual.Account.BillingUserID, - "name": req.Account.Name, - "address1": req.Account.Address1, - "city": req.Account.City, - "status": map[string]interface{}{ - "value": "active", - "title": "Active", - "options": []map[string]interface{}{{"selected":false,"title":"[Active Pending Disabled]","value":"[active pending disabled]"}}, - }, - "signup_user_id": &actual.Account.SignupUserID, + expectedMap := map[string]interface{}{ + "user": map[string]interface{}{ + "id": actual.User.ID, + "name": req.User.Name, + "email": req.User.Email, + "timezone": actual.User.Timezone, + "created_at": web.NewTimeResponse(ctx, actual.User.CreatedAt.Value), + "updated_at": web.NewTimeResponse(ctx, actual.User.UpdatedAt.Value), + }, + "account": map[string]interface{}{ + "updated_at": web.NewTimeResponse(ctx, actual.Account.UpdatedAt.Value), + "id": actual.Account.ID, + "address2": req.Account.Address2, + "region": req.Account.Region, + "zipcode": req.Account.Zipcode, + "timezone": actual.Account.Timezone, + "created_at": web.NewTimeResponse(ctx, actual.Account.CreatedAt.Value), + "country": req.Account.Country, + "billing_user_id": &actual.Account.BillingUserID, + "name": req.Account.Name, + "address1": req.Account.Address1, + "city": req.Account.City, + "status": map[string]interface{}{ + "value": "active", + "title": "Active", + "options": []map[string]interface{}{{"selected": false, "title": "[Active Pending Disabled]", "value": "[active pending disabled]"}}, }, + "signup_user_id": &actual.Account.SignupUserID, + }, + } + + var expected signup.SignupResponse + if err := decodeMapToStruct(expectedMap, &expected); err != nil { + t.Logf("\t\tGot error : %+v\nActual results to format expected : \n", err) + printResultMap(ctx, w.Body.Bytes()) // used to help format expectedMap + t.Fatalf("\t%s\tDecode expected failed.", tests.Failed) + } + + if diff := cmpDiff(t, actual, expected); diff { + if len(expectedMap) == 0 { + printResultMap(ctx, w.Body.Bytes()) // used to help format expectedMap } - expectedJson, err := json.Marshal(expectedMap) - if err != nil { - t.Logf("\t\tGot error : %+v", err) - return false - } + t.Fatalf("\t%s\tReceived expected result.", tests.Failed) + } + t.Logf("\t%s\tReceived expected result.", tests.Success) + } - var expected signup.SignupResponse - if err := json.Unmarshal([]byte(expectedJson), &expected); err != nil { - t.Logf("\t\tGot error : %+v", err) - printResultMap(ctx, body) - return false - } + // Test signup w/empty request. + { + expectedStatus := http.StatusBadRequest - if diff := cmp.Diff(actual, expected); diff != "" { - actualJSON, err := json.MarshalIndent(actual, "", " ") - if err != nil { - t.Logf("\t\tGot error : %+v", err) - return false - } - t.Logf("\t\tGot : %s\n", actualJSON) + rt := requestTest{ + fmt.Sprintf("Signup %d w/empty request", expectedStatus), + http.MethodPost, + "/v1/signup", + nil, + user.Token{}, + auth.Claims{}, + expectedStatus, + nil, + } + t.Logf("\tTest: %s - %s %s", rt.name, rt.method, rt.url) - expectedJSON, err := json.MarshalIndent(expected, "", " ") - if err != nil { - t.Logf("\t\tGot error : %+v", err) - return false - } - t.Logf("\t\tExpected : %s\n", expectedJSON) + w, ok := executeRequestTest(t, rt, ctx) + if !ok { + t.Fatalf("\t%s\tExecute request failed.", tests.Failed) + } + t.Logf("\t%s\tReceived valid status code of %d.", tests.Success, w.Code) - t.Logf("\t\tDiff : %s\n", diff) + var actual web.ErrorResponse + if err := json.Unmarshal(w.Body.Bytes(), &actual); err != nil { + t.Logf("\t\tGot error : %+v", err) + t.Fatalf("\t%s\tDecode response body failed.", tests.Failed) + } - if len(expectedMap) == 0 { - printResultMap(ctx, body) - } - - return false - } - - return true - }, - }) - - // Test 404 w/empty request. - rtests = append(rtests, requestTest{ - "Empty request", - http.MethodPost, - "/v1/signup", - nil, - user.Token{}, - auth.Claims{}, - http.StatusBadRequest, - web.ErrorResponse{ + expected := web.ErrorResponse{ Error: "decode request body failed: EOF", - }, - func(req requestTest, body []byte) bool { - return true - }, - }) + } - // Test 404 w/validation errors. - invalidReq := mockSignupRequest() - invalidReq.User.Email = "" - invalidReq.Account.Name = "" - rtests = append(rtests, requestTest{ - "Invalid request", - http.MethodPost, - "/v1/signup", - invalidReq, - user.Token{}, - auth.Claims{}, - http.StatusBadRequest, - web.ErrorResponse{ + if diff := cmpDiff(t, actual, expected); diff { + t.Fatalf("\t%s\tReceived expected error.", tests.Failed) + } + t.Logf("\t%s\tReceived expected error.", tests.Success) + } + + // Test signup w/validation errors. + { + expectedStatus := http.StatusBadRequest + + req := mockSignupRequest() + req.User.Email = "" + req.Account.Name = "" + rt := requestTest{ + fmt.Sprintf("Signup %d w/validation errors", expectedStatus), + http.MethodPost, + "/v1/signup", + req, + user.Token{}, + auth.Claims{}, + expectedStatus, + nil, + } + t.Logf("\tTest: %s - %s %s", rt.name, rt.method, rt.url) + + w, ok := executeRequestTest(t, rt, ctx) + if !ok { + t.Fatalf("\t%s\tExecute request failed.", tests.Failed) + } + t.Logf("\t%s\tReceived valid status code of %d.", tests.Success, w.Code) + + var actual web.ErrorResponse + if err := json.Unmarshal(w.Body.Bytes(), &actual); err != nil { + t.Logf("\t\tGot error : %+v", err) + t.Fatalf("\t%s\tDecode response body failed.", tests.Failed) + } + + expected := web.ErrorResponse{ Error: "field validation error", Fields: []web.FieldError{ {Field: "name", Error: "Key: 'SignupRequest.account.name' Error:Field validation for 'name' failed on the 'required' tag"}, {Field: "email", Error: "Key: 'SignupRequest.user.email' Error:Field validation for 'email' failed on the 'required' tag"}, }, - }, - func(req requestTest, body []byte) bool { - return true - }, - }) + } - runRequestTests(t, rtests) + if diff := cmpDiff(t, actual, expected); diff { + t.Fatalf("\t%s\tReceived expected error.", tests.Failed) + } + t.Logf("\t%s\tReceived expected error.", tests.Success) + } } diff --git a/example-project/cmd/web-api/tests/tests_test.go b/example-project/cmd/web-api/tests/tests_test.go index fd136b6..6274de1 100644 --- a/example-project/cmd/web-api/tests/tests_test.go +++ b/example-project/cmd/web-api/tests/tests_test.go @@ -5,9 +5,8 @@ import ( "context" "encoding/json" "fmt" - "geeks-accelerator/oss/saas-starter-kit/example-project/internal/platform/web" - "github.com/google/go-cmp/cmp" "io" + "io/ioutil" "net/http" "net/http/httptest" "os" @@ -15,40 +14,44 @@ import ( "testing" "time" - "geeks-accelerator/oss/saas-starter-kit/example-project/internal/account" - "geeks-accelerator/oss/saas-starter-kit/example-project/internal/signup" - "geeks-accelerator/oss/saas-starter-kit/example-project/internal/user_account" - "github.com/iancoleman/strcase" - "github.com/pborman/uuid" "geeks-accelerator/oss/saas-starter-kit/example-project/cmd/web-api/handlers" + "geeks-accelerator/oss/saas-starter-kit/example-project/internal/account" "geeks-accelerator/oss/saas-starter-kit/example-project/internal/platform/auth" "geeks-accelerator/oss/saas-starter-kit/example-project/internal/platform/tests" + "geeks-accelerator/oss/saas-starter-kit/example-project/internal/platform/web" + "geeks-accelerator/oss/saas-starter-kit/example-project/internal/signup" "geeks-accelerator/oss/saas-starter-kit/example-project/internal/user" + "geeks-accelerator/oss/saas-starter-kit/example-project/internal/user_account" + "github.com/google/go-cmp/cmp" + "github.com/iancoleman/strcase" + "github.com/pborman/uuid" + "github.com/pkg/errors" ) var a http.Handler var test *tests.Test +var authenticator *auth.Authenticator // Information about the users we have created for testing. type roleTest struct { - Token user.Token - Claims auth.Claims - SignupRequest *signup.SignupRequest - SignupResult *signup.SignupResult - User *user.User - Account *account.Account + Role string + Token user.Token + Claims auth.Claims + User mockUser + Account *account.Account + ForbiddenUser mockUser + ForbiddenAccount *account.Account } type requestTest struct { - name string - method string - url string - request interface{} - token user.Token - claims auth.Claims - statusCode int - error interface{} - expected func(req requestTest, result []byte) bool + name string + method string + url string + request interface{} + token user.Token + claims auth.Claims + statusCode int + error interface{} } var roleTests map[string]roleTest @@ -68,40 +71,28 @@ func testMain(m *testing.M) int { now := time.Date(2018, time.October, 1, 0, 0, 0, 0, time.UTC) - authenticator, err := auth.NewAuthenticatorMemory(now) + var err error + authenticator, err = auth.NewAuthenticatorMemory(now) if err != nil { panic(err) } shutdown := make(chan os.Signal, 1) - a = handlers.API(shutdown, test.Log, test.MasterDB, nil, authenticator) + + log := test.Log + log.SetOutput(ioutil.Discard) + a = handlers.API(shutdown, log, test.MasterDB, nil, authenticator) // Create a new account directly business logic. This creates an // initial account and user that we will use for admin validated endpoints. - signupReq := signup.SignupRequest{ - Account: signup.SignupAccount{ - Name: uuid.NewRandom().String(), - Address1: "103 East Main St", - Address2: "Unit 546", - City: "Valdez", - Region: "AK", - Country: "USA", - Zipcode: "99686", - }, - User: signup.SignupUser{ - Name: "Lee Brown", - Email: uuid.NewRandom().String() + "@geeksinthewoods.com", - Password: "akTechFr0n!ier", - PasswordConfirm: "akTechFr0n!ier", - }, - } - signup, err := signup.Signup(tests.Context(), auth.Claims{}, test.MasterDB, signupReq, now) + signupReq1 := mockSignupRequest() + signup1, err := signup.Signup(tests.Context(), auth.Claims{}, test.MasterDB, signupReq1, now) if err != nil { panic(err) } - expires := time.Now().UTC().Sub(signup.User.CreatedAt) + time.Hour - adminTkn, err := user.Authenticate(tests.Context(), test.MasterDB, authenticator, signupReq.User.Email, signupReq.User.Password, expires, now) + expires := time.Now().UTC().Sub(signup1.User.CreatedAt) + time.Hour + adminTkn, err := user.Authenticate(tests.Context(), test.MasterDB, authenticator, signupReq1.User.Email, signupReq1.User.Password, expires, now) if err != nil { panic(err) } @@ -111,13 +102,22 @@ func testMain(m *testing.M) int { panic(err) } + // Create a second account that the first account user should not have access to. + signupReq2 := mockSignupRequest() + signup2, err := signup.Signup(tests.Context(), auth.Claims{}, test.MasterDB, signupReq2, now) + if err != nil { + panic(err) + } + + // First test will be for role Admin roleTests[auth.RoleAdmin] = roleTest{ - Token: adminTkn, - Claims: adminClaims, - SignupRequest: &signupReq, - SignupResult: signup, - User: signup.User, - Account: signup.Account, + Role: auth.RoleAdmin, + Token: adminTkn, + Claims: adminClaims, + User: mockUser{signup1.User, signupReq1.User.Password}, + Account: signup1.Account, + ForbiddenUser: mockUser{signup2.User, signupReq2.User.Password}, + ForbiddenAccount: signup2.Account, } // Create a regular user to use when calling regular validated endpoints. @@ -133,9 +133,9 @@ func testMain(m *testing.M) int { } _, err = user_account.Create(tests.Context(), adminClaims, test.MasterDB, user_account.UserAccountCreateRequest{ - UserID: usr.ID, - AccountID: signup.Account.ID, - Roles: []user_account.UserAccountRole{user_account.UserAccountRole_User}, + UserID: usr.ID, + AccountID: signup1.Account.ID, + Roles: []user_account.UserAccountRole{user_account.UserAccountRole_User}, // Status: use default value }, now) if err != nil { @@ -152,83 +152,114 @@ func testMain(m *testing.M) int { panic(err) } + // Second test will be for role User roleTests[auth.RoleUser] = roleTest{ - Token: userTkn, - Claims: userClaims, - SignupRequest: &signupReq, - SignupResult: signup, - Account: signup.Account, - User: usr, + Role: auth.RoleUser, + Token: userTkn, + Claims: userClaims, + Account: signup1.Account, + User: mockUser{usr, userReq.Password}, + ForbiddenUser: mockUser{signup2.User, signupReq2.User.Password}, + ForbiddenAccount: signup2.Account, } return m.Run() } - -// runRequestTests helper function for testing endpoints. -func runRequestTests(t *testing.T, rtests []requestTest ) { - - for i, tt := range rtests { - t.Logf("\tTest: %d\tWhen running test: %s", i, tt.name) - { - var req []byte - var rr io.Reader - if tt.request != nil { - var ok bool - req, ok = tt.request.([]byte) - if !ok { - var err error - req, err = json.Marshal(tt.request) - if err != nil { - t.Logf("\t\tGot err : %+v", err) - t.Fatalf("\t%s\tEncode request failed.", tests.Failed) - } - } - rr = bytes.NewReader(req) +// executeRequestTest provides request execution and basic response validation +func executeRequestTest(t *testing.T, tt requestTest, ctx context.Context) (*httptest.ResponseRecorder, bool) { + var req []byte + var rr io.Reader + if tt.request != nil { + var ok bool + req, ok = tt.request.([]byte) + if !ok { + var err error + req, err = json.Marshal(tt.request) + if err != nil { + t.Logf("\t\tGot err : %+v", err) + t.Logf("\t\tEncode request failed.") + return nil, false } + } + rr = bytes.NewReader(req) + } - r := httptest.NewRequest(tt.method, tt.url , rr) - w := httptest.NewRecorder() + r := httptest.NewRequest(tt.method, tt.url, rr).WithContext(ctx) - r.Header.Set("Content-Type", web.MIMEApplicationJSONCharsetUTF8) - if tt.token.AccessToken != "" { - r.Header.Set("Authorization", tt.token.AuthorizationHeader()) - } + w := httptest.NewRecorder() - a.ServeHTTP(w, r) + r.Header.Set("Content-Type", web.MIMEApplicationJSONCharsetUTF8) + if tt.token.AccessToken != "" { + r.Header.Set("Authorization", tt.token.AuthorizationHeader()) + } - if w.Code != tt.statusCode { - t.Logf("\t\tRequest : %s\n", string(req)) - t.Logf("\t\tBody : %s\n", w.Body.String()) - t.Fatalf("\t%s\tShould receive a status code of %d for the response : %v", tests.Failed, tt.statusCode, w.Code) - } - t.Logf("\t%s\tReceived valid status code of %d.", tests.Success, w.Code) + a.ServeHTTP(w, r) - if tt.error != nil { + if w.Code != tt.statusCode { + t.Logf("\t\tRequest : %s\n", string(req)) + t.Logf("\t\tBody : %s\n", w.Body.String()) + t.Logf("\t\tShould receive a status code of %d for the response : %v", tt.statusCode, w.Code) + return w, false + } + if tt.error != nil { + var actual web.ErrorResponse + if err := json.Unmarshal(w.Body.Bytes(), &actual); err != nil { + t.Logf("\t\tBody : %s\n", w.Body.String()) + t.Logf("\t\tGot error : %+v", err) + t.Logf("\t\tShould get the expected error.") + return w, false + } - - var actual web.ErrorResponse - if err := json.Unmarshal(w.Body.Bytes(), &actual); err != nil { - t.Logf("\t\tBody : %s\n", w.Body.String()) - t.Logf("\t\tGot error : %+v", err) - t.Fatalf("\t%s\tShould get the expected error.", tests.Failed) - } - - if diff := cmp.Diff(actual, tt.error); diff != "" { - t.Logf("\t\tDiff : %s\n", diff) - t.Fatalf("\t%s\tShould get the expected error.", tests.Failed) - } - } - - if ok := tt.expected(tt, w.Body.Bytes()); !ok { - t.Fatalf("\t%s\tShould get the expected result.", tests.Failed) - } - t.Logf("\t%s\tReceived expected result.", tests.Success) + if diff := cmp.Diff(actual, tt.error); diff != "" { + t.Logf("\t\tDiff : %s\n", diff) + t.Logf("\t\tShould get the expected error.") + return w, false } } + + return w, true } +// decodeMapToStruct used to covert map to json struct so don't have a bunch of raw json strings running around test files. +func decodeMapToStruct(expectedMap map[string]interface{}, expected interface{}) error { + expectedJson, err := json.Marshal(expectedMap) + if err != nil { + return errors.WithStack(err) + } + + if err := json.Unmarshal([]byte(expectedJson), &expected); err != nil { + return errors.WithStack(err) + } + + return nil +} + +// cmpDiff prints out the raw json to help with debugging. +func cmpDiff(t *testing.T, actual, expected interface{}) bool { + if actual == nil && expected == nil { + return false + } + if diff := cmp.Diff(actual, expected); diff != "" { + actualJSON, err := json.MarshalIndent(actual, "", " ") + if err != nil { + t.Fatalf("\t%s\tGot error : %+v", tests.Failed, err) + } + t.Logf("\t\tGot : %s\n", actualJSON) + + expectedJSON, err := json.MarshalIndent(expected, "", " ") + if err != nil { + t.Fatalf("\t%s\tGot error : %+v", tests.Failed, err) + } + t.Logf("\t\tExpected : %s\n", expectedJSON) + + t.Logf("\t\tDiff : %s\n", diff) + + return true + } + return false +} func printResultMap(ctx context.Context, result []byte) { var m map[string]interface{} @@ -261,13 +292,13 @@ func printResultMapKeys(ctx context.Context, m map[string]interface{}, depth int fmt.Printf("%s\"%s\": []map[string]interface{}{\n", strings.Repeat("\t", depth), k) for _, smv := range sm { - printResultMapKeys(ctx, smv, depth +1) + printResultMapKeys(ctx, smv, depth+1) } fmt.Printf("%s},\n", strings.Repeat("\t", depth)) } else if sm, ok := kv.(map[string]interface{}); ok { fmt.Printf("%s\"%s\": map[string]interface{}{\n", strings.Repeat("\t", depth), k) - printResultMapKeys(ctx, sm, depth +1) + printResultMapKeys(ctx, sm, depth+1) fmt.Printf("%s},\n", strings.Repeat("\t", depth)) } else { var pv string @@ -282,4 +313,3 @@ func printResultMapKeys(ctx context.Context, m map[string]interface{}, depth int } } } - diff --git a/example-project/cmd/web-api/tests/user_account_test.go b/example-project/cmd/web-api/tests/user_account_test.go new file mode 100644 index 0000000..a0ea449 --- /dev/null +++ b/example-project/cmd/web-api/tests/user_account_test.go @@ -0,0 +1,930 @@ +package tests + +import ( + "context" + "encoding/json" + "fmt" + "net/http" + "testing" + "time" + + "geeks-accelerator/oss/saas-starter-kit/example-project/internal/account" + "geeks-accelerator/oss/saas-starter-kit/example-project/internal/mid" + "geeks-accelerator/oss/saas-starter-kit/example-project/internal/platform/auth" + "geeks-accelerator/oss/saas-starter-kit/example-project/internal/platform/tests" + "geeks-accelerator/oss/saas-starter-kit/example-project/internal/platform/web" + "geeks-accelerator/oss/saas-starter-kit/example-project/internal/user" + "geeks-accelerator/oss/saas-starter-kit/example-project/internal/user_account" + "github.com/pborman/uuid" +) + +// newMockUserAccount creates a new user user for testing and associates it with the supplied account ID. +func newMockUserAccount(accountID string, role user_account.UserAccountRole) *user_account.UserAccount { + req := mockUserCreateRequest() + u, err := user.Create(tests.Context(), auth.Claims{}, test.MasterDB, req, time.Now().UTC().AddDate(-1, -1, -1)) + if err != nil { + panic(err) + } + + ua, err := user_account.Create(tests.Context(), auth.Claims{}, test.MasterDB, user_account.UserAccountCreateRequest{ + UserID: u.ID, + AccountID: accountID, + Roles: []user_account.UserAccountRole{role}, + }, time.Now().UTC().AddDate(-1, -1, -1)) + if err != nil { + panic(err) + } + + return ua +} + +// TestUserAccountCRUDAdmin tests all the user account CRUD endpoints using an user with role admin. +func TestUserAccountCRUDAdmin(t *testing.T) { + defer tests.Recover(t) + + tr := roleTests[auth.RoleAdmin] + + // Add claims to the context for the user_account. + ctx := context.WithValue(tests.Context(), auth.Key, tr.Claims) + + // Test create. + var created user_account.UserAccountResponse + { + expectedStatus := http.StatusCreated + + rt := requestTest{ + fmt.Sprintf("Create %d w/role %s", expectedStatus, tr.Role), + http.MethodPost, + "/v1/user_accounts", + nil, + tr.Token, + tr.Claims, + expectedStatus, + nil, + } + t.Logf("\tTest: %s - %s %s", rt.name, rt.method, rt.url) + + newUser, err := user.Create(tests.Context(), auth.Claims{}, test.MasterDB, mockUserCreateRequest(), time.Now().UTC().AddDate(-1, -1, -1)) + if err != nil { + t.Fatalf("\t%s\tCreate new user failed.", tests.Failed) + } + req := user_account.UserAccountCreateRequest{ + UserID: newUser.ID, + AccountID: tr.Account.ID, + Roles: []user_account.UserAccountRole{user_account.UserAccountRole_User}, + } + rt.request = req + + w, ok := executeRequestTest(t, rt, ctx) + if !ok { + t.Fatalf("\t%s\tExecute request failed.", tests.Failed) + } + t.Logf("\t%s\tReceived valid status code of %d.", tests.Success, w.Code) + + var actual user_account.UserAccountResponse + if err := json.Unmarshal(w.Body.Bytes(), &actual); err != nil { + t.Logf("\t\tGot error : %+v", err) + t.Fatalf("\t%s\tDecode response body failed.", tests.Failed) + } + created = actual + + expectedMap := map[string]interface{}{ + "updated_at": web.NewTimeResponse(ctx, actual.UpdatedAt.Value), + "id": actual.ID, + "account_id": req.AccountID, + "user_id": req.UserID, + "status": web.NewEnumResponse(ctx, "active", user_account.UserAccountStatus_Values), + "roles": req.Roles, + "created_at": web.NewTimeResponse(ctx, actual.CreatedAt.Value), + } + + var expected user_account.UserAccountResponse + if err := decodeMapToStruct(expectedMap, &expected); err != nil { + t.Logf("\t\tGot error : %+v\nActual results to format expected : \n", err) + printResultMap(ctx, w.Body.Bytes()) // used to help format expectedMap + t.Fatalf("\t%s\tDecode expected failed.", tests.Failed) + } + + if diff := cmpDiff(t, actual, expected); diff { + if len(expectedMap) == 0 { + printResultMap(ctx, w.Body.Bytes()) // used to help format expectedMap + } + t.Fatalf("\t%s\tReceived expected result.", tests.Failed) + } + t.Logf("\t%s\tReceived expected result.", tests.Success) + } + + // Test read. + { + expectedStatus := http.StatusOK + + rt := requestTest{ + fmt.Sprintf("Read %d w/role %s", expectedStatus, tr.Role), + http.MethodGet, + fmt.Sprintf("/v1/user_accounts/%s", created.ID), + nil, + tr.Token, + tr.Claims, + expectedStatus, + nil, + } + t.Logf("\tTest: %s - %s %s", rt.name, rt.method, rt.url) + + w, ok := executeRequestTest(t, rt, ctx) + if !ok { + t.Fatalf("\t%s\tExecute request failed.", tests.Failed) + } + t.Logf("\t%s\tReceived valid status code of %d.", tests.Success, w.Code) + + var actual user_account.UserAccountResponse + if err := json.Unmarshal(w.Body.Bytes(), &actual); err != nil { + t.Logf("\t\tGot error : %+v", err) + t.Fatalf("\t%s\tDecode response body failed.", tests.Failed) + } + + if diff := cmpDiff(t, actual, created); diff { + t.Fatalf("\t%s\tReceived expected result.", tests.Failed) + } + t.Logf("\t%s\tReceived expected result.", tests.Success) + } + + // Test Read with random ID. + { + expectedStatus := http.StatusNotFound + + randID := uuid.NewRandom().String() + rt := requestTest{ + fmt.Sprintf("Read %d w/role %s using random ID", expectedStatus, tr.Role), + http.MethodGet, + fmt.Sprintf("/v1/user_accounts/%s", randID), + nil, + tr.Token, + tr.Claims, + expectedStatus, + nil, + } + t.Logf("\tTest: %s - %s %s", rt.name, rt.method, rt.url) + + w, ok := executeRequestTest(t, rt, ctx) + if !ok { + t.Fatalf("\t%s\tExecute request failed.", tests.Failed) + } + t.Logf("\t%s\tReceived valid status code of %d.", tests.Success, w.Code) + + var actual web.ErrorResponse + if err := json.Unmarshal(w.Body.Bytes(), &actual); err != nil { + t.Logf("\t\tGot error : %+v", err) + t.Fatalf("\t%s\tDecode response body failed.", tests.Failed) + } + + expected := web.ErrorResponse{ + Error: fmt.Sprintf("user account %s not found: Entity not found", randID), + } + + if diff := cmpDiff(t, actual, expected); diff { + t.Fatalf("\t%s\tReceived expected error.", tests.Failed) + } + t.Logf("\t%s\tReceived expected error.", tests.Success) + } + + // Test Read with forbidden ID. + forbiddenUserAccount := newMockUserAccount(newMockSignup().account.ID, user_account.UserAccountRole_Admin) + { + expectedStatus := http.StatusNotFound + + rt := requestTest{ + fmt.Sprintf("Read %d w/role %s using forbidden ID", expectedStatus, tr.Role), + http.MethodGet, + fmt.Sprintf("/v1/user_accounts/%s", forbiddenUserAccount.ID), + nil, + tr.Token, + tr.Claims, + expectedStatus, + nil, + } + t.Logf("\tTest: %s - %s %s", rt.name, rt.method, rt.url) + + w, ok := executeRequestTest(t, rt, ctx) + if !ok { + t.Fatalf("\t%s\tExecute request failed.", tests.Failed) + } + t.Logf("\t%s\tReceived valid status code of %d.", tests.Success, w.Code) + + var actual web.ErrorResponse + if err := json.Unmarshal(w.Body.Bytes(), &actual); err != nil { + t.Logf("\t\tGot error : %+v", err) + t.Fatalf("\t%s\tDecode response body failed.", tests.Failed) + } + + expected := web.ErrorResponse{ + Error: fmt.Sprintf("user account %s not found: Entity not found", forbiddenUserAccount.ID), + } + + if diff := cmpDiff(t, actual, expected); diff { + t.Fatalf("\t%s\tReceived expected error.", tests.Failed) + } + t.Logf("\t%s\tReceived expected error.", tests.Success) + } + + // Test update. + { + expectedStatus := http.StatusNoContent + + newStatus := user_account.UserAccountStatus_Invited + rt := requestTest{ + fmt.Sprintf("Update %d w/role %s", expectedStatus, tr.Role), + http.MethodPatch, + "/v1/user_accounts", + user_account.UserAccountUpdateRequest{ + UserID: created.UserID, + AccountID: created.AccountID, + Status: &newStatus, + }, + tr.Token, + tr.Claims, + expectedStatus, + nil, + } + t.Logf("\tTest: %s - %s %s", rt.name, rt.method, rt.url) + + w, ok := executeRequestTest(t, rt, ctx) + if !ok { + t.Fatalf("\t%s\tExecute request failed.", tests.Failed) + } + t.Logf("\t%s\tReceived valid status code of %d.", tests.Success, w.Code) + + if len(w.Body.String()) != 0 { + if diff := cmpDiff(t, w.Body.Bytes(), nil); diff { + t.Fatalf("\t%s\tReceived expected empty.", tests.Failed) + } + } + t.Logf("\t%s\tReceived expected empty.", tests.Success) + } + + // Test archive. + { + expectedStatus := http.StatusNoContent + + rt := requestTest{ + fmt.Sprintf("Archive %d w/role %s", expectedStatus, tr.Role), + http.MethodPatch, + "/v1/user_accounts/archive", + user_account.UserAccountArchiveRequest{ + UserID: created.UserID, + AccountID: created.AccountID, + }, + tr.Token, + tr.Claims, + expectedStatus, + nil, + } + t.Logf("\tTest: %s - %s %s", rt.name, rt.method, rt.url) + + w, ok := executeRequestTest(t, rt, ctx) + if !ok { + t.Fatalf("\t%s\tExecute request failed.", tests.Failed) + } + t.Logf("\t%s\tReceived valid status code of %d.", tests.Success, w.Code) + + if len(w.Body.String()) != 0 { + if diff := cmpDiff(t, w.Body.Bytes(), nil); diff { + t.Fatalf("\t%s\tReceived expected empty.", tests.Failed) + } + } + t.Logf("\t%s\tReceived expected empty.", tests.Success) + } + + // Test delete. + { + expectedStatus := http.StatusNoContent + + rt := requestTest{ + fmt.Sprintf("Delete %d w/role %s", expectedStatus, tr.Role), + http.MethodDelete, + "/v1/user_accounts", + user_account.UserAccountDeleteRequest{ + UserID: created.UserID, + AccountID: created.AccountID, + }, + tr.Token, + tr.Claims, + expectedStatus, + nil, + } + t.Logf("\tTest: %s - %s %s", rt.name, rt.method, rt.url) + + w, ok := executeRequestTest(t, rt, ctx) + if !ok { + t.Fatalf("\t%s\tExecute request failed.", tests.Failed) + } + t.Logf("\t%s\tReceived valid status code of %d.", tests.Success, w.Code) + + if len(w.Body.String()) != 0 { + if diff := cmpDiff(t, w.Body.Bytes(), nil); diff { + t.Fatalf("\t%s\tReceived expected empty.", tests.Failed) + } + } + t.Logf("\t%s\tReceived expected empty.", tests.Success) + } +} + +// TestUserAccountCRUDUser tests all the user account CRUD endpoints using an user with role user_account. +func TestUserAccountCRUDUser(t *testing.T) { + defer tests.Recover(t) + + tr := roleTests[auth.RoleUser] + + // Add claims to the context for the user_account. + ctx := context.WithValue(tests.Context(), auth.Key, tr.Claims) + + // Test create. + { + expectedStatus := http.StatusForbidden + + rt := requestTest{ + fmt.Sprintf("Create %d w/role %s", expectedStatus, tr.Role), + http.MethodPost, + "/v1/user_accounts", + user_account.UserAccountCreateRequest{ + UserID: uuid.NewRandom().String(), + AccountID: tr.Account.ID, + Roles: []user_account.UserAccountRole{user_account.UserAccountRole_User}, + }, + tr.Token, + tr.Claims, + expectedStatus, + nil, + } + t.Logf("\tTest: %s - %s %s", rt.name, rt.method, rt.url) + + w, ok := executeRequestTest(t, rt, ctx) + if !ok { + t.Fatalf("\t%s\tExecute request failed.", tests.Failed) + } + t.Logf("\t%s\tReceived valid status code of %d.", tests.Success, w.Code) + + var actual web.ErrorResponse + if err := json.Unmarshal(w.Body.Bytes(), &actual); err != nil { + t.Logf("\t\tGot error : %+v", err) + t.Fatalf("\t%s\tDecode response body failed.", tests.Failed) + } + + expected := web.ErrorResponse{ + Error: mid.ErrForbidden.Error(), + } + + if diff := cmpDiff(t, actual, expected); diff { + t.Fatalf("\t%s\tReceived expected error.", tests.Failed) + } + t.Logf("\t%s\tReceived expected error.", tests.Success) + } + + // Since role doesn't support create, bypass auth to test other endpoints. + created := newMockUserAccount(tr.Account.ID, user_account.UserAccountRole_User).Response(ctx) + + // Test read. + { + expectedStatus := http.StatusOK + + rt := requestTest{ + fmt.Sprintf("Read %d w/role %s", expectedStatus, tr.Role), + http.MethodGet, + fmt.Sprintf("/v1/user_accounts/%s", created.ID), + nil, + tr.Token, + tr.Claims, + expectedStatus, + nil, + } + t.Logf("\tTest: %s - %s %s", rt.name, rt.method, rt.url) + + w, ok := executeRequestTest(t, rt, ctx) + if !ok { + t.Fatalf("\t%s\tExecute request failed.", tests.Failed) + } + t.Logf("\t%s\tReceived valid status code of %d.", tests.Success, w.Code) + + var actual *user_account.UserAccountResponse + if err := json.Unmarshal(w.Body.Bytes(), &actual); err != nil { + t.Logf("\t\tGot error : %+v", err) + t.Fatalf("\t%s\tDecode response body failed.", tests.Failed) + } + + if diff := cmpDiff(t, actual, created); diff { + t.Fatalf("\t%s\tReceived expected result.", tests.Failed) + } + t.Logf("\t%s\tReceived expected result.", tests.Success) + } + + // Test Read with random ID. + { + expectedStatus := http.StatusNotFound + + randID := uuid.NewRandom().String() + rt := requestTest{ + fmt.Sprintf("Read %d w/role %s using random ID", expectedStatus, tr.Role), + http.MethodGet, + fmt.Sprintf("/v1/user_accounts/%s", randID), + nil, + tr.Token, + tr.Claims, + expectedStatus, + nil, + } + t.Logf("\tTest: %s - %s %s", rt.name, rt.method, rt.url) + + w, ok := executeRequestTest(t, rt, ctx) + if !ok { + t.Fatalf("\t%s\tExecute request failed.", tests.Failed) + } + t.Logf("\t%s\tReceived valid status code of %d.", tests.Success, w.Code) + + var actual web.ErrorResponse + if err := json.Unmarshal(w.Body.Bytes(), &actual); err != nil { + t.Logf("\t\tGot error : %+v", err) + t.Fatalf("\t%s\tDecode response body failed.", tests.Failed) + } + + expected := web.ErrorResponse{ + Error: fmt.Sprintf("user account %s not found: Entity not found", randID), + } + + if diff := cmpDiff(t, actual, expected); diff { + t.Fatalf("\t%s\tReceived expected error.", tests.Failed) + } + t.Logf("\t%s\tReceived expected error.", tests.Success) + } + + // Test Read with forbidden ID. + forbiddenUserAccount := newMockUserAccount(newMockSignup().account.ID, user_account.UserAccountRole_Admin) + { + expectedStatus := http.StatusNotFound + + rt := requestTest{ + fmt.Sprintf("Read %d w/role %s using forbidden ID", expectedStatus, tr.Role), + http.MethodGet, + fmt.Sprintf("/v1/user_accounts/%s", forbiddenUserAccount.ID), + nil, + tr.Token, + tr.Claims, + expectedStatus, + nil, + } + t.Logf("\tTest: %s - %s %s", rt.name, rt.method, rt.url) + + w, ok := executeRequestTest(t, rt, ctx) + if !ok { + t.Fatalf("\t%s\tExecute request failed.", tests.Failed) + } + t.Logf("\t%s\tReceived valid status code of %d.", tests.Success, w.Code) + + var actual web.ErrorResponse + if err := json.Unmarshal(w.Body.Bytes(), &actual); err != nil { + t.Logf("\t\tGot error : %+v", err) + t.Fatalf("\t%s\tDecode response body failed.", tests.Failed) + } + + expected := web.ErrorResponse{ + Error: fmt.Sprintf("user account %s not found: Entity not found", forbiddenUserAccount.ID), + } + + if diff := cmpDiff(t, actual, expected); diff { + t.Fatalf("\t%s\tReceived expected error.", tests.Failed) + } + t.Logf("\t%s\tReceived expected error.", tests.Success) + } + + // Test update. + { + expectedStatus := http.StatusForbidden + + newStatus := user_account.UserAccountStatus_Invited + rt := requestTest{ + fmt.Sprintf("Update %d w/role %s", expectedStatus, tr.Role), + http.MethodPatch, + "/v1/user_accounts", + user_account.UserAccountUpdateRequest{ + UserID: created.UserID, + AccountID: created.AccountID, + Status: &newStatus, + }, + tr.Token, + tr.Claims, + expectedStatus, + nil, + } + t.Logf("\tTest: %s - %s %s", rt.name, rt.method, rt.url) + + w, ok := executeRequestTest(t, rt, ctx) + if !ok { + t.Fatalf("\t%s\tExecute request failed.", tests.Failed) + } + t.Logf("\t%s\tReceived valid status code of %d.", tests.Success, w.Code) + + var actual web.ErrorResponse + if err := json.Unmarshal(w.Body.Bytes(), &actual); err != nil { + t.Logf("\t\tGot error : %+v", err) + t.Fatalf("\t%s\tDecode response body failed.", tests.Failed) + } + + expected := web.ErrorResponse{ + Error: account.ErrForbidden.Error(), + } + + if diff := cmpDiff(t, actual, expected); diff { + t.Fatalf("\t%s\tReceived expected error.", tests.Failed) + } + t.Logf("\t%s\tReceived expected error.", tests.Success) + } + + // Test archive. + { + expectedStatus := http.StatusForbidden + + rt := requestTest{ + fmt.Sprintf("Archive %d w/role %s", expectedStatus, tr.Role), + http.MethodPatch, + "/v1/user_accounts/archive", + user_account.UserAccountArchiveRequest{ + UserID: created.UserID, + AccountID: created.AccountID, + }, + tr.Token, + tr.Claims, + expectedStatus, + nil, + } + t.Logf("\tTest: %s - %s %s", rt.name, rt.method, rt.url) + + w, ok := executeRequestTest(t, rt, ctx) + if !ok { + t.Fatalf("\t%s\tExecute request failed.", tests.Failed) + } + t.Logf("\t%s\tReceived valid status code of %d.", tests.Success, w.Code) + + var actual web.ErrorResponse + if err := json.Unmarshal(w.Body.Bytes(), &actual); err != nil { + t.Logf("\t\tGot error : %+v", err) + t.Fatalf("\t%s\tDecode response body failed.", tests.Failed) + } + + expected := web.ErrorResponse{ + Error: mid.ErrForbidden.Error(), + } + + if diff := cmpDiff(t, actual, expected); diff { + t.Fatalf("\t%s\tReceived expected error.", tests.Failed) + } + t.Logf("\t%s\tReceived expected error.", tests.Success) + } + + // Test delete. + { + expectedStatus := http.StatusForbidden + + rt := requestTest{ + fmt.Sprintf("Delete %d w/role %s", expectedStatus, tr.Role), + http.MethodDelete, + "/v1/user_accounts", + user_account.UserAccountArchiveRequest{ + UserID: created.UserID, + AccountID: created.AccountID, + }, + tr.Token, + tr.Claims, + expectedStatus, + nil, + } + t.Logf("\tTest: %s - %s %s", rt.name, rt.method, rt.url) + + w, ok := executeRequestTest(t, rt, ctx) + if !ok { + t.Fatalf("\t%s\tExecute request failed.", tests.Failed) + } + t.Logf("\t%s\tReceived valid status code of %d.", tests.Success, w.Code) + + var actual web.ErrorResponse + if err := json.Unmarshal(w.Body.Bytes(), &actual); err != nil { + t.Logf("\t\tGot error : %+v", err) + t.Fatalf("\t%s\tDecode response body failed.", tests.Failed) + } + + expected := web.ErrorResponse{ + Error: mid.ErrForbidden.Error(), + } + + if diff := cmpDiff(t, actual, expected); diff { + t.Fatalf("\t%s\tReceived expected error.", tests.Failed) + } + t.Logf("\t%s\tReceived expected error.", tests.Success) + } +} + +// TestUserAccountCreate validates create user account endpoint. +func TestUserAccountCreate(t *testing.T) { + defer tests.Recover(t) + + tr := roleTests[auth.RoleAdmin] + + // Add claims to the context for the user_account. + ctx := context.WithValue(tests.Context(), auth.Key, tr.Claims) + + // Test create with invalid data. + { + expectedStatus := http.StatusBadRequest + + invalidStatus := user_account.UserAccountStatus("invalid status") + rt := requestTest{ + fmt.Sprintf("Create %d w/role %s using invalid data", expectedStatus, tr.Role), + http.MethodPost, + "/v1/user_accounts", + user_account.UserAccountCreateRequest{ + UserID: uuid.NewRandom().String(), + AccountID: tr.Account.ID, + Roles: []user_account.UserAccountRole{user_account.UserAccountRole_User}, + Status: &invalidStatus, + }, + tr.Token, + tr.Claims, + expectedStatus, + nil, + } + t.Logf("\tTest: %s - %s %s", rt.name, rt.method, rt.url) + + w, ok := executeRequestTest(t, rt, ctx) + if !ok { + t.Fatalf("\t%s\tExecute request failed.", tests.Failed) + } + t.Logf("\t%s\tReceived valid status code of %d.", tests.Success, w.Code) + + var actual web.ErrorResponse + if err := json.Unmarshal(w.Body.Bytes(), &actual); err != nil { + t.Logf("\t\tGot error : %+v", err) + t.Fatalf("\t%s\tDecode response body failed.", tests.Failed) + } + + expected := web.ErrorResponse{ + Error: "field validation error", + Fields: []web.FieldError{ + {Field: "status", Error: "Key: 'UserAccountCreateRequest.status' Error:Field validation for 'status' failed on the 'oneof' tag"}, + }, + } + + if diff := cmpDiff(t, actual, expected); diff { + t.Fatalf("\t%s\tReceived expected error.", tests.Failed) + } + t.Logf("\t%s\tReceived expected error.", tests.Success) + } +} + +// TestUserAccountUpdate validates update user account endpoint. +func TestUserAccountUpdate(t *testing.T) { + defer tests.Recover(t) + + tr := roleTests[auth.RoleAdmin] + + // Add claims to the context for the user_account. + ctx := context.WithValue(tests.Context(), auth.Key, tr.Claims) + + // Test update with invalid data. + { + expectedStatus := http.StatusBadRequest + + invalidStatus := user_account.UserAccountStatus("invalid status") + rt := requestTest{ + fmt.Sprintf("Update %d w/role %s using invalid data", expectedStatus, tr.Role), + http.MethodPatch, + "/v1/user_accounts", + user_account.UserAccountUpdateRequest{ + UserID: uuid.NewRandom().String(), + AccountID: uuid.NewRandom().String(), + Status: &invalidStatus, + }, + tr.Token, + tr.Claims, + expectedStatus, + nil, + } + t.Logf("\tTest: %s - %s %s", rt.name, rt.method, rt.url) + + w, ok := executeRequestTest(t, rt, ctx) + if !ok { + t.Fatalf("\t%s\tExecute request failed.", tests.Failed) + } + t.Logf("\t%s\tReceived valid status code of %d.", tests.Success, w.Code) + + var actual web.ErrorResponse + if err := json.Unmarshal(w.Body.Bytes(), &actual); err != nil { + t.Logf("\t\tGot error : %+v", err) + t.Fatalf("\t%s\tDecode response body failed.", tests.Failed) + } + + expected := web.ErrorResponse{ + Error: "field validation error", + Fields: []web.FieldError{ + {Field: "status", Error: "Key: 'UserAccountUpdateRequest.status' Error:Field validation for 'status' failed on the 'oneof' tag"}, + }, + } + + if diff := cmpDiff(t, actual, expected); diff { + t.Fatalf("\t%s\tReceived expected error.", tests.Failed) + } + t.Logf("\t%s\tReceived expected error.", tests.Success) + } +} + +// TestUserAccountArchive validates archive user account endpoint. +func TestUserAccountArchive(t *testing.T) { + defer tests.Recover(t) + + tr := roleTests[auth.RoleAdmin] + + // Add claims to the context for the user_account. + ctx := context.WithValue(tests.Context(), auth.Key, tr.Claims) + + // Test archive with invalid data. + { + expectedStatus := http.StatusBadRequest + + rt := requestTest{ + fmt.Sprintf("Archive %d w/role %s using invalid data", expectedStatus, tr.Role), + http.MethodPatch, + "/v1/user_accounts/archive", + user_account.UserAccountArchiveRequest{ + UserID: "foo", + AccountID: "bar", + }, + tr.Token, + tr.Claims, + expectedStatus, + nil, + } + t.Logf("\tTest: %s - %s %s", rt.name, rt.method, rt.url) + + w, ok := executeRequestTest(t, rt, ctx) + if !ok { + t.Fatalf("\t%s\tExecute request failed.", tests.Failed) + } + t.Logf("\t%s\tReceived valid status code of %d.", tests.Success, w.Code) + + var actual web.ErrorResponse + if err := json.Unmarshal(w.Body.Bytes(), &actual); err != nil { + t.Logf("\t\tGot error : %+v", err) + t.Fatalf("\t%s\tDecode response body failed.", tests.Failed) + } + + expected := web.ErrorResponse{ + Error: "field validation error", + Fields: []web.FieldError{ + {Field: "user_id", Error: "Key: 'UserAccountArchiveRequest.user_id' Error:Field validation for 'user_id' failed on the 'uuid' tag"}, + {Field: "account_id", Error: "Key: 'UserAccountArchiveRequest.account_id' Error:Field validation for 'account_id' failed on the 'uuid' tag"}, + }, + } + + if diff := cmpDiff(t, actual, expected); diff { + t.Fatalf("\t%s\tReceived expected error.", tests.Failed) + } + t.Logf("\t%s\tReceived expected error.", tests.Success) + } + + // Test archive with forbidden ID. + forbiddenUserAccount := newMockUserAccount(newMockSignup().account.ID, user_account.UserAccountRole_Admin) + { + expectedStatus := http.StatusForbidden + + rt := requestTest{ + fmt.Sprintf("Archive %d w/role %s using forbidden IDs", expectedStatus, tr.Role), + http.MethodPatch, + "/v1/user_accounts/archive", + user_account.UserAccountArchiveRequest{ + UserID: forbiddenUserAccount.UserID, + AccountID: forbiddenUserAccount.AccountID, + }, + tr.Token, + tr.Claims, + expectedStatus, + nil, + } + t.Logf("\tTest: %s - %s %s", rt.name, rt.method, rt.url) + + w, ok := executeRequestTest(t, rt, ctx) + if !ok { + t.Fatalf("\t%s\tExecute request failed.", tests.Failed) + } + t.Logf("\t%s\tReceived valid status code of %d.", tests.Success, w.Code) + + var actual web.ErrorResponse + if err := json.Unmarshal(w.Body.Bytes(), &actual); err != nil { + t.Logf("\t\tGot error : %+v", err) + t.Fatalf("\t%s\tDecode response body failed.", tests.Failed) + } + + expected := web.ErrorResponse{ + Error: user_account.ErrForbidden.Error(), + } + + if diff := cmpDiff(t, actual, expected); diff { + t.Fatalf("\t%s\tReceived expected error.", tests.Failed) + } + t.Logf("\t%s\tReceived expected error.", tests.Success) + } +} + +// TestUserAccountDelete validates delete user account endpoint. +func TestUserAccountDelete(t *testing.T) { + defer tests.Recover(t) + + tr := roleTests[auth.RoleAdmin] + + // Add claims to the context for the user_account. + ctx := context.WithValue(tests.Context(), auth.Key, tr.Claims) + + // Test delete with invalid data. + { + expectedStatus := http.StatusBadRequest + + req := user_account.UserAccountDeleteRequest{ + UserID: "foo", + AccountID: "bar", + } + + rt := requestTest{ + fmt.Sprintf("Delete %d w/role %s using invalid data", expectedStatus, tr.Role), + http.MethodDelete, + "/v1/user_accounts", + req, + tr.Token, + tr.Claims, + expectedStatus, + nil, + } + t.Logf("\tTest: %s - %s %s", rt.name, rt.method, rt.url) + + w, ok := executeRequestTest(t, rt, ctx) + if !ok { + t.Fatalf("\t%s\tExecute request failed.", tests.Failed) + } + t.Logf("\t%s\tReceived valid status code of %d.", tests.Success, w.Code) + + var actual web.ErrorResponse + if err := json.Unmarshal(w.Body.Bytes(), &actual); err != nil { + t.Logf("\t\tGot error : %+v", err) + t.Fatalf("\t%s\tDecode response body failed.", tests.Failed) + } + + expected := web.ErrorResponse{ + Error: "field validation error", + Fields: []web.FieldError{ + {Field: "user_id", Error: "Key: 'UserAccountDeleteRequest.user_id' Error:Field validation for 'user_id' failed on the 'uuid' tag"}, + {Field: "account_id", Error: "Key: 'UserAccountDeleteRequest.account_id' Error:Field validation for 'account_id' failed on the 'uuid' tag"}, + }, + } + + if diff := cmpDiff(t, actual, expected); diff { + t.Fatalf("\t%s\tReceived expected error.", tests.Failed) + } + t.Logf("\t%s\tReceived expected error.", tests.Success) + } + + // Test delete with forbidden ID. + forbiddenUserAccount := newMockUserAccount(newMockSignup().account.ID, user_account.UserAccountRole_Admin) + { + expectedStatus := http.StatusForbidden + + rt := requestTest{ + fmt.Sprintf("Delete %d w/role %s using forbidden IDs", expectedStatus, tr.Role), + http.MethodDelete, + fmt.Sprintf("/v1/user_accounts"), + user_account.UserAccountDeleteRequest{ + UserID: forbiddenUserAccount.UserID, + AccountID: forbiddenUserAccount.AccountID, + }, + tr.Token, + tr.Claims, + expectedStatus, + nil, + } + t.Logf("\tTest: %s - %s %s", rt.name, rt.method, rt.url) + + w, ok := executeRequestTest(t, rt, ctx) + if !ok { + t.Fatalf("\t%s\tExecute request failed.", tests.Failed) + } + t.Logf("\t%s\tReceived valid status code of %d.", tests.Success, w.Code) + + var actual web.ErrorResponse + if err := json.Unmarshal(w.Body.Bytes(), &actual); err != nil { + t.Logf("\t\tGot error : %+v", err) + t.Fatalf("\t%s\tDecode response body failed.", tests.Failed) + } + + expected := web.ErrorResponse{ + Error: user_account.ErrForbidden.Error(), + } + + if diff := cmpDiff(t, actual, expected); diff { + t.Fatalf("\t%s\tReceived expected error.", tests.Failed) + } + t.Logf("\t%s\tReceived expected error.", tests.Success) + } +} diff --git a/example-project/cmd/web-api/tests/user_test.go b/example-project/cmd/web-api/tests/user_test.go index 7d007be..ba38855 100644 --- a/example-project/cmd/web-api/tests/user_test.go +++ b/example-project/cmd/web-api/tests/user_test.go @@ -4,559 +4,1458 @@ import ( "context" "encoding/json" "fmt" - "geeks-accelerator/oss/saas-starter-kit/example-project/internal/mid" "net/http" - "strconv" + "net/http/httptest" "testing" "time" - "geeks-accelerator/oss/saas-starter-kit/example-project/internal/user" + "geeks-accelerator/oss/saas-starter-kit/example-project/internal/mid" "geeks-accelerator/oss/saas-starter-kit/example-project/internal/platform/auth" "geeks-accelerator/oss/saas-starter-kit/example-project/internal/platform/tests" "geeks-accelerator/oss/saas-starter-kit/example-project/internal/platform/web" - "github.com/google/go-cmp/cmp" + "geeks-accelerator/oss/saas-starter-kit/example-project/internal/user" + "geeks-accelerator/oss/saas-starter-kit/example-project/internal/user_account" "github.com/pborman/uuid" ) -func mockUser() *user.User { - req := user.UserCreateRequest{ +type mockUser struct { + *user.User + password string +} + +func mockUserCreateRequest() user.UserCreateRequest { + return user.UserCreateRequest{ Name: "Lee Brown", Email: uuid.NewRandom().String() + "@geeksinthewoods.com", Password: "akTechFr0n!ier", PasswordConfirm: "akTechFr0n!ier", } +} - a, err := user.Create(tests.Context(), auth.Claims{}, test.MasterDB, req, time.Now().UTC().AddDate(-1, -1, -1)) +// mockUser creates a new user for testing and associates it with the supplied account ID. +func newMockUser(accountID string, role user_account.UserAccountRole) mockUser { + req := mockUserCreateRequest() + u, err := user.Create(tests.Context(), auth.Claims{}, test.MasterDB, req, time.Now().UTC().AddDate(-1, -1, -1)) if err != nil { panic(err) } - return a + + _, err = user_account.Create(tests.Context(), auth.Claims{}, test.MasterDB, user_account.UserAccountCreateRequest{ + UserID: u.ID, + AccountID: accountID, + Roles: []user_account.UserAccountRole{role}, + }, time.Now().UTC().AddDate(-1, -1, -1)) + if err != nil { + panic(err) + } + + return mockUser{ + User: u, + password: req.Password, + } } -// TestUser is the entry point for the user endpoints. -func TestUser(t *testing.T) { +// TestUserCRUDAdmin tests all the user CRUD endpoints using an user with role admin. +func TestUserCRUDAdmin(t *testing.T) { defer tests.Recover(t) - t.Run("getUser", getUser) - t.Run("createUser", createUser) - t.Run("patchUser", patchUser) - t.Run("patchUserPassword", patchUserPassword) -} + tr := roleTests[auth.RoleAdmin] -// getUser validates get user by ID endpoint. -func getUser(t *testing.T) { + // Add claims to the context for the user. + ctx := context.WithValue(tests.Context(), auth.Key, tr.Claims) - var rtests []requestTest + // Test create. + var created user.UserResponse + { + expectedStatus := http.StatusCreated - forbiddenUser := mockUser() - - // Both roles should be able to read the user. - for rn, tr := range roleTests { - usr := tr.SignupResult.User - - // Test 200. - rtests = append(rtests, requestTest{ - fmt.Sprintf("Role %s 200", rn), - http.MethodGet, - fmt.Sprintf("/v1/users/%s", usr.ID), - nil, - tr.Token, - tr.Claims, - http.StatusOK, - nil, - func(treq requestTest, body []byte) bool { - var actual user.UserResponse - if err := json.Unmarshal(body, &actual); err != nil { - t.Logf("\t\tGot error : %+v", err) - return false - } - - // Add claims to the context so they can be retrieved later. - ctx := context.WithValue(tests.Context(), auth.Key, tr.Claims) - - expectedMap := map[string]interface{}{ - "updated_at": web.NewTimeResponse(ctx, usr.UpdatedAt), - "id": usr.ID, - "email": usr.Email, - "timezone": usr.Timezone, - "created_at": web.NewTimeResponse(ctx, usr.CreatedAt), - "name": usr.Name, - } - expectedJson, err := json.Marshal(expectedMap) - if err != nil { - t.Logf("\t\tGot error : %+v", err) - return false - } - - var expected user.UserResponse - if err := json.Unmarshal([]byte(expectedJson), &expected); err != nil { - t.Logf("\t\tGot error : %+v", err) - printResultMap(ctx, body) - return false - } - - if diff := cmp.Diff(actual, expected); diff != "" { - actualJSON, err := json.MarshalIndent(actual, "", " ") - if err != nil { - t.Logf("\t\tGot error : %+v", err) - return false - } - t.Logf("\t\tGot : %s\n", actualJSON) - - expectedJSON, err := json.MarshalIndent(expected, "", " ") - if err != nil { - t.Logf("\t\tGot error : %+v", err) - return false - } - t.Logf("\t\tExpected : %s\n", expectedJSON) - - t.Logf("\t\tDiff : %s\n", diff) - - if len(expectedMap) == 0 { - printResultMap(ctx, body) - } - - return false - } - - return true - }, - }) - - // Test 404. - invalidID := uuid.NewRandom().String() - rtests = append(rtests, requestTest{ - fmt.Sprintf("Role %s 404 w/invalid ID", rn), - http.MethodGet, - fmt.Sprintf("/v1/users/%s", invalidID), - nil, - tr.Token, - tr.Claims, - http.StatusNotFound, - web.ErrorResponse{ - Error: fmt.Sprintf("user %s not found: Entity not found", invalidID), - }, - func(treq requestTest, body []byte) bool { - return true - }, - }) - - // Test 404 - User exists but not allowed. - rtests = append(rtests, requestTest{ - fmt.Sprintf("Role %s 404 w/random user ID", rn), - http.MethodGet, - fmt.Sprintf("/v1/users/%s", forbiddenUser.ID), - nil, - tr.Token, - tr.Claims, - http.StatusNotFound, - web.ErrorResponse{ - Error: fmt.Sprintf("user %s not found: Entity not found", forbiddenUser.ID), - }, - func(treq requestTest, body []byte) bool { - return true - }, - }) - } - - runRequestTests(t, rtests) -} - -// createUser validates create user endpoint. -func createUser(t *testing.T) { - - var rtests []requestTest - - // Test create user. - // Admin role: 201 - // User role 403 - for rn, tr := range roleTests { - var expectedStatus int - var expectedErr interface{} - - // Test 201. - if rn == auth.RoleAdmin { - expectedStatus = http.StatusCreated - } else { - expectedStatus = http.StatusForbidden - expectedErr = web.ErrorResponse{ - Error: mid.ErrForbidden.Error(), - } - } - - rtests = append(rtests, requestTest{ - fmt.Sprintf("Role %s %d", rn, expectedStatus), + req := mockUserCreateRequest() + rt := requestTest{ + fmt.Sprintf("Create %d w/role %s", expectedStatus, tr.Role), http.MethodPost, "/v1/users", - user.UserCreateRequest{ - Name: "Lee Brown", - Email: uuid.NewRandom().String() + rn + strconv.Itoa(len(rtests))+ "@geeksinthewoods.com", - Password: "akTechFr0n!ier", - PasswordConfirm: "akTechFr0n!ier", - }, + req, tr.Token, tr.Claims, expectedStatus, - expectedErr, - func(treq requestTest, body []byte) bool { - if treq.error != nil { - return true - } + nil, + } + t.Logf("\tTest: %s - %s %s", rt.name, rt.method, rt.url) - var actual user.UserResponse - if err := json.Unmarshal(body, &actual); err != nil { - t.Logf("\t\tGot error : %+v", err) - return false - } + w, ok := executeRequestTest(t, rt, ctx) + if !ok { + t.Fatalf("\t%s\tExecute request failed.", tests.Failed) + } + t.Logf("\t%s\tReceived valid status code of %d.", tests.Success, w.Code) - // Add claims to the context so they can be retrieved later. - ctx := context.WithValue(tests.Context(), auth.Key, tr.Claims) + var actual user.UserResponse + if err := json.Unmarshal(w.Body.Bytes(), &actual); err != nil { + t.Logf("\t\tGot error : %+v", err) + t.Fatalf("\t%s\tDecode response body failed.", tests.Failed) + } + created = actual - req := treq.request.(user.UserCreateRequest) - - expectedMap := map[string]interface{}{ - "updated_at": web.NewTimeResponse(ctx, actual.UpdatedAt.Value), - "id": actual.ID, - "email": req.Email, - "timezone": actual.Timezone, - "created_at": web.NewTimeResponse(ctx, actual.CreatedAt.Value), - "name": req.Name, - } - expectedJson, err := json.Marshal(expectedMap) - if err != nil { - t.Logf("\t\tGot error : %+v", err) - return false - } - - var expected user.UserResponse - if err := json.Unmarshal([]byte(expectedJson), &expected); err != nil { - t.Logf("\t\tGot error : %+v", err) - printResultMap(ctx, body) - return false - } - - if diff := cmp.Diff(actual, expected); diff != "" { - actualJSON, err := json.MarshalIndent(actual, "", " ") - if err != nil { - t.Logf("\t\tGot error : %+v", err) - return false - } - t.Logf("\t\tGot : %s\n", actualJSON) - - expectedJSON, err := json.MarshalIndent(expected, "", " ") - if err != nil { - t.Logf("\t\tGot error : %+v", err) - return false - } - t.Logf("\t\tExpected : %s\n", expectedJSON) - - t.Logf("\t\tDiff : %s\n", diff) - - if len(expectedMap) == 0 { - printResultMap(ctx, body) - } - - return false - } - - return true - }, - }) - } - - // Test update a user with invalid data. - // Admin role: 400 - // User role 403 - for rn, tr := range roleTests { - var expectedStatus int - var expectedErr interface{} - - // Test 201. - if rn == auth.RoleAdmin { - expectedStatus = http.StatusBadRequest - expectedErr = web.ErrorResponse{ - Error: "field validation error", - Fields: []web.FieldError{ - {Field: "email", Error: "Key: 'UserCreateRequest.email' Error:Field validation for 'email' failed on the 'email' tag"}, - }, - } - } else { - expectedStatus = http.StatusForbidden - expectedErr = web.ErrorResponse{ - Error: mid.ErrForbidden.Error(), - } + expectedMap := map[string]interface{}{ + "updated_at": web.NewTimeResponse(ctx, actual.UpdatedAt.Value), + "id": actual.ID, + "email": req.Email, + "timezone": actual.Timezone, + "created_at": web.NewTimeResponse(ctx, actual.CreatedAt.Value), + "name": req.Name, } - rtests = append(rtests, requestTest{ - fmt.Sprintf("Role %s %d w/invalid data", rn, expectedStatus), - http.MethodPost, - "/v1/users", - user.UserCreateRequest{ - Name: "Lee Brown", - Email: "invalid email address", - Password: "akTechFr0n!ier", - PasswordConfirm: "akTechFr0n!ier", - }, + var expected user.UserResponse + if err := decodeMapToStruct(expectedMap, &expected); err != nil { + t.Logf("\t\tGot error : %+v\nActual results to format expected : \n", err) + printResultMap(ctx, w.Body.Bytes()) // used to help format expectedMap + t.Fatalf("\t%s\tDecode expected failed.", tests.Failed) + } + + if diff := cmpDiff(t, actual, expected); diff { + if len(expectedMap) == 0 { + printResultMap(ctx, w.Body.Bytes()) // used to help format expectedMap + } + t.Fatalf("\t%s\tReceived expected result.", tests.Failed) + } + t.Logf("\t%s\tReceived expected result.", tests.Success) + + // Only for user creation do we need to do this. + _, err := user_account.Create(tests.Context(), auth.Claims{}, test.MasterDB, user_account.UserAccountCreateRequest{ + UserID: actual.ID, + AccountID: tr.Account.ID, + Roles: []user_account.UserAccountRole{user_account.UserAccountRole_User}, + }, time.Now().UTC().AddDate(-1, -1, -1)) + if err != nil { + t.Fatalf("\t%s\tLink user to account.", tests.Failed) + } + } + + // Test read. + { + expectedStatus := http.StatusOK + + rt := requestTest{ + fmt.Sprintf("Read %d w/role %s", expectedStatus, tr.Role), + http.MethodGet, + fmt.Sprintf("/v1/users/%s", created.ID), + nil, tr.Token, tr.Claims, expectedStatus, - expectedErr, - func(treq requestTest, body []byte) bool { - return true - }, - }) + nil, + } + t.Logf("\tTest: %s - %s %s", rt.name, rt.method, rt.url) + + w, ok := executeRequestTest(t, rt, ctx) + if !ok { + t.Fatalf("\t%s\tExecute request failed.", tests.Failed) + } + t.Logf("\t%s\tReceived valid status code of %d.", tests.Success, w.Code) + + var actual user.UserResponse + if err := json.Unmarshal(w.Body.Bytes(), &actual); err != nil { + t.Logf("\t\tGot error : %+v", err) + t.Fatalf("\t%s\tDecode response body failed.", tests.Failed) + } + + if diff := cmpDiff(t, actual, created); diff { + t.Fatalf("\t%s\tReceived expected result.", tests.Failed) + } + t.Logf("\t%s\tReceived expected result.", tests.Success) } - runRequestTests(t, rtests) -} + // Test Read with random ID. + { + expectedStatus := http.StatusNotFound -// patchUser validates update user by ID endpoint. -func patchUser(t *testing.T) { + randID := uuid.NewRandom().String() + rt := requestTest{ + fmt.Sprintf("Read %d w/role %s using random ID", expectedStatus, tr.Role), + http.MethodGet, + fmt.Sprintf("/v1/users/%s", randID), + nil, + tr.Token, + tr.Claims, + expectedStatus, + nil, + } + t.Logf("\tTest: %s - %s %s", rt.name, rt.method, rt.url) - var rtests []requestTest + w, ok := executeRequestTest(t, rt, ctx) + if !ok { + t.Fatalf("\t%s\tExecute request failed.", tests.Failed) + } + t.Logf("\t%s\tReceived valid status code of %d.", tests.Success, w.Code) - // Test update a user - // Admin role: 204 - // User role 204 - user ID matches claims so OK - for rn, tr := range roleTests { + var actual web.ErrorResponse + if err := json.Unmarshal(w.Body.Bytes(), &actual); err != nil { + t.Logf("\t\tGot error : %+v", err) + t.Fatalf("\t%s\tDecode response body failed.", tests.Failed) + } + + expected := web.ErrorResponse{ + Error: fmt.Sprintf("user %s not found: Entity not found", randID), + } + + if diff := cmpDiff(t, actual, expected); diff { + t.Fatalf("\t%s\tReceived expected error.", tests.Failed) + } + t.Logf("\t%s\tReceived expected error.", tests.Success) + } + + // Test Read with forbidden ID. + { + expectedStatus := http.StatusNotFound + + rt := requestTest{ + fmt.Sprintf("Read %d w/role %s using forbidden ID", expectedStatus, tr.Role), + http.MethodGet, + fmt.Sprintf("/v1/users/%s", tr.ForbiddenUser.ID), + nil, + tr.Token, + tr.Claims, + expectedStatus, + nil, + } + t.Logf("\tTest: %s - %s %s", rt.name, rt.method, rt.url) + + w, ok := executeRequestTest(t, rt, ctx) + if !ok { + t.Fatalf("\t%s\tExecute request failed.", tests.Failed) + } + t.Logf("\t%s\tReceived valid status code of %d.", tests.Success, w.Code) + + var actual web.ErrorResponse + if err := json.Unmarshal(w.Body.Bytes(), &actual); err != nil { + t.Logf("\t\tGot error : %+v", err) + t.Fatalf("\t%s\tDecode response body failed.", tests.Failed) + } + + expected := web.ErrorResponse{ + Error: fmt.Sprintf("user %s not found: Entity not found", tr.ForbiddenUser.ID), + } + + if diff := cmpDiff(t, actual, expected); diff { + t.Fatalf("\t%s\tReceived expected error.", tests.Failed) + } + t.Logf("\t%s\tReceived expected error.", tests.Success) + } + + // Test update. + { expectedStatus := http.StatusNoContent - newName := rn + uuid.NewRandom().String() + strconv.Itoa(len(rtests)) - rtests = append(rtests, requestTest{ - fmt.Sprintf("Role %s %d", rn, expectedStatus), + + newName := uuid.NewRandom().String() + rt := requestTest{ + fmt.Sprintf("Update %d w/role %s", expectedStatus, tr.Role), http.MethodPatch, "/v1/users", user.UserUpdateRequest{ - ID: tr.SignupResult.User.ID, + ID: created.ID, Name: &newName, }, tr.Token, tr.Claims, expectedStatus, nil, - func(treq requestTest, body []byte) bool { - return true - }, - }) + } + t.Logf("\tTest: %s - %s %s", rt.name, rt.method, rt.url) + + w, ok := executeRequestTest(t, rt, ctx) + if !ok { + t.Fatalf("\t%s\tExecute request failed.", tests.Failed) + } + t.Logf("\t%s\tReceived valid status code of %d.", tests.Success, w.Code) + + if len(w.Body.String()) != 0 { + if diff := cmpDiff(t, w.Body.Bytes(), nil); diff { + t.Fatalf("\t%s\tReceived expected empty.", tests.Failed) + } + } + t.Logf("\t%s\tReceived expected empty.", tests.Success) } - // Test update a user with invalid data. - // Admin role: 400 - // User role 400 - for rn, tr := range roleTests { + // Test update password. + { + expectedStatus := http.StatusNoContent + + newPass := uuid.NewRandom().String() + rt := requestTest{ + fmt.Sprintf("Update password %d w/role %s", expectedStatus, tr.Role), + http.MethodPatch, + "/v1/users/password", + user.UserUpdatePasswordRequest{ + ID: created.ID, + Password: newPass, + PasswordConfirm: newPass, + }, + tr.Token, + tr.Claims, + expectedStatus, + nil, + } + t.Logf("\tTest: %s - %s %s", rt.name, rt.method, rt.url) + + w, ok := executeRequestTest(t, rt, ctx) + if !ok { + t.Fatalf("\t%s\tExecute request failed.", tests.Failed) + } + t.Logf("\t%s\tReceived valid status code of %d.", tests.Success, w.Code) + + if len(w.Body.String()) != 0 { + if diff := cmpDiff(t, w.Body.Bytes(), nil); diff { + t.Fatalf("\t%s\tReceived expected empty.", tests.Failed) + } + } + t.Logf("\t%s\tReceived expected empty.", tests.Success) + } + + // Test archive. + { + expectedStatus := http.StatusNoContent + + rt := requestTest{ + fmt.Sprintf("Archive %d w/role %s", expectedStatus, tr.Role), + http.MethodPatch, + "/v1/users/archive", + user.UserArchiveRequest{ + ID: created.ID, + }, + tr.Token, + tr.Claims, + expectedStatus, + nil, + } + t.Logf("\tTest: %s - %s %s", rt.name, rt.method, rt.url) + + w, ok := executeRequestTest(t, rt, ctx) + if !ok { + t.Fatalf("\t%s\tExecute request failed.", tests.Failed) + } + t.Logf("\t%s\tReceived valid status code of %d.", tests.Success, w.Code) + + if len(w.Body.String()) != 0 { + if diff := cmpDiff(t, w.Body.Bytes(), nil); diff { + t.Fatalf("\t%s\tReceived expected empty.", tests.Failed) + } + } + t.Logf("\t%s\tReceived expected empty.", tests.Success) + } + + // Test delete. + { + expectedStatus := http.StatusNoContent + + rt := requestTest{ + fmt.Sprintf("Delete %d w/role %s", expectedStatus, tr.Role), + http.MethodDelete, + fmt.Sprintf("/v1/users/%s", created.ID), + nil, + tr.Token, + tr.Claims, + expectedStatus, + nil, + } + t.Logf("\tTest: %s - %s %s", rt.name, rt.method, rt.url) + + w, ok := executeRequestTest(t, rt, ctx) + if !ok { + t.Fatalf("\t%s\tExecute request failed.", tests.Failed) + } + t.Logf("\t%s\tReceived valid status code of %d.", tests.Success, w.Code) + + if len(w.Body.String()) != 0 { + if diff := cmpDiff(t, w.Body.Bytes(), nil); diff { + t.Fatalf("\t%s\tReceived expected empty.", tests.Failed) + } + } + t.Logf("\t%s\tReceived expected empty.", tests.Success) + } + + // Test switch account. + { + expectedStatus := http.StatusOK + + newAccount := newMockSignup().account + rt := requestTest{ + fmt.Sprintf("Switch account %d w/role %s", expectedStatus, tr.Role), + http.MethodPatch, + fmt.Sprintf("/v1/users/switch-account/%s", newAccount.ID), + nil, + tr.Token, + tr.Claims, + expectedStatus, + nil, + } + t.Logf("\tTest: %s - %s %s", rt.name, rt.method, rt.url) + + _, err := user_account.Create(tests.Context(), auth.Claims{}, test.MasterDB, user_account.UserAccountCreateRequest{ + UserID: tr.User.ID, + AccountID: newAccount.ID, + Roles: []user_account.UserAccountRole{user_account.UserAccountRole_User}, + }, time.Now().UTC().AddDate(-1, -1, -1)) + if err != nil { + t.Fatalf("\t%s\tAdd user to account failed.", tests.Failed) + } + + w, ok := executeRequestTest(t, rt, ctx) + if !ok { + t.Fatalf("\t%s\tExecute request failed.", tests.Failed) + } + t.Logf("\t%s\tReceived valid status code of %d.", tests.Success, w.Code) + + var actual map[string]interface{} + if err := json.Unmarshal(w.Body.Bytes(), &actual); err != nil { + t.Logf("\t\tGot error : %+v", err) + t.Fatalf("\t%s\tDecode response body failed.", tests.Failed) + } + + // This is just for response format validation, will verify account from claims. + expected := map[string]interface{}{ + "access_token": actual["access_token"], + "token_type": actual["token_type"], + "expiry": actual["expiry"], + } + + if diff := cmpDiff(t, actual, expected); diff { + t.Fatalf("\t%s\tReceived expected result.", tests.Failed) + } + t.Logf("\t%s\tReceived expected result.", tests.Success) + + newClaims, err := authenticator.ParseClaims(actual["access_token"].(string)) + if err != nil { + t.Logf("\t\tGot error : %+v", err) + t.Fatalf("\t%s\tParse claims failed.", tests.Failed) + } else if newClaims.Audience != newAccount.ID { + t.Logf("\t\tGot : %+v", newClaims.Audience) + t.Logf("\t\tExpected : %+v", newAccount.ID) + t.Fatalf("\t%s\tParse claims expected audience to match new account.", tests.Failed) + } else if newClaims.Subject != tr.User.ID { + t.Logf("\t\tGot : %+v", newClaims.Subject) + t.Logf("\t\tExpected : %+v", tr.User.ID) + t.Fatalf("\t%s\tParse claims expected Subject to match user.", tests.Failed) + } + t.Logf("\t%s\tParse claims valid.", tests.Success) + } +} + +// TestUserCRUDUser tests all the user CRUD endpoints using an user with role user. +func TestUserCRUDUser(t *testing.T) { + defer tests.Recover(t) + + tr := roleTests[auth.RoleUser] + + // Add claims to the context for the user. + ctx := context.WithValue(tests.Context(), auth.Key, tr.Claims) + + // Test create. + { + expectedStatus := http.StatusForbidden + + req := mockUserCreateRequest() + rt := requestTest{ + fmt.Sprintf("Create %d w/role %s", expectedStatus, tr.Role), + http.MethodPost, + "/v1/users", + req, + tr.Token, + tr.Claims, + expectedStatus, + nil, + } + t.Logf("\tTest: %s - %s %s", rt.name, rt.method, rt.url) + + w, ok := executeRequestTest(t, rt, ctx) + if !ok { + t.Fatalf("\t%s\tExecute request failed.", tests.Failed) + } + t.Logf("\t%s\tReceived valid status code of %d.", tests.Success, w.Code) + + var actual web.ErrorResponse + if err := json.Unmarshal(w.Body.Bytes(), &actual); err != nil { + t.Logf("\t\tGot error : %+v", err) + t.Fatalf("\t%s\tDecode response body failed.", tests.Failed) + } + + expected := web.ErrorResponse{ + Error: mid.ErrForbidden.Error(), + } + + if diff := cmpDiff(t, actual, expected); diff { + t.Fatalf("\t%s\tReceived expected error.", tests.Failed) + } + t.Logf("\t%s\tReceived expected error.", tests.Success) + } + + // Since role doesn't support create, bypass auth to test other endpoints. + created := newMockUser(tr.Account.ID, user_account.UserAccountRole_User).Response(ctx) + + // Test read. + { + expectedStatus := http.StatusOK + + rt := requestTest{ + fmt.Sprintf("Read %d w/role %s", expectedStatus, tr.Role), + http.MethodGet, + fmt.Sprintf("/v1/users/%s", created.ID), + nil, + tr.Token, + tr.Claims, + expectedStatus, + nil, + } + t.Logf("\tTest: %s - %s %s", rt.name, rt.method, rt.url) + + w, ok := executeRequestTest(t, rt, ctx) + if !ok { + t.Fatalf("\t%s\tExecute request failed.", tests.Failed) + } + t.Logf("\t%s\tReceived valid status code of %d.", tests.Success, w.Code) + + var actual *user.UserResponse + if err := json.Unmarshal(w.Body.Bytes(), &actual); err != nil { + t.Logf("\t\tGot error : %+v", err) + t.Fatalf("\t%s\tDecode response body failed.", tests.Failed) + } + + if diff := cmpDiff(t, actual, created); diff { + t.Fatalf("\t%s\tReceived expected result.", tests.Failed) + } + t.Logf("\t%s\tReceived expected result.", tests.Success) + } + + // Test Read with random ID. + { + expectedStatus := http.StatusNotFound + + randID := uuid.NewRandom().String() + rt := requestTest{ + fmt.Sprintf("Read %d w/role %s using random ID", expectedStatus, tr.Role), + http.MethodGet, + fmt.Sprintf("/v1/users/%s", randID), + nil, + tr.Token, + tr.Claims, + expectedStatus, + nil, + } + t.Logf("\tTest: %s - %s %s", rt.name, rt.method, rt.url) + + w, ok := executeRequestTest(t, rt, ctx) + if !ok { + t.Fatalf("\t%s\tExecute request failed.", tests.Failed) + } + t.Logf("\t%s\tReceived valid status code of %d.", tests.Success, w.Code) + + var actual web.ErrorResponse + if err := json.Unmarshal(w.Body.Bytes(), &actual); err != nil { + t.Logf("\t\tGot error : %+v", err) + t.Fatalf("\t%s\tDecode response body failed.", tests.Failed) + } + + expected := web.ErrorResponse{ + Error: fmt.Sprintf("user %s not found: Entity not found", randID), + } + + if diff := cmpDiff(t, actual, expected); diff { + t.Fatalf("\t%s\tReceived expected error.", tests.Failed) + } + t.Logf("\t%s\tReceived expected error.", tests.Success) + } + + // Test Read with forbidden ID. + { + expectedStatus := http.StatusNotFound + + rt := requestTest{ + fmt.Sprintf("Read %d w/role %s using forbidden ID", expectedStatus, tr.Role), + http.MethodGet, + fmt.Sprintf("/v1/users/%s", tr.ForbiddenUser.ID), + nil, + tr.Token, + tr.Claims, + expectedStatus, + nil, + } + t.Logf("\tTest: %s - %s %s", rt.name, rt.method, rt.url) + + w, ok := executeRequestTest(t, rt, ctx) + if !ok { + t.Fatalf("\t%s\tExecute request failed.", tests.Failed) + } + t.Logf("\t%s\tReceived valid status code of %d.", tests.Success, w.Code) + + var actual web.ErrorResponse + if err := json.Unmarshal(w.Body.Bytes(), &actual); err != nil { + t.Logf("\t\tGot error : %+v", err) + t.Fatalf("\t%s\tDecode response body failed.", tests.Failed) + } + + expected := web.ErrorResponse{ + Error: fmt.Sprintf("user %s not found: Entity not found", tr.ForbiddenUser.ID), + } + + if diff := cmpDiff(t, actual, expected); diff { + t.Fatalf("\t%s\tReceived expected error.", tests.Failed) + } + t.Logf("\t%s\tReceived expected error.", tests.Success) + } + + // Test update. + { + expectedStatus := http.StatusForbidden + + newName := uuid.NewRandom().String() + rt := requestTest{ + fmt.Sprintf("Update %d w/role %s", expectedStatus, tr.Role), + http.MethodPatch, + "/v1/users", + user.UserUpdateRequest{ + ID: created.ID, + Name: &newName, + }, + tr.Token, + tr.Claims, + expectedStatus, + nil, + } + t.Logf("\tTest: %s - %s %s", rt.name, rt.method, rt.url) + + w, ok := executeRequestTest(t, rt, ctx) + if !ok { + t.Fatalf("\t%s\tExecute request failed.", tests.Failed) + } + t.Logf("\t%s\tReceived valid status code of %d.", tests.Success, w.Code) + + var actual web.ErrorResponse + if err := json.Unmarshal(w.Body.Bytes(), &actual); err != nil { + t.Logf("\t\tGot error : %+v", err) + t.Fatalf("\t%s\tDecode response body failed.", tests.Failed) + } + + expected := web.ErrorResponse{ + Error: user.ErrForbidden.Error(), + } + + if diff := cmpDiff(t, actual, expected); diff { + t.Fatalf("\t%s\tReceived expected error.", tests.Failed) + } + t.Logf("\t%s\tReceived expected error.", tests.Success) + } + + // Test update password. + { + expectedStatus := http.StatusForbidden + + newPass := uuid.NewRandom().String() + rt := requestTest{ + fmt.Sprintf("Update password %d w/role %s", expectedStatus, tr.Role), + http.MethodPatch, + "/v1/users/password", + user.UserUpdatePasswordRequest{ + ID: created.ID, + Password: newPass, + PasswordConfirm: newPass, + }, + tr.Token, + tr.Claims, + expectedStatus, + nil, + } + t.Logf("\tTest: %s - %s %s", rt.name, rt.method, rt.url) + + w, ok := executeRequestTest(t, rt, ctx) + if !ok { + t.Fatalf("\t%s\tExecute request failed.", tests.Failed) + } + t.Logf("\t%s\tReceived valid status code of %d.", tests.Success, w.Code) + + var actual web.ErrorResponse + if err := json.Unmarshal(w.Body.Bytes(), &actual); err != nil { + t.Logf("\t\tGot error : %+v", err) + t.Fatalf("\t%s\tDecode response body failed.", tests.Failed) + } + + expected := web.ErrorResponse{ + Error: user.ErrForbidden.Error(), + } + + if diff := cmpDiff(t, actual, expected); diff { + t.Fatalf("\t%s\tReceived expected error.", tests.Failed) + } + t.Logf("\t%s\tReceived expected error.", tests.Success) + } + + // Test archive. + { + expectedStatus := http.StatusForbidden + + rt := requestTest{ + fmt.Sprintf("Archive %d w/role %s", expectedStatus, tr.Role), + http.MethodPatch, + "/v1/users/archive", + user.UserArchiveRequest{ + ID: created.ID, + }, + tr.Token, + tr.Claims, + expectedStatus, + nil, + } + t.Logf("\tTest: %s - %s %s", rt.name, rt.method, rt.url) + + w, ok := executeRequestTest(t, rt, ctx) + if !ok { + t.Fatalf("\t%s\tExecute request failed.", tests.Failed) + } + t.Logf("\t%s\tReceived valid status code of %d.", tests.Success, w.Code) + + var actual web.ErrorResponse + if err := json.Unmarshal(w.Body.Bytes(), &actual); err != nil { + t.Logf("\t\tGot error : %+v", err) + t.Fatalf("\t%s\tDecode response body failed.", tests.Failed) + } + + expected := web.ErrorResponse{ + Error: mid.ErrForbidden.Error(), + } + + if diff := cmpDiff(t, actual, expected); diff { + t.Fatalf("\t%s\tReceived expected error.", tests.Failed) + } + t.Logf("\t%s\tReceived expected error.", tests.Success) + } + + // Test delete. + { + expectedStatus := http.StatusForbidden + + rt := requestTest{ + fmt.Sprintf("Delete %d w/role %s", expectedStatus, tr.Role), + http.MethodDelete, + fmt.Sprintf("/v1/users/%s", created.ID), + nil, + tr.Token, + tr.Claims, + expectedStatus, + nil, + } + t.Logf("\tTest: %s - %s %s", rt.name, rt.method, rt.url) + + w, ok := executeRequestTest(t, rt, ctx) + if !ok { + t.Fatalf("\t%s\tExecute request failed.", tests.Failed) + } + t.Logf("\t%s\tReceived valid status code of %d.", tests.Success, w.Code) + + var actual web.ErrorResponse + if err := json.Unmarshal(w.Body.Bytes(), &actual); err != nil { + t.Logf("\t\tGot error : %+v", err) + t.Fatalf("\t%s\tDecode response body failed.", tests.Failed) + } + + expected := web.ErrorResponse{ + Error: mid.ErrForbidden.Error(), + } + + if diff := cmpDiff(t, actual, expected); diff { + t.Fatalf("\t%s\tReceived expected error.", tests.Failed) + } + t.Logf("\t%s\tReceived expected error.", tests.Success) + } + + // Test switch account. + { + expectedStatus := http.StatusOK + + newAccount := newMockSignup().account + rt := requestTest{ + fmt.Sprintf("Switch account %d w/role %s", expectedStatus, tr.Role), + http.MethodPatch, + fmt.Sprintf("/v1/users/switch-account/%s", newAccount.ID), + nil, + tr.Token, + tr.Claims, + expectedStatus, + nil, + } + t.Logf("\tTest: %s - %s %s", rt.name, rt.method, rt.url) + + _, err := user_account.Create(tests.Context(), auth.Claims{}, test.MasterDB, user_account.UserAccountCreateRequest{ + UserID: tr.User.ID, + AccountID: newAccount.ID, + Roles: []user_account.UserAccountRole{user_account.UserAccountRole_User}, + }, time.Now().UTC().AddDate(-1, -1, -1)) + if err != nil { + t.Fatalf("\t%s\tAdd user to account failed.", tests.Failed) + } + + w, ok := executeRequestTest(t, rt, ctx) + if !ok { + t.Fatalf("\t%s\tExecute request failed.", tests.Failed) + } + t.Logf("\t%s\tReceived valid status code of %d.", tests.Success, w.Code) + + var actual map[string]interface{} + if err := json.Unmarshal(w.Body.Bytes(), &actual); err != nil { + t.Logf("\t\tGot error : %+v", err) + t.Fatalf("\t%s\tDecode response body failed.", tests.Failed) + } + + // This is just for response format validation, will verify account from claims. + expected := map[string]interface{}{ + "access_token": actual["access_token"], + "token_type": actual["token_type"], + "expiry": actual["expiry"], + } + + if diff := cmpDiff(t, actual, expected); diff { + t.Fatalf("\t%s\tReceived expected result.", tests.Failed) + } + t.Logf("\t%s\tReceived expected result.", tests.Success) + + newClaims, err := authenticator.ParseClaims(actual["access_token"].(string)) + if err != nil { + t.Logf("\t\tGot error : %+v", err) + t.Fatalf("\t%s\tParse claims failed.", tests.Failed) + } else if newClaims.Audience != newAccount.ID { + t.Logf("\t\tGot : %+v", newClaims.Audience) + t.Logf("\t\tExpected : %+v", newAccount.ID) + t.Fatalf("\t%s\tParse claims expected audience to match new account.", tests.Failed) + } else if newClaims.Subject != tr.User.ID { + t.Logf("\t\tGot : %+v", newClaims.Subject) + t.Logf("\t\tExpected : %+v", tr.User.ID) + t.Fatalf("\t%s\tParse claims expected Subject to match user.", tests.Failed) + } + t.Logf("\t%s\tParse claims valid.", tests.Success) + } +} + +// TestUserCreate validates create user endpoint. +func TestUserCreate(t *testing.T) { + defer tests.Recover(t) + + tr := roleTests[auth.RoleAdmin] + + // Add claims to the context for the user. + ctx := context.WithValue(tests.Context(), auth.Key, tr.Claims) + + // Test create with invalid data. + { expectedStatus := http.StatusBadRequest - expectedErr := web.ErrorResponse{ + + req := mockUserCreateRequest() + req.Email = "invalid email address.com" + rt := requestTest{ + fmt.Sprintf("Create %d w/role %s using invalid data", expectedStatus, tr.Role), + http.MethodPost, + "/v1/users", + req, + tr.Token, + tr.Claims, + expectedStatus, + nil, + } + t.Logf("\tTest: %s - %s %s", rt.name, rt.method, rt.url) + + w, ok := executeRequestTest(t, rt, ctx) + if !ok { + t.Fatalf("\t%s\tExecute request failed.", tests.Failed) + } + t.Logf("\t%s\tReceived valid status code of %d.", tests.Success, w.Code) + + var actual web.ErrorResponse + if err := json.Unmarshal(w.Body.Bytes(), &actual); err != nil { + t.Logf("\t\tGot error : %+v", err) + t.Fatalf("\t%s\tDecode response body failed.", tests.Failed) + } + + expected := web.ErrorResponse{ + Error: "field validation error", + Fields: []web.FieldError{ + {Field: "email", Error: "Key: 'UserCreateRequest.email' Error:Field validation for 'email' failed on the 'email' tag"}, + }, + } + + if diff := cmpDiff(t, actual, expected); diff { + t.Fatalf("\t%s\tReceived expected error.", tests.Failed) + } + t.Logf("\t%s\tReceived expected error.", tests.Success) + } +} + +// TestUserUpdate validates update user endpoint. +func TestUserUpdate(t *testing.T) { + defer tests.Recover(t) + + tr := roleTests[auth.RoleAdmin] + + // Add claims to the context for the user. + ctx := context.WithValue(tests.Context(), auth.Key, tr.Claims) + + // Test update with invalid data. + { + expectedStatus := http.StatusBadRequest + + invalidEmail := "invalid email address" + rt := requestTest{ + fmt.Sprintf("Update %d w/role %s using invalid data", expectedStatus, tr.Role), + http.MethodPatch, + "/v1/users", + user.UserUpdateRequest{ + ID: tr.User.ID, + Email: &invalidEmail, + }, + tr.Token, + tr.Claims, + expectedStatus, + nil, + } + t.Logf("\tTest: %s - %s %s", rt.name, rt.method, rt.url) + + w, ok := executeRequestTest(t, rt, ctx) + if !ok { + t.Fatalf("\t%s\tExecute request failed.", tests.Failed) + } + t.Logf("\t%s\tReceived valid status code of %d.", tests.Success, w.Code) + + var actual web.ErrorResponse + if err := json.Unmarshal(w.Body.Bytes(), &actual); err != nil { + t.Logf("\t\tGot error : %+v", err) + t.Fatalf("\t%s\tDecode response body failed.", tests.Failed) + } + + expected := web.ErrorResponse{ Error: "field validation error", Fields: []web.FieldError{ {Field: "email", Error: "Key: 'UserUpdateRequest.email' Error:Field validation for 'email' failed on the 'email' tag"}, }, } - invalidEmail := "invalid email address" - rtests = append(rtests, requestTest{ - fmt.Sprintf("Role %s %d w/invalid data", rn, expectedStatus), - http.MethodPatch, - "/v1/users", - user.UserUpdateRequest{ - ID: tr.SignupResult.User.ID, - Email: &invalidEmail, - }, - tr.Token, - tr.Claims, - expectedStatus, - expectedErr, - func(treq requestTest, body []byte) bool { - return true - }, - }) - } - - // Test update a user for with an invalid ID. - // Admin role: 403 - // User role 403 - for rn, tr := range roleTests { - - expectedStatus := http.StatusForbidden - expectedErr := web.ErrorResponse{ - Error: user.ErrForbidden.Error(), + if diff := cmpDiff(t, actual, expected); diff { + t.Fatalf("\t%s\tReceived expected error.", tests.Failed) } - - newName := rn + uuid.NewRandom().String() + strconv.Itoa(len(rtests)) - invalidID := uuid.NewRandom().String() - rtests = append(rtests, requestTest{ - fmt.Sprintf("Role %s %d w/invalid ID", rn, expectedStatus), - http.MethodPatch, - "/v1/users", - user.UserUpdateRequest{ - ID: invalidID, - Name: &newName, - }, - tr.Token, - tr.Claims, - expectedStatus, - expectedErr, - func(treq requestTest, body []byte) bool { - return true - }, - }) + t.Logf("\t%s\tReceived expected error.", tests.Success) } - - // Test update a user for with random user ID. - // Admin role: 403 - // User role 403 - forbiddenUser := mockUser() - for rn, tr := range roleTests { - - expectedStatus := http.StatusForbidden - expectedErr := web.ErrorResponse{ - Error: user.ErrForbidden.Error(), - } - - newName := rn+uuid.NewRandom().String()+strconv.Itoa(len(rtests)) - rtests = append(rtests, requestTest{ - fmt.Sprintf("Role %s %d w/random user ID", rn, expectedStatus), - http.MethodPatch, - "/v1/users", - user.UserUpdateRequest{ - ID: forbiddenUser.ID, - Name: &newName, - }, - tr.Token, - tr.Claims, - expectedStatus, - expectedErr, - func(treq requestTest, body []byte) bool { - return true - }, - }) - } - - runRequestTests(t, rtests) } -// patchUserPassword validates update user password by ID endpoint. -func patchUserPassword(t *testing.T) { +// TestUserUpdatePassword validates update user password endpoint. +func TestUserUpdatePassword(t *testing.T) { + defer tests.Recover(t) - var rtests []requestTest + tr := roleTests[auth.RoleAdmin] + + // Add claims to the context for the user. + ctx := context.WithValue(tests.Context(), auth.Key, tr.Claims) + + // Since role doesn't support create, bypass auth to test other endpoints. + created := newMockUser(tr.Account.ID, user_account.UserAccountRole_User).Response(ctx) + + // Test update user password with invalid data. + { + expectedStatus := http.StatusBadRequest - // Test update a user - // Admin role: 204 - // User role 204 - user ID matches claims so OK - for rn, tr := range roleTests { - expectedStatus := http.StatusNoContent newPass := uuid.NewRandom().String() - rtests = append(rtests, requestTest{ - fmt.Sprintf("Role %s %d", rn, expectedStatus), + rt := requestTest{ + fmt.Sprintf("Update password %d w/role %s using invalid data", expectedStatus, tr.Role), http.MethodPatch, "/v1/users/password", user.UserUpdatePasswordRequest{ - ID: tr.SignupResult.User.ID, - Password: newPass, - PasswordConfirm: newPass, + ID: created.ID, + Password: newPass, + PasswordConfirm: "different", }, tr.Token, tr.Claims, expectedStatus, nil, - func(treq requestTest, body []byte) bool { - return true - }, - }) - } + } + t.Logf("\tTest: %s - %s %s", rt.name, rt.method, rt.url) - // Test update a user password with invalid data. - // Admin role: 400 - // User role 400 - for rn, tr := range roleTests { - expectedStatus := http.StatusBadRequest - expectedErr := web.ErrorResponse{ + w, ok := executeRequestTest(t, rt, ctx) + if !ok { + t.Fatalf("\t%s\tExecute request failed.", tests.Failed) + } + t.Logf("\t%s\tReceived valid status code of %d.", tests.Success, w.Code) + + var actual web.ErrorResponse + if err := json.Unmarshal(w.Body.Bytes(), &actual); err != nil { + t.Logf("\t\tGot error : %+v", err) + t.Fatalf("\t%s\tDecode response body failed.", tests.Failed) + } + + expected := web.ErrorResponse{ Error: "field validation error", Fields: []web.FieldError{ {Field: "password_confirm", Error: "Key: 'UserUpdatePasswordRequest.password_confirm' Error:Field validation for 'password_confirm' failed on the 'eqfield' tag"}, }, } - newPass := uuid.NewRandom().String() - rtests = append(rtests, requestTest{ - fmt.Sprintf("Role %s %d w/invalid data", rn, expectedStatus), - http.MethodPatch, - "/v1/users/password", - user.UserUpdatePasswordRequest{ - ID: tr.SignupResult.User.ID, - Password: newPass, - PasswordConfirm: "different", - }, - tr.Token, - tr.Claims, - expectedStatus, - expectedErr, - func(treq requestTest, body []byte) bool { - return true - }, - }) - } - - // Test update a user password for with an invalid ID. - // Admin role: 403 - // User role 403 - for rn, tr := range roleTests { - - expectedStatus := http.StatusForbidden - expectedErr := web.ErrorResponse{ - Error: user.ErrForbidden.Error(), + if diff := cmpDiff(t, actual, expected); diff { + t.Fatalf("\t%s\tReceived expected error.", tests.Failed) } - - newPass := uuid.NewRandom().String() - invalidID := uuid.NewRandom().String() - rtests = append(rtests, requestTest{ - fmt.Sprintf("Role %s %d w/invalid ID", rn, expectedStatus), - http.MethodPatch, - "/v1/users/password", - user.UserUpdatePasswordRequest{ - ID: invalidID, - Password: newPass, - PasswordConfirm: newPass, - }, - tr.Token, - tr.Claims, - expectedStatus, - expectedErr, - func(treq requestTest, body []byte) bool { - return true - }, - }) + t.Logf("\t%s\tReceived expected error.", tests.Success) } - - // Test update a user password for with random user ID. - // Admin role: 403 - // User role 403 - forbiddenUser := mockUser() - for rn, tr := range roleTests { - - expectedStatus := http.StatusForbidden - expectedErr := web.ErrorResponse{ - Error: user.ErrForbidden.Error(), - } - - newPass := uuid.NewRandom().String() - rtests = append(rtests, requestTest{ - fmt.Sprintf("Role %s %d w/random user ID", rn, expectedStatus), - http.MethodPatch, - "/v1/users/password", - user.UserUpdatePasswordRequest{ - ID: forbiddenUser.ID, - Password: newPass, - PasswordConfirm: newPass, - }, - tr.Token, - tr.Claims, - expectedStatus, - expectedErr, - func(treq requestTest, body []byte) bool { - return true - }, - }) - } - - runRequestTests(t, rtests) } +// TestUserArchive validates archive user endpoint. +func TestUserArchive(t *testing.T) { + defer tests.Recover(t) + + tr := roleTests[auth.RoleAdmin] + + // Add claims to the context for the user. + ctx := context.WithValue(tests.Context(), auth.Key, tr.Claims) + + // Test archive user with invalid data. + { + expectedStatus := http.StatusBadRequest + + rt := requestTest{ + fmt.Sprintf("Archive %d w/role %s using invalid data", expectedStatus, tr.Role), + http.MethodPatch, + "/v1/users/archive", + user.UserArchiveRequest{ + ID: "a", + }, + tr.Token, + tr.Claims, + expectedStatus, + nil, + } + t.Logf("\tTest: %s - %s %s", rt.name, rt.method, rt.url) + + w, ok := executeRequestTest(t, rt, ctx) + if !ok { + t.Fatalf("\t%s\tExecute request failed.", tests.Failed) + } + t.Logf("\t%s\tReceived valid status code of %d.", tests.Success, w.Code) + + var actual web.ErrorResponse + if err := json.Unmarshal(w.Body.Bytes(), &actual); err != nil { + t.Logf("\t\tGot error : %+v", err) + t.Fatalf("\t%s\tDecode response body failed.", tests.Failed) + } + + expected := web.ErrorResponse{ + Error: "field validation error", + Fields: []web.FieldError{ + {Field: "id", Error: "Key: 'UserArchiveRequest.id' Error:Field validation for 'id' failed on the 'uuid' tag"}, + }, + } + + if diff := cmpDiff(t, actual, expected); diff { + t.Fatalf("\t%s\tReceived expected error.", tests.Failed) + } + t.Logf("\t%s\tReceived expected error.", tests.Success) + } + + // Test archive user with forbidden ID. + { + expectedStatus := http.StatusForbidden + + rt := requestTest{ + fmt.Sprintf("Archive %d w/role %s using forbidden ID", expectedStatus, tr.Role), + http.MethodPatch, + "/v1/users/archive", + user.UserArchiveRequest{ + ID: tr.ForbiddenUser.ID, + }, + tr.Token, + tr.Claims, + expectedStatus, + nil, + } + t.Logf("\tTest: %s - %s %s", rt.name, rt.method, rt.url) + + w, ok := executeRequestTest(t, rt, ctx) + if !ok { + t.Fatalf("\t%s\tExecute request failed.", tests.Failed) + } + t.Logf("\t%s\tReceived valid status code of %d.", tests.Success, w.Code) + + var actual web.ErrorResponse + if err := json.Unmarshal(w.Body.Bytes(), &actual); err != nil { + t.Logf("\t\tGot error : %+v", err) + t.Fatalf("\t%s\tDecode response body failed.", tests.Failed) + } + + expected := web.ErrorResponse{ + Error: user.ErrForbidden.Error(), + } + + if diff := cmpDiff(t, actual, expected); diff { + t.Fatalf("\t%s\tReceived expected error.", tests.Failed) + } + t.Logf("\t%s\tReceived expected error.", tests.Success) + } +} + +// TestUserDelete validates delete user endpoint. +func TestUserDelete(t *testing.T) { + defer tests.Recover(t) + + tr := roleTests[auth.RoleAdmin] + + // Add claims to the context for the user. + ctx := context.WithValue(tests.Context(), auth.Key, tr.Claims) + + // Test delete user with invalid data. + { + expectedStatus := http.StatusBadRequest + + rt := requestTest{ + fmt.Sprintf("Delete %d w/role %s using invalid data", expectedStatus, tr.Role), + http.MethodDelete, + "/v1/users/345345", + nil, + tr.Token, + tr.Claims, + expectedStatus, + nil, + } + t.Logf("\tTest: %s - %s %s", rt.name, rt.method, rt.url) + + w, ok := executeRequestTest(t, rt, ctx) + if !ok { + t.Fatalf("\t%s\tExecute request failed.", tests.Failed) + } + t.Logf("\t%s\tReceived valid status code of %d.", tests.Success, w.Code) + + var actual web.ErrorResponse + if err := json.Unmarshal(w.Body.Bytes(), &actual); err != nil { + t.Logf("\t\tGot error : %+v", err) + t.Fatalf("\t%s\tDecode response body failed.", tests.Failed) + } + + expected := web.ErrorResponse{ + Error: "field validation error", + Fields: []web.FieldError{ + {Field: "id", Error: "Key: 'id' Error:Field validation for 'id' failed on the 'uuid' tag"}, + }, + } + + if diff := cmpDiff(t, actual, expected); diff { + t.Fatalf("\t%s\tReceived expected error.", tests.Failed) + } + t.Logf("\t%s\tReceived expected error.", tests.Success) + } + + // Test delete user with forbidden ID. + { + expectedStatus := http.StatusForbidden + + rt := requestTest{ + fmt.Sprintf("Delete %d w/role %s using forbidden ID", expectedStatus, tr.Role), + http.MethodDelete, + fmt.Sprintf("/v1/users/%s", tr.ForbiddenUser.ID), + nil, + tr.Token, + tr.Claims, + expectedStatus, + nil, + } + t.Logf("\tTest: %s - %s %s", rt.name, rt.method, rt.url) + + w, ok := executeRequestTest(t, rt, ctx) + if !ok { + t.Fatalf("\t%s\tExecute request failed.", tests.Failed) + } + t.Logf("\t%s\tReceived valid status code of %d.", tests.Success, w.Code) + + var actual web.ErrorResponse + if err := json.Unmarshal(w.Body.Bytes(), &actual); err != nil { + t.Logf("\t\tGot error : %+v", err) + t.Fatalf("\t%s\tDecode response body failed.", tests.Failed) + } + + expected := web.ErrorResponse{ + Error: user.ErrForbidden.Error(), + } + + if diff := cmpDiff(t, actual, expected); diff { + t.Fatalf("\t%s\tReceived expected error.", tests.Failed) + } + t.Logf("\t%s\tReceived expected error.", tests.Success) + } +} + +// TestUserSwitchAccount validates user switch account endpoint. +func TestUserSwitchAccount(t *testing.T) { + defer tests.Recover(t) + + tr := roleTests[auth.RoleAdmin] + + // Add claims to the context for the user. + ctx := context.WithValue(tests.Context(), auth.Key, tr.Claims) + + // Test user switch account with invalid data. + { + expectedStatus := http.StatusBadRequest + + rt := requestTest{ + fmt.Sprintf("Switch account %d w/role %s using invalid data", expectedStatus, tr.Role), + http.MethodPatch, + "/v1/users/switch-account/sf", + nil, + tr.Token, + tr.Claims, + expectedStatus, + nil, + } + t.Logf("\tTest: %s - %s %s", rt.name, rt.method, rt.url) + + w, ok := executeRequestTest(t, rt, ctx) + if !ok { + t.Fatalf("\t%s\tExecute request failed.", tests.Failed) + } + t.Logf("\t%s\tReceived valid status code of %d.", tests.Success, w.Code) + + var actual web.ErrorResponse + if err := json.Unmarshal(w.Body.Bytes(), &actual); err != nil { + t.Logf("\t\tGot error : %+v", err) + t.Fatalf("\t%s\tDecode response body failed.", tests.Failed) + } + + expected := web.ErrorResponse{ + Error: "field validation error", + Fields: []web.FieldError{ + {Field: "account_id", Error: "Key: 'account_id' Error:Field validation for 'account_id' failed on the 'uuid' tag"}, + }, + } + + if diff := cmpDiff(t, actual, expected); diff { + t.Fatalf("\t%s\tReceived expected error.", tests.Failed) + } + t.Logf("\t%s\tReceived expected error.", tests.Success) + } + + // Test user switch account with forbidden ID. + { + expectedStatus := http.StatusUnauthorized + + rt := requestTest{ + fmt.Sprintf("Switch account %d w/role %s using forbidden ID", expectedStatus, tr.Role), + http.MethodPatch, + fmt.Sprintf("/v1/users/switch-account/%s", tr.ForbiddenAccount.ID), + nil, + tr.Token, + tr.Claims, + expectedStatus, + nil, + } + t.Logf("\tTest: %s - %s %s", rt.name, rt.method, rt.url) + + w, ok := executeRequestTest(t, rt, ctx) + if !ok { + t.Fatalf("\t%s\tExecute request failed.", tests.Failed) + } + t.Logf("\t%s\tReceived valid status code of %d.", tests.Success, w.Code) + + var actual web.ErrorResponse + if err := json.Unmarshal(w.Body.Bytes(), &actual); err != nil { + t.Logf("\t\tGot error : %+v", err) + t.Fatalf("\t%s\tDecode response body failed.", tests.Failed) + } + + expected := web.ErrorResponse{ + Error: user.ErrAuthenticationFailure.Error(), + } + + if diff := cmpDiff(t, actual, expected); diff { + t.Fatalf("\t%s\tReceived expected error.", tests.Failed) + } + t.Logf("\t%s\tReceived expected error.", tests.Success) + } +} + +// TestUserToken validates user token endpoint. +func TestUserToken(t *testing.T) { + defer tests.Recover(t) + + // Test user token with empty credentials. + { + expectedStatus := http.StatusUnauthorized + + rt := requestTest{ + fmt.Sprintf("Token %d using empty request", expectedStatus), + http.MethodPost, + "/v1/oauth/token", + nil, + user.Token{}, + auth.Claims{}, + expectedStatus, + nil, + } + t.Logf("\tTest: %s - %s %s", rt.name, rt.method, rt.url) + + w, ok := executeRequestTest(t, rt, tests.Context()) + if !ok { + t.Fatalf("\t%s\tExecute request failed.", tests.Failed) + } + t.Logf("\t%s\tReceived valid status code of %d.", tests.Success, w.Code) + + var actual web.ErrorResponse + if err := json.Unmarshal(w.Body.Bytes(), &actual); err != nil { + t.Logf("\t\tGot error : %+v", err) + t.Fatalf("\t%s\tDecode response body failed.", tests.Failed) + } + + expected := web.ErrorResponse{ + Error: "must provide email and password in Basic auth", + } + + if diff := cmpDiff(t, actual, expected); diff { + t.Fatalf("\t%s\tReceived expected error.", tests.Failed) + } + t.Logf("\t%s\tReceived expected error.", tests.Success) + } + + // Test user token with invalid email. + { + expectedStatus := http.StatusUnauthorized + + rt := requestTest{ + fmt.Sprintf("Token %d using invalid email", expectedStatus), + http.MethodPost, + "/v1/oauth/token", + nil, + user.Token{}, + auth.Claims{}, + expectedStatus, + nil, + } + t.Logf("\tTest: %s - %s %s", rt.name, rt.method, rt.url) + + r := httptest.NewRequest(rt.method, rt.url, nil) + r.SetBasicAuth("invalid email.com", "some random password") + + w := httptest.NewRecorder() + r.Header.Set("Content-Type", web.MIMEApplicationJSONCharsetUTF8) + + a.ServeHTTP(w, r) + + if w.Code != expectedStatus { + t.Logf("\t\tBody : %s\n", w.Body.String()) + t.Logf("\t\tShould receive a status code of %d for the response : %v", rt.statusCode, w.Code) + t.Fatalf("\t%s\tExecute request failed.", tests.Failed) + } + t.Logf("\t%s\tReceived valid status code of %d.", tests.Success, w.Code) + + var actual web.ErrorResponse + if err := json.Unmarshal(w.Body.Bytes(), &actual); err != nil { + t.Logf("\t\tGot error : %+v", err) + t.Fatalf("\t%s\tDecode response body failed.", tests.Failed) + } + + expected := web.ErrorResponse{ + Error: user.ErrAuthenticationFailure.Error(), + } + + if diff := cmpDiff(t, actual, expected); diff { + t.Fatalf("\t%s\tReceived expected error.", tests.Failed) + } + t.Logf("\t%s\tReceived expected error.", tests.Success) + } + + // Test user token with invalid password. + { + for _, tr := range roleTests { + expectedStatus := http.StatusUnauthorized + + rt := requestTest{ + fmt.Sprintf("Token %d w/role %s using invalid password", expectedStatus, tr.Role), + http.MethodPost, + "/v1/oauth/token", + nil, + user.Token{}, + auth.Claims{}, + expectedStatus, + nil, + } + t.Logf("\tTest: %s - %s %s", rt.name, rt.method, rt.url) + + r := httptest.NewRequest(rt.method, rt.url, nil) + r.SetBasicAuth(tr.User.Email, "invalid password") + + w := httptest.NewRecorder() + r.Header.Set("Content-Type", web.MIMEApplicationJSONCharsetUTF8) + + a.ServeHTTP(w, r) + + if w.Code != expectedStatus { + t.Logf("\t\tBody : %s\n", w.Body.String()) + t.Logf("\t\tShould receive a status code of %d for the response : %v", rt.statusCode, w.Code) + t.Fatalf("\t%s\tExecute request failed.", tests.Failed) + } + t.Logf("\t%s\tReceived valid status code of %d.", tests.Success, w.Code) + + var actual web.ErrorResponse + if err := json.Unmarshal(w.Body.Bytes(), &actual); err != nil { + t.Logf("\t\tGot error : %+v", err) + t.Fatalf("\t%s\tDecode response body failed.", tests.Failed) + } + + expected := web.ErrorResponse{ + Error: user.ErrAuthenticationFailure.Error(), + } + + if diff := cmpDiff(t, actual, expected); diff { + t.Fatalf("\t%s\tReceived expected error.", tests.Failed) + } + t.Logf("\t%s\tReceived expected error.", tests.Success) + } + } + + // Test user token with valid email and password. + { + for _, tr := range roleTests { + expectedStatus := http.StatusOK + + rt := requestTest{ + fmt.Sprintf("Token %d w/role %s using valid credentials", expectedStatus, tr.Role), + http.MethodPost, + "/v1/oauth/token", + nil, + user.Token{}, + auth.Claims{}, + expectedStatus, + nil, + } + t.Logf("\tTest: %s - %s %s", rt.name, rt.method, rt.url) + + r := httptest.NewRequest(rt.method, rt.url, nil) + r.SetBasicAuth(tr.User.Email, tr.User.password) + + w := httptest.NewRecorder() + r.Header.Set("Content-Type", web.MIMEApplicationJSONCharsetUTF8) + + a.ServeHTTP(w, r) + + if w.Code != expectedStatus { + t.Logf("\t\tBody : %s\n", w.Body.String()) + t.Logf("\t\tShould receive a status code of %d for the response : %v", rt.statusCode, w.Code) + t.Fatalf("\t%s\tExecute request failed.", tests.Failed) + } + t.Logf("\t%s\tReceived valid status code of %d.", tests.Success, w.Code) + + var actual map[string]interface{} + if err := json.Unmarshal(w.Body.Bytes(), &actual); err != nil { + t.Logf("\t\tGot error : %+v", err) + t.Fatalf("\t%s\tDecode response body failed.", tests.Failed) + } + + // This is just for response format validation, will verify account from claims. + expected := map[string]interface{}{ + "access_token": actual["access_token"], + "token_type": actual["token_type"], + "expiry": actual["expiry"], + } + + if diff := cmpDiff(t, actual, expected); diff { + t.Fatalf("\t%s\tReceived expected result.", tests.Failed) + } + t.Logf("\t%s\tReceived expected result.", tests.Success) + } + } +} diff --git a/example-project/internal/account/account.go b/example-project/internal/account/account.go index 6f8b510..7e39b2d 100644 --- a/example-project/internal/account/account.go +++ b/example-project/internal/account/account.go @@ -568,7 +568,7 @@ func Delete(ctx context.Context, claims auth.Claims, dbConn *sqlx.DB, accountID // Defines the struct to apply validation req := struct { - ID string `validate:"required,uuid"` + ID string `json:"id" validate:"required,uuid"` }{ ID: accountID, } diff --git a/example-project/internal/account/account_test.go b/example-project/internal/account/account_test.go index 103656b..d061d44 100644 --- a/example-project/internal/account/account_test.go +++ b/example-project/internal/account/account_test.go @@ -151,12 +151,12 @@ func TestCreateValidation(t *testing.T) { func(req AccountCreateRequest, res *Account) *Account { return nil }, - errors.New("Key: 'AccountCreateRequest.Name' Error:Field validation for 'Name' failed on the 'required' tag\n" + - "Key: 'AccountCreateRequest.Address1' Error:Field validation for 'Address1' failed on the 'required' tag\n" + - "Key: 'AccountCreateRequest.City' Error:Field validation for 'City' failed on the 'required' tag\n" + - "Key: 'AccountCreateRequest.Region' Error:Field validation for 'Region' failed on the 'required' tag\n" + - "Key: 'AccountCreateRequest.Country' Error:Field validation for 'Country' failed on the 'required' tag\n" + - "Key: 'AccountCreateRequest.Zipcode' Error:Field validation for 'Zipcode' failed on the 'required' tag"), + errors.New("Key: 'AccountCreateRequest.name' Error:Field validation for 'name' failed on the 'required' tag\n" + + "Key: 'AccountCreateRequest.address1' Error:Field validation for 'address1' failed on the 'required' tag\n" + + "Key: 'AccountCreateRequest.city' Error:Field validation for 'city' failed on the 'required' tag\n" + + "Key: 'AccountCreateRequest.region' Error:Field validation for 'region' failed on the 'required' tag\n" + + "Key: 'AccountCreateRequest.country' Error:Field validation for 'country' failed on the 'required' tag\n" + + "Key: 'AccountCreateRequest.zipcode' Error:Field validation for 'zipcode' failed on the 'required' tag"), }, {"Default Timezone & Status", @@ -204,7 +204,7 @@ func TestCreateValidation(t *testing.T) { func(req AccountCreateRequest, res *Account) *Account { return nil }, - errors.New("Key: 'AccountCreateRequest.Status' Error:Field validation for 'Status' failed on the 'oneof' tag"), + errors.New("Key: 'AccountCreateRequest.status' Error:Field validation for 'status' failed on the 'oneof' tag"), }, } @@ -286,7 +286,7 @@ func TestCreateValidationNameUnique(t *testing.T) { Country: "USA", Zipcode: "99686", } - expectedErr := errors.New("Key: 'AccountCreateRequest.Name' Error:Field validation for 'Name' failed on the 'unique' tag") + expectedErr := errors.New("Key: 'AccountCreateRequest.name' Error:Field validation for 'name' failed on the 'unique' tag") _, err = Create(ctx, auth.Claims{}, test.MasterDB, req2, now) if err == nil { t.Logf("\t\tWant: %+v", expectedErr) @@ -403,7 +403,7 @@ func TestUpdateValidation(t *testing.T) { var accountTests = []accountTest{ {"Required Fields", AccountUpdateRequest{}, - errors.New("Key: 'AccountUpdateRequest.ID' Error:Field validation for 'ID' failed on the 'required' tag"), + errors.New("Key: 'AccountUpdateRequest.id' Error:Field validation for 'id' failed on the 'required' tag"), }, } @@ -413,7 +413,7 @@ func TestUpdateValidation(t *testing.T) { ID: uuid.NewRandom().String(), Status: &invalidStatus, }, - errors.New("Key: 'AccountUpdateRequest.Status' Error:Field validation for 'Status' failed on the 'oneof' tag"), + errors.New("Key: 'AccountUpdateRequest.status' Error:Field validation for 'status' failed on the 'oneof' tag"), }) now := time.Date(2018, time.October, 1, 0, 0, 0, 0, time.UTC) @@ -494,7 +494,7 @@ func TestUpdateValidationNameUnique(t *testing.T) { ID: account2.ID, Name: &account1.Name, } - expectedErr := errors.New("Key: 'AccountUpdateRequest.Name' Error:Field validation for 'Name' failed on the 'unique' tag") + expectedErr := errors.New("Key: 'AccountUpdateRequest.name' Error:Field validation for 'name' failed on the 'unique' tag") err = Update(ctx, auth.Claims{}, test.MasterDB, updateReq, now) if err == nil { t.Logf("\t\tWant: %+v", expectedErr) diff --git a/example-project/internal/mid/auth.go b/example-project/internal/mid/auth.go index 538d59a..355ffa6 100644 --- a/example-project/internal/mid/auth.go +++ b/example-project/internal/mid/auth.go @@ -119,6 +119,3 @@ func parseAuthHeader(bearerStr string) (string, error) { return split[1], nil } - - - diff --git a/example-project/internal/platform/web/errors.go b/example-project/internal/platform/web/errors.go index 23ffa51..84e2a4e 100644 --- a/example-project/internal/platform/web/errors.go +++ b/example-project/internal/platform/web/errors.go @@ -31,7 +31,7 @@ type Error struct { func NewRequestError(err error, status int) error { // if its a validation error then - if verr, ok := NewValidationError(err); ok { + if verr, ok := NewValidationError(err); ok { return verr } diff --git a/example-project/internal/platform/web/request.go b/example-project/internal/platform/web/request.go index 41fb91d..99e8b55 100644 --- a/example-project/internal/platform/web/request.go +++ b/example-project/internal/platform/web/request.go @@ -83,7 +83,7 @@ func Decode(r *http.Request, val interface{}) error { } if err := validate.Struct(val); err != nil { - verr, _ := NewValidationError(err) + verr, _ := NewValidationError(err) return verr } @@ -130,7 +130,6 @@ func ExtractWhereArgs(where string) (string, []interface{}, error) { return where, vals, nil } - func RequestIsJson(r *http.Request) bool { if r == nil { return false @@ -155,4 +154,3 @@ func RequestIsJson(r *http.Request) bool { return false } - diff --git a/example-project/internal/project/project.go b/example-project/internal/project/project.go index 22603de..5484b30 100644 --- a/example-project/internal/project/project.go +++ b/example-project/internal/project/project.go @@ -199,7 +199,7 @@ func Read(ctx context.Context, claims auth.Claims, dbConn *sqlx.DB, id string, i res, err := find(ctx, claims, dbConn, query, []interface{}{}, includedArchived) if res == nil || len(res) == 0 { - err = errors.WithMessagef(ErrNotFound, "account %s not found", id) + err = errors.WithMessagef(ErrNotFound, "project %s not found", id) return nil, err } else if err != nil { return nil, err @@ -424,8 +424,10 @@ func Delete(ctx context.Context, claims auth.Claims, dbConn *sqlx.DB, id string) // Defines the struct to apply validation req := struct { - ID string `validate:"required,uuid"` - }{} + ID string `json:"id" validate:"required,uuid"` + }{ + ID: id, + } // Validate the request. v := web.NewValidator() diff --git a/example-project/internal/signup/models.go b/example-project/internal/signup/models.go index 1315425..cf78408 100644 --- a/example-project/internal/signup/models.go +++ b/example-project/internal/signup/models.go @@ -61,4 +61,3 @@ func (m *SignupResult) Response(ctx context.Context) *SignupResponse { return r } - diff --git a/example-project/internal/signup/signup.go b/example-project/internal/signup/signup.go index d9ada05..4b32e82 100644 --- a/example-project/internal/signup/signup.go +++ b/example-project/internal/signup/signup.go @@ -32,7 +32,6 @@ func Signup(ctx context.Context, claims auth.Claims, dbConn *sqlx.DB, req Signup return nil, err } - f := func(fl validator.FieldLevel) bool { if fl.Field().String() == "invalid" { return false diff --git a/example-project/internal/signup/signup_test.go b/example-project/internal/signup/signup_test.go index a975520..472d960 100644 --- a/example-project/internal/signup/signup_test.go +++ b/example-project/internal/signup/signup_test.go @@ -40,15 +40,15 @@ func TestSignupValidation(t *testing.T) { func(req SignupRequest, res *SignupResult) *SignupResult { return nil }, - errors.New("Key: 'SignupRequest.Account.Name' Error:Field validation for 'Name' failed on the 'required' tag\n" + - "Key: 'SignupRequest.Account.Address1' Error:Field validation for 'Address1' failed on the 'required' tag\n" + - "Key: 'SignupRequest.Account.City' Error:Field validation for 'City' failed on the 'required' tag\n" + - "Key: 'SignupRequest.Account.Region' Error:Field validation for 'Region' failed on the 'required' tag\n" + - "Key: 'SignupRequest.Account.Country' Error:Field validation for 'Country' failed on the 'required' tag\n" + - "Key: 'SignupRequest.Account.Zipcode' Error:Field validation for 'Zipcode' failed on the 'required' tag\n" + - "Key: 'SignupRequest.User.Name' Error:Field validation for 'Name' failed on the 'required' tag\n" + - "Key: 'SignupRequest.User.Email' Error:Field validation for 'Email' failed on the 'required' tag\n" + - "Key: 'SignupRequest.User.Password' Error:Field validation for 'Password' failed on the 'required' tag"), + errors.New("Key: 'SignupRequest.account.name' Error:Field validation for 'name' failed on the 'required' tag\n" + + "Key: 'SignupRequest.account.address1' Error:Field validation for 'address1' failed on the 'required' tag\n" + + "Key: 'SignupRequest.account.city' Error:Field validation for 'city' failed on the 'required' tag\n" + + "Key: 'SignupRequest.account.region' Error:Field validation for 'region' failed on the 'required' tag\n" + + "Key: 'SignupRequest.account.country' Error:Field validation for 'country' failed on the 'required' tag\n" + + "Key: 'SignupRequest.account.zipcode' Error:Field validation for 'zipcode' failed on the 'required' tag\n" + + "Key: 'SignupRequest.user.name' Error:Field validation for 'name' failed on the 'required' tag\n" + + "Key: 'SignupRequest.user.email' Error:Field validation for 'email' failed on the 'required' tag\n" + + "Key: 'SignupRequest.user.password' Error:Field validation for 'password' failed on the 'required' tag"), }, } diff --git a/example-project/internal/user/auth.go b/example-project/internal/user/auth.go index a280973..2cb5f9b 100644 --- a/example-project/internal/user/auth.go +++ b/example-project/internal/user/auth.go @@ -72,8 +72,8 @@ func SwitchAccount(ctx context.Context, dbConn *sqlx.DB, tknGen TokenGenerator, // Defines struct to apply validation for the supplied claims and account ID. req := struct { - UserID string `validate:"required,uuid"` - AccountID string `validate:"required,uuid"` + UserID string `json:"user_id" validate:"required,uuid"` + AccountID string `json:"account_id" validate:"required,uuid"` }{ UserID: claims.Subject, AccountID: accountID, diff --git a/example-project/internal/user/user.go b/example-project/internal/user/user.go index fae4329..a597150 100644 --- a/example-project/internal/user/user.go +++ b/example-project/internal/user/user.go @@ -90,35 +90,52 @@ func CanReadUser(ctx context.Context, claims auth.Claims, dbConn *sqlx.DB, userI // CanModifyUser determines if claims has the authority to modify the specified user ID. func CanModifyUser(ctx context.Context, claims auth.Claims, dbConn *sqlx.DB, userID string) error { - // If the request has claims from a specific account, ensure that the user - // has the correct access to the account. + // If the request has claims from a specific user, ensure that the user + // has the correct role for creating a new user. if claims.Subject != "" && claims.Subject != userID { - // When the claims Audience - AccountID - does not match the requested account, the - // claims Audience - AccountID - should have a record with an admin role. - // select id from users_accounts where account_id = [claims.Audience] and user_id = [userID] and any (roles) = 'admin' - query := sqlbuilder.NewSelectBuilder().Select("id").From(userAccountTableName) - query.Where(query.And( - query.Equal("account_id", claims.Audience), - query.Equal("user_id", userID), - "'"+auth.RoleAdmin+"' = ANY (roles)", - )) - queryStr, args := query.Build() - queryStr = dbConn.Rebind(queryStr) - - var userAccountId string - err := dbConn.QueryRowContext(ctx, queryStr, args...).Scan(&userAccountId) - if err != nil && err != sql.ErrNoRows { - err = errors.Wrapf(err, "query - %s", query.String()) + // Users with the role of admin are ony allows to create users. + if !claims.HasRole(auth.RoleAdmin) { + err := errors.WithStack(ErrForbidden) return err } - - // When there is no userAccount ID returned, then the current user does not have access - // to the specified account. - if userAccountId == "" { - return errors.WithStack(ErrForbidden) - } } + if err := CanReadUser(ctx, claims, dbConn, userID); err != nil { + return err + } + + // TODO: Review, this doesn't seem correct, replaced with above. + /* + // If the request has claims from a specific account, ensure that the user + // has the correct access to the account. + if claims.Subject != "" && claims.Subject != userID { + // When the claims Audience - AccountID - does not match the requested account, the + // claims Audience - AccountID - should have a record with an admin role. + // select id from users_accounts where account_id = [claims.Audience] and user_id = [userID] and any (roles) = 'admin' + query := sqlbuilder.NewSelectBuilder().Select("id").From(userAccountTableName) + query.Where(query.And( + query.Equal("account_id", claims.Audience), + query.Equal("user_id", userID), + "'"+auth.RoleAdmin+"' = ANY (roles)", + )) + queryStr, args := query.Build() + queryStr = dbConn.Rebind(queryStr) + + var userAccountId string + err := dbConn.QueryRowContext(ctx, queryStr, args...).Scan(&userAccountId) + if err != nil && err != sql.ErrNoRows { + err = errors.Wrapf(err, "query - %s", query.String()) + return err + } + + // When there is no userAccount ID returned, then the current user does not have access + // to the specified account. + if userAccountId == "" { + return errors.WithStack(ErrForbidden) + } + } + */ + return nil } @@ -598,7 +615,7 @@ func Delete(ctx context.Context, claims auth.Claims, dbConn *sqlx.DB, userID str // Defines the struct to apply validation req := struct { - ID string `validate:"required,uuid"` + ID string `json:"id" validate:"required,uuid"` }{ ID: userID, } diff --git a/example-project/internal/user/user_test.go b/example-project/internal/user/user_test.go index 59f7ed2..d1d4b24 100644 --- a/example-project/internal/user/user_test.go +++ b/example-project/internal/user/user_test.go @@ -1,7 +1,6 @@ package user import ( - "github.com/lib/pq" "math/rand" "os" "strings" @@ -13,6 +12,7 @@ import ( "github.com/dgrijalva/jwt-go" "github.com/google/go-cmp/cmp" "github.com/huandu/go-sqlbuilder" + "github.com/lib/pq" "github.com/pborman/uuid" "github.com/pkg/errors" ) @@ -149,9 +149,9 @@ func TestCreateValidation(t *testing.T) { func(req UserCreateRequest, res *User) *User { return nil }, - errors.New("Key: 'UserCreateRequest.Name' Error:Field validation for 'Name' failed on the 'required' tag\n" + - "Key: 'UserCreateRequest.Email' Error:Field validation for 'Email' failed on the 'required' tag\n" + - "Key: 'UserCreateRequest.Password' Error:Field validation for 'Password' failed on the 'required' tag"), + errors.New("Key: 'UserCreateRequest.name' Error:Field validation for 'name' failed on the 'required' tag\n" + + "Key: 'UserCreateRequest.email' Error:Field validation for 'email' failed on the 'required' tag\n" + + "Key: 'UserCreateRequest.password' Error:Field validation for 'password' failed on the 'required' tag"), }, {"Valid Email", UserCreateRequest{ @@ -163,7 +163,7 @@ func TestCreateValidation(t *testing.T) { func(req UserCreateRequest, res *User) *User { return nil }, - errors.New("Key: 'UserCreateRequest.Email' Error:Field validation for 'Email' failed on the 'email' tag"), + errors.New("Key: 'UserCreateRequest.email' Error:Field validation for 'email' failed on the 'email' tag"), }, {"Passwords Match", UserCreateRequest{ @@ -175,7 +175,7 @@ func TestCreateValidation(t *testing.T) { func(req UserCreateRequest, res *User) *User { return nil }, - errors.New("Key: 'UserCreateRequest.PasswordConfirm' Error:Field validation for 'PasswordConfirm' failed on the 'eqfield' tag"), + errors.New("Key: 'UserCreateRequest.password_confirm' Error:Field validation for 'password_confirm' failed on the 'eqfield' tag"), }, {"Default Timezone", UserCreateRequest{ @@ -276,7 +276,7 @@ func TestCreateValidationEmailUnique(t *testing.T) { Password: "W0rkL1fe#", PasswordConfirm: "W0rkL1fe#", } - expectedErr := errors.New("Key: 'UserCreateRequest.Email' Error:Field validation for 'Email' failed on the 'unique' tag") + expectedErr := errors.New("Key: 'UserCreateRequest.email' Error:Field validation for 'email' failed on the 'unique' tag") _, err = Create(ctx, auth.Claims{}, test.MasterDB, req2, now) if err == nil { t.Logf("\t\tWant: %+v", expectedErr) @@ -384,7 +384,7 @@ func TestUpdateValidation(t *testing.T) { var userTests = []userTest{ {"Required Fields", UserUpdateRequest{}, - errors.New("Key: 'UserUpdateRequest.ID' Error:Field validation for 'ID' failed on the 'required' tag"), + errors.New("Key: 'UserUpdateRequest.id' Error:Field validation for 'id' failed on the 'required' tag"), }, } @@ -394,7 +394,7 @@ func TestUpdateValidation(t *testing.T) { ID: uuid.NewRandom().String(), Email: &invalidEmail, }, - errors.New("Key: 'UserUpdateRequest.Email' Error:Field validation for 'Email' failed on the 'email' tag"), + errors.New("Key: 'UserUpdateRequest.email' Error:Field validation for 'email' failed on the 'email' tag"), }) now := time.Date(2018, time.October, 1, 0, 0, 0, 0, time.UTC) @@ -469,7 +469,7 @@ func TestUpdateValidationEmailUnique(t *testing.T) { ID: user2.ID, Email: &user1.Email, } - expectedErr := errors.New("Key: 'UserUpdateRequest.Email' Error:Field validation for 'Email' failed on the 'unique' tag") + expectedErr := errors.New("Key: 'UserUpdateRequest.email' Error:Field validation for 'email' failed on the 'unique' tag") err = Update(ctx, auth.Claims{}, test.MasterDB, updateReq, now) if err == nil { t.Logf("\t\tWant: %+v", expectedErr) @@ -533,8 +533,8 @@ func TestUpdatePassword(t *testing.T) { } // Ensure validation is working by trying UpdatePassword with an empty request. - expectedErr := errors.New("Key: 'UserUpdatePasswordRequest.ID' Error:Field validation for 'ID' failed on the 'required' tag\n" + - "Key: 'UserUpdatePasswordRequest.Password' Error:Field validation for 'Password' failed on the 'required' tag") + expectedErr := errors.New("Key: 'UserUpdatePasswordRequest.id' Error:Field validation for 'id' failed on the 'required' tag\n" + + "Key: 'UserUpdatePasswordRequest.password' Error:Field validation for 'password' failed on the 'required' tag") err = UpdatePassword(ctx, auth.Claims{}, test.MasterDB, UserUpdatePasswordRequest{}, now) if err == nil { t.Logf("\t\tWant: %+v", expectedErr) diff --git a/example-project/internal/user_account/models.go b/example-project/internal/user_account/models.go index 60ae5f2..ba1fd4b 100644 --- a/example-project/internal/user_account/models.go +++ b/example-project/internal/user_account/models.go @@ -53,7 +53,7 @@ func (m *UserAccount) Response(ctx context.Context) *UserAccountResponse { UserID: m.UserID, AccountID: m.AccountID, Roles: m.Roles, - Status: web.NewEnumResponse(ctx, m.Status, UserAccountRole_Values), + Status: web.NewEnumResponse(ctx, m.Status, UserAccountStatus_Values), CreatedAt: web.NewTimeResponse(ctx, m.CreatedAt), UpdatedAt: web.NewTimeResponse(ctx, m.UpdatedAt), } @@ -82,7 +82,7 @@ type UserAccountCreateRequest struct { type UserAccountUpdateRequest struct { UserID string `json:"user_id" validate:"required,uuid" example:"d69bdef7-173f-4d29-b52c-3edc60baf6a2"` AccountID string `json:"account_id" validate:"required,uuid" example:"c4653bf9-5978-48b7-89c5-95704aebb7e2"` - Roles *UserAccountRoles `json:"roles,omitempty" validate:"required,dive,oneof=admin user" enums:"admin,user" swaggertype:"array,string" example:"user"` + Roles *UserAccountRoles `json:"roles,omitempty" validate:"omitempty,dive,oneof=admin user" enums:"admin,user" swaggertype:"array,string" example:"user"` Status *UserAccountStatus `json:"status,omitempty" validate:"omitempty,oneof=active invited disabled" enums:"active,invited,disabled" swaggertype:"string" example:"disabled"` unArchive bool `json:"-"` // Internal use only. } diff --git a/example-project/internal/user_account/user_account.go b/example-project/internal/user_account/user_account.go index be91a02..f54cfe3 100644 --- a/example-project/internal/user_account/user_account.go +++ b/example-project/internal/user_account/user_account.go @@ -300,7 +300,7 @@ func Read(ctx context.Context, claims auth.Claims, dbConn *sqlx.DB, id string, i res, err := find(ctx, claims, dbConn, query, []interface{}{}, includedArchived) if res == nil || len(res) == 0 { - err = errors.WithMessagef(ErrNotFound, "account %s not found", id) + err = errors.WithMessagef(ErrNotFound, "user account %s not found", id) return nil, err } else if err != nil { return nil, err diff --git a/example-project/internal/user_account/user_account_test.go b/example-project/internal/user_account/user_account_test.go index ed7c7b1..5eb8b65 100644 --- a/example-project/internal/user_account/user_account_test.go +++ b/example-project/internal/user_account/user_account_test.go @@ -152,9 +152,9 @@ func TestCreateValidation(t *testing.T) { func(req UserAccountCreateRequest, res *UserAccount) *UserAccount { return nil }, - errors.New("Key: 'UserAccountCreateRequest.UserID' Error:Field validation for 'UserID' failed on the 'required' tag\n" + - "Key: 'UserAccountCreateRequest.AccountID' Error:Field validation for 'AccountID' failed on the 'required' tag\n" + - "Key: 'UserAccountCreateRequest.Roles' Error:Field validation for 'Roles' failed on the 'required' tag"), + errors.New("Key: 'UserAccountCreateRequest.user_id' Error:Field validation for 'user_id' failed on the 'required' tag\n" + + "Key: 'UserAccountCreateRequest.account_id' Error:Field validation for 'account_id' failed on the 'required' tag\n" + + "Key: 'UserAccountCreateRequest.roles' Error:Field validation for 'roles' failed on the 'required' tag"), }, {"Valid Role", UserAccountCreateRequest{ @@ -165,7 +165,7 @@ func TestCreateValidation(t *testing.T) { func(req UserAccountCreateRequest, res *UserAccount) *UserAccount { return nil }, - errors.New("Key: 'UserAccountCreateRequest.Roles[0]' Error:Field validation for 'Roles[0]' failed on the 'oneof' tag"), + errors.New("Key: 'UserAccountCreateRequest.roles[0]' Error:Field validation for 'roles[0]' failed on the 'oneof' tag"), }, {"Valid Status", UserAccountCreateRequest{ @@ -177,7 +177,7 @@ func TestCreateValidation(t *testing.T) { func(req UserAccountCreateRequest, res *UserAccount) *UserAccount { return nil }, - errors.New("Key: 'UserAccountCreateRequest.Status' Error:Field validation for 'Status' failed on the 'oneof' tag"), + errors.New("Key: 'UserAccountCreateRequest.status' Error:Field validation for 'status' failed on the 'oneof' tag"), }, {"Default Status", UserAccountCreateRequest{ @@ -373,9 +373,8 @@ func TestUpdateValidation(t *testing.T) { }{ {"Required Fields", UserAccountUpdateRequest{}, - errors.New("Key: 'UserAccountUpdateRequest.UserID' Error:Field validation for 'UserID' failed on the 'required' tag\n" + - "Key: 'UserAccountUpdateRequest.AccountID' Error:Field validation for 'AccountID' failed on the 'required' tag\n" + - "Key: 'UserAccountUpdateRequest.Roles' Error:Field validation for 'Roles' failed on the 'required' tag"), + errors.New("Key: 'UserAccountUpdateRequest.user_id' Error:Field validation for 'user_id' failed on the 'required' tag\n" + + "Key: 'UserAccountUpdateRequest.account_id' Error:Field validation for 'account_id' failed on the 'required' tag"), }, {"Valid Role", UserAccountUpdateRequest{ @@ -383,7 +382,7 @@ func TestUpdateValidation(t *testing.T) { AccountID: uuid.NewRandom().String(), Roles: &UserAccountRoles{invalidRole}, }, - errors.New("Key: 'UserAccountUpdateRequest.Roles[0]' Error:Field validation for 'Roles[0]' failed on the 'oneof' tag"), + errors.New("Key: 'UserAccountUpdateRequest.roles[0]' Error:Field validation for 'roles[0]' failed on the 'oneof' tag"), }, {"Valid Status", @@ -393,7 +392,7 @@ func TestUpdateValidation(t *testing.T) { Roles: &UserAccountRoles{UserAccountRole_User}, Status: &invalidStatus, }, - errors.New("Key: 'UserAccountUpdateRequest.Status' Error:Field validation for 'Status' failed on the 'oneof' tag"), + errors.New("Key: 'UserAccountUpdateRequest.status' Error:Field validation for 'status' failed on the 'oneof' tag"), }, }