1
0
mirror of https://github.com/raseels-repos/golang-saas-starter-kit.git synced 2025-06-15 00:15:15 +02:00

Complated web-api crud endpoints and unittests. unittest for find

endpoints still need to be implemented.
This commit is contained in:
Lee Brown
2019-06-27 04:48:18 -08:00
parent 48ae19bd6a
commit 24dd0dff42
27 changed files with 4062 additions and 1529 deletions

View File

@ -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 { if _, ok := errors.Cause(err).(*web.Error); !ok {
err = web.NewRequestError(err, http.StatusBadRequest) 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) err := account.Update(ctx, claims, a.MasterDB, req, v.Now)

View File

@ -22,6 +22,7 @@ type Project struct {
} }
// Find godoc // Find godoc
// TODO: Need to implement unittests on projects/find endpoint. There are none.
// @Summary List projects // @Summary List projects
// @Description Find returns the existing projects in the system. // @Description Find returns the existing projects in the system.
// @Tags project // @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 { if _, ok := errors.Cause(err).(*web.Error); !ok {
err = web.NewRequestError(err, http.StatusBadRequest) 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) 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 { if _, ok := errors.Cause(err).(*web.Error); !ok {
err = web.NewRequestError(err, http.StatusBadRequest) 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) 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 // @Success 204
// @Failure 400 {object} web.ErrorResponse // @Failure 400 {object} web.ErrorResponse
// @Failure 403 {object} web.ErrorResponse // @Failure 403 {object} web.ErrorResponse
// @Failure 404 {object} web.ErrorResponse
// @Failure 500 {object} web.ErrorResponse // @Failure 500 {object} web.ErrorResponse
// @Router /projects/archive [patch] // @Router /projects/archive [patch]
func (p *Project) Archive(ctx context.Context, w http.ResponseWriter, r *http.Request, params map[string]string) error { 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 { if _, ok := errors.Cause(err).(*web.Error); !ok {
err = web.NewRequestError(err, http.StatusBadRequest) 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) err := project.Archive(ctx, claims, p.MasterDB, req, v.Now)
if err != nil { if err != nil {
cause := errors.Cause(err) cause := errors.Cause(err)
switch cause { switch cause {
case project.ErrNotFound:
return web.RespondJsonError(ctx, w, web.NewRequestError(err, http.StatusNotFound))
case project.ErrForbidden: case project.ErrForbidden:
return web.RespondJsonError(ctx, w, web.NewRequestError(err, http.StatusForbidden)) return web.RespondJsonError(ctx, w, web.NewRequestError(err, http.StatusForbidden))
default: default:
@ -329,7 +327,6 @@ func (p *Project) Archive(ctx context.Context, w http.ResponseWriter, r *http.Re
// @Success 204 // @Success 204
// @Failure 400 {object} web.ErrorResponse // @Failure 400 {object} web.ErrorResponse
// @Failure 403 {object} web.ErrorResponse // @Failure 403 {object} web.ErrorResponse
// @Failure 404 {object} web.ErrorResponse
// @Failure 500 {object} web.ErrorResponse // @Failure 500 {object} web.ErrorResponse
// @Router /projects/{id} [delete] // @Router /projects/{id} [delete]
func (p *Project) Delete(ctx context.Context, w http.ResponseWriter, r *http.Request, params map[string]string) error { 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 { if err != nil {
cause := errors.Cause(err) cause := errors.Cause(err)
switch cause { switch cause {
case project.ErrNotFound:
return web.RespondJsonError(ctx, w, web.NewRequestError(err, http.StatusNotFound))
case project.ErrForbidden: case project.ErrForbidden:
return web.RespondJsonError(ctx, w, web.NewRequestError(err, http.StatusForbidden)) return web.RespondJsonError(ctx, w, web.NewRequestError(err, http.StatusForbidden))
default: 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"]) return errors.Wrapf(err, "Id: %s", params["id"])
} }
} }

View File

@ -42,10 +42,9 @@ func API(shutdown chan os.Signal, log *log.Logger, masterDB *sqlx.DB, redis *red
// This route is not authenticated // This route is not authenticated
app.Handle("POST", "/v1/oauth/token", u.Token) app.Handle("POST", "/v1/oauth/token", u.Token)
// Register user account management endpoints. // Register user account management endpoints.
ua := UserAccount{ ua := UserAccount{
MasterDB: masterDB, MasterDB: masterDB,
} }
app.Handle("GET", "/v1/user_accounts", ua.Find, mid.Authenticate(authenticator)) 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)) app.Handle("POST", "/v1/user_accounts", ua.Create, mid.Authenticate(authenticator), mid.HasRole(auth.RoleAdmin))

View File

@ -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 { if _, ok := errors.Cause(err).(*web.Error); !ok {
err = web.NewRequestError(err, http.StatusBadRequest) 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) res, err := signup.Signup(ctx, claims, c.MasterDB, req, v.Now)

View File

@ -27,6 +27,7 @@ type User struct {
} }
// Find godoc // Find godoc
// TODO: Need to implement unittests on users/find endpoint. There are none.
// @Summary List users // @Summary List users
// @Description Find returns the existing users in the system. // @Description Find returns the existing users in the system.
// @Tags user // @Tags user
@ -40,7 +41,6 @@ type User struct {
// @Param included-archived query boolean false "Included Archived, example: false" // @Param included-archived query boolean false "Included Archived, example: false"
// @Success 200 {array} user.UserResponse // @Success 200 {array} user.UserResponse
// @Failure 400 {object} web.ErrorResponse // @Failure 400 {object} web.ErrorResponse
// @Failure 403 {object} web.ErrorResponse
// @Failure 500 {object} web.ErrorResponse // @Failure 500 {object} web.ErrorResponse
// @Router /users [get] // @Router /users [get]
func (u *User) Find(ctx context.Context, w http.ResponseWriter, r *http.Request, params map[string]string) error { 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 { if _, ok := errors.Cause(err).(*web.Error); !ok {
err = web.NewRequestError(err, http.StatusBadRequest) 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) 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 { if _, ok := errors.Cause(err).(*web.Error); !ok {
err = web.NewRequestError(err, http.StatusBadRequest) 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) 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 { if _, ok := errors.Cause(err).(*web.Error); !ok {
err = web.NewRequestError(err, http.StatusBadRequest) 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) 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: case user.ErrNotFound:
return web.RespondJsonError(ctx, w, web.NewRequestError(err, http.StatusNotFound)) return web.RespondJsonError(ctx, w, web.NewRequestError(err, http.StatusNotFound))
case user.ErrForbidden: case user.ErrForbidden:
return web.RespondJsonError(ctx, w, web.NewRequestError(err, http.StatusForbidden)) return web.RespondJsonError(ctx, w, web.NewRequestError(err, http.StatusForbidden))
default: default:
_, ok := cause.(validator.ValidationErrors) _, ok := cause.(validator.ValidationErrors)
if ok { if ok {
@ -333,7 +333,6 @@ func (u *User) UpdatePassword(ctx context.Context, w http.ResponseWriter, r *htt
// @Success 204 // @Success 204
// @Failure 400 {object} web.ErrorResponse // @Failure 400 {object} web.ErrorResponse
// @Failure 403 {object} web.ErrorResponse // @Failure 403 {object} web.ErrorResponse
// @Failure 404 {object} web.ErrorResponse
// @Failure 500 {object} web.ErrorResponse // @Failure 500 {object} web.ErrorResponse
// @Router /users/archive [patch] // @Router /users/archive [patch]
func (u *User) Archive(ctx context.Context, w http.ResponseWriter, r *http.Request, params map[string]string) error { 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 { if _, ok := errors.Cause(err).(*web.Error); !ok {
err = web.NewRequestError(err, http.StatusBadRequest) 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) err := user.Archive(ctx, claims, u.MasterDB, req, v.Now)
if err != nil { if err != nil {
cause := errors.Cause(err) cause := errors.Cause(err)
switch cause { switch cause {
case user.ErrNotFound:
return web.RespondJsonError(ctx, w, web.NewRequestError(err, http.StatusNotFound))
case user.ErrForbidden: case user.ErrForbidden:
return web.RespondJsonError(ctx, w, web.NewRequestError(err, http.StatusForbidden)) return web.RespondJsonError(ctx, w, web.NewRequestError(err, http.StatusForbidden))
default: default:
@ -387,7 +384,6 @@ func (u *User) Archive(ctx context.Context, w http.ResponseWriter, r *http.Reque
// @Success 204 // @Success 204
// @Failure 400 {object} web.ErrorResponse // @Failure 400 {object} web.ErrorResponse
// @Failure 403 {object} web.ErrorResponse // @Failure 403 {object} web.ErrorResponse
// @Failure 404 {object} web.ErrorResponse
// @Failure 500 {object} web.ErrorResponse // @Failure 500 {object} web.ErrorResponse
// @Router /users/{id} [delete] // @Router /users/{id} [delete]
func (u *User) Delete(ctx context.Context, w http.ResponseWriter, r *http.Request, params map[string]string) error { 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 { if err != nil {
cause := errors.Cause(err) cause := errors.Cause(err)
switch cause { switch cause {
case user.ErrNotFound:
return web.RespondJsonError(ctx, w, web.NewRequestError(err, http.StatusNotFound))
case user.ErrForbidden: case user.ErrForbidden:
return web.RespondJsonError(ctx, w, web.NewRequestError(err, http.StatusForbidden)) return web.RespondJsonError(ctx, w, web.NewRequestError(err, http.StatusForbidden))
default: 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 // @Produce json
// @Security OAuth2Password // @Security OAuth2Password
// @Param account_id path int true "Account ID" // @Param account_id path int true "Account ID"
// @Success 201 // @Success 200
// @Failure 400 {object} web.ErrorResponse // @Failure 400 {object} web.ErrorResponse
// @Failure 403 {object} web.ErrorResponse // @Failure 401 {object} web.ErrorResponse
// @Failure 404 {object} web.ErrorResponse
// @Failure 500 {object} web.ErrorResponse // @Failure 500 {object} web.ErrorResponse
// @Router /users/switch-account/{account_id} [patch] // @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 { 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 // Token godoc
@ -464,10 +462,10 @@ func (u *User) SwitchAccount(ctx context.Context, w http.ResponseWriter, r *http
// @Produce json // @Produce json
// @Security BasicAuth // @Security BasicAuth
// @Param scope query string false "Scope" Enums(user, admin) // @Param scope query string false "Scope" Enums(user, admin)
// @Success 200 {object} user.Token // @Success 200
// @Header 200 {string} Token "qwerty" // @Failure 400 {object} web.ErrorResponse
// @Failure 400 {object} web.Error // @Failure 401 {object} web.ErrorResponse
// @Failure 403 {object} web.Error // @Failure 500 {object} web.ErrorResponse
// @Router /oauth/token [post] // @Router /oauth/token [post]
func (u *User) Token(ctx context.Context, w http.ResponseWriter, r *http.Request, params map[string]string) error { 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) 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: case user.ErrAuthenticationFailure:
return web.RespondJsonError(ctx, w, web.NewRequestError(err, http.StatusUnauthorized)) return web.RespondJsonError(ctx, w, web.NewRequestError(err, http.StatusUnauthorized))
default: default:
_, ok := cause.(validator.ValidationErrors)
if ok {
return web.RespondJsonError(ctx, w, web.NewRequestError(err, http.StatusBadRequest))
}
return errors.Wrap(err, "authenticating") return errors.Wrap(err, "authenticating")
} }
} }

View File

@ -15,12 +15,13 @@ import (
// UserAccount represents the UserAccount API method handler set. // UserAccount represents the UserAccount API method handler set.
type UserAccount struct { type UserAccount struct {
MasterDB *sqlx.DB MasterDB *sqlx.DB
// ADD OTHER STATE LIKE THE LOGGER AND CONFIG HERE. // ADD OTHER STATE LIKE THE LOGGER AND CONFIG HERE.
} }
// Find godoc // Find godoc
// TODO: Need to implement unittests on user_accounts/find endpoint. There are none.
// @Summary List user accounts // @Summary List user accounts
// @Description Find returns the existing user accounts in the system. // @Description Find returns the existing user accounts in the system.
// @Tags user_account // @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 { if _, ok := errors.Cause(err).(*web.Error); !ok {
err = web.NewRequestError(err, http.StatusBadRequest) 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) 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 { if _, ok := errors.Cause(err).(*web.Error); !ok {
err = web.NewRequestError(err, http.StatusBadRequest) 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) 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 // @Success 204
// @Failure 400 {object} web.ErrorResponse // @Failure 400 {object} web.ErrorResponse
// @Failure 403 {object} web.ErrorResponse // @Failure 403 {object} web.ErrorResponse
// @Failure 404 {object} web.ErrorResponse
// @Failure 500 {object} web.ErrorResponse // @Failure 500 {object} web.ErrorResponse
// @Router /user_accounts/archive [patch] // @Router /user_accounts/archive [patch]
func (u *UserAccount) Archive(ctx context.Context, w http.ResponseWriter, r *http.Request, params map[string]string) error { 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 { if _, ok := errors.Cause(err).(*web.Error); !ok {
err = web.NewRequestError(err, http.StatusBadRequest) 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) err := user_account.Archive(ctx, claims, u.MasterDB, req, v.Now)
if err != nil { if err != nil {
cause := errors.Cause(err) cause := errors.Cause(err)
switch cause { switch cause {
case user_account.ErrNotFound:
return web.RespondJsonError(ctx, w, web.NewRequestError(err, http.StatusNotFound))
case user_account.ErrForbidden: case user_account.ErrForbidden:
return web.RespondJsonError(ctx, w, web.NewRequestError(err, http.StatusForbidden)) return web.RespondJsonError(ctx, w, web.NewRequestError(err, http.StatusForbidden))
default: default:
@ -329,7 +327,6 @@ func (u *UserAccount) Archive(ctx context.Context, w http.ResponseWriter, r *htt
// @Success 204 // @Success 204
// @Failure 400 {object} web.ErrorResponse // @Failure 400 {object} web.ErrorResponse
// @Failure 403 {object} web.ErrorResponse // @Failure 403 {object} web.ErrorResponse
// @Failure 404 {object} web.ErrorResponse
// @Failure 500 {object} web.ErrorResponse // @Failure 500 {object} web.ErrorResponse
// @Router /user_accounts [delete] // @Router /user_accounts [delete]
func (u *UserAccount) Delete(ctx context.Context, w http.ResponseWriter, r *http.Request, params map[string]string) error { 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 { if _, ok := errors.Cause(err).(*web.Error); !ok {
err = web.NewRequestError(err, http.StatusBadRequest) 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) err := user_account.Delete(ctx, claims, u.MasterDB, req)
if err != nil { if err != nil {
cause := errors.Cause(err) cause := errors.Cause(err)
switch cause { switch cause {
case user_account.ErrNotFound:
return web.RespondJsonError(ctx, w, web.NewRequestError(err, http.StatusNotFound))
case user_account.ErrForbidden: case user_account.ErrForbidden:
return web.RespondJsonError(ctx, w, web.NewRequestError(err, http.StatusForbidden)) return web.RespondJsonError(ctx, w, web.NewRequestError(err, http.StatusForbidden))
default: 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 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)
} }
} }

View File

@ -5,335 +5,507 @@ import (
"encoding/json" "encoding/json"
"fmt" "fmt"
"net/http" "net/http"
"strconv"
"testing" "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/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/auth"
"geeks-accelerator/oss/saas-starter-kit/example-project/internal/platform/tests" "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/platform/web"
"github.com/google/go-cmp/cmp"
"github.com/pborman/uuid" "github.com/pborman/uuid"
) )
func mockAccount() *account.Account { // TestAccountCRUDAdmin tests all the account CRUD endpoints using an user with role admin.
req := account.AccountCreateRequest{ func TestAccountCRUDAdmin(t *testing.T) {
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) {
defer tests.Recover(t) defer tests.Recover(t)
t.Run("getAccount", getAccount) tr := roleTests[auth.RoleAdmin]
t.Run("patchAccount", patchAccount)
}
// getAccount validates get account by ID endpoint. s := newMockSignup()
func getAccount(t *testing.T) { 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() req := mockUserCreateRequest()
rt := requestTest{
// Both roles should be able to read the account. fmt.Sprintf("Create %d w/role %s", expectedStatus, tr.Role),
for rn, tr := range roleTests { http.MethodPost,
acc := tr.SignupResult.Account "/v1/accounts",
req,
// Test 200.
rtests = append(rtests, requestTest{
fmt.Sprintf("Role %s 200", rn),
http.MethodGet,
fmt.Sprintf("/v1/accounts/%s", acc.ID),
nil,
tr.Token, tr.Token,
tr.Claims, tr.Claims,
http.StatusOK, expectedStatus,
nil, nil,
func(treq requestTest, body []byte) bool { }
var actual account.AccountResponse t.Logf("\tTest: %s - %s %s", rt.name, rt.method, rt.url)
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. w, ok := executeRequestTest(t, rt, ctx)
ctx := context.WithValue(tests.Context(), auth.Key, tr.Claims) 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{}{ if len(w.Body.String()) != 0 {
"updated_at": web.NewTimeResponse(ctx, acc.UpdatedAt), if diff := cmpDiff(t, w.Body.Bytes(), nil); diff {
"id": acc.ID, t.Fatalf("\t%s\tReceived expected empty.", tests.Failed)
"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(),
} }
} }
t.Logf("\t%s\tReceived expected empty.", tests.Success)
}
newName := rn + uuid.NewRandom().String() + strconv.Itoa(len(rtests)) // Test read.
rtests = append(rtests, requestTest{ {
fmt.Sprintf("Role %s %d", rn, expectedStatus), 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, http.MethodPatch,
"/v1/accounts", "/v1/accounts",
account.AccountUpdateRequest{ account.AccountUpdateRequest{
ID: tr.SignupResult.Account.ID, ID: tr.Account.ID,
Name: &newName, Name: &newName,
}, },
tr.Token, tr.Token,
tr.Claims, tr.Claims,
expectedStatus, expectedStatus,
expectedErr, nil,
func(treq requestTest, body []byte) bool { }
return true t.Logf("\tTest: %s - %s %s", rt.name, rt.method, rt.url)
},
})
}
// Test update an account with invalid data. w, ok := executeRequestTest(t, rt, ctx)
// Admin role: 400 if !ok {
// User role 400 t.Fatalf("\t%s\tExecute request failed.", tests.Failed)
for rn, tr := range roleTests { }
var expectedStatus int t.Logf("\t%s\tReceived valid status code of %d.", tests.Success, w.Code)
var expectedErr interface{}
if rn == auth.RoleAdmin { if len(w.Body.String()) != 0 {
expectedStatus = http.StatusBadRequest if diff := cmpDiff(t, w.Body.Bytes(), nil); diff {
expectedErr = web.ErrorResponse{ t.Fatalf("\t%s\tReceived expected empty.", tests.Failed)
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(),
} }
} }
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") invalidStatus := account.AccountStatus("invalid status")
rtests = append(rtests, requestTest{ rt := requestTest{
fmt.Sprintf("Role %s %d w/invalid data", rn, expectedStatus), fmt.Sprintf("Update %d w/role %s using invalid data", expectedStatus, tr.Role),
http.MethodPatch, http.MethodPatch,
"/v1/accounts", "/v1/accounts",
account.AccountUpdateRequest{ account.AccountUpdateRequest{
ID: tr.SignupResult.User.ID, ID: tr.Account.ID,
Status: &invalidStatus, Status: &invalidStatus,
}, },
tr.Token, tr.Token,
tr.Claims, tr.Claims,
expectedStatus, expectedStatus,
expectedErr, nil,
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(),
}
} }
newName := rn + uuid.NewRandom().String() + strconv.Itoa(len(rtests)) t.Logf("\tTest: %s - %s %s", rt.name, rt.method, rt.url)
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
},
})
}
// Test update an account for with random account ID. w, ok := executeRequestTest(t, rt, ctx)
// Admin role: 403 if !ok {
// User role 403 t.Fatalf("\t%s\tExecute request failed.", tests.Failed)
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(),
}
} }
newName := rn+uuid.NewRandom().String()+strconv.Itoa(len(rtests)) t.Logf("\t%s\tReceived valid status code of %d.", tests.Success, w.Code)
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
},
})
}
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)
}
} }

File diff suppressed because it is too large Load Diff

View File

@ -1,19 +1,30 @@
package tests package tests
import ( import (
"context"
"encoding/json" "encoding/json"
"fmt"
"net/http" "net/http"
"testing" "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/auth"
"geeks-accelerator/oss/saas-starter-kit/example-project/internal/platform/tests" "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/platform/web"
"geeks-accelerator/oss/saas-starter-kit/example-project/internal/signup" "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"
"github.com/google/go-cmp/cmp"
"github.com/pborman/uuid" "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 { func mockSignupRequest() signup.SignupRequest {
return signup.SignupRequest{ return signup.SignupRequest{
Account: signup.SignupAccount{ 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) { func TestSignup(t *testing.T) {
defer tests.Recover(t) defer tests.Recover(t)
t.Run("postSigup", postSigup) ctx := tests.Context()
}
// postSigup validates the signup endpoint. // Test signup.
func postSigup(t *testing.T) { {
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. w, ok := executeRequestTest(t, rt, ctx)
// Signup does not require auth, so empty token and claims should result in success. if !ok {
req1 := mockSignupRequest() t.Fatalf("\t%s\tExecute request failed.", tests.Failed)
rtests = append(rtests, requestTest{ }
"No Authorization Valid", t.Logf("\t%s\tReceived valid status code of %d.", tests.Success, w.Code)
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
}
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{}{
expectedMap := map[string]interface{}{ "id": actual.User.ID,
"user": map[string]interface{}{ "name": req.User.Name,
"id": actual.User.ID, "email": req.User.Email,
"name": req.User.Name, "timezone": actual.User.Timezone,
"email": req.User.Email, "created_at": web.NewTimeResponse(ctx, actual.User.CreatedAt.Value),
"timezone": actual.User.Timezone, "updated_at": web.NewTimeResponse(ctx, actual.User.UpdatedAt.Value),
"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),
"account": map[string]interface{}{ "id": actual.Account.ID,
"updated_at": web.NewTimeResponse(ctx, actual.Account.UpdatedAt.Value), "address2": req.Account.Address2,
"id": actual.Account.ID, "region": req.Account.Region,
"address2": req.Account.Address2, "zipcode": req.Account.Zipcode,
"region": req.Account.Region, "timezone": actual.Account.Timezone,
"zipcode": req.Account.Zipcode, "created_at": web.NewTimeResponse(ctx, actual.Account.CreatedAt.Value),
"timezone": actual.Account.Timezone, "country": req.Account.Country,
"created_at": web.NewTimeResponse(ctx, actual.Account.CreatedAt.Value), "billing_user_id": &actual.Account.BillingUserID,
"country": req.Account.Country, "name": req.Account.Name,
"billing_user_id": &actual.Account.BillingUserID, "address1": req.Account.Address1,
"name": req.Account.Name, "city": req.Account.City,
"address1": req.Account.Address1, "status": map[string]interface{}{
"city": req.Account.City, "value": "active",
"status": map[string]interface{}{ "title": "Active",
"value": "active", "options": []map[string]interface{}{{"selected": false, "title": "[Active Pending Disabled]", "value": "[active pending disabled]"}},
"title": "Active",
"options": []map[string]interface{}{{"selected":false,"title":"[Active Pending Disabled]","value":"[active pending disabled]"}},
},
"signup_user_id": &actual.Account.SignupUserID,
}, },
"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) t.Fatalf("\t%s\tReceived expected result.", tests.Failed)
if err != nil { }
t.Logf("\t\tGot error : %+v", err) t.Logf("\t%s\tReceived expected result.", tests.Success)
return false }
}
var expected signup.SignupResponse // Test signup w/empty request.
if err := json.Unmarshal([]byte(expectedJson), &expected); err != nil { {
t.Logf("\t\tGot error : %+v", err) expectedStatus := http.StatusBadRequest
printResultMap(ctx, body)
return false
}
if diff := cmp.Diff(actual, expected); diff != "" { rt := requestTest{
actualJSON, err := json.MarshalIndent(actual, "", " ") fmt.Sprintf("Signup %d w/empty request", expectedStatus),
if err != nil { http.MethodPost,
t.Logf("\t\tGot error : %+v", err) "/v1/signup",
return false nil,
} user.Token{},
t.Logf("\t\tGot : %s\n", actualJSON) auth.Claims{},
expectedStatus,
nil,
}
t.Logf("\tTest: %s - %s %s", rt.name, rt.method, rt.url)
expectedJSON, err := json.MarshalIndent(expected, "", " ") w, ok := executeRequestTest(t, rt, ctx)
if err != nil { if !ok {
t.Logf("\t\tGot error : %+v", err) t.Fatalf("\t%s\tExecute request failed.", tests.Failed)
return false }
} t.Logf("\t%s\tReceived valid status code of %d.", tests.Success, w.Code)
t.Logf("\t\tExpected : %s\n", expectedJSON)
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 { expected := web.ErrorResponse{
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{
Error: "decode request body failed: EOF", Error: "decode request body failed: EOF",
}, }
func(req requestTest, body []byte) bool {
return true
},
})
// Test 404 w/validation errors. if diff := cmpDiff(t, actual, expected); diff {
invalidReq := mockSignupRequest() t.Fatalf("\t%s\tReceived expected error.", tests.Failed)
invalidReq.User.Email = "" }
invalidReq.Account.Name = "" t.Logf("\t%s\tReceived expected error.", tests.Success)
rtests = append(rtests, requestTest{ }
"Invalid request",
http.MethodPost, // Test signup w/validation errors.
"/v1/signup", {
invalidReq, expectedStatus := http.StatusBadRequest
user.Token{},
auth.Claims{}, req := mockSignupRequest()
http.StatusBadRequest, req.User.Email = ""
web.ErrorResponse{ 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", Error: "field validation error",
Fields: []web.FieldError{ Fields: []web.FieldError{
{Field: "name", Error: "Key: 'SignupRequest.account.name' Error:Field validation for 'name' failed on the 'required' tag"}, {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"}, {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)
}
} }

View File

@ -5,9 +5,8 @@ import (
"context" "context"
"encoding/json" "encoding/json"
"fmt" "fmt"
"geeks-accelerator/oss/saas-starter-kit/example-project/internal/platform/web"
"github.com/google/go-cmp/cmp"
"io" "io"
"io/ioutil"
"net/http" "net/http"
"net/http/httptest" "net/http/httptest"
"os" "os"
@ -15,40 +14,44 @@ import (
"testing" "testing"
"time" "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/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/auth"
"geeks-accelerator/oss/saas-starter-kit/example-project/internal/platform/tests" "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"
"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 a http.Handler
var test *tests.Test var test *tests.Test
var authenticator *auth.Authenticator
// Information about the users we have created for testing. // Information about the users we have created for testing.
type roleTest struct { type roleTest struct {
Token user.Token Role string
Claims auth.Claims Token user.Token
SignupRequest *signup.SignupRequest Claims auth.Claims
SignupResult *signup.SignupResult User mockUser
User *user.User Account *account.Account
Account *account.Account ForbiddenUser mockUser
ForbiddenAccount *account.Account
} }
type requestTest struct { type requestTest struct {
name string name string
method string method string
url string url string
request interface{} request interface{}
token user.Token token user.Token
claims auth.Claims claims auth.Claims
statusCode int statusCode int
error interface{} error interface{}
expected func(req requestTest, result []byte) bool
} }
var roleTests map[string]roleTest 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) 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 { if err != nil {
panic(err) panic(err)
} }
shutdown := make(chan os.Signal, 1) 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 // Create a new account directly business logic. This creates an
// initial account and user that we will use for admin validated endpoints. // initial account and user that we will use for admin validated endpoints.
signupReq := signup.SignupRequest{ signupReq1 := mockSignupRequest()
Account: signup.SignupAccount{ signup1, err := signup.Signup(tests.Context(), auth.Claims{}, test.MasterDB, signupReq1, now)
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)
if err != nil { if err != nil {
panic(err) panic(err)
} }
expires := time.Now().UTC().Sub(signup.User.CreatedAt) + time.Hour expires := time.Now().UTC().Sub(signup1.User.CreatedAt) + time.Hour
adminTkn, err := user.Authenticate(tests.Context(), test.MasterDB, authenticator, signupReq.User.Email, signupReq.User.Password, expires, now) adminTkn, err := user.Authenticate(tests.Context(), test.MasterDB, authenticator, signupReq1.User.Email, signupReq1.User.Password, expires, now)
if err != nil { if err != nil {
panic(err) panic(err)
} }
@ -111,13 +102,22 @@ func testMain(m *testing.M) int {
panic(err) 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{ roleTests[auth.RoleAdmin] = roleTest{
Token: adminTkn, Role: auth.RoleAdmin,
Claims: adminClaims, Token: adminTkn,
SignupRequest: &signupReq, Claims: adminClaims,
SignupResult: signup, User: mockUser{signup1.User, signupReq1.User.Password},
User: signup.User, Account: signup1.Account,
Account: signup.Account, ForbiddenUser: mockUser{signup2.User, signupReq2.User.Password},
ForbiddenAccount: signup2.Account,
} }
// Create a regular user to use when calling regular validated endpoints. // 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{ _, err = user_account.Create(tests.Context(), adminClaims, test.MasterDB, user_account.UserAccountCreateRequest{
UserID: usr.ID, UserID: usr.ID,
AccountID: signup.Account.ID, AccountID: signup1.Account.ID,
Roles: []user_account.UserAccountRole{user_account.UserAccountRole_User}, Roles: []user_account.UserAccountRole{user_account.UserAccountRole_User},
// Status: use default value // Status: use default value
}, now) }, now)
if err != nil { if err != nil {
@ -152,83 +152,114 @@ func testMain(m *testing.M) int {
panic(err) panic(err)
} }
// Second test will be for role User
roleTests[auth.RoleUser] = roleTest{ roleTests[auth.RoleUser] = roleTest{
Token: userTkn, Role: auth.RoleUser,
Claims: userClaims, Token: userTkn,
SignupRequest: &signupReq, Claims: userClaims,
SignupResult: signup, Account: signup1.Account,
Account: signup.Account, User: mockUser{usr, userReq.Password},
User: usr, ForbiddenUser: mockUser{signup2.User, signupReq2.User.Password},
ForbiddenAccount: signup2.Account,
} }
return m.Run() return m.Run()
} }
// executeRequestTest provides request execution and basic response validation
// runRequestTests helper function for testing endpoints. func executeRequestTest(t *testing.T, tt requestTest, ctx context.Context) (*httptest.ResponseRecorder, bool) {
func runRequestTests(t *testing.T, rtests []requestTest ) { var req []byte
var rr io.Reader
for i, tt := range rtests { if tt.request != nil {
t.Logf("\tTest: %d\tWhen running test: %s", i, tt.name) var ok bool
{ req, ok = tt.request.([]byte)
var req []byte if !ok {
var rr io.Reader var err error
if tt.request != nil { req, err = json.Marshal(tt.request)
var ok bool if err != nil {
req, ok = tt.request.([]byte) t.Logf("\t\tGot err : %+v", err)
if !ok { t.Logf("\t\tEncode request failed.")
var err error return nil, false
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)
} }
}
rr = bytes.NewReader(req)
}
r := httptest.NewRequest(tt.method, tt.url , rr) r := httptest.NewRequest(tt.method, tt.url, rr).WithContext(ctx)
w := httptest.NewRecorder()
r.Header.Set("Content-Type", web.MIMEApplicationJSONCharsetUTF8) w := httptest.NewRecorder()
if tt.token.AccessToken != "" {
r.Header.Set("Authorization", tt.token.AuthorizationHeader())
}
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 { a.ServeHTTP(w, r)
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)
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
}
if diff := cmp.Diff(actual, tt.error); diff != "" {
var actual web.ErrorResponse t.Logf("\t\tDiff : %s\n", diff)
if err := json.Unmarshal(w.Body.Bytes(), &actual); err != nil { t.Logf("\t\tShould get the expected error.")
t.Logf("\t\tBody : %s\n", w.Body.String()) return w, false
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)
} }
} }
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) { func printResultMap(ctx context.Context, result []byte) {
var m map[string]interface{} 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) fmt.Printf("%s\"%s\": []map[string]interface{}{\n", strings.Repeat("\t", depth), k)
for _, smv := range sm { for _, smv := range sm {
printResultMapKeys(ctx, smv, depth +1) printResultMapKeys(ctx, smv, depth+1)
} }
fmt.Printf("%s},\n", strings.Repeat("\t", depth)) fmt.Printf("%s},\n", strings.Repeat("\t", depth))
} else if sm, ok := kv.(map[string]interface{}); ok { } else if sm, ok := kv.(map[string]interface{}); ok {
fmt.Printf("%s\"%s\": map[string]interface{}{\n", strings.Repeat("\t", depth), k) 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)) fmt.Printf("%s},\n", strings.Repeat("\t", depth))
} else { } else {
var pv string var pv string
@ -282,4 +313,3 @@ func printResultMapKeys(ctx context.Context, m map[string]interface{}, depth int
} }
} }
} }

View File

@ -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)
}
}

File diff suppressed because it is too large Load Diff

View File

@ -568,7 +568,7 @@ func Delete(ctx context.Context, claims auth.Claims, dbConn *sqlx.DB, accountID
// Defines the struct to apply validation // Defines the struct to apply validation
req := struct { req := struct {
ID string `validate:"required,uuid"` ID string `json:"id" validate:"required,uuid"`
}{ }{
ID: accountID, ID: accountID,
} }

View File

@ -151,12 +151,12 @@ func TestCreateValidation(t *testing.T) {
func(req AccountCreateRequest, res *Account) *Account { func(req AccountCreateRequest, res *Account) *Account {
return nil return nil
}, },
errors.New("Key: 'AccountCreateRequest.Name' Error:Field validation for 'Name' failed on the 'required' tag\n" + 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.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.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.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.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"), "Key: 'AccountCreateRequest.zipcode' Error:Field validation for 'zipcode' failed on the 'required' tag"),
}, },
{"Default Timezone & Status", {"Default Timezone & Status",
@ -204,7 +204,7 @@ func TestCreateValidation(t *testing.T) {
func(req AccountCreateRequest, res *Account) *Account { func(req AccountCreateRequest, res *Account) *Account {
return nil 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", Country: "USA",
Zipcode: "99686", 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) _, err = Create(ctx, auth.Claims{}, test.MasterDB, req2, now)
if err == nil { if err == nil {
t.Logf("\t\tWant: %+v", expectedErr) t.Logf("\t\tWant: %+v", expectedErr)
@ -403,7 +403,7 @@ func TestUpdateValidation(t *testing.T) {
var accountTests = []accountTest{ var accountTests = []accountTest{
{"Required Fields", {"Required Fields",
AccountUpdateRequest{}, 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(), ID: uuid.NewRandom().String(),
Status: &invalidStatus, 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) 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, ID: account2.ID,
Name: &account1.Name, 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) err = Update(ctx, auth.Claims{}, test.MasterDB, updateReq, now)
if err == nil { if err == nil {
t.Logf("\t\tWant: %+v", expectedErr) t.Logf("\t\tWant: %+v", expectedErr)

View File

@ -119,6 +119,3 @@ func parseAuthHeader(bearerStr string) (string, error) {
return split[1], nil return split[1], nil
} }

View File

@ -31,7 +31,7 @@ type Error struct {
func NewRequestError(err error, status int) error { func NewRequestError(err error, status int) error {
// if its a validation error then // if its a validation error then
if verr, ok := NewValidationError(err); ok { if verr, ok := NewValidationError(err); ok {
return verr return verr
} }

View File

@ -83,7 +83,7 @@ func Decode(r *http.Request, val interface{}) error {
} }
if err := validate.Struct(val); err != nil { if err := validate.Struct(val); err != nil {
verr, _ := NewValidationError(err) verr, _ := NewValidationError(err)
return verr return verr
} }
@ -130,7 +130,6 @@ func ExtractWhereArgs(where string) (string, []interface{}, error) {
return where, vals, nil return where, vals, nil
} }
func RequestIsJson(r *http.Request) bool { func RequestIsJson(r *http.Request) bool {
if r == nil { if r == nil {
return false return false
@ -155,4 +154,3 @@ func RequestIsJson(r *http.Request) bool {
return false return false
} }

View File

@ -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) res, err := find(ctx, claims, dbConn, query, []interface{}{}, includedArchived)
if res == nil || len(res) == 0 { 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 return nil, err
} else if err != nil { } else if err != nil {
return nil, err 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 // Defines the struct to apply validation
req := struct { req := struct {
ID string `validate:"required,uuid"` ID string `json:"id" validate:"required,uuid"`
}{} }{
ID: id,
}
// Validate the request. // Validate the request.
v := web.NewValidator() v := web.NewValidator()

View File

@ -61,4 +61,3 @@ func (m *SignupResult) Response(ctx context.Context) *SignupResponse {
return r return r
} }

View File

@ -32,7 +32,6 @@ func Signup(ctx context.Context, claims auth.Claims, dbConn *sqlx.DB, req Signup
return nil, err return nil, err
} }
f := func(fl validator.FieldLevel) bool { f := func(fl validator.FieldLevel) bool {
if fl.Field().String() == "invalid" { if fl.Field().String() == "invalid" {
return false return false

View File

@ -40,15 +40,15 @@ func TestSignupValidation(t *testing.T) {
func(req SignupRequest, res *SignupResult) *SignupResult { func(req SignupRequest, res *SignupResult) *SignupResult {
return nil return nil
}, },
errors.New("Key: 'SignupRequest.Account.Name' Error:Field validation for 'Name' failed on the 'required' tag\n" + 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.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.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.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.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.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.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.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"), "Key: 'SignupRequest.user.password' Error:Field validation for 'password' failed on the 'required' tag"),
}, },
} }

View File

@ -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. // Defines struct to apply validation for the supplied claims and account ID.
req := struct { req := struct {
UserID string `validate:"required,uuid"` UserID string `json:"user_id" validate:"required,uuid"`
AccountID string `validate:"required,uuid"` AccountID string `json:"account_id" validate:"required,uuid"`
}{ }{
UserID: claims.Subject, UserID: claims.Subject,
AccountID: accountID, AccountID: accountID,

View File

@ -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. // 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 { 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 // If the request has claims from a specific user, ensure that the user
// has the correct access to the account. // has the correct role for creating a new user.
if claims.Subject != "" && claims.Subject != userID { if claims.Subject != "" && claims.Subject != userID {
// When the claims Audience - AccountID - does not match the requested account, the // Users with the role of admin are ony allows to create users.
// claims Audience - AccountID - should have a record with an admin role. if !claims.HasRole(auth.RoleAdmin) {
// select id from users_accounts where account_id = [claims.Audience] and user_id = [userID] and any (roles) = 'admin' err := errors.WithStack(ErrForbidden)
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 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 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 // Defines the struct to apply validation
req := struct { req := struct {
ID string `validate:"required,uuid"` ID string `json:"id" validate:"required,uuid"`
}{ }{
ID: userID, ID: userID,
} }

View File

@ -1,7 +1,6 @@
package user package user
import ( import (
"github.com/lib/pq"
"math/rand" "math/rand"
"os" "os"
"strings" "strings"
@ -13,6 +12,7 @@ import (
"github.com/dgrijalva/jwt-go" "github.com/dgrijalva/jwt-go"
"github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp"
"github.com/huandu/go-sqlbuilder" "github.com/huandu/go-sqlbuilder"
"github.com/lib/pq"
"github.com/pborman/uuid" "github.com/pborman/uuid"
"github.com/pkg/errors" "github.com/pkg/errors"
) )
@ -149,9 +149,9 @@ func TestCreateValidation(t *testing.T) {
func(req UserCreateRequest, res *User) *User { func(req UserCreateRequest, res *User) *User {
return nil return nil
}, },
errors.New("Key: 'UserCreateRequest.Name' Error:Field validation for 'Name' failed on the 'required' tag\n" + 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.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"), "Key: 'UserCreateRequest.password' Error:Field validation for 'password' failed on the 'required' tag"),
}, },
{"Valid Email", {"Valid Email",
UserCreateRequest{ UserCreateRequest{
@ -163,7 +163,7 @@ func TestCreateValidation(t *testing.T) {
func(req UserCreateRequest, res *User) *User { func(req UserCreateRequest, res *User) *User {
return nil 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", {"Passwords Match",
UserCreateRequest{ UserCreateRequest{
@ -175,7 +175,7 @@ func TestCreateValidation(t *testing.T) {
func(req UserCreateRequest, res *User) *User { func(req UserCreateRequest, res *User) *User {
return nil 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", {"Default Timezone",
UserCreateRequest{ UserCreateRequest{
@ -276,7 +276,7 @@ func TestCreateValidationEmailUnique(t *testing.T) {
Password: "W0rkL1fe#", Password: "W0rkL1fe#",
PasswordConfirm: "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) _, err = Create(ctx, auth.Claims{}, test.MasterDB, req2, now)
if err == nil { if err == nil {
t.Logf("\t\tWant: %+v", expectedErr) t.Logf("\t\tWant: %+v", expectedErr)
@ -384,7 +384,7 @@ func TestUpdateValidation(t *testing.T) {
var userTests = []userTest{ var userTests = []userTest{
{"Required Fields", {"Required Fields",
UserUpdateRequest{}, 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(), ID: uuid.NewRandom().String(),
Email: &invalidEmail, 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) 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, ID: user2.ID,
Email: &user1.Email, 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) err = Update(ctx, auth.Claims{}, test.MasterDB, updateReq, now)
if err == nil { if err == nil {
t.Logf("\t\tWant: %+v", expectedErr) 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. // 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" + 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") "Key: 'UserUpdatePasswordRequest.password' Error:Field validation for 'password' failed on the 'required' tag")
err = UpdatePassword(ctx, auth.Claims{}, test.MasterDB, UserUpdatePasswordRequest{}, now) err = UpdatePassword(ctx, auth.Claims{}, test.MasterDB, UserUpdatePasswordRequest{}, now)
if err == nil { if err == nil {
t.Logf("\t\tWant: %+v", expectedErr) t.Logf("\t\tWant: %+v", expectedErr)

View File

@ -53,7 +53,7 @@ func (m *UserAccount) Response(ctx context.Context) *UserAccountResponse {
UserID: m.UserID, UserID: m.UserID,
AccountID: m.AccountID, AccountID: m.AccountID,
Roles: m.Roles, 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), CreatedAt: web.NewTimeResponse(ctx, m.CreatedAt),
UpdatedAt: web.NewTimeResponse(ctx, m.UpdatedAt), UpdatedAt: web.NewTimeResponse(ctx, m.UpdatedAt),
} }
@ -82,7 +82,7 @@ type UserAccountCreateRequest struct {
type UserAccountUpdateRequest struct { type UserAccountUpdateRequest struct {
UserID string `json:"user_id" validate:"required,uuid" example:"d69bdef7-173f-4d29-b52c-3edc60baf6a2"` 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"` 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"` 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. unArchive bool `json:"-"` // Internal use only.
} }

View File

@ -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) res, err := find(ctx, claims, dbConn, query, []interface{}{}, includedArchived)
if res == nil || len(res) == 0 { 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 return nil, err
} else if err != nil { } else if err != nil {
return nil, err return nil, err

View File

@ -152,9 +152,9 @@ func TestCreateValidation(t *testing.T) {
func(req UserAccountCreateRequest, res *UserAccount) *UserAccount { func(req UserAccountCreateRequest, res *UserAccount) *UserAccount {
return nil return nil
}, },
errors.New("Key: 'UserAccountCreateRequest.UserID' Error:Field validation for 'UserID' failed on the 'required' tag\n" + errors.New("Key: 'UserAccountCreateRequest.user_id' Error:Field validation for 'user_id' failed on the 'required' tag\n" +
"Key: 'UserAccountCreateRequest.AccountID' Error:Field validation for 'AccountID' 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"), "Key: 'UserAccountCreateRequest.roles' Error:Field validation for 'roles' failed on the 'required' tag"),
}, },
{"Valid Role", {"Valid Role",
UserAccountCreateRequest{ UserAccountCreateRequest{
@ -165,7 +165,7 @@ func TestCreateValidation(t *testing.T) {
func(req UserAccountCreateRequest, res *UserAccount) *UserAccount { func(req UserAccountCreateRequest, res *UserAccount) *UserAccount {
return nil 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", {"Valid Status",
UserAccountCreateRequest{ UserAccountCreateRequest{
@ -177,7 +177,7 @@ func TestCreateValidation(t *testing.T) {
func(req UserAccountCreateRequest, res *UserAccount) *UserAccount { func(req UserAccountCreateRequest, res *UserAccount) *UserAccount {
return nil 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", {"Default Status",
UserAccountCreateRequest{ UserAccountCreateRequest{
@ -373,9 +373,8 @@ func TestUpdateValidation(t *testing.T) {
}{ }{
{"Required Fields", {"Required Fields",
UserAccountUpdateRequest{}, UserAccountUpdateRequest{},
errors.New("Key: 'UserAccountUpdateRequest.UserID' Error:Field validation for 'UserID' failed on the 'required' tag\n" + errors.New("Key: 'UserAccountUpdateRequest.user_id' Error:Field validation for 'user_id' failed on the 'required' tag\n" +
"Key: 'UserAccountUpdateRequest.AccountID' Error:Field validation for 'AccountID' failed on the 'required' tag\n" + "Key: 'UserAccountUpdateRequest.account_id' Error:Field validation for 'account_id' failed on the 'required' tag"),
"Key: 'UserAccountUpdateRequest.Roles' Error:Field validation for 'Roles' failed on the 'required' tag"),
}, },
{"Valid Role", {"Valid Role",
UserAccountUpdateRequest{ UserAccountUpdateRequest{
@ -383,7 +382,7 @@ func TestUpdateValidation(t *testing.T) {
AccountID: uuid.NewRandom().String(), AccountID: uuid.NewRandom().String(),
Roles: &UserAccountRoles{invalidRole}, 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", {"Valid Status",
@ -393,7 +392,7 @@ func TestUpdateValidation(t *testing.T) {
Roles: &UserAccountRoles{UserAccountRole_User}, Roles: &UserAccountRoles{UserAccountRole_User},
Status: &invalidStatus, 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"),
}, },
} }