From 4a5994906ce56effafa458db0feb212321431154 Mon Sep 17 00:00:00 2001 From: fume4mattermost Date: Tue, 17 Dec 2024 00:29:43 +0300 Subject: [PATCH] [MM-56031] Add DeleteSidebarCategory method and channel category API tests (#29161) * Add DeleteSidebarCategory method and channel category API tests * Add test to verify channels move to default categories when custom category is deleted * rename channel category tests and remove duplicate context * TestNotifications fix --------- Co-authored-by: Fume Co-authored-by: Ben Schumacher Co-authored-by: Mattermost Build --- server/channels/api4/channel_category_test.go | 705 ++++++++++++++++++ server/public/model/client4.go | 11 + 2 files changed, 716 insertions(+) diff --git a/server/channels/api4/channel_category_test.go b/server/channels/api4/channel_category_test.go index c22390d7ad09..d8b87ac69529 100644 --- a/server/channels/api4/channel_category_test.go +++ b/server/channels/api4/channel_category_test.go @@ -7,6 +7,7 @@ import ( "context" "encoding/json" "fmt" + "net/http" "testing" "time" @@ -550,6 +551,710 @@ func TestUpdateCategoriesForTeamForUser(t *testing.T) { }) } +func TestGetCategoriesForTeamForUser(t *testing.T) { + th := Setup(t).InitBasic() + defer th.TearDown() + + t.Run("should return categories when user has permission", func(t *testing.T) { + // Get categories for the basic user + categories, _, err := th.Client.GetSidebarCategoriesForTeamForUser(context.Background(), th.BasicUser.Id, th.BasicTeam.Id, "") + require.NoError(t, err) + require.NotNil(t, categories) + require.Len(t, categories.Categories, 3) // Default categories: Channels, Favorites, Direct Messages + require.Len(t, categories.Order, 3) + }) + + t.Run("should return error when user doesn't have permission", func(t *testing.T) { + // Create a new user that's not on the team + user, appErr := th.App.CreateUser(th.Context, &model.User{ + Email: th.GenerateTestEmail(), + Username: "user_" + model.NewId(), + Password: "password", + }) + require.Nil(t, appErr) + + client := th.CreateClient() + _, _, err := client.Login(context.Background(), user.Email, "password") + require.NoError(t, err) + + // Attempt to get categories for the basic user + _, resp, err := client.GetSidebarCategoriesForTeamForUser(context.Background(), th.BasicUser.Id, th.BasicTeam.Id, "") + require.Error(t, err) + require.Equal(t, http.StatusForbidden, resp.StatusCode) + }) + + t.Run("should return error with invalid user id", func(t *testing.T) { + _, resp, err := th.Client.GetSidebarCategoriesForTeamForUser(context.Background(), "invalid_user_id", th.BasicTeam.Id, "") + require.Error(t, err) + require.Equal(t, http.StatusNotFound, resp.StatusCode) + }) + + t.Run("should return error with invalid team id", func(t *testing.T) { + _, resp, err := th.Client.GetSidebarCategoriesForTeamForUser(context.Background(), th.BasicUser.Id, "invalid_team_id", "") + require.Error(t, err) + require.Equal(t, http.StatusNotFound, resp.StatusCode) + }) + + t.Run("should return error when user is not logged in", func(t *testing.T) { + client := th.CreateClient() + _, resp, err := client.GetSidebarCategoriesForTeamForUser(context.Background(), th.BasicUser.Id, th.BasicTeam.Id, "") + require.Error(t, err) + require.Equal(t, http.StatusUnauthorized, resp.StatusCode) + }) +} + +func TestGetCategoryOrderForTeamForUser(t *testing.T) { + th := Setup(t).InitBasic() + defer th.TearDown() + + t.Run("should return category order when user has permission", func(t *testing.T) { + // Get categories first to ensure order exists + categories, _, err := th.Client.GetSidebarCategoriesForTeamForUser(context.Background(), th.BasicUser.Id, th.BasicTeam.Id, "") + require.NoError(t, err) + require.NotNil(t, categories) + require.Len(t, categories.Order, 3) // Default categories: Channels, Favorites, Direct Messages + + // Get order + order, _, err := th.Client.GetSidebarCategoryOrderForTeamForUser(context.Background(), th.BasicUser.Id, th.BasicTeam.Id, "") + require.NoError(t, err) + require.NotNil(t, order) + require.Len(t, order, 3) + require.ElementsMatch(t, categories.Order, order) + }) + + t.Run("should return error when user doesn't have permission", func(t *testing.T) { + // Create a new user that's not on the team + user, appErr := th.App.CreateUser(th.Context, &model.User{ + Email: th.GenerateTestEmail(), + Username: "user_" + model.NewId(), + Password: "password", + }) + require.Nil(t, appErr) + + client := th.CreateClient() + _, _, err := client.Login(context.Background(), user.Email, "password") + require.NoError(t, err) + + // Attempt to get order for the basic user + _, resp, err := client.GetSidebarCategoryOrderForTeamForUser(context.Background(), th.BasicUser.Id, th.BasicTeam.Id, "") + require.Error(t, err) + require.Equal(t, http.StatusForbidden, resp.StatusCode) + }) + + t.Run("should return error with invalid user id", func(t *testing.T) { + _, resp, err := th.Client.GetSidebarCategoryOrderForTeamForUser(context.Background(), "invalid_user_id", th.BasicTeam.Id, "") + require.Error(t, err) + require.Equal(t, http.StatusNotFound, resp.StatusCode) + }) + + t.Run("should return error with invalid team id", func(t *testing.T) { + _, resp, err := th.Client.GetSidebarCategoryOrderForTeamForUser(context.Background(), th.BasicUser.Id, "invalid_team_id", "") + require.Error(t, err) + require.Equal(t, http.StatusNotFound, resp.StatusCode) + }) + + t.Run("should return error when user is not logged in", func(t *testing.T) { + client := th.CreateClient() + _, resp, err := client.GetSidebarCategoryOrderForTeamForUser(context.Background(), th.BasicUser.Id, th.BasicTeam.Id, "") + require.Error(t, err) + require.Equal(t, http.StatusUnauthorized, resp.StatusCode) + }) +} + +func TestUpdateCategoryOrderForTeamForUser(t *testing.T) { + th := Setup(t).InitBasic() + defer th.TearDown() + + t.Run("should update order", func(t *testing.T) { + user, client := setupUserForSubtest(t, th) + + categories, _, err := client.GetSidebarCategoriesForTeamForUser(context.Background(), user.Id, th.BasicTeam.Id, "") + require.NoError(t, err) + require.Len(t, categories.Categories, 3) + require.Len(t, categories.Order, 3) + + channelsCategory := categories.Categories[1] + require.Equal(t, model.SidebarCategoryChannels, channelsCategory.Type) + + // Update order + newOrder := []string{categories.Order[1], categories.Order[0], categories.Order[2]} + updatedOrder, _, err := client.UpdateSidebarCategoryOrderForTeamForUser(context.Background(), user.Id, th.BasicTeam.Id, newOrder) + require.NoError(t, err) + require.EqualValues(t, newOrder, updatedOrder) + + // Verify order was updated + categories, _, err = client.GetSidebarCategoriesForTeamForUser(context.Background(), user.Id, th.BasicTeam.Id, "") + require.NoError(t, err) + require.Len(t, categories.Categories, 3) + require.Len(t, categories.Order, 3) + require.EqualValues(t, newOrder, categories.Order) + }) + + t.Run("should return error when user doesn't have permission", func(t *testing.T) { + // Create a new user that's not on the team + user, appErr := th.App.CreateUser(th.Context, &model.User{ + Email: th.GenerateTestEmail(), + Username: "user_" + model.NewId(), + Password: "password", + }) + require.Nil(t, appErr) + + client := th.CreateClient() + _, _, err := client.Login(context.Background(), user.Email, "password") + require.NoError(t, err) + + // Get categories for basic user to try to update + categories, _, err := th.Client.GetSidebarCategoriesForTeamForUser(context.Background(), th.BasicUser.Id, th.BasicTeam.Id, "") + require.NoError(t, err) + + // Attempt to update order for the basic user + newOrder := []string{categories.Order[1], categories.Order[0], categories.Order[2]} + _, resp, err := client.UpdateSidebarCategoryOrderForTeamForUser(context.Background(), th.BasicUser.Id, th.BasicTeam.Id, newOrder) + require.Error(t, err) + require.Equal(t, http.StatusForbidden, resp.StatusCode) + }) + + t.Run("should return error with invalid category id", func(t *testing.T) { + categories, _, err := th.Client.GetSidebarCategoriesForTeamForUser(context.Background(), th.BasicUser.Id, th.BasicTeam.Id, "") + require.NoError(t, err) + + // Try to update with an invalid category ID + newOrder := []string{categories.Order[0], "invalid_category_id", categories.Order[2]} + _, resp, err := th.Client.UpdateSidebarCategoryOrderForTeamForUser(context.Background(), th.BasicUser.Id, th.BasicTeam.Id, newOrder) + require.Error(t, err) + require.Equal(t, http.StatusBadRequest, resp.StatusCode) + }) + + t.Run("should return error with missing category", func(t *testing.T) { + categories, _, err := th.Client.GetSidebarCategoriesForTeamForUser(context.Background(), th.BasicUser.Id, th.BasicTeam.Id, "") + require.NoError(t, err) + + // Try to update with a missing category + newOrder := []string{categories.Order[1], categories.Order[0]} // Missing one category + _, resp, err := th.Client.UpdateSidebarCategoryOrderForTeamForUser(context.Background(), th.BasicUser.Id, th.BasicTeam.Id, newOrder) + require.Error(t, err) + require.Equal(t, http.StatusInternalServerError, resp.StatusCode) + }) + + t.Run("should return error with invalid user id", func(t *testing.T) { + categories, _, err := th.Client.GetSidebarCategoriesForTeamForUser(context.Background(), th.BasicUser.Id, th.BasicTeam.Id, "") + require.NoError(t, err) + + newOrder := []string{categories.Order[1], categories.Order[0], categories.Order[2]} + _, resp, err := th.Client.UpdateSidebarCategoryOrderForTeamForUser(context.Background(), "invalid_user_id", th.BasicTeam.Id, newOrder) + require.Error(t, err) + require.Equal(t, http.StatusNotFound, resp.StatusCode) + }) + + t.Run("should return error with invalid team id", func(t *testing.T) { + categories, _, err := th.Client.GetSidebarCategoriesForTeamForUser(context.Background(), th.BasicUser.Id, th.BasicTeam.Id, "") + require.NoError(t, err) + + newOrder := []string{categories.Order[1], categories.Order[0], categories.Order[2]} + _, resp, err := th.Client.UpdateSidebarCategoryOrderForTeamForUser(context.Background(), th.BasicUser.Id, "invalid_team_id", newOrder) + require.Error(t, err) + require.Equal(t, http.StatusNotFound, resp.StatusCode) + }) + + t.Run("should return error when user is not logged in", func(t *testing.T) { + categories, _, err := th.Client.GetSidebarCategoriesForTeamForUser(context.Background(), th.BasicUser.Id, th.BasicTeam.Id, "") + require.NoError(t, err) + + client := th.CreateClient() + newOrder := []string{categories.Order[1], categories.Order[0], categories.Order[2]} + _, resp, err := client.UpdateSidebarCategoryOrderForTeamForUser(context.Background(), th.BasicUser.Id, th.BasicTeam.Id, newOrder) + require.Error(t, err) + require.Equal(t, http.StatusUnauthorized, resp.StatusCode) + }) + + t.Run("should not crash with null input", func(t *testing.T) { + require.NotPanics(t, func() { + user, client := setupUserForSubtest(t, th) + payload := []byte(`null`) + route := fmt.Sprintf("/users/%s/teams/%s/channels/categories/order", user.Id, th.BasicTeam.Id) + r, err := client.DoAPIPutBytes(context.Background(), route, payload) + require.Error(t, err) + closeBody(r) + }) + }) +} + +func TestGetCategoryForTeamForUser(t *testing.T) { + th := Setup(t).InitBasic() + defer th.TearDown() + + t.Run("should return category when user has permission", func(t *testing.T) { + user, client := setupUserForSubtest(t, th) + + // Get categories first to get a valid category ID + categories, _, err := client.GetSidebarCategoriesForTeamForUser(context.Background(), user.Id, th.BasicTeam.Id, "") + require.NoError(t, err) + require.NotEmpty(t, categories.Categories) + + // Get specific category + category := categories.Categories[0] + received, _, err := client.GetSidebarCategoryForTeamForUser(context.Background(), user.Id, th.BasicTeam.Id, category.Id, "") + require.NoError(t, err) + require.NotNil(t, received) + require.Equal(t, category.Id, received.Id) + require.Equal(t, category.DisplayName, received.DisplayName) + require.Equal(t, category.Type, received.Type) + }) + + t.Run("should return error when user doesn't have permission", func(t *testing.T) { + // Create a new user that's not on the team + user, appErr := th.App.CreateUser(th.Context, &model.User{ + Email: th.GenerateTestEmail(), + Username: "user_" + model.NewId(), + Password: "password", + }) + require.Nil(t, appErr) + + client := th.CreateClient() + _, _, err := client.Login(context.Background(), user.Email, "password") + require.NoError(t, err) + + // Get categories for basic user to get a valid category ID + categories, _, err := th.Client.GetSidebarCategoriesForTeamForUser(context.Background(), th.BasicUser.Id, th.BasicTeam.Id, "") + require.NoError(t, err) + require.NotEmpty(t, categories.Categories) + + // Attempt to get category for the basic user + _, resp, err := client.GetSidebarCategoryForTeamForUser(context.Background(), th.BasicUser.Id, th.BasicTeam.Id, categories.Categories[0].Id, "") + require.Error(t, err) + require.Equal(t, http.StatusForbidden, resp.StatusCode) + }) + + t.Run("should return error with invalid user id", func(t *testing.T) { + categories, _, err := th.Client.GetSidebarCategoriesForTeamForUser(context.Background(), th.BasicUser.Id, th.BasicTeam.Id, "") + require.NoError(t, err) + require.NotEmpty(t, categories.Categories) + + _, resp, err := th.Client.GetSidebarCategoryForTeamForUser(context.Background(), "invalid_user_id", th.BasicTeam.Id, categories.Categories[0].Id, "") + require.Error(t, err) + require.Equal(t, http.StatusNotFound, resp.StatusCode) + }) + + t.Run("should return error with invalid team id", func(t *testing.T) { + categories, _, err := th.Client.GetSidebarCategoriesForTeamForUser(context.Background(), th.BasicUser.Id, th.BasicTeam.Id, "") + require.NoError(t, err) + require.NotEmpty(t, categories.Categories) + + _, resp, err := th.Client.GetSidebarCategoryForTeamForUser(context.Background(), th.BasicUser.Id, "invalid_team_id", categories.Categories[0].Id, "") + require.Error(t, err) + require.Equal(t, http.StatusNotFound, resp.StatusCode) + }) + + t.Run("should return error with invalid category id", func(t *testing.T) { + _, resp, err := th.Client.GetSidebarCategoryForTeamForUser(context.Background(), th.BasicUser.Id, th.BasicTeam.Id, "invalid_category_id", "") + require.Error(t, err) + require.Equal(t, http.StatusBadRequest, resp.StatusCode) + }) + + t.Run("should return error when user is not logged in", func(t *testing.T) { + categories, _, err := th.Client.GetSidebarCategoriesForTeamForUser(context.Background(), th.BasicUser.Id, th.BasicTeam.Id, "") + require.NoError(t, err) + require.NotEmpty(t, categories.Categories) + + client := th.CreateClient() + _, resp, err := client.GetSidebarCategoryForTeamForUser(context.Background(), th.BasicUser.Id, th.BasicTeam.Id, categories.Categories[0].Id, "") + require.Error(t, err) + require.Equal(t, http.StatusUnauthorized, resp.StatusCode) + }) + + t.Run("should not crash with null input", func(t *testing.T) { + require.NotPanics(t, func() { + user, client := setupUserForSubtest(t, th) + categories, _, err := client.GetSidebarCategoriesForTeamForUser(context.Background(), user.Id, th.BasicTeam.Id, "") + require.NoError(t, err) + require.NotEmpty(t, categories.Categories) + + route := fmt.Sprintf("/users/%s/teams/%s/channels/categories/%s", user.Id, th.BasicTeam.Id, categories.Categories[0].Id) + r, err := client.DoAPIGet(context.Background(), route, "") + require.NoError(t, err) + closeBody(r) + }) + }) +} + +func TestValidateSidebarCategory(t *testing.T) { + th := Setup(t).InitBasic() + defer th.TearDown() + + // Create a test context with logger once for all subtests + c := &Context{ + App: th.App, + AppContext: th.Context, + Logger: th.App.Log(), + } + + t.Run("should validate category with valid channels", func(t *testing.T) { + user, _ := setupUserForSubtest(t, th) + + // Create a category with channels the user is a member of + category := &model.SidebarCategoryWithChannels{ + SidebarCategory: model.SidebarCategory{ + UserId: user.Id, + TeamId: th.BasicTeam.Id, + }, + Channels: []string{th.BasicChannel.Id, th.BasicChannel2.Id}, + } + + err := validateSidebarCategory(c, th.BasicTeam.Id, user.Id, category) + require.Nil(t, err) + require.Len(t, category.Channels, 2) + require.Contains(t, category.Channels, th.BasicChannel.Id) + require.Contains(t, category.Channels, th.BasicChannel2.Id) + }) + + t.Run("should filter out invalid channel IDs", func(t *testing.T) { + user, _ := setupUserForSubtest(t, th) + + category := &model.SidebarCategoryWithChannels{ + SidebarCategory: model.SidebarCategory{ + UserId: user.Id, + TeamId: th.BasicTeam.Id, + }, + Channels: []string{th.BasicChannel.Id, "invalid_channel_id"}, + } + + err := validateSidebarCategory(c, th.BasicTeam.Id, user.Id, category) + require.Nil(t, err) + require.Len(t, category.Channels, 1) + require.Contains(t, category.Channels, th.BasicChannel.Id) + require.NotContains(t, category.Channels, "invalid_channel_id") + }) + + t.Run("should filter out channels user is not a member of", func(t *testing.T) { + user, _ := setupUserForSubtest(t, th) + + // Create a channel that the user is not a member of + channel, appErr := th.App.CreateChannel(th.Context, &model.Channel{ + TeamId: th.BasicTeam.Id, + Type: model.ChannelTypeOpen, + Name: "testchannel", + DisplayName: "Test Channel", + }, false) + require.Nil(t, appErr) + + category := &model.SidebarCategoryWithChannels{ + SidebarCategory: model.SidebarCategory{ + UserId: user.Id, + TeamId: th.BasicTeam.Id, + }, + Channels: []string{th.BasicChannel.Id, channel.Id}, + } + + err := validateSidebarCategory(c, th.BasicTeam.Id, user.Id, category) + require.Nil(t, err) + require.Len(t, category.Channels, 1) + require.Contains(t, category.Channels, th.BasicChannel.Id) + require.NotContains(t, category.Channels, channel.Id) + }) + + t.Run("should return error with invalid team id", func(t *testing.T) { + user, _ := setupUserForSubtest(t, th) + + category := &model.SidebarCategoryWithChannels{ + SidebarCategory: model.SidebarCategory{ + UserId: user.Id, + TeamId: "invalid_team_id", + }, + Channels: []string{th.BasicChannel.Id}, + } + + err := validateSidebarCategory(c, "invalid_team_id", user.Id, category) + require.NotNil(t, err) + require.Equal(t, http.StatusBadRequest, err.StatusCode) + }) + + t.Run("should return error with invalid user id", func(t *testing.T) { + category := &model.SidebarCategoryWithChannels{ + SidebarCategory: model.SidebarCategory{ + UserId: "invalid_user_id", + TeamId: th.BasicTeam.Id, + }, + Channels: []string{th.BasicChannel.Id}, + } + + err := validateSidebarCategory(c, th.BasicTeam.Id, "invalid_user_id", category) + require.NotNil(t, err) + require.Equal(t, http.StatusBadRequest, err.StatusCode) + }) + + t.Run("should handle empty channel list", func(t *testing.T) { + user, _ := setupUserForSubtest(t, th) + + category := &model.SidebarCategoryWithChannels{ + SidebarCategory: model.SidebarCategory{ + UserId: user.Id, + TeamId: th.BasicTeam.Id, + }, + Channels: []string{}, + } + + err := validateSidebarCategory(c, th.BasicTeam.Id, user.Id, category) + require.Nil(t, err) + require.Empty(t, category.Channels) + }) +} + +func TestValidateSidebarCategoryChannels(t *testing.T) { + th := Setup(t).InitBasic() + defer th.TearDown() + + // Create a test context with logger once for all subtests + c := &Context{ + App: th.App, + AppContext: th.Context, + Logger: th.App.Log(), + } + + t.Run("should filter valid channels", func(t *testing.T) { + // Create test channels + channels := model.ChannelList{ + th.BasicChannel, + th.BasicChannel2, + } + + // Test with valid channel IDs + channelIds := []string{ + th.BasicChannel.Id, + th.BasicChannel2.Id, + } + + filtered := validateSidebarCategoryChannels(c, th.BasicUser.Id, channelIds, channels) + require.Len(t, filtered, 2) + require.ElementsMatch(t, channelIds, filtered) + }) + + t.Run("should filter out invalid channels", func(t *testing.T) { + channels := model.ChannelList{ + th.BasicChannel, + } + + channelIds := []string{ + th.BasicChannel.Id, + "invalid_channel_id", + } + + filtered := validateSidebarCategoryChannels(c, th.BasicUser.Id, channelIds, channels) + require.Len(t, filtered, 1) + require.Contains(t, filtered, th.BasicChannel.Id) + require.NotContains(t, filtered, "invalid_channel_id") + }) + + t.Run("should handle empty channel list", func(t *testing.T) { + channels := model.ChannelList{} + channelIds := []string{th.BasicChannel.Id} + + filtered := validateSidebarCategoryChannels(c, th.BasicUser.Id, channelIds, channels) + require.Empty(t, filtered) + }) + + t.Run("should handle empty channelIds", func(t *testing.T) { + channels := model.ChannelList{ + th.BasicChannel, + th.BasicChannel2, + } + + filtered := validateSidebarCategoryChannels(c, th.BasicUser.Id, []string{}, channels) + require.Empty(t, filtered) + }) + + t.Run("should handle nil inputs", func(t *testing.T) { + filtered := validateSidebarCategoryChannels(c, th.BasicUser.Id, nil, nil) + require.Empty(t, filtered) + }) + + t.Run("should preserve duplicate channel IDs", func(t *testing.T) { + channels := model.ChannelList{ + th.BasicChannel, + } + + // Include duplicate channel IDs + channelIds := []string{ + th.BasicChannel.Id, + th.BasicChannel.Id, + } + + filtered := validateSidebarCategoryChannels(c, th.BasicUser.Id, channelIds, channels) + require.Len(t, filtered, 2) // Function preserves duplicates as per implementation + require.Equal(t, []string{th.BasicChannel.Id, th.BasicChannel.Id}, filtered) + }) +} + +func TestDeleteCategoryForTeamForUser(t *testing.T) { + th := Setup(t).InitBasic() + defer th.TearDown() + t.Run("should move channels to default categories when custom category is deleted", func(t *testing.T) { + user, client := setupUserForSubtest(t, th) + + // Create a custom category with different types of channels + customCategory, _, err := client.CreateSidebarCategoryForTeamForUser(context.Background(), user.Id, th.BasicTeam.Id, &model.SidebarCategoryWithChannels{ + SidebarCategory: model.SidebarCategory{ + UserId: user.Id, + TeamId: th.BasicTeam.Id, + DisplayName: "Custom Category", + Type: model.SidebarCategoryCustom, + }, + // Add public, private channels and DM + Channels: []string{th.BasicChannel.Id, th.BasicPrivateChannel.Id}, + }) + require.NoError(t, err) + require.NotNil(t, customCategory) + + // Create a DM channel + dmChannel, _, err := client.CreateDirectChannel(context.Background(), user.Id, th.BasicUser2.Id) + require.NoError(t, err) + + // Add DM to custom category + customCategory.Channels = append(customCategory.Channels, dmChannel.Id) + updatedCategory, _, err := client.UpdateSidebarCategoryForTeamForUser(context.Background(), user.Id, th.BasicTeam.Id, customCategory.Id, customCategory) + require.NoError(t, err) + require.ElementsMatch(t, []string{th.BasicChannel.Id, th.BasicPrivateChannel.Id, dmChannel.Id}, updatedCategory.Channels) + + // Delete the custom category + resp, err := client.DeleteSidebarCategoryForTeamForUser(context.Background(), user.Id, th.BasicTeam.Id, customCategory.Id) + require.NoError(t, err) + require.Equal(t, http.StatusOK, resp.StatusCode) + + // Get all categories to verify channel redistribution + categories, _, err := client.GetSidebarCategoriesForTeamForUser(context.Background(), user.Id, th.BasicTeam.Id, "") + require.NoError(t, err) + + // Find default categories + var channelsCategory, dmsCategory *model.SidebarCategoryWithChannels + for _, cat := range categories.Categories { + switch cat.Type { + case model.SidebarCategoryChannels: + channelsCategory = cat + case model.SidebarCategoryDirectMessages: + dmsCategory = cat + } + } + + require.NotNil(t, channelsCategory, "Channels category should exist") + require.NotNil(t, dmsCategory, "DMs category should exist") + + // Verify public and private channels moved to channels category + require.Contains(t, channelsCategory.Channels, th.BasicChannel.Id, "Public channel should be in channels category") + require.Contains(t, channelsCategory.Channels, th.BasicPrivateChannel.Id, "Private channel should be in channels category") + + // Verify DM moved to DMs category + require.Contains(t, dmsCategory.Channels, dmChannel.Id, "DM should be in direct messages category") + }) + + t.Run("should delete category when user has permission", func(t *testing.T) { + user, client := setupUserForSubtest(t, th) + + // Create a new custom category to delete (don't delete default categories) + category, _, err := client.CreateSidebarCategoryForTeamForUser(context.Background(), user.Id, th.BasicTeam.Id, &model.SidebarCategoryWithChannels{ + SidebarCategory: model.SidebarCategory{ + UserId: user.Id, + TeamId: th.BasicTeam.Id, + DisplayName: "Custom Category", + Type: model.SidebarCategoryCustom, + }, + }) + require.NoError(t, err) + require.NotNil(t, category) + + // Delete the category + resp, err := client.DeleteSidebarCategoryForTeamForUser(context.Background(), user.Id, th.BasicTeam.Id, category.Id) + require.NoError(t, err) + require.Equal(t, http.StatusOK, resp.StatusCode) + + // Verify category was deleted + categories, _, err := client.GetSidebarCategoriesForTeamForUser(context.Background(), user.Id, th.BasicTeam.Id, "") + require.NoError(t, err) + for _, cat := range categories.Categories { + require.NotEqual(t, category.Id, cat.Id) + } + }) + + t.Run("should return error when user doesn't have permission", func(t *testing.T) { + // Create a new user that's not on the team + user, appErr := th.App.CreateUser(th.Context, &model.User{ + Email: th.GenerateTestEmail(), + Username: "user_" + model.NewId(), + Password: "password", + }) + require.Nil(t, appErr) + + client := th.CreateClient() + _, _, err := client.Login(context.Background(), user.Email, "password") + require.NoError(t, err) + + // Get categories for basic user to get a valid category ID + categories, _, err := th.Client.GetSidebarCategoriesForTeamForUser(context.Background(), th.BasicUser.Id, th.BasicTeam.Id, "") + require.NoError(t, err) + require.NotEmpty(t, categories.Categories) + + // Attempt to delete category for the basic user + resp, err := client.DeleteSidebarCategoryForTeamForUser(context.Background(), th.BasicUser.Id, th.BasicTeam.Id, categories.Categories[0].Id) + require.Error(t, err) + require.Equal(t, http.StatusForbidden, resp.StatusCode) + }) + + t.Run("should return error with invalid user id", func(t *testing.T) { + // Get a valid category ID first + categories, _, err := th.Client.GetSidebarCategoriesForTeamForUser(context.Background(), th.BasicUser.Id, th.BasicTeam.Id, "") + require.NoError(t, err) + require.NotEmpty(t, categories.Categories) + + resp, err := th.Client.DeleteSidebarCategoryForTeamForUser(context.Background(), "invalid_user_id", th.BasicTeam.Id, categories.Categories[0].Id) + require.Error(t, err) + require.Equal(t, http.StatusNotFound, resp.StatusCode) + }) + + t.Run("should return error with invalid team id", func(t *testing.T) { + // Get a valid category ID first + categories, _, err := th.Client.GetSidebarCategoriesForTeamForUser(context.Background(), th.BasicUser.Id, th.BasicTeam.Id, "") + require.NoError(t, err) + require.NotEmpty(t, categories.Categories) + + resp, err := th.Client.DeleteSidebarCategoryForTeamForUser(context.Background(), th.BasicUser.Id, "invalid_team_id", categories.Categories[0].Id) + require.Error(t, err) + require.Equal(t, http.StatusNotFound, resp.StatusCode) + }) + + t.Run("should return error with invalid category id", func(t *testing.T) { + resp, err := th.Client.DeleteSidebarCategoryForTeamForUser(context.Background(), th.BasicUser.Id, th.BasicTeam.Id, "invalid_category_id") + require.Error(t, err) + require.Equal(t, http.StatusBadRequest, resp.StatusCode) + }) + + t.Run("should return error when user is not logged in", func(t *testing.T) { + // Get a valid category ID first + categories, _, err := th.Client.GetSidebarCategoriesForTeamForUser(context.Background(), th.BasicUser.Id, th.BasicTeam.Id, "") + require.NoError(t, err) + require.NotEmpty(t, categories.Categories) + + client := th.CreateClient() + resp, err := client.DeleteSidebarCategoryForTeamForUser(context.Background(), th.BasicUser.Id, th.BasicTeam.Id, categories.Categories[0].Id) + require.Error(t, err) + require.Equal(t, http.StatusUnauthorized, resp.StatusCode) + }) + + t.Run("should not allow deletion of default categories", func(t *testing.T) { + user, client := setupUserForSubtest(t, th) + + categories, _, err := client.GetSidebarCategoriesForTeamForUser(context.Background(), user.Id, th.BasicTeam.Id, "") + require.NoError(t, err) + require.NotEmpty(t, categories.Categories) + + // Try to delete each default category + for _, category := range categories.Categories { + if category.Type != model.SidebarCategoryCustom { + resp, err := client.DeleteSidebarCategoryForTeamForUser(context.Background(), user.Id, th.BasicTeam.Id, category.Id) + require.Error(t, err) + require.Equal(t, http.StatusBadRequest, resp.StatusCode) + } + } + }) +} + func setupUserForSubtest(t *testing.T, th *TestHelper) (*model.User, *model.Client4) { password := "password" user, appErr := th.App.CreateUser(th.Context, &model.User{ diff --git a/server/public/model/client4.go b/server/public/model/client4.go index eb884d18ff4a..27ffe46da825 100644 --- a/server/public/model/client4.go +++ b/server/public/model/client4.go @@ -8306,6 +8306,17 @@ func (c *Client4) UpdateSidebarCategoryForTeamForUser(ctx context.Context, userI return cat, BuildResponse(r), nil } +// DeleteSidebarCategoryForTeamForUser deletes a sidebar category for a user in a team. +func (c *Client4) DeleteSidebarCategoryForTeamForUser(ctx context.Context, userId string, teamId string, categoryId string) (*Response, error) { + url := fmt.Sprintf("%s/%s", c.userCategoryRoute(userId, teamId), categoryId) + r, err := c.DoAPIDelete(ctx, url) + if err != nil { + return BuildResponse(r), err + } + defer closeBody(r) + return BuildResponse(r), nil +} + // CheckIntegrity performs a database integrity check. func (c *Client4) CheckIntegrity(ctx context.Context) ([]IntegrityCheckResult, *Response, error) { r, err := c.DoAPIPost(ctx, "/integrity", "")