diff --git a/backend/src/controllers/club.go b/backend/src/controllers/club.go new file mode 100644 index 000000000..4eab57c55 --- /dev/null +++ b/backend/src/controllers/club.go @@ -0,0 +1,77 @@ +package controllers + +import ( + "strconv" + + "github.com/GenerateNU/sac/backend/src/errors" + "github.com/GenerateNU/sac/backend/src/models" + "github.com/GenerateNU/sac/backend/src/services" + "github.com/gofiber/fiber/v2" +) + +type ClubController struct { + clubService services.ClubServiceInterface +} + +func NewClubController(clubService services.ClubServiceInterface) *ClubController { + return &ClubController{clubService: clubService} +} + +func (l *ClubController) GetAllClubs(c *fiber.Ctx) error { + defaultLimit := 10 + defaultPage := 1 + + clubs, err := l.clubService.GetClubs(c.Query("limit", strconv.Itoa(defaultLimit)), c.Query("page", strconv.Itoa(defaultPage))) + if err != nil { + return err.FiberError(c) + } + + return c.Status(fiber.StatusOK).JSON(clubs) +} + +func (l *ClubController) CreateClub(c *fiber.Ctx) error { + var clubBody models.CreateClubRequestBody + if err := c.BodyParser(&clubBody); err != nil { + return errors.FailedToParseRequestBody.FiberError(c) + } + + club, err := l.clubService.CreateClub(clubBody) + if err != nil { + return err.FiberError(c) + } + + return c.Status(fiber.StatusCreated).JSON(club) +} + +func (l *ClubController) GetClub(c *fiber.Ctx) error { + club, err := l.clubService.GetClub(c.Params("id")) + if err != nil { + return err.FiberError(c) + } + + return c.Status(fiber.StatusOK).JSON(club) +} + +func (l *ClubController) UpdateClub(c *fiber.Ctx) error { + var clubBody models.UpdateClubRequestBody + + if err := c.BodyParser(&clubBody); err != nil { + return errors.FailedToParseRequestBody.FiberError(c) + } + + updatedClub, err := l.clubService.UpdateClub(c.Params("id"), clubBody) + if err != nil { + return err.FiberError(c) + } + + return c.Status(fiber.StatusOK).JSON(updatedClub) +} + +func (l *ClubController) DeleteClub(c *fiber.Ctx) error { + err := l.clubService.DeleteClub(c.Params("id")) + if err != nil { + return err.FiberError(c) + } + + return c.SendStatus(fiber.StatusNoContent) +} diff --git a/backend/src/database/db.go b/backend/src/database/db.go index 2d8d3b9bf..d7c131a71 100644 --- a/backend/src/database/db.go +++ b/backend/src/database/db.go @@ -113,9 +113,16 @@ func createSuperUser(settings config.Settings, db *gorm.DB) error { } superClub := models.Club{ - Name: "SAC", - Preview: "SAC", - Description: "SAC", + Name: "SAC", + Preview: "SAC", + Description: "SAC", + NumMembers: 0, + IsRecruiting: true, + RecruitmentCycle: models.RecruitmentCycle(models.Always), + RecruitmentType: models.Application, + ApplicationLink: "https://generatenu.com/apply", + Logo: "https://aws.amazon.com/s3", + Admin: []models.User{superUser}, } if err := tx.Create(&superClub).Error; err != nil { tx.Rollback() diff --git a/backend/src/errors/club.go b/backend/src/errors/club.go new file mode 100644 index 000000000..226c44890 --- /dev/null +++ b/backend/src/errors/club.go @@ -0,0 +1,38 @@ +package errors + +import "github.com/gofiber/fiber/v2" + +var ( + FailedToValidateUserID = Error{ + StatusCode: fiber.StatusBadRequest, + Message: "failed to validate user id", + } + FailedToValidateClub = Error{ + StatusCode: fiber.StatusBadRequest, + Message: "failed to validate club", + } + FailedToCreateClub = Error{ + StatusCode: fiber.StatusInternalServerError, + Message: "failed to create club", + } + FailedToGetClubs = Error{ + StatusCode: fiber.StatusInternalServerError, + Message: "failed to get clubs", + } + FailedToGetClub = Error{ + StatusCode: fiber.StatusInternalServerError, + Message: "failed to get club", + } + FailedToDeleteClub = Error{ + StatusCode: fiber.StatusInternalServerError, + Message: "failed to delete club", + } + FailedToUpdateClub = Error{ + StatusCode: fiber.StatusInternalServerError, + Message: "failed to update club", + } + ClubNotFound = Error{ + StatusCode: fiber.StatusNotFound, + Message: "club not found", + } +) diff --git a/backend/src/models/club.go b/backend/src/models/club.go index 01542a7be..353a56f47 100644 --- a/backend/src/models/club.go +++ b/backend/src/models/club.go @@ -1,9 +1,8 @@ package models import ( - "time" - "github.com/google/uuid" + "gorm.io/gorm" ) type RecruitmentCycle string @@ -26,21 +25,22 @@ const ( type Club struct { Model - SoftDeletedAt time.Time `gorm:"type:timestamptz;default:NULL" json:"-" validate:"-"` + SoftDeletedAt gorm.DeletedAt `gorm:"type:timestamptz;default:NULL" json:"-" validate:"-"` Name string `gorm:"type:varchar(255)" json:"name" validate:"required,max=255"` Preview string `gorm:"type:varchar(255)" json:"preview" validate:"required,max=255"` - Description string `gorm:"type:varchar(255)" json:"description" validate:"required,url,max=255"` // MongoDB URL + Description string `gorm:"type:varchar(255)" json:"description" validate:"required,http_url,mongo_url,max=255"` // MongoDB URL NumMembers int `gorm:"type:int" json:"num_members" validate:"required,min=1"` IsRecruiting bool `gorm:"type:bool;default:false" json:"is_recruiting" validate:"required"` - RecruitmentCycle RecruitmentCycle `gorm:"type:varchar(255);default:always" json:"recruitment_cycle" validate:"required,max=255"` - RecruitmentType RecruitmentType `gorm:"type:varchar(255);default:unrestricted" json:"recruitment_type" validate:"required,max=255"` - ApplicationLink string `gorm:"type:varchar(255);default:NULL" json:"application_link" validate:"required,max=255"` - Logo string `gorm:"type:varchar(255);default:NULL" json:"logo" validate:"url,max=255"` // S3 URL + RecruitmentCycle RecruitmentCycle `gorm:"type:varchar(255);default:always" json:"recruitment_cycle" validate:"required,max=255,oneof=fall spring fallSpring always"` + RecruitmentType RecruitmentType `gorm:"type:varchar(255);default:unrestricted" json:"recruitment_type" validate:"required,max=255,oneof=unrestricted tryout application"` + ApplicationLink string `gorm:"type:varchar(255);default:NULL" json:"application_link" validate:"required,max=255,http_url"` + Logo string `gorm:"type:varchar(255);default:NULL" json:"logo" validate:"omitempty,http_url,s3_url,max=255"` // S3 URL Parent *uuid.UUID `gorm:"foreignKey:Parent" json:"-" validate:"uuid4"` Tag []Tag `gorm:"many2many:club_tags;constraint:OnUpdate:CASCADE,OnDelete:CASCADE;" json:"-" validate:"-"` // User + Admin []User `gorm:"many2many:user_club_admins;constraint:OnUpdate:CASCADE,OnDelete:CASCADE;" json:"-" validate:"required"` Member []User `gorm:"many2many:user_club_members;constraint:OnUpdate:CASCADE,OnDelete:CASCADE;" json:"-" validate:"required"` Follower []User `gorm:"many2many:user_club_followers;constraint:OnUpdate:CASCADE,OnDelete:CASCADE;" json:"-" validate:"-"` IntendedApplicant []User `gorm:"many2many:user_club_intended_applicants;constraint:OnUpdate:CASCADE,OnDelete:CASCADE;" json:"-" validate:"-"` @@ -51,3 +51,28 @@ type Club struct { Event []Event `gorm:"many2many:club_events;constraint:OnUpdate:CASCADE,OnDelete:CASCADE;" json:"-" validate:"-"` Notifcation []Notification `gorm:"polymorphic:Reference;constraint:OnUpdate:CASCADE,OnDelete:CASCADE;" json:"-" validate:"-"` } + +type CreateClubRequestBody struct { + UserID uuid.UUID `json:"user_id" validate:"required,uuid4"` + Name string `json:"name" validate:"required,max=255"` + Preview string `json:"preview" validate:"required,max=255"` + Description string `json:"description" validate:"required,http_url,mongo_url,max=255"` // MongoDB URL + NumMembers int `json:"num_members" validate:"required,min=1"` + IsRecruiting bool `json:"is_recruiting" validate:"required"` + RecruitmentCycle RecruitmentCycle `gorm:"type:varchar(255);default:always" json:"recruitment_cycle" validate:"required,max=255,oneof=fall spring fallSpring always"` + RecruitmentType RecruitmentType `gorm:"type:varchar(255);default:unrestricted" json:"recruitment_type" validate:"required,max=255,oneof=unrestricted tryout application"` + ApplicationLink string `json:"application_link" validate:"required,max=255,http_url"` + Logo string `json:"logo" validate:"omitempty,http_url,s3_url,max=255"` // S3 URL +} + +type UpdateClubRequestBody struct { + Name string `json:"name" validate:"omitempty,max=255"` + Preview string `json:"preview" validate:"omitempty,max=255"` + Description string `json:"description" validate:"omitempty,http_url,mongo_url,max=255"` // MongoDB URL + NumMembers int `json:"num_members" validate:"omitempty,min=1"` + IsRecruiting bool `json:"is_recruiting" validate:"omitempty"` + RecruitmentCycle RecruitmentCycle `gorm:"type:varchar(255);default:always" json:"recruitment_cycle" validate:"required,max=255,oneof=fall spring fallSpring always"` + RecruitmentType RecruitmentType `gorm:"type:varchar(255);default:unrestricted" json:"recruitment_type" validate:"required,max=255,oneof=unrestricted tryout application"` + ApplicationLink string `json:"application_link" validate:"omitempty,required,max=255,http_url"` + Logo string `json:"logo" validate:"omitempty,http_url,s3_url,max=255"` // S3 URL +} diff --git a/backend/src/models/contact.go b/backend/src/models/contact.go index 93e2b2bce..527ad678d 100644 --- a/backend/src/models/contact.go +++ b/backend/src/models/contact.go @@ -18,7 +18,7 @@ type Contact struct { Model Type Media `gorm:"type:varchar(255)" json:"type" validate:"required,max=255"` - Content string `gorm:"type:varchar(255)" json:"content" validate:"required,url,max=255"` // media URL + Content string `gorm:"type:varchar(255)" json:"content" validate:"required,http_url,max=255"` // media URL ClubID uuid.UUID `gorm:"foreignKey:ClubID" json:"-" validate:"uuid4"` } diff --git a/backend/src/models/notification.go b/backend/src/models/notification.go index d20db3288..fd07e4580 100644 --- a/backend/src/models/notification.go +++ b/backend/src/models/notification.go @@ -20,7 +20,7 @@ type Notification struct { Title string `gorm:"type:varchar(255)" json:"title" validate:"required,max=255"` Content string `gorm:"type:varchar(255)" json:"content" validate:"required,max=255"` DeepLink string `gorm:"type:varchar(255)" json:"deep_link" validate:"required,max=255"` - Icon string `gorm:"type:varchar(255)" json:"icon" validate:"required,url,max=255"` // S3 URL + Icon string `gorm:"type:varchar(255)" json:"icon" validate:"required,http_url,max=255"` // S3 URL ReferenceID uuid.UUID `gorm:"type:int" json:"-" validate:"uuid4"` ReferenceType NotificationType `gorm:"type:varchar(255)" json:"-" validate:"max=255"` diff --git a/backend/src/models/point_of_contact.go b/backend/src/models/point_of_contact.go index a6b6ffab1..826630306 100644 --- a/backend/src/models/point_of_contact.go +++ b/backend/src/models/point_of_contact.go @@ -7,7 +7,7 @@ type PointOfContact struct { Name string `gorm:"type:varchar(255)" json:"name" validate:"required,max=255"` Email string `gorm:"type:varchar(255)" json:"email" validate:"required,email,max=255"` - Photo string `gorm:"type:varchar(255);default:NULL" json:"photo" validate:"url,max=255"` // S3 URL, fallback to default logo if null + Photo string `gorm:"type:varchar(255);default:NULL" json:"photo" validate:"http_url,max=255"` // S3 URL, fallback to default logo if null Position string `gorm:"type:varchar(255);" json:"position" validate:"required,max=255"` ClubID uuid.UUID `gorm:"foreignKey:ClubID" json:"-" validate:"uuid4"` diff --git a/backend/src/server/server.go b/backend/src/server/server.go index 4d622d646..a4181a4f9 100644 --- a/backend/src/server/server.go +++ b/backend/src/server/server.go @@ -35,6 +35,7 @@ func Init(db *gorm.DB) *fiber.App { apiv1 := app.Group("/api/v1") userRoutes(apiv1, &services.UserService{DB: db, Validate: validate}) + clubRoutes(apiv1, &services.ClubService{DB: db, Validate: validate}) categoryRoutes(apiv1, &services.CategoryService{DB: db, Validate: validate}) tagRoutes(apiv1, &services.TagService{DB: db, Validate: validate}) @@ -75,6 +76,18 @@ func userRoutes(router fiber.Router, userService services.UserServiceInterface) users.Delete("/:id", userController.DeleteUser) } +func clubRoutes(router fiber.Router, clubService services.ClubServiceInterface) { + clubController := controllers.NewClubController(clubService) + + clubs := router.Group("/clubs") + + clubs.Get("/", clubController.GetAllClubs) + clubs.Post("/", clubController.CreateClub) + clubs.Get("/:id", clubController.GetClub) + clubs.Patch("/:id", clubController.UpdateClub) + clubs.Delete("/:id", clubController.DeleteClub) +} + func categoryRoutes(router fiber.Router, categoryService services.CategoryServiceInterface) { categoryController := controllers.NewCategoryController(categoryService) diff --git a/backend/src/services/club.go b/backend/src/services/club.go new file mode 100644 index 000000000..fa93877e3 --- /dev/null +++ b/backend/src/services/club.go @@ -0,0 +1,91 @@ +package services + +import ( + "github.com/GenerateNU/sac/backend/src/errors" + "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" + + "gorm.io/gorm" +) + +type ClubServiceInterface interface { + GetClubs(limit string, page string) ([]models.Club, *errors.Error) + GetClub(id string) (*models.Club, *errors.Error) + CreateClub(clubBody models.CreateClubRequestBody) (*models.Club, *errors.Error) + UpdateClub(id string, clubBody models.UpdateClubRequestBody) (*models.Club, *errors.Error) + DeleteClub(id string) *errors.Error +} + +type ClubService struct { + DB *gorm.DB + Validate *validator.Validate +} + +func (c *ClubService) GetClubs(limit string, page string) ([]models.Club, *errors.Error) { + limitAsInt, err := utilities.ValidateNonNegative(limit) + + if err != nil { + return nil, &errors.FailedToValidateLimit + } + + pageAsInt, err := utilities.ValidateNonNegative(page) + + if err != nil { + return nil, &errors.FailedToValidatePage + } + + offset := (*pageAsInt - 1) * *limitAsInt + + return transactions.GetClubs(c.DB, *limitAsInt, offset) +} + +func (c *ClubService) CreateClub(clubBody models.CreateClubRequestBody) (*models.Club, *errors.Error) { + if err := c.Validate.Struct(clubBody); err != nil { + return nil, &errors.FailedToValidateClub + } + + club, err := utilities.MapRequestToModel(clubBody, &models.Club{}) + if err != nil { + return nil, &errors.FailedToMapRequestToModel + } + + return transactions.CreateClub(c.DB, clubBody.UserID, *club) +} + +func (c *ClubService) GetClub(id string) (*models.Club, *errors.Error) { + idAsUUID, err := utilities.ValidateID(id) + if err != nil { + return nil, &errors.FailedToValidateID + } + + return transactions.GetClub(c.DB, *idAsUUID) +} + +func (c *ClubService) UpdateClub(id string, clubBody models.UpdateClubRequestBody) (*models.Club, *errors.Error) { + idAsUUID, idErr := utilities.ValidateID(id) + if idErr != nil { + return nil, idErr + } + + if err := c.Validate.Struct(clubBody); err != nil { + return nil, &errors.FailedToValidateClub + } + + club, err := utilities.MapRequestToModel(clubBody, &models.Club{}) + if err != nil { + return nil, &errors.FailedToMapRequestToModel + } + + return transactions.UpdateClub(c.DB, *idAsUUID, *club) +} + +func (c *ClubService) DeleteClub(id string) *errors.Error { + idAsUUID, err := utilities.ValidateID(id) + if err != nil { + return &errors.FailedToValidateID + } + + return transactions.DeleteClub(c.DB, *idAsUUID) +} diff --git a/backend/src/transactions/club.go b/backend/src/transactions/club.go new file mode 100644 index 000000000..18413bd26 --- /dev/null +++ b/backend/src/transactions/club.go @@ -0,0 +1,99 @@ +package transactions + +import ( + stdliberrors "errors" + + "github.com/GenerateNU/sac/backend/src/errors" + "github.com/GenerateNU/sac/backend/src/models" + "github.com/google/uuid" + + "gorm.io/gorm" +) + +func GetClubs(db *gorm.DB, limit int, offset int) ([]models.Club, *errors.Error) { + var clubs []models.Club + result := db.Limit(limit).Offset(offset).Find(&clubs) + if result.Error != nil { + return nil, &errors.FailedToGetClubs + } + + return clubs, nil +} + +func CreateClub(db *gorm.DB, userId uuid.UUID, club models.Club) (*models.Club, *errors.Error) { + user, err := GetUser(db, userId) + if err != nil { + return nil, &errors.UserNotFound + } + + tx := db.Begin() + + if err := tx.Create(&club).Error; err != nil { + tx.Rollback() + return nil, &errors.FailedToCreateClub + } + + if err := tx.Model(&club).Association("Admin").Append(user); err != nil { + tx.Rollback() + return nil, &errors.FailedToCreateClub + } + + if err := tx.Commit().Error; err != nil { + tx.Rollback() + return nil, &errors.FailedToCreateClub + } + + return &club, nil +} + +func GetClub(db *gorm.DB, id uuid.UUID) (*models.Club, *errors.Error) { + var club models.Club + if err := db.First(&club, id).Error; err != nil { + if stdliberrors.Is(err, gorm.ErrRecordNotFound) { + return nil, &errors.ClubNotFound + } else { + return nil, &errors.FailedToGetClub + } + } + + return &club, nil +} + +func UpdateClub(db *gorm.DB, id uuid.UUID, club models.Club) (*models.Club, *errors.Error) { + result := db.Model(&models.User{}).Where("id = ?", id).Updates(club) + if result.Error != nil { + if stdliberrors.Is(result.Error, gorm.ErrRecordNotFound) { + return nil, &errors.UserNotFound + } else { + return nil, &errors.FailedToUpdateClub + } + } + var existingClub models.Club + + err := db.First(&existingClub, id).Error + if err != nil { + if stdliberrors.Is(err, gorm.ErrRecordNotFound) { + return nil, &errors.ClubNotFound + } else { + return nil, &errors.FailedToCreateClub + } + } + + if err := db.Model(&existingClub).Updates(&club).Error; err != nil { + return nil, &errors.FailedToUpdateUser + } + + return &existingClub, nil +} + +func DeleteClub(db *gorm.DB, id uuid.UUID) *errors.Error { + if result := db.Delete(&models.Club{}, id); result.RowsAffected == 0 { + if result.Error == nil { + return &errors.ClubNotFound + } else { + return &errors.FailedToDeleteClub + } + } + + return nil +} diff --git a/backend/src/utilities/manipulator.go b/backend/src/utilities/manipulator.go index ff0a1d791..64486b93e 100644 --- a/backend/src/utilities/manipulator.go +++ b/backend/src/utilities/manipulator.go @@ -4,6 +4,7 @@ import ( "github.com/mitchellh/mapstructure" ) + // MapRequestToModel maps request data to a target model using mapstructure func MapRequestToModel[T any, U any](responseData T, targetModel *U) (*U, error) { config := &mapstructure.DecoderConfig{ diff --git a/backend/src/utilities/validator.go b/backend/src/utilities/validator.go index 5ec5d71d3..e4c6bafeb 100644 --- a/backend/src/utilities/validator.go +++ b/backend/src/utilities/validator.go @@ -12,11 +12,13 @@ import ( ) func RegisterCustomValidators(validate *validator.Validate) { - validate.RegisterValidation("neu_email", ValidateEmail) - validate.RegisterValidation("password", ValidatePassword) + validate.RegisterValidation("neu_email", validateEmail) + validate.RegisterValidation("password", validatePassword) + validate.RegisterValidation("mongo_url", validateMongoURL) + validate.RegisterValidation("s3_url", validateS3URL) } -func ValidateEmail(fl validator.FieldLevel) bool { +func validateEmail(fl validator.FieldLevel) bool { email, err := emailaddress.Parse(fl.Field().String()) if err != nil { return false @@ -29,7 +31,7 @@ func ValidateEmail(fl validator.FieldLevel) bool { return true } -func ValidatePassword(fl validator.FieldLevel) bool { +func validatePassword(fl validator.FieldLevel) bool { if len(fl.Field().String()) < 8 { return false } @@ -38,7 +40,14 @@ func ValidatePassword(fl validator.FieldLevel) bool { return specialCharactersMatch && numbersMatch } -// Validates that an id follows postgres uint format, returns a uint otherwise returns an error +func validateMongoURL(fl validator.FieldLevel) bool { + return true +} + +func validateS3URL(fl validator.FieldLevel) bool { + return true +} + func ValidateID(id string) (*uuid.UUID, *errors.Error) { idAsUUID, err := uuid.Parse(id) @@ -49,6 +58,7 @@ func ValidateID(id string) (*uuid.UUID, *errors.Error) { return &idAsUUID, nil } + func ValidateNonNegative(value string) (*int, *errors.Error) { valueAsInt, err := strconv.Atoi(value) diff --git a/backend/tests/api/category_test.go b/backend/tests/api/category_test.go index 2d22fcb7a..f96a2abe7 100644 --- a/backend/tests/api/category_test.go +++ b/backend/tests/api/category_test.go @@ -116,6 +116,10 @@ func TestCreateCategoryIgnoresid(t *testing.T) { ).Close() } +func Assert1Category(app TestApp, assert *assert.A, resp *http.Response) { + AssertNumCategoriesRemainsAtN(app, assert, resp, 1) +} + func AssertNoCategories(app TestApp, assert *assert.A, resp *http.Response) { AssertNumCategoriesRemainsAtN(app, assert, resp, 0) } @@ -137,7 +141,7 @@ func TestCreateCategoryFailsIfNameIsNotString(t *testing.T) { Body: &map[string]interface{}{ "name": 1231, }, - }.TestOnStatusMessageAndDB(t, nil, + }.TestOnErrorAndDB(t, nil, ErrorWithDBTester{ Error: errors.FailedToParseRequestBody, DBTester: AssertNoCategories, @@ -150,7 +154,7 @@ func TestCreateCategoryFailsIfNameIsMissing(t *testing.T) { Method: fiber.MethodPost, Path: "/api/v1/categories/", Body: &map[string]interface{}{}, - }.TestOnStatusMessageAndDB(t, nil, + }.TestOnErrorAndDB(t, nil, ErrorWithDBTester{ Error: errors.FailedToValidateCategory, DBTester: AssertNoCategories, @@ -173,7 +177,7 @@ func TestCreateCategoryFailsIfCategoryWithThatNameAlreadyExists(t *testing.T) { Method: fiber.MethodPost, Path: "/api/v1/categories/", Body: &modifiedSampleCategoryBody, - }.TestOnStatusMessageAndDB(t, &existingAppAssert, + }.TestOnErrorAndDB(t, &existingAppAssert, ErrorWithDBTester{ Error: errors.CategoryAlreadyExists, DBTester: TestNumCategoriesRemainsAt1, @@ -362,6 +366,8 @@ func TestDeleteCategoryWorks(t *testing.T) { } func TestDeleteCategoryFailsBadRequest(t *testing.T) { + existingAppAssert, _ := CreateSampleCategory(t, nil) + badRequests := []string{ "0", "-1", @@ -374,13 +380,27 @@ func TestDeleteCategoryFailsBadRequest(t *testing.T) { TestRequest{ Method: fiber.MethodDelete, Path: fmt.Sprintf("/api/v1/categories/%s", badRequest), - }.TestOnError(t, nil, errors.FailedToValidateID).Close() + }.TestOnErrorAndDB(t, &existingAppAssert, + ErrorWithDBTester{ + Error: errors.FailedToValidateID, + DBTester: Assert1Category, + }, + ) } + + existingAppAssert.Close() } func TestDeleteCategoryFailsNotFound(t *testing.T) { + existingAppAssert, _ := CreateSampleCategory(t, nil) + TestRequest{ Method: fiber.MethodDelete, Path: fmt.Sprintf("/api/v1/categories/%s", uuid.New()), - }.TestOnError(t, nil, errors.CategoryNotFound).Close() + }.TestOnErrorAndDB(t, &existingAppAssert, + ErrorWithDBTester{ + Error: errors.CategoryNotFound, + DBTester: Assert1Category, + }, + ).Close() } diff --git a/backend/tests/api/club_test.go b/backend/tests/api/club_test.go new file mode 100644 index 000000000..fb78e4fd2 --- /dev/null +++ b/backend/tests/api/club_test.go @@ -0,0 +1,478 @@ +package tests + +import ( + stdliberrors "errors" + "fmt" + "net/http" + "testing" + + "github.com/GenerateNU/sac/backend/src/errors" + "github.com/GenerateNU/sac/backend/src/models" + "github.com/goccy/go-json" + "github.com/gofiber/fiber/v2" + "github.com/google/uuid" + "github.com/huandu/go-assert" + "gorm.io/gorm" +) + +func SampleClubFactory(userID uuid.UUID) *map[string]interface{} { + return &map[string]interface{}{ + "user_id": userID, + "name": "Generate", + "preview": "Generate is Northeastern's premier student-led product development studio.", + "description": "https://mongodb.com", + "num_members": 1, + "is_recruiting": true, + "recruitment_cycle": "always", + "recruitment_type": "application", + "application_link": "https://generatenu.com/apply", + "logo": "https://aws.amazon.com/s3/", + } +} + +func AssertClubBodyRespDB(app TestApp, assert *assert.A, resp *http.Response, body *map[string]interface{}) uuid.UUID { + var respClub models.Club + + err := json.NewDecoder(resp.Body).Decode(&respClub) + + assert.NilError(err) + + var dbClubs []models.Club + + err = app.Conn.Order("created_at desc").Find(&dbClubs).Error + + assert.NilError(err) + + assert.Equal(2, len(dbClubs)) + + dbClub := dbClubs[0] + + assert.Equal(dbClub.ID, respClub.ID) + assert.Equal(dbClub.Name, respClub.Name) + assert.Equal(dbClub.Preview, respClub.Preview) + assert.Equal(dbClub.Description, respClub.Description) + assert.Equal(dbClub.NumMembers, respClub.NumMembers) + assert.Equal(dbClub.IsRecruiting, respClub.IsRecruiting) + assert.Equal(dbClub.RecruitmentCycle, respClub.RecruitmentCycle) + assert.Equal(dbClub.RecruitmentType, respClub.RecruitmentType) + assert.Equal(dbClub.ApplicationLink, respClub.ApplicationLink) + assert.Equal(dbClub.Logo, respClub.Logo) + + var dbAdmins []models.User + + err = app.Conn.Model(&dbClub).Association("Admin").Find(&dbAdmins) + + assert.NilError(err) + + assert.Equal(1, len(dbAdmins)) + + assert.Equal((*body)["user_id"].(uuid.UUID), dbAdmins[0].ID) + assert.Equal((*body)["name"].(string), dbClub.Name) + assert.Equal((*body)["preview"].(string), dbClub.Preview) + assert.Equal((*body)["description"].(string), dbClub.Description) + assert.Equal((*body)["num_members"].(int), dbClub.NumMembers) + assert.Equal((*body)["is_recruiting"].(bool), dbClub.IsRecruiting) + assert.Equal(models.RecruitmentCycle((*body)["recruitment_cycle"].(string)), dbClub.RecruitmentCycle) + assert.Equal(models.RecruitmentType((*body)["recruitment_type"].(string)), dbClub.RecruitmentType) + assert.Equal((*body)["application_link"].(string), dbClub.ApplicationLink) + assert.Equal((*body)["logo"].(string), dbClub.Logo) + + return dbClub.ID +} + +func AssertClubWithBodyRespDBMostRecent(app TestApp, assert *assert.A, resp *http.Response, body *map[string]interface{}) uuid.UUID { + var respClub models.Club + + err := json.NewDecoder(resp.Body).Decode(&respClub) + + assert.NilError(err) + + var dbClub models.Club + + err = app.Conn.Order("created_at desc").First(&dbClub).Error + + assert.NilError(err) + + assert.Equal(dbClub.ID, respClub.ID) + assert.Equal(dbClub.Name, respClub.Name) + assert.Equal(dbClub.Preview, respClub.Preview) + assert.Equal(dbClub.Description, respClub.Description) + assert.Equal(dbClub.NumMembers, respClub.NumMembers) + assert.Equal(dbClub.IsRecruiting, respClub.IsRecruiting) + assert.Equal(dbClub.RecruitmentCycle, respClub.RecruitmentCycle) + assert.Equal(dbClub.RecruitmentType, respClub.RecruitmentType) + assert.Equal(dbClub.ApplicationLink, respClub.ApplicationLink) + assert.Equal(dbClub.Logo, respClub.Logo) + + var dbAdmins []models.User + + err = app.Conn.Model(&dbClub).Association("Admins").Find(&dbAdmins) + + assert.NilError(err) + + assert.Equal(1, len(dbAdmins)) + + dbAdmin := dbAdmins[0] + + assert.Equal((*body)["user_id"].(uuid.UUID), dbAdmin.ID) + assert.Equal((*body)["name"].(string), dbClub.Name) + assert.Equal((*body)["preview"].(string), dbClub.Preview) + assert.Equal((*body)["description"].(string), dbClub.Description) + assert.Equal((*body)["num_members"].(int), dbClub.NumMembers) + assert.Equal((*body)["is_recruiting"].(bool), dbClub.IsRecruiting) + assert.Equal((*body)["recruitment_cycle"].(string), dbClub.RecruitmentCycle) + assert.Equal((*body)["recruitment_type"].(string), dbClub.RecruitmentType) + assert.Equal((*body)["application_link"].(string), dbClub.ApplicationLink) + assert.Equal((*body)["logo"].(string), dbClub.Logo) + + return dbClub.ID +} + +func AssertSampleClubBodyRespDB(app TestApp, assert *assert.A, resp *http.Response, userID uuid.UUID) uuid.UUID { + return AssertClubBodyRespDB(app, assert, resp, SampleClubFactory(userID)) +} + +func CreateSampleClub(t *testing.T, existingAppAssert *ExistingAppAssert) (eaa ExistingAppAssert, userUUID uuid.UUID, clubUUID uuid.UUID) { + appAssert, userID := CreateSampleUser(t, existingAppAssert) + + var sampleClubUUID uuid.UUID + + newAppAssert := TestRequest{ + Method: fiber.MethodPost, + Path: "/api/v1/clubs/", + Body: SampleClubFactory(userID), + }.TestOnStatusAndDB(t, &appAssert, + DBTesterWithStatus{ + Status: fiber.StatusCreated, + DBTester: func(app TestApp, assert *assert.A, resp *http.Response) { + sampleClubUUID = AssertSampleClubBodyRespDB(app, assert, resp, userID) + }, + }, + ) + + if existingAppAssert == nil { + return newAppAssert, userID, sampleClubUUID + } else { + return *existingAppAssert, userID, sampleClubUUID + } +} + +func TestCreateClubWorks(t *testing.T) { + existingAppAssert, _, _ := CreateSampleClub(t, nil) + existingAppAssert.Close() +} + +func TestGetClubsWorks(t *testing.T) { + TestRequest{ + Method: fiber.MethodGet, + Path: "/api/v1/clubs/", + }.TestOnStatusAndDB(t, nil, + DBTesterWithStatus{ + Status: fiber.StatusOK, + DBTester: func(app TestApp, assert *assert.A, resp *http.Response) { + var respClubs []models.Club + + err := json.NewDecoder(resp.Body).Decode(&respClubs) + + assert.NilError(err) + + assert.Equal(1, len(respClubs)) + + respClub := respClubs[0] + + var dbClubs []models.Club + + err = app.Conn.Order("created_at desc").Find(&dbClubs).Error + + assert.NilError(err) + + assert.Equal(1, len(dbClubs)) + + dbClub := dbClubs[0] + + assert.Equal(dbClub.ID, respClub.ID) + assert.Equal(dbClub.Name, respClub.Name) + assert.Equal(dbClub.Preview, respClub.Preview) + assert.Equal(dbClub.Description, respClub.Description) + assert.Equal(dbClub.NumMembers, respClub.NumMembers) + assert.Equal(dbClub.IsRecruiting, respClub.IsRecruiting) + assert.Equal(dbClub.RecruitmentCycle, respClub.RecruitmentCycle) + assert.Equal(dbClub.RecruitmentType, respClub.RecruitmentType) + assert.Equal(dbClub.ApplicationLink, respClub.ApplicationLink) + assert.Equal(dbClub.Logo, respClub.Logo) + + assert.Equal("SAC", dbClub.Name) + assert.Equal("SAC", dbClub.Preview) + assert.Equal("SAC", dbClub.Description) + assert.Equal(1, dbClub.NumMembers) + assert.Equal(true, dbClub.IsRecruiting) + assert.Equal(models.RecruitmentCycle(models.Always), dbClub.RecruitmentCycle) + assert.Equal(models.RecruitmentType(models.Application), dbClub.RecruitmentType) + assert.Equal("https://generatenu.com/apply", dbClub.ApplicationLink) + assert.Equal("https://aws.amazon.com/s3", dbClub.Logo) + }, + }, + ).Close() +} + +func AssertNumClubsRemainsAtN(app TestApp, assert *assert.A, resp *http.Response, n int) { + var dbClubs []models.Club + + err := app.Conn.Order("created_at desc").Find(&dbClubs).Error + + assert.NilError(err) + + assert.Equal(n, len(dbClubs)) +} + +var TestNumClubsRemainsAt1 = func(app TestApp, assert *assert.A, resp *http.Response) { + AssertNumClubsRemainsAtN(app, assert, resp, 1) +} + +func AssertCreateBadClubDataFails(t *testing.T, jsonKey string, badValues []interface{}) { + appAssert, uuid := CreateSampleUser(t, nil) + + for _, badValue := range badValues { + sampleClubPermutation := *SampleClubFactory(uuid) + sampleClubPermutation[jsonKey] = badValue + + TestRequest{ + Method: fiber.MethodPost, + Path: "/api/v1/clubs/", + Body: &sampleClubPermutation, + }.TestOnErrorAndDB(t, &appAssert, + ErrorWithDBTester{ + Error: errors.FailedToValidateClub, + DBTester: TestNumClubsRemainsAt1, + }, + ) + } + appAssert.Close() +} + +func TestCreateClubFailsOnInvalidDescription(t *testing.T) { + AssertCreateBadClubDataFails(t, + "description", + []interface{}{ + "Not an URL", + "@#139081#$Ad_Axf", + // "https://google.com", <-- TODO fix once we handle mongo urls + }, + ) +} + +func TestCreateClubFailsOnInvalidRecruitmentCycle(t *testing.T) { + AssertCreateBadClubDataFails(t, + "recruitment_cycle", + []interface{}{ + "1234", + "garbanzo", + "@#139081#$Ad_Axf", + "https://google.com", + }, + ) +} + +func TestCreateClubFailsOnInvalidRecruitmentType(t *testing.T) { + AssertCreateBadClubDataFails(t, + "recruitment_type", + []interface{}{ + "1234", + "garbanzo", + "@#139081#$Ad_Axf", + "https://google.com", + }, + ) + +} + +func TestCreateClubFailsOnInvalidApplicationLink(t *testing.T) { + AssertCreateBadClubDataFails(t, + "application_link", + []interface{}{ + "Not an URL", + "@#139081#$Ad_Axf", + }, + ) + +} + +func TestCreateClubFailsOnInvalidLogo(t *testing.T) { + AssertCreateBadClubDataFails(t, + "logo", + []interface{}{ + "Not an URL", + "@#139081#$Ad_Axf", + //"https://google.com", <-- TODO uncomment once we figure out s3 url validation + }, + ) +} + +func TestUpdateClubWorks(t *testing.T) { + appAssert, userUUID, clubUUID := CreateSampleClub(t, nil) + + updatedClub := SampleClubFactory(userUUID) + (*updatedClub)["name"] = "Updated Name" + (*updatedClub)["preview"] = "Updated Preview" + + TestRequest{ + Method: fiber.MethodPatch, + Path: fmt.Sprintf("/api/v1/clubs/%s", clubUUID), + Body: updatedClub, + }.TestOnStatusAndDB(t, &appAssert, + DBTesterWithStatus{ + Status: fiber.StatusOK, + DBTester: func(app TestApp, assert *assert.A, resp *http.Response) { + AssertClubBodyRespDB(app, assert, resp, updatedClub) + }, + }, + ).Close() +} + +func TestUpdateClubFailsOnInvalidBody(t *testing.T) { + appAssert, userUUID, clubUUID := CreateSampleClub(t, nil) + + body := SampleClubFactory(userUUID) + + for _, invalidData := range []map[string]interface{}{ + {"description": "Not a URL"}, + {"recruitment_cycle": "1234"}, + {"recruitment_type": "ALLLLWAYSSSS"}, + {"application_link": "Not an URL"}, + {"logo": "@12394X_2"}, + } { + TestRequest{ + Method: fiber.MethodPatch, + Path: fmt.Sprintf("/api/v1/clubs/%s", clubUUID), + Body: &invalidData, + }.TestOnErrorAndDB(t, &appAssert, + ErrorWithDBTester{ + Error: errors.FailedToValidateClub, + DBTester: func(app TestApp, assert *assert.A, resp *http.Response) { + var dbClubs []models.Club + + err := app.Conn.Order("created_at desc").Find(&dbClubs).Error + + assert.NilError(err) + + assert.Equal(2, len(dbClubs)) + + dbClub := dbClubs[0] + + var dbAdmins []models.User + + err = app.Conn.Model(&dbClub).Association("Admin").Find(&dbAdmins) + + assert.NilError(err) + + assert.Equal(1, len(dbAdmins)) + + assert.Equal((*body)["user_id"].(uuid.UUID), dbAdmins[0].ID) + assert.Equal((*body)["name"].(string), dbClub.Name) + assert.Equal((*body)["preview"].(string), dbClub.Preview) + assert.Equal((*body)["description"].(string), dbClub.Description) + assert.Equal((*body)["num_members"].(int), dbClub.NumMembers) + assert.Equal((*body)["is_recruiting"].(bool), dbClub.IsRecruiting) + assert.Equal(models.RecruitmentCycle((*body)["recruitment_cycle"].(string)), dbClub.RecruitmentCycle) + assert.Equal(models.RecruitmentType((*body)["recruitment_type"].(string)), dbClub.RecruitmentType) + assert.Equal((*body)["application_link"].(string), dbClub.ApplicationLink) + assert.Equal((*body)["logo"].(string), dbClub.Logo) + }, + }, + ) + } + appAssert.Close() +} + +func TestUpdateClubFailsBadRequest(t *testing.T) { + badRequests := []string{ + "0", + "-1", + "1.1", + "foo", + "null", + } + + for _, badRequest := range badRequests { + TestRequest{ + Method: fiber.MethodPatch, + Path: fmt.Sprintf("/api/v1/clubs/%s", badRequest), + Body: SampleUserFactory(), + }.TestOnError(t, nil, errors.FailedToValidateID).Close() + } +} + +func TestUpdateClubFailsOnClubIdNotExist(t *testing.T) { + appAssert, userUUID := CreateSampleUser(t, nil) + + uuid := uuid.New() + + TestRequest{ + Method: fiber.MethodPatch, + Path: fmt.Sprintf("/api/v1/clubs/%s", uuid), + Body: SampleClubFactory(userUUID), + }.TestOnErrorAndDB(t, &appAssert, + ErrorWithDBTester{ + Error: errors.ClubNotFound, + DBTester: func(app TestApp, assert *assert.A, resp *http.Response) { + var club models.Club + + err := app.Conn.Where("id = ?", uuid).First(&club).Error + + assert.Assert(stdliberrors.Is(err, gorm.ErrRecordNotFound)) + }, + }, + ).Close() +} + +func TestDeleteClubWorks(t *testing.T) { + appAssert, _, clubUUID := CreateSampleClub(t, nil) + + TestRequest{ + Method: fiber.MethodDelete, + Path: fmt.Sprintf("/api/v1/clubs/%s", clubUUID), + }.TestOnStatusAndDB(t, &appAssert, + DBTesterWithStatus{ + Status: fiber.StatusNoContent, + DBTester: TestNumClubsRemainsAt1, + }, + ).Close() +} + +func TestDeleteClubNotExist(t *testing.T) { + uuid := uuid.New() + TestRequest{ + Method: fiber.MethodDelete, + Path: fmt.Sprintf("/api/v1/clubs/%s", uuid), + }.TestOnErrorAndDB(t, nil, + ErrorWithDBTester{ + Error: errors.ClubNotFound, + DBTester: func(app TestApp, assert *assert.A, resp *http.Response) { + var club models.Club + + err := app.Conn.Where("id = ?", uuid).First(&club).Error + + assert.Assert(stdliberrors.Is(err, gorm.ErrRecordNotFound)) + + AssertNumClubsRemainsAtN(app, assert, resp, 1) + }, + }, + ).Close() +} + +func TestDeleteClubBadRequest(t *testing.T) { + badRequests := []string{ + "0", + "-1", + "1.1", + "hello", + "null", + } + + for _, badRequest := range badRequests { + TestRequest{ + Method: fiber.MethodDelete, + Path: fmt.Sprintf("/api/v1/clubs/%s", badRequest), + }.TestOnError(t, nil, errors.FailedToValidateID).Close() + } +} diff --git a/backend/tests/api/helpers.go b/backend/tests/api/helpers.go index 13a7a95dc..09ee4ff87 100644 --- a/backend/tests/api/helpers.go +++ b/backend/tests/api/helpers.go @@ -228,7 +228,7 @@ type ErrorWithDBTester struct { DBTester DBTester } -func (request TestRequest) TestOnStatusMessageAndDB(t *testing.T, existingAppAssert *ExistingAppAssert, errorWithDBTester ErrorWithDBTester) ExistingAppAssert { +func (request TestRequest) TestOnErrorAndDB(t *testing.T, existingAppAssert *ExistingAppAssert, errorWithDBTester ErrorWithDBTester) ExistingAppAssert { appAssert := request.TestOnError(t, existingAppAssert, errorWithDBTester.Error) errorWithDBTester.DBTester(appAssert.App, appAssert.Assert, nil) return appAssert diff --git a/backend/tests/api/tag_test.go b/backend/tests/api/tag_test.go index dcc4d3d81..6c9d07db1 100644 --- a/backend/tests/api/tag_test.go +++ b/backend/tests/api/tag_test.go @@ -76,14 +76,22 @@ func TestCreateTagWorks(t *testing.T) { appAssert.Close() } -var AssertNoTags = func(app TestApp, assert *assert.A, resp *http.Response) { +func AssertNumTagsRemainsAtN(app TestApp, assert *assert.A, resp *http.Response, n int) { var tags []models.Tag err := app.Conn.Find(&tags).Error assert.NilError(err) - assert.Equal(0, len(tags)) + assert.Equal(n, len(tags)) +} + +func AssertNoTags(app TestApp, assert *assert.A, resp *http.Response) { + AssertNumTagsRemainsAtN(app, assert, resp, 0) +} + +func Assert1Tag(app TestApp, assert *assert.A, resp *http.Response) { + AssertNumTagsRemainsAtN(app, assert, resp, 1) } func TestCreateTagFailsBadRequest(t *testing.T) { @@ -103,7 +111,7 @@ func TestCreateTagFailsBadRequest(t *testing.T) { Method: fiber.MethodPost, Path: "/api/v1/tags/", Body: &badBody, - }.TestOnStatusMessageAndDB(t, nil, + }.TestOnErrorAndDB(t, nil, ErrorWithDBTester{ Error: errors.FailedToParseRequestBody, DBTester: AssertNoTags, @@ -128,7 +136,7 @@ func TestCreateTagFailsValidation(t *testing.T) { Method: fiber.MethodPost, Path: "/api/v1/tags/", Body: &badBody, - }.TestOnStatusMessageAndDB(t, nil, + }.TestOnErrorAndDB(t, nil, ErrorWithDBTester{ Error: errors.FailedToValidateTag, DBTester: AssertNoTags, @@ -292,6 +300,8 @@ func TestDeleteTagWorks(t *testing.T) { } func TestDeleteTagFailsBadRequest(t *testing.T) { + appAssert, _, _ := CreateSampleTag(t) + badRequests := []string{ "0", "-1", @@ -304,13 +314,27 @@ func TestDeleteTagFailsBadRequest(t *testing.T) { TestRequest{ Method: fiber.MethodDelete, Path: fmt.Sprintf("/api/v1/tags/%s", badRequest), - }.TestOnError(t, nil, errors.FailedToValidateID).Close() + }.TestOnErrorAndDB(t, &appAssert, + ErrorWithDBTester{ + Error: errors.FailedToValidateID, + DBTester: Assert1Tag, + }, + ) } + + appAssert.Close() } func TestDeleteTagFailsNotFound(t *testing.T) { + appAssert, _, _ := CreateSampleTag(t) + TestRequest{ Method: fiber.MethodDelete, Path: fmt.Sprintf("/api/v1/tags/%s", uuid.New()), - }.TestOnError(t, nil, errors.TagNotFound).Close() + }.TestOnErrorAndDB(t, &appAssert, + ErrorWithDBTester{ + Error: errors.TagNotFound, + DBTester: Assert1Tag, + }, + ).Close() } diff --git a/backend/tests/api/user_test.go b/backend/tests/api/user_test.go index 9655a2b3b..15a6ac80c 100644 --- a/backend/tests/api/user_test.go +++ b/backend/tests/api/user_test.go @@ -116,7 +116,7 @@ func TestGetUserFailsNotExist(t *testing.T) { TestRequest{ Method: fiber.MethodGet, Path: fmt.Sprintf("/api/v1/users/%s", uuid), - }.TestOnStatusMessageAndDB(t, nil, + }.TestOnErrorAndDB(t, nil, ErrorWithDBTester{ Error: errors.UserNotFound, DBTester: func(app TestApp, assert *assert.A, resp *http.Response) { @@ -192,7 +192,7 @@ func TestUpdateUserFailsOnInvalidBody(t *testing.T) { Method: fiber.MethodPatch, Path: fmt.Sprintf("/api/v1/users/%s", uuid), Body: &invalidData, - }.TestOnStatusMessageAndDB(t, &appAssert, + }.TestOnErrorAndDB(t, &appAssert, ErrorWithDBTester{ Error: errors.FailedToValidateUser, DBTester: TestNumUsersRemainsAt2, @@ -227,7 +227,7 @@ func TestUpdateUserFailsOnIdNotExist(t *testing.T) { Method: fiber.MethodPatch, Path: fmt.Sprintf("/api/v1/users/%s", uuid), Body: SampleUserFactory(), - }.TestOnStatusMessageAndDB(t, nil, + }.TestOnErrorAndDB(t, nil, ErrorWithDBTester{ Error: errors.UserNotFound, DBTester: func(app TestApp, assert *assert.A, resp *http.Response) { @@ -260,7 +260,7 @@ func TestDeleteUserNotExist(t *testing.T) { TestRequest{ Method: fiber.MethodDelete, Path: fmt.Sprintf("/api/v1/users/%s", uuid), - }.TestOnStatusMessageAndDB(t, nil, + }.TestOnErrorAndDB(t, nil, ErrorWithDBTester{ Error: errors.UserNotFound, DBTester: func(app TestApp, assert *assert.A, resp *http.Response) { @@ -269,6 +269,8 @@ func TestDeleteUserNotExist(t *testing.T) { err := app.Conn.Where("id = ?", uuid).First(&user).Error assert.Assert(stdliberrors.Is(err, gorm.ErrRecordNotFound)) + + TestNumUsersRemainsAt1(app, assert, resp) }, }, ).Close() @@ -287,7 +289,12 @@ func TestDeleteUserBadRequest(t *testing.T) { TestRequest{ Method: fiber.MethodDelete, Path: fmt.Sprintf("/api/v1/users/%s", badRequest), - }.TestOnError(t, nil, errors.FailedToValidateID).Close() + }.TestOnErrorAndDB(t, nil, + ErrorWithDBTester{ + Error: errors.FailedToValidateID, + DBTester: TestNumUsersRemainsAt1, + }, + ) } } @@ -401,7 +408,7 @@ func TestCreateUserFailsIfUserWithEmailAlreadyExists(t *testing.T) { Method: fiber.MethodPost, Path: "/api/v1/users/", Body: SampleUserFactory(), - }.TestOnStatusMessageAndDB(t, &appAssert, + }.TestOnErrorAndDB(t, &appAssert, ErrorWithDBTester{ Error: errors.UserAlreadyExists, DBTester: TestNumUsersRemainsAt2, @@ -428,7 +435,7 @@ func TestCreateUserFailsIfUserWithNUIDAlreadyExists(t *testing.T) { Method: fiber.MethodPost, Path: "/api/v1/users/", Body: slightlyDifferentSampleUser, - }.TestOnStatusMessageAndDB(t, &appAssert, + }.TestOnErrorAndDB(t, &appAssert, ErrorWithDBTester{ Error: errors.UserAlreadyExists, DBTester: TestNumUsersRemainsAt2, @@ -447,7 +454,7 @@ func AssertCreateBadDataFails(t *testing.T, jsonKey string, badValues []interfac Method: fiber.MethodPost, Path: "/api/v1/users/", Body: &sampleUserPermutation, - }.TestOnStatusMessageAndDB(t, &appAssert, + }.TestOnErrorAndDB(t, &appAssert, ErrorWithDBTester{ Error: errors.FailedToValidateUser, DBTester: TestNumUsersRemainsAt2, @@ -537,7 +544,7 @@ func TestCreateUserFailsOnMissingFields(t *testing.T) { Method: fiber.MethodPost, Path: "/api/v1/users/", Body: &sampleUserPermutation, - }.TestOnStatusMessageAndDB(t, &appAssert, + }.TestOnErrorAndDB(t, &appAssert, ErrorWithDBTester{ Error: errors.FailedToValidateUser, DBTester: TestNumUsersRemainsAt2,