Files
team/server/public/model/channel_bookmark_test.go
zeekayandHanzo Dev 05e7bfa24b call the product Hanzo Team, and paint it in one ink
Renames the product across user-facing text -- 1475 lines, i18n and the
defaultMessage strings that must match it -- while leaving the names that
are code rather than prose: upstream copyright, Go import paths, the
@mattermost npm scope, i18n keys, Go template fields, and legacy theme
identifiers like AuthorType.Mattermost and the 'Mattermost' key that maps
an old theme to a new one. The rule is a word boundary on identifier
characters; MattermostEntry is a symbol, "Mattermost" in a sentence is
the product.

The theme goes monochrome. Hanzo and Hanzo Dark are the two built-ins and
Hanzo is the default, with the CSS custom properties that carry brand
colour outside the theme object -- sidebar, buttons, links, titles --
moved to black. Status colours stay: online, away, do-not-disturb and
error encode state, not brand, and flattening them to grey would cost
meaning to buy consistency.

The sign-in header drops the TEAM EDITION badge. It advertised which
licence tier you were on, and there are no tiers here. Booting without a
licence is now logged at debug rather than warned about, because it is
this server's normal condition rather than a fault.

Co-authored-by: Hanzo Dev <[email protected]>
2026-08-13 22:28:09 -07:00

794 lines
19 KiB
Go

// 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://hanzo.ai",
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://hanzo.ai",
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://hanzo.ai",
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://hanzo.ai",
ImageUrl: "https://hanzo.ai/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://hanzo.ai",
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://hanzo.ai",
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://hanzo.ai",
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")))
}