package app import ( "testing" "github.com/mattermost/focalboard/server/utils" "github.com/stretchr/testify/assert" "github.com/mattermost/focalboard/server/model" "github.com/stretchr/testify/mock" "github.com/stretchr/testify/require" ) func TestAddMemberToBoard(t *testing.T) { th, tearDown := SetupTestHelper(t) defer tearDown() t.Run("base case", func(t *testing.T) { const boardID = "board_id_1" const userID = "user_id_1" boardMember := &model.BoardMember{ BoardID: boardID, UserID: userID, SchemeEditor: true, } th.Store.EXPECT().GetBoard(boardID).Return(&model.Board{ ID: "board_id_1", TeamID: "team_id_1", }, nil) th.Store.EXPECT().GetMemberForBoard(boardID, userID).Return(nil, nil) th.Store.EXPECT().SaveMember(mock.MatchedBy(func(i interface{}) bool { p := i.(*model.BoardMember) return p.BoardID == boardID && p.UserID == userID })).Return(&model.BoardMember{ BoardID: boardID, }, nil) // for WS change broadcast th.Store.EXPECT().GetMembersForBoard(boardID).Return([]*model.BoardMember{}, nil) th.Store.EXPECT().GetUserCategoryBoards("user_id_1", "team_id_1").Return([]model.CategoryBoards{ { Category: model.Category{ ID: "default_category_id", Name: "Boards", Type: "system", }, }, }, nil).Times(2) th.Store.EXPECT().AddUpdateCategoryBoard("user_id_1", "default_category_id", []string{"board_id_1"}).Return(nil) addedBoardMember, err := th.App.AddMemberToBoard(boardMember) require.NoError(t, err) require.Equal(t, boardID, addedBoardMember.BoardID) }) t.Run("return existing non-synthetic membership if any", func(t *testing.T) { const boardID = "board_id_1" const userID = "user_id_1" boardMember := &model.BoardMember{ BoardID: boardID, UserID: userID, SchemeEditor: true, } th.Store.EXPECT().GetBoard(boardID).Return(&model.Board{ TeamID: "team_id_1", }, nil) th.Store.EXPECT().GetMemberForBoard(boardID, userID).Return(&model.BoardMember{ UserID: userID, BoardID: boardID, Synthetic: false, }, nil) addedBoardMember, err := th.App.AddMemberToBoard(boardMember) require.NoError(t, err) require.Equal(t, boardID, addedBoardMember.BoardID) }) t.Run("should convert synthetic membership into natural membership", func(t *testing.T) { const boardID = "board_id_1" const userID = "user_id_1" boardMember := &model.BoardMember{ BoardID: boardID, UserID: userID, SchemeEditor: true, } th.Store.EXPECT().GetBoard(boardID).Return(&model.Board{ ID: "board_id_1", TeamID: "team_id_1", }, nil) th.Store.EXPECT().GetMemberForBoard(boardID, userID).Return(&model.BoardMember{ UserID: userID, BoardID: boardID, Synthetic: true, }, nil) th.Store.EXPECT().SaveMember(mock.MatchedBy(func(i interface{}) bool { p := i.(*model.BoardMember) return p.BoardID == boardID && p.UserID == userID })).Return(&model.BoardMember{ UserID: userID, BoardID: boardID, Synthetic: false, }, nil) // for WS change broadcast th.Store.EXPECT().GetMembersForBoard(boardID).Return([]*model.BoardMember{}, nil) th.Store.EXPECT().GetUserCategoryBoards("user_id_1", "team_id_1").Return([]model.CategoryBoards{ { Category: model.Category{ ID: "default_category_id", Name: "Boards", Type: "system", }, }, }, nil).Times(2) th.Store.EXPECT().AddUpdateCategoryBoard("user_id_1", "default_category_id", []string{"board_id_1"}).Return(nil) th.API.EXPECT().HasPermissionToTeam("user_id_1", "team_id_1", model.PermissionManageTeam).Return(false).Times(1) addedBoardMember, err := th.App.AddMemberToBoard(boardMember) require.NoError(t, err) require.Equal(t, boardID, addedBoardMember.BoardID) }) } func TestPatchBoard(t *testing.T) { th, tearDown := SetupTestHelper(t) defer tearDown() t.Run("base case, title patch", func(t *testing.T) { const boardID = "board_id_1" const userID = "user_id_1" const teamID = "team_id_1" patchTitle := "Patched Title" patch := &model.BoardPatch{ Title: &patchTitle, } th.Store.EXPECT().PatchBoard(boardID, patch, userID).Return( &model.Board{ ID: boardID, TeamID: teamID, Title: patchTitle, }, nil) // for WS BroadcastBoardChange th.Store.EXPECT().GetMembersForBoard(boardID).Return([]*model.BoardMember{}, nil).Times(1) patchedBoard, err := th.App.PatchBoard(patch, boardID, userID) require.NoError(t, err) require.Equal(t, patchTitle, patchedBoard.Title) }) t.Run("patch type open, no users", func(t *testing.T) { const boardID = "board_id_1" const userID = "user_id_2" const teamID = "team_id_1" patchType := model.BoardTypeOpen patch := &model.BoardPatch{ Type: &patchType, } // Type not nil, will cause board to be reteived // to check isTemplate th.Store.EXPECT().GetBoard(boardID).Return(&model.Board{ ID: boardID, TeamID: teamID, IsTemplate: true, }, nil).Times(2) // Type not null will retrieve team members th.Store.EXPECT().GetUsersByTeam(teamID, "", false, false).Return([]*model.User{}, nil) th.Store.EXPECT().PatchBoard(boardID, patch, userID).Return( &model.Board{ ID: boardID, TeamID: teamID, }, nil) // Should call GetMembersForBoard 2 times // - for WS BroadcastBoardChange // - for AddTeamMembers check th.Store.EXPECT().GetMembersForBoard(boardID).Return([]*model.BoardMember{}, nil).Times(2) patchedBoard, err := th.App.PatchBoard(patch, boardID, userID) require.NoError(t, err) require.Equal(t, boardID, patchedBoard.ID) }) t.Run("patch type private, no users", func(t *testing.T) { const boardID = "board_id_1" const userID = "user_id_2" const teamID = "team_id_1" patchType := model.BoardTypePrivate patch := &model.BoardPatch{ Type: &patchType, } // Type not nil, will cause board to be reteived // to check isTemplate th.Store.EXPECT().GetBoard(boardID).Return(&model.Board{ ID: boardID, TeamID: teamID, IsTemplate: true, }, nil).Times(2) // Type not null will retrieve team members th.Store.EXPECT().GetUsersByTeam(teamID, "", false, false).Return([]*model.User{}, nil) th.Store.EXPECT().PatchBoard(boardID, patch, userID).Return( &model.Board{ ID: boardID, TeamID: teamID, }, nil) // Should call GetMembersForBoard 2 times // - for WS BroadcastBoardChange // - for AddTeamMembers check th.Store.EXPECT().GetMembersForBoard(boardID).Return([]*model.BoardMember{}, nil).Times(2) patchedBoard, err := th.App.PatchBoard(patch, boardID, userID) require.NoError(t, err) require.Equal(t, boardID, patchedBoard.ID) }) t.Run("patch type open, single user", func(t *testing.T) { const boardID = "board_id_1" const userID = "user_id_2" const teamID = "team_id_1" patchType := model.BoardTypeOpen patch := &model.BoardPatch{ Type: &patchType, } // Type not nil, will cause board to be reteived // to check isTemplate th.Store.EXPECT().GetBoard(boardID).Return(&model.Board{ ID: boardID, TeamID: teamID, IsTemplate: true, }, nil).Times(2) // Type not null will retrieve team members th.Store.EXPECT().GetUsersByTeam(teamID, "", false, false).Return([]*model.User{{ID: userID}}, nil) th.Store.EXPECT().PatchBoard(boardID, patch, userID).Return( &model.Board{ ID: boardID, TeamID: teamID, }, nil) // Should call GetMembersForBoard 3 times // for WS BroadcastBoardChange // for AddTeamMembers check // for WS BroadcastMemberChange th.Store.EXPECT().GetMembersForBoard(boardID).Return([]*model.BoardMember{}, nil).Times(3) patchedBoard, err := th.App.PatchBoard(patch, boardID, userID) require.NoError(t, err) require.Equal(t, boardID, patchedBoard.ID) }) t.Run("patch type private, single user", func(t *testing.T) { const boardID = "board_id_1" const userID = "user_id_2" const teamID = "team_id_1" patchType := model.BoardTypePrivate patch := &model.BoardPatch{ Type: &patchType, } // Type not nil, will cause board to be reteived // to check isTemplate th.Store.EXPECT().GetBoard(boardID).Return(&model.Board{ ID: boardID, TeamID: teamID, IsTemplate: true, }, nil).Times(2) // Type not null will retrieve team members th.Store.EXPECT().GetUsersByTeam(teamID, "", false, false).Return([]*model.User{{ID: userID}}, nil) th.Store.EXPECT().PatchBoard(boardID, patch, userID).Return( &model.Board{ ID: boardID, TeamID: teamID, }, nil) // Should call GetMembersForBoard 3 times // for WS BroadcastBoardChange // for AddTeamMembers check // for WS BroadcastMemberChange th.Store.EXPECT().GetMembersForBoard(boardID).Return([]*model.BoardMember{}, nil).Times(3) patchedBoard, err := th.App.PatchBoard(patch, boardID, userID) require.NoError(t, err) require.Equal(t, boardID, patchedBoard.ID) }) t.Run("patch type open, user with member", func(t *testing.T) { const boardID = "board_id_1" const userID = "user_id_2" const teamID = "team_id_1" patchType := model.BoardTypeOpen patch := &model.BoardPatch{ Type: &patchType, } // Type not nil, will cause board to be reteived // to check isTemplate th.Store.EXPECT().GetBoard(boardID).Return(&model.Board{ ID: boardID, TeamID: teamID, IsTemplate: true, }, nil).Times(3) th.API.EXPECT().HasPermissionToTeam(userID, teamID, model.PermissionManageTeam).Return(false).Times(1) // Type not null will retrieve team members th.Store.EXPECT().GetUsersByTeam(teamID, "", false, false).Return([]*model.User{{ID: userID}}, nil) th.Store.EXPECT().PatchBoard(boardID, patch, userID).Return( &model.Board{ ID: boardID, TeamID: teamID, }, nil) // Should call GetMembersForBoard 2 times // for WS BroadcastBoardChange // for AddTeamMembers check // We are returning the user as a direct Board Member, so BroadcastMemberDelete won't be called th.Store.EXPECT().GetMembersForBoard(boardID).Return([]*model.BoardMember{{BoardID: boardID, UserID: userID, SchemeEditor: true}}, nil).Times(2) patchedBoard, err := th.App.PatchBoard(patch, boardID, userID) require.NoError(t, err) require.Equal(t, boardID, patchedBoard.ID) }) t.Run("patch type private, user with member", func(t *testing.T) { const boardID = "board_id_1" const userID = "user_id_2" const teamID = "team_id_1" patchType := model.BoardTypePrivate patch := &model.BoardPatch{ Type: &patchType, } // Type not nil, will cause board to be reteived // to check isTemplate th.Store.EXPECT().GetBoard(boardID).Return(&model.Board{ ID: boardID, TeamID: teamID, IsTemplate: true, ChannelID: "", }, nil).Times(1) th.API.EXPECT().HasPermissionToTeam(userID, teamID, model.PermissionManageTeam).Return(false).Times(1) // Type not null will retrieve team members th.Store.EXPECT().GetUsersByTeam(teamID, "", false, false).Return([]*model.User{{ID: userID}}, nil) th.Store.EXPECT().PatchBoard(boardID, patch, userID).Return( &model.Board{ ID: boardID, TeamID: teamID, }, nil) // Should call GetMembersForBoard 2 times // for WS BroadcastBoardChange // for AddTeamMembers check // We are returning the user as a direct Board Member, so BroadcastMemberDelete won't be called th.Store.EXPECT().GetMembersForBoard(boardID).Return([]*model.BoardMember{{BoardID: boardID, UserID: userID, SchemeEditor: true}}, nil).Times(2) patchedBoard, err := th.App.PatchBoard(patch, boardID, userID) require.NoError(t, err) require.Equal(t, boardID, patchedBoard.ID) }) t.Run("patch type channel, user without post permissions", func(t *testing.T) { const boardID = "board_id_1" const userID = "user_id_2" const teamID = "team_id_1" channelID := "myChannel" patchType := model.BoardTypeOpen patch := &model.BoardPatch{ Type: &patchType, ChannelID: &channelID, } // Type not nil, will cause board to be reteived // to check isTemplate th.Store.EXPECT().GetBoard(boardID).Return(&model.Board{ ID: boardID, TeamID: teamID, IsTemplate: true, }, nil).Times(1) th.API.EXPECT().HasPermissionToChannel(userID, channelID, model.PermissionCreatePost).Return(false).Times(1) _, err := th.App.PatchBoard(patch, boardID, userID) require.Error(t, err) }) t.Run("patch type remove channel, user without post permissions", func(t *testing.T) { const boardID = "board_id_1" const userID = "user_id_2" const teamID = "team_id_1" channelID := "myChannel" clearChannel := "" patchType := model.BoardTypeOpen patch := &model.BoardPatch{ Type: &patchType, ChannelID: &clearChannel, } // Type not nil, will cause board to be reteived // to check isTemplate th.Store.EXPECT().GetBoard(boardID).Return(&model.Board{ ID: boardID, TeamID: teamID, IsTemplate: true, ChannelID: channelID, }, nil).Times(2) th.API.EXPECT().HasPermissionToChannel(userID, channelID, model.PermissionCreatePost).Return(false).Times(1) th.API.EXPECT().HasPermissionToTeam(userID, teamID, model.PermissionManageTeam).Return(false).Times(1) // Should call GetMembersForBoard 2 times // for WS BroadcastBoardChange // for AddTeamMembers check // We are returning the user as a direct Board Member, so BroadcastMemberDelete won't be called th.Store.EXPECT().GetMembersForBoard(boardID).Return([]*model.BoardMember{{BoardID: boardID, UserID: userID, SchemeEditor: true}}, nil).Times(1) _, err := th.App.PatchBoard(patch, boardID, userID) require.Error(t, err) }) } func TestGetBoardCount(t *testing.T) { th, tearDown := SetupTestHelper(t) defer tearDown() t.Run("base case", func(t *testing.T) { boardCount := int64(100) th.Store.EXPECT().GetBoardCount().Return(boardCount, nil) count, err := th.App.GetBoardCount() require.NoError(t, err) require.Equal(t, boardCount, count) }) } func TestBoardCategory(t *testing.T) { th, tearDown := SetupTestHelper(t) defer tearDown() t.Run("no boards default category exists", func(t *testing.T) { th.Store.EXPECT().GetUserCategoryBoards("user_id", "team_id").Return([]model.CategoryBoards{ { Category: model.Category{ID: "category_id_1", Name: "Category 1"}, BoardMetadata: []model.CategoryBoardMetadata{ {BoardID: "board_id_1"}, {BoardID: "board_id_2"}, }, }, { Category: model.Category{ID: "category_id_2", Name: "Category 2"}, BoardMetadata: []model.CategoryBoardMetadata{ {BoardID: "board_id_3"}, }, }, { Category: model.Category{ID: "category_id_3", Name: "Category 3"}, BoardMetadata: []model.CategoryBoardMetadata{}, }, }, nil).Times(1) // when this function is called the second time, the default category is created th.Store.EXPECT().GetUserCategoryBoards("user_id", "team_id").Return([]model.CategoryBoards{ { Category: model.Category{ID: "category_id_1", Name: "Category 1"}, BoardMetadata: []model.CategoryBoardMetadata{ {BoardID: "board_id_1"}, {BoardID: "board_id_2"}, }, }, { Category: model.Category{ID: "category_id_2", Name: "Category 2"}, BoardMetadata: []model.CategoryBoardMetadata{ {BoardID: "board_id_3"}, }, }, { Category: model.Category{ID: "category_id_3", Name: "Category 3"}, BoardMetadata: []model.CategoryBoardMetadata{}, }, { Category: model.Category{ID: "default_category_id", Type: model.CategoryTypeSystem, Name: "Boards"}, }, }, nil).Times(1) th.Store.EXPECT().CreateCategory(utils.Anything).Return(nil) th.Store.EXPECT().GetCategory(utils.Anything).Return(&model.Category{ ID: "default_category_id", Name: "Boards", }, nil) th.Store.EXPECT().GetMembersForUser("user_id").Return([]*model.BoardMember{}, nil) th.Store.EXPECT().GetBoardsForUserAndTeam("user_id", "team_id", false).Return([]*model.Board{}, nil) th.Store.EXPECT().AddUpdateCategoryBoard("user_id", "default_category_id", []string{ "board_id_1", "board_id_2", "board_id_3", }).Return(nil) boards := []*model.Board{ {ID: "board_id_1"}, {ID: "board_id_2"}, {ID: "board_id_3"}, } err := th.App.addBoardsToDefaultCategory("user_id", "team_id", boards) assert.NoError(t, err) }) } func TestDuplicateBoard(t *testing.T) { th, tearDown := SetupTestHelper(t) defer tearDown() t.Run("base case", func(t *testing.T) { board := &model.Board{ ID: "board_id_2", Title: "Duplicated Board", } block := &model.Block{ ID: "block_id_1", Type: "image", } th.Store.EXPECT().DuplicateBoard("board_id_1", "user_id_1", "team_id_1", false).Return( &model.BoardsAndBlocks{ Boards: []*model.Board{ board, }, Blocks: []*model.Block{ block, }, }, []*model.BoardMember{}, nil, ) th.Store.EXPECT().GetBoard("board_id_1").Return(&model.Board{}, nil) th.Store.EXPECT().GetUserCategoryBoards("user_id_1", "team_id_1").Return([]model.CategoryBoards{ { Category: model.Category{ ID: "category_id_1", Name: "Boards", Type: "system", }, }, }, nil).Times(3) th.Store.EXPECT().AddUpdateCategoryBoard("user_id_1", "category_id_1", utils.Anything).Return(nil) // for WS change broadcast th.Store.EXPECT().GetMembersForBoard(utils.Anything).Return([]*model.BoardMember{}, nil).Times(2) bab, members, err := th.App.DuplicateBoard("board_id_1", "user_id_1", "team_id_1", false) assert.NoError(t, err) assert.NotNil(t, bab) assert.NotNil(t, members) }) t.Run("duplicating board as template should not set it's category", func(t *testing.T) { board := &model.Board{ ID: "board_id_2", Title: "Duplicated Board", } block := &model.Block{ ID: "block_id_1", Type: "image", } th.Store.EXPECT().DuplicateBoard("board_id_1", "user_id_1", "team_id_1", true).Return( &model.BoardsAndBlocks{ Boards: []*model.Board{ board, }, Blocks: []*model.Block{ block, }, }, []*model.BoardMember{}, nil, ) th.Store.EXPECT().GetBoard("board_id_1").Return(&model.Board{}, nil) // for WS change broadcast th.Store.EXPECT().GetMembersForBoard(utils.Anything).Return([]*model.BoardMember{}, nil).Times(2) bab, members, err := th.App.DuplicateBoard("board_id_1", "user_id_1", "team_id_1", true) assert.NoError(t, err) assert.NotNil(t, bab) assert.NotNil(t, members) }) } func TestGetMembersForBoard(t *testing.T) { th, tearDown := SetupTestHelper(t) defer tearDown() const boardID = "board_id_1" const userID = "user_id_1" const teamID = "team_id_1" th.Store.EXPECT().GetMembersForBoard(boardID).Return([]*model.BoardMember{ { BoardID: boardID, UserID: userID, SchemeEditor: true, }, }, nil).Times(3) th.Store.EXPECT().GetBoard(boardID).Return(nil, nil).Times(1) t.Run("-base case", func(t *testing.T) { members, err := th.App.GetMembersForBoard(boardID) assert.NoError(t, err) assert.NotNil(t, members) assert.False(t, members[0].SchemeAdmin) }) board := &model.Board{ ID: boardID, TeamID: teamID, } th.Store.EXPECT().GetBoard(boardID).Return(board, nil).Times(2) th.API.EXPECT().HasPermissionToTeam(userID, teamID, model.PermissionManageTeam).Return(false).Times(1) t.Run("-team check false ", func(t *testing.T) { members, err := th.App.GetMembersForBoard(boardID) assert.NoError(t, err) assert.NotNil(t, members) assert.False(t, members[0].SchemeAdmin) }) th.API.EXPECT().HasPermissionToTeam(userID, teamID, model.PermissionManageTeam).Return(true).Times(1) t.Run("-team check true", func(t *testing.T) { members, err := th.App.GetMembersForBoard(boardID) assert.NoError(t, err) assert.NotNil(t, members) assert.True(t, members[0].SchemeAdmin) }) } func TestGetMembersForUser(t *testing.T) { th, tearDown := SetupTestHelper(t) defer tearDown() const boardID = "board_id_1" const userID = "user_id_1" const teamID = "team_id_1" th.Store.EXPECT().GetMembersForUser(userID).Return([]*model.BoardMember{ { BoardID: boardID, UserID: userID, SchemeEditor: true, }, }, nil).Times(3) th.Store.EXPECT().GetBoard(boardID).Return(nil, nil) t.Run("-base case", func(t *testing.T) { members, err := th.App.GetMembersForUser(userID) assert.NoError(t, err) assert.NotNil(t, members) assert.False(t, members[0].SchemeAdmin) }) board := &model.Board{ ID: boardID, TeamID: teamID, } th.Store.EXPECT().GetBoard(boardID).Return(board, nil).Times(2) th.API.EXPECT().HasPermissionToTeam(userID, teamID, model.PermissionManageTeam).Return(false).Times(1) t.Run("-team check false ", func(t *testing.T) { members, err := th.App.GetMembersForUser(userID) assert.NoError(t, err) assert.NotNil(t, members) assert.False(t, members[0].SchemeAdmin) }) th.API.EXPECT().HasPermissionToTeam(userID, teamID, model.PermissionManageTeam).Return(true).Times(1) t.Run("-team check true", func(t *testing.T) { members, err := th.App.GetMembersForUser(userID) assert.NoError(t, err) assert.NotNil(t, members) assert.True(t, members[0].SchemeAdmin) }) }