Files
upstream-mattermost/server/public/model/channel_bookmark_test.go
upstream-mirror 240db4e2a7
Some checks failed
Server CI / Postgres (shard 0) (push) Has been skipped
Server CI / Postgres (shard 3) (push) Has been skipped
BuildEnv Docker Image / build-image (push) Failing after 4s
BuildEnv Docker Image / build-image-fips (push) Failing after 4s
Migration Review & Release Notes / Generate migration review & release notes (push) Failing after 3s
Server CI / Build Environment (push) Failing after 3s
Server CI / Postgres (shard 1) (push) Has been skipped
Tools CI / check-style (mattermost-govet) (push) Failing after 4s
Server CI / Postgres (shard 2) (push) Has been skipped
Tools CI / check-style (mmgotool) (push) Failing after 3s
Server CI / Elasticsearch v8 Compatibility (push) Has been skipped
Server CI / OpenSearch v2 Compatibility (push) Has been skipped
Server CI / Postgres FIPS (shard 0) (push) Has been skipped
Tools CI / Test (mattermost-govet) (push) Failing after 3s
Server CI / Postgres FIPS (shard 1) (push) Has been skipped
Server CI / Postgres FIPS (shard 2) (push) Has been skipped
Tools CI / Test (mmgotool) (push) Failing after 3s
Server CI / Postgres FIPS (shard 3) (push) Has been skipped
Server CI / Run mmctl tests (push) Has been skipped
Server CI / Run mmctl tests (FIPS) (push) Has been skipped
Server CI / Merge Postgres Test Results (push) Has been skipped
Server CI / Merge Postgres FIPS Test Results (push) Has been skipped
Server CI / Check backport migrations (push) Has been skipped
Server CI / Check generated files (push) Has been skipped
Server CI / check-style (push) Has been skipped
Server CI / Vet API (push) Has been skipped
Server CI / Build mattermost server app (push) Has been skipped
Server CI / Server CI Complete (push) Failing after 2s
Server CI / Server CI Artifacts (push) Has been skipped
Server CI / Sentry Upload (push) Has been skipped
Server CI / Server CI Report (push) Failing after 5m1s
Server CI Nightly Race / Race Detector (push) Has been skipped
CodeQL / Analyze (go) (push) Has been cancelled
CodeQL / Analyze (javascript) (push) Has been cancelled
Webapp CI Cache Warm / Warm Webapp node_modules Cache (push) Has been cancelled
E2E CI Cache Warm / Warm E2E npm Registry Cache (push) Has been cancelled
API / build (push) Has been cancelled
Web App CI / check-types (push) Has been cancelled
Web App CI / check-lint (push) Has been cancelled
Web App CI / check-i18n (push) Has been cancelled
Web App CI / check-external-links (push) Has been cancelled
Web App CI / test (platform) (push) Has been cancelled
Web App CI / test (mattermost-redux) (push) Has been cancelled
Web App CI / test (channels shard 1/4) (push) Has been cancelled
Web App CI / test (channels shard 2/4) (push) Has been cancelled
Web App CI / test (channels shard 3/4) (push) Has been cancelled
Web App CI / test (channels shard 4/4) (push) Has been cancelled
Web App CI / upload-coverage (push) Has been cancelled
Web App CI / build (push) Has been cancelled
YAML Lint / yamllint (push) Has been cancelled
Push mirrored docker images / cd/Push mirrored docker images (push) Has been cancelled
docs-cd / Build and deploy docs (push) Has been cancelled
Docs CI / build (push) Has been cancelled
E2E Cache Warm: Cypress deps / Warm Cypress deps cache (push) Has been cancelled
Scorecards supply-chain security / Scorecard analysis (push) Has been cancelled
Server CI Weekly / Postgres FIPS (push) Has been skipped
Server CI Weekly / Run mmctl tests (FIPS) (push) Has been skipped
Server CI Weekly / Postgres with binary parameters (push) Has been skipped
Server CI Weekly / Build Environment (push) Failing after 1s
Server CI Cache Warm / Warm Go Module and Build Cache (push) Failing after 2s
Server CI Nightly Race / Build Environment (push) Failing after 1s
snapshot of https://github.com/mattermost/mattermost.git (master, 2026-09-16; blobs > 131072B omitted)
2026-09-16 00:29:55 +05: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://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")))
}