From 2ab62a2f6eaf96aa68bf79eab529a8019a334a07 Mon Sep 17 00:00:00 2001 From: akshayd2020 Date: Sat, 20 Jan 2024 10:19:31 -0500 Subject: [PATCH 1/6] feat: Initial commit for post user --- backend/src/controllers/user.go | 29 +++++++++++++++++++++++++++- backend/src/models/user.go | 10 ++++++++++ backend/src/server/server.go | 1 + backend/src/services/user.go | 33 +++++++++++++++++++++++++++++++- backend/src/transactions/user.go | 8 ++++++++ go.work.sum | 1 + 6 files changed, 80 insertions(+), 2 deletions(-) diff --git a/backend/src/controllers/user.go b/backend/src/controllers/user.go index c23ce0c0c..d3a1cedfa 100644 --- a/backend/src/controllers/user.go +++ b/backend/src/controllers/user.go @@ -2,7 +2,7 @@ package controllers import ( "github.com/GenerateNU/sac/backend/src/services" - + "github.com/GenerateNU/sac/backend/src/models" "github.com/gofiber/fiber/v2" ) @@ -33,3 +33,30 @@ func (u *UserController) GetAllUsers(c *fiber.Ctx) error { return c.Status(fiber.StatusOK).JSON(users) } + +// Create User godoc +// +// @Summary Creates a User +// @Description Creates a user +// @ID create-user +// @Tags user +// @Accept json +// @Produce json +// @Success 201 {object} models.User +// @Failure 400 {string} string "failed to create user" +// @Failure 500 {string} string "internal server error" +// @Router /api/v1/users/ [post] +func (u *UserController) CreateUser(c *fiber.Ctx) error { + var userBody models.CreateUserRequestBody + + if err := c.BodyParser(&userBody); err != nil { + return fiber.NewError(fiber.StatusBadRequest, "failed to process the request") + } + + user, err := u.userService.CreateUser(userBody) + if err != nil { + return err + } + + return c.Status(fiber.StatusCreated).JSON(user) +} diff --git a/backend/src/models/user.go b/backend/src/models/user.go index 792fb8181..da40b5c03 100644 --- a/backend/src/models/user.go +++ b/backend/src/models/user.go @@ -56,3 +56,13 @@ type User struct { RSVP []Event `gorm:"many2many:user_event_rsvps;constraint:OnUpdate:CASCADE,OnDelete:CASCADE;" json:"-" validate:"-"` Waitlist []Event `gorm:"many2many:user_event_waitlists;constraint:OnUpdate:CASCADE,OnDelete:CASCADE;" json:"-" validate:"-"` } + +type CreateUserRequestBody struct { + NUID string `json:"nuid" validate:"number,len=9"` + FirstName string `json:"first_name" validate:"max=255"` + LastName string `json:"last_name" validate:"max=255"` + Email string `json:"email" validate:"email"` + Password string `json:"password" validate:"password"` + College string `json:"college" validate:"oneof=CAMD DMSB KCCS CE BCHS SL CPS CS CSSH"` + Year uint `json:"year" validate:"min=1,max=6"` +} diff --git a/backend/src/server/server.go b/backend/src/server/server.go index 2a23e9a0f..6dc93c610 100644 --- a/backend/src/server/server.go +++ b/backend/src/server/server.go @@ -64,6 +64,7 @@ func userRoutes(router fiber.Router, userService services.UserServiceInterface) users := router.Group("/users") users.Get("/", userController.GetAllUsers) + users.Post("/", userController.CreateUser) } func categoryRoutes(router fiber.Router, categoryService services.CategoryServiceInterface) { diff --git a/backend/src/services/user.go b/backend/src/services/user.go index fe27eb731..2d2d1f101 100644 --- a/backend/src/services/user.go +++ b/backend/src/services/user.go @@ -3,12 +3,14 @@ package services import ( "github.com/GenerateNU/sac/backend/src/models" "github.com/GenerateNU/sac/backend/src/transactions" - + "github.com/GenerateNU/sac/backend/src/utilities" + "github.com/gofiber/fiber/v2" "gorm.io/gorm" ) type UserServiceInterface interface { GetAllUsers() ([]models.User, error) + CreateUser(userBody models.CreateUserRequestBody) (*models.User, error) } type UserService struct { @@ -19,3 +21,32 @@ type UserService struct { func (u *UserService) GetAllUsers() ([]models.User, error) { return transactions.GetAllUsers(u.DB) } + +// temporary +func createUserFromRequestBody(userBody models.CreateUserRequestBody) (models.User, error) { + // TL DAVID -- some validation still needs to be done but depends on design + if err := utilities.ValidateData(userBody); err != nil { + return models.User{}, fiber.NewError(fiber.StatusBadRequest, "failed to validate the data") + } + + var user models.User + user.NUID = userBody.NUID + user.FirstName = userBody.FirstName + user.LastName = userBody.LastName + user.Email = userBody.Email + // TODO: hash + user.PasswordHash = userBody.Password + user.College = models.College(userBody.College) + user.Year = models.Year(userBody.Year) + + return user, nil +} + +func (u *UserService) CreateUser(userBody models.CreateUserRequestBody) (*models.User, error) { + user, err := createUserFromRequestBody(userBody) + if err != nil { + return nil, err + } + + return transactions.CreateUser(u.DB, &user) +} diff --git a/backend/src/transactions/user.go b/backend/src/transactions/user.go index 3d15f1385..9e50473ab 100644 --- a/backend/src/transactions/user.go +++ b/backend/src/transactions/user.go @@ -16,3 +16,11 @@ func GetAllUsers(db *gorm.DB) ([]models.User, error) { return users, nil } + +func CreateUser(db *gorm.DB, user *models.User) (*models.User, error) { + if err := db.Create(user).Error; err != nil { + return nil, fiber.NewError(fiber.StatusInternalServerError, "failed to create user") + } + + return user, nil +} diff --git a/go.work.sum b/go.work.sum index 3bc8690e6..d56aaba60 100644 --- a/go.work.sum +++ b/go.work.sum @@ -1,4 +1,5 @@ github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= +github.com/fatih/color v1.14.1/go.mod h1:2oHN61fhTpgcxD3TSWCgKDiH1+x4OiDVVGH8WlgGZGg= github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= github.com/hashicorp/golang-lru v0.5.4/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4= github.com/jackc/puddle/v2 v2.2.1/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4= From 9b96c72619a95a207fe9ff62dbac51be81a6649b Mon Sep 17 00:00:00 2001 From: akshayd2020 Date: Sat, 20 Jan 2024 12:44:15 -0500 Subject: [PATCH 2/6] feat: Add Create User Tests --- backend/go.mod | 5 ++ backend/src/models/user.go | 15 ++-- backend/src/services/user.go | 9 ++- backend/src/transactions/user.go | 34 ++++++++- backend/src/utilities/validator.go | 23 ++++++ backend/tests/api/user_test.go | 109 +++++++++++++++++++++++++++++ go.work.sum | 3 + 7 files changed, 188 insertions(+), 10 deletions(-) diff --git a/backend/go.mod b/backend/go.mod index 2273d3c0c..ee9f65bdd 100644 --- a/backend/go.mod +++ b/backend/go.mod @@ -12,6 +12,11 @@ require ( gorm.io/gorm v1.25.5 ) +require ( + github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect + github.com/mcnijman/go-emailaddress v1.1.1 // indirect +) + require github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect require ( diff --git a/backend/src/models/user.go b/backend/src/models/user.go index da40b5c03..ca43d0381 100644 --- a/backend/src/models/user.go +++ b/backend/src/models/user.go @@ -57,12 +57,13 @@ type User struct { Waitlist []Event `gorm:"many2many:user_event_waitlists;constraint:OnUpdate:CASCADE,OnDelete:CASCADE;" json:"-" validate:"-"` } +// TODO: Should we change error message for missing required fields? type CreateUserRequestBody struct { - NUID string `json:"nuid" validate:"number,len=9"` - FirstName string `json:"first_name" validate:"max=255"` - LastName string `json:"last_name" validate:"max=255"` - Email string `json:"email" validate:"email"` - Password string `json:"password" validate:"password"` - College string `json:"college" validate:"oneof=CAMD DMSB KCCS CE BCHS SL CPS CS CSSH"` - Year uint `json:"year" validate:"min=1,max=6"` + NUID string `json:"nuid" validate:"required,number,len=9"` + FirstName string `json:"first_name" validate:"required,max=255"` + LastName string `json:"last_name" validate:"required,max=255"` + Email string `json:"email" validate:"required,neu_email"` + Password string `json:"password" validate:"required,password"` + College string `json:"college" validate:"required,oneof=CAMD DMSB KCCS CE BCHS SL CPS CS CSSH"` + Year uint `json:"year" validate:"required,min=1,max=6"` } diff --git a/backend/src/services/user.go b/backend/src/services/user.go index 2d2d1f101..559d082cf 100644 --- a/backend/src/services/user.go +++ b/backend/src/services/user.go @@ -4,6 +4,7 @@ import ( "github.com/GenerateNU/sac/backend/src/models" "github.com/GenerateNU/sac/backend/src/transactions" "github.com/GenerateNU/sac/backend/src/utilities" + "github.com/go-playground/validator/v10" "github.com/gofiber/fiber/v2" "gorm.io/gorm" ) @@ -25,8 +26,12 @@ func (u *UserService) GetAllUsers() ([]models.User, error) { // temporary func createUserFromRequestBody(userBody models.CreateUserRequestBody) (models.User, error) { // TL DAVID -- some validation still needs to be done but depends on design - if err := utilities.ValidateData(userBody); err != nil { - return models.User{}, fiber.NewError(fiber.StatusBadRequest, "failed to validate the data") + + validate := validator.New() + validate.RegisterValidation("neu_email", utilities.ValidateEmail) + validate.RegisterValidation("password", utilities.ValidatePassword) + if err := validate.Struct(userBody); err != nil { + return models.User{}, fiber.NewError(fiber.StatusBadRequest, err.Error()) } var user models.User diff --git a/backend/src/transactions/user.go b/backend/src/transactions/user.go index 9e50473ab..5f3bbbf75 100644 --- a/backend/src/transactions/user.go +++ b/backend/src/transactions/user.go @@ -2,7 +2,7 @@ package transactions import ( "github.com/GenerateNU/sac/backend/src/models" - + "errors" "github.com/gofiber/fiber/v2" "gorm.io/gorm" ) @@ -17,7 +17,39 @@ func GetAllUsers(db *gorm.DB) ([]models.User, error) { return users, nil } +func GetUser(db *gorm.DB, id uint) (*models.User, error) { + var user models.User + if err := db.First(&user, id).Error; err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return nil, fiber.NewError(fiber.StatusNotFound, err.Error()) + } + + return nil, fiber.NewError(fiber.StatusInternalServerError, err.Error()) + } + + return &user, nil +} + func CreateUser(db *gorm.DB, user *models.User) (*models.User, error) { + + var existing models.User + + if err := db.Where("email = ?", user.Email).First(&existing).Error; err != nil { + if !errors.Is(err, gorm.ErrRecordNotFound) { + return nil, fiber.NewError(fiber.StatusInternalServerError, "failed to create user") + } + } else { + return nil, fiber.NewError(fiber.StatusBadRequest, "user with that email already exists") + } + + if err := db.Where("nuid = ?", user.NUID).First(&existing).Error; err != nil { + if !errors.Is(err, gorm.ErrRecordNotFound) { + return nil, fiber.NewError(fiber.StatusInternalServerError, "failed to create user") + } + } else { + return nil, fiber.NewError(fiber.StatusBadRequest, "user with that nuid already exists") + } + if err := db.Create(user).Error; err != nil { return nil, fiber.NewError(fiber.StatusInternalServerError, "failed to create user") } diff --git a/backend/src/utilities/validator.go b/backend/src/utilities/validator.go index 770097268..397df37df 100644 --- a/backend/src/utilities/validator.go +++ b/backend/src/utilities/validator.go @@ -2,8 +2,31 @@ package utilities import ( "github.com/go-playground/validator/v10" + "github.com/mcnijman/go-emailaddress" ) +func ValidateEmail(fl validator.FieldLevel) bool { + email, err := emailaddress.Parse(fl.Field().String()) + if err != nil { + return false + } + + if email.Domain != "northeastern.edu" { + return false + } + + return true +} + +func ValidatePassword(fl validator.FieldLevel) bool { + // TODO: we need to think of validation rules + if len(fl.Field().String()) < 6 { + return false + } + + return true +} + // Validate the data sent to the server if the data is invalid, return an error otherwise, return nil func ValidateData(model interface{}) error { validate := validator.New(validator.WithRequiredStructEnabled()) diff --git a/backend/tests/api/user_test.go b/backend/tests/api/user_test.go index 8307c0dc9..609046ecb 100644 --- a/backend/tests/api/user_test.go +++ b/backend/tests/api/user_test.go @@ -1,6 +1,8 @@ package tests import ( + "fmt" + "math/rand" "net/http" "testing" @@ -45,3 +47,110 @@ func TestGetAllUsersWorks(t *testing.T) { }, ) } + +func CreateSampleUser(t *testing.T, email string, nuid string) ExistingAppAssert { + return TestRequest{ + Method: "POST", + Path: "/api/v1/users/", + Body: &map[string]interface{}{ + "first_name": "TestX", + "last_name": "TestY", + "email": email, + "password": "1234567890", + "nuid": nuid, + "college": "CAMD", + "year": 3, + }, + }.TestOnStatusAndDBKeepDB(t, nil, + DBTesterWithStatus{ + Status: 201, + DBTester: func(app TestApp, assert *assert.A, resp *http.Response) { + + var respUser models.User + + err := json.NewDecoder(resp.Body).Decode(&respUser) + + assert.NilError(err) + + dbUser, err := transactions.GetUser(app.Conn, respUser.ID) + assert.NilError(err) + + // This is done because password hash is ommitted in response + respUser.PasswordHash = dbUser.PasswordHash + + assert.Equal(dbUser, &respUser) + }, + }, + ) +} + +func TestCreateUserWorks(t *testing.T) { + appAssert := CreateSampleUser(t, "test@northeastern.edu", "001159263") + appAssert.App.DropDB() +} + +func TestCreateUserFailsIfCategoryWithEmailAlreadyExists(t *testing.T) { + email := "test@northeastern.edu" + + existingAppAssert := CreateSampleUser(t, "test@northeastern.edu", "001159263") + + for _, permutation := range AllCasingPermutations(email) { + fmt.Println(permutation) + var numberRunes = []rune("1234567890") + + nuid_arr := make([]rune, 9) + + for i := range nuid_arr { + nuid_arr[i] = numberRunes[rand.Intn(len(numberRunes))] + } + nuid := string(nuid_arr) + + TestRequest{ + Method: "POST", + Path: "/api/v1/users/", + Body: &map[string]interface{}{ + "first_name": "TestX", + "last_name": "TestY", + "email": email, + "password": "1234567890", + "nuid": nuid, + "college": "CAMD", + "year": 3, + }, + }.TestOnStatusAndMessageKeepDB(t, &existingAppAssert, + MessageWithStatus{ + Status: 400, + Message: "user with that email already exists", + }, + ) + } + + existingAppAssert.App.DropDB() +} + + +func TestCreateUserFailsIfCategoryWithNUIDAlreadyExists(t *testing.T) { + + existingAppAssert := CreateSampleUser(t, "test@northeastern.edu", "001159263") + + TestRequest{ + Method: "POST", + Path: "/api/v1/users/", + Body: &map[string]interface{}{ + "first_name": "TestX", + "last_name": "TestY", + "email": "test2@northeastern.edu", + "password": "1234567890", + "nuid": "001159263", + "college": "CAMD", + "year": 3, + }, + }.TestOnStatusAndMessageKeepDB(t, &existingAppAssert, + MessageWithStatus{ + Status: 400, + Message: "user with that nuid already exists", + }, + ) + + existingAppAssert.App.DropDB() +} diff --git a/go.work.sum b/go.work.sum index d56aaba60..c33b0811d 100644 --- a/go.work.sum +++ b/go.work.sum @@ -6,10 +6,13 @@ github.com/jackc/puddle/v2 v2.2.1/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFr github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo= github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg= +github.com/mcnijman/go-emailaddress v1.1.1 h1:AGhgVDG3tCDaL0/Vc6erlPQjDuDN3dAT7rRdgFtetr0= +github.com/mcnijman/go-emailaddress v1.1.1/go.mod h1:5whZrhS8Xp5LxO8zOD35BC+b76kROtsh+dPomeRt/II= github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= go.uber.org/zap v1.21.0/go.mod h1:wjWOCqI0f2ZZrJF/UufIOkiC8ii6tm1iqIsLo76RfJw= +golang.org/x/net v0.0.0-20180911220305-26e67e76b6c3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= google.golang.org/protobuf v1.31.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= From e49c5aff7326f9c086ef1baeb59e5cb3b516ef01 Mon Sep 17 00:00:00 2001 From: OJisMe Date: Sun, 21 Jan 2024 02:26:14 -0500 Subject: [PATCH 3/6] test: invalid and missing fields for createUser --- backend/src/utilities/validator.go | 6 +- backend/tests/api/user_test.go | 185 ++++++++++++++++++++++++++++- 2 files changed, 182 insertions(+), 9 deletions(-) diff --git a/backend/src/utilities/validator.go b/backend/src/utilities/validator.go index 397df37df..a257e7f50 100644 --- a/backend/src/utilities/validator.go +++ b/backend/src/utilities/validator.go @@ -20,11 +20,7 @@ func ValidateEmail(fl validator.FieldLevel) bool { func ValidatePassword(fl validator.FieldLevel) bool { // TODO: we need to think of validation rules - if len(fl.Field().String()) < 6 { - return false - } - - return true + return len(fl.Field().String()) >= 6 } // Validate the data sent to the server if the data is invalid, return an error otherwise, return nil diff --git a/backend/tests/api/user_test.go b/backend/tests/api/user_test.go index 609046ecb..da96f1b02 100644 --- a/backend/tests/api/user_test.go +++ b/backend/tests/api/user_test.go @@ -9,7 +9,6 @@ import ( "github.com/GenerateNU/sac/backend/src/models" "github.com/GenerateNU/sac/backend/src/transactions" "github.com/huandu/go-assert" - "github.com/goccy/go-json" ) @@ -128,10 +127,10 @@ func TestCreateUserFailsIfCategoryWithEmailAlreadyExists(t *testing.T) { existingAppAssert.App.DropDB() } - func TestCreateUserFailsIfCategoryWithNUIDAlreadyExists(t *testing.T) { + nuid := "001159263" - existingAppAssert := CreateSampleUser(t, "test@northeastern.edu", "001159263") + existingAppAssert := CreateSampleUser(t, "test@northeastern.edu", nuid) TestRequest{ Method: "POST", @@ -141,7 +140,7 @@ func TestCreateUserFailsIfCategoryWithNUIDAlreadyExists(t *testing.T) { "last_name": "TestY", "email": "test2@northeastern.edu", "password": "1234567890", - "nuid": "001159263", + "nuid": nuid, "college": "CAMD", "year": 3, }, @@ -154,3 +153,181 @@ func TestCreateUserFailsIfCategoryWithNUIDAlreadyExists(t *testing.T) { existingAppAssert.App.DropDB() } + +func CreateInvalidUser(t *testing.T, body map[string]interface{}, expectedMessage string) ExistingAppAssert { + return TestRequest{ + Method: "POST", + Path: "/api/v1/users/", + Body: &body, + }.TestOnStatusAndMessageKeepDB(t, nil, + MessageWithStatus{ + Status: 400, + Message: expectedMessage, + }, + ) +} + +func TestCreateUserFailsOnInvalidNUID(t *testing.T) { + first := "Jermaine" + last := "Cole" + goodEmail := "test@northeastern.edu" + goodPassword := "1234567890" + goodCollege := "CAMD" + goodYear := 3 + + body := map[string]interface{}{ + "first_name": first, + "last_name": last, + "email": goodEmail, + "password": goodPassword, + "college": goodCollege, + "year": goodYear, + } + + // test that it fails on <9 digits + badNUIDLen := "012" + body["nuid"] = badNUIDLen + //TODO change error messages to be more readable + expectedMessageLen := "Key: 'CreateUserRequestBody.NUID' Error:Field validation for 'NUID' failed on the 'len' tag" + appAssertLen := CreateInvalidUser(t, body, expectedMessageLen) + appAssertLen.App.DropDB() + + // test that it fails on non-numbers + badNUIDNumber := "01234578a" + body["nuid"] = badNUIDNumber + expectedMessageNumber := "Key: 'CreateUserRequestBody.NUID' Error:Field validation for 'NUID' failed on the 'number' tag" + appAssertNumber := CreateInvalidUser(t, body, expectedMessageNumber) + appAssertNumber.App.DropDB() +} + +func TestCreateUserFailsOnInvalidEmail(t *testing.T) { + first := "Jermaine" + last := "Cole" + badEmail := "test@gmail.com" + goodPassword := "1234567890" + goodCollege := "CAMD" + goodYear := 3 + goodNUID := "001159263" + + body := map[string]interface{}{ + "first_name": first, + "last_name": last, + "email": badEmail, + "password": goodPassword, + "college": goodCollege, + "year": goodYear, + "nuid": goodNUID, + } + + expectedMessage := "Key: 'CreateUserRequestBody.Email' Error:Field validation for 'Email' failed on the 'neu_email' tag" + + appAssert := CreateInvalidUser(t, body, expectedMessage) + appAssert.App.DropDB() +} + +func TestCreateUserFailsOnInvalidPassword(t *testing.T) { + badPassword := "123" + first := "Jermaine" + last := "Cole" + goodEmail := "test@northeastern.edu" + goodCollege := "CAMD" + goodYear := 3 + goodNUID := "001159263" + expectedMessage := "Key: 'CreateUserRequestBody.Password' Error:Field validation for 'Password' failed on the 'password' tag" + + body := map[string]interface{}{ + "first_name": first, + "last_name": last, + "email": goodEmail, + "password": badPassword, + "college": goodCollege, + "year": goodYear, + "nuid": goodNUID, + } + + appAssert := CreateInvalidUser(t, body, expectedMessage) + appAssert.App.DropDB() +} + +func TestCreateUserFailsOnInvalidYear(t *testing.T) { + goodPassword := "123456789" + first := "Jermaine" + last := "Cole" + goodEmail := "test@northeastern.edu" + goodCollege := "CAMD" + badYear := 7 + goodNUID := "001159263" + expectedMessage := "Key: 'CreateUserRequestBody.Year' Error:Field validation for 'Year' failed on the 'max' tag" + + body := map[string]interface{}{ + "first_name": first, + "last_name": last, + "email": goodEmail, + "password": goodPassword, + "college": goodCollege, + "year": badYear, + "nuid": goodNUID, + } + + appAssert := CreateInvalidUser(t, body, expectedMessage) + appAssert.App.DropDB() +} + +func TestCreateUserFailsOnInvalidCollege(t *testing.T) { + goodPassword := "123456789" + first := "Jermaine" + last := "Cole" + goodEmail := "test@northeastern.edu" + badCollege := "oopsies" + goodYear := 6 + goodNUID := "001159263" + expectedMessage := "Key: 'CreateUserRequestBody.College' Error:Field validation for 'College' failed on the 'oneof' tag" + + body := map[string]interface{}{ + "first_name": first, + "last_name": last, + "email": goodEmail, + "password": goodPassword, + "college": badCollege, + "year": goodYear, + "nuid": goodNUID, + } + + appAssert := CreateInvalidUser(t, body, expectedMessage) + appAssert.App.DropDB() +} + +func TestCreateUserFailsOnMissingField(t *testing.T) { + + // tests that: + // if a field is missing, the body parser should fail and return a 400 + // if a field is present but empty, the body parser should fail and return a 400 + + password := "123456789" + first := "Jermaine" + last := "Cole" + email := "test@northeastern.edu" + college := "CS" + year := 6 + nuid := "001159263" + expectedMessage := "Key: 'CreateUserRequestBody.FirstName' Error:Field validation for 'FirstName' failed on the 'required' tag" + + body := map[string]interface{}{ + "first_name": first, + "last_name": last, + "email": email, + "password": password, + "college": college, + "year": year, + "nuid": nuid, + } + + delete(body, "first_name") + + appAssert := CreateInvalidUser(t, body, expectedMessage) + appAssert.App.DropDB() + + //TODO loop through all fields +} + +// TODO test extra fields From 256b786371e37886908783ae7e9a1cff9a5e302f Mon Sep 17 00:00:00 2001 From: OJisMe Date: Sun, 21 Jan 2024 18:43:18 -0500 Subject: [PATCH 4/6] test: missing fields in createUser --- backend/tests/api/user_test.go | 82 ++++++++++++++++++++++++++++++---- 1 file changed, 74 insertions(+), 8 deletions(-) diff --git a/backend/tests/api/user_test.go b/backend/tests/api/user_test.go index da96f1b02..a16920bc2 100644 --- a/backend/tests/api/user_test.go +++ b/backend/tests/api/user_test.go @@ -155,6 +155,7 @@ func TestCreateUserFailsIfCategoryWithNUIDAlreadyExists(t *testing.T) { } func CreateInvalidUser(t *testing.T, body map[string]interface{}, expectedMessage string) ExistingAppAssert { + // To use for testing invalid users that should fail to be created return TestRequest{ Method: "POST", Path: "/api/v1/users/", @@ -168,6 +169,10 @@ func CreateInvalidUser(t *testing.T, body map[string]interface{}, expectedMessag } func TestCreateUserFailsOnInvalidNUID(t *testing.T) { + // tests that: + // if nuid is not 9 digits, the Post Request should fail and return a 400 + // if nuid is not a number, the Post Request should fail and return a 400 + first := "Jermaine" last := "Cole" goodEmail := "test@northeastern.edu" @@ -201,6 +206,8 @@ func TestCreateUserFailsOnInvalidNUID(t *testing.T) { } func TestCreateUserFailsOnInvalidEmail(t *testing.T) { + // tests that: + // if email is not a northeastern email (ends in @northeastern.edu), the Post Request should fail and return a 400 first := "Jermaine" last := "Cole" badEmail := "test@gmail.com" @@ -226,6 +233,9 @@ func TestCreateUserFailsOnInvalidEmail(t *testing.T) { } func TestCreateUserFailsOnInvalidPassword(t *testing.T) { + // tests that: + // if password is not at least 10 characters, the Post Request should fail and return a 400 + // TODO create better password requirements badPassword := "123" first := "Jermaine" last := "Cole" @@ -250,6 +260,8 @@ func TestCreateUserFailsOnInvalidPassword(t *testing.T) { } func TestCreateUserFailsOnInvalidYear(t *testing.T) { + // tests that: + // if year is not within range [1,6], the Post Request should fail and return a 400 goodPassword := "123456789" first := "Jermaine" last := "Cole" @@ -274,6 +286,9 @@ func TestCreateUserFailsOnInvalidYear(t *testing.T) { } func TestCreateUserFailsOnInvalidCollege(t *testing.T) { + // tests that: + // if college is not one of CAMD DMSB KCCS CE BCHS SL CPS CS CSSH, the Post Request should fail and return a 400 + goodPassword := "123456789" first := "Jermaine" last := "Cole" @@ -300,8 +315,8 @@ func TestCreateUserFailsOnInvalidCollege(t *testing.T) { func TestCreateUserFailsOnMissingField(t *testing.T) { // tests that: - // if a field is missing, the body parser should fail and return a 400 - // if a field is present but empty, the body parser should fail and return a 400 + // if a field is missing, the Post Request should fail and return a 400 + // if a field is present but empty, the Post Request should fail and return a 400 password := "123456789" first := "Jermaine" @@ -310,7 +325,6 @@ func TestCreateUserFailsOnMissingField(t *testing.T) { college := "CS" year := 6 nuid := "001159263" - expectedMessage := "Key: 'CreateUserRequestBody.FirstName' Error:Field validation for 'FirstName' failed on the 'required' tag" body := map[string]interface{}{ "first_name": first, @@ -322,12 +336,64 @@ func TestCreateUserFailsOnMissingField(t *testing.T) { "nuid": nuid, } - delete(body, "first_name") + // map from json field name to struct field name + fields := map[string]string{"first_name":"FirstName", "last_name":"LastName", "email":"Email", "password":"Password", "college":"College", "year":"Year", "nuid":"NUID"} - appAssert := CreateInvalidUser(t, body, expectedMessage) - appAssert.App.DropDB() - //TODO loop through all fields + //loops through each field and removes it from the body then tests that the post fails and returns a 400 + for structKey, jsonKey := range fields { + // Create a copy of the body without the current field + updatedBody := make(map[string]interface{}) + for key, value := range body { + if key != structKey { + updatedBody[key] = value + } + } + + expectedMesage := "Key: 'CreateUserRequestBody." +jsonKey + "' Error:Field validation for '" + jsonKey + "' failed on the 'required' tag" + + appAssert := CreateInvalidUser(t, updatedBody, expectedMesage) + appAssert.App.DropDB() + } + } -// TODO test extra fields +/* + +Dear TLs David, Garret, + +I can not for the life of me figure out how to do this test. It has become the bane of me +I also have not touched nor smelled the notion of "grass" in the last 27 hours +Please forgive me. I am but a humble student +- Olivier + +*/ +func TestCreateUserFailsOnExtraFields(t *testing.T) { + // tests that: + // if extra fields are present, the Post Request should fail and return a 400 + + password := "123456789" + first := "Jermaine" + last := "Cole" + email := "jermaine@northeastern.edu" + college := "CS" + year := 6 + nuid := "001159263" + someField := "someField" + + body := map[string]interface{}{ + "first_name": first, + "last_name": last, + "email": email, + "password": password, + "college": college, + "year": year, + "nuid": nuid, + // foreign fields should not be allowed + "extra": someField, + } + + appAssert := CreateInvalidUser(t, body, "expectedMessage") + appAssert.App.DropDB() + +} From 3897e194cfd9d86b13c1fc5876ff187df2d99aad Mon Sep 17 00:00:00 2001 From: akshayd2020 Date: Sun, 21 Jan 2024 19:56:14 -0500 Subject: [PATCH 5/6] Fix test --- backend/tests/api/user_test.go | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/backend/tests/api/user_test.go b/backend/tests/api/user_test.go index a16920bc2..3bb429834 100644 --- a/backend/tests/api/user_test.go +++ b/backend/tests/api/user_test.go @@ -92,8 +92,12 @@ func TestCreateUserFailsIfCategoryWithEmailAlreadyExists(t *testing.T) { email := "test@northeastern.edu" existingAppAssert := CreateSampleUser(t, "test@northeastern.edu", "001159263") + perms := 0 for _, permutation := range AllCasingPermutations(email) { + if perms == 20 { + break + } fmt.Println(permutation) var numberRunes = []rune("1234567890") @@ -122,6 +126,7 @@ func TestCreateUserFailsIfCategoryWithEmailAlreadyExists(t *testing.T) { Message: "user with that email already exists", }, ) + perms++ } existingAppAssert.App.DropDB() From b229f76924173c1e067c0fa971829346c39d5f8c Mon Sep 17 00:00:00 2001 From: garrettladley Date: Mon, 22 Jan 2024 17:33:07 -0500 Subject: [PATCH 6/6] fixes --- backend/go.mod | 15 +++++---------- backend/go.sum | 6 ------ backend/src/controllers/user.go | 6 ++++-- backend/src/services/user.go | 31 ++++++++++++++++-------------- backend/src/transactions/user.go | 5 ++--- backend/src/utilities/validator.go | 10 ---------- backend/tests/api/user_test.go | 12 ++++-------- 7 files changed, 32 insertions(+), 53 deletions(-) diff --git a/backend/go.mod b/backend/go.mod index 4c453b144..1ef878c4d 100644 --- a/backend/go.mod +++ b/backend/go.mod @@ -12,18 +12,12 @@ require ( gorm.io/gorm v1.25.5 ) -require ( - github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect - github.com/mcnijman/go-emailaddress v1.1.1 // indirect - -require github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect - - require ( github.com/KyleBanks/depth v1.2.1 // indirect github.com/PuerkitoBio/purell v1.1.1 // indirect github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 // indirect github.com/andybalholm/brotli v1.0.5 // indirect + github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect github.com/fsnotify/fsnotify v1.7.0 // indirect github.com/gabriel-vasile/mimetype v1.4.2 // indirect github.com/go-openapi/jsonpointer v0.19.5 // indirect @@ -49,7 +43,8 @@ require ( github.com/mattn/go-colorable v0.1.13 // indirect github.com/mattn/go-isatty v0.0.20 // indirect github.com/mattn/go-runewidth v0.0.15 // indirect - github.com/mitchellh/mapstructure v1.5.0 // indirect + github.com/mcnijman/go-emailaddress v1.1.1 + github.com/mitchellh/mapstructure v1.5.0 github.com/pelletier/go-toml/v2 v2.1.0 // indirect github.com/rivo/uniseg v0.2.0 // indirect github.com/rogpeppe/go-internal v1.12.0 // indirect @@ -66,11 +61,11 @@ require ( github.com/valyala/tcplisten v1.0.0 // indirect go.uber.org/atomic v1.9.0 // indirect go.uber.org/multierr v1.9.0 // indirect - golang.org/x/crypto v0.18.0 // indirect + golang.org/x/crypto v0.18.0 golang.org/x/exp v0.0.0-20230905200255-921286631fa9 // indirect golang.org/x/net v0.20.0 // indirect golang.org/x/sys v0.16.0 // indirect - golang.org/x/text v0.14.0 // indirect + golang.org/x/text v0.14.0 golang.org/x/tools v0.13.0 // indirect gopkg.in/ini.v1 v1.67.0 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect diff --git a/backend/go.sum b/backend/go.sum index 5fb160261..1234323f4 100644 --- a/backend/go.sum +++ b/backend/go.sum @@ -158,8 +158,6 @@ go.uber.org/multierr v1.9.0/go.mod h1:X2jQV1h+kxSjClGpnseKVIxpmcjrj7MNnI0bnlfKTV golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU= -golang.org/x/crypto v0.17.0 h1:r8bRNjWL3GshPW3gkd+RpvzWrZAwPS49OmTGZ/uhM4k= -golang.org/x/crypto v0.17.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4= golang.org/x/crypto v0.18.0 h1:PGVlW0xEltQnzFZ55hkuX5+KLyrMYhHld1YHO4AKcdc= golang.org/x/crypto v0.18.0/go.mod h1:R0j02AL6hcrfOiy9T4ZYp/rcWeMxM3L6QYxlOuEG1mg= golang.org/x/exp v0.0.0-20230905200255-921286631fa9 h1:GoHiUyI/Tp2nVkLI2mCxVkOjsbSXD66ic0XW0js0R9g= @@ -178,8 +176,6 @@ golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug golang.org/x/net v0.3.0/go.mod h1:MBQ8lrhLObU/6UmLb4fmbmk5OcyYmqtbGd/9yIeKjEE= golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc= -golang.org/x/net v0.19.0 h1:zTwKpTd2XuCqf8huc7Fo2iSy+4RHPd10s4KzeTnVr1c= -golang.org/x/net v0.19.0/go.mod h1:CfAk/cbD4CthTvqiEl8NpboMuiuOYsAr/7NOjZJtv1U= golang.org/x/net v0.20.0 h1:aCL9BSgETF1k+blQaYUBx9hJ9LOGP3gAVemcZlf1Kpo= golang.org/x/net v0.20.0/go.mod h1:z8BVo6PvndSri0LbOE3hAn0apkU+1YvI6E70E9jsnvY= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -196,8 +192,6 @@ golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.15.0 h1:h48lPFYpsTvQJZF4EKyI4aLHaev3CxivZmv7yZig9pc= -golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.16.0 h1:xWw16ngr6ZMtmxDyKyIgsE93KNKz5HKmMa3b8ALHidU= golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= diff --git a/backend/src/controllers/user.go b/backend/src/controllers/user.go index 612abd8b9..55bad7a97 100644 --- a/backend/src/controllers/user.go +++ b/backend/src/controllers/user.go @@ -3,7 +3,7 @@ package controllers import ( "github.com/GenerateNU/sac/backend/src/models" "github.com/GenerateNU/sac/backend/src/services" - "github.com/GenerateNU/sac/backend/src/models" + "github.com/GenerateNU/sac/backend/src/utilities" "github.com/gofiber/fiber/v2" ) @@ -47,7 +47,7 @@ func (u *UserController) GetAllUsers(c *fiber.Ctx) error { // @Failure 500 {string} string "internal server error" // @Router /api/v1/users/ [post] func (u *UserController) CreateUser(c *fiber.Ctx) error { - var userBody models.CreateUserRequestBody + var userBody models.UserRequestBody if err := c.BodyParser(&userBody); err != nil { return fiber.NewError(fiber.StatusBadRequest, "failed to process the request") @@ -59,6 +59,8 @@ func (u *UserController) CreateUser(c *fiber.Ctx) error { } return c.Status(fiber.StatusCreated).JSON(user) +} + // GetUser godoc // // @Summary Gets a user diff --git a/backend/src/services/user.go b/backend/src/services/user.go index f21f6f67d..52df779a0 100644 --- a/backend/src/services/user.go +++ b/backend/src/services/user.go @@ -13,7 +13,7 @@ import ( type UserServiceInterface interface { GetAllUsers() ([]models.User, error) - CreateUser(userBody models.CreateUserRequestBody) (*models.User, error) + CreateUser(userBody models.UserRequestBody) (*models.User, error) GetUser(id string) (*models.User, error) UpdateUser(id string, userBody models.UserRequestBody) (*models.User, error) } @@ -28,15 +28,20 @@ func (u *UserService) GetAllUsers() ([]models.User, error) { return transactions.GetAllUsers(u.DB) } -// temporary -func createUserFromRequestBody(userBody models.CreateUserRequestBody) (models.User, error) { - // TL DAVID -- some validation still needs to be done but depends on design - +func createUserFromRequestBody(userBody models.UserRequestBody) (*models.User, error) { validate := validator.New() + validate.RegisterValidation("neu_email", utilities.ValidateEmail) validate.RegisterValidation("password", utilities.ValidatePassword) + if err := validate.Struct(userBody); err != nil { - return models.User{}, fiber.NewError(fiber.StatusBadRequest, err.Error()) + return nil, fiber.NewError(fiber.StatusBadRequest, err.Error()) + } + + passwordHash, err := auth.ComputePasswordHash(userBody.Password) + + if err != nil { + return nil, fiber.NewError(fiber.StatusInternalServerError, err.Error()) } var user models.User @@ -44,24 +49,23 @@ func createUserFromRequestBody(userBody models.CreateUserRequestBody) (models.Us user.FirstName = userBody.FirstName user.LastName = userBody.LastName user.Email = userBody.Email - // TODO: hash - user.PasswordHash = userBody.Password + user.PasswordHash = *passwordHash user.College = models.College(userBody.College) user.Year = models.Year(userBody.Year) - return user, nil + return &user, nil } -func (u *UserService) CreateUser(userBody models.CreateUserRequestBody) (*models.User, error) { +func (u *UserService) CreateUser(userBody models.UserRequestBody) (*models.User, error) { user, err := createUserFromRequestBody(userBody) if err != nil { return nil, err } - return transactions.CreateUser(u.DB, &user) + return transactions.CreateUser(u.DB, user) } - - func (u *UserService) GetUser(id string) (*models.User, error) { + +func (u *UserService) GetUser(id string) (*models.User, error) { idAsUint, err := utilities.ValidateID(id) if err != nil { return nil, fiber.ErrBadRequest @@ -70,7 +74,6 @@ func (u *UserService) CreateUser(userBody models.CreateUserRequestBody) (*models return transactions.GetUser(u.DB, *idAsUint) } -// Updates a user func (u *UserService) UpdateUser(id string, userBody models.UserRequestBody) (*models.User, error) { idAsUint, err := utilities.ValidateID(id) if err != nil { diff --git a/backend/src/transactions/user.go b/backend/src/transactions/user.go index 179719ea4..3d69fecb6 100644 --- a/backend/src/transactions/user.go +++ b/backend/src/transactions/user.go @@ -4,7 +4,6 @@ import ( "errors" "github.com/GenerateNU/sac/backend/src/models" - "errors" "github.com/gofiber/fiber/v2" "gorm.io/gorm" ) @@ -33,7 +32,7 @@ func GetUser(db *gorm.DB, id uint) (*models.User, error) { } func CreateUser(db *gorm.DB, user *models.User) (*models.User, error) { - + var existing models.User if err := db.Where("email = ?", user.Email).First(&existing).Error; err != nil { @@ -51,7 +50,7 @@ func CreateUser(db *gorm.DB, user *models.User) (*models.User, error) { } else { return nil, fiber.NewError(fiber.StatusBadRequest, "user with that nuid already exists") } - + if err := db.Create(user).Error; err != nil { return nil, fiber.NewError(fiber.StatusInternalServerError, "failed to create user") } diff --git a/backend/src/utilities/validator.go b/backend/src/utilities/validator.go index 3514a9c43..b032c1780 100644 --- a/backend/src/utilities/validator.go +++ b/backend/src/utilities/validator.go @@ -29,16 +29,6 @@ func ValidateEmail(fl validator.FieldLevel) bool { } func ValidatePassword(fl validator.FieldLevel) bool { - // TODO: we need to think of validation rules - return len(fl.Field().String()) >= 6 -} - -// Validate the data sent to the server if the data is invalid, return an error otherwise, return nil -func ValidateData(model interface{}) error { - validate := validator.New(validator.WithRequiredStructEnabled()) - if err := validate.Struct(model); err != nil { - return err - } if len(fl.Field().String()) < 8 { return false } diff --git a/backend/tests/api/user_test.go b/backend/tests/api/user_test.go index c021acdfd..2bc0c3048 100644 --- a/backend/tests/api/user_test.go +++ b/backend/tests/api/user_test.go @@ -1,10 +1,9 @@ package tests import ( - "fmt" - "math/rand" "bytes" "fmt" + "math/rand" "net/http" "net/http/httptest" @@ -12,8 +11,8 @@ import ( "github.com/GenerateNU/sac/backend/src/models" "github.com/GenerateNU/sac/backend/src/transactions" - "github.com/huandu/go-assert" "github.com/goccy/go-json" + "github.com/huandu/go-assert" ) func TestGetAllUsersWorks(t *testing.T) { @@ -530,8 +529,7 @@ func TestCreateUserFailsOnMissingField(t *testing.T) { } // map from json field name to struct field name - fields := map[string]string{"first_name":"FirstName", "last_name":"LastName", "email":"Email", "password":"Password", "college":"College", "year":"Year", "nuid":"NUID"} - + fields := map[string]string{"first_name": "FirstName", "last_name": "LastName", "email": "Email", "password": "Password", "college": "College", "year": "Year", "nuid": "NUID"} //loops through each field and removes it from the body then tests that the post fails and returns a 400 for structKey, jsonKey := range fields { @@ -543,7 +541,7 @@ func TestCreateUserFailsOnMissingField(t *testing.T) { } } - expectedMesage := "Key: 'CreateUserRequestBody." +jsonKey + "' Error:Field validation for '" + jsonKey + "' failed on the 'required' tag" + expectedMesage := "Key: 'CreateUserRequestBody." + jsonKey + "' Error:Field validation for '" + jsonKey + "' failed on the 'required' tag" appAssert := CreateInvalidUser(t, updatedBody, expectedMesage) appAssert.App.DropDB() @@ -552,14 +550,12 @@ func TestCreateUserFailsOnMissingField(t *testing.T) { } /* - Dear TLs David, Garret, I can not for the life of me figure out how to do this test. It has become the bane of me I also have not touched nor smelled the notion of "grass" in the last 27 hours Please forgive me. I am but a humble student - Olivier - */ func TestCreateUserFailsOnExtraFields(t *testing.T) { // tests that: