From 52611a1761cf05c1431e4324bec5113dd8b4ae25 Mon Sep 17 00:00:00 2001 From: Harrison Healey Date: Fri, 21 Aug 2020 15:07:55 -0400 Subject: [PATCH] Move channel category code into its own files (#15319) * Move channel category code into its own files * Move channel category tests into app/channel_category_test.go --- api4/channel.go | 253 --- api4/channel_category.go | 264 +++ api4/channel_category_test.go | 139 ++ api4/channel_test.go | 127 -- app/channel.go | 105 -- app/channel_category.go | 117 ++ app/channel_category_test.go | 134 ++ app/channel_test.go | 121 -- store/sqlstore/channel_store.go | 960 ---------- store/sqlstore/channel_store_categories.go | 974 ++++++++++ .../sqlstore/channel_store_categories_test.go | 14 + store/storetest/channel_store.go | 1655 ---------------- store/storetest/channel_store_categories.go | 1678 +++++++++++++++++ 13 files changed, 3320 insertions(+), 3221 deletions(-) create mode 100644 api4/channel_category.go create mode 100644 api4/channel_category_test.go create mode 100644 app/channel_category.go create mode 100644 app/channel_category_test.go create mode 100644 store/sqlstore/channel_store_categories.go create mode 100644 store/sqlstore/channel_store_categories_test.go create mode 100644 store/storetest/channel_store_categories.go diff --git a/api4/channel.go b/api4/channel.go index a41fabc4bf..a68040bbfa 100644 --- a/api4/channel.go +++ b/api4/channel.go @@ -1895,256 +1895,3 @@ func moveChannel(c *Context, w http.ResponseWriter, r *http.Request) { w.Write([]byte(channel.ToJson())) } - -func getCategoriesForTeamForUser(c *Context, w http.ResponseWriter, r *http.Request) { - c.RequireUserId().RequireTeamId() - if c.Err != nil { - return - } - - if !c.App.SessionHasPermissionToUser(*c.App.Session(), c.Params.UserId) { - c.SetPermissionError(model.PERMISSION_EDIT_OTHER_USERS) - return - } - - categories, err := c.App.GetSidebarCategories(c.Params.UserId, c.Params.TeamId) - if err != nil { - c.Err = err - return - } - - w.Write(categories.ToJson()) -} - -func createCategoryForTeamForUser(c *Context, w http.ResponseWriter, r *http.Request) { - c.RequireUserId().RequireTeamId() - if c.Err != nil { - return - } - - if !c.App.SessionHasPermissionToUser(*c.App.Session(), c.Params.UserId) { - c.SetPermissionError(model.PERMISSION_EDIT_OTHER_USERS) - return - } - - auditRec := c.MakeAuditRecord("createCategoryForTeamForUser", audit.Fail) - defer c.LogAuditRec(auditRec) - - categoryCreateRequest, err := model.SidebarCategoryFromJson(r.Body) - if err != nil || c.Params.UserId != categoryCreateRequest.UserId || c.Params.TeamId != categoryCreateRequest.TeamId { - c.SetInvalidParam("category") - return - } - if appErr := validateUserChannels("createCategoryForTeamForUser", c, c.Params.TeamId, c.Params.UserId, categoryCreateRequest.Channels); appErr != nil { - c.Err = appErr - return - } - category, appErr := c.App.CreateSidebarCategory(c.Params.UserId, c.Params.TeamId, categoryCreateRequest) - if appErr != nil { - c.Err = appErr - return - } - - auditRec.Success() - w.Write(category.ToJson()) -} - -func getCategoryOrderForTeamForUser(c *Context, w http.ResponseWriter, r *http.Request) { - c.RequireUserId().RequireTeamId() - if c.Err != nil { - return - } - - if !c.App.SessionHasPermissionToUser(*c.App.Session(), c.Params.UserId) { - c.SetPermissionError(model.PERMISSION_EDIT_OTHER_USERS) - return - } - - order, err := c.App.GetSidebarCategoryOrder(c.Params.UserId, c.Params.TeamId) - if err != nil { - c.Err = err - return - } - - w.Write([]byte(model.ArrayToJson(order))) -} - -func updateCategoryOrderForTeamForUser(c *Context, w http.ResponseWriter, r *http.Request) { - c.RequireUserId().RequireTeamId() - if c.Err != nil { - return - } - - if !c.App.SessionHasPermissionToUser(*c.App.Session(), c.Params.UserId) { - c.SetPermissionError(model.PERMISSION_EDIT_OTHER_USERS) - return - } - - auditRec := c.MakeAuditRecord("updateCategoryOrderForTeamForUser", audit.Fail) - defer c.LogAuditRec(auditRec) - - categoryOrder := model.ArrayFromJson(r.Body) - - for _, categoryId := range categoryOrder { - if !c.App.SessionHasPermissionToCategory(*c.App.Session(), c.Params.UserId, c.Params.TeamId, categoryId) { - c.SetInvalidParam("category") - return - } - } - - err := c.App.UpdateSidebarCategoryOrder(c.Params.UserId, c.Params.TeamId, categoryOrder) - if err != nil { - c.Err = err - return - } - - auditRec.Success() - w.Write([]byte(model.ArrayToJson(categoryOrder))) -} - -func getCategoryForTeamForUser(c *Context, w http.ResponseWriter, r *http.Request) { - c.RequireUserId().RequireTeamId().RequireCategoryId() - if c.Err != nil { - return - } - - if !c.App.SessionHasPermissionToCategory(*c.App.Session(), c.Params.UserId, c.Params.TeamId, c.Params.CategoryId) { - c.SetPermissionError(model.PERMISSION_EDIT_OTHER_USERS) - return - } - - categories, err := c.App.GetSidebarCategory(c.Params.CategoryId) - if err != nil { - c.Err = err - return - } - - w.Write(categories.ToJson()) -} - -func updateCategoriesForTeamForUser(c *Context, w http.ResponseWriter, r *http.Request) { - c.RequireUserId().RequireTeamId() - if c.Err != nil { - return - } - - if !c.App.SessionHasPermissionToUser(*c.App.Session(), c.Params.UserId) { - c.SetPermissionError(model.PERMISSION_EDIT_OTHER_USERS) - return - } - - auditRec := c.MakeAuditRecord("updateCategoriesForTeamForUser", audit.Fail) - defer c.LogAuditRec(auditRec) - - categoriesUpdateRequest, err := model.SidebarCategoriesFromJson(r.Body) - if err != nil { - c.SetInvalidParam("category") - return - } - var channelsToCheck []string - for _, category := range categoriesUpdateRequest { - if !c.App.SessionHasPermissionToCategory(*c.App.Session(), c.Params.UserId, c.Params.TeamId, category.Id) { - c.SetInvalidParam("category") - return - } - channelsToCheck = append(channelsToCheck, category.Channels...) - } - if appErr := validateUserChannels("updateCategoriesForTeamForUser", c, c.Params.TeamId, c.Params.UserId, channelsToCheck); appErr != nil { - c.Err = appErr - return - } - - categories, appErr := c.App.UpdateSidebarCategories(c.Params.UserId, c.Params.TeamId, categoriesUpdateRequest) - if appErr != nil { - c.Err = appErr - return - } - - auditRec.Success() - w.Write(model.SidebarCategoriesWithChannelsToJson(categories)) -} - -// validateUserChannels confirms that the given user is a member of the given channel IDs. Returns an error if the user -// is not a member of any channel or nil if the user is a member of each channel. -func validateUserChannels(operationName string, c *Context, teamId, userId string, channelIDs []string) *model.AppError { - channels, err := c.App.GetChannelsForUser(teamId, userId, true, 0) - if err != nil { - return model.NewAppError("Api4."+operationName, "api.invalid_channel", nil, err.Error(), http.StatusBadRequest) - } - - for _, channelId := range channelIDs { - found := false - for _, channel := range *channels { - if channel.Id == channelId { - found = true - break - } - } - - if !found { - return model.NewAppError("Api4."+operationName, "api.invalid_channel", nil, "", http.StatusBadRequest) - } - } - - return nil -} - -func updateCategoryForTeamForUser(c *Context, w http.ResponseWriter, r *http.Request) { - c.RequireUserId().RequireTeamId().RequireCategoryId() - if c.Err != nil { - return - } - - if !c.App.SessionHasPermissionToCategory(*c.App.Session(), c.Params.UserId, c.Params.TeamId, c.Params.CategoryId) { - c.SetPermissionError(model.PERMISSION_EDIT_OTHER_USERS) - return - } - - auditRec := c.MakeAuditRecord("updateCategoryForTeamForUser", audit.Fail) - defer c.LogAuditRec(auditRec) - - categoryUpdateRequest, err := model.SidebarCategoryFromJson(r.Body) - if err != nil || categoryUpdateRequest.TeamId != c.Params.TeamId || categoryUpdateRequest.UserId != c.Params.UserId { - c.SetInvalidParam("category") - return - } - - if appErr := validateUserChannels("updateCategoryForTeamForUser", c, c.Params.TeamId, c.Params.UserId, categoryUpdateRequest.Channels); appErr != nil { - c.Err = appErr - return - } - categoryUpdateRequest.Id = c.Params.CategoryId - - categories, appErr := c.App.UpdateSidebarCategories(c.Params.UserId, c.Params.TeamId, []*model.SidebarCategoryWithChannels{categoryUpdateRequest}) - if appErr != nil { - c.Err = appErr - return - } - - auditRec.Success() - w.Write(categories[0].ToJson()) -} - -func deleteCategoryForTeamForUser(c *Context, w http.ResponseWriter, r *http.Request) { - c.RequireUserId().RequireTeamId().RequireCategoryId() - if c.Err != nil { - return - } - - if !c.App.SessionHasPermissionToCategory(*c.App.Session(), c.Params.UserId, c.Params.TeamId, c.Params.CategoryId) { - c.SetPermissionError(model.PERMISSION_EDIT_OTHER_USERS) - return - } - - auditRec := c.MakeAuditRecord("deleteCategoryForTeamForUser", audit.Fail) - defer c.LogAuditRec(auditRec) - - appErr := c.App.DeleteSidebarCategory(c.Params.UserId, c.Params.TeamId, c.Params.CategoryId) - if appErr != nil { - c.Err = appErr - return - } - - auditRec.Success() - ReturnStatusOK(w) -} diff --git a/api4/channel_category.go b/api4/channel_category.go new file mode 100644 index 0000000000..72bcff2b8f --- /dev/null +++ b/api4/channel_category.go @@ -0,0 +1,264 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +package api4 + +import ( + "net/http" + + "github.com/mattermost/mattermost-server/v5/audit" + "github.com/mattermost/mattermost-server/v5/model" +) + +func getCategoriesForTeamForUser(c *Context, w http.ResponseWriter, r *http.Request) { + c.RequireUserId().RequireTeamId() + if c.Err != nil { + return + } + + if !c.App.SessionHasPermissionToUser(*c.App.Session(), c.Params.UserId) { + c.SetPermissionError(model.PERMISSION_EDIT_OTHER_USERS) + return + } + + categories, err := c.App.GetSidebarCategories(c.Params.UserId, c.Params.TeamId) + if err != nil { + c.Err = err + return + } + + w.Write(categories.ToJson()) +} + +func createCategoryForTeamForUser(c *Context, w http.ResponseWriter, r *http.Request) { + c.RequireUserId().RequireTeamId() + if c.Err != nil { + return + } + + if !c.App.SessionHasPermissionToUser(*c.App.Session(), c.Params.UserId) { + c.SetPermissionError(model.PERMISSION_EDIT_OTHER_USERS) + return + } + + auditRec := c.MakeAuditRecord("createCategoryForTeamForUser", audit.Fail) + defer c.LogAuditRec(auditRec) + + categoryCreateRequest, err := model.SidebarCategoryFromJson(r.Body) + if err != nil || c.Params.UserId != categoryCreateRequest.UserId || c.Params.TeamId != categoryCreateRequest.TeamId { + c.SetInvalidParam("category") + return + } + if appErr := validateUserChannels("createCategoryForTeamForUser", c, c.Params.TeamId, c.Params.UserId, categoryCreateRequest.Channels); appErr != nil { + c.Err = appErr + return + } + category, appErr := c.App.CreateSidebarCategory(c.Params.UserId, c.Params.TeamId, categoryCreateRequest) + if appErr != nil { + c.Err = appErr + return + } + + auditRec.Success() + w.Write(category.ToJson()) +} + +func getCategoryOrderForTeamForUser(c *Context, w http.ResponseWriter, r *http.Request) { + c.RequireUserId().RequireTeamId() + if c.Err != nil { + return + } + + if !c.App.SessionHasPermissionToUser(*c.App.Session(), c.Params.UserId) { + c.SetPermissionError(model.PERMISSION_EDIT_OTHER_USERS) + return + } + + order, err := c.App.GetSidebarCategoryOrder(c.Params.UserId, c.Params.TeamId) + if err != nil { + c.Err = err + return + } + + w.Write([]byte(model.ArrayToJson(order))) +} + +func updateCategoryOrderForTeamForUser(c *Context, w http.ResponseWriter, r *http.Request) { + c.RequireUserId().RequireTeamId() + if c.Err != nil { + return + } + + if !c.App.SessionHasPermissionToUser(*c.App.Session(), c.Params.UserId) { + c.SetPermissionError(model.PERMISSION_EDIT_OTHER_USERS) + return + } + + auditRec := c.MakeAuditRecord("updateCategoryOrderForTeamForUser", audit.Fail) + defer c.LogAuditRec(auditRec) + + categoryOrder := model.ArrayFromJson(r.Body) + + for _, categoryId := range categoryOrder { + if !c.App.SessionHasPermissionToCategory(*c.App.Session(), c.Params.UserId, c.Params.TeamId, categoryId) { + c.SetInvalidParam("category") + return + } + } + + err := c.App.UpdateSidebarCategoryOrder(c.Params.UserId, c.Params.TeamId, categoryOrder) + if err != nil { + c.Err = err + return + } + + auditRec.Success() + w.Write([]byte(model.ArrayToJson(categoryOrder))) +} + +func getCategoryForTeamForUser(c *Context, w http.ResponseWriter, r *http.Request) { + c.RequireUserId().RequireTeamId().RequireCategoryId() + if c.Err != nil { + return + } + + if !c.App.SessionHasPermissionToCategory(*c.App.Session(), c.Params.UserId, c.Params.TeamId, c.Params.CategoryId) { + c.SetPermissionError(model.PERMISSION_EDIT_OTHER_USERS) + return + } + + categories, err := c.App.GetSidebarCategory(c.Params.CategoryId) + if err != nil { + c.Err = err + return + } + + w.Write(categories.ToJson()) +} + +func updateCategoriesForTeamForUser(c *Context, w http.ResponseWriter, r *http.Request) { + c.RequireUserId().RequireTeamId() + if c.Err != nil { + return + } + + if !c.App.SessionHasPermissionToUser(*c.App.Session(), c.Params.UserId) { + c.SetPermissionError(model.PERMISSION_EDIT_OTHER_USERS) + return + } + + auditRec := c.MakeAuditRecord("updateCategoriesForTeamForUser", audit.Fail) + defer c.LogAuditRec(auditRec) + + categoriesUpdateRequest, err := model.SidebarCategoriesFromJson(r.Body) + if err != nil { + c.SetInvalidParam("category") + return + } + var channelsToCheck []string + for _, category := range categoriesUpdateRequest { + if !c.App.SessionHasPermissionToCategory(*c.App.Session(), c.Params.UserId, c.Params.TeamId, category.Id) { + c.SetInvalidParam("category") + return + } + channelsToCheck = append(channelsToCheck, category.Channels...) + } + if appErr := validateUserChannels("updateCategoriesForTeamForUser", c, c.Params.TeamId, c.Params.UserId, channelsToCheck); appErr != nil { + c.Err = appErr + return + } + + categories, appErr := c.App.UpdateSidebarCategories(c.Params.UserId, c.Params.TeamId, categoriesUpdateRequest) + if appErr != nil { + c.Err = appErr + return + } + + auditRec.Success() + w.Write(model.SidebarCategoriesWithChannelsToJson(categories)) +} + +// validateUserChannels confirms that the given user is a member of the given channel IDs. Returns an error if the user +// is not a member of any channel or nil if the user is a member of each channel. +func validateUserChannels(operationName string, c *Context, teamId, userId string, channelIDs []string) *model.AppError { + channels, err := c.App.GetChannelsForUser(teamId, userId, true, 0) + if err != nil { + return model.NewAppError("Api4."+operationName, "api.invalid_channel", nil, err.Error(), http.StatusBadRequest) + } + + for _, channelId := range channelIDs { + found := false + for _, channel := range *channels { + if channel.Id == channelId { + found = true + break + } + } + + if !found { + return model.NewAppError("Api4."+operationName, "api.invalid_channel", nil, "", http.StatusBadRequest) + } + } + + return nil +} + +func updateCategoryForTeamForUser(c *Context, w http.ResponseWriter, r *http.Request) { + c.RequireUserId().RequireTeamId().RequireCategoryId() + if c.Err != nil { + return + } + + if !c.App.SessionHasPermissionToCategory(*c.App.Session(), c.Params.UserId, c.Params.TeamId, c.Params.CategoryId) { + c.SetPermissionError(model.PERMISSION_EDIT_OTHER_USERS) + return + } + + auditRec := c.MakeAuditRecord("updateCategoryForTeamForUser", audit.Fail) + defer c.LogAuditRec(auditRec) + + categoryUpdateRequest, err := model.SidebarCategoryFromJson(r.Body) + if err != nil || categoryUpdateRequest.TeamId != c.Params.TeamId || categoryUpdateRequest.UserId != c.Params.UserId { + c.SetInvalidParam("category") + return + } + + if appErr := validateUserChannels("updateCategoryForTeamForUser", c, c.Params.TeamId, c.Params.UserId, categoryUpdateRequest.Channels); appErr != nil { + c.Err = appErr + return + } + categoryUpdateRequest.Id = c.Params.CategoryId + + categories, appErr := c.App.UpdateSidebarCategories(c.Params.UserId, c.Params.TeamId, []*model.SidebarCategoryWithChannels{categoryUpdateRequest}) + if appErr != nil { + c.Err = appErr + return + } + + auditRec.Success() + w.Write(categories[0].ToJson()) +} + +func deleteCategoryForTeamForUser(c *Context, w http.ResponseWriter, r *http.Request) { + c.RequireUserId().RequireTeamId().RequireCategoryId() + if c.Err != nil { + return + } + + if !c.App.SessionHasPermissionToCategory(*c.App.Session(), c.Params.UserId, c.Params.TeamId, c.Params.CategoryId) { + c.SetPermissionError(model.PERMISSION_EDIT_OTHER_USERS) + return + } + + auditRec := c.MakeAuditRecord("deleteCategoryForTeamForUser", audit.Fail) + defer c.LogAuditRec(auditRec) + + appErr := c.App.DeleteSidebarCategory(c.Params.UserId, c.Params.TeamId, c.Params.CategoryId) + if appErr != nil { + c.Err = appErr + return + } + + auditRec.Success() + ReturnStatusOK(w) +} diff --git a/api4/channel_category_test.go b/api4/channel_category_test.go new file mode 100644 index 0000000000..426068c7fa --- /dev/null +++ b/api4/channel_category_test.go @@ -0,0 +1,139 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +package api4 + +import ( + "testing" + + "github.com/mattermost/mattermost-server/v5/model" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestUpdateCategoryForTeamForUser(t *testing.T) { + t.Run("should update the channel order of the Channels category", func(t *testing.T) { + th := Setup(t).InitBasic() + defer th.TearDown() + + categories, resp := th.Client.GetSidebarCategoriesForTeamForUser(th.BasicUser.Id, th.BasicTeam.Id, "") + require.Nil(t, resp.Error) + require.Len(t, categories.Categories, 3) + require.Len(t, categories.Order, 3) + + channelsCategory := categories.Categories[1] + require.Equal(t, model.SidebarCategoryChannels, channelsCategory.Type) + require.Len(t, channelsCategory.Channels, 5) // Town Square, Off Topic, and the 3 channels created by InitBasic + + // Should return the correct values from the API + updatedCategory := &model.SidebarCategoryWithChannels{ + SidebarCategory: channelsCategory.SidebarCategory, + Channels: []string{channelsCategory.Channels[1], channelsCategory.Channels[0], channelsCategory.Channels[4], channelsCategory.Channels[3], channelsCategory.Channels[2]}, + } + + received, resp := th.Client.UpdateSidebarCategoryForTeamForUser(th.BasicUser.Id, th.BasicTeam.Id, channelsCategory.Id, updatedCategory) + assert.Nil(t, resp.Error) + assert.Equal(t, channelsCategory.Id, received.Id) + assert.Equal(t, updatedCategory.Channels, received.Channels) + + // And when requesting the category later + received, resp = th.Client.GetSidebarCategoryForTeamForUser(th.BasicUser.Id, th.BasicTeam.Id, channelsCategory.Id, "") + assert.Nil(t, resp.Error) + assert.Equal(t, channelsCategory.Id, received.Id) + assert.Equal(t, updatedCategory.Channels, received.Channels) + }) + + t.Run("should update the sort order of the DM category", func(t *testing.T) { + th := Setup(t).InitBasic() + defer th.TearDown() + + categories, resp := th.Client.GetSidebarCategoriesForTeamForUser(th.BasicUser.Id, th.BasicTeam.Id, "") + require.Nil(t, resp.Error) + require.Len(t, categories.Categories, 3) + require.Len(t, categories.Order, 3) + + dmsCategory := categories.Categories[2] + require.Equal(t, model.SidebarCategoryDirectMessages, dmsCategory.Type) + require.Equal(t, model.SidebarCategorySortRecent, dmsCategory.Sorting) + + // Should return the correct values from the API + updatedCategory := &model.SidebarCategoryWithChannels{ + SidebarCategory: dmsCategory.SidebarCategory, + Channels: dmsCategory.Channels, + } + updatedCategory.Sorting = model.SidebarCategorySortAlphabetical + + received, resp := th.Client.UpdateSidebarCategoryForTeamForUser(th.BasicUser.Id, th.BasicTeam.Id, dmsCategory.Id, updatedCategory) + assert.Nil(t, resp.Error) + assert.Equal(t, dmsCategory.Id, received.Id) + assert.Equal(t, model.SidebarCategorySortAlphabetical, received.Sorting) + + // And when requesting the category later + received, resp = th.Client.GetSidebarCategoryForTeamForUser(th.BasicUser.Id, th.BasicTeam.Id, dmsCategory.Id, "") + assert.Nil(t, resp.Error) + assert.Equal(t, dmsCategory.Id, received.Id) + assert.Equal(t, model.SidebarCategorySortAlphabetical, received.Sorting) + }) + + t.Run("should update the display name of a custom category", func(t *testing.T) { + th := Setup(t).InitBasic() + defer th.TearDown() + + customCategory, resp := th.Client.CreateSidebarCategoryForTeamForUser(th.BasicUser.Id, th.BasicTeam.Id, &model.SidebarCategoryWithChannels{ + SidebarCategory: model.SidebarCategory{ + UserId: th.BasicUser.Id, + TeamId: th.BasicTeam.Id, + DisplayName: "custom123", + }, + }) + require.Nil(t, resp.Error) + require.Equal(t, "custom123", customCategory.DisplayName) + + // Should return the correct values from the API + updatedCategory := &model.SidebarCategoryWithChannels{ + SidebarCategory: customCategory.SidebarCategory, + Channels: customCategory.Channels, + } + updatedCategory.DisplayName = "abcCustom" + + received, resp := th.Client.UpdateSidebarCategoryForTeamForUser(th.BasicUser.Id, th.BasicTeam.Id, customCategory.Id, updatedCategory) + assert.Nil(t, resp.Error) + assert.Equal(t, customCategory.Id, received.Id) + assert.Equal(t, updatedCategory.DisplayName, received.DisplayName) + + // And when requesting the category later + received, resp = th.Client.GetSidebarCategoryForTeamForUser(th.BasicUser.Id, th.BasicTeam.Id, customCategory.Id, "") + assert.Nil(t, resp.Error) + assert.Equal(t, customCategory.Id, received.Id) + assert.Equal(t, updatedCategory.DisplayName, received.DisplayName) + }) + + t.Run("should update the channel order of the category even if it contains archived channels", func(t *testing.T) { + th := Setup(t).InitBasic() + defer th.TearDown() + + categories, resp := th.Client.GetSidebarCategoriesForTeamForUser(th.BasicUser.Id, th.BasicTeam.Id, "") + require.Nil(t, resp.Error) + require.Len(t, categories.Categories, 3) + require.Len(t, categories.Order, 3) + + channelsCategory := categories.Categories[1] + require.Equal(t, model.SidebarCategoryChannels, channelsCategory.Type) + require.Len(t, channelsCategory.Channels, 5) // Town Square, Off Topic, and the 3 channels created by InitBasic + + // Delete one of the channels + _, resp = th.Client.DeleteChannel(th.BasicChannel.Id) + require.Nil(t, resp.Error) + + // Should still be able to reorder the channels + updatedCategory := &model.SidebarCategoryWithChannels{ + SidebarCategory: channelsCategory.SidebarCategory, + Channels: []string{channelsCategory.Channels[1], channelsCategory.Channels[0], channelsCategory.Channels[4], channelsCategory.Channels[3], channelsCategory.Channels[2]}, + } + + received, resp := th.Client.UpdateSidebarCategoryForTeamForUser(th.BasicUser.Id, th.BasicTeam.Id, channelsCategory.Id, updatedCategory) + require.Nil(t, resp.Error) + assert.Equal(t, channelsCategory.Id, received.Id) + assert.Equal(t, updatedCategory.Channels, received.Channels) + }) +} diff --git a/api4/channel_test.go b/api4/channel_test.go index e068932446..5bf9510345 100644 --- a/api4/channel_test.go +++ b/api4/channel_test.go @@ -4001,130 +4001,3 @@ func TestMoveChannel(t *testing.T) { require.Equal(t, team2.Id, newChannel.TeamId) }, "Should be able to (force) move channel by a member that is not member of target team") } - -func TestUpdateCategoryForTeamForUser(t *testing.T) { - t.Run("should update the channel order of the Channels category", func(t *testing.T) { - th := Setup(t).InitBasic() - defer th.TearDown() - - categories, resp := th.Client.GetSidebarCategoriesForTeamForUser(th.BasicUser.Id, th.BasicTeam.Id, "") - require.Nil(t, resp.Error) - require.Len(t, categories.Categories, 3) - require.Len(t, categories.Order, 3) - - channelsCategory := categories.Categories[1] - require.Equal(t, model.SidebarCategoryChannels, channelsCategory.Type) - require.Len(t, channelsCategory.Channels, 5) // Town Square, Off Topic, and the 3 channels created by InitBasic - - // Should return the correct values from the API - updatedCategory := &model.SidebarCategoryWithChannels{ - SidebarCategory: channelsCategory.SidebarCategory, - Channels: []string{channelsCategory.Channels[1], channelsCategory.Channels[0], channelsCategory.Channels[4], channelsCategory.Channels[3], channelsCategory.Channels[2]}, - } - - received, resp := th.Client.UpdateSidebarCategoryForTeamForUser(th.BasicUser.Id, th.BasicTeam.Id, channelsCategory.Id, updatedCategory) - assert.Nil(t, resp.Error) - assert.Equal(t, channelsCategory.Id, received.Id) - assert.Equal(t, updatedCategory.Channels, received.Channels) - - // And when requesting the category later - received, resp = th.Client.GetSidebarCategoryForTeamForUser(th.BasicUser.Id, th.BasicTeam.Id, channelsCategory.Id, "") - assert.Nil(t, resp.Error) - assert.Equal(t, channelsCategory.Id, received.Id) - assert.Equal(t, updatedCategory.Channels, received.Channels) - }) - - t.Run("should update the sort order of the DM category", func(t *testing.T) { - th := Setup(t).InitBasic() - defer th.TearDown() - - categories, resp := th.Client.GetSidebarCategoriesForTeamForUser(th.BasicUser.Id, th.BasicTeam.Id, "") - require.Nil(t, resp.Error) - require.Len(t, categories.Categories, 3) - require.Len(t, categories.Order, 3) - - dmsCategory := categories.Categories[2] - require.Equal(t, model.SidebarCategoryDirectMessages, dmsCategory.Type) - require.Equal(t, model.SidebarCategorySortRecent, dmsCategory.Sorting) - - // Should return the correct values from the API - updatedCategory := &model.SidebarCategoryWithChannels{ - SidebarCategory: dmsCategory.SidebarCategory, - Channels: dmsCategory.Channels, - } - updatedCategory.Sorting = model.SidebarCategorySortAlphabetical - - received, resp := th.Client.UpdateSidebarCategoryForTeamForUser(th.BasicUser.Id, th.BasicTeam.Id, dmsCategory.Id, updatedCategory) - assert.Nil(t, resp.Error) - assert.Equal(t, dmsCategory.Id, received.Id) - assert.Equal(t, model.SidebarCategorySortAlphabetical, received.Sorting) - - // And when requesting the category later - received, resp = th.Client.GetSidebarCategoryForTeamForUser(th.BasicUser.Id, th.BasicTeam.Id, dmsCategory.Id, "") - assert.Nil(t, resp.Error) - assert.Equal(t, dmsCategory.Id, received.Id) - assert.Equal(t, model.SidebarCategorySortAlphabetical, received.Sorting) - }) - - t.Run("should update the display name of a custom category", func(t *testing.T) { - th := Setup(t).InitBasic() - defer th.TearDown() - - customCategory, resp := th.Client.CreateSidebarCategoryForTeamForUser(th.BasicUser.Id, th.BasicTeam.Id, &model.SidebarCategoryWithChannels{ - SidebarCategory: model.SidebarCategory{ - UserId: th.BasicUser.Id, - TeamId: th.BasicTeam.Id, - DisplayName: "custom123", - }, - }) - require.Nil(t, resp.Error) - require.Equal(t, "custom123", customCategory.DisplayName) - - // Should return the correct values from the API - updatedCategory := &model.SidebarCategoryWithChannels{ - SidebarCategory: customCategory.SidebarCategory, - Channels: customCategory.Channels, - } - updatedCategory.DisplayName = "abcCustom" - - received, resp := th.Client.UpdateSidebarCategoryForTeamForUser(th.BasicUser.Id, th.BasicTeam.Id, customCategory.Id, updatedCategory) - assert.Nil(t, resp.Error) - assert.Equal(t, customCategory.Id, received.Id) - assert.Equal(t, updatedCategory.DisplayName, received.DisplayName) - - // And when requesting the category later - received, resp = th.Client.GetSidebarCategoryForTeamForUser(th.BasicUser.Id, th.BasicTeam.Id, customCategory.Id, "") - assert.Nil(t, resp.Error) - assert.Equal(t, customCategory.Id, received.Id) - assert.Equal(t, updatedCategory.DisplayName, received.DisplayName) - }) - - t.Run("should update the channel order of the category even if it contains archived channels", func(t *testing.T) { - th := Setup(t).InitBasic() - defer th.TearDown() - - categories, resp := th.Client.GetSidebarCategoriesForTeamForUser(th.BasicUser.Id, th.BasicTeam.Id, "") - require.Nil(t, resp.Error) - require.Len(t, categories.Categories, 3) - require.Len(t, categories.Order, 3) - - channelsCategory := categories.Categories[1] - require.Equal(t, model.SidebarCategoryChannels, channelsCategory.Type) - require.Len(t, channelsCategory.Channels, 5) // Town Square, Off Topic, and the 3 channels created by InitBasic - - // Delete one of the channels - _, resp = th.Client.DeleteChannel(th.BasicChannel.Id) - require.Nil(t, resp.Error) - - // Should still be able to reorder the channels - updatedCategory := &model.SidebarCategoryWithChannels{ - SidebarCategory: channelsCategory.SidebarCategory, - Channels: []string{channelsCategory.Channels[1], channelsCategory.Channels[0], channelsCategory.Channels[4], channelsCategory.Channels[3], channelsCategory.Channels[2]}, - } - - received, resp := th.Client.UpdateSidebarCategoryForTeamForUser(th.BasicUser.Id, th.BasicTeam.Id, channelsCategory.Id, updatedCategory) - require.Nil(t, resp.Error) - assert.Equal(t, channelsCategory.Id, received.Id) - assert.Equal(t, updatedCategory.Channels, received.Channels) - }) -} diff --git a/app/channel.go b/app/channel.go index 39845739db..ab42e23985 100644 --- a/app/channel.go +++ b/app/channel.go @@ -2630,108 +2630,3 @@ func (a *App) ClearChannelMembersCache(channelID string) { page++ } } - -func (a *App) createInitialSidebarCategories(userId, teamId string) *model.AppError { - nErr := a.Srv().Store.Channel().CreateInitialSidebarCategories(userId, teamId) - - if nErr != nil { - return model.NewAppError("createInitialSidebarCategories", "app.channel.create_initial_sidebar_categories.internal_error", nil, nErr.Error(), http.StatusInternalServerError) - } - - return nil -} - -func (a *App) GetSidebarCategories(userId, teamId string) (*model.OrderedSidebarCategories, *model.AppError) { - categories, err := a.Srv().Store.Channel().GetSidebarCategories(userId, teamId) - - if err == nil && len(categories.Categories) == 0 { - // A user must always have categories, so migration must not have happened yet, and we should run it ourselves - nErr := a.createInitialSidebarCategories(userId, teamId) - if nErr != nil { - return nil, nErr - } - - categories, err = a.waitForSidebarCategories(userId, teamId) - } - - return categories, err -} - -// waitForSidebarCategories is used to get a user's sidebar categories after they've been created since there may be -// replication lag if any database replicas exist. It will wait until results are available to return them. -func (a *App) waitForSidebarCategories(userId, teamId string) (*model.OrderedSidebarCategories, *model.AppError) { - if len(a.Config().SqlSettings.DataSourceReplicas) == 0 { - // The categories should be available immediately on a single database - return a.Srv().Store.Channel().GetSidebarCategories(userId, teamId) - } - - now := model.GetMillis() - - for model.GetMillis()-now < 12000 { - time.Sleep(100 * time.Millisecond) - - categories, err := a.Srv().Store.Channel().GetSidebarCategories(userId, teamId) - - if err != nil || len(categories.Categories) > 0 { - // We've found something, so return - return categories, err - } - } - - mlog.Error("waitForSidebarCategories giving up", mlog.String("user_id", userId), mlog.String("team_id", teamId)) - - return &model.OrderedSidebarCategories{}, nil -} - -func (a *App) GetSidebarCategoryOrder(userId, teamId string) ([]string, *model.AppError) { - return a.Srv().Store.Channel().GetSidebarCategoryOrder(userId, teamId) -} - -func (a *App) GetSidebarCategory(categoryId string) (*model.SidebarCategoryWithChannels, *model.AppError) { - return a.Srv().Store.Channel().GetSidebarCategory(categoryId) -} - -func (a *App) CreateSidebarCategory(userId, teamId string, newCategory *model.SidebarCategoryWithChannels) (*model.SidebarCategoryWithChannels, *model.AppError) { - category, err := a.Srv().Store.Channel().CreateSidebarCategory(userId, teamId, newCategory) - if err != nil { - return nil, err - } - message := model.NewWebSocketEvent(model.WEBSOCKET_EVENT_SIDEBAR_CATEGORY_CREATED, teamId, "", userId, nil) - message.Add("category_id", category.Id) - a.Publish(message) - return category, nil -} - -func (a *App) UpdateSidebarCategoryOrder(userId, teamId string, categoryOrder []string) *model.AppError { - err := a.Srv().Store.Channel().UpdateSidebarCategoryOrder(userId, teamId, categoryOrder) - if err != nil { - return err - } - message := model.NewWebSocketEvent(model.WEBSOCKET_EVENT_SIDEBAR_CATEGORY_ORDER_UPDATED, teamId, "", userId, nil) - message.Add("order", categoryOrder) - a.Publish(message) - return nil -} - -func (a *App) UpdateSidebarCategories(userId, teamId string, categories []*model.SidebarCategoryWithChannels) ([]*model.SidebarCategoryWithChannels, *model.AppError) { - result, err := a.Srv().Store.Channel().UpdateSidebarCategories(userId, teamId, categories) - if err != nil { - return nil, err - } - message := model.NewWebSocketEvent(model.WEBSOCKET_EVENT_SIDEBAR_CATEGORY_UPDATED, teamId, "", userId, nil) - a.Publish(message) - return result, nil -} - -func (a *App) DeleteSidebarCategory(userId, teamId, categoryId string) *model.AppError { - err := a.Srv().Store.Channel().DeleteSidebarCategory(categoryId) - if err != nil { - return err - } - - message := model.NewWebSocketEvent(model.WEBSOCKET_EVENT_SIDEBAR_CATEGORY_DELETED, teamId, "", userId, nil) - message.Add("category_id", categoryId) - a.Publish(message) - - return nil -} diff --git a/app/channel_category.go b/app/channel_category.go new file mode 100644 index 0000000000..5f6e63ab1b --- /dev/null +++ b/app/channel_category.go @@ -0,0 +1,117 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +package app + +import ( + "net/http" + "time" + + "github.com/mattermost/mattermost-server/v5/mlog" + "github.com/mattermost/mattermost-server/v5/model" +) + +func (a *App) createInitialSidebarCategories(userId, teamId string) *model.AppError { + nErr := a.Srv().Store.Channel().CreateInitialSidebarCategories(userId, teamId) + + if nErr != nil { + return model.NewAppError("createInitialSidebarCategories", "app.channel.create_initial_sidebar_categories.internal_error", nil, nErr.Error(), http.StatusInternalServerError) + } + + return nil +} + +func (a *App) GetSidebarCategories(userId, teamId string) (*model.OrderedSidebarCategories, *model.AppError) { + categories, err := a.Srv().Store.Channel().GetSidebarCategories(userId, teamId) + + if err == nil && len(categories.Categories) == 0 { + // A user must always have categories, so migration must not have happened yet, and we should run it ourselves + nErr := a.createInitialSidebarCategories(userId, teamId) + if nErr != nil { + return nil, nErr + } + + categories, err = a.waitForSidebarCategories(userId, teamId) + } + + return categories, err +} + +// waitForSidebarCategories is used to get a user's sidebar categories after they've been created since there may be +// replication lag if any database replicas exist. It will wait until results are available to return them. +func (a *App) waitForSidebarCategories(userId, teamId string) (*model.OrderedSidebarCategories, *model.AppError) { + if len(a.Config().SqlSettings.DataSourceReplicas) == 0 { + // The categories should be available immediately on a single database + return a.Srv().Store.Channel().GetSidebarCategories(userId, teamId) + } + + now := model.GetMillis() + + for model.GetMillis()-now < 12000 { + time.Sleep(100 * time.Millisecond) + + categories, err := a.Srv().Store.Channel().GetSidebarCategories(userId, teamId) + + if err != nil || len(categories.Categories) > 0 { + // We've found something, so return + return categories, err + } + } + + mlog.Error("waitForSidebarCategories giving up", mlog.String("user_id", userId), mlog.String("team_id", teamId)) + + return &model.OrderedSidebarCategories{}, nil +} + +func (a *App) GetSidebarCategoryOrder(userId, teamId string) ([]string, *model.AppError) { + return a.Srv().Store.Channel().GetSidebarCategoryOrder(userId, teamId) +} + +func (a *App) GetSidebarCategory(categoryId string) (*model.SidebarCategoryWithChannels, *model.AppError) { + return a.Srv().Store.Channel().GetSidebarCategory(categoryId) +} + +func (a *App) CreateSidebarCategory(userId, teamId string, newCategory *model.SidebarCategoryWithChannels) (*model.SidebarCategoryWithChannels, *model.AppError) { + category, err := a.Srv().Store.Channel().CreateSidebarCategory(userId, teamId, newCategory) + if err != nil { + return nil, err + } + message := model.NewWebSocketEvent(model.WEBSOCKET_EVENT_SIDEBAR_CATEGORY_CREATED, teamId, "", userId, nil) + message.Add("category_id", category.Id) + a.Publish(message) + return category, nil +} + +func (a *App) UpdateSidebarCategoryOrder(userId, teamId string, categoryOrder []string) *model.AppError { + err := a.Srv().Store.Channel().UpdateSidebarCategoryOrder(userId, teamId, categoryOrder) + if err != nil { + return err + } + message := model.NewWebSocketEvent(model.WEBSOCKET_EVENT_SIDEBAR_CATEGORY_ORDER_UPDATED, teamId, "", userId, nil) + message.Add("order", categoryOrder) + a.Publish(message) + return nil +} + +func (a *App) UpdateSidebarCategories(userId, teamId string, categories []*model.SidebarCategoryWithChannels) ([]*model.SidebarCategoryWithChannels, *model.AppError) { + result, err := a.Srv().Store.Channel().UpdateSidebarCategories(userId, teamId, categories) + if err != nil { + return nil, err + } + message := model.NewWebSocketEvent(model.WEBSOCKET_EVENT_SIDEBAR_CATEGORY_UPDATED, teamId, "", userId, nil) + a.Publish(message) + return result, nil +} + +func (a *App) DeleteSidebarCategory(userId, teamId, categoryId string) *model.AppError { + err := a.Srv().Store.Channel().DeleteSidebarCategory(categoryId) + if err != nil { + return err + } + + message := model.NewWebSocketEvent(model.WEBSOCKET_EVENT_SIDEBAR_CATEGORY_DELETED, teamId, "", userId, nil) + message.Add("category_id", categoryId) + a.Publish(message) + + return nil +} diff --git a/app/channel_category_test.go b/app/channel_category_test.go new file mode 100644 index 0000000000..0051a62301 --- /dev/null +++ b/app/channel_category_test.go @@ -0,0 +1,134 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +package app + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/mattermost/mattermost-server/v5/model" +) + +func TestSidebarCategory(t *testing.T) { + th := Setup(t).InitBasic() + defer th.TearDown() + + basicChannel2 := th.CreateChannel(th.BasicTeam) + defer th.App.PermanentDeleteChannel(basicChannel2) + user := th.CreateUser() + defer th.App.Srv().Store.User().PermanentDelete(user.Id) + th.LinkUserToTeam(user, th.BasicTeam) + th.AddUserToChannel(user, basicChannel2) + + var createdCategory *model.SidebarCategoryWithChannels + t.Run("CreateSidebarCategory", func(t *testing.T) { + catData := model.SidebarCategoryWithChannels{ + SidebarCategory: model.SidebarCategory{ + DisplayName: "TEST", + }, + Channels: []string{th.BasicChannel.Id, basicChannel2.Id, basicChannel2.Id}, + } + _, err := th.App.CreateSidebarCategory(user.Id, th.BasicTeam.Id, &catData) + require.NotNil(t, err, "Should return error due to duplicate IDs") + catData.Channels = []string{th.BasicChannel.Id, basicChannel2.Id} + cat, err := th.App.CreateSidebarCategory(user.Id, th.BasicTeam.Id, &catData) + require.Nil(t, err, "Expected no error") + require.NotNil(t, cat, "Expected category object, got nil") + createdCategory = cat + }) + + t.Run("UpdateSidebarCategories", func(t *testing.T) { + require.NotNil(t, createdCategory) + createdCategory.Channels = []string{th.BasicChannel.Id} + updatedCat, err := th.App.UpdateSidebarCategories(user.Id, th.BasicTeam.Id, []*model.SidebarCategoryWithChannels{createdCategory}) + require.Nil(t, err, "Expected no error") + require.NotNil(t, updatedCat, "Expected category object, got nil") + require.Len(t, updatedCat, 1) + require.Len(t, updatedCat[0].Channels, 1) + require.Equal(t, updatedCat[0].Channels[0], th.BasicChannel.Id) + }) + + t.Run("UpdateSidebarCategoryOrder", func(t *testing.T) { + err := th.App.UpdateSidebarCategoryOrder(user.Id, th.BasicTeam.Id, []string{th.BasicChannel.Id, basicChannel2.Id}) + require.NotNil(t, err, "Should return error due to invalid order") + + actualOrder, err := th.App.GetSidebarCategoryOrder(user.Id, th.BasicTeam.Id) + require.Nil(t, err, "Should fetch order successfully") + + actualOrder[2], actualOrder[3] = actualOrder[3], actualOrder[2] + err = th.App.UpdateSidebarCategoryOrder(user.Id, th.BasicTeam.Id, actualOrder) + require.Nil(t, err, "Should update order successfully") + + actualOrder[2] = "asd" + err = th.App.UpdateSidebarCategoryOrder(user.Id, th.BasicTeam.Id, actualOrder) + require.NotNil(t, err, "Should return error due to invalid id") + }) + + t.Run("GetSidebarCategoryOrder", func(t *testing.T) { + catOrder, err := th.App.GetSidebarCategoryOrder(user.Id, th.BasicTeam.Id) + require.Nil(t, err, "Expected no error") + require.Len(t, catOrder, 4) + require.Equal(t, catOrder[1], createdCategory.Id, "the newly created category should be after favorites") + }) +} + +func TestGetSidebarCategories(t *testing.T) { + t.Run("should return the sidebar categories for the given user/team", func(t *testing.T) { + th := Setup(t).InitBasic() + defer th.TearDown() + + _, err := th.App.CreateSidebarCategory(th.BasicUser.Id, th.BasicTeam.Id, &model.SidebarCategoryWithChannels{ + SidebarCategory: model.SidebarCategory{ + UserId: th.BasicUser.Id, + TeamId: th.BasicTeam.Id, + DisplayName: "new category", + }, + }) + require.Nil(t, err) + + categories, err := th.App.GetSidebarCategories(th.BasicUser.Id, th.BasicTeam.Id) + assert.Nil(t, err) + assert.Len(t, categories.Categories, 4) + }) + + t.Run("should create the initial categories even if migration hasn't ran yet", func(t *testing.T) { + th := Setup(t).InitBasic() + defer th.TearDown() + + // Manually add the user to the team without going through the app layer to simulate a pre-existing user/team + // relationship that hasn't been migrated yet + team := th.CreateTeam() + _, err := th.App.Srv().Store.Team().SaveMember(&model.TeamMember{ + TeamId: team.Id, + UserId: th.BasicUser.Id, + SchemeUser: true, + }, 100) + require.Nil(t, err) + + categories, err := th.App.GetSidebarCategories(th.BasicUser.Id, team.Id) + assert.Nil(t, err) + assert.Len(t, categories.Categories, 3) + }) + + t.Run("should return a store error if a db table is missing", func(t *testing.T) { + th := Setup(t).InitBasic() + defer th.TearDown() + + // Temporarily renaming a table to force a DB error. + sqlSupplier := mainHelper.GetSQLSupplier() + _, err := sqlSupplier.GetMaster().Exec("ALTER TABLE SidebarCategories RENAME TO SidebarCategoriesTest") + require.Nil(t, err) + defer func() { + _, err := sqlSupplier.GetMaster().Exec("ALTER TABLE SidebarCategoriesTest RENAME TO SidebarCategories") + require.Nil(t, err) + }() + + categories, appErr := th.App.GetSidebarCategories(th.BasicUser.Id, th.BasicTeam.Id) + assert.Nil(t, categories) + assert.NotNil(t, appErr) + assert.Equal(t, "store.sql_channel.sidebar_categories.app_error", appErr.Id) + }) +} diff --git a/app/channel_test.go b/app/channel_test.go index ad06e15778..c6774ed742 100644 --- a/app/channel_test.go +++ b/app/channel_test.go @@ -1898,124 +1898,3 @@ func TestClearChannelMembersCache(t *testing.T) { th.App.ClearChannelMembersCache("channelID") } - -func TestSidebarCategory(t *testing.T) { - th := Setup(t).InitBasic() - defer th.TearDown() - - basicChannel2 := th.CreateChannel(th.BasicTeam) - defer th.App.PermanentDeleteChannel(basicChannel2) - user := th.CreateUser() - defer th.App.Srv().Store.User().PermanentDelete(user.Id) - th.LinkUserToTeam(user, th.BasicTeam) - th.AddUserToChannel(user, basicChannel2) - - var createdCategory *model.SidebarCategoryWithChannels - t.Run("CreateSidebarCategory", func(t *testing.T) { - catData := model.SidebarCategoryWithChannels{ - SidebarCategory: model.SidebarCategory{ - DisplayName: "TEST", - }, - Channels: []string{th.BasicChannel.Id, basicChannel2.Id, basicChannel2.Id}, - } - _, err := th.App.CreateSidebarCategory(user.Id, th.BasicTeam.Id, &catData) - require.NotNil(t, err, "Should return error due to duplicate IDs") - catData.Channels = []string{th.BasicChannel.Id, basicChannel2.Id} - cat, err := th.App.CreateSidebarCategory(user.Id, th.BasicTeam.Id, &catData) - require.Nil(t, err, "Expected no error") - require.NotNil(t, cat, "Expected category object, got nil") - createdCategory = cat - }) - - t.Run("UpdateSidebarCategories", func(t *testing.T) { - require.NotNil(t, createdCategory) - createdCategory.Channels = []string{th.BasicChannel.Id} - updatedCat, err := th.App.UpdateSidebarCategories(user.Id, th.BasicTeam.Id, []*model.SidebarCategoryWithChannels{createdCategory}) - require.Nil(t, err, "Expected no error") - require.NotNil(t, updatedCat, "Expected category object, got nil") - require.Len(t, updatedCat, 1) - require.Len(t, updatedCat[0].Channels, 1) - require.Equal(t, updatedCat[0].Channels[0], th.BasicChannel.Id) - }) - - t.Run("UpdateSidebarCategoryOrder", func(t *testing.T) { - err := th.App.UpdateSidebarCategoryOrder(user.Id, th.BasicTeam.Id, []string{th.BasicChannel.Id, basicChannel2.Id}) - require.NotNil(t, err, "Should return error due to invalid order") - - actualOrder, err := th.App.GetSidebarCategoryOrder(user.Id, th.BasicTeam.Id) - require.Nil(t, err, "Should fetch order successfully") - - actualOrder[2], actualOrder[3] = actualOrder[3], actualOrder[2] - err = th.App.UpdateSidebarCategoryOrder(user.Id, th.BasicTeam.Id, actualOrder) - require.Nil(t, err, "Should update order successfully") - - actualOrder[2] = "asd" - err = th.App.UpdateSidebarCategoryOrder(user.Id, th.BasicTeam.Id, actualOrder) - require.NotNil(t, err, "Should return error due to invalid id") - }) - - t.Run("GetSidebarCategoryOrder", func(t *testing.T) { - catOrder, err := th.App.GetSidebarCategoryOrder(user.Id, th.BasicTeam.Id) - require.Nil(t, err, "Expected no error") - require.Len(t, catOrder, 4) - require.Equal(t, catOrder[1], createdCategory.Id, "the newly created category should be after favorites") - }) -} - -func TestGetSidebarCategories(t *testing.T) { - t.Run("should return the sidebar categories for the given user/team", func(t *testing.T) { - th := Setup(t).InitBasic() - defer th.TearDown() - - _, err := th.App.CreateSidebarCategory(th.BasicUser.Id, th.BasicTeam.Id, &model.SidebarCategoryWithChannels{ - SidebarCategory: model.SidebarCategory{ - UserId: th.BasicUser.Id, - TeamId: th.BasicTeam.Id, - DisplayName: "new category", - }, - }) - require.Nil(t, err) - - categories, err := th.App.GetSidebarCategories(th.BasicUser.Id, th.BasicTeam.Id) - assert.Nil(t, err) - assert.Len(t, categories.Categories, 4) - }) - - t.Run("should create the initial categories even if migration hasn't ran yet", func(t *testing.T) { - th := Setup(t).InitBasic() - defer th.TearDown() - - // Manually add the user to the team without going through the app layer to simulate a pre-existing user/team - // relationship that hasn't been migrated yet - team := th.CreateTeam() - _, err := th.App.Srv().Store.Team().SaveMember(&model.TeamMember{ - TeamId: team.Id, - UserId: th.BasicUser.Id, - SchemeUser: true, - }, 100) - require.Nil(t, err) - - categories, err := th.App.GetSidebarCategories(th.BasicUser.Id, team.Id) - assert.Nil(t, err) - assert.Len(t, categories.Categories, 3) - }) - - t.Run("should return a store error if a db table is missing", func(t *testing.T) { - th := Setup(t).InitBasic() - defer th.TearDown() - - // Temporarily renaming a table to force a DB error. - sqlSupplier := mainHelper.GetSQLSupplier() - _, err := sqlSupplier.GetMaster().Exec("ALTER TABLE SidebarCategories RENAME TO SidebarCategoriesTest") - require.Nil(t, err) - defer func() { - _, err := sqlSupplier.GetMaster().Exec("ALTER TABLE SidebarCategoriesTest RENAME TO SidebarCategories") - require.Nil(t, err) - }() - - categories, appErr := th.App.GetSidebarCategories(th.BasicUser.Id, th.BasicTeam.Id) - assert.Nil(t, categories) - assert.NotNil(t, appErr) - assert.Equal(t, "store.sql_channel.sidebar_categories.app_error", appErr.Id) - }) -} diff --git a/store/sqlstore/channel_store.go b/store/sqlstore/channel_store.go index bcc4c90567..e22653f414 100644 --- a/store/sqlstore/channel_store.go +++ b/store/sqlstore/channel_store.go @@ -436,210 +436,6 @@ func (s SqlChannelStore) createIndexesIfNotExists() { s.CreateIndexIfNotExists("idx_channels_scheme_id", "Channels", "SchemeId") } -func (s SqlChannelStore) CreateInitialSidebarCategories(userId, teamId string) error { - transaction, err := s.GetMaster().Begin() - if err != nil { - return errors.Wrap(err, "CreateInitialSidebarCategories: begin_transaction") - } - defer finalizeTransaction(transaction) - - if err := s.createInitialSidebarCategoriesT(transaction, userId, teamId); err != nil { - return errors.Wrap(err, "CreateInitialSidebarCategories: createInitialSidebarCategoriesT") - } - - if err := transaction.Commit(); err != nil { - return errors.Wrap(err, "CreateInitialSidebarCategories: commit_transaction") - } - - return nil -} - -func (s SqlChannelStore) createInitialSidebarCategoriesT(transaction *gorp.Transaction, userId, teamId string) error { - selectQuery, selectParams, _ := s.getQueryBuilder(). - Select("Type"). - From("SidebarCategories"). - Where(sq.Eq{ - "UserId": userId, - "TeamId": teamId, - "Type": []model.SidebarCategoryType{model.SidebarCategoryFavorites, model.SidebarCategoryChannels, model.SidebarCategoryDirectMessages}, - }).ToSql() - - var existingTypes []model.SidebarCategoryType - _, err := transaction.Select(&existingTypes, selectQuery, selectParams...) - if err != nil { - return errors.Wrap(err, "createInitialSidebarCategoriesT: failed to select existing categories") - } - - hasCategoryOfType := make(map[model.SidebarCategoryType]bool, len(existingTypes)) - for _, existingType := range existingTypes { - hasCategoryOfType[existingType] = true - } - - if !hasCategoryOfType[model.SidebarCategoryFavorites] { - favoritesCategoryId := model.NewId() - - // Create the SidebarChannels first since there's more opportunity for something to fail here - if err := s.migrateFavoritesToSidebarT(transaction, userId, teamId, favoritesCategoryId); err != nil { - return errors.Wrap(err, "createInitialSidebarCategoriesT: failed to migrate favorites to sidebar") - } - - if err := transaction.Insert(&model.SidebarCategory{ - DisplayName: "Favorites", // This will be retranslated by the client into the user's locale - Id: favoritesCategoryId, - UserId: userId, - TeamId: teamId, - Sorting: model.SidebarCategorySortDefault, - SortOrder: model.DefaultSidebarSortOrderFavorites, - Type: model.SidebarCategoryFavorites, - }); err != nil { - return errors.Wrap(err, "createInitialSidebarCategoriesT: failed to insert favorites category") - } - } - - if !hasCategoryOfType[model.SidebarCategoryChannels] { - if err := transaction.Insert(&model.SidebarCategory{ - DisplayName: "Channels", // This will be retranslateed by the client into the user's locale - Id: model.NewId(), - UserId: userId, - TeamId: teamId, - Sorting: model.SidebarCategorySortDefault, - SortOrder: model.DefaultSidebarSortOrderChannels, - Type: model.SidebarCategoryChannels, - }); err != nil { - return errors.Wrap(err, "createInitialSidebarCategoriesT: failed to insert channels category") - } - } - - if !hasCategoryOfType[model.SidebarCategoryDirectMessages] { - if err := transaction.Insert(&model.SidebarCategory{ - DisplayName: "Direct Messages", // This will be retranslateed by the client into the user's locale - Id: model.NewId(), - UserId: userId, - TeamId: teamId, - Sorting: model.SidebarCategorySortRecent, - SortOrder: model.DefaultSidebarSortOrderDMs, - Type: model.SidebarCategoryDirectMessages, - }); err != nil { - return errors.Wrap(err, "createInitialSidebarCategoriesT: failed to insert direct messages category") - } - } - - return nil -} - -type userMembership struct { - UserId string - ChannelId string - CategoryId string -} - -func (s SqlChannelStore) migrateMembershipToSidebar(transaction *gorp.Transaction, runningOrder *int64, sql string, args ...interface{}) ([]userMembership, error) { - var memberships []userMembership - if _, err := transaction.Select(&memberships, sql, args...); err != nil { - return nil, err - } - - for _, favorite := range memberships { - sql, args, _ := s.getQueryBuilder(). - Insert("SidebarChannels"). - Columns("ChannelId", "UserId", "CategoryId", "SortOrder"). - Values(favorite.ChannelId, favorite.UserId, favorite.CategoryId, *runningOrder).ToSql() - - if _, err := transaction.Exec(sql, args...); err != nil && !IsUniqueConstraintError(err, []string{"UserId", "PRIMARY"}) { - return nil, err - } - *runningOrder = *runningOrder + model.MinimalSidebarSortDistance - } - - if err := transaction.Commit(); err != nil { - return nil, err - } - return memberships, nil -} - -func (s SqlChannelStore) migrateFavoritesToSidebarT(transaction *gorp.Transaction, userId, teamId, favoritesCategoryId string) error { - favoritesQuery, favoritesParams, _ := s.getQueryBuilder(). - Select("Preferences.Name"). - From("Preferences"). - Join("Channels on Preferences.Name = Channels.Id"). - Join("ChannelMembers on Preferences.Name = ChannelMembers.ChannelId and Preferences.UserId = ChannelMembers.UserId"). - Where(sq.Eq{ - "Preferences.UserId": userId, - "Preferences.Category": model.PREFERENCE_CATEGORY_FAVORITE_CHANNEL, - "Preferences.Value": "true", - }). - Where(sq.Or{ - sq.Eq{"Channels.TeamId": teamId}, - sq.Eq{"Channels.TeamId": ""}, - }). - OrderBy( - "Channels.DisplayName", - "Channels.Name ASC", - ).ToSql() - - var favoriteChannelIds []string - if _, err := transaction.Select(&favoriteChannelIds, favoritesQuery, favoritesParams...); err != nil { - return errors.Wrap(err, "migrateFavoritesToSidebarT: unable to get favorite channel IDs") - } - - for i, channelId := range favoriteChannelIds { - if err := transaction.Insert(&model.SidebarChannel{ - ChannelId: channelId, - CategoryId: favoritesCategoryId, - UserId: userId, - SortOrder: int64(i * model.MinimalSidebarSortDistance), - }); err != nil { - return errors.Wrap(err, "migrateFavoritesToSidebarT: unable to insert SidebarChannel") - } - } - - return nil -} - -// MigrateFavoritesToSidebarChannels populates the SidebarChannels table by analyzing existing user preferences for favorites -// **IMPORTANT** This function should only be called from the migration task and shouldn't be used by itself -func (s SqlChannelStore) MigrateFavoritesToSidebarChannels(lastUserId string, runningOrder int64) (map[string]interface{}, error) { - transaction, err := s.GetMaster().Begin() - if err != nil { - return nil, err - } - - defer finalizeTransaction(transaction) - - sb := s. - getQueryBuilder(). - Select("Preferences.UserId", "Preferences.Name AS ChannelId", "SidebarCategories.Id AS CategoryId"). - From("Preferences"). - Where(sq.And{ - sq.Eq{"Preferences.Category": model.PREFERENCE_CATEGORY_FAVORITE_CHANNEL}, - sq.NotEq{"Preferences.Value": "false"}, - sq.NotEq{"SidebarCategories.Id": nil}, - sq.Gt{"Preferences.UserId": lastUserId}, - }). - LeftJoin("Channels ON (Channels.Id=Preferences.Name)"). - LeftJoin("SidebarCategories ON (SidebarCategories.UserId=Preferences.UserId AND SidebarCategories.Type='"+string(model.SidebarCategoryFavorites)+"' AND (SidebarCategories.TeamId=Channels.TeamId OR Channels.TeamId=''))"). - OrderBy("Preferences.UserId", "Channels.Name DESC"). - Limit(100) - - sql, args, err := sb.ToSql() - if err != nil { - return nil, err - } - - userFavorites, err := s.migrateMembershipToSidebar(transaction, &runningOrder, sql, args...) - if err != nil { - return nil, err - } - if len(userFavorites) == 0 { - return nil, nil - } - - data := make(map[string]interface{}) - data["UserId"] = userFavorites[len(userFavorites)-1].UserId - data["SortOrder"] = runningOrder - return data, nil -} - // MigratePublicChannels initializes the PublicChannels table with data created before this version // of the Mattermost server kept it up-to-date. func (s SqlChannelStore) MigratePublicChannels() error { @@ -3530,759 +3326,3 @@ func (s SqlChannelStore) GroupSyncedChannelCount() (int64, *model.AppError) { return count, nil } - -type sidebarCategoryForJoin struct { - model.SidebarCategory - ChannelId *string -} - -func (s SqlChannelStore) CreateSidebarCategory(userId, teamId string, newCategory *model.SidebarCategoryWithChannels) (*model.SidebarCategoryWithChannels, *model.AppError) { - transaction, err := s.GetMaster().Begin() - if err != nil { - return nil, model.NewAppError("SqlChannelStore.CreateSidebarCategory", "store.sql_channel.sidebar_categories.open_transaction.app_error", nil, err.Error(), http.StatusInternalServerError) - } - - defer finalizeTransaction(transaction) - - categoriesWithOrder, appErr := s.getSidebarCategoriesT(transaction, userId, teamId) - if appErr != nil { - return nil, appErr - } - if len(categoriesWithOrder.Categories) < 1 { - return nil, model.NewAppError("SqlChannelStore.CreateSidebarCategory", "store.sql_channel.sidebar_categories.app_error", nil, "", http.StatusInternalServerError) - } - newOrder := categoriesWithOrder.Order - newCategoryId := model.NewId() - newCategorySortOrder := 0 - /* - When a new category is created, it should be placed as follows: - 1. If the Favorites category is first, the new category should be placed after it - 2. Otherwise, the new category should be placed first. - */ - if categoriesWithOrder.Categories[0].Type == model.SidebarCategoryFavorites { - newOrder = append([]string{newOrder[0], newCategoryId}, newOrder[1:]...) - newCategorySortOrder = model.MinimalSidebarSortDistance - } else { - newOrder = append([]string{newCategoryId}, newOrder...) - } - - category := &model.SidebarCategory{ - DisplayName: newCategory.DisplayName, - Id: newCategoryId, - UserId: userId, - TeamId: teamId, - Sorting: model.SidebarCategorySortDefault, - SortOrder: int64(model.MinimalSidebarSortDistance * len(newOrder)), // first we place it at the end of the list - Type: model.SidebarCategoryCustom, - } - if err = transaction.Insert(category); err != nil { - return nil, model.NewAppError("SqlPostStore.CreateSidebarCategory", "store.sql_channel.sidebar_categories.app_error", nil, err.Error(), http.StatusInternalServerError) - } - - if len(newCategory.Channels) > 0 { - channelIdsKeys, deleteParams := MapStringsToQueryParams(newCategory.Channels, "ChannelId") - deleteParams["UserId"] = userId - deleteParams["TeamId"] = teamId - - // Remove any channels from their previous categories and add them to the new one - var deleteQuery string - if s.DriverName() == model.DATABASE_DRIVER_MYSQL { - deleteQuery = ` - DELETE - SidebarChannels - FROM - SidebarChannels - JOIN - SidebarCategories ON SidebarChannels.CategoryId = SidebarCategories.Id - WHERE - SidebarChannels.UserId = :UserId - AND SidebarChannels.ChannelId IN ` + channelIdsKeys + ` - AND SidebarCategories.TeamId = :TeamId` - } else { - deleteQuery = ` - DELETE FROM - SidebarChannels - USING - SidebarCategories - WHERE - SidebarChannels.CategoryId = SidebarCategories.Id - AND SidebarChannels.UserId = :UserId - AND SidebarChannels.ChannelId IN ` + channelIdsKeys + ` - AND SidebarCategories.TeamId = :TeamId` - } - - _, err = transaction.Exec(deleteQuery, deleteParams) - if err != nil { - return nil, model.NewAppError("SqlPostStore.CreateSidebarCategory", "store.sql_channel.sidebar_categories.app_error", nil, err.Error(), http.StatusInternalServerError) - } - - var channels []interface{} - for i, channelID := range newCategory.Channels { - channels = append(channels, &model.SidebarChannel{ - ChannelId: channelID, - CategoryId: newCategoryId, - SortOrder: int64(i * model.MinimalSidebarSortDistance), - UserId: userId, - }) - } - if err = transaction.Insert(channels...); err != nil { - return nil, model.NewAppError("SqlPostStore.CreateSidebarCategory", "store.sql_channel.sidebar_categories.app_error", nil, err.Error(), http.StatusInternalServerError) - } - } - - // now we re-order the categories according to the new order - if appErr := s.updateSidebarCategoryOrderT(transaction, userId, teamId, newOrder); appErr != nil { - return nil, appErr - } - - if err = transaction.Commit(); err != nil { - return nil, model.NewAppError("SqlChannelStore.CreateSidebarCategory", "store.sql_channel.sidebar_categories.commit_transaction.app_error", nil, err.Error(), http.StatusInternalServerError) - } - - // patch category to return proper sort order - category.SortOrder = int64(newCategorySortOrder) - result := &model.SidebarCategoryWithChannels{ - SidebarCategory: *category, - Channels: newCategory.Channels, - } - - return result, nil -} - -func (s SqlChannelStore) completePopulatingCategoryChannels(category *model.SidebarCategoryWithChannels) (*model.SidebarCategoryWithChannels, *model.AppError) { - transaction, err := s.GetMaster().Begin() - if err != nil { - return nil, model.NewAppError("SqlChannelStore.completePopulatingCategoryChannels", "store.sql_channel.sidebar_categories.open_transaction.app_error", nil, err.Error(), http.StatusInternalServerError) - } - defer finalizeTransaction(transaction) - - result, appErr := s.completePopulatingCategoryChannelsT(transaction, category) - if appErr != nil { - return nil, appErr - } - - if err = transaction.Commit(); err != nil { - return nil, model.NewAppError("SqlChannelStore.completePopulatingCategoryChannels", "store.sql_channel.sidebar_categories.commit_transaction.app_error", nil, err.Error(), http.StatusInternalServerError) - } - - return result, nil -} - -func (s SqlChannelStore) completePopulatingCategoryChannelsT(transation *gorp.Transaction, category *model.SidebarCategoryWithChannels) (*model.SidebarCategoryWithChannels, *model.AppError) { - if category.Type == model.SidebarCategoryCustom || category.Type == model.SidebarCategoryFavorites { - return category, nil - } - - var channelTypeFilter sq.Sqlizer - if category.Type == model.SidebarCategoryDirectMessages { - // any DM/GM channels that aren't in any category should be returned as part of the Direct Messages category - channelTypeFilter = sq.Eq{"Channels.Type": []string{model.CHANNEL_DIRECT, model.CHANNEL_GROUP}} - } else if category.Type == model.SidebarCategoryChannels { - // any public/private channels that are on the current team and aren't in any category should be returned as part of the Channels category - channelTypeFilter = sq.And{ - sq.Eq{"Channels.Type": []string{model.CHANNEL_OPEN, model.CHANNEL_PRIVATE}}, - sq.Eq{"Channels.TeamId": category.TeamId}, - } - } - - // A subquery that is true if the channel does not have a SidebarChannel entry for the current user on the current team - doesNotHaveSidebarChannel := sq.Select("1"). - Prefix("NOT EXISTS ("). - From("SidebarChannels"). - Join("SidebarCategories on SidebarChannels.CategoryId=SidebarCategories.Id"). - Where(sq.And{ - sq.Expr("SidebarChannels.ChannelId = ChannelMembers.ChannelId"), - sq.Eq{"SidebarCategories.UserId": category.UserId}, - sq.Eq{"SidebarCategories.TeamId": category.TeamId}, - }). - Suffix(")") - - var channels []string - sql, args, _ := s.getQueryBuilder(). - Select("Id"). - From("ChannelMembers"). - LeftJoin("Channels ON Channels.Id=ChannelMembers.ChannelId"). - Where(sq.And{ - sq.Eq{"ChannelMembers.UserId": category.UserId}, - channelTypeFilter, - sq.Eq{"Channels.DeleteAt": 0}, - doesNotHaveSidebarChannel, - }). - OrderBy("DisplayName ASC").ToSql() - - if _, err := transation.Select(&channels, sql, args...); err != nil { - return nil, model.NewAppError("SqlPostStore.completePopulatingCategoryChannelsT", "store.sql_channel.sidebar_categories.app_error", nil, err.Error(), http.StatusNotFound) - } - - category.Channels = append(channels, category.Channels...) - return category, nil -} - -func (s SqlChannelStore) GetSidebarCategory(categoryId string) (*model.SidebarCategoryWithChannels, *model.AppError) { - var categories []*sidebarCategoryForJoin - sql, args, _ := s.getQueryBuilder(). - Select("SidebarCategories.*", "SidebarChannels.ChannelId"). - From("SidebarCategories"). - LeftJoin("SidebarChannels ON SidebarChannels.CategoryId=SidebarCategories.Id"). - Where(sq.Eq{"SidebarCategories.Id": categoryId}). - OrderBy("SidebarChannels.SortOrder ASC").ToSql() - if _, err := s.GetReplica().Select(&categories, sql, args...); err != nil { - return nil, model.NewAppError("SqlPostStore.GetSidebarCategory", "store.sql_channel.sidebar_categories.app_error", nil, err.Error(), http.StatusNotFound) - } - result := &model.SidebarCategoryWithChannels{ - SidebarCategory: categories[0].SidebarCategory, - Channels: make([]string, 0), - } - for _, category := range categories { - if category.ChannelId != nil { - result.Channels = append(result.Channels, *category.ChannelId) - } - } - return s.completePopulatingCategoryChannels(result) -} - -func (s SqlChannelStore) getSidebarCategoriesT(transaction *gorp.Transaction, userId, teamId string) (*model.OrderedSidebarCategories, *model.AppError) { - oc := model.OrderedSidebarCategories{ - Categories: make(model.SidebarCategoriesWithChannels, 0), - Order: make([]string, 0), - } - - var categories []*sidebarCategoryForJoin - sql, args, _ := s.getQueryBuilder(). - Select("SidebarCategories.*", "SidebarChannels.ChannelId"). - From("SidebarCategories"). - LeftJoin("SidebarChannels ON SidebarChannels.CategoryId=Id"). - Where(sq.And{ - sq.Eq{"SidebarCategories.UserId": userId}, - sq.Eq{"SidebarCategories.TeamId": teamId}, - }). - OrderBy("SidebarCategories.SortOrder ASC, SidebarChannels.SortOrder ASC").ToSql() - - if _, err := transaction.Select(&categories, sql, args...); err != nil { - return nil, model.NewAppError("SqlPostStore.GetSidebarCategories", "store.sql_channel.sidebar_categories.app_error", nil, err.Error(), http.StatusNotFound) - } - for _, category := range categories { - var prevCategory *model.SidebarCategoryWithChannels - for _, existing := range oc.Categories { - if existing.Id == category.Id { - prevCategory = existing - break - } - } - if prevCategory == nil { - prevCategory = &model.SidebarCategoryWithChannels{ - SidebarCategory: category.SidebarCategory, - Channels: make([]string, 0), - } - oc.Categories = append(oc.Categories, prevCategory) - oc.Order = append(oc.Order, category.Id) - } - if category.ChannelId != nil { - prevCategory.Channels = append(prevCategory.Channels, *category.ChannelId) - } - } - for _, category := range oc.Categories { - if _, err := s.completePopulatingCategoryChannelsT(transaction, category); err != nil { - return nil, err - } - } - - return &oc, nil -} - -func (s SqlChannelStore) GetSidebarCategories(userId, teamId string) (*model.OrderedSidebarCategories, *model.AppError) { - transaction, err := s.GetMaster().Begin() - if err != nil { - return nil, model.NewAppError("SqlChannelStore.GetSidebarCategories", "store.sql_channel.sidebar_categories.open_transaction.app_error", nil, err.Error(), http.StatusInternalServerError) - } - - defer finalizeTransaction(transaction) - - oc, appErr := s.getSidebarCategoriesT(transaction, userId, teamId) - if appErr != nil { - return nil, appErr - } - - if err = transaction.Commit(); err != nil { - return nil, model.NewAppError("SqlChannelStore.GetSidebarCategories", "store.sql_channel.sidebar_categories.commit_transaction.app_error", nil, err.Error(), http.StatusInternalServerError) - } - - return oc, nil -} - -func (s SqlChannelStore) GetSidebarCategoryOrder(userId, teamId string) ([]string, *model.AppError) { - var ids []string - - sql, args, _ := s.getQueryBuilder(). - Select("Id"). - From("SidebarCategories"). - Where(sq.And{ - sq.Eq{"UserId": userId}, - sq.Eq{"TeamId": teamId}, - }). - OrderBy("SidebarCategories.SortOrder ASC").ToSql() - - if _, err := s.GetReplica().Select(&ids, sql, args...); err != nil { - return nil, model.NewAppError("SqlPostStore.GetSidebarCategoryOrder", "store.sql_channel.sidebar_categories.app_error", nil, err.Error(), http.StatusNotFound) - } - return ids, nil -} - -func (s SqlChannelStore) updateSidebarCategoryOrderT(transaction *gorp.Transaction, userId, teamId string, categoryOrder []string) *model.AppError { - var newOrder []interface{} - runningOrder := 0 - for _, categoryId := range categoryOrder { - newOrder = append(newOrder, &model.SidebarCategory{ - Id: categoryId, - SortOrder: int64(runningOrder), - }) - runningOrder += model.MinimalSidebarSortDistance - } - - // There's a bug in gorp where UpdateColumns messes up the stored query for any other attempt to use .Update or - // .UpdateColumns on this table, so it's okay to use here as long as we don't use those methods for SidebarCategories - // anywhere else. - if _, err := transaction.UpdateColumns(func(col *gorp.ColumnMap) bool { - return col.ColumnName == "SortOrder" - }, newOrder...); err != nil { - return model.NewAppError("SqlPostStore.UpdateSidebarCategoryOrder", "store.sql_channel.sidebar_categories.app_error", nil, err.Error(), http.StatusInternalServerError) - } - - return nil -} - -func (s SqlChannelStore) UpdateSidebarCategoryOrder(userId, teamId string, categoryOrder []string) *model.AppError { - transaction, err := s.GetMaster().Begin() - if err != nil { - return model.NewAppError("SqlChannelStore.UpdateSidebarCategoryOrder", "store.sql_channel.sidebar_categories.open_transaction.app_error", nil, err.Error(), http.StatusInternalServerError) - } - - defer finalizeTransaction(transaction) - - // Ensure no invalid categories are included and that no categories are left out - existingOrder, appErr := s.GetSidebarCategoryOrder(userId, teamId) - if appErr != nil { - return appErr - } - if len(existingOrder) != len(categoryOrder) { - return model.NewAppError("SqlPostStore.UpdateSidebarCategoryOrder", "store.sql_channel.sidebar_categories.app_error", nil, "Cannot update category order, passed list of categories different size than in DB", http.StatusInternalServerError) - } - for _, originalCategoryId := range existingOrder { - found := false - for _, newCategoryId := range categoryOrder { - if newCategoryId == originalCategoryId { - found = true - break - } - } - if !found { - return model.NewAppError("SqlPostStore.UpdateSidebarCategoryOrder", "store.sql_channel.sidebar_categories.app_error", nil, "Cannot update category order, passed list of categories contains unrecognized category IDs", http.StatusBadRequest) - } - } - - if appErr := s.updateSidebarCategoryOrderT(transaction, userId, teamId, categoryOrder); appErr != nil { - return appErr - } - - if err = transaction.Commit(); err != nil { - return model.NewAppError("SqlChannelStore.UpdateSidebarCategoryOrder", "store.sql_channel.sidebar_categories.commit_transaction.app_error", nil, err.Error(), http.StatusInternalServerError) - } - - return nil -} - -func (s SqlChannelStore) UpdateSidebarCategories(userId, teamId string, categories []*model.SidebarCategoryWithChannels) ([]*model.SidebarCategoryWithChannels, *model.AppError) { - transaction, err := s.GetMaster().Begin() - if err != nil { - return nil, model.NewAppError("SqlChannelStore.UpdateSidebarCategory", "store.sql_channel.sidebar_categories.open_transaction.app_error", nil, err.Error(), http.StatusInternalServerError) - } - defer finalizeTransaction(transaction) - - updatedCategories := []*model.SidebarCategoryWithChannels{} - for _, category := range categories { - originalCategory, appErr := s.GetSidebarCategory(category.Id) - if appErr != nil { - return nil, model.NewAppError("SqlPostStore.UpdateSidebarCategory", "store.sql_channel.sidebar_categories.app_error", nil, appErr.Error(), http.StatusInternalServerError) - } - - // Copy category to avoid modifying an argument - updatedCategory := &model.SidebarCategoryWithChannels{ - SidebarCategory: category.SidebarCategory, - } - - // Prevent any changes to read-only fields of SidebarCategories - updatedCategory.UserId = originalCategory.UserId - updatedCategory.TeamId = originalCategory.TeamId - updatedCategory.SortOrder = originalCategory.SortOrder - updatedCategory.Type = originalCategory.Type - - if updatedCategory.Type != model.SidebarCategoryCustom { - updatedCategory.DisplayName = originalCategory.DisplayName - } - - if category.Type != model.SidebarCategoryDirectMessages { - updatedCategory.Channels = make([]string, len(category.Channels)) - copy(updatedCategory.Channels, category.Channels) - } - - updateQuery, updateParams, _ := s.getQueryBuilder(). - Update("SidebarCategories"). - Set("DisplayName", updatedCategory.DisplayName). - Set("Sorting", updatedCategory.Sorting). - Where(sq.Eq{"Id": updatedCategory.Id}).ToSql() - - if _, err = transaction.Exec(updateQuery, updateParams...); err != nil { - return nil, model.NewAppError("SqlPostStore.UpdateSidebarCategory", "store.sql_channel.sidebar_categories.app_error", nil, err.Error(), http.StatusInternalServerError) - } - - // if we are updating DM category, it's order can't channel order cannot be changed. - if category.Type != model.SidebarCategoryDirectMessages { - // Remove any SidebarChannels entries that were either: - // - previously in this category (and any ones that are still in the category will be recreated below) - // - in another category and are being added to this category - sql, args, _ := s.getQueryBuilder(). - Delete("SidebarChannels"). - Where( - sq.And{ - sq.Or{ - sq.Eq{"ChannelId": originalCategory.Channels}, - sq.Eq{"ChannelId": updatedCategory.Channels}, - }, - sq.Eq{"CategoryId": category.Id}, - }, - ).ToSql() - - if _, err = transaction.Exec(sql, args...); err != nil { - return nil, model.NewAppError("SqlPostStore.UpdateSidebarCategory", "store.sql_channel.sidebar_categories.app_error", nil, err.Error(), http.StatusInternalServerError) - } - - var channels []interface{} - runningOrder := 0 - for _, channelID := range category.Channels { - channels = append(channels, &model.SidebarChannel{ - ChannelId: channelID, - CategoryId: category.Id, - SortOrder: int64(runningOrder), - UserId: userId, - }) - runningOrder += model.MinimalSidebarSortDistance - } - - if err = transaction.Insert(channels...); err != nil { - return nil, model.NewAppError("SqlPostStore.UpdateSidebarCategory", "store.sql_channel.sidebar_categories.app_error", nil, err.Error(), http.StatusInternalServerError) - } - } - - // Update the favorites preferences based on channels moving into or out of the Favorites category for compatibility - if category.Type == model.SidebarCategoryFavorites { - // Remove any old favorites - sql, args, _ := s.getQueryBuilder().Delete("Preferences").Where( - sq.Eq{ - "UserId": userId, - "Name": originalCategory.Channels, - "Category": model.PREFERENCE_CATEGORY_FAVORITE_CHANNEL, - }, - ).ToSql() - - if _, err = transaction.Exec(sql, args...); err != nil { - return nil, model.NewAppError("SqlPostStore.UpdateSidebarCategory", "store.sql_channel.sidebar_categories.app_error", nil, err.Error(), http.StatusInternalServerError) - } - - // And then add the new ones - for _, channelID := range category.Channels { - // This breaks the PreferenceStore abstraction, but it should be safe to assume that everything is a SQL - // store in this package. - if err = s.Preference().(*SqlPreferenceStore).save(transaction, &model.Preference{ - Name: channelID, - UserId: userId, - Category: model.PREFERENCE_CATEGORY_FAVORITE_CHANNEL, - Value: "true", - }); err != nil { - return nil, model.NewAppError("SqlPostStore.UpdateSidebarCategory", "store.sql_channel.sidebar_categories.app_error", nil, err.Error(), http.StatusInternalServerError) - } - } - } else { - // Remove any old favorites that might have been in this category - sql, args, _ := s.getQueryBuilder().Delete("Preferences").Where( - sq.Eq{ - "UserId": userId, - "Name": category.Channels, - "Category": model.PREFERENCE_CATEGORY_FAVORITE_CHANNEL, - }, - ).ToSql() - - if _, err = transaction.Exec(sql, args...); err != nil { - return nil, model.NewAppError("SqlPostStore.UpdateSidebarCategory", "store.sql_channel.sidebar_categories.app_error", nil, err.Error(), http.StatusInternalServerError) - } - } - - updatedCategories = append(updatedCategories, updatedCategory) - } - - // Ensure Channels are populated for Channels/Direct Messages category if they change - for i, updatedCategory := range updatedCategories { - populated, err := s.completePopulatingCategoryChannelsT(transaction, updatedCategory) - if err != nil { - return nil, model.NewAppError("SqlPostStore.UpdateSidebarCategory", "store.sql_channel.sidebar_categories.app_error", nil, err.Error(), http.StatusInternalServerError) - } - - updatedCategories[i] = populated - } - - if err = transaction.Commit(); err != nil { - return nil, model.NewAppError("SqlChannelStore.UpdateSidebarCategory", "store.sql_channel.sidebar_categories.commit_transaction.app_error", nil, err.Error(), http.StatusInternalServerError) - } - - return updatedCategories, nil -} - -// UpdateSidebarChannelsByPreferences is called when the Preference table is being updated to keep SidebarCategories in sync -// At the moment, it's only handling Favorites and NOT DMs/GMs (those will be handled client side) -func (s SqlChannelStore) UpdateSidebarChannelsByPreferences(preferences *model.Preferences) error { - transaction, err := s.GetMaster().Begin() - if err != nil { - return errors.Wrap(err, "UpdateSidebarChannelsByPreferences: begin_transaction") - } - defer finalizeTransaction(transaction) - - for _, preference := range *preferences { - preference := preference - - if preference.Category != model.PREFERENCE_CATEGORY_FAVORITE_CHANNEL { - continue - } - - // if new preference is false - remove the channel from the appropriate sidebar category - if preference.Value == "false" { - if err := s.removeSidebarEntriesForPreferenceT(transaction, &preference); err != nil { - return errors.Wrap(err, "UpdateSidebarChannelsByPreferences: removeSidebarEntriesForPreferenceT") - } - } else { - if err := s.addChannelToFavoritesCategoryT(transaction, &preference); err != nil { - return errors.Wrap(err, "UpdateSidebarChannelsByPreferences: addChannelToFavoritesCategoryT") - } - } - } - - if err := transaction.Commit(); err != nil { - return errors.Wrap(err, "UpdateSidebarChannelsByPreferences: commit_transaction") - } - - return nil -} - -func (s SqlChannelStore) removeSidebarEntriesForPreferenceT(transaction *gorp.Transaction, preference *model.Preference) error { - if preference.Category != model.PREFERENCE_CATEGORY_FAVORITE_CHANNEL { - return nil - } - - // Delete any corresponding SidebarChannels entries in a Favorites category corresponding to this preference. This - // can't use the query builder because it uses DB-specific syntax - params := map[string]interface{}{ - "UserId": preference.UserId, - "ChannelId": preference.Name, - "CategoryType": model.SidebarCategoryFavorites, - } - var query string - if s.DriverName() == model.DATABASE_DRIVER_MYSQL { - query = ` - DELETE - SidebarChannels - FROM - SidebarChannels - JOIN - SidebarCategories ON SidebarChannels.CategoryId = SidebarCategories.Id - WHERE - SidebarChannels.UserId = :UserId - AND SidebarChannels.ChannelId = :ChannelId - AND SidebarCategories.Type = :CategoryType` - } else { - query = ` - DELETE FROM - SidebarChannels - USING - SidebarCategories - WHERE - SidebarChannels.CategoryId = SidebarCategories.Id - AND SidebarChannels.UserId = :UserId - AND SidebarChannels.ChannelId = :ChannelId - AND SidebarCategories.Type = :CategoryType` - } - - if _, err := transaction.Exec(query, params); err != nil { - return errors.Wrap(err, "Failed to remove sidebar entries for preference") - } - - return nil -} - -func (s SqlChannelStore) addChannelToFavoritesCategoryT(transaction *gorp.Transaction, preference *model.Preference) error { - if preference.Category != model.PREFERENCE_CATEGORY_FAVORITE_CHANNEL { - return nil - } - - var channel *model.Channel - if obj, err := transaction.Get(&model.Channel{}, preference.Name); err != nil { - return errors.Wrapf(err, "Failed to get favorited channel with id=%s", preference.Name) - } else { - channel = obj.(*model.Channel) - } - - // Get the IDs of the Favorites category/categories that the channel needs to be added to - builder := s.getQueryBuilder(). - Select("SidebarCategories.Id"). - From("SidebarCategories"). - LeftJoin("SidebarChannels on SidebarCategories.Id = SidebarChannels.CategoryId and SidebarChannels.ChannelId = ?", preference.Name). - Where(sq.Eq{ - "SidebarCategories.UserId": preference.UserId, - "Type": model.SidebarCategoryFavorites, - }). - Where("SidebarChannels.ChannelId is null") - - if channel.TeamId != "" { - builder = builder.Where(sq.Eq{"TeamId": channel.TeamId}) - } - - idsQuery, idsParams, _ := builder.ToSql() - - var categoryIds []string - if _, err := transaction.Select(&categoryIds, idsQuery, idsParams...); err != nil { - return errors.Wrap(err, "Failed to get Favorites sidebar categories") - } - - if len(categoryIds) == 0 { - // The channel is already in the Favorites category/categories - return nil - } - - // For each category ID, insert a row into SidebarChannels with the given channel ID and a SortOrder that's less than - // all existing SortOrders in the category so that the newly favorited channel comes first - insertQuery, insertParams, _ := s.getQueryBuilder(). - Insert("SidebarChannels"). - Columns( - "ChannelId", - "CategoryId", - "UserId", - "SortOrder", - ). - Select( - sq.Select(). - Column("? as ChannelId", preference.Name). - Column("SidebarCategories.Id as CategoryId"). - Column("? as UserId", preference.UserId). - Column("COALESCE(MIN(SidebarChannels.SortOrder) - 10, 0) as SortOrder"). - From("SidebarCategories"). - LeftJoin("SidebarChannels on SidebarCategories.Id = SidebarChannels.CategoryId"). - Where(sq.Eq{ - "SidebarCategories.Id": categoryIds, - }). - GroupBy("SidebarCategories.Id")).ToSql() - - if _, err := transaction.Exec(insertQuery, insertParams...); err != nil { - return errors.Wrap(err, "Failed to add sidebar entries for favorited channel") - } - - return nil -} - -// DeleteSidebarChannelsByPreferences is called when the Preference table is being updated to keep SidebarCategories in sync -// At the moment, it's only handling Favorites and NOT DMs/GMs (those will be handled client side) -func (s SqlChannelStore) DeleteSidebarChannelsByPreferences(preferences *model.Preferences) error { - transaction, err := s.GetMaster().Begin() - if err != nil { - return errors.Wrap(err, "DeleteSidebarChannelsByPreferences: begin_transaction") - } - defer finalizeTransaction(transaction) - - for _, preference := range *preferences { - preference := preference - - if preference.Category != model.PREFERENCE_CATEGORY_FAVORITE_CHANNEL { - continue - } - - if err := s.removeSidebarEntriesForPreferenceT(transaction, &preference); err != nil { - return errors.Wrap(err, "DeleteSidebarChannelsByPreferences: removeSidebarEntriesForPreferenceT") - } - } - - if err := transaction.Commit(); err != nil { - return errors.Wrap(err, "DeleteSidebarChannelsByPreferences: commit_transaction") - } - - return nil -} - -func (s SqlChannelStore) UpdateSidebarChannelCategoryOnMove(channel *model.Channel, newTeamId string) *model.AppError { - // if channel is being moved, remove it from the categories, since it's possible that there's no matching category in the new team - if _, err := s.GetMaster().Exec("DELETE FROM SidebarChannels WHERE ChannelId=:ChannelId", map[string]interface{}{"ChannelId": channel.Id}); err != nil { - return model.NewAppError("SqlChannelStore.UpdateSidebarChannelCategoryOnMove", "store.sql_channel.sidebar_categories.app_error", nil, err.Error(), http.StatusInternalServerError) - } - return nil -} - -func (s SqlChannelStore) ClearSidebarOnTeamLeave(userId, teamId string) *model.AppError { - // if user leaves the team, clean his team related entries in sidebar channels and categories - params := map[string]interface{}{ - "UserId": userId, - "TeamId": teamId, - } - - var deleteQuery string - if s.DriverName() == model.DATABASE_DRIVER_MYSQL { - deleteQuery = "DELETE SidebarChannels FROM SidebarChannels LEFT JOIN SidebarCategories ON SidebarCategories.Id = SidebarChannels.CategoryId WHERE SidebarCategories.TeamId=:TeamId AND SidebarCategories.UserId=:UserId" - } else { - deleteQuery = "DELETE FROM SidebarChannels USING SidebarChannels AS chan LEFT OUTER JOIN SidebarCategories AS cat ON cat.Id = chan.CategoryId WHERE cat.UserId = :UserId AND cat.TeamId = :TeamId" - } - if _, err := s.GetMaster().Exec(deleteQuery, params); err != nil { - return model.NewAppError("SqlChannelStore.ClearSidebarOnTeamLeave", "store.sql_channel.sidebar_categories.app_error", nil, err.Error(), http.StatusInternalServerError) - } - if _, err := s.GetMaster().Exec("DELETE FROM SidebarCategories WHERE SidebarCategories.TeamId = :TeamId AND SidebarCategories.UserId = :UserId", params); err != nil { - return model.NewAppError("SqlChannelStore.ClearSidebarOnTeamLeave", "store.sql_channel.sidebar_categories.app_error", nil, err.Error(), http.StatusInternalServerError) - } - return nil -} - -// DeleteSidebarCategory removes a custom category and moves any channels into it into the Channels and Direct Messages -// categories respectively. Assumes that the provided user ID and team ID match the given category ID. -func (s SqlChannelStore) DeleteSidebarCategory(categoryId string) *model.AppError { - transaction, err := s.GetMaster().Begin() - if err != nil { - return model.NewAppError("SqlChannelStore.DeleteSidebarCategory", "store.sql_channel.sidebar_categories.open_transaction.app_error", nil, err.Error(), http.StatusInternalServerError) - } - defer finalizeTransaction(transaction) - - // Ensure that we're deleting a custom category - var category *model.SidebarCategory - if err = transaction.SelectOne(&category, "SELECT * FROM SidebarCategories WHERE Id = :Id", map[string]interface{}{"Id": categoryId}); err != nil { - return model.NewAppError("SqlPostStore.DeleteSidebarCategory", "store.sql_channel.sidebar_categories.app_error", nil, err.Error(), http.StatusInternalServerError) - } - - if category.Type != model.SidebarCategoryCustom { - return model.NewAppError("SqlPostStore.DeleteSidebarCategory", "store.sql_channel.sidebar_categories.delete_invalid.app_error", nil, "", http.StatusBadRequest) - } - - // Delete the channels in the category - sql, args, _ := s.getQueryBuilder(). - Delete("SidebarChannels"). - Where(sq.Eq{"CategoryId": categoryId}).ToSql() - - if _, err := transaction.Exec(sql, args...); err != nil { - return model.NewAppError("SqlPostStore.DeleteSidebarCategory", "store.sql_channel.sidebar_categories.app_error", nil, err.Error(), http.StatusInternalServerError) - } - - // Delete the category itself - sql, args, _ = s.getQueryBuilder(). - Delete("SidebarCategories"). - Where(sq.Eq{"Id": categoryId}).ToSql() - - if _, err := transaction.Exec(sql, args...); err != nil { - return model.NewAppError("SqlChannelStore.DeleteSidebarCategory", "store.sql_channel.sidebar_categories.app_error", nil, err.Error(), http.StatusInternalServerError) - } - - if err := transaction.Commit(); err != nil { - return model.NewAppError("SqlChannelStore.DeleteSidebarCategory", "store.sql_channel.sidebar_categories.commit_transaction.app_error", nil, err.Error(), http.StatusInternalServerError) - } - - return nil -} diff --git a/store/sqlstore/channel_store_categories.go b/store/sqlstore/channel_store_categories.go new file mode 100644 index 0000000000..9e19f5683f --- /dev/null +++ b/store/sqlstore/channel_store_categories.go @@ -0,0 +1,974 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +package sqlstore + +import ( + "net/http" + + "github.com/mattermost/gorp" + "github.com/mattermost/mattermost-server/v5/model" + + sq "github.com/Masterminds/squirrel" + "github.com/pkg/errors" +) + +func (s SqlChannelStore) CreateInitialSidebarCategories(userId, teamId string) error { + transaction, err := s.GetMaster().Begin() + if err != nil { + return errors.Wrap(err, "CreateInitialSidebarCategories: begin_transaction") + } + defer finalizeTransaction(transaction) + + if err := s.createInitialSidebarCategoriesT(transaction, userId, teamId); err != nil { + return errors.Wrap(err, "CreateInitialSidebarCategories: createInitialSidebarCategoriesT") + } + + if err := transaction.Commit(); err != nil { + return errors.Wrap(err, "CreateInitialSidebarCategories: commit_transaction") + } + + return nil +} + +func (s SqlChannelStore) createInitialSidebarCategoriesT(transaction *gorp.Transaction, userId, teamId string) error { + selectQuery, selectParams, _ := s.getQueryBuilder(). + Select("Type"). + From("SidebarCategories"). + Where(sq.Eq{ + "UserId": userId, + "TeamId": teamId, + "Type": []model.SidebarCategoryType{model.SidebarCategoryFavorites, model.SidebarCategoryChannels, model.SidebarCategoryDirectMessages}, + }).ToSql() + + var existingTypes []model.SidebarCategoryType + _, err := transaction.Select(&existingTypes, selectQuery, selectParams...) + if err != nil { + return errors.Wrap(err, "createInitialSidebarCategoriesT: failed to select existing categories") + } + + hasCategoryOfType := make(map[model.SidebarCategoryType]bool, len(existingTypes)) + for _, existingType := range existingTypes { + hasCategoryOfType[existingType] = true + } + + if !hasCategoryOfType[model.SidebarCategoryFavorites] { + favoritesCategoryId := model.NewId() + + // Create the SidebarChannels first since there's more opportunity for something to fail here + if err := s.migrateFavoritesToSidebarT(transaction, userId, teamId, favoritesCategoryId); err != nil { + return errors.Wrap(err, "createInitialSidebarCategoriesT: failed to migrate favorites to sidebar") + } + + if err := transaction.Insert(&model.SidebarCategory{ + DisplayName: "Favorites", // This will be retranslated by the client into the user's locale + Id: favoritesCategoryId, + UserId: userId, + TeamId: teamId, + Sorting: model.SidebarCategorySortDefault, + SortOrder: model.DefaultSidebarSortOrderFavorites, + Type: model.SidebarCategoryFavorites, + }); err != nil { + return errors.Wrap(err, "createInitialSidebarCategoriesT: failed to insert favorites category") + } + } + + if !hasCategoryOfType[model.SidebarCategoryChannels] { + if err := transaction.Insert(&model.SidebarCategory{ + DisplayName: "Channels", // This will be retranslateed by the client into the user's locale + Id: model.NewId(), + UserId: userId, + TeamId: teamId, + Sorting: model.SidebarCategorySortDefault, + SortOrder: model.DefaultSidebarSortOrderChannels, + Type: model.SidebarCategoryChannels, + }); err != nil { + return errors.Wrap(err, "createInitialSidebarCategoriesT: failed to insert channels category") + } + } + + if !hasCategoryOfType[model.SidebarCategoryDirectMessages] { + if err := transaction.Insert(&model.SidebarCategory{ + DisplayName: "Direct Messages", // This will be retranslateed by the client into the user's locale + Id: model.NewId(), + UserId: userId, + TeamId: teamId, + Sorting: model.SidebarCategorySortRecent, + SortOrder: model.DefaultSidebarSortOrderDMs, + Type: model.SidebarCategoryDirectMessages, + }); err != nil { + return errors.Wrap(err, "createInitialSidebarCategoriesT: failed to insert direct messages category") + } + } + + return nil +} + +type userMembership struct { + UserId string + ChannelId string + CategoryId string +} + +func (s SqlChannelStore) migrateMembershipToSidebar(transaction *gorp.Transaction, runningOrder *int64, sql string, args ...interface{}) ([]userMembership, error) { + var memberships []userMembership + if _, err := transaction.Select(&memberships, sql, args...); err != nil { + return nil, err + } + + for _, favorite := range memberships { + sql, args, _ := s.getQueryBuilder(). + Insert("SidebarChannels"). + Columns("ChannelId", "UserId", "CategoryId", "SortOrder"). + Values(favorite.ChannelId, favorite.UserId, favorite.CategoryId, *runningOrder).ToSql() + + if _, err := transaction.Exec(sql, args...); err != nil && !IsUniqueConstraintError(err, []string{"UserId", "PRIMARY"}) { + return nil, err + } + *runningOrder = *runningOrder + model.MinimalSidebarSortDistance + } + + if err := transaction.Commit(); err != nil { + return nil, err + } + return memberships, nil +} + +func (s SqlChannelStore) migrateFavoritesToSidebarT(transaction *gorp.Transaction, userId, teamId, favoritesCategoryId string) error { + favoritesQuery, favoritesParams, _ := s.getQueryBuilder(). + Select("Preferences.Name"). + From("Preferences"). + Join("Channels on Preferences.Name = Channels.Id"). + Join("ChannelMembers on Preferences.Name = ChannelMembers.ChannelId and Preferences.UserId = ChannelMembers.UserId"). + Where(sq.Eq{ + "Preferences.UserId": userId, + "Preferences.Category": model.PREFERENCE_CATEGORY_FAVORITE_CHANNEL, + "Preferences.Value": "true", + }). + Where(sq.Or{ + sq.Eq{"Channels.TeamId": teamId}, + sq.Eq{"Channels.TeamId": ""}, + }). + OrderBy( + "Channels.DisplayName", + "Channels.Name ASC", + ).ToSql() + + var favoriteChannelIds []string + if _, err := transaction.Select(&favoriteChannelIds, favoritesQuery, favoritesParams...); err != nil { + return errors.Wrap(err, "migrateFavoritesToSidebarT: unable to get favorite channel IDs") + } + + for i, channelId := range favoriteChannelIds { + if err := transaction.Insert(&model.SidebarChannel{ + ChannelId: channelId, + CategoryId: favoritesCategoryId, + UserId: userId, + SortOrder: int64(i * model.MinimalSidebarSortDistance), + }); err != nil { + return errors.Wrap(err, "migrateFavoritesToSidebarT: unable to insert SidebarChannel") + } + } + + return nil +} + +// MigrateFavoritesToSidebarChannels populates the SidebarChannels table by analyzing existing user preferences for favorites +// **IMPORTANT** This function should only be called from the migration task and shouldn't be used by itself +func (s SqlChannelStore) MigrateFavoritesToSidebarChannels(lastUserId string, runningOrder int64) (map[string]interface{}, error) { + transaction, err := s.GetMaster().Begin() + if err != nil { + return nil, err + } + + defer finalizeTransaction(transaction) + + sb := s. + getQueryBuilder(). + Select("Preferences.UserId", "Preferences.Name AS ChannelId", "SidebarCategories.Id AS CategoryId"). + From("Preferences"). + Where(sq.And{ + sq.Eq{"Preferences.Category": model.PREFERENCE_CATEGORY_FAVORITE_CHANNEL}, + sq.NotEq{"Preferences.Value": "false"}, + sq.NotEq{"SidebarCategories.Id": nil}, + sq.Gt{"Preferences.UserId": lastUserId}, + }). + LeftJoin("Channels ON (Channels.Id=Preferences.Name)"). + LeftJoin("SidebarCategories ON (SidebarCategories.UserId=Preferences.UserId AND SidebarCategories.Type='"+string(model.SidebarCategoryFavorites)+"' AND (SidebarCategories.TeamId=Channels.TeamId OR Channels.TeamId=''))"). + OrderBy("Preferences.UserId", "Channels.Name DESC"). + Limit(100) + + sql, args, err := sb.ToSql() + if err != nil { + return nil, err + } + + userFavorites, err := s.migrateMembershipToSidebar(transaction, &runningOrder, sql, args...) + if err != nil { + return nil, err + } + if len(userFavorites) == 0 { + return nil, nil + } + + data := make(map[string]interface{}) + data["UserId"] = userFavorites[len(userFavorites)-1].UserId + data["SortOrder"] = runningOrder + return data, nil +} + +type sidebarCategoryForJoin struct { + model.SidebarCategory + ChannelId *string +} + +func (s SqlChannelStore) CreateSidebarCategory(userId, teamId string, newCategory *model.SidebarCategoryWithChannels) (*model.SidebarCategoryWithChannels, *model.AppError) { + transaction, err := s.GetMaster().Begin() + if err != nil { + return nil, model.NewAppError("SqlChannelStore.CreateSidebarCategory", "store.sql_channel.sidebar_categories.open_transaction.app_error", nil, err.Error(), http.StatusInternalServerError) + } + + defer finalizeTransaction(transaction) + + categoriesWithOrder, appErr := s.getSidebarCategoriesT(transaction, userId, teamId) + if appErr != nil { + return nil, appErr + } + if len(categoriesWithOrder.Categories) < 1 { + return nil, model.NewAppError("SqlChannelStore.CreateSidebarCategory", "store.sql_channel.sidebar_categories.app_error", nil, "", http.StatusInternalServerError) + } + newOrder := categoriesWithOrder.Order + newCategoryId := model.NewId() + newCategorySortOrder := 0 + /* + When a new category is created, it should be placed as follows: + 1. If the Favorites category is first, the new category should be placed after it + 2. Otherwise, the new category should be placed first. + */ + if categoriesWithOrder.Categories[0].Type == model.SidebarCategoryFavorites { + newOrder = append([]string{newOrder[0], newCategoryId}, newOrder[1:]...) + newCategorySortOrder = model.MinimalSidebarSortDistance + } else { + newOrder = append([]string{newCategoryId}, newOrder...) + } + + category := &model.SidebarCategory{ + DisplayName: newCategory.DisplayName, + Id: newCategoryId, + UserId: userId, + TeamId: teamId, + Sorting: model.SidebarCategorySortDefault, + SortOrder: int64(model.MinimalSidebarSortDistance * len(newOrder)), // first we place it at the end of the list + Type: model.SidebarCategoryCustom, + } + if err = transaction.Insert(category); err != nil { + return nil, model.NewAppError("SqlPostStore.CreateSidebarCategory", "store.sql_channel.sidebar_categories.app_error", nil, err.Error(), http.StatusInternalServerError) + } + + if len(newCategory.Channels) > 0 { + channelIdsKeys, deleteParams := MapStringsToQueryParams(newCategory.Channels, "ChannelId") + deleteParams["UserId"] = userId + deleteParams["TeamId"] = teamId + + // Remove any channels from their previous categories and add them to the new one + var deleteQuery string + if s.DriverName() == model.DATABASE_DRIVER_MYSQL { + deleteQuery = ` + DELETE + SidebarChannels + FROM + SidebarChannels + JOIN + SidebarCategories ON SidebarChannels.CategoryId = SidebarCategories.Id + WHERE + SidebarChannels.UserId = :UserId + AND SidebarChannels.ChannelId IN ` + channelIdsKeys + ` + AND SidebarCategories.TeamId = :TeamId` + } else { + deleteQuery = ` + DELETE FROM + SidebarChannels + USING + SidebarCategories + WHERE + SidebarChannels.CategoryId = SidebarCategories.Id + AND SidebarChannels.UserId = :UserId + AND SidebarChannels.ChannelId IN ` + channelIdsKeys + ` + AND SidebarCategories.TeamId = :TeamId` + } + + _, err = transaction.Exec(deleteQuery, deleteParams) + if err != nil { + return nil, model.NewAppError("SqlPostStore.CreateSidebarCategory", "store.sql_channel.sidebar_categories.app_error", nil, err.Error(), http.StatusInternalServerError) + } + + var channels []interface{} + for i, channelID := range newCategory.Channels { + channels = append(channels, &model.SidebarChannel{ + ChannelId: channelID, + CategoryId: newCategoryId, + SortOrder: int64(i * model.MinimalSidebarSortDistance), + UserId: userId, + }) + } + if err = transaction.Insert(channels...); err != nil { + return nil, model.NewAppError("SqlPostStore.CreateSidebarCategory", "store.sql_channel.sidebar_categories.app_error", nil, err.Error(), http.StatusInternalServerError) + } + } + + // now we re-order the categories according to the new order + if appErr := s.updateSidebarCategoryOrderT(transaction, userId, teamId, newOrder); appErr != nil { + return nil, appErr + } + + if err = transaction.Commit(); err != nil { + return nil, model.NewAppError("SqlChannelStore.CreateSidebarCategory", "store.sql_channel.sidebar_categories.commit_transaction.app_error", nil, err.Error(), http.StatusInternalServerError) + } + + // patch category to return proper sort order + category.SortOrder = int64(newCategorySortOrder) + result := &model.SidebarCategoryWithChannels{ + SidebarCategory: *category, + Channels: newCategory.Channels, + } + + return result, nil +} + +func (s SqlChannelStore) completePopulatingCategoryChannels(category *model.SidebarCategoryWithChannels) (*model.SidebarCategoryWithChannels, *model.AppError) { + transaction, err := s.GetMaster().Begin() + if err != nil { + return nil, model.NewAppError("SqlChannelStore.completePopulatingCategoryChannels", "store.sql_channel.sidebar_categories.open_transaction.app_error", nil, err.Error(), http.StatusInternalServerError) + } + defer finalizeTransaction(transaction) + + result, appErr := s.completePopulatingCategoryChannelsT(transaction, category) + if appErr != nil { + return nil, appErr + } + + if err = transaction.Commit(); err != nil { + return nil, model.NewAppError("SqlChannelStore.completePopulatingCategoryChannels", "store.sql_channel.sidebar_categories.commit_transaction.app_error", nil, err.Error(), http.StatusInternalServerError) + } + + return result, nil +} + +func (s SqlChannelStore) completePopulatingCategoryChannelsT(transation *gorp.Transaction, category *model.SidebarCategoryWithChannels) (*model.SidebarCategoryWithChannels, *model.AppError) { + if category.Type == model.SidebarCategoryCustom || category.Type == model.SidebarCategoryFavorites { + return category, nil + } + + var channelTypeFilter sq.Sqlizer + if category.Type == model.SidebarCategoryDirectMessages { + // any DM/GM channels that aren't in any category should be returned as part of the Direct Messages category + channelTypeFilter = sq.Eq{"Channels.Type": []string{model.CHANNEL_DIRECT, model.CHANNEL_GROUP}} + } else if category.Type == model.SidebarCategoryChannels { + // any public/private channels that are on the current team and aren't in any category should be returned as part of the Channels category + channelTypeFilter = sq.And{ + sq.Eq{"Channels.Type": []string{model.CHANNEL_OPEN, model.CHANNEL_PRIVATE}}, + sq.Eq{"Channels.TeamId": category.TeamId}, + } + } + + // A subquery that is true if the channel does not have a SidebarChannel entry for the current user on the current team + doesNotHaveSidebarChannel := sq.Select("1"). + Prefix("NOT EXISTS ("). + From("SidebarChannels"). + Join("SidebarCategories on SidebarChannels.CategoryId=SidebarCategories.Id"). + Where(sq.And{ + sq.Expr("SidebarChannels.ChannelId = ChannelMembers.ChannelId"), + sq.Eq{"SidebarCategories.UserId": category.UserId}, + sq.Eq{"SidebarCategories.TeamId": category.TeamId}, + }). + Suffix(")") + + var channels []string + sql, args, _ := s.getQueryBuilder(). + Select("Id"). + From("ChannelMembers"). + LeftJoin("Channels ON Channels.Id=ChannelMembers.ChannelId"). + Where(sq.And{ + sq.Eq{"ChannelMembers.UserId": category.UserId}, + channelTypeFilter, + sq.Eq{"Channels.DeleteAt": 0}, + doesNotHaveSidebarChannel, + }). + OrderBy("DisplayName ASC").ToSql() + + if _, err := transation.Select(&channels, sql, args...); err != nil { + return nil, model.NewAppError("SqlPostStore.completePopulatingCategoryChannelsT", "store.sql_channel.sidebar_categories.app_error", nil, err.Error(), http.StatusNotFound) + } + + category.Channels = append(channels, category.Channels...) + return category, nil +} + +func (s SqlChannelStore) GetSidebarCategory(categoryId string) (*model.SidebarCategoryWithChannels, *model.AppError) { + var categories []*sidebarCategoryForJoin + sql, args, _ := s.getQueryBuilder(). + Select("SidebarCategories.*", "SidebarChannels.ChannelId"). + From("SidebarCategories"). + LeftJoin("SidebarChannels ON SidebarChannels.CategoryId=SidebarCategories.Id"). + Where(sq.Eq{"SidebarCategories.Id": categoryId}). + OrderBy("SidebarChannels.SortOrder ASC").ToSql() + if _, err := s.GetReplica().Select(&categories, sql, args...); err != nil { + return nil, model.NewAppError("SqlPostStore.GetSidebarCategory", "store.sql_channel.sidebar_categories.app_error", nil, err.Error(), http.StatusNotFound) + } + result := &model.SidebarCategoryWithChannels{ + SidebarCategory: categories[0].SidebarCategory, + Channels: make([]string, 0), + } + for _, category := range categories { + if category.ChannelId != nil { + result.Channels = append(result.Channels, *category.ChannelId) + } + } + return s.completePopulatingCategoryChannels(result) +} + +func (s SqlChannelStore) getSidebarCategoriesT(transaction *gorp.Transaction, userId, teamId string) (*model.OrderedSidebarCategories, *model.AppError) { + oc := model.OrderedSidebarCategories{ + Categories: make(model.SidebarCategoriesWithChannels, 0), + Order: make([]string, 0), + } + + var categories []*sidebarCategoryForJoin + sql, args, _ := s.getQueryBuilder(). + Select("SidebarCategories.*", "SidebarChannels.ChannelId"). + From("SidebarCategories"). + LeftJoin("SidebarChannels ON SidebarChannels.CategoryId=Id"). + Where(sq.And{ + sq.Eq{"SidebarCategories.UserId": userId}, + sq.Eq{"SidebarCategories.TeamId": teamId}, + }). + OrderBy("SidebarCategories.SortOrder ASC, SidebarChannels.SortOrder ASC").ToSql() + + if _, err := transaction.Select(&categories, sql, args...); err != nil { + return nil, model.NewAppError("SqlPostStore.GetSidebarCategories", "store.sql_channel.sidebar_categories.app_error", nil, err.Error(), http.StatusNotFound) + } + for _, category := range categories { + var prevCategory *model.SidebarCategoryWithChannels + for _, existing := range oc.Categories { + if existing.Id == category.Id { + prevCategory = existing + break + } + } + if prevCategory == nil { + prevCategory = &model.SidebarCategoryWithChannels{ + SidebarCategory: category.SidebarCategory, + Channels: make([]string, 0), + } + oc.Categories = append(oc.Categories, prevCategory) + oc.Order = append(oc.Order, category.Id) + } + if category.ChannelId != nil { + prevCategory.Channels = append(prevCategory.Channels, *category.ChannelId) + } + } + for _, category := range oc.Categories { + if _, err := s.completePopulatingCategoryChannelsT(transaction, category); err != nil { + return nil, err + } + } + + return &oc, nil +} + +func (s SqlChannelStore) GetSidebarCategories(userId, teamId string) (*model.OrderedSidebarCategories, *model.AppError) { + transaction, err := s.GetMaster().Begin() + if err != nil { + return nil, model.NewAppError("SqlChannelStore.GetSidebarCategories", "store.sql_channel.sidebar_categories.open_transaction.app_error", nil, err.Error(), http.StatusInternalServerError) + } + + defer finalizeTransaction(transaction) + + oc, appErr := s.getSidebarCategoriesT(transaction, userId, teamId) + if appErr != nil { + return nil, appErr + } + + if err = transaction.Commit(); err != nil { + return nil, model.NewAppError("SqlChannelStore.GetSidebarCategories", "store.sql_channel.sidebar_categories.commit_transaction.app_error", nil, err.Error(), http.StatusInternalServerError) + } + + return oc, nil +} + +func (s SqlChannelStore) GetSidebarCategoryOrder(userId, teamId string) ([]string, *model.AppError) { + var ids []string + + sql, args, _ := s.getQueryBuilder(). + Select("Id"). + From("SidebarCategories"). + Where(sq.And{ + sq.Eq{"UserId": userId}, + sq.Eq{"TeamId": teamId}, + }). + OrderBy("SidebarCategories.SortOrder ASC").ToSql() + + if _, err := s.GetReplica().Select(&ids, sql, args...); err != nil { + return nil, model.NewAppError("SqlPostStore.GetSidebarCategoryOrder", "store.sql_channel.sidebar_categories.app_error", nil, err.Error(), http.StatusNotFound) + } + return ids, nil +} + +func (s SqlChannelStore) updateSidebarCategoryOrderT(transaction *gorp.Transaction, userId, teamId string, categoryOrder []string) *model.AppError { + var newOrder []interface{} + runningOrder := 0 + for _, categoryId := range categoryOrder { + newOrder = append(newOrder, &model.SidebarCategory{ + Id: categoryId, + SortOrder: int64(runningOrder), + }) + runningOrder += model.MinimalSidebarSortDistance + } + + // There's a bug in gorp where UpdateColumns messes up the stored query for any other attempt to use .Update or + // .UpdateColumns on this table, so it's okay to use here as long as we don't use those methods for SidebarCategories + // anywhere else. + if _, err := transaction.UpdateColumns(func(col *gorp.ColumnMap) bool { + return col.ColumnName == "SortOrder" + }, newOrder...); err != nil { + return model.NewAppError("SqlPostStore.UpdateSidebarCategoryOrder", "store.sql_channel.sidebar_categories.app_error", nil, err.Error(), http.StatusInternalServerError) + } + + return nil +} + +func (s SqlChannelStore) UpdateSidebarCategoryOrder(userId, teamId string, categoryOrder []string) *model.AppError { + transaction, err := s.GetMaster().Begin() + if err != nil { + return model.NewAppError("SqlChannelStore.UpdateSidebarCategoryOrder", "store.sql_channel.sidebar_categories.open_transaction.app_error", nil, err.Error(), http.StatusInternalServerError) + } + + defer finalizeTransaction(transaction) + + // Ensure no invalid categories are included and that no categories are left out + existingOrder, appErr := s.GetSidebarCategoryOrder(userId, teamId) + if appErr != nil { + return appErr + } + if len(existingOrder) != len(categoryOrder) { + return model.NewAppError("SqlPostStore.UpdateSidebarCategoryOrder", "store.sql_channel.sidebar_categories.app_error", nil, "Cannot update category order, passed list of categories different size than in DB", http.StatusInternalServerError) + } + for _, originalCategoryId := range existingOrder { + found := false + for _, newCategoryId := range categoryOrder { + if newCategoryId == originalCategoryId { + found = true + break + } + } + if !found { + return model.NewAppError("SqlPostStore.UpdateSidebarCategoryOrder", "store.sql_channel.sidebar_categories.app_error", nil, "Cannot update category order, passed list of categories contains unrecognized category IDs", http.StatusBadRequest) + } + } + + if appErr := s.updateSidebarCategoryOrderT(transaction, userId, teamId, categoryOrder); appErr != nil { + return appErr + } + + if err = transaction.Commit(); err != nil { + return model.NewAppError("SqlChannelStore.UpdateSidebarCategoryOrder", "store.sql_channel.sidebar_categories.commit_transaction.app_error", nil, err.Error(), http.StatusInternalServerError) + } + + return nil +} + +func (s SqlChannelStore) UpdateSidebarCategories(userId, teamId string, categories []*model.SidebarCategoryWithChannels) ([]*model.SidebarCategoryWithChannels, *model.AppError) { + transaction, err := s.GetMaster().Begin() + if err != nil { + return nil, model.NewAppError("SqlChannelStore.UpdateSidebarCategory", "store.sql_channel.sidebar_categories.open_transaction.app_error", nil, err.Error(), http.StatusInternalServerError) + } + defer finalizeTransaction(transaction) + + updatedCategories := []*model.SidebarCategoryWithChannels{} + for _, category := range categories { + originalCategory, appErr := s.GetSidebarCategory(category.Id) + if appErr != nil { + return nil, model.NewAppError("SqlPostStore.UpdateSidebarCategory", "store.sql_channel.sidebar_categories.app_error", nil, appErr.Error(), http.StatusInternalServerError) + } + + // Copy category to avoid modifying an argument + updatedCategory := &model.SidebarCategoryWithChannels{ + SidebarCategory: category.SidebarCategory, + } + + // Prevent any changes to read-only fields of SidebarCategories + updatedCategory.UserId = originalCategory.UserId + updatedCategory.TeamId = originalCategory.TeamId + updatedCategory.SortOrder = originalCategory.SortOrder + updatedCategory.Type = originalCategory.Type + + if updatedCategory.Type != model.SidebarCategoryCustom { + updatedCategory.DisplayName = originalCategory.DisplayName + } + + if category.Type != model.SidebarCategoryDirectMessages { + updatedCategory.Channels = make([]string, len(category.Channels)) + copy(updatedCategory.Channels, category.Channels) + } + + updateQuery, updateParams, _ := s.getQueryBuilder(). + Update("SidebarCategories"). + Set("DisplayName", updatedCategory.DisplayName). + Set("Sorting", updatedCategory.Sorting). + Where(sq.Eq{"Id": updatedCategory.Id}).ToSql() + + if _, err = transaction.Exec(updateQuery, updateParams...); err != nil { + return nil, model.NewAppError("SqlPostStore.UpdateSidebarCategory", "store.sql_channel.sidebar_categories.app_error", nil, err.Error(), http.StatusInternalServerError) + } + + // if we are updating DM category, it's order can't channel order cannot be changed. + if category.Type != model.SidebarCategoryDirectMessages { + // Remove any SidebarChannels entries that were either: + // - previously in this category (and any ones that are still in the category will be recreated below) + // - in another category and are being added to this category + sql, args, _ := s.getQueryBuilder(). + Delete("SidebarChannels"). + Where( + sq.And{ + sq.Or{ + sq.Eq{"ChannelId": originalCategory.Channels}, + sq.Eq{"ChannelId": updatedCategory.Channels}, + }, + sq.Eq{"CategoryId": category.Id}, + }, + ).ToSql() + + if _, err = transaction.Exec(sql, args...); err != nil { + return nil, model.NewAppError("SqlPostStore.UpdateSidebarCategory", "store.sql_channel.sidebar_categories.app_error", nil, err.Error(), http.StatusInternalServerError) + } + + var channels []interface{} + runningOrder := 0 + for _, channelID := range category.Channels { + channels = append(channels, &model.SidebarChannel{ + ChannelId: channelID, + CategoryId: category.Id, + SortOrder: int64(runningOrder), + UserId: userId, + }) + runningOrder += model.MinimalSidebarSortDistance + } + + if err = transaction.Insert(channels...); err != nil { + return nil, model.NewAppError("SqlPostStore.UpdateSidebarCategory", "store.sql_channel.sidebar_categories.app_error", nil, err.Error(), http.StatusInternalServerError) + } + } + + // Update the favorites preferences based on channels moving into or out of the Favorites category for compatibility + if category.Type == model.SidebarCategoryFavorites { + // Remove any old favorites + sql, args, _ := s.getQueryBuilder().Delete("Preferences").Where( + sq.Eq{ + "UserId": userId, + "Name": originalCategory.Channels, + "Category": model.PREFERENCE_CATEGORY_FAVORITE_CHANNEL, + }, + ).ToSql() + + if _, err = transaction.Exec(sql, args...); err != nil { + return nil, model.NewAppError("SqlPostStore.UpdateSidebarCategory", "store.sql_channel.sidebar_categories.app_error", nil, err.Error(), http.StatusInternalServerError) + } + + // And then add the new ones + for _, channelID := range category.Channels { + // This breaks the PreferenceStore abstraction, but it should be safe to assume that everything is a SQL + // store in this package. + if err = s.Preference().(*SqlPreferenceStore).save(transaction, &model.Preference{ + Name: channelID, + UserId: userId, + Category: model.PREFERENCE_CATEGORY_FAVORITE_CHANNEL, + Value: "true", + }); err != nil { + return nil, model.NewAppError("SqlPostStore.UpdateSidebarCategory", "store.sql_channel.sidebar_categories.app_error", nil, err.Error(), http.StatusInternalServerError) + } + } + } else { + // Remove any old favorites that might have been in this category + sql, args, _ := s.getQueryBuilder().Delete("Preferences").Where( + sq.Eq{ + "UserId": userId, + "Name": category.Channels, + "Category": model.PREFERENCE_CATEGORY_FAVORITE_CHANNEL, + }, + ).ToSql() + + if _, err = transaction.Exec(sql, args...); err != nil { + return nil, model.NewAppError("SqlPostStore.UpdateSidebarCategory", "store.sql_channel.sidebar_categories.app_error", nil, err.Error(), http.StatusInternalServerError) + } + } + + updatedCategories = append(updatedCategories, updatedCategory) + } + + // Ensure Channels are populated for Channels/Direct Messages category if they change + for i, updatedCategory := range updatedCategories { + populated, err := s.completePopulatingCategoryChannelsT(transaction, updatedCategory) + if err != nil { + return nil, model.NewAppError("SqlPostStore.UpdateSidebarCategory", "store.sql_channel.sidebar_categories.app_error", nil, err.Error(), http.StatusInternalServerError) + } + + updatedCategories[i] = populated + } + + if err = transaction.Commit(); err != nil { + return nil, model.NewAppError("SqlChannelStore.UpdateSidebarCategory", "store.sql_channel.sidebar_categories.commit_transaction.app_error", nil, err.Error(), http.StatusInternalServerError) + } + + return updatedCategories, nil +} + +// UpdateSidebarChannelsByPreferences is called when the Preference table is being updated to keep SidebarCategories in sync +// At the moment, it's only handling Favorites and NOT DMs/GMs (those will be handled client side) +func (s SqlChannelStore) UpdateSidebarChannelsByPreferences(preferences *model.Preferences) error { + transaction, err := s.GetMaster().Begin() + if err != nil { + return errors.Wrap(err, "UpdateSidebarChannelsByPreferences: begin_transaction") + } + defer finalizeTransaction(transaction) + + for _, preference := range *preferences { + preference := preference + + if preference.Category != model.PREFERENCE_CATEGORY_FAVORITE_CHANNEL { + continue + } + + // if new preference is false - remove the channel from the appropriate sidebar category + if preference.Value == "false" { + if err := s.removeSidebarEntriesForPreferenceT(transaction, &preference); err != nil { + return errors.Wrap(err, "UpdateSidebarChannelsByPreferences: removeSidebarEntriesForPreferenceT") + } + } else { + if err := s.addChannelToFavoritesCategoryT(transaction, &preference); err != nil { + return errors.Wrap(err, "UpdateSidebarChannelsByPreferences: addChannelToFavoritesCategoryT") + } + } + } + + if err := transaction.Commit(); err != nil { + return errors.Wrap(err, "UpdateSidebarChannelsByPreferences: commit_transaction") + } + + return nil +} + +func (s SqlChannelStore) removeSidebarEntriesForPreferenceT(transaction *gorp.Transaction, preference *model.Preference) error { + if preference.Category != model.PREFERENCE_CATEGORY_FAVORITE_CHANNEL { + return nil + } + + // Delete any corresponding SidebarChannels entries in a Favorites category corresponding to this preference. This + // can't use the query builder because it uses DB-specific syntax + params := map[string]interface{}{ + "UserId": preference.UserId, + "ChannelId": preference.Name, + "CategoryType": model.SidebarCategoryFavorites, + } + var query string + if s.DriverName() == model.DATABASE_DRIVER_MYSQL { + query = ` + DELETE + SidebarChannels + FROM + SidebarChannels + JOIN + SidebarCategories ON SidebarChannels.CategoryId = SidebarCategories.Id + WHERE + SidebarChannels.UserId = :UserId + AND SidebarChannels.ChannelId = :ChannelId + AND SidebarCategories.Type = :CategoryType` + } else { + query = ` + DELETE FROM + SidebarChannels + USING + SidebarCategories + WHERE + SidebarChannels.CategoryId = SidebarCategories.Id + AND SidebarChannels.UserId = :UserId + AND SidebarChannels.ChannelId = :ChannelId + AND SidebarCategories.Type = :CategoryType` + } + + if _, err := transaction.Exec(query, params); err != nil { + return errors.Wrap(err, "Failed to remove sidebar entries for preference") + } + + return nil +} + +func (s SqlChannelStore) addChannelToFavoritesCategoryT(transaction *gorp.Transaction, preference *model.Preference) error { + if preference.Category != model.PREFERENCE_CATEGORY_FAVORITE_CHANNEL { + return nil + } + + var channel *model.Channel + if obj, err := transaction.Get(&model.Channel{}, preference.Name); err != nil { + return errors.Wrapf(err, "Failed to get favorited channel with id=%s", preference.Name) + } else { + channel = obj.(*model.Channel) + } + + // Get the IDs of the Favorites category/categories that the channel needs to be added to + builder := s.getQueryBuilder(). + Select("SidebarCategories.Id"). + From("SidebarCategories"). + LeftJoin("SidebarChannels on SidebarCategories.Id = SidebarChannels.CategoryId and SidebarChannels.ChannelId = ?", preference.Name). + Where(sq.Eq{ + "SidebarCategories.UserId": preference.UserId, + "Type": model.SidebarCategoryFavorites, + }). + Where("SidebarChannels.ChannelId is null") + + if channel.TeamId != "" { + builder = builder.Where(sq.Eq{"TeamId": channel.TeamId}) + } + + idsQuery, idsParams, _ := builder.ToSql() + + var categoryIds []string + if _, err := transaction.Select(&categoryIds, idsQuery, idsParams...); err != nil { + return errors.Wrap(err, "Failed to get Favorites sidebar categories") + } + + if len(categoryIds) == 0 { + // The channel is already in the Favorites category/categories + return nil + } + + // For each category ID, insert a row into SidebarChannels with the given channel ID and a SortOrder that's less than + // all existing SortOrders in the category so that the newly favorited channel comes first + insertQuery, insertParams, _ := s.getQueryBuilder(). + Insert("SidebarChannels"). + Columns( + "ChannelId", + "CategoryId", + "UserId", + "SortOrder", + ). + Select( + sq.Select(). + Column("? as ChannelId", preference.Name). + Column("SidebarCategories.Id as CategoryId"). + Column("? as UserId", preference.UserId). + Column("COALESCE(MIN(SidebarChannels.SortOrder) - 10, 0) as SortOrder"). + From("SidebarCategories"). + LeftJoin("SidebarChannels on SidebarCategories.Id = SidebarChannels.CategoryId"). + Where(sq.Eq{ + "SidebarCategories.Id": categoryIds, + }). + GroupBy("SidebarCategories.Id")).ToSql() + + if _, err := transaction.Exec(insertQuery, insertParams...); err != nil { + return errors.Wrap(err, "Failed to add sidebar entries for favorited channel") + } + + return nil +} + +// DeleteSidebarChannelsByPreferences is called when the Preference table is being updated to keep SidebarCategories in sync +// At the moment, it's only handling Favorites and NOT DMs/GMs (those will be handled client side) +func (s SqlChannelStore) DeleteSidebarChannelsByPreferences(preferences *model.Preferences) error { + transaction, err := s.GetMaster().Begin() + if err != nil { + return errors.Wrap(err, "DeleteSidebarChannelsByPreferences: begin_transaction") + } + defer finalizeTransaction(transaction) + + for _, preference := range *preferences { + preference := preference + + if preference.Category != model.PREFERENCE_CATEGORY_FAVORITE_CHANNEL { + continue + } + + if err := s.removeSidebarEntriesForPreferenceT(transaction, &preference); err != nil { + return errors.Wrap(err, "DeleteSidebarChannelsByPreferences: removeSidebarEntriesForPreferenceT") + } + } + + if err := transaction.Commit(); err != nil { + return errors.Wrap(err, "DeleteSidebarChannelsByPreferences: commit_transaction") + } + + return nil +} + +func (s SqlChannelStore) UpdateSidebarChannelCategoryOnMove(channel *model.Channel, newTeamId string) *model.AppError { + // if channel is being moved, remove it from the categories, since it's possible that there's no matching category in the new team + if _, err := s.GetMaster().Exec("DELETE FROM SidebarChannels WHERE ChannelId=:ChannelId", map[string]interface{}{"ChannelId": channel.Id}); err != nil { + return model.NewAppError("SqlChannelStore.UpdateSidebarChannelCategoryOnMove", "store.sql_channel.sidebar_categories.app_error", nil, err.Error(), http.StatusInternalServerError) + } + return nil +} + +func (s SqlChannelStore) ClearSidebarOnTeamLeave(userId, teamId string) *model.AppError { + // if user leaves the team, clean his team related entries in sidebar channels and categories + params := map[string]interface{}{ + "UserId": userId, + "TeamId": teamId, + } + + var deleteQuery string + if s.DriverName() == model.DATABASE_DRIVER_MYSQL { + deleteQuery = "DELETE SidebarChannels FROM SidebarChannels LEFT JOIN SidebarCategories ON SidebarCategories.Id = SidebarChannels.CategoryId WHERE SidebarCategories.TeamId=:TeamId AND SidebarCategories.UserId=:UserId" + } else { + deleteQuery = "DELETE FROM SidebarChannels USING SidebarChannels AS chan LEFT OUTER JOIN SidebarCategories AS cat ON cat.Id = chan.CategoryId WHERE cat.UserId = :UserId AND cat.TeamId = :TeamId" + } + if _, err := s.GetMaster().Exec(deleteQuery, params); err != nil { + return model.NewAppError("SqlChannelStore.ClearSidebarOnTeamLeave", "store.sql_channel.sidebar_categories.app_error", nil, err.Error(), http.StatusInternalServerError) + } + if _, err := s.GetMaster().Exec("DELETE FROM SidebarCategories WHERE SidebarCategories.TeamId = :TeamId AND SidebarCategories.UserId = :UserId", params); err != nil { + return model.NewAppError("SqlChannelStore.ClearSidebarOnTeamLeave", "store.sql_channel.sidebar_categories.app_error", nil, err.Error(), http.StatusInternalServerError) + } + return nil +} + +// DeleteSidebarCategory removes a custom category and moves any channels into it into the Channels and Direct Messages +// categories respectively. Assumes that the provided user ID and team ID match the given category ID. +func (s SqlChannelStore) DeleteSidebarCategory(categoryId string) *model.AppError { + transaction, err := s.GetMaster().Begin() + if err != nil { + return model.NewAppError("SqlChannelStore.DeleteSidebarCategory", "store.sql_channel.sidebar_categories.open_transaction.app_error", nil, err.Error(), http.StatusInternalServerError) + } + defer finalizeTransaction(transaction) + + // Ensure that we're deleting a custom category + var category *model.SidebarCategory + if err = transaction.SelectOne(&category, "SELECT * FROM SidebarCategories WHERE Id = :Id", map[string]interface{}{"Id": categoryId}); err != nil { + return model.NewAppError("SqlPostStore.DeleteSidebarCategory", "store.sql_channel.sidebar_categories.app_error", nil, err.Error(), http.StatusInternalServerError) + } + + if category.Type != model.SidebarCategoryCustom { + return model.NewAppError("SqlPostStore.DeleteSidebarCategory", "store.sql_channel.sidebar_categories.delete_invalid.app_error", nil, "", http.StatusBadRequest) + } + + // Delete the channels in the category + sql, args, _ := s.getQueryBuilder(). + Delete("SidebarChannels"). + Where(sq.Eq{"CategoryId": categoryId}).ToSql() + + if _, err := transaction.Exec(sql, args...); err != nil { + return model.NewAppError("SqlPostStore.DeleteSidebarCategory", "store.sql_channel.sidebar_categories.app_error", nil, err.Error(), http.StatusInternalServerError) + } + + // Delete the category itself + sql, args, _ = s.getQueryBuilder(). + Delete("SidebarCategories"). + Where(sq.Eq{"Id": categoryId}).ToSql() + + if _, err := transaction.Exec(sql, args...); err != nil { + return model.NewAppError("SqlChannelStore.DeleteSidebarCategory", "store.sql_channel.sidebar_categories.app_error", nil, err.Error(), http.StatusInternalServerError) + } + + if err := transaction.Commit(); err != nil { + return model.NewAppError("SqlChannelStore.DeleteSidebarCategory", "store.sql_channel.sidebar_categories.commit_transaction.app_error", nil, err.Error(), http.StatusInternalServerError) + } + + return nil +} diff --git a/store/sqlstore/channel_store_categories_test.go b/store/sqlstore/channel_store_categories_test.go new file mode 100644 index 0000000000..bf586ecbe6 --- /dev/null +++ b/store/sqlstore/channel_store_categories_test.go @@ -0,0 +1,14 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +package sqlstore + +import ( + "testing" + + "github.com/mattermost/mattermost-server/v5/store/storetest" +) + +func TestChannelStoreCategories(t *testing.T) { + StoreTestWithSqlSupplier(t, storetest.TestChannelStoreCategories) +} diff --git a/store/storetest/channel_store.go b/store/storetest/channel_store.go index 15f70e2b48..6229586745 100644 --- a/store/storetest/channel_store.go +++ b/store/storetest/channel_store.go @@ -4,7 +4,6 @@ package storetest import ( - "database/sql" "errors" "sort" "strconv" @@ -6755,1657 +6754,3 @@ func testGroupSyncedChannelCount(t *testing.T, ss store.Store) { require.Nil(t, appErr) require.GreaterOrEqual(t, countAfter, count+1) } - -func testGetSidebarCategory(t *testing.T, ss store.Store, s SqlSupplier) { - t.Run("should return a custom category with its Channels field set", func(t *testing.T) { - userId := model.NewId() - teamId := model.NewId() - - channelId1 := model.NewId() - channelId2 := model.NewId() - channelId3 := model.NewId() - - nErr := ss.Channel().CreateInitialSidebarCategories(userId, teamId) - require.Nil(t, nErr) - - // Create a category and assign some channels to it - created, err := ss.Channel().CreateSidebarCategory(userId, teamId, &model.SidebarCategoryWithChannels{ - SidebarCategory: model.SidebarCategory{ - UserId: userId, - TeamId: teamId, - DisplayName: model.NewId(), - }, - Channels: []string{channelId1, channelId2, channelId3}, - }) - require.Nil(t, err) - require.NotNil(t, created) - - // Ensure that they're returned in order - res, err := ss.Channel().GetSidebarCategory(created.Id) - assert.Nil(t, err) - assert.Equal(t, created.Id, res.Id) - assert.Equal(t, model.SidebarCategoryCustom, res.Type) - assert.Equal(t, created.DisplayName, res.DisplayName) - assert.Equal(t, []string{channelId1, channelId2, channelId3}, res.Channels) - }) - - t.Run("should return any orphaned channels with the Channels category", func(t *testing.T) { - userId := model.NewId() - teamId := model.NewId() - - // Create the initial categories and find the channels category - nErr := ss.Channel().CreateInitialSidebarCategories(userId, teamId) - require.Nil(t, nErr) - - categories, err := ss.Channel().GetSidebarCategories(userId, teamId) - require.Nil(t, err) - - channelsCategory := categories.Categories[1] - require.Equal(t, model.SidebarCategoryChannels, channelsCategory.Type) - - // Join some channels - channel1, nErr := ss.Channel().Save(&model.Channel{ - Name: "channel1", - DisplayName: "DEF", - TeamId: teamId, - Type: model.CHANNEL_PRIVATE, - }, 10) - require.Nil(t, nErr) - _, err = ss.Channel().SaveMember(&model.ChannelMember{ - UserId: userId, - ChannelId: channel1.Id, - NotifyProps: model.GetDefaultChannelNotifyProps(), - }) - require.Nil(t, err) - - channel2, nErr := ss.Channel().Save(&model.Channel{ - Name: "channel2", - DisplayName: "ABC", - TeamId: teamId, - Type: model.CHANNEL_OPEN, - }, 10) - require.Nil(t, nErr) - _, err = ss.Channel().SaveMember(&model.ChannelMember{ - UserId: userId, - ChannelId: channel2.Id, - NotifyProps: model.GetDefaultChannelNotifyProps(), - }) - require.Nil(t, err) - - // Confirm that they're not in the Channels category in the DB - count, countErr := s.GetMaster().SelectInt(` - SELECT - COUNT(*) - FROM - SidebarChannels - WHERE - CategoryId = :CategoryId`, map[string]interface{}{"CategoryId": channelsCategory.Id}) - require.Nil(t, countErr) - assert.Equal(t, int64(0), count) - - // Ensure that the Channels are returned in alphabetical order - res, err := ss.Channel().GetSidebarCategory(channelsCategory.Id) - assert.Nil(t, err) - assert.Equal(t, channelsCategory.Id, res.Id) - assert.Equal(t, model.SidebarCategoryChannels, channelsCategory.Type) - assert.Equal(t, []string{channel2.Id, channel1.Id}, res.Channels) - }) - - t.Run("shouldn't return orphaned channels on another team with the Channels category", func(t *testing.T) { - userId := model.NewId() - teamId := model.NewId() - - // Create the initial categories and find the channels category - nErr := ss.Channel().CreateInitialSidebarCategories(userId, teamId) - require.Nil(t, nErr) - - categories, err := ss.Channel().GetSidebarCategories(userId, teamId) - require.Nil(t, err) - require.Equal(t, model.SidebarCategoryChannels, categories.Categories[1].Type) - - channelsCategory := categories.Categories[1] - - // Join a channel on another team - channel1, nErr := ss.Channel().Save(&model.Channel{ - Name: "abc", - TeamId: model.NewId(), - Type: model.CHANNEL_OPEN, - }, 10) - require.Nil(t, nErr) - - _, err = ss.Channel().SaveMember(&model.ChannelMember{ - UserId: userId, - ChannelId: channel1.Id, - NotifyProps: model.GetDefaultChannelNotifyProps(), - }) - require.Nil(t, err) - - // Ensure that no channels are returned - res, err := ss.Channel().GetSidebarCategory(channelsCategory.Id) - assert.Nil(t, err) - assert.Equal(t, channelsCategory.Id, res.Id) - assert.Equal(t, model.SidebarCategoryChannels, channelsCategory.Type) - assert.Len(t, res.Channels, 0) - }) - - t.Run("shouldn't return non-orphaned channels with the Channels category", func(t *testing.T) { - userId := model.NewId() - teamId := model.NewId() - - // Create the initial categories and find the channels category - nErr := ss.Channel().CreateInitialSidebarCategories(userId, teamId) - require.Nil(t, nErr) - - categories, err := ss.Channel().GetSidebarCategories(userId, teamId) - require.Nil(t, err) - - favoritesCategory := categories.Categories[0] - require.Equal(t, model.SidebarCategoryFavorites, favoritesCategory.Type) - channelsCategory := categories.Categories[1] - require.Equal(t, model.SidebarCategoryChannels, channelsCategory.Type) - - // Join some channels - channel1, nErr := ss.Channel().Save(&model.Channel{ - Name: "channel1", - DisplayName: "DEF", - TeamId: teamId, - Type: model.CHANNEL_PRIVATE, - }, 10) - require.Nil(t, nErr) - _, err = ss.Channel().SaveMember(&model.ChannelMember{ - UserId: userId, - ChannelId: channel1.Id, - NotifyProps: model.GetDefaultChannelNotifyProps(), - }) - require.Nil(t, err) - - channel2, nErr := ss.Channel().Save(&model.Channel{ - Name: "channel2", - DisplayName: "ABC", - TeamId: teamId, - Type: model.CHANNEL_OPEN, - }, 10) - require.Nil(t, nErr) - _, err = ss.Channel().SaveMember(&model.ChannelMember{ - UserId: userId, - ChannelId: channel2.Id, - NotifyProps: model.GetDefaultChannelNotifyProps(), - }) - require.Nil(t, err) - - // And assign one to another category - _, err = ss.Channel().UpdateSidebarCategories(userId, teamId, []*model.SidebarCategoryWithChannels{ - { - SidebarCategory: favoritesCategory.SidebarCategory, - Channels: []string{channel2.Id}, - }, - }) - require.Nil(t, err) - - // Ensure that the correct channel is returned in the Channels category - res, err := ss.Channel().GetSidebarCategory(channelsCategory.Id) - assert.Nil(t, err) - assert.Equal(t, channelsCategory.Id, res.Id) - assert.Equal(t, model.SidebarCategoryChannels, channelsCategory.Type) - assert.Equal(t, []string{channel1.Id}, res.Channels) - }) - - t.Run("should return any orphaned DM channels with the Direct Messages category", func(t *testing.T) { - userId := model.NewId() - teamId := model.NewId() - - // Create the initial categories and find the DMs category - nErr := ss.Channel().CreateInitialSidebarCategories(userId, teamId) - require.Nil(t, nErr) - - categories, err := ss.Channel().GetSidebarCategories(userId, teamId) - require.Nil(t, err) - require.Equal(t, model.SidebarCategoryDirectMessages, categories.Categories[2].Type) - - dmsCategory := categories.Categories[2] - - // Create a DM - otherUserId := model.NewId() - dmChannel, nErr := ss.Channel().SaveDirectChannel( - &model.Channel{ - Name: model.GetDMNameFromIds(userId, otherUserId), - Type: model.CHANNEL_DIRECT, - }, - &model.ChannelMember{ - UserId: userId, - NotifyProps: model.GetDefaultChannelNotifyProps(), - }, - &model.ChannelMember{ - UserId: otherUserId, - NotifyProps: model.GetDefaultChannelNotifyProps(), - }, - ) - require.Nil(t, nErr) - - // Ensure that the DM is returned - res, err := ss.Channel().GetSidebarCategory(dmsCategory.Id) - assert.Nil(t, err) - assert.Equal(t, dmsCategory.Id, res.Id) - assert.Equal(t, model.SidebarCategoryDirectMessages, res.Type) - assert.Equal(t, []string{dmChannel.Id}, res.Channels) - }) - - t.Run("should return any orphaned GM channels with the Direct Messages category", func(t *testing.T) { - userId := model.NewId() - teamId := model.NewId() - - // Create the initial categories and find the DMs category - nErr := ss.Channel().CreateInitialSidebarCategories(userId, teamId) - require.Nil(t, nErr) - - categories, err := ss.Channel().GetSidebarCategories(userId, teamId) - require.Nil(t, err) - require.Equal(t, model.SidebarCategoryDirectMessages, categories.Categories[2].Type) - - dmsCategory := categories.Categories[2] - - // Create a GM - gmChannel, nErr := ss.Channel().Save(&model.Channel{ - Name: "abc", - TeamId: "", - Type: model.CHANNEL_GROUP, - }, 10) - require.Nil(t, nErr) - _, err = ss.Channel().SaveMember(&model.ChannelMember{ - UserId: userId, - ChannelId: gmChannel.Id, - NotifyProps: model.GetDefaultChannelNotifyProps(), - }) - require.Nil(t, err) - - // Ensure that the DM is returned - res, err := ss.Channel().GetSidebarCategory(dmsCategory.Id) - assert.Nil(t, err) - assert.Equal(t, dmsCategory.Id, res.Id) - assert.Equal(t, model.SidebarCategoryDirectMessages, res.Type) - assert.Equal(t, []string{gmChannel.Id}, res.Channels) - }) - - t.Run("should return orphaned DM channels in the DMs categorywhich are in a custom category on another team", func(t *testing.T) { - userId := model.NewId() - teamId := model.NewId() - - // Create the initial categories and find the DMs category - nErr := ss.Channel().CreateInitialSidebarCategories(userId, teamId) - require.Nil(t, nErr) - - categories, err := ss.Channel().GetSidebarCategories(userId, teamId) - require.Nil(t, err) - require.Equal(t, model.SidebarCategoryDirectMessages, categories.Categories[2].Type) - - dmsCategory := categories.Categories[2] - - // Create a DM - otherUserId := model.NewId() - dmChannel, nErr := ss.Channel().SaveDirectChannel( - &model.Channel{ - Name: model.GetDMNameFromIds(userId, otherUserId), - Type: model.CHANNEL_DIRECT, - }, - &model.ChannelMember{ - UserId: userId, - NotifyProps: model.GetDefaultChannelNotifyProps(), - }, - &model.ChannelMember{ - UserId: otherUserId, - NotifyProps: model.GetDefaultChannelNotifyProps(), - }, - ) - require.Nil(t, nErr) - - // Create another team and assign the DM to a custom category on that team - otherTeamId := model.NewId() - - nErr = ss.Channel().CreateInitialSidebarCategories(userId, otherTeamId) - require.Nil(t, nErr) - - _, err = ss.Channel().CreateSidebarCategory(userId, otherTeamId, &model.SidebarCategoryWithChannels{ - SidebarCategory: model.SidebarCategory{ - UserId: userId, - TeamId: teamId, - }, - Channels: []string{dmChannel.Id}, - }) - require.Nil(t, err) - - // Ensure that the DM is returned with the DMs category on the original team - res, err := ss.Channel().GetSidebarCategory(dmsCategory.Id) - assert.Nil(t, err) - assert.Equal(t, dmsCategory.Id, res.Id) - assert.Equal(t, model.SidebarCategoryDirectMessages, res.Type) - assert.Equal(t, []string{dmChannel.Id}, res.Channels) - }) -} - -func testGetSidebarCategories(t *testing.T, ss store.Store) { - t.Run("should return channels in the same order between different ways of getting categories", func(t *testing.T) { - userId := model.NewId() - teamId := model.NewId() - - nErr := ss.Channel().CreateInitialSidebarCategories(userId, teamId) - require.Nil(t, nErr) - - channelIds := []string{ - model.NewId(), - model.NewId(), - model.NewId(), - } - - newCategory, err := ss.Channel().CreateSidebarCategory(userId, teamId, &model.SidebarCategoryWithChannels{ - Channels: channelIds, - }) - require.Nil(t, err) - require.NotNil(t, newCategory) - - gotCategory, err := ss.Channel().GetSidebarCategory(newCategory.Id) - require.Nil(t, err) - - res, err := ss.Channel().GetSidebarCategories(userId, teamId) - require.Nil(t, err) - require.Len(t, res.Categories, 4) - - require.Equal(t, model.SidebarCategoryCustom, res.Categories[1].Type) - - // This looks unnecessary, but I was getting different results from some of these before - assert.Equal(t, newCategory.Channels, res.Categories[1].Channels) - assert.Equal(t, gotCategory.Channels, res.Categories[1].Channels) - assert.Equal(t, channelIds, res.Categories[1].Channels) - }) -} - -func testUpdateSidebarCategories(t *testing.T, ss store.Store, s SqlSupplier) { - t.Run("ensure the query to update SidebarCategories hasn't been polluted by UpdateSidebarCategoryOrder", func(t *testing.T) { - userId := model.NewId() - teamId := model.NewId() - - // Create the initial categories - err := ss.Channel().CreateInitialSidebarCategories(userId, teamId) - require.Nil(t, err) - - initialCategories, err := ss.Channel().GetSidebarCategories(userId, teamId) - require.Nil(t, err) - - favoritesCategory := initialCategories.Categories[0] - channelsCategory := initialCategories.Categories[1] - dmsCategory := initialCategories.Categories[2] - - // And then update one of them - updated, err := ss.Channel().UpdateSidebarCategories(userId, teamId, []*model.SidebarCategoryWithChannels{ - channelsCategory, - }) - require.Nil(t, err) - assert.Equal(t, channelsCategory, updated[0]) - assert.Equal(t, "Channels", updated[0].DisplayName) - - // And then reorder the categories - err = ss.Channel().UpdateSidebarCategoryOrder(userId, teamId, []string{dmsCategory.Id, favoritesCategory.Id, channelsCategory.Id}) - require.Nil(t, err) - - // Which somehow blanks out stuff because ??? - got, err := ss.Channel().GetSidebarCategory(favoritesCategory.Id) - require.Nil(t, err) - assert.Equal(t, "Favorites", got.DisplayName) - }) - - t.Run("categories should be returned in their original order", func(t *testing.T) { - userId := model.NewId() - teamId := model.NewId() - - // Create the initial categories - err := ss.Channel().CreateInitialSidebarCategories(userId, teamId) - require.Nil(t, err) - - initialCategories, err := ss.Channel().GetSidebarCategories(userId, teamId) - require.Nil(t, err) - - favoritesCategory := initialCategories.Categories[0] - channelsCategory := initialCategories.Categories[1] - dmsCategory := initialCategories.Categories[2] - - // And then update them - updatedCategories, err := ss.Channel().UpdateSidebarCategories(userId, teamId, []*model.SidebarCategoryWithChannels{ - favoritesCategory, - channelsCategory, - dmsCategory, - }) - assert.Nil(t, err) - assert.Equal(t, favoritesCategory.Id, updatedCategories[0].Id) - assert.Equal(t, channelsCategory.Id, updatedCategories[1].Id) - assert.Equal(t, dmsCategory.Id, updatedCategories[2].Id) - }) - - t.Run("should silently fail to update read only fields", func(t *testing.T) { - userId := model.NewId() - teamId := model.NewId() - - nErr := ss.Channel().CreateInitialSidebarCategories(userId, teamId) - require.Nil(t, nErr) - - initialCategories, err := ss.Channel().GetSidebarCategories(userId, teamId) - require.Nil(t, err) - - favoritesCategory := initialCategories.Categories[0] - channelsCategory := initialCategories.Categories[1] - dmsCategory := initialCategories.Categories[2] - - customCategory, err := ss.Channel().CreateSidebarCategory(userId, teamId, &model.SidebarCategoryWithChannels{}) - require.Nil(t, err) - - categoriesToUpdate := []*model.SidebarCategoryWithChannels{ - // Try to change the type of Favorites - { - SidebarCategory: model.SidebarCategory{ - Id: favoritesCategory.Id, - DisplayName: "something else", - }, - Channels: favoritesCategory.Channels, - }, - // Try to change the type of Channels - { - SidebarCategory: model.SidebarCategory{ - Id: channelsCategory.Id, - Type: model.SidebarCategoryDirectMessages, - }, - Channels: channelsCategory.Channels, - }, - // Try to change the Channels of DMs - { - SidebarCategory: dmsCategory.SidebarCategory, - Channels: []string{"fakechannel"}, - }, - // Try to change the UserId/TeamId of a custom category - { - SidebarCategory: model.SidebarCategory{ - Id: customCategory.Id, - UserId: model.NewId(), - TeamId: model.NewId(), - Sorting: customCategory.Sorting, - DisplayName: customCategory.DisplayName, - }, - Channels: customCategory.Channels, - }, - } - - updatedCategories, err := ss.Channel().UpdateSidebarCategories(userId, teamId, categoriesToUpdate) - assert.Nil(t, err) - - assert.NotEqual(t, "Favorites", categoriesToUpdate[0].DisplayName) - assert.Equal(t, "Favorites", updatedCategories[0].DisplayName) - assert.NotEqual(t, model.SidebarCategoryChannels, categoriesToUpdate[1].Type) - assert.Equal(t, model.SidebarCategoryChannels, updatedCategories[1].Type) - assert.NotEqual(t, []string{}, categoriesToUpdate[2].Channels) - assert.Equal(t, []string{}, updatedCategories[2].Channels) - assert.NotEqual(t, userId, categoriesToUpdate[3].UserId) - assert.Equal(t, userId, updatedCategories[3].UserId) - }) - - t.Run("should add and remove favorites preferences based on the Favorites category", func(t *testing.T) { - userId := model.NewId() - teamId := model.NewId() - - // Create the initial categories and find the favorites category - nErr := ss.Channel().CreateInitialSidebarCategories(userId, teamId) - require.Nil(t, nErr) - - categories, err := ss.Channel().GetSidebarCategories(userId, teamId) - require.Nil(t, err) - - favoritesCategory := categories.Categories[0] - require.Equal(t, model.SidebarCategoryFavorites, favoritesCategory.Type) - - // Join a channel - channel, nErr := ss.Channel().Save(&model.Channel{ - Name: "channel", - Type: model.CHANNEL_OPEN, - TeamId: teamId, - }, 10) - require.Nil(t, nErr) - _, err = ss.Channel().SaveMember(&model.ChannelMember{ - UserId: userId, - ChannelId: channel.Id, - NotifyProps: model.GetDefaultChannelNotifyProps(), - }) - require.Nil(t, err) - - // Assign it to favorites - _, err = ss.Channel().UpdateSidebarCategories(userId, teamId, []*model.SidebarCategoryWithChannels{ - { - SidebarCategory: favoritesCategory.SidebarCategory, - Channels: []string{channel.Id}, - }, - }) - assert.Nil(t, err) - - res, nErr := ss.Preference().Get(userId, model.PREFERENCE_CATEGORY_FAVORITE_CHANNEL, channel.Id) - assert.Nil(t, nErr) - assert.NotNil(t, res) - assert.Equal(t, "true", res.Value) - - // And then remove it - channelsCategory := categories.Categories[1] - require.Equal(t, model.SidebarCategoryChannels, channelsCategory.Type) - - _, err = ss.Channel().UpdateSidebarCategories(userId, teamId, []*model.SidebarCategoryWithChannels{ - { - SidebarCategory: channelsCategory.SidebarCategory, - Channels: []string{channel.Id}, - }, - }) - assert.Nil(t, err) - - res, nErr = ss.Preference().Get(userId, model.PREFERENCE_CATEGORY_FAVORITE_CHANNEL, channel.Id) - assert.NotNil(t, nErr) - assert.True(t, errors.Is(nErr, sql.ErrNoRows)) - assert.Nil(t, res) - }) - - t.Run("should add and remove favorites preferences for DMs", func(t *testing.T) { - userId := model.NewId() - teamId := model.NewId() - - // Create the initial categories and find the favorites category - nErr := ss.Channel().CreateInitialSidebarCategories(userId, teamId) - require.Nil(t, nErr) - - categories, err := ss.Channel().GetSidebarCategories(userId, teamId) - require.Nil(t, err) - - favoritesCategory := categories.Categories[0] - require.Equal(t, model.SidebarCategoryFavorites, favoritesCategory.Type) - - // Create a direct channel - otherUserId := model.NewId() - - dmChannel, nErr := ss.Channel().SaveDirectChannel( - &model.Channel{ - Name: model.GetDMNameFromIds(userId, otherUserId), - Type: model.CHANNEL_DIRECT, - }, - &model.ChannelMember{ - UserId: userId, - NotifyProps: model.GetDefaultChannelNotifyProps(), - }, - &model.ChannelMember{ - UserId: otherUserId, - NotifyProps: model.GetDefaultChannelNotifyProps(), - }, - ) - assert.Nil(t, nErr) - - // Assign it to favorites - _, err = ss.Channel().UpdateSidebarCategories(userId, teamId, []*model.SidebarCategoryWithChannels{ - { - SidebarCategory: favoritesCategory.SidebarCategory, - Channels: []string{dmChannel.Id}, - }, - }) - assert.Nil(t, err) - - res, nErr := ss.Preference().Get(userId, model.PREFERENCE_CATEGORY_FAVORITE_CHANNEL, dmChannel.Id) - assert.Nil(t, nErr) - assert.NotNil(t, res) - assert.Equal(t, "true", res.Value) - - // And then remove it - dmsCategory := categories.Categories[2] - require.Equal(t, model.SidebarCategoryDirectMessages, dmsCategory.Type) - - _, err = ss.Channel().UpdateSidebarCategories(userId, teamId, []*model.SidebarCategoryWithChannels{ - { - SidebarCategory: dmsCategory.SidebarCategory, - Channels: []string{dmChannel.Id}, - }, - }) - assert.Nil(t, err) - - res, nErr = ss.Preference().Get(userId, model.PREFERENCE_CATEGORY_FAVORITE_CHANNEL, dmChannel.Id) - assert.NotNil(t, nErr) - assert.True(t, errors.Is(nErr, sql.ErrNoRows)) - assert.Nil(t, res) - }) - - t.Run("should add and remove favorites preferences, even if the channel is already favorited in preferences", func(t *testing.T) { - userId := model.NewId() - teamId := model.NewId() - teamId2 := model.NewId() - - // Create the initial categories and find the favorites categories in each team - nErr := ss.Channel().CreateInitialSidebarCategories(userId, teamId) - require.Nil(t, nErr) - - categories, err := ss.Channel().GetSidebarCategories(userId, teamId) - require.Nil(t, err) - - favoritesCategory := categories.Categories[0] - require.Equal(t, model.SidebarCategoryFavorites, favoritesCategory.Type) - - nErr = ss.Channel().CreateInitialSidebarCategories(userId, teamId2) - require.Nil(t, nErr) - - categories2, err := ss.Channel().GetSidebarCategories(userId, teamId2) - require.Nil(t, err) - - favoritesCategory2 := categories2.Categories[0] - require.Equal(t, model.SidebarCategoryFavorites, favoritesCategory2.Type) - - // Create a direct channel - otherUserId := model.NewId() - - dmChannel, nErr := ss.Channel().SaveDirectChannel( - &model.Channel{ - Name: model.GetDMNameFromIds(userId, otherUserId), - Type: model.CHANNEL_DIRECT, - }, - &model.ChannelMember{ - UserId: userId, - NotifyProps: model.GetDefaultChannelNotifyProps(), - }, - &model.ChannelMember{ - UserId: otherUserId, - NotifyProps: model.GetDefaultChannelNotifyProps(), - }, - ) - assert.Nil(t, nErr) - - // Assign it to favorites on the first team. The favorites preference gets set for all teams. - _, err = ss.Channel().UpdateSidebarCategories(userId, teamId, []*model.SidebarCategoryWithChannels{ - { - SidebarCategory: favoritesCategory.SidebarCategory, - Channels: []string{dmChannel.Id}, - }, - }) - assert.Nil(t, err) - - res, nErr := ss.Preference().Get(userId, model.PREFERENCE_CATEGORY_FAVORITE_CHANNEL, dmChannel.Id) - assert.Nil(t, nErr) - assert.NotNil(t, res) - assert.Equal(t, "true", res.Value) - - // Assign it to favorites on the second team. The favorites preference is already set. - updated, err := ss.Channel().UpdateSidebarCategories(userId, teamId, []*model.SidebarCategoryWithChannels{ - { - SidebarCategory: favoritesCategory2.SidebarCategory, - Channels: []string{dmChannel.Id}, - }, - }) - assert.Nil(t, err) - assert.Equal(t, []string{dmChannel.Id}, updated[0].Channels) - - res, nErr = ss.Preference().Get(userId, model.PREFERENCE_CATEGORY_FAVORITE_CHANNEL, dmChannel.Id) - assert.NoError(t, nErr) - assert.NotNil(t, res) - assert.Equal(t, "true", res.Value) - - // Remove it from favorites on the first team. This clears the favorites preference for all teams. - _, err = ss.Channel().UpdateSidebarCategories(userId, teamId, []*model.SidebarCategoryWithChannels{ - { - SidebarCategory: favoritesCategory.SidebarCategory, - Channels: []string{}, - }, - }) - assert.Nil(t, err) - - res, nErr = ss.Preference().Get(userId, model.PREFERENCE_CATEGORY_FAVORITE_CHANNEL, dmChannel.Id) - require.Error(t, nErr) - assert.Nil(t, res) - - // Remove it from favorites on the second team. The favorites preference was already deleted. - _, err = ss.Channel().UpdateSidebarCategories(userId, teamId2, []*model.SidebarCategoryWithChannels{ - { - SidebarCategory: favoritesCategory2.SidebarCategory, - Channels: []string{}, - }, - }) - assert.Nil(t, err) - - res, nErr = ss.Preference().Get(userId, model.PREFERENCE_CATEGORY_FAVORITE_CHANNEL, dmChannel.Id) - require.Error(t, nErr) - assert.Nil(t, res) - }) - - t.Run("should not affect other users' favorites preferences", func(t *testing.T) { - userId := model.NewId() - teamId := model.NewId() - - // Create the initial categories and find the favorites category - nErr := ss.Channel().CreateInitialSidebarCategories(userId, teamId) - require.Nil(t, nErr) - - categories, err := ss.Channel().GetSidebarCategories(userId, teamId) - require.Nil(t, err) - - favoritesCategory := categories.Categories[0] - require.Equal(t, model.SidebarCategoryFavorites, favoritesCategory.Type) - channelsCategory := categories.Categories[1] - require.Equal(t, model.SidebarCategoryChannels, channelsCategory.Type) - - // Create the other users' categories - userId2 := model.NewId() - - nErr = ss.Channel().CreateInitialSidebarCategories(userId2, teamId) - require.Nil(t, nErr) - - categories2, err := ss.Channel().GetSidebarCategories(userId2, teamId) - require.Nil(t, err) - - favoritesCategory2 := categories2.Categories[0] - require.Equal(t, model.SidebarCategoryFavorites, favoritesCategory2.Type) - channelsCategory2 := categories2.Categories[1] - require.Equal(t, model.SidebarCategoryChannels, channelsCategory2.Type) - - // Have both users join a channel - channel, nErr := ss.Channel().Save(&model.Channel{ - Name: "channel", - Type: model.CHANNEL_OPEN, - TeamId: teamId, - }, 10) - require.Nil(t, nErr) - _, err = ss.Channel().SaveMember(&model.ChannelMember{ - UserId: userId, - ChannelId: channel.Id, - NotifyProps: model.GetDefaultChannelNotifyProps(), - }) - require.Nil(t, err) - _, err = ss.Channel().SaveMember(&model.ChannelMember{ - UserId: userId2, - ChannelId: channel.Id, - NotifyProps: model.GetDefaultChannelNotifyProps(), - }) - require.Nil(t, err) - - // Have user1 favorite it - _, err = ss.Channel().UpdateSidebarCategories(userId, teamId, []*model.SidebarCategoryWithChannels{ - { - SidebarCategory: favoritesCategory.SidebarCategory, - Channels: []string{channel.Id}, - }, - { - SidebarCategory: channelsCategory.SidebarCategory, - Channels: []string{}, - }, - }) - assert.Nil(t, err) - - res, nErr := ss.Preference().Get(userId, model.PREFERENCE_CATEGORY_FAVORITE_CHANNEL, channel.Id) - assert.Nil(t, nErr) - assert.NotNil(t, res) - assert.Equal(t, "true", res.Value) - - res, nErr = ss.Preference().Get(userId2, model.PREFERENCE_CATEGORY_FAVORITE_CHANNEL, channel.Id) - assert.True(t, errors.Is(nErr, sql.ErrNoRows)) - assert.Nil(t, res) - - // And user2 favorite it - _, err = ss.Channel().UpdateSidebarCategories(userId2, teamId, []*model.SidebarCategoryWithChannels{ - { - SidebarCategory: favoritesCategory2.SidebarCategory, - Channels: []string{channel.Id}, - }, - { - SidebarCategory: channelsCategory2.SidebarCategory, - Channels: []string{}, - }, - }) - assert.Nil(t, err) - - res, nErr = ss.Preference().Get(userId, model.PREFERENCE_CATEGORY_FAVORITE_CHANNEL, channel.Id) - assert.Nil(t, nErr) - assert.NotNil(t, res) - assert.Equal(t, "true", res.Value) - - res, nErr = ss.Preference().Get(userId2, model.PREFERENCE_CATEGORY_FAVORITE_CHANNEL, channel.Id) - assert.Nil(t, nErr) - assert.NotNil(t, res) - assert.Equal(t, "true", res.Value) - - // And then user1 unfavorite it - _, err = ss.Channel().UpdateSidebarCategories(userId, teamId, []*model.SidebarCategoryWithChannels{ - { - SidebarCategory: channelsCategory.SidebarCategory, - Channels: []string{channel.Id}, - }, - { - SidebarCategory: favoritesCategory.SidebarCategory, - Channels: []string{}, - }, - }) - assert.Nil(t, err) - - res, nErr = ss.Preference().Get(userId, model.PREFERENCE_CATEGORY_FAVORITE_CHANNEL, channel.Id) - assert.True(t, errors.Is(nErr, sql.ErrNoRows)) - assert.Nil(t, res) - - res, nErr = ss.Preference().Get(userId2, model.PREFERENCE_CATEGORY_FAVORITE_CHANNEL, channel.Id) - assert.Nil(t, nErr) - assert.NotNil(t, res) - assert.Equal(t, "true", res.Value) - - // And finally user2 favorite it - _, err = ss.Channel().UpdateSidebarCategories(userId2, teamId, []*model.SidebarCategoryWithChannels{ - { - SidebarCategory: channelsCategory2.SidebarCategory, - Channels: []string{channel.Id}, - }, - { - SidebarCategory: favoritesCategory2.SidebarCategory, - Channels: []string{}, - }, - }) - assert.Nil(t, err) - - res, nErr = ss.Preference().Get(userId, model.PREFERENCE_CATEGORY_FAVORITE_CHANNEL, channel.Id) - assert.True(t, errors.Is(nErr, sql.ErrNoRows)) - assert.Nil(t, res) - - res, nErr = ss.Preference().Get(userId2, model.PREFERENCE_CATEGORY_FAVORITE_CHANNEL, channel.Id) - assert.True(t, errors.Is(nErr, sql.ErrNoRows)) - assert.Nil(t, res) - }) - - t.Run("channels removed from Channels or DMs categories should be re-added", func(t *testing.T) { - userId := model.NewId() - teamId := model.NewId() - - // Create some channels - channel, nErr := ss.Channel().Save(&model.Channel{ - Name: "channel", - Type: model.CHANNEL_OPEN, - TeamId: teamId, - }, 10) - require.Nil(t, nErr) - _, err := ss.Channel().SaveMember(&model.ChannelMember{ - UserId: userId, - ChannelId: channel.Id, - NotifyProps: model.GetDefaultChannelNotifyProps(), - }) - require.Nil(t, err) - - otherUserId := model.NewId() - dmChannel, nErr := ss.Channel().SaveDirectChannel( - &model.Channel{ - Name: model.GetDMNameFromIds(userId, otherUserId), - Type: model.CHANNEL_DIRECT, - }, - &model.ChannelMember{ - UserId: userId, - NotifyProps: model.GetDefaultChannelNotifyProps(), - }, - &model.ChannelMember{ - UserId: otherUserId, - NotifyProps: model.GetDefaultChannelNotifyProps(), - }, - ) - require.Nil(t, nErr) - - nErr = ss.Channel().CreateInitialSidebarCategories(userId, teamId) - require.Nil(t, nErr) - - // And some categories - initialCategories, err := ss.Channel().GetSidebarCategories(userId, teamId) - require.Nil(t, err) - - channelsCategory := initialCategories.Categories[1] - dmsCategory := initialCategories.Categories[2] - - require.Equal(t, []string{channel.Id}, channelsCategory.Channels) - require.Equal(t, []string{dmChannel.Id}, dmsCategory.Channels) - - // Try to save the categories with no channels in them - categoriesToUpdate := []*model.SidebarCategoryWithChannels{ - { - SidebarCategory: channelsCategory.SidebarCategory, - Channels: []string{}, - }, - { - SidebarCategory: dmsCategory.SidebarCategory, - Channels: []string{}, - }, - } - - updatedCategories, err := ss.Channel().UpdateSidebarCategories(userId, teamId, categoriesToUpdate) - assert.Nil(t, err) - - // The channels should still exist in the category because they would otherwise be orphaned - assert.Equal(t, []string{channel.Id}, updatedCategories[0].Channels) - assert.Equal(t, []string{dmChannel.Id}, updatedCategories[1].Channels) - }) - - t.Run("should be able to move DMs into and out of custom categories", func(t *testing.T) { - userId := model.NewId() - teamId := model.NewId() - - otherUserId := model.NewId() - dmChannel, nErr := ss.Channel().SaveDirectChannel( - &model.Channel{ - Name: model.GetDMNameFromIds(userId, otherUserId), - Type: model.CHANNEL_DIRECT, - }, - &model.ChannelMember{ - UserId: userId, - NotifyProps: model.GetDefaultChannelNotifyProps(), - }, - &model.ChannelMember{ - UserId: otherUserId, - NotifyProps: model.GetDefaultChannelNotifyProps(), - }, - ) - require.Nil(t, nErr) - - nErr = ss.Channel().CreateInitialSidebarCategories(userId, teamId) - require.Nil(t, nErr) - - // The DM should start in the DMs category - initialCategories, err := ss.Channel().GetSidebarCategories(userId, teamId) - require.Nil(t, err) - - dmsCategory := initialCategories.Categories[2] - require.Equal(t, []string{dmChannel.Id}, dmsCategory.Channels) - - // Now move the DM into a custom category - customCategory, err := ss.Channel().CreateSidebarCategory(userId, teamId, &model.SidebarCategoryWithChannels{}) - require.Nil(t, err) - - categoriesToUpdate := []*model.SidebarCategoryWithChannels{ - { - SidebarCategory: dmsCategory.SidebarCategory, - Channels: []string{}, - }, - { - SidebarCategory: customCategory.SidebarCategory, - Channels: []string{dmChannel.Id}, - }, - } - - updatedCategories, err := ss.Channel().UpdateSidebarCategories(userId, teamId, categoriesToUpdate) - assert.Nil(t, err) - assert.Equal(t, dmsCategory.Id, updatedCategories[0].Id) - assert.Equal(t, []string{}, updatedCategories[0].Channels) - assert.Equal(t, customCategory.Id, updatedCategories[1].Id) - assert.Equal(t, []string{dmChannel.Id}, updatedCategories[1].Channels) - - updatedDmsCategory, err := ss.Channel().GetSidebarCategory(dmsCategory.Id) - require.Nil(t, err) - assert.Equal(t, []string{}, updatedDmsCategory.Channels) - - updatedCustomCategory, err := ss.Channel().GetSidebarCategory(customCategory.Id) - require.Nil(t, err) - assert.Equal(t, []string{dmChannel.Id}, updatedCustomCategory.Channels) - - // And move it back out of the custom category - categoriesToUpdate = []*model.SidebarCategoryWithChannels{ - { - SidebarCategory: dmsCategory.SidebarCategory, - Channels: []string{dmChannel.Id}, - }, - { - SidebarCategory: customCategory.SidebarCategory, - Channels: []string{}, - }, - } - - updatedCategories, err = ss.Channel().UpdateSidebarCategories(userId, teamId, categoriesToUpdate) - assert.Nil(t, err) - assert.Equal(t, dmsCategory.Id, updatedCategories[0].Id) - assert.Equal(t, []string{dmChannel.Id}, updatedCategories[0].Channels) - assert.Equal(t, customCategory.Id, updatedCategories[1].Id) - assert.Equal(t, []string{}, updatedCategories[1].Channels) - - updatedDmsCategory, err = ss.Channel().GetSidebarCategory(dmsCategory.Id) - require.Nil(t, err) - assert.Equal(t, []string{dmChannel.Id}, updatedDmsCategory.Channels) - - updatedCustomCategory, err = ss.Channel().GetSidebarCategory(customCategory.Id) - require.Nil(t, err) - assert.Equal(t, []string{}, updatedCustomCategory.Channels) - }) - - t.Run("should successfully move channels between categories", func(t *testing.T) { - userId := model.NewId() - teamId := model.NewId() - - // Join a channel - channel, nErr := ss.Channel().Save(&model.Channel{ - Name: "channel", - Type: model.CHANNEL_OPEN, - TeamId: teamId, - }, 10) - require.Nil(t, nErr) - _, err := ss.Channel().SaveMember(&model.ChannelMember{ - UserId: userId, - ChannelId: channel.Id, - NotifyProps: model.GetDefaultChannelNotifyProps(), - }) - require.Nil(t, err) - - // And then create the initial categories so that it includes the channel - nErr = ss.Channel().CreateInitialSidebarCategories(userId, teamId) - require.Nil(t, nErr) - - initialCategories, err := ss.Channel().GetSidebarCategories(userId, teamId) - require.Nil(t, err) - - channelsCategory := initialCategories.Categories[1] - require.Equal(t, []string{channel.Id}, channelsCategory.Channels) - - customCategory, err := ss.Channel().CreateSidebarCategory(userId, teamId, &model.SidebarCategoryWithChannels{}) - require.Nil(t, err) - - // Move the channel one way - updatedCategories, err := ss.Channel().UpdateSidebarCategories(userId, teamId, []*model.SidebarCategoryWithChannels{ - { - SidebarCategory: channelsCategory.SidebarCategory, - Channels: []string{}, - }, - { - SidebarCategory: customCategory.SidebarCategory, - Channels: []string{channel.Id}, - }, - }) - assert.Nil(t, err) - - assert.Equal(t, []string{}, updatedCategories[0].Channels) - assert.Equal(t, []string{channel.Id}, updatedCategories[1].Channels) - - // And then the other - updatedCategories, err = ss.Channel().UpdateSidebarCategories(userId, teamId, []*model.SidebarCategoryWithChannels{ - { - SidebarCategory: channelsCategory.SidebarCategory, - Channels: []string{channel.Id}, - }, - { - SidebarCategory: customCategory.SidebarCategory, - Channels: []string{}, - }, - }) - assert.Nil(t, err) - assert.Equal(t, []string{channel.Id}, updatedCategories[0].Channels) - assert.Equal(t, []string{}, updatedCategories[1].Channels) - }) -} - -func testCreateInitialSidebarCategories(t *testing.T, ss store.Store) { - t.Run("should create initial favorites/channels/DMs categories", func(t *testing.T) { - userId := model.NewId() - teamId := model.NewId() - - nErr := ss.Channel().CreateInitialSidebarCategories(userId, teamId) - assert.Nil(t, nErr) - - res, err := ss.Channel().GetSidebarCategories(userId, teamId) - assert.Nil(t, err) - assert.Len(t, res.Categories, 3) - assert.Equal(t, model.SidebarCategoryFavorites, res.Categories[0].Type) - assert.Equal(t, model.SidebarCategoryChannels, res.Categories[1].Type) - assert.Equal(t, model.SidebarCategoryDirectMessages, res.Categories[2].Type) - }) - - t.Run("should create initial favorites/channels/DMs categories for multiple users", func(t *testing.T) { - userId := model.NewId() - teamId := model.NewId() - - nErr := ss.Channel().CreateInitialSidebarCategories(userId, teamId) - require.Nil(t, nErr) - - userId2 := model.NewId() - - nErr = ss.Channel().CreateInitialSidebarCategories(userId2, teamId) - assert.Nil(t, nErr) - - res, err := ss.Channel().GetSidebarCategories(userId2, teamId) - assert.Nil(t, err) - assert.Len(t, res.Categories, 3) - assert.Equal(t, model.SidebarCategoryFavorites, res.Categories[0].Type) - assert.Equal(t, model.SidebarCategoryChannels, res.Categories[1].Type) - assert.Equal(t, model.SidebarCategoryDirectMessages, res.Categories[2].Type) - }) - - t.Run("should create initial favorites/channels/DMs categories on different teams", func(t *testing.T) { - userId := model.NewId() - teamId := model.NewId() - - nErr := ss.Channel().CreateInitialSidebarCategories(userId, teamId) - require.Nil(t, nErr) - - teamId2 := model.NewId() - - nErr = ss.Channel().CreateInitialSidebarCategories(userId, teamId2) - assert.Nil(t, nErr) - - res, err := ss.Channel().GetSidebarCategories(userId, teamId2) - assert.Nil(t, err) - assert.Len(t, res.Categories, 3) - assert.Equal(t, model.SidebarCategoryFavorites, res.Categories[0].Type) - assert.Equal(t, model.SidebarCategoryChannels, res.Categories[1].Type) - assert.Equal(t, model.SidebarCategoryDirectMessages, res.Categories[2].Type) - }) - - t.Run("shouldn't create additional categories when ones already exist", func(t *testing.T) { - userId := model.NewId() - teamId := model.NewId() - - nErr := ss.Channel().CreateInitialSidebarCategories(userId, teamId) - require.Nil(t, nErr) - - initialCategories, err := ss.Channel().GetSidebarCategories(userId, teamId) - require.Nil(t, err) - - // Calling CreateInitialSidebarCategories a second time shouldn't create any new categories - nErr = ss.Channel().CreateInitialSidebarCategories(userId, teamId) - assert.Nil(t, nErr) - - res, err := ss.Channel().GetSidebarCategories(userId, teamId) - assert.Nil(t, err) - assert.Equal(t, initialCategories.Categories, res.Categories) - }) - - t.Run("should populate the Favorites category with regular channels", func(t *testing.T) { - userId := model.NewId() - teamId := model.NewId() - - // Set up two channels, one favorited and one not - channel1, nErr := ss.Channel().Save(&model.Channel{ - TeamId: teamId, - Type: model.CHANNEL_OPEN, - Name: "channel1", - }, 1000) - require.Nil(t, nErr) - _, err := ss.Channel().SaveMember(&model.ChannelMember{ - ChannelId: channel1.Id, - UserId: userId, - NotifyProps: model.GetDefaultChannelNotifyProps(), - }) - require.Nil(t, err) - - channel2, nErr := ss.Channel().Save(&model.Channel{ - TeamId: teamId, - Type: model.CHANNEL_OPEN, - Name: "channel2", - }, 1000) - require.Nil(t, nErr) - _, err = ss.Channel().SaveMember(&model.ChannelMember{ - ChannelId: channel2.Id, - UserId: userId, - NotifyProps: model.GetDefaultChannelNotifyProps(), - }) - require.Nil(t, err) - - nErr = ss.Preference().Save(&model.Preferences{ - { - UserId: userId, - Category: model.PREFERENCE_CATEGORY_FAVORITE_CHANNEL, - Name: channel1.Id, - Value: "true", - }, - }) - require.Nil(t, nErr) - - // Create the categories - nErr = ss.Channel().CreateInitialSidebarCategories(userId, teamId) - require.Nil(t, nErr) - - // Get and check the categories for channels - categories, err := ss.Channel().GetSidebarCategories(userId, teamId) - require.Nil(t, err) - require.Len(t, categories.Categories, 3) - assert.Equal(t, model.SidebarCategoryFavorites, categories.Categories[0].Type) - assert.Equal(t, []string{channel1.Id}, categories.Categories[0].Channels) - assert.Equal(t, model.SidebarCategoryChannels, categories.Categories[1].Type) - assert.Equal(t, []string{channel2.Id}, categories.Categories[1].Channels) - }) - - t.Run("should populate the Favorites category in alphabetical order", func(t *testing.T) { - userId := model.NewId() - teamId := model.NewId() - - // Set up two channels - channel1, nErr := ss.Channel().Save(&model.Channel{ - TeamId: teamId, - Type: model.CHANNEL_OPEN, - Name: "channel1", - DisplayName: "zebra", - }, 1000) - require.Nil(t, nErr) - _, err := ss.Channel().SaveMember(&model.ChannelMember{ - ChannelId: channel1.Id, - UserId: userId, - NotifyProps: model.GetDefaultChannelNotifyProps(), - }) - require.Nil(t, err) - - channel2, nErr := ss.Channel().Save(&model.Channel{ - TeamId: teamId, - Type: model.CHANNEL_OPEN, - Name: "channel2", - DisplayName: "aardvark", - }, 1000) - require.Nil(t, nErr) - _, err = ss.Channel().SaveMember(&model.ChannelMember{ - ChannelId: channel2.Id, - UserId: userId, - NotifyProps: model.GetDefaultChannelNotifyProps(), - }) - require.Nil(t, err) - - nErr = ss.Preference().Save(&model.Preferences{ - { - UserId: userId, - Category: model.PREFERENCE_CATEGORY_FAVORITE_CHANNEL, - Name: channel1.Id, - Value: "true", - }, - { - UserId: userId, - Category: model.PREFERENCE_CATEGORY_FAVORITE_CHANNEL, - Name: channel2.Id, - Value: "true", - }, - }) - require.Nil(t, nErr) - - // Create the categories - nErr = ss.Channel().CreateInitialSidebarCategories(userId, teamId) - require.Nil(t, nErr) - - // Get and check the categories for channels - categories, err := ss.Channel().GetSidebarCategories(userId, teamId) - require.Nil(t, err) - require.Len(t, categories.Categories, 3) - assert.Equal(t, model.SidebarCategoryFavorites, categories.Categories[0].Type) - assert.Equal(t, []string{channel2.Id, channel1.Id}, categories.Categories[0].Channels) - }) - - t.Run("should populate the Favorites category with DMs and GMs", func(t *testing.T) { - userId := model.NewId() - teamId := model.NewId() - - otherUserId1 := model.NewId() - otherUserId2 := model.NewId() - - // Set up two direct channels, one favorited and one not - dmChannel1, err := ss.Channel().SaveDirectChannel( - &model.Channel{ - Name: model.GetDMNameFromIds(userId, otherUserId1), - Type: model.CHANNEL_DIRECT, - }, - &model.ChannelMember{ - UserId: userId, - NotifyProps: model.GetDefaultChannelNotifyProps(), - }, - &model.ChannelMember{ - UserId: otherUserId1, - NotifyProps: model.GetDefaultChannelNotifyProps(), - }, - ) - require.Nil(t, err) - - dmChannel2, err := ss.Channel().SaveDirectChannel( - &model.Channel{ - Name: model.GetDMNameFromIds(userId, otherUserId2), - Type: model.CHANNEL_DIRECT, - }, - &model.ChannelMember{ - UserId: userId, - NotifyProps: model.GetDefaultChannelNotifyProps(), - }, - &model.ChannelMember{ - UserId: otherUserId2, - NotifyProps: model.GetDefaultChannelNotifyProps(), - }, - ) - require.Nil(t, err) - - err = ss.Preference().Save(&model.Preferences{ - { - UserId: userId, - Category: model.PREFERENCE_CATEGORY_FAVORITE_CHANNEL, - Name: dmChannel1.Id, - Value: "true", - }, - }) - require.Nil(t, err) - - // Create the categories - nErr := ss.Channel().CreateInitialSidebarCategories(userId, teamId) - require.Nil(t, nErr) - - // Get and check the categories for channels - categories, err := ss.Channel().GetSidebarCategories(userId, teamId) - require.Nil(t, err) - require.Len(t, categories.Categories, 3) - assert.Equal(t, model.SidebarCategoryFavorites, categories.Categories[0].Type) - assert.Equal(t, []string{dmChannel1.Id}, categories.Categories[0].Channels) - assert.Equal(t, model.SidebarCategoryDirectMessages, categories.Categories[2].Type) - assert.Equal(t, []string{dmChannel2.Id}, categories.Categories[2].Channels) - }) - - t.Run("should not populate the Favorites category with channels from other teams", func(t *testing.T) { - userId := model.NewId() - teamId := model.NewId() - teamId2 := model.NewId() - - // Set up a channel on another team and favorite it - channel1, nErr := ss.Channel().Save(&model.Channel{ - TeamId: teamId2, - Type: model.CHANNEL_OPEN, - Name: "channel1", - }, 1000) - require.Nil(t, nErr) - _, err := ss.Channel().SaveMember(&model.ChannelMember{ - ChannelId: channel1.Id, - UserId: userId, - NotifyProps: model.GetDefaultChannelNotifyProps(), - }) - require.Nil(t, err) - - nErr = ss.Preference().Save(&model.Preferences{ - { - UserId: userId, - Category: model.PREFERENCE_CATEGORY_FAVORITE_CHANNEL, - Name: channel1.Id, - Value: "true", - }, - }) - require.Nil(t, nErr) - - // Create the categories - nErr = ss.Channel().CreateInitialSidebarCategories(userId, teamId) - require.Nil(t, nErr) - - // Get and check the categories for channels - categories, err := ss.Channel().GetSidebarCategories(userId, teamId) - require.Nil(t, err) - require.Len(t, categories.Categories, 3) - assert.Equal(t, model.SidebarCategoryFavorites, categories.Categories[0].Type) - assert.Equal(t, []string{}, categories.Categories[0].Channels) - assert.Equal(t, model.SidebarCategoryChannels, categories.Categories[1].Type) - assert.Equal(t, []string{}, categories.Categories[1].Channels) - }) -} - -func testCreateSidebarCategory(t *testing.T, ss store.Store) { - t.Run("should place the new category second if Favorites comes first", func(t *testing.T) { - userId := model.NewId() - teamId := model.NewId() - - nErr := ss.Channel().CreateInitialSidebarCategories(userId, teamId) - require.Nil(t, nErr) - - // Create the category - created, err := ss.Channel().CreateSidebarCategory(userId, teamId, &model.SidebarCategoryWithChannels{ - SidebarCategory: model.SidebarCategory{ - DisplayName: model.NewId(), - }, - }) - require.Nil(t, err) - - // Confirm that it comes second - res, err := ss.Channel().GetSidebarCategories(userId, teamId) - require.Nil(t, err) - require.Len(t, res.Categories, 4) - assert.Equal(t, model.SidebarCategoryFavorites, res.Categories[0].Type) - assert.Equal(t, model.SidebarCategoryCustom, res.Categories[1].Type) - assert.Equal(t, created.Id, res.Categories[1].Id) - }) - - t.Run("should place the new category first if Favorites is not first", func(t *testing.T) { - userId := model.NewId() - teamId := model.NewId() - - nErr := ss.Channel().CreateInitialSidebarCategories(userId, teamId) - require.Nil(t, nErr) - - // Re-arrange the categories so that Favorites comes last - categories, err := ss.Channel().GetSidebarCategories(userId, teamId) - require.Nil(t, err) - require.Len(t, categories.Categories, 3) - require.Equal(t, model.SidebarCategoryFavorites, categories.Categories[0].Type) - - err = ss.Channel().UpdateSidebarCategoryOrder(userId, teamId, []string{ - categories.Categories[1].Id, - categories.Categories[2].Id, - categories.Categories[0].Id, - }) - require.Nil(t, err) - - // Create the category - created, err := ss.Channel().CreateSidebarCategory(userId, teamId, &model.SidebarCategoryWithChannels{ - SidebarCategory: model.SidebarCategory{ - DisplayName: model.NewId(), - }, - }) - require.Nil(t, err) - - // Confirm that it comes first - res, err := ss.Channel().GetSidebarCategories(userId, teamId) - require.Nil(t, err) - require.Len(t, res.Categories, 4) - assert.Equal(t, model.SidebarCategoryCustom, res.Categories[0].Type) - assert.Equal(t, created.Id, res.Categories[0].Id) - }) - - t.Run("should create the category with its channels", func(t *testing.T) { - userId := model.NewId() - teamId := model.NewId() - - nErr := ss.Channel().CreateInitialSidebarCategories(userId, teamId) - require.Nil(t, nErr) - - // Create some channels - channel1, err := ss.Channel().Save(&model.Channel{ - Type: model.CHANNEL_OPEN, - TeamId: teamId, - Name: model.NewId(), - }, 100) - require.Nil(t, err) - channel2, err := ss.Channel().Save(&model.Channel{ - Type: model.CHANNEL_OPEN, - TeamId: teamId, - Name: model.NewId(), - }, 100) - require.Nil(t, err) - - // Create the category - created, err := ss.Channel().CreateSidebarCategory(userId, teamId, &model.SidebarCategoryWithChannels{ - SidebarCategory: model.SidebarCategory{ - DisplayName: model.NewId(), - }, - Channels: []string{channel2.Id, channel1.Id}, - }) - require.Nil(t, err) - assert.Equal(t, []string{channel2.Id, channel1.Id}, created.Channels) - - // Get the channel again to ensure that the SidebarChannels were saved correctly - res, err := ss.Channel().GetSidebarCategory(created.Id) - require.Nil(t, err) - assert.Equal(t, []string{channel2.Id, channel1.Id}, res.Channels) - }) - - t.Run("should remove any channels from their previous categories", func(t *testing.T) { - userId := model.NewId() - teamId := model.NewId() - - nErr := ss.Channel().CreateInitialSidebarCategories(userId, teamId) - require.Nil(t, nErr) - - categories, err := ss.Channel().GetSidebarCategories(userId, teamId) - require.Nil(t, err) - require.Len(t, categories.Categories, 3) - - favoritesCategory := categories.Categories[0] - require.Equal(t, model.SidebarCategoryFavorites, favoritesCategory.Type) - channelsCategory := categories.Categories[1] - require.Equal(t, model.SidebarCategoryChannels, channelsCategory.Type) - - // Create some channels - channel1, nErr := ss.Channel().Save(&model.Channel{ - Type: model.CHANNEL_OPEN, - TeamId: teamId, - Name: model.NewId(), - }, 100) - require.Nil(t, nErr) - channel2, nErr := ss.Channel().Save(&model.Channel{ - Type: model.CHANNEL_OPEN, - TeamId: teamId, - Name: model.NewId(), - }, 100) - require.Nil(t, nErr) - - // Assign them to categories - favoritesCategory.Channels = []string{channel1.Id} - channelsCategory.Channels = []string{channel2.Id} - _, err = ss.Channel().UpdateSidebarCategories(userId, teamId, []*model.SidebarCategoryWithChannels{ - favoritesCategory, - channelsCategory, - }) - require.Nil(t, err) - - // Create the category - created, err := ss.Channel().CreateSidebarCategory(userId, teamId, &model.SidebarCategoryWithChannels{ - SidebarCategory: model.SidebarCategory{ - DisplayName: model.NewId(), - }, - Channels: []string{channel2.Id, channel1.Id}, - }) - require.Nil(t, err) - assert.Equal(t, []string{channel2.Id, channel1.Id}, created.Channels) - - // Confirm that the channels were removed from their original categories - res, err := ss.Channel().GetSidebarCategory(favoritesCategory.Id) - require.Nil(t, err) - assert.Equal(t, []string{}, res.Channels) - - res, err = ss.Channel().GetSidebarCategory(channelsCategory.Id) - require.Nil(t, err) - assert.Equal(t, []string{}, res.Channels) - }) -} - -func testDeleteSidebarCategory(t *testing.T, ss store.Store, s SqlSupplier) { - setupInitialSidebarCategories := func(t *testing.T, ss store.Store) (string, string) { - userId := model.NewId() - teamId := model.NewId() - - nErr := ss.Channel().CreateInitialSidebarCategories(userId, teamId) - require.Nil(t, nErr) - - res, err := ss.Channel().GetSidebarCategories(userId, teamId) - require.Nil(t, err) - require.Len(t, res.Categories, 3) - - return userId, teamId - } - - t.Run("should correctly remove an empty category", func(t *testing.T) { - userId, teamId := setupInitialSidebarCategories(t, ss) - defer ss.User().PermanentDelete(userId) - - newCategory, err := ss.Channel().CreateSidebarCategory(userId, teamId, &model.SidebarCategoryWithChannels{}) - require.Nil(t, err) - require.NotNil(t, newCategory) - - // Ensure that the category was created properly - res, err := ss.Channel().GetSidebarCategories(userId, teamId) - require.Nil(t, err) - require.Len(t, res.Categories, 4) - - // Then delete it and confirm that was done correctly - err = ss.Channel().DeleteSidebarCategory(newCategory.Id) - assert.Nil(t, err) - - res, err = ss.Channel().GetSidebarCategories(userId, teamId) - require.Nil(t, err) - require.Len(t, res.Categories, 3) - }) - - t.Run("should correctly remove a category and its channels", func(t *testing.T) { - userId, teamId := setupInitialSidebarCategories(t, ss) - defer ss.User().PermanentDelete(userId) - - user := &model.User{ - Id: userId, - } - - // Create some channels - channel1, nErr := ss.Channel().Save(&model.Channel{ - Name: model.NewId(), - TeamId: teamId, - Type: model.CHANNEL_OPEN, - }, 1000) - require.Nil(t, nErr) - defer ss.Channel().PermanentDelete(channel1.Id) - - channel2, nErr := ss.Channel().Save(&model.Channel{ - Name: model.NewId(), - TeamId: teamId, - Type: model.CHANNEL_PRIVATE, - }, 1000) - require.Nil(t, nErr) - defer ss.Channel().PermanentDelete(channel2.Id) - - dmChannel1, nErr := ss.Channel().CreateDirectChannel(user, &model.User{ - Id: model.NewId(), - }) - require.Nil(t, nErr) - defer ss.Channel().PermanentDelete(dmChannel1.Id) - - // Assign some of those channels to a custom category - newCategory, err := ss.Channel().CreateSidebarCategory(userId, teamId, &model.SidebarCategoryWithChannels{ - Channels: []string{channel1.Id, channel2.Id, dmChannel1.Id}, - }) - require.Nil(t, err) - require.NotNil(t, newCategory) - - // Ensure that the categories are set up correctly - res, err := ss.Channel().GetSidebarCategories(userId, teamId) - require.Nil(t, err) - require.Len(t, res.Categories, 4) - - require.Equal(t, model.SidebarCategoryCustom, res.Categories[1].Type) - require.Equal(t, []string{channel1.Id, channel2.Id, dmChannel1.Id}, res.Categories[1].Channels) - - // Actually delete the channel - err = ss.Channel().DeleteSidebarCategory(newCategory.Id) - assert.Nil(t, err) - - // Confirm that the category was deleted... - res, err = ss.Channel().GetSidebarCategories(userId, teamId) - assert.Nil(t, err) - assert.Len(t, res.Categories, 3) - - // ...and that the corresponding SidebarChannel entries were deleted - count, countErr := s.GetMaster().SelectInt(` - SELECT - COUNT(*) - FROM - SidebarChannels - WHERE - CategoryId = :CategoryId`, map[string]interface{}{"CategoryId": newCategory.Id}) - require.Nil(t, countErr) - assert.Equal(t, int64(0), count) - }) - - t.Run("should not allow you to remove non-custom categories", func(t *testing.T) { - userId, teamId := setupInitialSidebarCategories(t, ss) - defer ss.User().PermanentDelete(userId) - res, err := ss.Channel().GetSidebarCategories(userId, teamId) - require.Nil(t, err) - require.Len(t, res.Categories, 3) - require.Equal(t, model.SidebarCategoryFavorites, res.Categories[0].Type) - require.Equal(t, model.SidebarCategoryChannels, res.Categories[1].Type) - require.Equal(t, model.SidebarCategoryDirectMessages, res.Categories[2].Type) - - err = ss.Channel().DeleteSidebarCategory(res.Categories[0].Id) - assert.NotNil(t, err) - - err = ss.Channel().DeleteSidebarCategory(res.Categories[1].Id) - assert.NotNil(t, err) - - err = ss.Channel().DeleteSidebarCategory(res.Categories[2].Id) - assert.NotNil(t, err) - }) -} diff --git a/store/storetest/channel_store_categories.go b/store/storetest/channel_store_categories.go new file mode 100644 index 0000000000..5127c66179 --- /dev/null +++ b/store/storetest/channel_store_categories.go @@ -0,0 +1,1678 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +package storetest + +import ( + "database/sql" + "errors" + "testing" + + "github.com/mattermost/mattermost-server/v5/model" + "github.com/mattermost/mattermost-server/v5/store" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestChannelStoreCategories(t *testing.T, ss store.Store, s SqlSupplier) { + t.Run("CreateInitialSidebarCategories", func(t *testing.T) { testCreateInitialSidebarCategories(t, ss) }) + t.Run("CreateSidebarCategory", func(t *testing.T) { testCreateSidebarCategory(t, ss) }) + t.Run("GetSidebarCategory", func(t *testing.T) { testGetSidebarCategory(t, ss, s) }) + t.Run("GetSidebarCategories", func(t *testing.T) { testGetSidebarCategories(t, ss) }) + t.Run("UpdateSidebarCategories", func(t *testing.T) { testUpdateSidebarCategories(t, ss, s) }) + t.Run("DeleteSidebarCategory", func(t *testing.T) { testDeleteSidebarCategory(t, ss, s) }) +} + +func testCreateInitialSidebarCategories(t *testing.T, ss store.Store) { + t.Run("should create initial favorites/channels/DMs categories", func(t *testing.T) { + userId := model.NewId() + teamId := model.NewId() + + nErr := ss.Channel().CreateInitialSidebarCategories(userId, teamId) + assert.Nil(t, nErr) + + res, err := ss.Channel().GetSidebarCategories(userId, teamId) + assert.Nil(t, err) + assert.Len(t, res.Categories, 3) + assert.Equal(t, model.SidebarCategoryFavorites, res.Categories[0].Type) + assert.Equal(t, model.SidebarCategoryChannels, res.Categories[1].Type) + assert.Equal(t, model.SidebarCategoryDirectMessages, res.Categories[2].Type) + }) + + t.Run("should create initial favorites/channels/DMs categories for multiple users", func(t *testing.T) { + userId := model.NewId() + teamId := model.NewId() + + nErr := ss.Channel().CreateInitialSidebarCategories(userId, teamId) + require.Nil(t, nErr) + + userId2 := model.NewId() + + nErr = ss.Channel().CreateInitialSidebarCategories(userId2, teamId) + assert.Nil(t, nErr) + + res, err := ss.Channel().GetSidebarCategories(userId2, teamId) + assert.Nil(t, err) + assert.Len(t, res.Categories, 3) + assert.Equal(t, model.SidebarCategoryFavorites, res.Categories[0].Type) + assert.Equal(t, model.SidebarCategoryChannels, res.Categories[1].Type) + assert.Equal(t, model.SidebarCategoryDirectMessages, res.Categories[2].Type) + }) + + t.Run("should create initial favorites/channels/DMs categories on different teams", func(t *testing.T) { + userId := model.NewId() + teamId := model.NewId() + + nErr := ss.Channel().CreateInitialSidebarCategories(userId, teamId) + require.Nil(t, nErr) + + teamId2 := model.NewId() + + nErr = ss.Channel().CreateInitialSidebarCategories(userId, teamId2) + assert.Nil(t, nErr) + + res, err := ss.Channel().GetSidebarCategories(userId, teamId2) + assert.Nil(t, err) + assert.Len(t, res.Categories, 3) + assert.Equal(t, model.SidebarCategoryFavorites, res.Categories[0].Type) + assert.Equal(t, model.SidebarCategoryChannels, res.Categories[1].Type) + assert.Equal(t, model.SidebarCategoryDirectMessages, res.Categories[2].Type) + }) + + t.Run("shouldn't create additional categories when ones already exist", func(t *testing.T) { + userId := model.NewId() + teamId := model.NewId() + + nErr := ss.Channel().CreateInitialSidebarCategories(userId, teamId) + require.Nil(t, nErr) + + initialCategories, err := ss.Channel().GetSidebarCategories(userId, teamId) + require.Nil(t, err) + + // Calling CreateInitialSidebarCategories a second time shouldn't create any new categories + nErr = ss.Channel().CreateInitialSidebarCategories(userId, teamId) + assert.Nil(t, nErr) + + res, err := ss.Channel().GetSidebarCategories(userId, teamId) + assert.Nil(t, err) + assert.Equal(t, initialCategories.Categories, res.Categories) + }) + + t.Run("should populate the Favorites category with regular channels", func(t *testing.T) { + userId := model.NewId() + teamId := model.NewId() + + // Set up two channels, one favorited and one not + channel1, nErr := ss.Channel().Save(&model.Channel{ + TeamId: teamId, + Type: model.CHANNEL_OPEN, + Name: "channel1", + }, 1000) + require.Nil(t, nErr) + _, err := ss.Channel().SaveMember(&model.ChannelMember{ + ChannelId: channel1.Id, + UserId: userId, + NotifyProps: model.GetDefaultChannelNotifyProps(), + }) + require.Nil(t, err) + + channel2, nErr := ss.Channel().Save(&model.Channel{ + TeamId: teamId, + Type: model.CHANNEL_OPEN, + Name: "channel2", + }, 1000) + require.Nil(t, nErr) + _, err = ss.Channel().SaveMember(&model.ChannelMember{ + ChannelId: channel2.Id, + UserId: userId, + NotifyProps: model.GetDefaultChannelNotifyProps(), + }) + require.Nil(t, err) + + nErr = ss.Preference().Save(&model.Preferences{ + { + UserId: userId, + Category: model.PREFERENCE_CATEGORY_FAVORITE_CHANNEL, + Name: channel1.Id, + Value: "true", + }, + }) + require.Nil(t, nErr) + + // Create the categories + nErr = ss.Channel().CreateInitialSidebarCategories(userId, teamId) + require.Nil(t, nErr) + + // Get and check the categories for channels + categories, err := ss.Channel().GetSidebarCategories(userId, teamId) + require.Nil(t, err) + require.Len(t, categories.Categories, 3) + assert.Equal(t, model.SidebarCategoryFavorites, categories.Categories[0].Type) + assert.Equal(t, []string{channel1.Id}, categories.Categories[0].Channels) + assert.Equal(t, model.SidebarCategoryChannels, categories.Categories[1].Type) + assert.Equal(t, []string{channel2.Id}, categories.Categories[1].Channels) + }) + + t.Run("should populate the Favorites category in alphabetical order", func(t *testing.T) { + userId := model.NewId() + teamId := model.NewId() + + // Set up two channels + channel1, nErr := ss.Channel().Save(&model.Channel{ + TeamId: teamId, + Type: model.CHANNEL_OPEN, + Name: "channel1", + DisplayName: "zebra", + }, 1000) + require.Nil(t, nErr) + _, err := ss.Channel().SaveMember(&model.ChannelMember{ + ChannelId: channel1.Id, + UserId: userId, + NotifyProps: model.GetDefaultChannelNotifyProps(), + }) + require.Nil(t, err) + + channel2, nErr := ss.Channel().Save(&model.Channel{ + TeamId: teamId, + Type: model.CHANNEL_OPEN, + Name: "channel2", + DisplayName: "aardvark", + }, 1000) + require.Nil(t, nErr) + _, err = ss.Channel().SaveMember(&model.ChannelMember{ + ChannelId: channel2.Id, + UserId: userId, + NotifyProps: model.GetDefaultChannelNotifyProps(), + }) + require.Nil(t, err) + + nErr = ss.Preference().Save(&model.Preferences{ + { + UserId: userId, + Category: model.PREFERENCE_CATEGORY_FAVORITE_CHANNEL, + Name: channel1.Id, + Value: "true", + }, + { + UserId: userId, + Category: model.PREFERENCE_CATEGORY_FAVORITE_CHANNEL, + Name: channel2.Id, + Value: "true", + }, + }) + require.Nil(t, nErr) + + // Create the categories + nErr = ss.Channel().CreateInitialSidebarCategories(userId, teamId) + require.Nil(t, nErr) + + // Get and check the categories for channels + categories, err := ss.Channel().GetSidebarCategories(userId, teamId) + require.Nil(t, err) + require.Len(t, categories.Categories, 3) + assert.Equal(t, model.SidebarCategoryFavorites, categories.Categories[0].Type) + assert.Equal(t, []string{channel2.Id, channel1.Id}, categories.Categories[0].Channels) + }) + + t.Run("should populate the Favorites category with DMs and GMs", func(t *testing.T) { + userId := model.NewId() + teamId := model.NewId() + + otherUserId1 := model.NewId() + otherUserId2 := model.NewId() + + // Set up two direct channels, one favorited and one not + dmChannel1, err := ss.Channel().SaveDirectChannel( + &model.Channel{ + Name: model.GetDMNameFromIds(userId, otherUserId1), + Type: model.CHANNEL_DIRECT, + }, + &model.ChannelMember{ + UserId: userId, + NotifyProps: model.GetDefaultChannelNotifyProps(), + }, + &model.ChannelMember{ + UserId: otherUserId1, + NotifyProps: model.GetDefaultChannelNotifyProps(), + }, + ) + require.Nil(t, err) + + dmChannel2, err := ss.Channel().SaveDirectChannel( + &model.Channel{ + Name: model.GetDMNameFromIds(userId, otherUserId2), + Type: model.CHANNEL_DIRECT, + }, + &model.ChannelMember{ + UserId: userId, + NotifyProps: model.GetDefaultChannelNotifyProps(), + }, + &model.ChannelMember{ + UserId: otherUserId2, + NotifyProps: model.GetDefaultChannelNotifyProps(), + }, + ) + require.Nil(t, err) + + err = ss.Preference().Save(&model.Preferences{ + { + UserId: userId, + Category: model.PREFERENCE_CATEGORY_FAVORITE_CHANNEL, + Name: dmChannel1.Id, + Value: "true", + }, + }) + require.Nil(t, err) + + // Create the categories + nErr := ss.Channel().CreateInitialSidebarCategories(userId, teamId) + require.Nil(t, nErr) + + // Get and check the categories for channels + categories, err := ss.Channel().GetSidebarCategories(userId, teamId) + require.Nil(t, err) + require.Len(t, categories.Categories, 3) + assert.Equal(t, model.SidebarCategoryFavorites, categories.Categories[0].Type) + assert.Equal(t, []string{dmChannel1.Id}, categories.Categories[0].Channels) + assert.Equal(t, model.SidebarCategoryDirectMessages, categories.Categories[2].Type) + assert.Equal(t, []string{dmChannel2.Id}, categories.Categories[2].Channels) + }) + + t.Run("should not populate the Favorites category with channels from other teams", func(t *testing.T) { + userId := model.NewId() + teamId := model.NewId() + teamId2 := model.NewId() + + // Set up a channel on another team and favorite it + channel1, nErr := ss.Channel().Save(&model.Channel{ + TeamId: teamId2, + Type: model.CHANNEL_OPEN, + Name: "channel1", + }, 1000) + require.Nil(t, nErr) + _, err := ss.Channel().SaveMember(&model.ChannelMember{ + ChannelId: channel1.Id, + UserId: userId, + NotifyProps: model.GetDefaultChannelNotifyProps(), + }) + require.Nil(t, err) + + nErr = ss.Preference().Save(&model.Preferences{ + { + UserId: userId, + Category: model.PREFERENCE_CATEGORY_FAVORITE_CHANNEL, + Name: channel1.Id, + Value: "true", + }, + }) + require.Nil(t, nErr) + + // Create the categories + nErr = ss.Channel().CreateInitialSidebarCategories(userId, teamId) + require.Nil(t, nErr) + + // Get and check the categories for channels + categories, err := ss.Channel().GetSidebarCategories(userId, teamId) + require.Nil(t, err) + require.Len(t, categories.Categories, 3) + assert.Equal(t, model.SidebarCategoryFavorites, categories.Categories[0].Type) + assert.Equal(t, []string{}, categories.Categories[0].Channels) + assert.Equal(t, model.SidebarCategoryChannels, categories.Categories[1].Type) + assert.Equal(t, []string{}, categories.Categories[1].Channels) + }) +} + +func testCreateSidebarCategory(t *testing.T, ss store.Store) { + t.Run("should place the new category second if Favorites comes first", func(t *testing.T) { + userId := model.NewId() + teamId := model.NewId() + + nErr := ss.Channel().CreateInitialSidebarCategories(userId, teamId) + require.Nil(t, nErr) + + // Create the category + created, err := ss.Channel().CreateSidebarCategory(userId, teamId, &model.SidebarCategoryWithChannels{ + SidebarCategory: model.SidebarCategory{ + DisplayName: model.NewId(), + }, + }) + require.Nil(t, err) + + // Confirm that it comes second + res, err := ss.Channel().GetSidebarCategories(userId, teamId) + require.Nil(t, err) + require.Len(t, res.Categories, 4) + assert.Equal(t, model.SidebarCategoryFavorites, res.Categories[0].Type) + assert.Equal(t, model.SidebarCategoryCustom, res.Categories[1].Type) + assert.Equal(t, created.Id, res.Categories[1].Id) + }) + + t.Run("should place the new category first if Favorites is not first", func(t *testing.T) { + userId := model.NewId() + teamId := model.NewId() + + nErr := ss.Channel().CreateInitialSidebarCategories(userId, teamId) + require.Nil(t, nErr) + + // Re-arrange the categories so that Favorites comes last + categories, err := ss.Channel().GetSidebarCategories(userId, teamId) + require.Nil(t, err) + require.Len(t, categories.Categories, 3) + require.Equal(t, model.SidebarCategoryFavorites, categories.Categories[0].Type) + + err = ss.Channel().UpdateSidebarCategoryOrder(userId, teamId, []string{ + categories.Categories[1].Id, + categories.Categories[2].Id, + categories.Categories[0].Id, + }) + require.Nil(t, err) + + // Create the category + created, err := ss.Channel().CreateSidebarCategory(userId, teamId, &model.SidebarCategoryWithChannels{ + SidebarCategory: model.SidebarCategory{ + DisplayName: model.NewId(), + }, + }) + require.Nil(t, err) + + // Confirm that it comes first + res, err := ss.Channel().GetSidebarCategories(userId, teamId) + require.Nil(t, err) + require.Len(t, res.Categories, 4) + assert.Equal(t, model.SidebarCategoryCustom, res.Categories[0].Type) + assert.Equal(t, created.Id, res.Categories[0].Id) + }) + + t.Run("should create the category with its channels", func(t *testing.T) { + userId := model.NewId() + teamId := model.NewId() + + nErr := ss.Channel().CreateInitialSidebarCategories(userId, teamId) + require.Nil(t, nErr) + + // Create some channels + channel1, err := ss.Channel().Save(&model.Channel{ + Type: model.CHANNEL_OPEN, + TeamId: teamId, + Name: model.NewId(), + }, 100) + require.Nil(t, err) + channel2, err := ss.Channel().Save(&model.Channel{ + Type: model.CHANNEL_OPEN, + TeamId: teamId, + Name: model.NewId(), + }, 100) + require.Nil(t, err) + + // Create the category + created, err := ss.Channel().CreateSidebarCategory(userId, teamId, &model.SidebarCategoryWithChannels{ + SidebarCategory: model.SidebarCategory{ + DisplayName: model.NewId(), + }, + Channels: []string{channel2.Id, channel1.Id}, + }) + require.Nil(t, err) + assert.Equal(t, []string{channel2.Id, channel1.Id}, created.Channels) + + // Get the channel again to ensure that the SidebarChannels were saved correctly + res, err := ss.Channel().GetSidebarCategory(created.Id) + require.Nil(t, err) + assert.Equal(t, []string{channel2.Id, channel1.Id}, res.Channels) + }) + + t.Run("should remove any channels from their previous categories", func(t *testing.T) { + userId := model.NewId() + teamId := model.NewId() + + nErr := ss.Channel().CreateInitialSidebarCategories(userId, teamId) + require.Nil(t, nErr) + + categories, err := ss.Channel().GetSidebarCategories(userId, teamId) + require.Nil(t, err) + require.Len(t, categories.Categories, 3) + + favoritesCategory := categories.Categories[0] + require.Equal(t, model.SidebarCategoryFavorites, favoritesCategory.Type) + channelsCategory := categories.Categories[1] + require.Equal(t, model.SidebarCategoryChannels, channelsCategory.Type) + + // Create some channels + channel1, nErr := ss.Channel().Save(&model.Channel{ + Type: model.CHANNEL_OPEN, + TeamId: teamId, + Name: model.NewId(), + }, 100) + require.Nil(t, nErr) + channel2, nErr := ss.Channel().Save(&model.Channel{ + Type: model.CHANNEL_OPEN, + TeamId: teamId, + Name: model.NewId(), + }, 100) + require.Nil(t, nErr) + + // Assign them to categories + favoritesCategory.Channels = []string{channel1.Id} + channelsCategory.Channels = []string{channel2.Id} + _, err = ss.Channel().UpdateSidebarCategories(userId, teamId, []*model.SidebarCategoryWithChannels{ + favoritesCategory, + channelsCategory, + }) + require.Nil(t, err) + + // Create the category + created, err := ss.Channel().CreateSidebarCategory(userId, teamId, &model.SidebarCategoryWithChannels{ + SidebarCategory: model.SidebarCategory{ + DisplayName: model.NewId(), + }, + Channels: []string{channel2.Id, channel1.Id}, + }) + require.Nil(t, err) + assert.Equal(t, []string{channel2.Id, channel1.Id}, created.Channels) + + // Confirm that the channels were removed from their original categories + res, err := ss.Channel().GetSidebarCategory(favoritesCategory.Id) + require.Nil(t, err) + assert.Equal(t, []string{}, res.Channels) + + res, err = ss.Channel().GetSidebarCategory(channelsCategory.Id) + require.Nil(t, err) + assert.Equal(t, []string{}, res.Channels) + }) +} + +func testGetSidebarCategory(t *testing.T, ss store.Store, s SqlSupplier) { + t.Run("should return a custom category with its Channels field set", func(t *testing.T) { + userId := model.NewId() + teamId := model.NewId() + + channelId1 := model.NewId() + channelId2 := model.NewId() + channelId3 := model.NewId() + + nErr := ss.Channel().CreateInitialSidebarCategories(userId, teamId) + require.Nil(t, nErr) + + // Create a category and assign some channels to it + created, err := ss.Channel().CreateSidebarCategory(userId, teamId, &model.SidebarCategoryWithChannels{ + SidebarCategory: model.SidebarCategory{ + UserId: userId, + TeamId: teamId, + DisplayName: model.NewId(), + }, + Channels: []string{channelId1, channelId2, channelId3}, + }) + require.Nil(t, err) + require.NotNil(t, created) + + // Ensure that they're returned in order + res, err := ss.Channel().GetSidebarCategory(created.Id) + assert.Nil(t, err) + assert.Equal(t, created.Id, res.Id) + assert.Equal(t, model.SidebarCategoryCustom, res.Type) + assert.Equal(t, created.DisplayName, res.DisplayName) + assert.Equal(t, []string{channelId1, channelId2, channelId3}, res.Channels) + }) + + t.Run("should return any orphaned channels with the Channels category", func(t *testing.T) { + userId := model.NewId() + teamId := model.NewId() + + // Create the initial categories and find the channels category + nErr := ss.Channel().CreateInitialSidebarCategories(userId, teamId) + require.Nil(t, nErr) + + categories, err := ss.Channel().GetSidebarCategories(userId, teamId) + require.Nil(t, err) + + channelsCategory := categories.Categories[1] + require.Equal(t, model.SidebarCategoryChannels, channelsCategory.Type) + + // Join some channels + channel1, nErr := ss.Channel().Save(&model.Channel{ + Name: "channel1", + DisplayName: "DEF", + TeamId: teamId, + Type: model.CHANNEL_PRIVATE, + }, 10) + require.Nil(t, nErr) + _, err = ss.Channel().SaveMember(&model.ChannelMember{ + UserId: userId, + ChannelId: channel1.Id, + NotifyProps: model.GetDefaultChannelNotifyProps(), + }) + require.Nil(t, err) + + channel2, nErr := ss.Channel().Save(&model.Channel{ + Name: "channel2", + DisplayName: "ABC", + TeamId: teamId, + Type: model.CHANNEL_OPEN, + }, 10) + require.Nil(t, nErr) + _, err = ss.Channel().SaveMember(&model.ChannelMember{ + UserId: userId, + ChannelId: channel2.Id, + NotifyProps: model.GetDefaultChannelNotifyProps(), + }) + require.Nil(t, err) + + // Confirm that they're not in the Channels category in the DB + count, countErr := s.GetMaster().SelectInt(` + SELECT + COUNT(*) + FROM + SidebarChannels + WHERE + CategoryId = :CategoryId`, map[string]interface{}{"CategoryId": channelsCategory.Id}) + require.Nil(t, countErr) + assert.Equal(t, int64(0), count) + + // Ensure that the Channels are returned in alphabetical order + res, err := ss.Channel().GetSidebarCategory(channelsCategory.Id) + assert.Nil(t, err) + assert.Equal(t, channelsCategory.Id, res.Id) + assert.Equal(t, model.SidebarCategoryChannels, channelsCategory.Type) + assert.Equal(t, []string{channel2.Id, channel1.Id}, res.Channels) + }) + + t.Run("shouldn't return orphaned channels on another team with the Channels category", func(t *testing.T) { + userId := model.NewId() + teamId := model.NewId() + + // Create the initial categories and find the channels category + nErr := ss.Channel().CreateInitialSidebarCategories(userId, teamId) + require.Nil(t, nErr) + + categories, err := ss.Channel().GetSidebarCategories(userId, teamId) + require.Nil(t, err) + require.Equal(t, model.SidebarCategoryChannels, categories.Categories[1].Type) + + channelsCategory := categories.Categories[1] + + // Join a channel on another team + channel1, nErr := ss.Channel().Save(&model.Channel{ + Name: "abc", + TeamId: model.NewId(), + Type: model.CHANNEL_OPEN, + }, 10) + require.Nil(t, nErr) + + _, err = ss.Channel().SaveMember(&model.ChannelMember{ + UserId: userId, + ChannelId: channel1.Id, + NotifyProps: model.GetDefaultChannelNotifyProps(), + }) + require.Nil(t, err) + + // Ensure that no channels are returned + res, err := ss.Channel().GetSidebarCategory(channelsCategory.Id) + assert.Nil(t, err) + assert.Equal(t, channelsCategory.Id, res.Id) + assert.Equal(t, model.SidebarCategoryChannels, channelsCategory.Type) + assert.Len(t, res.Channels, 0) + }) + + t.Run("shouldn't return non-orphaned channels with the Channels category", func(t *testing.T) { + userId := model.NewId() + teamId := model.NewId() + + // Create the initial categories and find the channels category + nErr := ss.Channel().CreateInitialSidebarCategories(userId, teamId) + require.Nil(t, nErr) + + categories, err := ss.Channel().GetSidebarCategories(userId, teamId) + require.Nil(t, err) + + favoritesCategory := categories.Categories[0] + require.Equal(t, model.SidebarCategoryFavorites, favoritesCategory.Type) + channelsCategory := categories.Categories[1] + require.Equal(t, model.SidebarCategoryChannels, channelsCategory.Type) + + // Join some channels + channel1, nErr := ss.Channel().Save(&model.Channel{ + Name: "channel1", + DisplayName: "DEF", + TeamId: teamId, + Type: model.CHANNEL_PRIVATE, + }, 10) + require.Nil(t, nErr) + _, err = ss.Channel().SaveMember(&model.ChannelMember{ + UserId: userId, + ChannelId: channel1.Id, + NotifyProps: model.GetDefaultChannelNotifyProps(), + }) + require.Nil(t, err) + + channel2, nErr := ss.Channel().Save(&model.Channel{ + Name: "channel2", + DisplayName: "ABC", + TeamId: teamId, + Type: model.CHANNEL_OPEN, + }, 10) + require.Nil(t, nErr) + _, err = ss.Channel().SaveMember(&model.ChannelMember{ + UserId: userId, + ChannelId: channel2.Id, + NotifyProps: model.GetDefaultChannelNotifyProps(), + }) + require.Nil(t, err) + + // And assign one to another category + _, err = ss.Channel().UpdateSidebarCategories(userId, teamId, []*model.SidebarCategoryWithChannels{ + { + SidebarCategory: favoritesCategory.SidebarCategory, + Channels: []string{channel2.Id}, + }, + }) + require.Nil(t, err) + + // Ensure that the correct channel is returned in the Channels category + res, err := ss.Channel().GetSidebarCategory(channelsCategory.Id) + assert.Nil(t, err) + assert.Equal(t, channelsCategory.Id, res.Id) + assert.Equal(t, model.SidebarCategoryChannels, channelsCategory.Type) + assert.Equal(t, []string{channel1.Id}, res.Channels) + }) + + t.Run("should return any orphaned DM channels with the Direct Messages category", func(t *testing.T) { + userId := model.NewId() + teamId := model.NewId() + + // Create the initial categories and find the DMs category + nErr := ss.Channel().CreateInitialSidebarCategories(userId, teamId) + require.Nil(t, nErr) + + categories, err := ss.Channel().GetSidebarCategories(userId, teamId) + require.Nil(t, err) + require.Equal(t, model.SidebarCategoryDirectMessages, categories.Categories[2].Type) + + dmsCategory := categories.Categories[2] + + // Create a DM + otherUserId := model.NewId() + dmChannel, nErr := ss.Channel().SaveDirectChannel( + &model.Channel{ + Name: model.GetDMNameFromIds(userId, otherUserId), + Type: model.CHANNEL_DIRECT, + }, + &model.ChannelMember{ + UserId: userId, + NotifyProps: model.GetDefaultChannelNotifyProps(), + }, + &model.ChannelMember{ + UserId: otherUserId, + NotifyProps: model.GetDefaultChannelNotifyProps(), + }, + ) + require.Nil(t, nErr) + + // Ensure that the DM is returned + res, err := ss.Channel().GetSidebarCategory(dmsCategory.Id) + assert.Nil(t, err) + assert.Equal(t, dmsCategory.Id, res.Id) + assert.Equal(t, model.SidebarCategoryDirectMessages, res.Type) + assert.Equal(t, []string{dmChannel.Id}, res.Channels) + }) + + t.Run("should return any orphaned GM channels with the Direct Messages category", func(t *testing.T) { + userId := model.NewId() + teamId := model.NewId() + + // Create the initial categories and find the DMs category + nErr := ss.Channel().CreateInitialSidebarCategories(userId, teamId) + require.Nil(t, nErr) + + categories, err := ss.Channel().GetSidebarCategories(userId, teamId) + require.Nil(t, err) + require.Equal(t, model.SidebarCategoryDirectMessages, categories.Categories[2].Type) + + dmsCategory := categories.Categories[2] + + // Create a GM + gmChannel, nErr := ss.Channel().Save(&model.Channel{ + Name: "abc", + TeamId: "", + Type: model.CHANNEL_GROUP, + }, 10) + require.Nil(t, nErr) + _, err = ss.Channel().SaveMember(&model.ChannelMember{ + UserId: userId, + ChannelId: gmChannel.Id, + NotifyProps: model.GetDefaultChannelNotifyProps(), + }) + require.Nil(t, err) + + // Ensure that the DM is returned + res, err := ss.Channel().GetSidebarCategory(dmsCategory.Id) + assert.Nil(t, err) + assert.Equal(t, dmsCategory.Id, res.Id) + assert.Equal(t, model.SidebarCategoryDirectMessages, res.Type) + assert.Equal(t, []string{gmChannel.Id}, res.Channels) + }) + + t.Run("should return orphaned DM channels in the DMs categorywhich are in a custom category on another team", func(t *testing.T) { + userId := model.NewId() + teamId := model.NewId() + + // Create the initial categories and find the DMs category + nErr := ss.Channel().CreateInitialSidebarCategories(userId, teamId) + require.Nil(t, nErr) + + categories, err := ss.Channel().GetSidebarCategories(userId, teamId) + require.Nil(t, err) + require.Equal(t, model.SidebarCategoryDirectMessages, categories.Categories[2].Type) + + dmsCategory := categories.Categories[2] + + // Create a DM + otherUserId := model.NewId() + dmChannel, nErr := ss.Channel().SaveDirectChannel( + &model.Channel{ + Name: model.GetDMNameFromIds(userId, otherUserId), + Type: model.CHANNEL_DIRECT, + }, + &model.ChannelMember{ + UserId: userId, + NotifyProps: model.GetDefaultChannelNotifyProps(), + }, + &model.ChannelMember{ + UserId: otherUserId, + NotifyProps: model.GetDefaultChannelNotifyProps(), + }, + ) + require.Nil(t, nErr) + + // Create another team and assign the DM to a custom category on that team + otherTeamId := model.NewId() + + nErr = ss.Channel().CreateInitialSidebarCategories(userId, otherTeamId) + require.Nil(t, nErr) + + _, err = ss.Channel().CreateSidebarCategory(userId, otherTeamId, &model.SidebarCategoryWithChannels{ + SidebarCategory: model.SidebarCategory{ + UserId: userId, + TeamId: teamId, + }, + Channels: []string{dmChannel.Id}, + }) + require.Nil(t, err) + + // Ensure that the DM is returned with the DMs category on the original team + res, err := ss.Channel().GetSidebarCategory(dmsCategory.Id) + assert.Nil(t, err) + assert.Equal(t, dmsCategory.Id, res.Id) + assert.Equal(t, model.SidebarCategoryDirectMessages, res.Type) + assert.Equal(t, []string{dmChannel.Id}, res.Channels) + }) +} + +func testGetSidebarCategories(t *testing.T, ss store.Store) { + t.Run("should return channels in the same order between different ways of getting categories", func(t *testing.T) { + userId := model.NewId() + teamId := model.NewId() + + nErr := ss.Channel().CreateInitialSidebarCategories(userId, teamId) + require.Nil(t, nErr) + + channelIds := []string{ + model.NewId(), + model.NewId(), + model.NewId(), + } + + newCategory, err := ss.Channel().CreateSidebarCategory(userId, teamId, &model.SidebarCategoryWithChannels{ + Channels: channelIds, + }) + require.Nil(t, err) + require.NotNil(t, newCategory) + + gotCategory, err := ss.Channel().GetSidebarCategory(newCategory.Id) + require.Nil(t, err) + + res, err := ss.Channel().GetSidebarCategories(userId, teamId) + require.Nil(t, err) + require.Len(t, res.Categories, 4) + + require.Equal(t, model.SidebarCategoryCustom, res.Categories[1].Type) + + // This looks unnecessary, but I was getting different results from some of these before + assert.Equal(t, newCategory.Channels, res.Categories[1].Channels) + assert.Equal(t, gotCategory.Channels, res.Categories[1].Channels) + assert.Equal(t, channelIds, res.Categories[1].Channels) + }) +} + +func testUpdateSidebarCategories(t *testing.T, ss store.Store, s SqlSupplier) { + t.Run("ensure the query to update SidebarCategories hasn't been polluted by UpdateSidebarCategoryOrder", func(t *testing.T) { + userId := model.NewId() + teamId := model.NewId() + + // Create the initial categories + err := ss.Channel().CreateInitialSidebarCategories(userId, teamId) + require.Nil(t, err) + + initialCategories, err := ss.Channel().GetSidebarCategories(userId, teamId) + require.Nil(t, err) + + favoritesCategory := initialCategories.Categories[0] + channelsCategory := initialCategories.Categories[1] + dmsCategory := initialCategories.Categories[2] + + // And then update one of them + updated, err := ss.Channel().UpdateSidebarCategories(userId, teamId, []*model.SidebarCategoryWithChannels{ + channelsCategory, + }) + require.Nil(t, err) + assert.Equal(t, channelsCategory, updated[0]) + assert.Equal(t, "Channels", updated[0].DisplayName) + + // And then reorder the categories + err = ss.Channel().UpdateSidebarCategoryOrder(userId, teamId, []string{dmsCategory.Id, favoritesCategory.Id, channelsCategory.Id}) + require.Nil(t, err) + + // Which somehow blanks out stuff because ??? + got, err := ss.Channel().GetSidebarCategory(favoritesCategory.Id) + require.Nil(t, err) + assert.Equal(t, "Favorites", got.DisplayName) + }) + + t.Run("categories should be returned in their original order", func(t *testing.T) { + userId := model.NewId() + teamId := model.NewId() + + // Create the initial categories + err := ss.Channel().CreateInitialSidebarCategories(userId, teamId) + require.Nil(t, err) + + initialCategories, err := ss.Channel().GetSidebarCategories(userId, teamId) + require.Nil(t, err) + + favoritesCategory := initialCategories.Categories[0] + channelsCategory := initialCategories.Categories[1] + dmsCategory := initialCategories.Categories[2] + + // And then update them + updatedCategories, err := ss.Channel().UpdateSidebarCategories(userId, teamId, []*model.SidebarCategoryWithChannels{ + favoritesCategory, + channelsCategory, + dmsCategory, + }) + assert.Nil(t, err) + assert.Equal(t, favoritesCategory.Id, updatedCategories[0].Id) + assert.Equal(t, channelsCategory.Id, updatedCategories[1].Id) + assert.Equal(t, dmsCategory.Id, updatedCategories[2].Id) + }) + + t.Run("should silently fail to update read only fields", func(t *testing.T) { + userId := model.NewId() + teamId := model.NewId() + + nErr := ss.Channel().CreateInitialSidebarCategories(userId, teamId) + require.Nil(t, nErr) + + initialCategories, err := ss.Channel().GetSidebarCategories(userId, teamId) + require.Nil(t, err) + + favoritesCategory := initialCategories.Categories[0] + channelsCategory := initialCategories.Categories[1] + dmsCategory := initialCategories.Categories[2] + + customCategory, err := ss.Channel().CreateSidebarCategory(userId, teamId, &model.SidebarCategoryWithChannels{}) + require.Nil(t, err) + + categoriesToUpdate := []*model.SidebarCategoryWithChannels{ + // Try to change the type of Favorites + { + SidebarCategory: model.SidebarCategory{ + Id: favoritesCategory.Id, + DisplayName: "something else", + }, + Channels: favoritesCategory.Channels, + }, + // Try to change the type of Channels + { + SidebarCategory: model.SidebarCategory{ + Id: channelsCategory.Id, + Type: model.SidebarCategoryDirectMessages, + }, + Channels: channelsCategory.Channels, + }, + // Try to change the Channels of DMs + { + SidebarCategory: dmsCategory.SidebarCategory, + Channels: []string{"fakechannel"}, + }, + // Try to change the UserId/TeamId of a custom category + { + SidebarCategory: model.SidebarCategory{ + Id: customCategory.Id, + UserId: model.NewId(), + TeamId: model.NewId(), + Sorting: customCategory.Sorting, + DisplayName: customCategory.DisplayName, + }, + Channels: customCategory.Channels, + }, + } + + updatedCategories, err := ss.Channel().UpdateSidebarCategories(userId, teamId, categoriesToUpdate) + assert.Nil(t, err) + + assert.NotEqual(t, "Favorites", categoriesToUpdate[0].DisplayName) + assert.Equal(t, "Favorites", updatedCategories[0].DisplayName) + assert.NotEqual(t, model.SidebarCategoryChannels, categoriesToUpdate[1].Type) + assert.Equal(t, model.SidebarCategoryChannels, updatedCategories[1].Type) + assert.NotEqual(t, []string{}, categoriesToUpdate[2].Channels) + assert.Equal(t, []string{}, updatedCategories[2].Channels) + assert.NotEqual(t, userId, categoriesToUpdate[3].UserId) + assert.Equal(t, userId, updatedCategories[3].UserId) + }) + + t.Run("should add and remove favorites preferences based on the Favorites category", func(t *testing.T) { + userId := model.NewId() + teamId := model.NewId() + + // Create the initial categories and find the favorites category + nErr := ss.Channel().CreateInitialSidebarCategories(userId, teamId) + require.Nil(t, nErr) + + categories, err := ss.Channel().GetSidebarCategories(userId, teamId) + require.Nil(t, err) + + favoritesCategory := categories.Categories[0] + require.Equal(t, model.SidebarCategoryFavorites, favoritesCategory.Type) + + // Join a channel + channel, nErr := ss.Channel().Save(&model.Channel{ + Name: "channel", + Type: model.CHANNEL_OPEN, + TeamId: teamId, + }, 10) + require.Nil(t, nErr) + _, err = ss.Channel().SaveMember(&model.ChannelMember{ + UserId: userId, + ChannelId: channel.Id, + NotifyProps: model.GetDefaultChannelNotifyProps(), + }) + require.Nil(t, err) + + // Assign it to favorites + _, err = ss.Channel().UpdateSidebarCategories(userId, teamId, []*model.SidebarCategoryWithChannels{ + { + SidebarCategory: favoritesCategory.SidebarCategory, + Channels: []string{channel.Id}, + }, + }) + assert.Nil(t, err) + + res, nErr := ss.Preference().Get(userId, model.PREFERENCE_CATEGORY_FAVORITE_CHANNEL, channel.Id) + assert.Nil(t, nErr) + assert.NotNil(t, res) + assert.Equal(t, "true", res.Value) + + // And then remove it + channelsCategory := categories.Categories[1] + require.Equal(t, model.SidebarCategoryChannels, channelsCategory.Type) + + _, err = ss.Channel().UpdateSidebarCategories(userId, teamId, []*model.SidebarCategoryWithChannels{ + { + SidebarCategory: channelsCategory.SidebarCategory, + Channels: []string{channel.Id}, + }, + }) + assert.Nil(t, err) + + res, nErr = ss.Preference().Get(userId, model.PREFERENCE_CATEGORY_FAVORITE_CHANNEL, channel.Id) + assert.NotNil(t, nErr) + assert.True(t, errors.Is(nErr, sql.ErrNoRows)) + assert.Nil(t, res) + }) + + t.Run("should add and remove favorites preferences for DMs", func(t *testing.T) { + userId := model.NewId() + teamId := model.NewId() + + // Create the initial categories and find the favorites category + nErr := ss.Channel().CreateInitialSidebarCategories(userId, teamId) + require.Nil(t, nErr) + + categories, err := ss.Channel().GetSidebarCategories(userId, teamId) + require.Nil(t, err) + + favoritesCategory := categories.Categories[0] + require.Equal(t, model.SidebarCategoryFavorites, favoritesCategory.Type) + + // Create a direct channel + otherUserId := model.NewId() + + dmChannel, nErr := ss.Channel().SaveDirectChannel( + &model.Channel{ + Name: model.GetDMNameFromIds(userId, otherUserId), + Type: model.CHANNEL_DIRECT, + }, + &model.ChannelMember{ + UserId: userId, + NotifyProps: model.GetDefaultChannelNotifyProps(), + }, + &model.ChannelMember{ + UserId: otherUserId, + NotifyProps: model.GetDefaultChannelNotifyProps(), + }, + ) + assert.Nil(t, nErr) + + // Assign it to favorites + _, err = ss.Channel().UpdateSidebarCategories(userId, teamId, []*model.SidebarCategoryWithChannels{ + { + SidebarCategory: favoritesCategory.SidebarCategory, + Channels: []string{dmChannel.Id}, + }, + }) + assert.Nil(t, err) + + res, nErr := ss.Preference().Get(userId, model.PREFERENCE_CATEGORY_FAVORITE_CHANNEL, dmChannel.Id) + assert.Nil(t, nErr) + assert.NotNil(t, res) + assert.Equal(t, "true", res.Value) + + // And then remove it + dmsCategory := categories.Categories[2] + require.Equal(t, model.SidebarCategoryDirectMessages, dmsCategory.Type) + + _, err = ss.Channel().UpdateSidebarCategories(userId, teamId, []*model.SidebarCategoryWithChannels{ + { + SidebarCategory: dmsCategory.SidebarCategory, + Channels: []string{dmChannel.Id}, + }, + }) + assert.Nil(t, err) + + res, nErr = ss.Preference().Get(userId, model.PREFERENCE_CATEGORY_FAVORITE_CHANNEL, dmChannel.Id) + assert.NotNil(t, nErr) + assert.True(t, errors.Is(nErr, sql.ErrNoRows)) + assert.Nil(t, res) + }) + + t.Run("should add and remove favorites preferences, even if the channel is already favorited in preferences", func(t *testing.T) { + userId := model.NewId() + teamId := model.NewId() + teamId2 := model.NewId() + + // Create the initial categories and find the favorites categories in each team + nErr := ss.Channel().CreateInitialSidebarCategories(userId, teamId) + require.Nil(t, nErr) + + categories, err := ss.Channel().GetSidebarCategories(userId, teamId) + require.Nil(t, err) + + favoritesCategory := categories.Categories[0] + require.Equal(t, model.SidebarCategoryFavorites, favoritesCategory.Type) + + nErr = ss.Channel().CreateInitialSidebarCategories(userId, teamId2) + require.Nil(t, nErr) + + categories2, err := ss.Channel().GetSidebarCategories(userId, teamId2) + require.Nil(t, err) + + favoritesCategory2 := categories2.Categories[0] + require.Equal(t, model.SidebarCategoryFavorites, favoritesCategory2.Type) + + // Create a direct channel + otherUserId := model.NewId() + + dmChannel, nErr := ss.Channel().SaveDirectChannel( + &model.Channel{ + Name: model.GetDMNameFromIds(userId, otherUserId), + Type: model.CHANNEL_DIRECT, + }, + &model.ChannelMember{ + UserId: userId, + NotifyProps: model.GetDefaultChannelNotifyProps(), + }, + &model.ChannelMember{ + UserId: otherUserId, + NotifyProps: model.GetDefaultChannelNotifyProps(), + }, + ) + assert.Nil(t, nErr) + + // Assign it to favorites on the first team. The favorites preference gets set for all teams. + _, err = ss.Channel().UpdateSidebarCategories(userId, teamId, []*model.SidebarCategoryWithChannels{ + { + SidebarCategory: favoritesCategory.SidebarCategory, + Channels: []string{dmChannel.Id}, + }, + }) + assert.Nil(t, err) + + res, nErr := ss.Preference().Get(userId, model.PREFERENCE_CATEGORY_FAVORITE_CHANNEL, dmChannel.Id) + assert.Nil(t, nErr) + assert.NotNil(t, res) + assert.Equal(t, "true", res.Value) + + // Assign it to favorites on the second team. The favorites preference is already set. + updated, err := ss.Channel().UpdateSidebarCategories(userId, teamId, []*model.SidebarCategoryWithChannels{ + { + SidebarCategory: favoritesCategory2.SidebarCategory, + Channels: []string{dmChannel.Id}, + }, + }) + assert.Nil(t, err) + assert.Equal(t, []string{dmChannel.Id}, updated[0].Channels) + + res, nErr = ss.Preference().Get(userId, model.PREFERENCE_CATEGORY_FAVORITE_CHANNEL, dmChannel.Id) + assert.NoError(t, nErr) + assert.NotNil(t, res) + assert.Equal(t, "true", res.Value) + + // Remove it from favorites on the first team. This clears the favorites preference for all teams. + _, err = ss.Channel().UpdateSidebarCategories(userId, teamId, []*model.SidebarCategoryWithChannels{ + { + SidebarCategory: favoritesCategory.SidebarCategory, + Channels: []string{}, + }, + }) + assert.Nil(t, err) + + res, nErr = ss.Preference().Get(userId, model.PREFERENCE_CATEGORY_FAVORITE_CHANNEL, dmChannel.Id) + require.Error(t, nErr) + assert.Nil(t, res) + + // Remove it from favorites on the second team. The favorites preference was already deleted. + _, err = ss.Channel().UpdateSidebarCategories(userId, teamId2, []*model.SidebarCategoryWithChannels{ + { + SidebarCategory: favoritesCategory2.SidebarCategory, + Channels: []string{}, + }, + }) + assert.Nil(t, err) + + res, nErr = ss.Preference().Get(userId, model.PREFERENCE_CATEGORY_FAVORITE_CHANNEL, dmChannel.Id) + require.Error(t, nErr) + assert.Nil(t, res) + }) + + t.Run("should not affect other users' favorites preferences", func(t *testing.T) { + userId := model.NewId() + teamId := model.NewId() + + // Create the initial categories and find the favorites category + nErr := ss.Channel().CreateInitialSidebarCategories(userId, teamId) + require.Nil(t, nErr) + + categories, err := ss.Channel().GetSidebarCategories(userId, teamId) + require.Nil(t, err) + + favoritesCategory := categories.Categories[0] + require.Equal(t, model.SidebarCategoryFavorites, favoritesCategory.Type) + channelsCategory := categories.Categories[1] + require.Equal(t, model.SidebarCategoryChannels, channelsCategory.Type) + + // Create the other users' categories + userId2 := model.NewId() + + nErr = ss.Channel().CreateInitialSidebarCategories(userId2, teamId) + require.Nil(t, nErr) + + categories2, err := ss.Channel().GetSidebarCategories(userId2, teamId) + require.Nil(t, err) + + favoritesCategory2 := categories2.Categories[0] + require.Equal(t, model.SidebarCategoryFavorites, favoritesCategory2.Type) + channelsCategory2 := categories2.Categories[1] + require.Equal(t, model.SidebarCategoryChannels, channelsCategory2.Type) + + // Have both users join a channel + channel, nErr := ss.Channel().Save(&model.Channel{ + Name: "channel", + Type: model.CHANNEL_OPEN, + TeamId: teamId, + }, 10) + require.Nil(t, nErr) + _, err = ss.Channel().SaveMember(&model.ChannelMember{ + UserId: userId, + ChannelId: channel.Id, + NotifyProps: model.GetDefaultChannelNotifyProps(), + }) + require.Nil(t, err) + _, err = ss.Channel().SaveMember(&model.ChannelMember{ + UserId: userId2, + ChannelId: channel.Id, + NotifyProps: model.GetDefaultChannelNotifyProps(), + }) + require.Nil(t, err) + + // Have user1 favorite it + _, err = ss.Channel().UpdateSidebarCategories(userId, teamId, []*model.SidebarCategoryWithChannels{ + { + SidebarCategory: favoritesCategory.SidebarCategory, + Channels: []string{channel.Id}, + }, + { + SidebarCategory: channelsCategory.SidebarCategory, + Channels: []string{}, + }, + }) + assert.Nil(t, err) + + res, nErr := ss.Preference().Get(userId, model.PREFERENCE_CATEGORY_FAVORITE_CHANNEL, channel.Id) + assert.Nil(t, nErr) + assert.NotNil(t, res) + assert.Equal(t, "true", res.Value) + + res, nErr = ss.Preference().Get(userId2, model.PREFERENCE_CATEGORY_FAVORITE_CHANNEL, channel.Id) + assert.True(t, errors.Is(nErr, sql.ErrNoRows)) + assert.Nil(t, res) + + // And user2 favorite it + _, err = ss.Channel().UpdateSidebarCategories(userId2, teamId, []*model.SidebarCategoryWithChannels{ + { + SidebarCategory: favoritesCategory2.SidebarCategory, + Channels: []string{channel.Id}, + }, + { + SidebarCategory: channelsCategory2.SidebarCategory, + Channels: []string{}, + }, + }) + assert.Nil(t, err) + + res, nErr = ss.Preference().Get(userId, model.PREFERENCE_CATEGORY_FAVORITE_CHANNEL, channel.Id) + assert.Nil(t, nErr) + assert.NotNil(t, res) + assert.Equal(t, "true", res.Value) + + res, nErr = ss.Preference().Get(userId2, model.PREFERENCE_CATEGORY_FAVORITE_CHANNEL, channel.Id) + assert.Nil(t, nErr) + assert.NotNil(t, res) + assert.Equal(t, "true", res.Value) + + // And then user1 unfavorite it + _, err = ss.Channel().UpdateSidebarCategories(userId, teamId, []*model.SidebarCategoryWithChannels{ + { + SidebarCategory: channelsCategory.SidebarCategory, + Channels: []string{channel.Id}, + }, + { + SidebarCategory: favoritesCategory.SidebarCategory, + Channels: []string{}, + }, + }) + assert.Nil(t, err) + + res, nErr = ss.Preference().Get(userId, model.PREFERENCE_CATEGORY_FAVORITE_CHANNEL, channel.Id) + assert.True(t, errors.Is(nErr, sql.ErrNoRows)) + assert.Nil(t, res) + + res, nErr = ss.Preference().Get(userId2, model.PREFERENCE_CATEGORY_FAVORITE_CHANNEL, channel.Id) + assert.Nil(t, nErr) + assert.NotNil(t, res) + assert.Equal(t, "true", res.Value) + + // And finally user2 favorite it + _, err = ss.Channel().UpdateSidebarCategories(userId2, teamId, []*model.SidebarCategoryWithChannels{ + { + SidebarCategory: channelsCategory2.SidebarCategory, + Channels: []string{channel.Id}, + }, + { + SidebarCategory: favoritesCategory2.SidebarCategory, + Channels: []string{}, + }, + }) + assert.Nil(t, err) + + res, nErr = ss.Preference().Get(userId, model.PREFERENCE_CATEGORY_FAVORITE_CHANNEL, channel.Id) + assert.True(t, errors.Is(nErr, sql.ErrNoRows)) + assert.Nil(t, res) + + res, nErr = ss.Preference().Get(userId2, model.PREFERENCE_CATEGORY_FAVORITE_CHANNEL, channel.Id) + assert.True(t, errors.Is(nErr, sql.ErrNoRows)) + assert.Nil(t, res) + }) + + t.Run("channels removed from Channels or DMs categories should be re-added", func(t *testing.T) { + userId := model.NewId() + teamId := model.NewId() + + // Create some channels + channel, nErr := ss.Channel().Save(&model.Channel{ + Name: "channel", + Type: model.CHANNEL_OPEN, + TeamId: teamId, + }, 10) + require.Nil(t, nErr) + _, err := ss.Channel().SaveMember(&model.ChannelMember{ + UserId: userId, + ChannelId: channel.Id, + NotifyProps: model.GetDefaultChannelNotifyProps(), + }) + require.Nil(t, err) + + otherUserId := model.NewId() + dmChannel, nErr := ss.Channel().SaveDirectChannel( + &model.Channel{ + Name: model.GetDMNameFromIds(userId, otherUserId), + Type: model.CHANNEL_DIRECT, + }, + &model.ChannelMember{ + UserId: userId, + NotifyProps: model.GetDefaultChannelNotifyProps(), + }, + &model.ChannelMember{ + UserId: otherUserId, + NotifyProps: model.GetDefaultChannelNotifyProps(), + }, + ) + require.Nil(t, nErr) + + nErr = ss.Channel().CreateInitialSidebarCategories(userId, teamId) + require.Nil(t, nErr) + + // And some categories + initialCategories, err := ss.Channel().GetSidebarCategories(userId, teamId) + require.Nil(t, err) + + channelsCategory := initialCategories.Categories[1] + dmsCategory := initialCategories.Categories[2] + + require.Equal(t, []string{channel.Id}, channelsCategory.Channels) + require.Equal(t, []string{dmChannel.Id}, dmsCategory.Channels) + + // Try to save the categories with no channels in them + categoriesToUpdate := []*model.SidebarCategoryWithChannels{ + { + SidebarCategory: channelsCategory.SidebarCategory, + Channels: []string{}, + }, + { + SidebarCategory: dmsCategory.SidebarCategory, + Channels: []string{}, + }, + } + + updatedCategories, err := ss.Channel().UpdateSidebarCategories(userId, teamId, categoriesToUpdate) + assert.Nil(t, err) + + // The channels should still exist in the category because they would otherwise be orphaned + assert.Equal(t, []string{channel.Id}, updatedCategories[0].Channels) + assert.Equal(t, []string{dmChannel.Id}, updatedCategories[1].Channels) + }) + + t.Run("should be able to move DMs into and out of custom categories", func(t *testing.T) { + userId := model.NewId() + teamId := model.NewId() + + otherUserId := model.NewId() + dmChannel, nErr := ss.Channel().SaveDirectChannel( + &model.Channel{ + Name: model.GetDMNameFromIds(userId, otherUserId), + Type: model.CHANNEL_DIRECT, + }, + &model.ChannelMember{ + UserId: userId, + NotifyProps: model.GetDefaultChannelNotifyProps(), + }, + &model.ChannelMember{ + UserId: otherUserId, + NotifyProps: model.GetDefaultChannelNotifyProps(), + }, + ) + require.Nil(t, nErr) + + nErr = ss.Channel().CreateInitialSidebarCategories(userId, teamId) + require.Nil(t, nErr) + + // The DM should start in the DMs category + initialCategories, err := ss.Channel().GetSidebarCategories(userId, teamId) + require.Nil(t, err) + + dmsCategory := initialCategories.Categories[2] + require.Equal(t, []string{dmChannel.Id}, dmsCategory.Channels) + + // Now move the DM into a custom category + customCategory, err := ss.Channel().CreateSidebarCategory(userId, teamId, &model.SidebarCategoryWithChannels{}) + require.Nil(t, err) + + categoriesToUpdate := []*model.SidebarCategoryWithChannels{ + { + SidebarCategory: dmsCategory.SidebarCategory, + Channels: []string{}, + }, + { + SidebarCategory: customCategory.SidebarCategory, + Channels: []string{dmChannel.Id}, + }, + } + + updatedCategories, err := ss.Channel().UpdateSidebarCategories(userId, teamId, categoriesToUpdate) + assert.Nil(t, err) + assert.Equal(t, dmsCategory.Id, updatedCategories[0].Id) + assert.Equal(t, []string{}, updatedCategories[0].Channels) + assert.Equal(t, customCategory.Id, updatedCategories[1].Id) + assert.Equal(t, []string{dmChannel.Id}, updatedCategories[1].Channels) + + updatedDmsCategory, err := ss.Channel().GetSidebarCategory(dmsCategory.Id) + require.Nil(t, err) + assert.Equal(t, []string{}, updatedDmsCategory.Channels) + + updatedCustomCategory, err := ss.Channel().GetSidebarCategory(customCategory.Id) + require.Nil(t, err) + assert.Equal(t, []string{dmChannel.Id}, updatedCustomCategory.Channels) + + // And move it back out of the custom category + categoriesToUpdate = []*model.SidebarCategoryWithChannels{ + { + SidebarCategory: dmsCategory.SidebarCategory, + Channels: []string{dmChannel.Id}, + }, + { + SidebarCategory: customCategory.SidebarCategory, + Channels: []string{}, + }, + } + + updatedCategories, err = ss.Channel().UpdateSidebarCategories(userId, teamId, categoriesToUpdate) + assert.Nil(t, err) + assert.Equal(t, dmsCategory.Id, updatedCategories[0].Id) + assert.Equal(t, []string{dmChannel.Id}, updatedCategories[0].Channels) + assert.Equal(t, customCategory.Id, updatedCategories[1].Id) + assert.Equal(t, []string{}, updatedCategories[1].Channels) + + updatedDmsCategory, err = ss.Channel().GetSidebarCategory(dmsCategory.Id) + require.Nil(t, err) + assert.Equal(t, []string{dmChannel.Id}, updatedDmsCategory.Channels) + + updatedCustomCategory, err = ss.Channel().GetSidebarCategory(customCategory.Id) + require.Nil(t, err) + assert.Equal(t, []string{}, updatedCustomCategory.Channels) + }) + + t.Run("should successfully move channels between categories", func(t *testing.T) { + userId := model.NewId() + teamId := model.NewId() + + // Join a channel + channel, nErr := ss.Channel().Save(&model.Channel{ + Name: "channel", + Type: model.CHANNEL_OPEN, + TeamId: teamId, + }, 10) + require.Nil(t, nErr) + _, err := ss.Channel().SaveMember(&model.ChannelMember{ + UserId: userId, + ChannelId: channel.Id, + NotifyProps: model.GetDefaultChannelNotifyProps(), + }) + require.Nil(t, err) + + // And then create the initial categories so that it includes the channel + nErr = ss.Channel().CreateInitialSidebarCategories(userId, teamId) + require.Nil(t, nErr) + + initialCategories, err := ss.Channel().GetSidebarCategories(userId, teamId) + require.Nil(t, err) + + channelsCategory := initialCategories.Categories[1] + require.Equal(t, []string{channel.Id}, channelsCategory.Channels) + + customCategory, err := ss.Channel().CreateSidebarCategory(userId, teamId, &model.SidebarCategoryWithChannels{}) + require.Nil(t, err) + + // Move the channel one way + updatedCategories, err := ss.Channel().UpdateSidebarCategories(userId, teamId, []*model.SidebarCategoryWithChannels{ + { + SidebarCategory: channelsCategory.SidebarCategory, + Channels: []string{}, + }, + { + SidebarCategory: customCategory.SidebarCategory, + Channels: []string{channel.Id}, + }, + }) + assert.Nil(t, err) + + assert.Equal(t, []string{}, updatedCategories[0].Channels) + assert.Equal(t, []string{channel.Id}, updatedCategories[1].Channels) + + // And then the other + updatedCategories, err = ss.Channel().UpdateSidebarCategories(userId, teamId, []*model.SidebarCategoryWithChannels{ + { + SidebarCategory: channelsCategory.SidebarCategory, + Channels: []string{channel.Id}, + }, + { + SidebarCategory: customCategory.SidebarCategory, + Channels: []string{}, + }, + }) + assert.Nil(t, err) + assert.Equal(t, []string{channel.Id}, updatedCategories[0].Channels) + assert.Equal(t, []string{}, updatedCategories[1].Channels) + }) +} + +func testDeleteSidebarCategory(t *testing.T, ss store.Store, s SqlSupplier) { + setupInitialSidebarCategories := func(t *testing.T, ss store.Store) (string, string) { + userId := model.NewId() + teamId := model.NewId() + + nErr := ss.Channel().CreateInitialSidebarCategories(userId, teamId) + require.Nil(t, nErr) + + res, err := ss.Channel().GetSidebarCategories(userId, teamId) + require.Nil(t, err) + require.Len(t, res.Categories, 3) + + return userId, teamId + } + + t.Run("should correctly remove an empty category", func(t *testing.T) { + userId, teamId := setupInitialSidebarCategories(t, ss) + defer ss.User().PermanentDelete(userId) + + newCategory, err := ss.Channel().CreateSidebarCategory(userId, teamId, &model.SidebarCategoryWithChannels{}) + require.Nil(t, err) + require.NotNil(t, newCategory) + + // Ensure that the category was created properly + res, err := ss.Channel().GetSidebarCategories(userId, teamId) + require.Nil(t, err) + require.Len(t, res.Categories, 4) + + // Then delete it and confirm that was done correctly + err = ss.Channel().DeleteSidebarCategory(newCategory.Id) + assert.Nil(t, err) + + res, err = ss.Channel().GetSidebarCategories(userId, teamId) + require.Nil(t, err) + require.Len(t, res.Categories, 3) + }) + + t.Run("should correctly remove a category and its channels", func(t *testing.T) { + userId, teamId := setupInitialSidebarCategories(t, ss) + defer ss.User().PermanentDelete(userId) + + user := &model.User{ + Id: userId, + } + + // Create some channels + channel1, nErr := ss.Channel().Save(&model.Channel{ + Name: model.NewId(), + TeamId: teamId, + Type: model.CHANNEL_OPEN, + }, 1000) + require.Nil(t, nErr) + defer ss.Channel().PermanentDelete(channel1.Id) + + channel2, nErr := ss.Channel().Save(&model.Channel{ + Name: model.NewId(), + TeamId: teamId, + Type: model.CHANNEL_PRIVATE, + }, 1000) + require.Nil(t, nErr) + defer ss.Channel().PermanentDelete(channel2.Id) + + dmChannel1, nErr := ss.Channel().CreateDirectChannel(user, &model.User{ + Id: model.NewId(), + }) + require.Nil(t, nErr) + defer ss.Channel().PermanentDelete(dmChannel1.Id) + + // Assign some of those channels to a custom category + newCategory, err := ss.Channel().CreateSidebarCategory(userId, teamId, &model.SidebarCategoryWithChannels{ + Channels: []string{channel1.Id, channel2.Id, dmChannel1.Id}, + }) + require.Nil(t, err) + require.NotNil(t, newCategory) + + // Ensure that the categories are set up correctly + res, err := ss.Channel().GetSidebarCategories(userId, teamId) + require.Nil(t, err) + require.Len(t, res.Categories, 4) + + require.Equal(t, model.SidebarCategoryCustom, res.Categories[1].Type) + require.Equal(t, []string{channel1.Id, channel2.Id, dmChannel1.Id}, res.Categories[1].Channels) + + // Actually delete the channel + err = ss.Channel().DeleteSidebarCategory(newCategory.Id) + assert.Nil(t, err) + + // Confirm that the category was deleted... + res, err = ss.Channel().GetSidebarCategories(userId, teamId) + assert.Nil(t, err) + assert.Len(t, res.Categories, 3) + + // ...and that the corresponding SidebarChannel entries were deleted + count, countErr := s.GetMaster().SelectInt(` + SELECT + COUNT(*) + FROM + SidebarChannels + WHERE + CategoryId = :CategoryId`, map[string]interface{}{"CategoryId": newCategory.Id}) + require.Nil(t, countErr) + assert.Equal(t, int64(0), count) + }) + + t.Run("should not allow you to remove non-custom categories", func(t *testing.T) { + userId, teamId := setupInitialSidebarCategories(t, ss) + defer ss.User().PermanentDelete(userId) + res, err := ss.Channel().GetSidebarCategories(userId, teamId) + require.Nil(t, err) + require.Len(t, res.Categories, 3) + require.Equal(t, model.SidebarCategoryFavorites, res.Categories[0].Type) + require.Equal(t, model.SidebarCategoryChannels, res.Categories[1].Type) + require.Equal(t, model.SidebarCategoryDirectMessages, res.Categories[2].Type) + + err = ss.Channel().DeleteSidebarCategory(res.Categories[0].Id) + assert.NotNil(t, err) + + err = ss.Channel().DeleteSidebarCategory(res.Categories[1].Id) + assert.NotNil(t, err) + + err = ss.Channel().DeleteSidebarCategory(res.Categories[2].Id) + assert.NotNil(t, err) + }) +}