diff --git a/backend/src/controllers/user_tag.go b/backend/src/controllers/user_tag.go index 66db53f4f..843459e3e 100644 --- a/backend/src/controllers/user_tag.go +++ b/backend/src/controllers/user_tag.go @@ -66,3 +66,27 @@ func (ut *UserTagController) CreateUserTags(c *fiber.Ctx) error { return c.Status(fiber.StatusCreated).JSON(&tags) } + +// DeleteUserTag godoc +// +// @Summary Create user tags +// @Description Creates tags for a user +// @ID create-user-tags +// @Tags user-tag +// @Accept json +// @Produce json +// @Param userID path string true "User ID" +// @Success 201 {object} []models.Tag +// @Failure 400 {object} errors.Error +// @Failure 401 {object} errors.Error +// @Failure 404 {object} errors.Error +// @Failure 500 {object} errors.Error +// @Router /users/{userID}/tags/ [post] +func (ut *UserTagController) DeleteUserTag(c *fiber.Ctx) error { + err := ut.userTagService.DeleteUserTag(c.Params("userID"), c.Params("tagID")) + if err != nil { + return err.FiberError(c) + } + + return c.SendStatus(fiber.StatusNoContent) +} diff --git a/backend/src/server/routes/user_tag.go b/backend/src/server/routes/user_tag.go index e2f83380b..5992aa117 100644 --- a/backend/src/server/routes/user_tag.go +++ b/backend/src/server/routes/user_tag.go @@ -14,4 +14,7 @@ func UserTag(usersRouter fiber.Router, userTagService services.UserTagServiceInt userTags.Post("/", userTagController.CreateUserTags) userTags.Get("/", userTagController.GetUserTags) + + tagID := userTags.Group("/:tagID") + tagID.Delete("/", userTagController.DeleteUserTag) } diff --git a/backend/src/services/user_tag.go b/backend/src/services/user_tag.go index 27f979f9d..de36dbbb2 100644 --- a/backend/src/services/user_tag.go +++ b/backend/src/services/user_tag.go @@ -12,6 +12,7 @@ import ( type UserTagServiceInterface interface { GetUserTags(id string) ([]models.Tag, *errors.Error) CreateUserTags(id string, tagIDs models.CreateUserTagsBody) ([]models.Tag, *errors.Error) + DeleteUserTag(id string, tagID string) *errors.Error } type UserTagService struct { @@ -52,3 +53,18 @@ func (u *UserTagService) CreateUserTags(id string, tagIDs models.CreateUserTagsB // Update the user to reflect the new tags: return transactions.CreateUserTags(u.DB, *idAsUUID, tags) } + +func (u *UserTagService) DeleteUserTag(id string, tagID string) *errors.Error { + // Validate the userID: + userIDAsUUID, err := utilities.ValidateID(id) + if err != nil { + return err + } + + tagIDAsUUID, err := utilities.ValidateID(tagID) + if err != nil { + return err + } + + return transactions.DeleteUserTag(u.DB, *userIDAsUUID, *tagIDAsUUID) +} diff --git a/backend/src/transactions/user_tag.go b/backend/src/transactions/user_tag.go index 3df46a0b8..49a193098 100644 --- a/backend/src/transactions/user_tag.go +++ b/backend/src/transactions/user_tag.go @@ -33,3 +33,21 @@ func CreateUserTags(db *gorm.DB, id uuid.UUID, tags []models.Tag) ([]models.Tag, return tags, nil } + +func DeleteUserTag(db *gorm.DB, id uuid.UUID, tagID uuid.UUID) *errors.Error { + user, err := GetUser(db, id, PreloadTag()) + if err != nil { + return err + } + + tag, err := GetTag(db, tagID) + if err != nil { + return err + } + + if err := db.Model(&user).Association("Tag").Delete(&tag); err != nil { + return &errors.FailedToUpdateUser + } + + return nil +} diff --git a/backend/tests/api/user_tag_test.go b/backend/tests/api/user_tag_test.go index e6a200c50..08da819ba 100644 --- a/backend/tests/api/user_tag_test.go +++ b/backend/tests/api/user_tag_test.go @@ -7,6 +7,7 @@ import ( "github.com/GenerateNU/sac/backend/src/errors" "github.com/GenerateNU/sac/backend/src/models" + "github.com/GenerateNU/sac/backend/src/transactions" h "github.com/GenerateNU/sac/backend/tests/api/helpers" "github.com/goccy/go-json" "github.com/gofiber/fiber/v2" @@ -368,3 +369,204 @@ func TestGetUserTagsReturnsCorrectList(t *testing.T) { }, ).Close() } + +func TestDeleteUserTagFailsOnNonExistentUser(t *testing.T) { + userID := uuid.New() + tagID := uuid.New() + + h.InitTest(t).TestOnErrorAndTester( + h.TestRequest{ + Method: fiber.MethodDelete, + Path: fmt.Sprintf("/api/v1/users/%s/tags/%s/", userID, tagID), + Role: &models.Super, + }, + h.ErrorWithTester{ + Error: errors.UserNotFound, + Tester: func(eaa h.ExistingAppAssert, resp *http.Response) { + var dbUser models.User + + err := eaa.App.Conn.First(&dbUser, userID).Error + + eaa.Assert.Assert(err != nil) + }, + }, + ).Close() +} + +func TestDeleteUserTagFailsOnNonExistentTag(t *testing.T) { + tagID := uuid.New() + + h.InitTest(t).TestOnErrorAndTester( + h.TestRequest{ + Method: fiber.MethodDelete, + Path: fmt.Sprintf("/api/v1/users/:userID/tags/%s/", tagID), + Role: &models.Super, + TestUserIDReplaces: h.StringToPointer(":userID"), + }, + h.ErrorWithTester{ + Error: errors.TagNotFound, + Tester: func(eaa h.ExistingAppAssert, resp *http.Response) { + var dbTag models.Tag + + err := eaa.App.Conn.First(&dbTag, tagID).Error + eaa.Assert.Assert(err != nil) + }, + }, + ).Close() +} + +func TestDeleteUserTagFailsOnInvalidUserUUID(t *testing.T) { + appAssert := h.InitTest(t) + + badUserUUIDs := []string{ + "0", + "-1", + "1.1", + "foo", + "null", + } + + for _, badUserUUID := range badUserUUIDs { + appAssert = appAssert.TestOnError( + h.TestRequest{ + Method: fiber.MethodDelete, + Path: fmt.Sprintf("/api/v1/users/%s/tags/%s/", badUserUUID, uuid.New()), + Role: &models.Super, + }, + errors.FailedToValidateID, + ) + } + + appAssert.Close() +} + +func TestDeleteUserTagFailsOnInvalidTagUUID(t *testing.T) { + appAssert := h.InitTest(t) + + badTagUUIDs := []string{ + "0", + "-1", + "1.1", + "foo", + "null", + } + + for _, badTagUUID := range badTagUUIDs { + appAssert = appAssert.TestOnError( + h.TestRequest{ + Method: fiber.MethodDelete, + Path: fmt.Sprintf("/api/v1/users/:userID/tags/%s/", badTagUUID), + Role: &models.Super, + TestUserIDReplaces: h.StringToPointer(":userID"), + }, + errors.FailedToValidateID, + ) + } + + appAssert.Close() +} + +func TestDeleteUserTagDoesNotAlterTagListOnNonAssociation(t *testing.T) { + tagUUIDs, appAssert := CreateSetOfTags(h.InitTest(t)) + appAssert.Assert.Assert(len(tagUUIDs) > 1) + + // Tag to be queried: + tagID := tagUUIDs[0] + + // Tags to be added to the user: + tagUUIDs = tagUUIDs[1:] + + appAssert.TestOnStatus( + h.TestRequest{ + Method: fiber.MethodPost, + Path: "/api/v1/users/:userID/tags/", + Body: SampleTagIDsFactory(&tagUUIDs), + Role: &models.Student, + TestUserIDReplaces: h.StringToPointer(":userID"), + }, + fiber.StatusCreated, + ) + + userTagsBeforeDeletion, err := transactions.GetUserTags(appAssert.App.Conn, appAssert.App.TestUser.UUID) + appAssert.Assert.NilError(&err) + + appAssert.TestOnStatusAndTester( + h.TestRequest{ + Method: fiber.MethodDelete, + Path: fmt.Sprintf("/api/v1/users/:userID/tags/%s/", tagID), + Role: &models.Super, + TestUserIDReplaces: h.StringToPointer(":userID"), + }, + h.TesterWithStatus{ + Status: fiber.StatusNoContent, + Tester: func(eaa h.ExistingAppAssert, resp *http.Response) { + var dbUser models.User + + err := eaa.App.Conn.Where("id = ?", appAssert.App.TestUser.UUID).Preload("Tag").First(&dbUser).Error + eaa.Assert.NilError(err) + + eaa.Assert.Equal(dbUser.Tag, userTagsBeforeDeletion) + }, + }, + ).Close() +} + +func TestDeleteUserTagRemovesTagFromUser(t *testing.T) { + tagUUIDs, appAssert := CreateSetOfTags(h.InitTest(t)) + appAssert.Assert.Assert(len(tagUUIDs) > 1) + + tagID := tagUUIDs[0] + + appAssert.TestOnStatusAndTester( + h.TestRequest{ + Method: fiber.MethodPost, + Path: "/api/v1/users/:userID/tags/", + Body: SampleTagIDsFactory(&tagUUIDs), + Role: &models.Student, + TestUserIDReplaces: h.StringToPointer(":userID"), + }, + h.TesterWithStatus{ + Status: fiber.StatusCreated, + Tester: func(eaa h.ExistingAppAssert, resp *http.Response) { + var dbUser models.User + + err := eaa.App.Conn.Where("id = ?", eaa.App.TestUser.UUID).Preload("Tag").First(&dbUser) + eaa.Assert.NilError(err) + + eaa.Assert.Equal(len(dbUser.Tag), len(tagUUIDs)) + + var dbTag models.Tag + + err = eaa.App.Conn.Where("id = ?", tagID).Preload("User").First(&dbTag) + eaa.Assert.NilError(err) + + eaa.Assert.Equal(len(dbTag.User), 1) + }, + }, + ).TestOnStatusAndTester( + h.TestRequest{ + Method: fiber.MethodDelete, + Path: fmt.Sprintf("/api/v1/users/:userID/tags/%s/", tagID), + Role: &models.Student, + TestUserIDReplaces: h.StringToPointer(":userID"), + }, + h.TesterWithStatus{ + Status: fiber.StatusNoContent, + Tester: func(eaa h.ExistingAppAssert, resp *http.Response) { + var dbUser models.User + + err := eaa.App.Conn.Where("id = ?", eaa.App.TestUser.UUID).Preload("Tag").First(&dbUser) + eaa.Assert.NilError(err) + + eaa.Assert.Equal(len(dbUser.Tag), len(tagUUIDs)-1) + + var dbTag models.Tag + + err = eaa.App.Conn.Where("id = ?", tagID).Preload("User").First(&dbTag) + eaa.Assert.NilError(err) + + eaa.Assert.Equal(len(dbTag.User), 0) + }, + }, + ) +}