package tests import ( "context" "encoding/json" "fmt" "geeks-accelerator/oss/saas-starter-kit/example-project/internal/mid" "net/http" "strconv" "testing" "time" "geeks-accelerator/oss/saas-starter-kit/example-project/internal/user" "geeks-accelerator/oss/saas-starter-kit/example-project/internal/platform/auth" "geeks-accelerator/oss/saas-starter-kit/example-project/internal/platform/tests" "geeks-accelerator/oss/saas-starter-kit/example-project/internal/platform/web" "github.com/google/go-cmp/cmp" "github.com/pborman/uuid" ) func mockUser() *user.User { req := user.UserCreateRequest{ Name: "Lee Brown", Email: uuid.NewRandom().String() + "@geeksinthewoods.com", Password: "akTechFr0n!ier", PasswordConfirm: "akTechFr0n!ier", } a, err := user.Create(tests.Context(), auth.Claims{}, test.MasterDB, req, time.Now().UTC().AddDate(-1, -1, -1)) if err != nil { panic(err) } return a } // TestUser is the entry point for the user endpoints. func TestUser(t *testing.T) { defer tests.Recover(t) t.Run("getUser", getUser) t.Run("createUser", createUser) t.Run("patchUser", patchUser) t.Run("patchUserPassword", patchUserPassword) } // getUser validates get user by ID endpoint. func getUser(t *testing.T) { var rtests []requestTest forbiddenUser := mockUser() // Both roles should be able to read the user. for rn, tr := range roleTests { usr := tr.SignupResult.User // Test 200. rtests = append(rtests, requestTest{ fmt.Sprintf("Role %s 200", rn), http.MethodGet, fmt.Sprintf("/v1/users/%s", usr.ID), nil, tr.Token, tr.Claims, http.StatusOK, nil, func(treq requestTest, body []byte) bool { var actual user.UserResponse if err := json.Unmarshal(body, &actual); err != nil { t.Logf("\t\tGot error : %+v", err) return false } // Add claims to the context so they can be retrieved later. ctx := context.WithValue(tests.Context(), auth.Key, tr.Claims) expectedMap := map[string]interface{}{ "updated_at": web.NewTimeResponse(ctx, usr.UpdatedAt), "id": usr.ID, "email": usr.Email, "timezone": usr.Timezone, "created_at": web.NewTimeResponse(ctx, usr.CreatedAt), "name": usr.Name, } expectedJson, err := json.Marshal(expectedMap) if err != nil { t.Logf("\t\tGot error : %+v", err) return false } var expected user.UserResponse if err := json.Unmarshal([]byte(expectedJson), &expected); err != nil { t.Logf("\t\tGot error : %+v", err) printResultMap(ctx, body) return false } if diff := cmp.Diff(actual, expected); diff != "" { actualJSON, err := json.MarshalIndent(actual, "", " ") if err != nil { t.Logf("\t\tGot error : %+v", err) return false } t.Logf("\t\tGot : %s\n", actualJSON) expectedJSON, err := json.MarshalIndent(expected, "", " ") if err != nil { t.Logf("\t\tGot error : %+v", err) return false } t.Logf("\t\tExpected : %s\n", expectedJSON) t.Logf("\t\tDiff : %s\n", diff) if len(expectedMap) == 0 { printResultMap(ctx, body) } return false } return true }, }) // Test 404. invalidID := uuid.NewRandom().String() rtests = append(rtests, requestTest{ fmt.Sprintf("Role %s 404 w/invalid ID", rn), http.MethodGet, fmt.Sprintf("/v1/users/%s", invalidID), nil, tr.Token, tr.Claims, http.StatusNotFound, web.ErrorResponse{ Error: fmt.Sprintf("user %s not found: Entity not found", invalidID), }, func(treq requestTest, body []byte) bool { return true }, }) // Test 404 - User exists but not allowed. rtests = append(rtests, requestTest{ fmt.Sprintf("Role %s 404 w/random user ID", rn), http.MethodGet, fmt.Sprintf("/v1/users/%s", forbiddenUser.ID), nil, tr.Token, tr.Claims, http.StatusNotFound, web.ErrorResponse{ Error: fmt.Sprintf("user %s not found: Entity not found", forbiddenUser.ID), }, func(treq requestTest, body []byte) bool { return true }, }) } runRequestTests(t, rtests) } // createUser validates create user endpoint. func createUser(t *testing.T) { var rtests []requestTest // Test create user. // Admin role: 201 // User role 403 for rn, tr := range roleTests { var expectedStatus int var expectedErr interface{} // Test 201. if rn == auth.RoleAdmin { expectedStatus = http.StatusCreated } else { expectedStatus = http.StatusForbidden expectedErr = web.ErrorResponse{ Error: mid.ErrForbidden.Error(), } } rtests = append(rtests, requestTest{ fmt.Sprintf("Role %s %d", rn, expectedStatus), http.MethodPost, "/v1/users", user.UserCreateRequest{ Name: "Lee Brown", Email: uuid.NewRandom().String() + rn + strconv.Itoa(len(rtests))+ "@geeksinthewoods.com", Password: "akTechFr0n!ier", PasswordConfirm: "akTechFr0n!ier", }, tr.Token, tr.Claims, expectedStatus, expectedErr, func(treq requestTest, body []byte) bool { if treq.error != nil { return true } var actual user.UserResponse if err := json.Unmarshal(body, &actual); err != nil { t.Logf("\t\tGot error : %+v", err) return false } // Add claims to the context so they can be retrieved later. ctx := context.WithValue(tests.Context(), auth.Key, tr.Claims) req := treq.request.(user.UserCreateRequest) expectedMap := map[string]interface{}{ "updated_at": web.NewTimeResponse(ctx, actual.UpdatedAt.Value), "id": actual.ID, "email": req.Email, "timezone": actual.Timezone, "created_at": web.NewTimeResponse(ctx, actual.CreatedAt.Value), "name": req.Name, } expectedJson, err := json.Marshal(expectedMap) if err != nil { t.Logf("\t\tGot error : %+v", err) return false } var expected user.UserResponse if err := json.Unmarshal([]byte(expectedJson), &expected); err != nil { t.Logf("\t\tGot error : %+v", err) printResultMap(ctx, body) return false } if diff := cmp.Diff(actual, expected); diff != "" { actualJSON, err := json.MarshalIndent(actual, "", " ") if err != nil { t.Logf("\t\tGot error : %+v", err) return false } t.Logf("\t\tGot : %s\n", actualJSON) expectedJSON, err := json.MarshalIndent(expected, "", " ") if err != nil { t.Logf("\t\tGot error : %+v", err) return false } t.Logf("\t\tExpected : %s\n", expectedJSON) t.Logf("\t\tDiff : %s\n", diff) if len(expectedMap) == 0 { printResultMap(ctx, body) } return false } return true }, }) } // Test update a user with invalid data. // Admin role: 400 // User role 403 for rn, tr := range roleTests { var expectedStatus int var expectedErr interface{} // Test 201. if rn == auth.RoleAdmin { expectedStatus = http.StatusBadRequest expectedErr = web.ErrorResponse{ Error: "field validation error", Fields: []web.FieldError{ {Field: "email", Error: "Key: 'UserCreateRequest.email' Error:Field validation for 'email' failed on the 'email' tag"}, }, } } else { expectedStatus = http.StatusForbidden expectedErr = web.ErrorResponse{ Error: mid.ErrForbidden.Error(), } } rtests = append(rtests, requestTest{ fmt.Sprintf("Role %s %d w/invalid data", rn, expectedStatus), http.MethodPost, "/v1/users", user.UserCreateRequest{ Name: "Lee Brown", Email: "invalid email address", Password: "akTechFr0n!ier", PasswordConfirm: "akTechFr0n!ier", }, tr.Token, tr.Claims, expectedStatus, expectedErr, func(treq requestTest, body []byte) bool { return true }, }) } runRequestTests(t, rtests) } // patchUser validates update user by ID endpoint. func patchUser(t *testing.T) { var rtests []requestTest // Test update a user // Admin role: 204 // User role 204 - user ID matches claims so OK for rn, tr := range roleTests { expectedStatus := http.StatusNoContent newName := rn + uuid.NewRandom().String() + strconv.Itoa(len(rtests)) rtests = append(rtests, requestTest{ fmt.Sprintf("Role %s %d", rn, expectedStatus), http.MethodPatch, "/v1/users", user.UserUpdateRequest{ ID: tr.SignupResult.User.ID, Name: &newName, }, tr.Token, tr.Claims, expectedStatus, nil, func(treq requestTest, body []byte) bool { return true }, }) } // Test update a user with invalid data. // Admin role: 400 // User role 400 for rn, tr := range roleTests { expectedStatus := http.StatusBadRequest expectedErr := web.ErrorResponse{ Error: "field validation error", Fields: []web.FieldError{ {Field: "email", Error: "Key: 'UserUpdateRequest.email' Error:Field validation for 'email' failed on the 'email' tag"}, }, } invalidEmail := "invalid email address" rtests = append(rtests, requestTest{ fmt.Sprintf("Role %s %d w/invalid data", rn, expectedStatus), http.MethodPatch, "/v1/users", user.UserUpdateRequest{ ID: tr.SignupResult.User.ID, Email: &invalidEmail, }, tr.Token, tr.Claims, expectedStatus, expectedErr, func(treq requestTest, body []byte) bool { return true }, }) } // Test update a user for with an invalid ID. // Admin role: 403 // User role 403 for rn, tr := range roleTests { expectedStatus := http.StatusForbidden expectedErr := web.ErrorResponse{ Error: user.ErrForbidden.Error(), } newName := rn + uuid.NewRandom().String() + strconv.Itoa(len(rtests)) invalidID := uuid.NewRandom().String() rtests = append(rtests, requestTest{ fmt.Sprintf("Role %s %d w/invalid ID", rn, expectedStatus), http.MethodPatch, "/v1/users", user.UserUpdateRequest{ ID: invalidID, Name: &newName, }, tr.Token, tr.Claims, expectedStatus, expectedErr, func(treq requestTest, body []byte) bool { return true }, }) } // Test update a user for with random user ID. // Admin role: 403 // User role 403 forbiddenUser := mockUser() for rn, tr := range roleTests { expectedStatus := http.StatusForbidden expectedErr := web.ErrorResponse{ Error: user.ErrForbidden.Error(), } newName := rn+uuid.NewRandom().String()+strconv.Itoa(len(rtests)) rtests = append(rtests, requestTest{ fmt.Sprintf("Role %s %d w/random user ID", rn, expectedStatus), http.MethodPatch, "/v1/users", user.UserUpdateRequest{ ID: forbiddenUser.ID, Name: &newName, }, tr.Token, tr.Claims, expectedStatus, expectedErr, func(treq requestTest, body []byte) bool { return true }, }) } runRequestTests(t, rtests) } // patchUserPassword validates update user password by ID endpoint. func patchUserPassword(t *testing.T) { var rtests []requestTest // Test update a user // Admin role: 204 // User role 204 - user ID matches claims so OK for rn, tr := range roleTests { expectedStatus := http.StatusNoContent newPass := uuid.NewRandom().String() rtests = append(rtests, requestTest{ fmt.Sprintf("Role %s %d", rn, expectedStatus), http.MethodPatch, "/v1/users/password", user.UserUpdatePasswordRequest{ ID: tr.SignupResult.User.ID, Password: newPass, PasswordConfirm: newPass, }, tr.Token, tr.Claims, expectedStatus, nil, func(treq requestTest, body []byte) bool { return true }, }) } // Test update a user password with invalid data. // Admin role: 400 // User role 400 for rn, tr := range roleTests { expectedStatus := http.StatusBadRequest expectedErr := web.ErrorResponse{ Error: "field validation error", Fields: []web.FieldError{ {Field: "password_confirm", Error: "Key: 'UserUpdatePasswordRequest.password_confirm' Error:Field validation for 'password_confirm' failed on the 'eqfield' tag"}, }, } newPass := uuid.NewRandom().String() rtests = append(rtests, requestTest{ fmt.Sprintf("Role %s %d w/invalid data", rn, expectedStatus), http.MethodPatch, "/v1/users/password", user.UserUpdatePasswordRequest{ ID: tr.SignupResult.User.ID, Password: newPass, PasswordConfirm: "different", }, tr.Token, tr.Claims, expectedStatus, expectedErr, func(treq requestTest, body []byte) bool { return true }, }) } // Test update a user password for with an invalid ID. // Admin role: 403 // User role 403 for rn, tr := range roleTests { expectedStatus := http.StatusForbidden expectedErr := web.ErrorResponse{ Error: user.ErrForbidden.Error(), } newPass := uuid.NewRandom().String() invalidID := uuid.NewRandom().String() rtests = append(rtests, requestTest{ fmt.Sprintf("Role %s %d w/invalid ID", rn, expectedStatus), http.MethodPatch, "/v1/users/password", user.UserUpdatePasswordRequest{ ID: invalidID, Password: newPass, PasswordConfirm: newPass, }, tr.Token, tr.Claims, expectedStatus, expectedErr, func(treq requestTest, body []byte) bool { return true }, }) } // Test update a user password for with random user ID. // Admin role: 403 // User role 403 forbiddenUser := mockUser() for rn, tr := range roleTests { expectedStatus := http.StatusForbidden expectedErr := web.ErrorResponse{ Error: user.ErrForbidden.Error(), } newPass := uuid.NewRandom().String() rtests = append(rtests, requestTest{ fmt.Sprintf("Role %s %d w/random user ID", rn, expectedStatus), http.MethodPatch, "/v1/users/password", user.UserUpdatePasswordRequest{ ID: forbiddenUser.ID, Password: newPass, PasswordConfirm: newPass, }, tr.Token, tr.Claims, expectedStatus, expectedErr, func(treq requestTest, body []byte) bool { return true }, }) } runRequestTests(t, rtests) }