diff --git a/backend/src/controllers/club.go b/backend/src/controllers/club.go index d49019a2a..e84f4ecef 100644 --- a/backend/src/controllers/club.go +++ b/backend/src/controllers/club.go @@ -17,6 +17,18 @@ func NewClubController(clubService services.ClubServiceInterface) *ClubControlle return &ClubController{clubService: clubService} } +// GetAllClubs returns all clubs. +// +// @Summary Get all clubs +// @Description Get all clubs with pagination support +// @ID get-all-clubs +// @Tags club +// @Produce json +// @Param limit query int false "Number of clubs per page" +// @Param page query int false "Page number" +// @Success 200 {object} []models.Club +// @Failure 400 {string} string "failed to get clubs" +// @Router /api/v1/clubs/ [get] func (l *ClubController) GetAllClubs(c *fiber.Ctx) error { defaultLimit := 10 defaultPage := 1 @@ -29,6 +41,18 @@ func (l *ClubController) GetAllClubs(c *fiber.Ctx) error { return c.Status(fiber.StatusOK).JSON(clubs) } +// CreateClub creates a new club. +// +// @Summary Create a club +// @Description Create a new club +// @ID create-club +// @Tags club +// @Accept json +// @Produce json +// @Param clubBody body models.CreateClubRequestBody true "Club details" +// @Success 201 {object} models.Club +// @Failure 400 {string} string "failed to create club" +// @Router /api/v1/clubs/ [post] func (l *ClubController) CreateClub(c *fiber.Ctx) error { var clubBody models.CreateClubRequestBody if err := c.BodyParser(&clubBody); err != nil { @@ -43,6 +67,17 @@ func (l *ClubController) CreateClub(c *fiber.Ctx) error { return c.Status(fiber.StatusCreated).JSON(club) } +// GetClub returns a club by ID. +// +// @Summary Get a club by ID +// @Description Get a club by its ID +// @ID get-club +// @Tags club +// @Produce json +// @Param id path string true "Club ID" +// @Success 200 {object} models.Club +// @Failure 400 {string} string "failed to get club" +// @Router /api/v1/clubs/{id} [get] func (l *ClubController) GetClub(c *fiber.Ctx) error { club, err := l.clubService.GetClub(c.Params("id")) if err != nil { @@ -52,6 +87,19 @@ func (l *ClubController) GetClub(c *fiber.Ctx) error { return c.Status(fiber.StatusOK).JSON(club) } +// UpdateClub updates a club by ID. +// +// @Summary Update a club by ID +// @Description Update a club by its ID +// @ID update-club +// @Tags club +// @Accept json +// @Produce json +// @Param id path string true "Club ID" +// @Param clubBody body models.UpdateClubRequestBody true "Club details" +// @Success 200 {object} models.Club +// @Failure 400 {string} string "failed to update club" +// @Router /api/v1/clubs/{id} [put] func (l *ClubController) UpdateClub(c *fiber.Ctx) error { var clubBody models.UpdateClubRequestBody @@ -67,6 +115,17 @@ func (l *ClubController) UpdateClub(c *fiber.Ctx) error { return c.Status(fiber.StatusOK).JSON(updatedClub) } +// DeleteClub deletes a club by ID. +// +// @Summary Delete a club by ID +// @Description Delete a club by its ID +// @ID delete-club +// @Tags club +// @Produce json +// @Param id path string true "Club ID" +// @Success 204 - No Content +// @Failure 400 {string} string "failed to delete club" +// @Router /api/v1/clubs/{id} [delete] func (l *ClubController) DeleteClub(c *fiber.Ctx) error { err := l.clubService.DeleteClub(c.Params("id")) if err != nil { @@ -76,10 +135,38 @@ func (l *ClubController) DeleteClub(c *fiber.Ctx) error { return c.SendStatus(fiber.StatusNoContent) } -// func (l* ClubController) GetContact(c *fiber.Ctx) error { +// GetContact returns a contact by ID. +// +// @Summary Get a contact by ID +// @Description Get a contact by its ID +// @ID get-contact +// @Tags club +// @Produce json +// @Param id path string true "Contact ID" +// @Success 200 {object} models.Contact +// @Failure 400 {string} string "failed to get contact" +// @Router /api/v1/contacts/{id} [get] +func (l *ClubController) GetContact(c *fiber.Ctx) error { + contact, err := l.clubService.GetContact(c.Params("id")) + if err != nil { + return err.FiberError(c) + } -// } + return c.Status(fiber.StatusOK).JSON(contact) +} +// GetContacts returns all contacts. +// +// @Summary Get all contacts +// @Description Get all contacts with pagination support +// @ID get-all-contacts +// @Tags club +// @Produce json +// @Param limit query int false "Number of contacts per page" +// @Param page query int false "Page number" +// @Success 200 {object} []models.Contact +// @Failure 400 {string} string "failed to get contacts" +// @Router /api/v1/contacts/ [get] func (l *ClubController) GetContacts(c *fiber.Ctx) error { defaultLimit := 10 defaultPage := 1 @@ -92,6 +179,17 @@ func (l *ClubController) GetContacts(c *fiber.Ctx) error { return c.Status(fiber.StatusOK).JSON(contacts) } +// GetClubContacts returns all contacts of a club. +// +// @Summary Get all contacts of a club +// @Description Get all contacts of a club with pagination support +// @ID get-club-contacts +// @Tags club +// @Produce json +// @Param id path string true "Club ID" +// @Success 200 {object} []models.Contact +// @Failure 400 {string} string "failed to get club contacts" +// @Router /api/v1/clubs/{id}/contacts [get] func (l *ClubController) GetClubContacts(c *fiber.Ctx) error { contacts, err := l.clubService.GetClubContacts(c.Params("id")) if err != nil { @@ -101,17 +199,19 @@ func (l *ClubController) GetClubContacts(c *fiber.Ctx) error { return c.Status(fiber.StatusOK).JSON(contacts) } -// TODO fix godoc -// PutContact godoc +// PutContact creates or updates a contact for a club. // -// @Summary Creates a contact for a club, if it does not exist, other wise updates an existing contact -// @Description Returns the updated contact -// @ID put-contact -// @Tags club -// @Produce json -// @Success 200 {object} []models.Contact -// @Failure 400 {string} string "failed to update contact" -// @Router /api/v1/users/ [put] +// @Summary Create or update a contact for a club +// @Description Creates a contact for a club if it does not exist, otherwise updates an existing contact +// @ID put-contact +// @Tags club +// @Accept json +// @Produce json +// @Param id path string true "Club ID" +// @Param contactBody body models.PutContactRequestBody true "Contact details" +// @Success 200 {object} models.Contact +// @Failure 400 {string} string "failed to create/update contact" +// @Router /api/v1/clubs/{id}/contacts [put] func (l *ClubController) PutContact(c *fiber.Ctx) error { var contactBody models.PutContactRequestBody @@ -127,8 +227,19 @@ func (l *ClubController) PutContact(c *fiber.Ctx) error { return c.Status(fiber.StatusOK).JSON(contact) } +// DeleteContact deletes a contact by ID. +// +// @Summary Delete a contact by ID +// @Description Delete a contact by its ID +// @ID delete-contact +// @Tags club +// @Produce json +// @Param id path string true "Contact ID" +// @Success 204 - No Content +// @Failure 400 {string} string "failed to delete contact" +// @Router /api/v1/contacts/{id} [delete] func (l *ClubController) DeleteContact(c *fiber.Ctx) error { - err := l.clubService.DeleteContact(c.Params("contactID")) + err := l.clubService.DeleteContact(c.Params("id")) if err != nil { return err.FiberError(c) } diff --git a/backend/src/errors/club.go b/backend/src/errors/club.go index 666c3cb2e..253b9d9b2 100644 --- a/backend/src/errors/club.go +++ b/backend/src/errors/club.go @@ -39,6 +39,14 @@ var ( StatusCode: fiber.StatusInternalServerError, Message: "failed to get contacts", } + FailedToGetContact = Error{ + StatusCode: fiber.StatusInternalServerError, + Message: "failed to get contact", + } + ContactNotFound = Error{ + StatusCode: fiber.StatusNotFound, + Message: "contact not found", + } FailedToCreateContact = Error{ StatusCode: fiber.StatusInternalServerError, Message: "failed to create contact", diff --git a/backend/src/models/contact.go b/backend/src/models/contact.go index 821db93a5..f795f989f 100644 --- a/backend/src/models/contact.go +++ b/backend/src/models/contact.go @@ -27,7 +27,7 @@ type Contact struct { } type PutContactRequestBody struct { - Type ContactType `gorm:"type:varchar(255)" json:"type" validate:"required,max=255,oneof=facebook instagram twitter linkedin youtube github slack discord email customSite,contact_pointer"` + Type ContactType `json:"type" validate:"required,max=255,oneof=facebook instagram twitter linkedin youtube github slack discord email customSite,contact_pointer"` - Content string `gorm:"type:varchar(255)" json:"content" validate:"required,max=255"` + Content string `json:"content" validate:"required,max=255"` } diff --git a/backend/src/server/server.go b/backend/src/server/server.go index 1f4756d0f..d50043c36 100644 --- a/backend/src/server/server.go +++ b/backend/src/server/server.go @@ -36,6 +36,7 @@ func Init(db *gorm.DB) *fiber.App { userRoutes(apiv1, &services.UserService{DB: db, Validate: validate}) clubRoutes(apiv1, &services.ClubService{DB: db, Validate: validate}) + contactRoutes(apiv1, &services.ClubService{DB: db, Validate: validate}) categoryRouter := categoryRoutes(apiv1, &services.CategoryService{DB: db, Validate: validate}) tagRoutes(categoryRouter, &services.TagService{DB: db, Validate: validate}) @@ -99,7 +100,6 @@ func clubRoutes(router fiber.Router, clubService services.ClubServiceInterface) // club-dependent contact routes contacts.Get("/", clubController.GetClubContacts) contacts.Put("/", clubController.PutContact) // contact to be updated is identified in the body media type - contacts.Delete("/:contactID", clubController.DeleteContact) } func contactRoutes(router fiber.Router, clubService services.ClubServiceInterface) { @@ -108,6 +108,8 @@ func contactRoutes(router fiber.Router, clubService services.ClubServiceInterfac contacts := router.Group("/contacts") contacts.Get("/", clubController.GetContacts) + contacts.Get("/:id", clubController.GetContact) + contacts.Delete("/:id", clubController.DeleteContact) } func categoryRoutes(router fiber.Router, categoryService services.CategoryServiceInterface) fiber.Router { diff --git a/backend/src/services/club.go b/backend/src/services/club.go index 9f8b010bd..415f03431 100644 --- a/backend/src/services/club.go +++ b/backend/src/services/club.go @@ -17,6 +17,7 @@ type ClubServiceInterface interface { UpdateClub(id string, clubBody models.UpdateClubRequestBody) (*models.Club, *errors.Error) DeleteClub(id string) *errors.Error GetContacts(limit string, page string) ([]models.Contact, *errors.Error) + GetContact(id string) (*models.Contact, *errors.Error) GetClubContacts(id string) ([]models.Contact, *errors.Error) // GetClubContact(contactId string) (*models.Contact, *errors.Error) PutContact(clubID string, contactBody models.PutContactRequestBody) (*models.Contact, *errors.Error) @@ -113,6 +114,15 @@ func (c *ClubService) GetContacts(limit string, page string) ([]models.Contact, return transactions.GetContacts(c.DB, *limitAsInt, offset) } +func (c *ClubService) GetContact(id string) (*models.Contact, *errors.Error) { + idAsUUID, err := utilities.ValidateID(id) + if err != nil { + return nil, &errors.FailedToValidateID + } + + return transactions.GetContact(c.DB, *idAsUUID) +} + func (c *ClubService) GetClubContacts(id string) ([]models.Contact, *errors.Error) { idAsUUID, err := utilities.ValidateID(id) if err != nil { diff --git a/backend/src/transactions/club.go b/backend/src/transactions/club.go index eb8f9a968..e75da2c0d 100644 --- a/backend/src/transactions/club.go +++ b/backend/src/transactions/club.go @@ -88,6 +88,19 @@ func GetClubContacts(db *gorm.DB, id uuid.UUID) ([]models.Contact, *errors.Error return club.Contact, nil } +func GetContact(db *gorm.DB, id uuid.UUID) (*models.Contact, *errors.Error) { + var contact models.Contact + if err := db.First(&contact, id).Error; err != nil { + if stdliberrors.Is(err, gorm.ErrRecordNotFound) { + return nil, &errors.ContactNotFound + } else { + return nil, &errors.FailedToGetContact + } + } + + return &contact, nil +} + func PutContact(db *gorm.DB, clubID uuid.UUID, contact models.Contact) (*models.Contact, *errors.Error) { // if the club already has a contact of the same type, update the existing contact err := db.Clauses(clause.OnConflict{ @@ -107,11 +120,13 @@ func PutContact(db *gorm.DB, clubID uuid.UUID, contact models.Contact) (*models. } func DeleteContact(db *gorm.DB, id uuid.UUID) *errors.Error { - result := db.Delete(&models.Contact{}, id) - if result.Error != nil { - return &errors.FailedToDeleteClub + if result := db.Delete(&models.Contact{}, id); result.RowsAffected == 0 { + if result.Error == nil { + return &errors.ContactNotFound + } else { + return &errors.FailedToDeleteContact + } } - return nil } func UpdateClub(db *gorm.DB, id uuid.UUID, club models.Club) (*models.Club, *errors.Error) { diff --git a/backend/src/utilities/validator.go b/backend/src/utilities/validator.go index 970497e22..b02db6c31 100644 --- a/backend/src/utilities/validator.go +++ b/backend/src/utilities/validator.go @@ -56,7 +56,6 @@ func validateS3URL(fl validator.FieldLevel) bool { func validateContactPointer(validate *validator.Validate, fl validator.FieldLevel) bool { contact, ok := fl.Parent().Interface().(models.PutContactRequestBody) if !ok { - println("Failed to cast to PutContactRequestBody") return false } switch contact.Type { diff --git a/backend/tests/api/club_test.go b/backend/tests/api/club_test.go index d50b7a82c..dca641c2d 100644 --- a/backend/tests/api/club_test.go +++ b/backend/tests/api/club_test.go @@ -478,292 +478,3 @@ func TestDeleteClubBadRequest(t *testing.T) { } -// contact tests - -/* -C- test contact creation works with valid data -test contact creation fails with invalid data -- type - - missing - - not a valid type - -R- test get contacts works - -test delete contact works -*/ - -func SampleContactFactory() *map[string]interface{} { - return &map[string]interface{}{ - "type": "email", - "content": "jermaine@gmail.com", - } -} - -func ManyContactsFactory() *map[string]map[string]interface{} { - arr := make(map[string]map[string]interface{}) - - arr["email"] = map[string]interface{}{ - "type": "email", - "content": "cheeseClub@gmail.com", - } - - arr["youtube"] = map[string]interface{}{ - "type": "youtube", - "content": "youtube.com/cheeseClub", - } - - arr["facebook"] = map[string]interface{}{ - "type": "facebook", - "content": "facebook.com/cheeseClub", - } - - arr["discord"] = map[string]interface{}{ - "type": "discord", - "content": "discord.com/cheeseClub", - } - - arr["instagram"] = map[string]interface{}{ - "type": "instagram", - "content": "instagram.com/cheeseClub", - } - arr["github"]= map[string]interface{}{ - "type": "github", - "content": "github.com/cheeseClub", - } - - return &arr -} - -func AssertContactBodyRespDB(app TestApp, assert *assert.A, resp *http.Response, body *map[string]interface{}) uuid.UUID { - var respContact models.Contact - - // decode the response body into a respContact - err := json.NewDecoder(resp.Body).Decode(&respContact) - - assert.NilError(err) - - var dbContacts []models.Contact - - // get all contacts from the database ordered by created_at and store them in dbContacts - // err = app.Conn.Order("created_at desc").Find(&dbContacts).Error - - assert.NilError(err) - - // assert.Equal(1, len(dbContacts)) - - dbContact := dbContacts[0] - - assert.Equal(dbContact.ID, respContact.ID) - assert.Equal(dbContact.Type, respContact.Type) - assert.Equal(dbContact.Content, respContact.Content) - - return dbContact.ID -} - -func AssertSampleContactBodyRespDB(app TestApp, assert *assert.A, resp *http.Response, clubUUID uuid.UUID) uuid.UUID { - return AssertContactBodyRespDB(app, assert, resp, SampleContactFactory()) -} - -func CreateSampleContact(t *testing.T, existingAppAssert *ExistingAppAssert) (eaa ExistingAppAssert, clubUUID uuid.UUID, contactUUID uuid.UUID) { - appAssert, _, clubUUID := CreateSampleClub(t, nil) - - for _, contact := range *ManyContactsFactory() { - - appAssert = TestRequest{ - Method: fiber.MethodPut, - Path: fmt.Sprintf("/api/v1/clubs/%s/contacts", clubUUID), - Body: &contact, - }.TestOnStatusAndDB(t, &appAssert, - DBTesterWithStatus{ - Status: fiber.StatusOK, - DBTester: func(app TestApp, assert *assert.A, resp *http.Response) { - AssertContactBodyRespDB(app, assert, resp, &contact) - }, - }, - ) - } - - - var sampleContactUUID uuid.UUID - - newAppAssert := TestRequest{ - Method: fiber.MethodPut, - Path: fmt.Sprintf("/api/v1/clubs/%s/contacts", clubUUID), - Body: SampleContactFactory(), - }.TestOnStatusAndDB(t, &appAssert, - DBTesterWithStatus{ - Status: fiber.StatusOK, - DBTester: func(app TestApp, assert *assert.A, resp *http.Response) { - sampleContactUUID = AssertSampleContactBodyRespDB(app, assert, resp, clubUUID) - }, - }, - ) - - if existingAppAssert == nil { - return newAppAssert, clubUUID, sampleContactUUID - } else { - return *existingAppAssert, clubUUID, sampleContactUUID - } -} - -func TestCreateContactWorks(t *testing.T) { - existingAppAssert, _, _ := CreateSampleContact(t, nil) - existingAppAssert.Close() -} - -func AssertNumContactsRemainsAtN(app TestApp, assert *assert.A, resp *http.Response, n int) { - var dbContacts []models.Contact - - err := app.Conn.Order("created_at desc").Find(&dbContacts).Error - - assert.NilError(err) - - assert.Equal(n, len(dbContacts)) -} - -var TestNumContactsRemainsAt0 = func(app TestApp, assert *assert.A, resp *http.Response) { - AssertNumContactsRemainsAtN(app, assert, resp, 0) -} - -func AssertCreateBadContactDataFails(t *testing.T, jsonKey string, badValues []interface{}) { - appAssert, _, clubUUID := CreateSampleClub(t, nil) - - for _, badValue := range badValues { - sampleContactPermutation := *SampleContactFactory() - sampleContactPermutation[jsonKey] = badValue - - TestRequest{ - Method: fiber.MethodPut, - Path: fmt.Sprintf("/api/v1/clubs/%s/contacts", clubUUID), - Body: &sampleContactPermutation, - }.TestOnErrorAndDB(t, &appAssert, - ErrorWithDBTester{ - Error: errors.FailedToValidateContact, - DBTester: TestNumContactsRemainsAt0, - }, - ) - } - appAssert.Close() -} - -func TestCreateContactFailsOnInvalidType(t *testing.T) { - AssertCreateBadContactDataFails(t, - "type", - []interface{}{ - "Not a valid type", - "@#139081#$Ad_Axf", - }, - ) -} - -func TestCreateContactFailsOnInvalidContent(t *testing.T) { - AssertCreateBadContactDataFails(t, - "content", - []interface{}{ - "Not a valid url", - "@#139081#$Ad_Axf", - }, - ) -} - -func TestPutContactFailsOnClubIdNotExist(t *testing.T) { - appAssert, _, _ := CreateSampleClub(t, nil) - - uuid := uuid.New() - - TestRequest{ - Method: fiber.MethodPut, - Path: fmt.Sprintf("/api/v1/clubs/%s/contacts", uuid), - Body: SampleContactFactory(), - }.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() -} - -// if a club already has a contact of the same type, the new contact should replace the old one -// func TestPutContactUpdatesExistingContact(t *testing.T){ -// appAssert, clubUUID, contactUUID := CreateSampleContact(t, nil) - -// updatedContact := SampleContactFactory() -// (*updatedContact)["content"] = "" - -// } - -// func TestGetContactByIdWorks(t *testing.T) { -// appAssert, clubUUID, contactUUID := CreateSampleContact(t, nil) - -// TestRequest{ -// Method: fiber.MethodGet, -// Path: fmt.Sprintf("/api/v1/clubs/%s/contacts", clubUUID), -// }.TestOnStatusAndDB(t, &appAssert, -// DBTesterWithStatus{ -// Status: fiber.StatusOK, -// DBTester: func(app TestApp, assert *assert.A, resp *http.Response) { -// var respContacts []models.Contact - -// err := json.NewDecoder(resp.Body).Decode(&respContacts) - -// assert.NilError(err) - -// assert.Equal(1, len(respContacts)) - -// respContact := respContacts[0] - -// var dbContacts []models.Contact - -// err = app.Conn.Order("created_at desc").Find(&dbContacts).Error - -// assert.NilError(err) - -// assert.Equal(1, len(dbContacts)) - -// dbContact := dbContacts[0] - -// assert.Equal(dbContact.ID, respContact.ID) -// assert.Equal(dbContact.Type, respContact.Type) -// assert.Equal(dbContact.Content, respContact.Content) -// }, -// }, -// ).Close() -// } - -// func TestGetContactsByClubIDWorks(t *testing.T ){ -// appAssert, club1, contact1 := CreateSampleContact(t, nil) - -// appAssert, club - -// appAssert = appTestRequest{ -// Method: fiber.MethodGet, -// Path: fmt.Sprintf("/api/v1/clubs/%s/contacts/%s", clubUUID, contactUUID), -// }.TestOnStatusAndDB(t, &appAssert, -// DBTesterWithStatus{ -// Status: fiber.StatusOK, -// DBTester: func(app TestApp, assert *assert.A, resp *http.Response) { -// var respContact models.Contact - -// err := json.NewDecoder(resp.Body).Decode(&respContact) - -// assert.NilError(err) - -// var dbContact models.Contact - -// err = app.Conn.Order("created_at desc").First(&dbContact).Error - -// assert.NilError(err) - -// assert.Equal(dbContact.ID, respContact.ID) -// assert.Equal(dbContact.Type, respContact.Type) -// assert.Equal(dbContact.Content, respContact.Content) -// }, -// }, -// ) -// } diff --git a/backend/tests/api/contact_test.go b/backend/tests/api/contact_test.go new file mode 100644 index 000000000..58b730adb --- /dev/null +++ b/backend/tests/api/contact_test.go @@ -0,0 +1,389 @@ +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 SampleContactFactory() *map[string]interface{} { + return &map[string]interface{}{ + "type": "email", + "content": "jermaine@gmail.com", + } +} + +func ManyContactsFactory() map[string](*map[string]interface{}) { + arr := make(map[string]*map[string]interface{}) + + arr["email"] = &map[string]interface{}{ + "type": "email", + "content": "cheeseClub@gmail.com", + } + + arr["youtube"] = &map[string]interface{}{ + "type": "youtube", + "content": "https://youtube.com/cheeseClub", + } + + arr["facebook"] = &map[string]interface{}{ + "type": "facebook", + "content": "https://facebook.com/cheeseClub", + } + + arr["discord"] = &map[string]interface{}{ + "type": "discord", + "content": "https://discord.com/cheeseClub", + } + + arr["instagram"] = &map[string]interface{}{ + "type": "instagram", + "content": "https://instagram.com/cheeseClub", + } + arr["github"] = &map[string]interface{}{ + "type": "github", + "content": "https://github.com/cheeseClub", + } + + return arr +} + +func AssertContactBodyRespDB(app TestApp, assert *assert.A, resp *http.Response, body *map[string]interface{}) uuid.UUID { + var respContact models.Contact + + // decode the response body into a respContact + err := json.NewDecoder(resp.Body).Decode(&respContact) + + assert.NilError(err) + + var dbContacts []models.Contact + + // get all contacts from the database ordered by created_at and store them in dbContacts + err = app.Conn.Order("created_at desc").Find(&dbContacts).Error + + assert.NilError(err) + + dbContact := dbContacts[0] + + assert.Equal(dbContact.ID, respContact.ID) + assert.Equal(dbContact.Type, respContact.Type) + assert.Equal(dbContact.Content, respContact.Content) + + return dbContact.ID +} + +func CreateSampleContact(t *testing.T, existingAppAssert *ExistingAppAssert) (eaa ExistingAppAssert, clubUUID uuid.UUID, contactUUID uuid.UUID) { + appAssert, _, clubUUID := CreateSampleClub(t, existingAppAssert) + + var sampleContactUUID uuid.UUID + + + appAssert = TestRequest{ + Method: fiber.MethodPut, + Path: fmt.Sprintf("/api/v1/clubs/%s/contacts", clubUUID), + Body: SampleContactFactory(), + }.TestOnStatusAndDB(t, &appAssert, + DBTesterWithStatus{ + Status: fiber.StatusOK, + DBTester: func(app TestApp, assert *assert.A, resp *http.Response) { + sampleContactUUID = AssertContactBodyRespDB(app, assert, resp, SampleContactFactory()) + AssertNumContactsRemainsAtN(app, assert, resp, 1) + }, + }, + ) + + return appAssert, clubUUID, sampleContactUUID + +} + +func CreateManyContacts(t *testing.T, existingAppAssert *ExistingAppAssert) (eaa ExistingAppAssert, clubUUID uuid.UUID, contactUUIDs map[string]uuid.UUID) { + appAssert, _, clubUUID := CreateSampleClub(t, existingAppAssert) + + contactUUIDs = make(map[string]uuid.UUID) + + currentLength := 0 + for key, contact := range ManyContactsFactory() { + TestRequest{ + Method: fiber.MethodPut, + Path: fmt.Sprintf("/api/v1/clubs/%s/contacts", clubUUID), + Body: contact, + }.TestOnStatusAndDB(t, &appAssert, + DBTesterWithStatus{ + Status: fiber.StatusOK, + DBTester: func(app TestApp, assert *assert.A, resp *http.Response) { + contactUUIDs[key] = AssertContactBodyRespDB(app, assert, resp, contact) + currentLength++ + AssertNumContactsRemainsAtN(app, assert, resp, currentLength) + }, + }, + ) + + } + + return appAssert, clubUUID, contactUUIDs +} + +func TestCreateManyContactsWorks(t *testing.T) { + existingAppAssert, _, _ := CreateManyContacts(t, nil) + existingAppAssert.Close() +} + +func TestCreateContactWorks(t *testing.T) { + existingAppAssert, _, _ := CreateSampleContact(t, nil) + existingAppAssert.Close() +} + +func AssertNumContactsRemainsAtN(app TestApp, assert *assert.A, resp *http.Response, n int) { + var dbContacts []models.Contact + + err := app.Conn.Order("created_at desc").Find(&dbContacts).Error + + assert.NilError(err) + + assert.Equal(n, len(dbContacts)) +} + + +func AssertCreateBadContactDataFails(t *testing.T, jsonKey string, badValues []interface{}) { + appAssert, _, clubUUID := CreateSampleClub(t, nil) + + for _, badValue := range badValues { + sampleContactPermutation := *SampleContactFactory() + sampleContactPermutation[jsonKey] = badValue + + TestRequest{ + Method: fiber.MethodPut, + Path: fmt.Sprintf("/api/v1/clubs/%s/contacts", clubUUID), + Body: &sampleContactPermutation, + }.TestOnErrorAndDB(t, &appAssert, + ErrorWithDBTester{ + Error: errors.FailedToValidateContact, + DBTester: func(app TestApp, assert *assert.A, resp *http.Response) { + AssertNumContactsRemainsAtN(app, assert, resp, 0) + }, + }, + ) + } + appAssert.Close() +} + +// if an invalid type is given, the request should fail +func TestCreateContactFailsOnInvalidType(t *testing.T) { + AssertCreateBadContactDataFails(t, + "type", + []interface{}{ + "Not a valid type", + "@#139081#$Ad_Axf", + }, + ) +} + +// if a bad link/email is given, the request should fail +func TestCreateContactFailsOnInvalidContent(t *testing.T) { + AssertCreateBadContactDataFails(t, + "content", + []interface{}{ + "Not a valid url", + "@#139081#$Ad_Axf", + }, + ) +} + +// if given an invalid club ID, the request should fail +func TestPutContactFailsOnClubIdNotExist(t *testing.T) { + appAssert, _, _ := CreateSampleClub(t, nil) + + uuid := uuid.New() + + TestRequest{ + Method: fiber.MethodPut, + Path: fmt.Sprintf("/api/v1/clubs/%s/contacts", uuid), + Body: SampleContactFactory(), + }.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() +} + +// if a club already has a contact of the same type, the new contact should replace the old one +func TestPutContactUpdatesExistingContact(t *testing.T){ + appAssert, clubUUID, contactUUID := CreateSampleContact(t, nil) + + updatedContact := SampleContactFactory() + (*updatedContact)["content"] = "nedFlanders@gmail.com" + + TestRequest{ + Method: fiber.MethodPut, + Path: fmt.Sprintf("/api/v1/clubs/%s/contacts", clubUUID), + Body: updatedContact, + }.TestOnStatusAndDB(t, &appAssert, + DBTesterWithStatus{ + Status: fiber.StatusOK, + DBTester: func(app TestApp, assert *assert.A, resp *http.Response) { + var dbContact models.Contact + + err := app.Conn.Where("id = ?", contactUUID).First(&dbContact).Error + + assert.NilError(err) + + assert.Equal(dbContact.Content, (*updatedContact)["content"]) + }, + }, + ).Close() +} + +// given a valid contactID the request should return the contact +func TestGetContactByIdWorks(t *testing.T) { + appAssert, _, contactUUID := CreateSampleContact(t, nil) + + TestRequest{ + Method: fiber.MethodGet, + Path: fmt.Sprintf("/api/v1/contacts/%s", contactUUID), + }.TestOnStatusAndDB(t, &appAssert, + DBTesterWithStatus{ + Status: fiber.StatusOK, + DBTester: func(app TestApp, assert *assert.A, resp *http.Response) { + var respContact models.Contact + + err := json.NewDecoder(resp.Body).Decode(&respContact) + + assert.NilError(err) + + var dbContacts []models.Contact + + err = app.Conn.Order("created_at desc").Find(&dbContacts).Error + + assert.NilError(err) + + assert.Equal(dbContacts[0].ID, respContact.ID) + assert.Equal(dbContacts[0].Type, respContact.Type) + assert.Equal(dbContacts[0].Content, respContact.Content) + }, + }, + ).Close() +} + +// if a contactID does not exist, request should fail +func TestGetContactFailsOnContactIdNotExist(t *testing.T) { + appAssert, _, _ := CreateSampleContact(t, nil) + + uuid := uuid.New() + + TestRequest{ + Method: fiber.MethodGet, + Path: fmt.Sprintf("/api/v1/contacts/%s", uuid), + }.TestOnErrorAndDB(t, &appAssert, + ErrorWithDBTester{ + Error: errors.ContactNotFound, + DBTester: func(app TestApp, assert *assert.A, resp *http.Response) { + var contact models.Contact + + err := app.Conn.Where("id = ?", uuid).First(&contact).Error + + assert.Assert(stdliberrors.Is(err, gorm.ErrRecordNotFound)) + }, + }, + ).Close() +} + +// given a valid contactID the request should delete the contact +func TestDeleteContactWorks(t *testing.T) { + appAssert, _, contactUUID := CreateSampleContact(t, nil) + + TestRequest{ + Method: fiber.MethodDelete, + Path: fmt.Sprintf("/api/v1/contacts/%s", contactUUID), + }.TestOnStatusAndDB(t, &appAssert, + DBTesterWithStatus{ + Status: fiber.StatusNoContent, + DBTester: func(app TestApp, assert *assert.A, resp *http.Response) { + var contact models.Contact + + err := app.Conn.Where("id = ?", contactUUID).First(&contact).Error + + assert.Assert(stdliberrors.Is(err, gorm.ErrRecordNotFound)) + }, + }, + ).Close() +} + +// if a contactID does not exist, request should fail +func TestDeleteContactFailsOnContactIdNotExist(t *testing.T) { + appAssert, _, _ := CreateSampleContact(t, nil) + uuid := uuid.New() + TestRequest{ + Method: fiber.MethodDelete, + Path: fmt.Sprintf("/api/v1/contacts/%s", uuid), + }.TestOnErrorAndDB(t, &appAssert, + ErrorWithDBTester{ + Error: errors.ContactNotFound, + DBTester: func(app TestApp, assert *assert.A, resp *http.Response) { + var contact models.Contact + err := app.Conn.Where("id = ?", uuid).First(&contact).Error + assert.Assert(stdliberrors.Is(err, gorm.ErrRecordNotFound)) + + AssertNumContactsRemainsAtN(app, assert, resp, 1) + }, + }, + ).Close() +} + +// test that the request returns paginated contacts +func TestGetContactsWorks(t *testing.T) { + appAssert, _, _ := CreateManyContacts(t, nil) + + TestRequest{ + Method: fiber.MethodGet, + Path: "/api/v1/contacts", + }.TestOnStatus(t, &appAssert, fiber.StatusOK) + + appAssert.Close() +} + +// test that the request returns contacts that belong to a club +func TestGetClubContacts(t *testing.T) { + appAssert, clubUUID, _ := CreateManyContacts(t, nil) + // TODO create another contact that does not belong to the current club + // and assert that it is not returned in the response + // using createSampleClub twice returns a 409 ID conflict error + // so I may have to create a club manually + + TestRequest{ + Method: fiber.MethodGet, + Path: fmt.Sprintf("/api/v1/clubs/%s/contacts", clubUUID), + }.TestOnStatusAndDB(t, &appAssert, + DBTesterWithStatus{ + Status: fiber.StatusOK, + DBTester: func(app TestApp, assert *assert.A, resp *http.Response) { + var respContacts []models.Contact + var dbContacts []models.Contact + err := json.NewDecoder(resp.Body).Decode(&respContacts) + assert.NilError(err) + + err = app.Conn.Where("club_id = ?", clubUUID).Find(&dbContacts).Error + assert.NilError(err) + + assert.Equal(len(respContacts), len(dbContacts)) + }, + }, + ) + + appAssert.Close() +} \ No newline at end of file