From 62e629381ddee1322d6d0e2b7d43e5191011af74 Mon Sep 17 00:00:00 2001 From: Garrett Ladley <92384606+garrettladley@users.noreply.github.com> Date: Sun, 21 Jan 2024 11:55:06 -0500 Subject: [PATCH] SAC-14 Update Tag Patch (#35) --- backend/src/controllers/tag.go | 34 ++++++- backend/src/models/tag.go | 12 ++- backend/src/server/server.go | 1 + backend/src/services/tag.go | 22 ++++- backend/src/transactions/tag.go | 13 +++ backend/src/utilities/validator.go | 1 + backend/tests/api/category_test.go | 8 +- backend/tests/api/tag_test.go | 146 ++++++++++++++++++++++------- 8 files changed, 193 insertions(+), 44 deletions(-) diff --git a/backend/src/controllers/tag.go b/backend/src/controllers/tag.go index 58e55afc8..fda5186f9 100644 --- a/backend/src/controllers/tag.go +++ b/backend/src/controllers/tag.go @@ -67,6 +67,38 @@ func (t *TagController) GetTag(c *fiber.Ctx) error { return c.Status(fiber.StatusOK).JSON(&tag) } +// UpdateTag godoc +// +// @Summary Updates a tag +// @Description Updates a tag +// @ID update-tag +// @Tags tag +// @Accept json +// @Produce json +// @Param id path int true "Tag ID" +// @Success 200 {object} models.Tag +// @Failure 400 {string} string "failed to process the request" +// @Failure 400 {string} string "failed to validate id" +// @Failure 400 {string} string "failed to validate the data" +// @Failure 404 {string} string "failed to find tag" +// @Failure 500 {string} string "failed to update tag" +// @Router /api/v1/tags/{id} [patch] +func (t *TagController) UpdateTag(c *fiber.Ctx) error { + var tagBody models.UpdateTagRequestBody + + if err := c.BodyParser(&tagBody); err != nil { + return fiber.NewError(fiber.StatusBadRequest, "failed to process the request") + } + + tag, err := t.tagService.UpdateTag(c.Params("id"), tagBody) + + if err != nil { + return err + } + + return c.Status(fiber.StatusOK).JSON(&tag) +} + // DeleteTag godoc // // @Summary Deletes a tag @@ -74,7 +106,7 @@ func (t *TagController) GetTag(c *fiber.Ctx) error { // @ID delete-tag // @Tags tag // @Param id path int true "Tag ID" -// @Success 204 {string} string "No Content" +// @Success 204 // @Failure 400 {string} string "failed to validate id" // @Failure 404 {string} string "failed to find tag" // @Failure 500 {string} string "failed to delete tag" diff --git a/backend/src/models/tag.go b/backend/src/models/tag.go index d515fb692..767b3d37b 100644 --- a/backend/src/models/tag.go +++ b/backend/src/models/tag.go @@ -9,14 +9,22 @@ type Tag struct { Name string `gorm:"type:varchar(255)" json:"name" validate:"required,max=255"` - CategoryID uint `gorm:"foreignKey:CategoryID" json:"category_id" validate:"required"` + CategoryID uint `gorm:"foreignKey:CategoryID" json:"category_id" validate:"required,min=1"` User []User `gorm:"many2many:user_tags;constraint:OnUpdate:CASCADE,OnDelete:CASCADE;" json:"-" validate:"-"` Club []Club `gorm:"many2many:club_tags;constraint:OnUpdate:CASCADE,OnDelete:CASCADE;" json:"-" validate:"-"` Event []Event `gorm:"many2many:event_tags;constraint:OnUpdate:CASCADE,OnDelete:CASCADE;" json:"-" validate:"-"` } -type CreateTagRequestBody struct { +type PartialTag struct { Name string `json:"name" validate:"required,max=255"` CategoryID uint `json:"category_id" validate:"required,min=1"` } + +type CreateTagRequestBody struct { + PartialTag +} + +type UpdateTagRequestBody struct { + PartialTag +} diff --git a/backend/src/server/server.go b/backend/src/server/server.go index 4f02f90a3..a59011369 100644 --- a/backend/src/server/server.go +++ b/backend/src/server/server.go @@ -82,5 +82,6 @@ func tagRoutes(router fiber.Router, tagService services.TagServiceInterface) { tags.Post("/", tagController.CreateTag) tags.Get("/:id", tagController.GetTag) + tags.Patch("/:id", tagController.UpdateTag) tags.Delete("/:id", tagController.DeleteTag) } diff --git a/backend/src/services/tag.go b/backend/src/services/tag.go index f76309ec7..dac2203b3 100644 --- a/backend/src/services/tag.go +++ b/backend/src/services/tag.go @@ -11,7 +11,8 @@ import ( type TagServiceInterface interface { CreateTag(tagBody models.CreateTagRequestBody) (*models.Tag, error) GetTag(id string) (*models.Tag, error) - DeleteTag(id string) error + UpdateTag(id string, tagBody models.UpdateTagRequestBody) (*models.Tag, error) + DeleteTag(id string) error } type TagService struct { @@ -41,6 +42,25 @@ func (t *TagService) GetTag(id string) (*models.Tag, error) { return transactions.GetTag(t.DB, *idAsUint) } +func (t *TagService) UpdateTag(id string, tagBody models.UpdateTagRequestBody) (*models.Tag, error) { + idAsUint, err := utilities.ValidateID(id) + + if err != nil { + return nil, err + } + + tag := models.Tag{ + Name: tagBody.Name, + CategoryID: tagBody.CategoryID, + } + + if err := utilities.ValidateData(tag); err != nil { + return nil, fiber.NewError(fiber.StatusBadRequest, "failed to validate the data") + } + + return transactions.UpdateTag(t.DB, *idAsUint, tag) +} + func (t *TagService) DeleteTag(id string) error { idAsUint, err := utilities.ValidateID(id) diff --git a/backend/src/transactions/tag.go b/backend/src/transactions/tag.go index 6f033babe..8ba91210a 100644 --- a/backend/src/transactions/tag.go +++ b/backend/src/transactions/tag.go @@ -30,6 +30,19 @@ func GetTag(db *gorm.DB, id uint) (*models.Tag, error) { return &tag, nil } +func UpdateTag(db *gorm.DB, id uint, tag models.Tag) (*models.Tag, error) { + if err := db.Model(&models.Tag{}).Where("id = ?", id).Updates(tag).First(&tag, id).Error; err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return nil, fiber.NewError(fiber.StatusNotFound, "failed to find tag") + } else { + return nil, fiber.NewError(fiber.StatusInternalServerError, "failed to update tag") + } + } + + return &tag, nil + +} + func DeleteTag(db *gorm.DB, id uint) error { if result := db.Delete(&models.Tag{}, id); result.RowsAffected == 0 { if result.Error != nil { diff --git a/backend/src/utilities/validator.go b/backend/src/utilities/validator.go index a161587c6..07ef87d87 100644 --- a/backend/src/utilities/validator.go +++ b/backend/src/utilities/validator.go @@ -17,6 +17,7 @@ func ValidateData(model interface{}) error { return nil } +// Validates that an id follows postgres uint format, returns a uint otherwise returns an error func ValidateID(id string) (*uint, error) { idAsInt, err := strconv.Atoi(id) diff --git a/backend/tests/api/category_test.go b/backend/tests/api/category_test.go index a43039a8c..83e6e0b2d 100644 --- a/backend/tests/api/category_test.go +++ b/backend/tests/api/category_test.go @@ -1,7 +1,6 @@ package tests import ( - "fmt" "net/http" "testing" @@ -72,7 +71,7 @@ func TestCreateCategoryIgnoresid(t *testing.T) { ) } -func AssertNoCategoryCreation(app TestApp, assert *assert.A, resp *http.Response) { +func AssertNoCategories(app TestApp, assert *assert.A, resp *http.Response) { AssertNumCategoriesRemainsAtN(app, assert, resp, 0) } @@ -99,7 +98,7 @@ func TestCreateCategoryFailsIfNameIsNotString(t *testing.T) { Status: 400, Message: "failed to process the request", }, - DBTester: AssertNoCategoryCreation, + DBTester: AssertNoCategories, }, ) } @@ -115,7 +114,7 @@ func TestCreateCategoryFailsIfNameIsMissing(t *testing.T) { Status: 400, Message: "failed to validate the data", }, - DBTester: AssertNoCategoryCreation, + DBTester: AssertNoCategories, }, ) } @@ -130,7 +129,6 @@ func TestCreateCategoryFailsIfCategoryWithThatNameAlreadyExists(t *testing.T) { } for _, permutation := range AllCasingPermutations(categoryName) { - fmt.Println(permutation) TestRequest{ Method: "POST", Path: "/api/v1/categories/", diff --git a/backend/tests/api/tag_test.go b/backend/tests/api/tag_test.go index 61675eb1a..2b35f7820 100644 --- a/backend/tests/api/tag_test.go +++ b/backend/tests/api/tag_test.go @@ -12,14 +12,26 @@ import ( "github.com/goccy/go-json" ) +var AssertRespTagSameAsDBTag = func(app TestApp, assert *assert.A, resp *http.Response) { + var respTag models.Tag + + err := json.NewDecoder(resp.Body).Decode(&respTag) + + assert.NilError(err) + + fmt.Printf("respTag: %+v\n", respTag) + fmt.Printf("respTag.ID: %+v\n", respTag.ID) + + dbTag, err := transactions.GetTag(app.Conn, respTag.ID) + + assert.NilError(err) + + assert.Equal(dbTag, &respTag) +} + func CreateSampleTag(t *testing.T, tagName string, categoryName string, existingAppAssert *ExistingAppAssert) ExistingAppAssert { - var appAssert ExistingAppAssert + appAssert := CreateSampleCategory(t, categoryName, existingAppAssert) - if existingAppAssert == nil { - appAssert = CreateSampleCategory(t, categoryName, existingAppAssert) - } else { - appAssert = CreateSampleCategory(t, categoryName, existingAppAssert) - } return TestRequest{ Method: "POST", Path: "/api/v1/tags/", @@ -29,26 +41,17 @@ func CreateSampleTag(t *testing.T, tagName string, categoryName string, existing }, }.TestOnStatusAndDB(t, &appAssert, DBTesterWithStatus{ - Status: 201, - DBTester: func(app TestApp, assert *assert.A, resp *http.Response) { - var respTag models.Tag - - err := json.NewDecoder(resp.Body).Decode(&respTag) - - assert.NilError(err) - - dbTag, err := transactions.GetTag(app.Conn, respTag.ID) - - assert.NilError(err) - - assert.Equal(dbTag, &respTag) - }, + Status: 201, + DBTester: AssertRespTagSameAsDBTag, }, ) } -var AssertNoTags = func(app TestApp, assert *assert.A, resp *http.Response) { +func TestCreateTagWorks(t *testing.T) { + CreateSampleTag(t, "Generate", "Science", nil) +} +var AssertNoTags = func(app TestApp, assert *assert.A, resp *http.Response) { var tags []models.Tag err := app.Conn.Find(&tags).Error @@ -123,20 +126,8 @@ func TestGetTagWorks(t *testing.T) { Path: "/api/v1/tags/1", }.TestOnStatusAndDB(t, &existingAppAssert, DBTesterWithStatus{ - Status: 200, - DBTester: func(app TestApp, assert *assert.A, resp *http.Response) { - var respTag models.Tag - - err := json.NewDecoder(resp.Body).Decode(&respTag) - - assert.NilError(err) - - dbTag, err := transactions.GetTag(app.Conn, respTag.ID) - - assert.NilError(err) - - assert.Equal(dbTag, &respTag) - }, + Status: 200, + DBTester: AssertRespTagSameAsDBTag, }, ) } @@ -174,6 +165,91 @@ func TestGetTagFailsNotFound(t *testing.T) { }, ) } + +func TestUpdateTagWorksUpdateName(t *testing.T) { + existingAppAssert := CreateSampleTag(t, "Generate", "Science", nil) + + TestRequest{ + Method: "PATCH", + Path: "/api/v1/tags/1", + Body: &map[string]interface{}{ + "name": "GenerateNU", + "category_id": 1, + }, + }.TestOnStatusAndDB(t, &existingAppAssert, + DBTesterWithStatus{ + Status: 200, + DBTester: AssertRespTagSameAsDBTag, + }, + ) +} + +func TestUpdateTagWorksUpdateCategory(t *testing.T) { + existingAppAssert := CreateSampleTag(t, "Generate", "Science", nil) + existingAppAssert = CreateSampleCategory(t, "Technology", &existingAppAssert) + + TestRequest{ + Method: "PATCH", + Path: "/api/v1/tags/1", + Body: &map[string]interface{}{ + "name": "Generate", + "category_id": 2, + }, + }.TestOnStatusAndDB(t, &existingAppAssert, + DBTesterWithStatus{ + Status: 200, + DBTester: AssertRespTagSameAsDBTag, + }, + ) +} + +func TestUpdateTagWorksWithSameDetails(t *testing.T) { + existingAppAssert := CreateSampleTag(t, "Generate", "Science", nil) + + TestRequest{ + Method: "PATCH", + Path: "/api/v1/tags/1", + Body: &map[string]interface{}{ + "name": "Generate", + "category_id": 1, + }, + }.TestOnStatusAndDB(t, &existingAppAssert, + DBTesterWithStatus{ + Status: 200, + DBTester: AssertRespTagSameAsDBTag, + }, + ) +} + +func TestUpdateTagFailsBadRequest(t *testing.T) { + badBodys := []map[string]interface{}{ + { + "name": "Generate", + "category_id": "1", + }, + { + "name": 1, + "category_id": 1, + }, + } + + for _, badBody := range badBodys { + TestRequest{ + Method: "PATCH", + Path: "/api/v1/tags/1", + Body: &badBody, + }.TestOnStatusMessageAndDB(t, nil, + StatusMessageDBTester{ + MessageWithStatus: MessageWithStatus{ + Status: 400, + Message: "failed to process the request", + }, + DBTester: AssertNoTags, + }, + ) + } +} + func TestDeleteTagWorks(t *testing.T) { existingAppAssert := CreateSampleTag(t, "Generate", "Science", nil)