// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. // See LICENSE.txt for license information. package model import ( "strings" "testing" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) func TestChannelBookmarkIsValid(t *testing.T) { testCases := []struct { Description string Bookmark *ChannelBookmark ExpectedIsValid bool }{ { "nil bookmark", &ChannelBookmark{}, false, }, { "bookmark without create at timestamp", &ChannelBookmark{ Id: NewId(), OwnerId: NewId(), ChannelId: "", FileId: "", DisplayName: "display name", SortOrder: 0, LinkUrl: "", ImageUrl: "", Emoji: "", Type: ChannelBookmarkLink, CreateAt: 0, UpdateAt: 3, DeleteAt: 4, }, false, }, { "bookmark without update at timestamp", &ChannelBookmark{ Id: NewId(), OwnerId: NewId(), ChannelId: "", FileId: "", DisplayName: "display name", SortOrder: 0, LinkUrl: "", ImageUrl: "", Emoji: "", Type: ChannelBookmarkLink, CreateAt: 2, UpdateAt: 0, DeleteAt: 4, }, false, }, { "bookmark with missing channel id", &ChannelBookmark{ Id: NewId(), OwnerId: NewId(), ChannelId: "", FileId: "", DisplayName: "display name", SortOrder: 0, LinkUrl: "", ImageUrl: "", Emoji: "", Type: ChannelBookmarkLink, CreateAt: 2, UpdateAt: 3, DeleteAt: 4, }, false, }, { "bookmark with invalid channel id", &ChannelBookmark{ Id: NewId(), OwnerId: NewId(), ChannelId: "invalid", FileId: "", DisplayName: "display name", SortOrder: 0, LinkUrl: "", ImageUrl: "", Emoji: "", Type: ChannelBookmarkLink, CreateAt: 2, UpdateAt: 3, DeleteAt: 4, }, false, }, { "bookmark with missing owner id", &ChannelBookmark{ Id: NewId(), ChannelId: NewId(), OwnerId: "", FileId: "", DisplayName: "display name", SortOrder: 0, LinkUrl: "", ImageUrl: "", Emoji: "", Type: ChannelBookmarkLink, CreateAt: 2, UpdateAt: 3, DeleteAt: 4, }, false, }, { "bookmark with invalid user id", &ChannelBookmark{ Id: NewId(), ChannelId: NewId(), OwnerId: "invalid", FileId: "", DisplayName: "display name", SortOrder: 0, LinkUrl: "", ImageUrl: "", Emoji: "", Type: ChannelBookmarkLink, CreateAt: 2, UpdateAt: 3, DeleteAt: 4, }, false, }, { "bookmark with missing displayname", &ChannelBookmark{ Id: NewId(), ChannelId: NewId(), OwnerId: NewId(), FileId: "", DisplayName: "", SortOrder: 0, LinkUrl: "", ImageUrl: "", Emoji: "", Type: ChannelBookmarkLink, CreateAt: 2, UpdateAt: 3, DeleteAt: 4, }, false, }, { "bookmark with missing type", &ChannelBookmark{ Id: NewId(), ChannelId: NewId(), OwnerId: NewId(), FileId: "", DisplayName: "display name", SortOrder: 0, LinkUrl: "", ImageUrl: "", Emoji: "", Type: "", CreateAt: 2, UpdateAt: 3, DeleteAt: 4, }, false, }, { "bookmark with invalid type", &ChannelBookmark{ Id: NewId(), ChannelId: NewId(), OwnerId: NewId(), FileId: "", DisplayName: "display name", SortOrder: 0, LinkUrl: "", ImageUrl: "", Emoji: "", Type: "invalid", CreateAt: 2, UpdateAt: 3, DeleteAt: 4, }, false, }, { "bookmark of type link with missing link url", &ChannelBookmark{ Id: NewId(), ChannelId: NewId(), OwnerId: NewId(), FileId: "", DisplayName: "display name", SortOrder: 0, LinkUrl: "", ImageUrl: "", Emoji: "", Type: ChannelBookmarkLink, CreateAt: 2, UpdateAt: 3, DeleteAt: 4, }, false, }, { "bookmark of type link with invalid link url", &ChannelBookmark{ Id: NewId(), ChannelId: NewId(), OwnerId: NewId(), FileId: "", DisplayName: "display name", SortOrder: 0, LinkUrl: "invalid", ImageUrl: "", Emoji: "", Type: ChannelBookmarkLink, CreateAt: 2, UpdateAt: 3, DeleteAt: 4, }, false, }, { "bookmark of type link with valid link url", &ChannelBookmark{ Id: NewId(), ChannelId: NewId(), OwnerId: NewId(), FileId: "", DisplayName: "display name", SortOrder: 0, LinkUrl: "https://mattermost.com", ImageUrl: "", Emoji: "", Type: ChannelBookmarkLink, CreateAt: 2, UpdateAt: 3, DeleteAt: 4, }, true, }, { "bookmark of type link with empty image url", &ChannelBookmark{ Id: NewId(), ChannelId: NewId(), OwnerId: NewId(), FileId: "", DisplayName: "display name", SortOrder: 0, LinkUrl: "https://mattermost.com", ImageUrl: "", Emoji: "", Type: ChannelBookmarkLink, CreateAt: 2, UpdateAt: 3, DeleteAt: 4, }, true, }, { "bookmark of type link with invalid image url", &ChannelBookmark{ Id: NewId(), ChannelId: NewId(), OwnerId: NewId(), FileId: "", DisplayName: "display name", SortOrder: 0, LinkUrl: "https://mattermost.com", ImageUrl: "invalid", Emoji: "", Type: ChannelBookmarkLink, CreateAt: 2, UpdateAt: 3, DeleteAt: 4, }, false, }, { "bookmark of type link with valid image url", &ChannelBookmark{ Id: NewId(), ChannelId: NewId(), OwnerId: NewId(), FileId: "", DisplayName: "display name", SortOrder: 0, LinkUrl: "https://mattermost.com", ImageUrl: "https://mattermost.com/some-image-without-extension", // we don't care if the URL is an actual image as the client should handle the error Emoji: "", Type: ChannelBookmarkLink, CreateAt: 2, UpdateAt: 3, DeleteAt: 4, }, true, }, { "bookmark of type file with missing file id", &ChannelBookmark{ Id: NewId(), ChannelId: NewId(), OwnerId: NewId(), FileId: "", DisplayName: "display name", SortOrder: 0, LinkUrl: "", ImageUrl: "", Emoji: "", Type: ChannelBookmarkFile, CreateAt: 2, UpdateAt: 3, DeleteAt: 4, }, false, }, { "bookmark of type file with invalid file id", &ChannelBookmark{ Id: NewId(), ChannelId: NewId(), OwnerId: NewId(), FileId: "invalid", DisplayName: "display name", SortOrder: 0, LinkUrl: "", ImageUrl: "", Emoji: "", Type: ChannelBookmarkFile, CreateAt: 2, UpdateAt: 3, DeleteAt: 4, }, false, }, { "bookmark of type file with valid file id", &ChannelBookmark{ Id: NewId(), ChannelId: NewId(), OwnerId: NewId(), FileId: NewId(), DisplayName: "display name", SortOrder: 0, LinkUrl: "", ImageUrl: "", Emoji: "", Type: ChannelBookmarkFile, CreateAt: 2, UpdateAt: 3, DeleteAt: 4, }, true, }, { "bookmark of type file with invalid original id", &ChannelBookmark{ Id: NewId(), ChannelId: NewId(), OwnerId: NewId(), FileId: NewId(), DisplayName: "display name", SortOrder: 0, LinkUrl: "", ImageUrl: "", Emoji: "", Type: ChannelBookmarkFile, CreateAt: 2, UpdateAt: 3, DeleteAt: 4, OriginalId: "invalid", }, false, }, { "bookmark of type file with invalid parent id", &ChannelBookmark{ Id: NewId(), ChannelId: NewId(), OwnerId: NewId(), FileId: NewId(), DisplayName: "display name", SortOrder: 0, LinkUrl: "", ImageUrl: "", Emoji: "", Type: ChannelBookmarkFile, CreateAt: 2, UpdateAt: 3, DeleteAt: 4, ParentId: "invalid", }, false, }, { "bookmark of type link with a file Id attached", &ChannelBookmark{ Id: NewId(), OwnerId: NewId(), ChannelId: NewId(), FileId: NewId(), DisplayName: "display name", SortOrder: 0, LinkUrl: "http://somelink", ImageUrl: "", Emoji: "", Type: ChannelBookmarkLink, CreateAt: 2, UpdateAt: 3, DeleteAt: 0, }, false, }, { "bookmark of type file with a url", &ChannelBookmark{ Id: NewId(), OwnerId: NewId(), ChannelId: NewId(), FileId: NewId(), DisplayName: "display name", SortOrder: 0, LinkUrl: "http://somelink", ImageUrl: "", Emoji: "", Type: ChannelBookmarkFile, CreateAt: 2, UpdateAt: 3, DeleteAt: 0, }, false, }, { "bookmark with long display name > limit", &ChannelBookmark{ Id: NewId(), OwnerId: NewId(), ChannelId: NewId(), FileId: "", DisplayName: strings.Repeat("1", 65), SortOrder: 0, LinkUrl: "http://somelink", ImageUrl: "", Emoji: "", Type: ChannelBookmarkLink, CreateAt: 3, UpdateAt: 3, DeleteAt: 0, }, false, }, { "bookmark with long display name < limit", &ChannelBookmark{ Id: NewId(), OwnerId: NewId(), ChannelId: NewId(), FileId: "", DisplayName: strings.Repeat("1", 64), SortOrder: 0, LinkUrl: "http://somelink", ImageUrl: "", Emoji: "", Type: ChannelBookmarkLink, CreateAt: 3, UpdateAt: 3, DeleteAt: 0, }, true, }, { "bookmark with link url > limit", &ChannelBookmark{ Id: NewId(), OwnerId: NewId(), ChannelId: NewId(), FileId: "", DisplayName: "not last test", SortOrder: 0, LinkUrl: "http://somelink?" + strings.Repeat("h", 1024), ImageUrl: "", Emoji: "", Type: ChannelBookmarkLink, CreateAt: 3, UpdateAt: 3, DeleteAt: 0, }, false, }, { "bookmark with image url > limit", &ChannelBookmark{ Id: NewId(), OwnerId: NewId(), ChannelId: NewId(), FileId: "", DisplayName: "last test", SortOrder: 0, LinkUrl: "", ImageUrl: "http://somelink?" + strings.Repeat("h", 1024), Emoji: "", Type: ChannelBookmarkLink, CreateAt: 3, UpdateAt: 3, DeleteAt: 0, }, false, }, } for _, testCase := range testCases { t.Run(testCase.Description, func(t *testing.T) { if testCase.ExpectedIsValid { require.Nil(t, testCase.Bookmark.IsValid()) } else { require.NotNil(t, testCase.Bookmark.IsValid()) } }) } } func TestChannelBookmarkPreSave(t *testing.T) { bookmark := &ChannelBookmark{ Id: NewId(), ChannelId: NewId(), OwnerId: NewId(), DisplayName: "display name", SortOrder: 0, LinkUrl: "https://mattermost.com", Type: ChannelBookmarkLink, DeleteAt: 0, } originalBookmark := &ChannelBookmark{ Id: bookmark.Id, ChannelId: bookmark.ChannelId, OwnerId: bookmark.OwnerId, DisplayName: bookmark.DisplayName, SortOrder: bookmark.SortOrder, LinkUrl: bookmark.LinkUrl, Type: bookmark.Type, DeleteAt: bookmark.DeleteAt, } bookmark.PreSave() assert.NotEqual(t, 0, bookmark.CreateAt) assert.NotEqual(t, 0, bookmark.UpdateAt) originalBookmark.CreateAt = bookmark.CreateAt originalBookmark.UpdateAt = bookmark.UpdateAt assert.Equal(t, originalBookmark, bookmark) } func TestChannelBookmarkPreUpdate(t *testing.T) { bookmark := &ChannelBookmark{ Id: NewId(), ChannelId: NewId(), OwnerId: NewId(), DisplayName: "display name", SortOrder: 0, LinkUrl: "https://mattermost.com", Type: ChannelBookmarkLink, CreateAt: 2, DeleteAt: 0, } originalBookmark := &ChannelBookmark{ Id: bookmark.Id, ChannelId: bookmark.ChannelId, OwnerId: bookmark.OwnerId, DisplayName: bookmark.DisplayName, SortOrder: bookmark.SortOrder, LinkUrl: bookmark.LinkUrl, Type: bookmark.Type, DeleteAt: bookmark.DeleteAt, } bookmark.PreSave() assert.NotEqual(t, 0, bookmark.UpdateAt) originalBookmark.CreateAt = bookmark.CreateAt originalBookmark.UpdateAt = bookmark.UpdateAt assert.Equal(t, originalBookmark, bookmark) bookmark.PreUpdate() assert.Greater(t, bookmark.UpdateAt, originalBookmark.UpdateAt) } func TestToBookmarkWithFileInfo(t *testing.T) { testCases := []struct { name string bookmark *ChannelBookmark fileInfo *FileInfo expectedEmoji string }{ { name: "emoji with colons", bookmark: &ChannelBookmark{ Id: NewId(), DisplayName: "test bookmark", Emoji: ":smile:", Type: ChannelBookmarkLink, }, fileInfo: nil, expectedEmoji: "smile", }, { name: "emoji without colons", bookmark: &ChannelBookmark{ Id: NewId(), DisplayName: "test bookmark", Emoji: "smile", Type: ChannelBookmarkLink, }, fileInfo: nil, expectedEmoji: "smile", }, { name: "empty emoji", bookmark: &ChannelBookmark{ Id: NewId(), DisplayName: "test bookmark", Emoji: "", Type: ChannelBookmarkLink, }, fileInfo: nil, expectedEmoji: "", }, } for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { result := tc.bookmark.ToBookmarkWithFileInfo(tc.fileInfo) assert.Equal(t, tc.expectedEmoji, result.Emoji) }) } } func TestChannelBookmarkPatch(t *testing.T) { p := &ChannelBookmarkPatch{ DisplayName: new(NewId()), SortOrder: new(int64(1)), LinkUrl: new(NewId()), } b := ChannelBookmark{ Id: NewId(), DisplayName: NewId(), Type: ChannelBookmarkLink, // should not update LinkUrl: NewId(), } b.Patch(p) require.Empty(t, b.FileId) require.Equal(t, *p.DisplayName, b.DisplayName) require.Equal(t, *p.SortOrder, b.SortOrder) require.Equal(t, *p.LinkUrl, b.LinkUrl) require.Equal(t, ChannelBookmarkLink, b.Type) } func validBoardChannelBookmark() *ChannelBookmark { boardID := NewId() return &ChannelBookmark{ Id: NewId(), CreateAt: GetMillis(), UpdateAt: GetMillis(), DeleteAt: 0, ChannelId: NewId(), OwnerId: NewId(), FileId: "", DisplayName: "Board tab", SortOrder: 0, LinkUrl: "/teamname/boards/" + boardID, ImageUrl: "", Emoji: "", Type: ChannelBookmarkBoard, TargetId: boardID, } } func TestChannelBookmarkIsValidBoard(t *testing.T) { t.Run("valid board bookmark", func(t *testing.T) { require.Nil(t, validBoardChannelBookmark().IsValid()) }) t.Run("missing target id", func(t *testing.T) { b := validBoardChannelBookmark() b.TargetId = "" err := b.IsValid() require.NotNil(t, err) require.Equal(t, "model.channel_bookmark.is_valid.board.target_id.app_error", err.Id) }) t.Run("invalid target id", func(t *testing.T) { b := validBoardChannelBookmark() b.TargetId = "notaulid" err := b.IsValid() require.NotNil(t, err) require.Equal(t, "model.channel_bookmark.is_valid.board.target_id.app_error", err.Id) }) t.Run("link url without leading slash", func(t *testing.T) { b := validBoardChannelBookmark() b.LinkUrl = "team/boards/" + b.TargetId err := b.IsValid() require.NotNil(t, err) require.Equal(t, "model.channel_bookmark.is_valid.board.link_url.app_error", err.Id) }) t.Run("link url contains scheme", func(t *testing.T) { b := validBoardChannelBookmark() b.LinkUrl = "http://x/" + b.TargetId err := b.IsValid() require.NotNil(t, err) require.Equal(t, "model.channel_bookmark.is_valid.board.link_url.app_error", err.Id) }) t.Run("link url is protocol-relative", func(t *testing.T) { b := validBoardChannelBookmark() b.LinkUrl = "//evil.com/" + b.TargetId err := b.IsValid() require.NotNil(t, err) require.Equal(t, "model.channel_bookmark.is_valid.board.link_url.app_error", err.Id) }) t.Run("link url exceeds max runes", func(t *testing.T) { b := validBoardChannelBookmark() b.LinkUrl = "/" + strings.Repeat("a", LinkMaxRunes) err := b.IsValid() require.NotNil(t, err) require.Equal(t, "model.channel_bookmark.is_valid.board.link_url.app_error", err.Id) }) t.Run("non-empty file id", func(t *testing.T) { b := validBoardChannelBookmark() b.FileId = NewId() err := b.IsValid() require.NotNil(t, err) require.Equal(t, "model.channel_bookmark.is_valid.board.file_id.app_error", err.Id) }) t.Run("non-empty image url", func(t *testing.T) { b := validBoardChannelBookmark() b.ImageUrl = "https://example.com/x.png" err := b.IsValid() require.NotNil(t, err) require.Equal(t, "model.channel_bookmark.is_valid.board.image_url.app_error", err.Id) }) t.Run("link type with target id", func(t *testing.T) { b := &ChannelBookmark{ Id: NewId(), CreateAt: GetMillis(), UpdateAt: GetMillis(), ChannelId: NewId(), OwnerId: NewId(), DisplayName: "x", LinkUrl: "https://mattermost.com", Type: ChannelBookmarkLink, TargetId: NewId(), } err := b.IsValid() require.NotNil(t, err) require.Equal(t, "model.channel_bookmark.is_valid.target_id.app_error", err.Id) }) t.Run("file type with target id", func(t *testing.T) { b := &ChannelBookmark{ Id: NewId(), CreateAt: GetMillis(), UpdateAt: GetMillis(), ChannelId: NewId(), OwnerId: NewId(), DisplayName: "x", FileId: NewId(), Type: ChannelBookmarkFile, TargetId: NewId(), } err := b.IsValid() require.NotNil(t, err) require.Equal(t, "model.channel_bookmark.is_valid.target_id.app_error", err.Id) }) } func TestIsExternallyManagedChannelBookmarkType(t *testing.T) { assert.True(t, IsExternallyManagedChannelBookmarkType(ChannelBookmarkBoard)) assert.False(t, IsExternallyManagedChannelBookmarkType(ChannelBookmarkLink)) assert.False(t, IsExternallyManagedChannelBookmarkType(ChannelBookmarkFile)) assert.False(t, IsExternallyManagedChannelBookmarkType(ChannelBookmarkType("unknown"))) }