* create ChannelBookmarks table

* ChannelBookmark model

* channel bookamrks Store layer

* add GetBookmarksForAllChannelByIdSince

* add channel bookmarks to test store

* Add channel bookmarks to app layer

* remove index for createAt in channel bookmarks migrations

* remove createAt from select channel bookmark query and enable store delete bookmark test

* update reponse of UpdateBookmark

* rename db migration files

* channel bookmarks store update sort order

* channel bookmarks app layer update sort order

* fix lint & tests

* Fix lint and introduce util functions to insert / remove from slice

* remove model etag

* i18n

* defer remove file info after test run

* Fix tests passing the request context

* fix migrations

* fix TestRetry

* Add bookmark permissions (#25560)

* Adds channel bookmarks permissions

* Fix linter

* Remove unnecessary empty lines

* Remove scss change as it's not necessary anymore

* Fix mock store

* Fix mock store and add role entry

* Fix test

* Adds cypress test and update permissions migration to update admin roles

* Adds channel bookmarks roles to default admin roles

* Adds bookmark permissions to default role permissions constant in webapp

* Update mmctl test

* Update permission test after normalising the roles

* fix store tests

* fix app layer tests

* Add new bookmark endpoint (#25624)

* Adds channel bookmarks api scaffold and create endpoint

* Applies review comments to the API docs

* Adds websocket test to create channel bookmark

---------

Co-authored-by: Mattermost Build <build@mattermost.com>

* MM-54426 exclude Channel Bookmarks files from data retention (#25656)

* Augment channel APIs to include bookmarks (#25567)

* update files docs for server 9.4

* Adds update channel bookmark endpoint (#25653)

* Adds update channel bookmark sort order endpoint (#25686)

* Adds update channel bookmark endpoint

* Updates edit app method to return the right deleted bookmark and adds tests

* Adds the update channel bookmark sort order endpoint

* Fix repeated test after merge

* Assign right permissions to each test

* Update store and app layer to return specific errors and add tests

* Adds delete channel bookmark endpoint (#25693)

* Updates edit app method to return the right deleted bookmark and adds tests

* Fix repeated test after merge

* Updates edit app method to return the right deleted bookmark and adds tests

* Adds delete channel bookmark endpoint

* Adds list channel bookmarks endpoint (#25700)

* Add channel moderation to bookmarks (#25716)

* fix migrations index

* fix getChannelsForTeamForUser

* fix getChannelsForTeamForUser

* fix bad merge client4

* fix file api with bookmark permission

* add ChannelBookmarks feature flag

* add missing translations

* Set DB column for type as enum

* use custom type for bookmark query using sqlx

* use transaction when saving bookmark

* return NewErrNotFound instead of Sql.ErrNoRows

* use squirrel for IN query

* add a limit of 1K for records in GetBookmarksForAllChannelByIdSince

* UpdateSortOrder with one single query instead of multiple updates

* fix shadow declaration

* fix channel bookmarks permission string definition in admin console

* fix another shadow declaration

* Fix model conversion

* add SplitSliceInChunks

* remove include bookmarks in channels api

* Cap amount of bookmarks per channel

* add etag back to get channels

* feedback review

* update file info when replacing a bookmark file

* return 501 not implemented when the license is not available

* add detail message when getting channel member on bookmark api

* start audit before permission check on create bookmark api

* use require.Eventuallyf for testing WS events

* remove unnecessary log in app layer

* use require instead of assert to avoid panics

* enforce limit when querying bookmarks since

* prevent to create/update bookmark if file is already attached

* fix lint

* delete file when a bookmark is deleted

* Dot allow to set a fileId and a url at the same time to a bookmark

* fix query to delete a file that belongs to a bookmark

* do not patch the bookmark type

* Server side FeatureFlag check (#26145)

* use ff in server, set ff to false

* turn on FF for unit tests

* defer unset FF for unit tests

* turn ff on for testing

* only allow attaching files that were uploaded for bookmark

* Set feature flag off as default

* fix lint

* update email templates as PR failed

* revert templates

* force the assignment of ID when creating a bookmark

* Fix unit tests

---------

Co-authored-by: Miguel de la Cruz <miguel@mcrx.me>
Co-authored-by: Mattermost Build <build@mattermost.com>
Co-authored-by: Caleb Roseland <caleb@calebroseland.com>
Co-authored-by: Scott Bishel <scott.bishel@mattermost.com>
Этот коммит содержится в:
Elias Nahum
2024-03-12 22:36:05 +08:00
коммит произвёл GitHub
родитель 2480a6c646
Коммит 7e9cd04a8b
66 изменённых файлов: 6578 добавлений и 57 удалений

Просмотреть файл

@@ -0,0 +1,483 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package storetest
import (
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/mattermost/mattermost/server/public/model"
"github.com/mattermost/mattermost/server/public/shared/request"
"github.com/mattermost/mattermost/server/v8/channels/store"
)
func find_bookmark(slice []*model.ChannelBookmarkWithFileInfo, id string) *model.ChannelBookmarkWithFileInfo {
for _, element := range slice {
if element.Id == id {
return element
}
}
return nil
}
func TestChannelBookmarkStore(t *testing.T, rctx request.CTX, ss store.Store, s SqlStore) {
t.Run("SaveChannelBookmark", func(t *testing.T) { testSaveChannelBookmark(t, rctx, ss) })
t.Run("UpdateChannelBookmark", func(t *testing.T) { testUpdateChannelBookmark(t, rctx, ss) })
t.Run("UpdateSortOrderChannelBookmark", func(t *testing.T) { testUpdateSortOrderChannelBookmark(t, rctx, ss) })
t.Run("DeleteChannelBookmark", func(t *testing.T) { testDeleteChannelBookmark(t, rctx, ss) })
t.Run("GetChannelBookmark", func(t *testing.T) { testGetChannelBookmark(t, rctx, ss) })
}
func testSaveChannelBookmark(t *testing.T, rctx request.CTX, ss store.Store) {
channelId := model.NewId()
userId := model.NewId()
bookmark1 := &model.ChannelBookmark{
ChannelId: channelId,
OwnerId: userId,
DisplayName: "Link bookmark test",
LinkUrl: "https://mattermost.com",
Type: model.ChannelBookmarkLink,
Emoji: ":smile:",
}
file := &model.FileInfo{
Id: model.NewId(),
CreatorId: model.BookmarkFileOwner,
Path: "somepath",
ThumbnailPath: "thumbpath",
PreviewPath: "prevPath",
Name: "test file",
Extension: "png",
MimeType: "images/png",
Size: 873182,
Width: 3076,
Height: 2200,
HasPreviewImage: true,
}
bookmark2 := &model.ChannelBookmark{
ChannelId: channelId,
OwnerId: userId,
DisplayName: "file bookmark test",
FileId: file.Id,
Type: model.ChannelBookmarkFile,
Emoji: ":smile:",
}
bookmark3 := &model.ChannelBookmark{
ChannelId: channelId,
OwnerId: userId,
DisplayName: "file already attached",
FileId: file.Id,
Type: model.ChannelBookmarkFile,
Emoji: ":smile:",
}
file2 := &model.FileInfo{
Id: model.NewId(),
CreatorId: userId,
Path: "somepath",
ThumbnailPath: "thumbpath",
PreviewPath: "prevPath",
Name: "test file",
Extension: "png",
MimeType: "images/png",
Size: 873182,
Width: 3076,
Height: 2200,
HasPreviewImage: true,
}
bookmark4 := &model.ChannelBookmark{
ChannelId: channelId,
OwnerId: userId,
DisplayName: "file already attached to a post",
FileId: file2.Id,
Type: model.ChannelBookmarkFile,
Emoji: ":smile:",
}
_, err := ss.FileInfo().Save(rctx, file)
require.NoError(t, err)
defer ss.FileInfo().PermanentDelete(rctx, file.Id)
_, err = ss.FileInfo().Save(rctx, file2)
require.NoError(t, err)
defer ss.FileInfo().PermanentDelete(rctx, file2.Id)
err = ss.FileInfo().AttachToPost(rctx, file2.Id, model.NewId(), channelId, userId)
require.NoError(t, err)
t.Run("save bookmarks", func(t *testing.T) {
bookmarkResp, err := ss.ChannelBookmark().Save(bookmark1.Clone(), true)
assert.NoError(t, err)
assert.NotEmpty(t, bookmarkResp.Id)
assert.Equal(t, bookmark1.ChannelId, bookmarkResp.ChannelId)
assert.Nil(t, bookmarkResp.FileInfo)
bookmarkResp, err = ss.ChannelBookmark().Save(bookmark2.Clone(), true)
assert.NoError(t, err)
assert.NotEmpty(t, bookmarkResp.Id)
assert.Equal(t, bookmark2.ChannelId, bookmarkResp.ChannelId)
assert.NotNil(t, bookmarkResp.FileInfo)
bookmarks, err := ss.ChannelBookmark().GetBookmarksForChannelSince(channelId, 0)
assert.NoError(t, err)
assert.Len(t, bookmarks, 2)
_, err = ss.ChannelBookmark().Save(bookmark3.Clone(), true)
assert.Error(t, err) // Error as the file is attached to a bookmark
_, err = ss.ChannelBookmark().Save(bookmark4.Clone(), true)
assert.Error(t, err) // Error as the file is attached to a post
})
}
func testUpdateChannelBookmark(t *testing.T, rctx request.CTX, ss store.Store) {
channelId := model.NewId()
userId := model.NewId()
bookmark1 := &model.ChannelBookmark{
ChannelId: channelId,
OwnerId: userId,
DisplayName: "Link bookmark test",
LinkUrl: "https://mattermost.com",
Type: model.ChannelBookmarkLink,
}
t.Run("update bookmark", func(t *testing.T) {
bookmarkResp, err := ss.ChannelBookmark().Save(bookmark1.Clone(), true)
assert.NoError(t, err)
now := model.GetMillis()
bookmark2 := bookmarkResp.ChannelBookmark.Clone()
bookmark2.DisplayName = "Updated display name"
bookmark2.Emoji = ":smile:"
bookmark2.LinkUrl = "https://mattermost.com/about"
time.Sleep(time.Millisecond * 250)
err = ss.ChannelBookmark().Update(bookmark2.Clone())
assert.NoError(t, err)
bookmarks, err := ss.ChannelBookmark().GetBookmarksForChannelSince(channelId, now)
assert.NoError(t, err)
assert.Len(t, bookmarks, 1)
b := find_bookmark(bookmarks, bookmark2.Id)
assert.NotNil(t, b)
assert.Equal(t, b.DisplayName, bookmark2.DisplayName)
assert.Equal(t, b.Type, model.ChannelBookmarkLink)
assert.NotEmpty(t, b.Emoji)
assert.Equal(t, b.CreateAt, bookmark2.CreateAt)
assert.Greater(t, b.UpdateAt, bookmark2.UpdateAt)
err = ss.ChannelBookmark().Update(bookmark1.Clone())
assert.Error(t, err)
bookmark3 := bookmark2.Clone()
bookmark3.Type = model.ChannelBookmarkFile
err = ss.ChannelBookmark().Update(bookmark3)
assert.Error(t, err)
})
}
func testUpdateSortOrderChannelBookmark(t *testing.T, rctx request.CTX, ss store.Store) {
channelId := model.NewId()
userId := model.NewId()
bookmark0 := &model.ChannelBookmark{
ChannelId: channelId,
OwnerId: userId,
DisplayName: "Bookmark 0",
LinkUrl: "https://mattermost.com",
Type: model.ChannelBookmarkLink,
Emoji: ":smile:",
}
file := &model.FileInfo{
Id: model.NewId(),
CreatorId: model.BookmarkFileOwner,
Path: "somepath",
ThumbnailPath: "thumbpath",
PreviewPath: "prevPath",
Name: "test file",
Extension: "png",
MimeType: "images/png",
Size: 873182,
Width: 3076,
Height: 2200,
HasPreviewImage: true,
}
bookmark1 := &model.ChannelBookmark{
ChannelId: channelId,
OwnerId: userId,
DisplayName: "Bookmark 1",
FileId: file.Id,
Type: model.ChannelBookmarkFile,
Emoji: ":smile:",
}
_, err := ss.FileInfo().Save(rctx, file)
require.NoError(t, err)
defer ss.FileInfo().PermanentDelete(rctx, file.Id)
bookmark2 := &model.ChannelBookmark{
ChannelId: channelId,
OwnerId: userId,
DisplayName: "Bookmark 2",
LinkUrl: "https://mattermost.com",
Type: model.ChannelBookmarkLink,
}
bookmark3 := &model.ChannelBookmark{
ChannelId: channelId,
OwnerId: userId,
DisplayName: "Bookmark 3",
LinkUrl: "https://mattermost.com",
Type: model.ChannelBookmarkLink,
}
bookmark4 := &model.ChannelBookmark{
ChannelId: channelId,
OwnerId: userId,
DisplayName: "Bookmark 4",
LinkUrl: "https://mattermost.com",
Type: model.ChannelBookmarkLink,
}
bookmarkResp, err := ss.ChannelBookmark().Save(bookmark0.Clone(), true)
assert.NoError(t, err)
bookmark0 = bookmarkResp.ChannelBookmark.Clone()
assert.NotEmpty(t, bookmarkResp.Id)
assert.Equal(t, bookmark0.ChannelId, bookmarkResp.ChannelId)
assert.Nil(t, bookmarkResp.FileInfo)
bookmarkResp, err = ss.ChannelBookmark().Save(bookmark1.Clone(), true)
assert.NoError(t, err)
bookmark1 = bookmarkResp.ChannelBookmark.Clone()
bookmarkResp, err = ss.ChannelBookmark().Save(bookmark2.Clone(), true)
assert.NoError(t, err)
bookmark2 = bookmarkResp.ChannelBookmark.Clone()
bookmarkResp, err = ss.ChannelBookmark().Save(bookmark3.Clone(), true)
assert.NoError(t, err)
bookmark3 = bookmarkResp.ChannelBookmark.Clone()
bookmarkResp, err = ss.ChannelBookmark().Save(bookmark4.Clone(), true)
assert.NoError(t, err)
bookmark4 = bookmarkResp.ChannelBookmark.Clone()
t.Run("change order of bookmarks first to last", func(t *testing.T) {
bookmarks, sortError := ss.ChannelBookmark().UpdateSortOrder(bookmark0.Id, channelId, 4)
assert.NoError(t, sortError)
assert.Equal(t, find_bookmark(bookmarks, bookmark1.Id).SortOrder, int64(0))
assert.Equal(t, find_bookmark(bookmarks, bookmark2.Id).SortOrder, int64(1))
assert.Equal(t, find_bookmark(bookmarks, bookmark3.Id).SortOrder, int64(2))
assert.Equal(t, find_bookmark(bookmarks, bookmark4.Id).SortOrder, int64(3))
assert.Equal(t, find_bookmark(bookmarks, bookmark0.Id).SortOrder, int64(4))
})
t.Run("change order of bookmarks last to first", func(t *testing.T) {
bookmarks, sortError := ss.ChannelBookmark().UpdateSortOrder(bookmark0.Id, channelId, 0)
assert.NoError(t, sortError)
assert.Equal(t, find_bookmark(bookmarks, bookmark0.Id).SortOrder, int64(0))
assert.Equal(t, find_bookmark(bookmarks, bookmark1.Id).SortOrder, int64(1))
assert.Equal(t, find_bookmark(bookmarks, bookmark2.Id).SortOrder, int64(2))
assert.Equal(t, find_bookmark(bookmarks, bookmark3.Id).SortOrder, int64(3))
assert.Equal(t, find_bookmark(bookmarks, bookmark4.Id).SortOrder, int64(4))
})
t.Run("change order of bookmarks first to third", func(t *testing.T) {
bookmarks, sortError := ss.ChannelBookmark().UpdateSortOrder(bookmark0.Id, channelId, 2)
assert.NoError(t, sortError)
assert.Equal(t, find_bookmark(bookmarks, bookmark1.Id).SortOrder, int64(0))
assert.Equal(t, find_bookmark(bookmarks, bookmark2.Id).SortOrder, int64(1))
assert.Equal(t, find_bookmark(bookmarks, bookmark0.Id).SortOrder, int64(2))
assert.Equal(t, find_bookmark(bookmarks, bookmark3.Id).SortOrder, int64(3))
assert.Equal(t, find_bookmark(bookmarks, bookmark4.Id).SortOrder, int64(4))
// now reset order
ss.ChannelBookmark().UpdateSortOrder(bookmark0.Id, channelId, 0)
})
t.Run("change order of bookmarks second to third", func(t *testing.T) {
bookmarks, sortError := ss.ChannelBookmark().UpdateSortOrder(bookmark1.Id, channelId, 2)
assert.NoError(t, sortError)
assert.Equal(t, find_bookmark(bookmarks, bookmark0.Id).SortOrder, int64(0))
assert.Equal(t, find_bookmark(bookmarks, bookmark2.Id).SortOrder, int64(1))
assert.Equal(t, find_bookmark(bookmarks, bookmark1.Id).SortOrder, int64(2))
assert.Equal(t, find_bookmark(bookmarks, bookmark3.Id).SortOrder, int64(3))
assert.Equal(t, find_bookmark(bookmarks, bookmark4.Id).SortOrder, int64(4))
})
t.Run("change order of bookmarks third to second", func(t *testing.T) {
bookmarks, sortError := ss.ChannelBookmark().UpdateSortOrder(bookmark1.Id, channelId, 1)
assert.NoError(t, sortError)
assert.Equal(t, find_bookmark(bookmarks, bookmark0.Id).SortOrder, int64(0))
assert.Equal(t, find_bookmark(bookmarks, bookmark1.Id).SortOrder, int64(1))
assert.Equal(t, find_bookmark(bookmarks, bookmark2.Id).SortOrder, int64(2))
assert.Equal(t, find_bookmark(bookmarks, bookmark3.Id).SortOrder, int64(3))
assert.Equal(t, find_bookmark(bookmarks, bookmark4.Id).SortOrder, int64(4))
})
t.Run("change order of bookmarks last to previous last", func(t *testing.T) {
bookmarks, sortError := ss.ChannelBookmark().UpdateSortOrder(bookmark4.Id, channelId, 3)
assert.NoError(t, sortError)
assert.Equal(t, find_bookmark(bookmarks, bookmark0.Id).SortOrder, int64(0))
assert.Equal(t, find_bookmark(bookmarks, bookmark1.Id).SortOrder, int64(1))
assert.Equal(t, find_bookmark(bookmarks, bookmark2.Id).SortOrder, int64(2))
assert.Equal(t, find_bookmark(bookmarks, bookmark4.Id).SortOrder, int64(3))
assert.Equal(t, find_bookmark(bookmarks, bookmark3.Id).SortOrder, int64(4))
})
t.Run("change order of bookmarks last to second", func(t *testing.T) {
bookmarks, sortError := ss.ChannelBookmark().UpdateSortOrder(bookmark3.Id, channelId, 1)
assert.NoError(t, sortError)
assert.Equal(t, find_bookmark(bookmarks, bookmark0.Id).SortOrder, int64(0))
assert.Equal(t, find_bookmark(bookmarks, bookmark3.Id).SortOrder, int64(1))
assert.Equal(t, find_bookmark(bookmarks, bookmark1.Id).SortOrder, int64(2))
assert.Equal(t, find_bookmark(bookmarks, bookmark2.Id).SortOrder, int64(3))
assert.Equal(t, find_bookmark(bookmarks, bookmark4.Id).SortOrder, int64(4))
})
t.Run("change order of bookmarks error when new index is out of bounds", func(t *testing.T) {
var iiErr *store.ErrInvalidInput
_, err = ss.ChannelBookmark().UpdateSortOrder(bookmark3.Id, channelId, -1)
assert.Error(t, err)
assert.ErrorAs(t, err, &iiErr)
_, err = ss.ChannelBookmark().UpdateSortOrder(bookmark3.Id, channelId, 5)
assert.Error(t, err)
assert.ErrorAs(t, err, &iiErr)
})
t.Run("change order of bookmarks error when bookmark not found", func(t *testing.T) {
_, err = ss.ChannelBookmark().UpdateSortOrder(model.NewId(), channelId, 0)
assert.Error(t, err)
var nfErr *store.ErrNotFound
assert.ErrorAs(t, err, &nfErr)
})
}
func testDeleteChannelBookmark(t *testing.T, rctx request.CTX, ss store.Store) {
channelId := model.NewId()
userId := model.NewId()
bookmark1 := &model.ChannelBookmark{
ChannelId: channelId,
OwnerId: userId,
DisplayName: "Link bookmark test",
LinkUrl: "https://mattermost.com",
Type: model.ChannelBookmarkLink,
Emoji: ":smile:",
}
file := &model.FileInfo{
Id: model.NewId(),
CreatorId: model.BookmarkFileOwner,
Path: "somepath",
ThumbnailPath: "thumbpath",
PreviewPath: "prevPath",
Name: "test file",
Extension: "png",
MimeType: "images/png",
Size: 873182,
Width: 3076,
Height: 2200,
HasPreviewImage: true,
}
bookmark2 := &model.ChannelBookmark{
ChannelId: channelId,
OwnerId: userId,
DisplayName: "file bookmark test",
FileId: file.Id,
Type: model.ChannelBookmarkFile,
Emoji: ":smile:",
}
_, err := ss.FileInfo().Save(rctx, file)
require.NoError(t, err)
defer ss.FileInfo().PermanentDelete(rctx, file.Id)
t.Run("delete bookmark", func(t *testing.T) {
now := model.GetMillis()
bookmarkResp, err := ss.ChannelBookmark().Save(bookmark1.Clone(), true)
assert.NoError(t, err)
bookmark1 = bookmarkResp.ChannelBookmark.Clone()
assert.NotEmpty(t, bookmarkResp.Id)
assert.Equal(t, bookmark1.ChannelId, bookmarkResp.ChannelId)
assert.Nil(t, bookmarkResp.FileInfo)
bookmarkResp, err = ss.ChannelBookmark().Save(bookmark2.Clone(), true)
assert.NoError(t, err)
bookmark2 = bookmarkResp.ChannelBookmark.Clone()
err = ss.ChannelBookmark().Delete(bookmark2.Id, true)
assert.NoError(t, err)
bookmarks, err := ss.ChannelBookmark().GetBookmarksForChannelSince(channelId, now)
assert.NoError(t, err)
assert.Len(t, bookmarks, 2) // we have two as the deleted record also gets returned for sync'ing purposes
b := find_bookmark(bookmarks, bookmark2.Id)
assert.NotNil(t, b)
assert.Equal(t, bookmarks[0].Type, model.ChannelBookmarkLink)
})
}
func testGetChannelBookmark(t *testing.T, rctx request.CTX, ss store.Store) {
channelId := model.NewId()
userId := model.NewId()
bookmark1 := &model.ChannelBookmark{
ChannelId: channelId,
OwnerId: userId,
DisplayName: "Link bookmark test",
LinkUrl: "https://mattermost.com",
Type: model.ChannelBookmarkLink,
Emoji: ":smile:",
}
t.Run("get bookmark", func(t *testing.T) {
bookmarkResp, err := ss.ChannelBookmark().Save(bookmark1.Clone(), true)
assert.NoError(t, err)
bookmark1 = bookmarkResp.ChannelBookmark.Clone()
bookmarkResp, err = ss.ChannelBookmark().Get(bookmark1.Id, false)
assert.NoError(t, err)
assert.NotEmpty(t, bookmarkResp.Id)
assert.Equal(t, bookmark1.ChannelId, bookmarkResp.ChannelId)
assert.Nil(t, bookmarkResp.FileInfo)
err = ss.ChannelBookmark().Delete(bookmark1.Id, true)
assert.NoError(t, err)
bookmarkResp, err = ss.ChannelBookmark().Get(bookmark1.Id, false)
assert.Error(t, err)
assert.Nil(t, bookmarkResp)
bookmarkResp, err = ss.ChannelBookmark().Get(bookmark1.Id, true)
assert.NoError(t, err)
assert.NotNil(t, bookmarkResp)
})
}