Skip to content

Commit

Permalink
SAC-14 Update Tag Patch (#35)
Browse files Browse the repository at this point in the history
  • Loading branch information
garrettladley authored Jan 21, 2024
1 parent 77ddd0b commit 62e6293
Show file tree
Hide file tree
Showing 8 changed files with 193 additions and 44 deletions.
34 changes: 33 additions & 1 deletion backend/src/controllers/tag.go
Original file line number Diff line number Diff line change
Expand Up @@ -67,14 +67,46 @@ 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
// @Description Deletes a tag
// @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"
Expand Down
12 changes: 10 additions & 2 deletions backend/src/models/tag.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
1 change: 1 addition & 0 deletions backend/src/server/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
22 changes: 21 additions & 1 deletion backend/src/services/tag.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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)

Expand Down
13 changes: 13 additions & 0 deletions backend/src/transactions/tag.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
1 change: 1 addition & 0 deletions backend/src/utilities/validator.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down
8 changes: 3 additions & 5 deletions backend/tests/api/category_test.go
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
package tests

import (
"fmt"
"net/http"
"testing"

Expand Down Expand Up @@ -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)
}

Expand All @@ -99,7 +98,7 @@ func TestCreateCategoryFailsIfNameIsNotString(t *testing.T) {
Status: 400,
Message: "failed to process the request",
},
DBTester: AssertNoCategoryCreation,
DBTester: AssertNoCategories,
},
)
}
Expand All @@ -115,7 +114,7 @@ func TestCreateCategoryFailsIfNameIsMissing(t *testing.T) {
Status: 400,
Message: "failed to validate the data",
},
DBTester: AssertNoCategoryCreation,
DBTester: AssertNoCategories,
},
)
}
Expand All @@ -130,7 +129,6 @@ func TestCreateCategoryFailsIfCategoryWithThatNameAlreadyExists(t *testing.T) {
}

for _, permutation := range AllCasingPermutations(categoryName) {
fmt.Println(permutation)
TestRequest{
Method: "POST",
Path: "/api/v1/categories/",
Expand Down
146 changes: 111 additions & 35 deletions backend/tests/api/tag_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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/",
Expand All @@ -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
Expand Down Expand Up @@ -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,
},
)
}
Expand Down Expand Up @@ -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)

Expand Down

0 comments on commit 62e6293

Please sign in to comment.