mirror of
https://github.com/hanzoai/team.git
synced 2026-09-30 01:57:35 +00:00
* Mattermost Blocks * Rethink some block props and styles * Remove obsolete comment * Style fixes and add examples * Add mention, search, and image metadata support * Several UI improvements * Validate blocks by translating them * Add initial e2e tests * Fix lint * Fix tests and lint * Fix lint * Improve e2e and simplify doPostAction * server side cleanup * Fix typo * Fix style * Fix some tests * Fix more tests * Fix cypress tests * Fix playwhright tests * Fix flaky system console test * Frontend cleanup * Add blocks editor * Fix CI * Limit all blocks width * Fix test * Enable webhook service on playwhright CI * fix flaky test * Fix style * Cleanups * Address coderabbit comments * Address coderabbit comments * Address CI errors * Fix tests * Address changes in valid actions * UX fixes * Fix test * Fix danger buttons and add allow emojis in buttons * style tweaks use semantic color tokens for buttons and accents, update max-heights for scollables, adjustments to containers, removed btn-sm (using default button size for mm blocks with height and padding adjustments for use within blocks. removed hover selector from utils to prevent hover state issue with primary button, refined semantic-color-warning. * Address feedback * Add interactive content to post previews * Add collapse transitions * Address autocomplete position * Handle columns within column sets * Fix css lint * Fix tests * Use feature flag * Address feedback * Fix lint and add missing tests * Fix e2e tests * Fix e2e test * Allow interactive posts without a message * Fix lint * Address feedback * Fix ci * Fix lint * Add limits to block translations * Fix lint * Address feedback * Address feedback * Update snapshot * Fix collapsible not defaulting to the right value * Add total text limit * Sync translations with mobile * Fix snapshot --------- Co-authored-by: Matthew Birtch <[email protected]> Co-authored-by: Mattermost Build <[email protected]>
3891 lines
122 KiB
Go
3891 lines
122 KiB
Go
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
|
// See LICENSE.txt for license information.
|
|
|
|
package app
|
|
|
|
import (
|
|
"bytes"
|
|
"fmt"
|
|
"image"
|
|
"image/png"
|
|
"io"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"net/url"
|
|
"os"
|
|
"strconv"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/mattermost/mattermost/server/v8/channels/store"
|
|
"github.com/mattermost/mattermost/server/v8/channels/store/sqlstore"
|
|
"github.com/mattermost/mattermost/server/v8/channels/store/storetest/mocks"
|
|
"github.com/stretchr/testify/mock"
|
|
|
|
"github.com/dyatlov/go-opengraph/opengraph"
|
|
ogimage "github.com/dyatlov/go-opengraph/opengraph/types/image"
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
|
|
"github.com/mattermost/mattermost/server/public/model"
|
|
"github.com/mattermost/mattermost/server/public/shared/httpservice"
|
|
"github.com/mattermost/mattermost/server/public/shared/mlog"
|
|
"github.com/mattermost/mattermost/server/public/shared/request"
|
|
"github.com/mattermost/mattermost/server/v8/channels/app/platform"
|
|
"github.com/mattermost/mattermost/server/v8/channels/utils/testutils"
|
|
"github.com/mattermost/mattermost/server/v8/platform/services/imageproxy"
|
|
)
|
|
|
|
func TestPreparePostListForClient(t *testing.T) {
|
|
mainHelper.Parallel(t)
|
|
// Most of this logic is covered by TestPreparePostForClient, so this just tests handling of multiple posts
|
|
|
|
th := Setup(t)
|
|
|
|
postList := model.NewPostList()
|
|
for range 5 {
|
|
postList.AddPost(&model.Post{})
|
|
}
|
|
|
|
clientPostList := th.App.PreparePostListForClient(th.Context, postList)
|
|
|
|
t.Run("doesn't mutate provided post list", func(t *testing.T) {
|
|
assert.NotEqual(t, clientPostList, postList, "should've returned a new post list")
|
|
assert.NotEqual(t, clientPostList.Posts, postList.Posts, "should've returned a new PostList.Posts")
|
|
assert.Equal(t, clientPostList.Order, postList.Order, "should've returned the existing PostList.Order")
|
|
|
|
for id, originalPost := range postList.Posts {
|
|
assert.NotEqual(t, clientPostList.Posts[id], originalPost, "should've returned new post objects")
|
|
assert.Equal(t, clientPostList.Posts[id].Id, originalPost.Id, "should've returned the same posts")
|
|
}
|
|
})
|
|
|
|
t.Run("adds metadata to each post", func(t *testing.T) {
|
|
for _, clientPost := range clientPostList.Posts {
|
|
assert.NotNil(t, clientPost.Metadata, "should've populated metadata for each post")
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestPreparePostForClient(t *testing.T) {
|
|
mainHelper.Parallel(t)
|
|
var serverURL string
|
|
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
switch r.URL.Path {
|
|
case "/":
|
|
w.Header().Set("Content-Type", "text/html")
|
|
_, err := w.Write([]byte(`
|
|
<html>
|
|
<head>
|
|
<meta property="og:image" content="` + serverURL + `/test-image3.png" />
|
|
<meta property="og:site_name" content="GitHub" />
|
|
<meta property="og:type" content="object" />
|
|
<meta property="og:title" content="hmhealey/test-files" />
|
|
<meta property="og:url" content="https://github.com/hmhealey/test-files" />
|
|
<meta property="og:description" content="Contribute to hmhealey/test-files development by creating an account on GitHub." />
|
|
</head>
|
|
</html>`))
|
|
require.NoError(t, err)
|
|
case "/test-image1.png":
|
|
file, err := testutils.ReadTestFile("test.png")
|
|
require.NoError(t, err)
|
|
|
|
w.Header().Set("Content-Type", "image/png")
|
|
_, err = w.Write(file)
|
|
require.NoError(t, err)
|
|
case "/test-image2.png":
|
|
file, err := testutils.ReadTestFile("test-data-graph.png")
|
|
require.NoError(t, err)
|
|
|
|
w.Header().Set("Content-Type", "image/png")
|
|
_, err = w.Write(file)
|
|
require.NoError(t, err)
|
|
case "/test-image3.png":
|
|
file, err := testutils.ReadTestFile("qa-data-graph.png")
|
|
require.NoError(t, err)
|
|
|
|
w.Header().Set("Content-Type", "image/png")
|
|
_, err = w.Write(file)
|
|
require.NoError(t, err)
|
|
default:
|
|
require.Fail(t, "Invalid path", r.URL.Path)
|
|
}
|
|
}))
|
|
serverURL = server.URL
|
|
defer server.Close()
|
|
|
|
setup := func(t *testing.T) *TestHelper {
|
|
th := Setup(t).InitBasic(t)
|
|
|
|
th.App.UpdateConfig(func(cfg *model.Config) {
|
|
*cfg.ServiceSettings.EnableLinkPreviews = true
|
|
*cfg.ImageProxySettings.Enable = false
|
|
*cfg.ServiceSettings.AllowedUntrustedInternalConnections = "localhost,127.0.0.1"
|
|
})
|
|
|
|
return th
|
|
}
|
|
|
|
t.Run("no metadata needed", func(t *testing.T) {
|
|
th := setup(t)
|
|
|
|
message := model.NewId()
|
|
post := &model.Post{
|
|
Message: message,
|
|
}
|
|
|
|
clientPost := th.App.PreparePostForClient(th.Context, post, &model.PreparePostForClientOpts{IsEditPost: true})
|
|
|
|
t.Run("doesn't mutate provided post", func(t *testing.T) {
|
|
assert.NotEqual(t, clientPost, post, "should've returned a new post")
|
|
|
|
assert.Equal(t, message, post.Message, "shouldn't have mutated post.Message")
|
|
assert.Equal(t, (*model.PostMetadata)(nil), post.Metadata, "shouldn't have mutated post.Metadata")
|
|
})
|
|
|
|
t.Run("populates all fields", func(t *testing.T) {
|
|
assert.Equal(t, message, clientPost.Message, "shouldn't have changed Message")
|
|
assert.NotEqual(t, nil, clientPost.Metadata, "should've populated Metadata")
|
|
assert.Empty(t, clientPost.Metadata.Embeds, "should've populated Embeds")
|
|
assert.Empty(t, clientPost.Metadata.Reactions, "should've populated Reactions")
|
|
assert.Empty(t, clientPost.Metadata.Files, "should've populated Files")
|
|
assert.Empty(t, clientPost.Metadata.Emojis, "should've populated Emojis")
|
|
assert.Empty(t, clientPost.Metadata.Images, "should've populated Images")
|
|
})
|
|
})
|
|
|
|
t.Run("metadata already set", func(t *testing.T) {
|
|
th := setup(t)
|
|
|
|
post := th.CreatePost(t, th.BasicChannel)
|
|
|
|
clientPost := th.App.PreparePostForClient(th.Context, post, &model.PreparePostForClientOpts{})
|
|
|
|
assert.False(t, clientPost == post, "should've returned a new post")
|
|
assert.Equal(t, clientPost, post, "shouldn't have changed any metadata")
|
|
})
|
|
|
|
t.Run("priority read from master when context flag set", func(t *testing.T) {
|
|
// Verifies priority is read from master DB when master context flag is set
|
|
th := setup(t)
|
|
|
|
th.App.UpdateConfig(func(cfg *model.Config) {
|
|
*cfg.ServiceSettings.PostPriority = true
|
|
})
|
|
|
|
th.Context.Session().UserId = th.BasicUser.Id
|
|
|
|
// Create a real post with priority
|
|
post, _, err := th.App.CreatePost(th.Context, &model.Post{
|
|
UserId: th.BasicUser.Id,
|
|
ChannelId: th.BasicChannel.Id,
|
|
Message: "test message with priority",
|
|
Metadata: &model.PostMetadata{
|
|
Priority: &model.PostPriority{
|
|
Priority: model.NewPointer(model.PostPriorityUrgent),
|
|
RequestedAck: new(true),
|
|
},
|
|
},
|
|
}, th.BasicChannel, model.CreatePostFlags{SetOnline: true})
|
|
require.Nil(t, err)
|
|
|
|
// Clear metadata to simulate fresh fetch
|
|
postWithoutPriority := post.Clone()
|
|
postWithoutPriority.Metadata = &model.PostMetadata{}
|
|
|
|
// Use master context to ensure we read from master DB
|
|
masterCtx := sqlstore.RequestContextWithMaster(th.Context)
|
|
clientPost := th.App.PreparePostForClient(masterCtx, postWithoutPriority, &model.PreparePostForClientOpts{
|
|
IncludePriority: true,
|
|
})
|
|
|
|
require.NotNil(t, clientPost.Metadata)
|
|
require.NotNil(t, clientPost.Metadata.Priority, "priority should be fetched from master DB")
|
|
assert.Equal(t, model.PostPriorityUrgent, *clientPost.Metadata.Priority.Priority)
|
|
assert.True(t, *clientPost.Metadata.Priority.RequestedAck)
|
|
})
|
|
|
|
t.Run("priority fetched from database when IncludePriority is true", func(t *testing.T) {
|
|
// Verifies priority is fetched from DB when it exists.
|
|
th := setup(t)
|
|
|
|
th.App.UpdateConfig(func(cfg *model.Config) {
|
|
*cfg.ServiceSettings.PostPriority = true
|
|
})
|
|
|
|
th.Context.Session().UserId = th.BasicUser.Id
|
|
|
|
post, _, err := th.App.CreatePost(th.Context, &model.Post{
|
|
UserId: th.BasicUser.Id,
|
|
ChannelId: th.BasicChannel.Id,
|
|
Message: "test message with priority",
|
|
Metadata: &model.PostMetadata{
|
|
Priority: &model.PostPriority{
|
|
Priority: model.NewPointer(model.PostPriorityUrgent),
|
|
RequestedAck: new(true),
|
|
},
|
|
},
|
|
}, th.BasicChannel, model.CreatePostFlags{SetOnline: true})
|
|
require.Nil(t, err)
|
|
|
|
// Clear metadata to simulate fresh fetch
|
|
postWithoutPriority := post.Clone()
|
|
postWithoutPriority.Metadata = &model.PostMetadata{}
|
|
|
|
clientPost := th.App.PreparePostForClient(th.Context, postWithoutPriority, &model.PreparePostForClientOpts{
|
|
IncludePriority: true,
|
|
})
|
|
|
|
require.NotNil(t, clientPost.Metadata)
|
|
require.NotNil(t, clientPost.Metadata.Priority, "priority should be fetched from DB")
|
|
assert.Equal(t, model.PostPriorityUrgent, *clientPost.Metadata.Priority.Priority)
|
|
assert.True(t, *clientPost.Metadata.Priority.RequestedAck)
|
|
})
|
|
|
|
t.Run("burn on read post priority read from master", func(t *testing.T) {
|
|
// Verifies BoR post priority is correctly fetched when using master context
|
|
th := setup(t)
|
|
|
|
// Enable BurnOnRead feature flag
|
|
th.App.UpdateConfig(func(cfg *model.Config) { cfg.FeatureFlags.BurnOnRead = true })
|
|
|
|
// Enable BoR feature with license and config
|
|
th.App.Srv().SetLicense(model.NewTestLicenseSKU(model.LicenseShortSkuEnterpriseAdvanced))
|
|
th.App.UpdateConfig(func(cfg *model.Config) {
|
|
*cfg.ServiceSettings.PostPriority = true
|
|
*cfg.ServiceSettings.EnableBurnOnRead = true
|
|
})
|
|
|
|
th.Context.Session().UserId = th.BasicUser.Id
|
|
|
|
// Create a burn-on-read post with priority
|
|
post, _, err := th.App.CreatePost(th.Context, &model.Post{
|
|
UserId: th.BasicUser.Id,
|
|
ChannelId: th.BasicChannel.Id,
|
|
Message: "burn on read message with priority",
|
|
Type: model.PostTypeBurnOnRead,
|
|
Metadata: &model.PostMetadata{
|
|
Priority: &model.PostPriority{
|
|
Priority: model.NewPointer(model.PostPriorityUrgent),
|
|
RequestedAck: new(true),
|
|
},
|
|
},
|
|
}, th.BasicChannel, model.CreatePostFlags{SetOnline: true})
|
|
require.Nil(t, err)
|
|
require.Equal(t, model.PostTypeBurnOnRead, post.Type)
|
|
|
|
// Clear metadata to simulate websocket broadcast scenario
|
|
postWithoutPriority := post.Clone()
|
|
postWithoutPriority.Metadata = &model.PostMetadata{}
|
|
|
|
// Use master context (as processBroadcastHookForBurnOnRead does)
|
|
masterCtx := sqlstore.RequestContextWithMaster(th.Context)
|
|
clientPost := th.App.PreparePostForClient(masterCtx, postWithoutPriority, &model.PreparePostForClientOpts{
|
|
IncludePriority: true,
|
|
RetainContent: true,
|
|
})
|
|
|
|
require.NotNil(t, clientPost.Metadata)
|
|
require.NotNil(t, clientPost.Metadata.Priority, "BoR post priority should be fetched from master DB")
|
|
assert.Equal(t, model.PostPriorityUrgent, *clientPost.Metadata.Priority.Priority)
|
|
assert.True(t, *clientPost.Metadata.Priority.RequestedAck)
|
|
})
|
|
|
|
t.Run("reactions", func(t *testing.T) {
|
|
th := setup(t)
|
|
|
|
post := th.CreatePost(t, th.BasicChannel)
|
|
reaction1 := th.AddReactionToPost(t, post, th.BasicUser, "smile")
|
|
reaction2 := th.AddReactionToPost(t, post, th.BasicUser2, "smile")
|
|
reaction3 := th.AddReactionToPost(t, post, th.BasicUser2, "ice_cream")
|
|
reactions := []*model.Reaction{reaction1, reaction2, reaction3}
|
|
post.HasReactions = true
|
|
|
|
clientPost := th.App.PreparePostForClient(th.Context, post, &model.PreparePostForClientOpts{})
|
|
|
|
assert.Len(t, clientPost.Metadata.Reactions, 3, "should've populated Reactions")
|
|
assert.ElementsMatch(t, reactions, clientPost.Metadata.Reactions)
|
|
})
|
|
|
|
t.Run("files", func(t *testing.T) {
|
|
th := setup(t)
|
|
|
|
fileInfo, err := th.App.DoUploadFile(th.Context, time.Now(), th.BasicTeam.Id, th.BasicChannel.Id, th.BasicUser.Id, "test.txt", []byte("test"), true)
|
|
require.Nil(t, err)
|
|
|
|
post, _, err := th.App.CreatePost(th.Context, &model.Post{
|
|
UserId: th.BasicUser.Id,
|
|
ChannelId: th.BasicChannel.Id,
|
|
FileIds: []string{fileInfo.Id},
|
|
}, th.BasicChannel, model.CreatePostFlags{SetOnline: true})
|
|
require.Nil(t, err)
|
|
|
|
// DoUploadFile kicks off async content extraction; wait for attach + extraction on
|
|
// master before PreparePostForClient so the file-info cache is not seeded early.
|
|
require.EventuallyWithT(t, func(c *assert.CollectT) {
|
|
attached, storeErr := th.App.Srv().Store().FileInfo().GetFromMaster(fileInfo.Id)
|
|
if !assert.NoError(c, storeErr) || attached == nil {
|
|
return
|
|
}
|
|
assert.Equal(c, post.Id, attached.PostId)
|
|
if *th.App.Config().FileSettings.ExtractContent {
|
|
assert.Equal(c, "test", attached.Content)
|
|
}
|
|
}, 10*time.Second, 25*time.Millisecond)
|
|
|
|
expectedFileInfo, storeErr := th.App.Srv().Store().FileInfo().GetFromMaster(fileInfo.Id)
|
|
require.NoError(t, storeErr)
|
|
|
|
var clientPost *model.Post
|
|
assert.EventuallyWithT(t, func(c *assert.CollectT) {
|
|
clientPost = th.App.PreparePostForClient(th.Context, post, &model.PreparePostForClientOpts{})
|
|
assert.True(c, assert.ObjectsAreEqual([]*model.FileInfo{expectedFileInfo}, clientPost.Metadata.Files))
|
|
}, 10*time.Second, 25*time.Millisecond)
|
|
|
|
assert.Equal(t, []*model.FileInfo{expectedFileInfo}, clientPost.Metadata.Files, "should've populated Files")
|
|
})
|
|
|
|
t.Run("emojis without custom emojis enabled", func(t *testing.T) {
|
|
th := setup(t)
|
|
|
|
th.App.UpdateConfig(func(cfg *model.Config) {
|
|
*cfg.ServiceSettings.EnableCustomEmoji = false
|
|
})
|
|
|
|
emoji := th.CreateEmoji(t)
|
|
|
|
post, _, err := th.App.CreatePost(th.Context, &model.Post{
|
|
UserId: th.BasicUser.Id,
|
|
ChannelId: th.BasicChannel.Id,
|
|
Message: ":" + emoji.Name + ": :taco:",
|
|
Props: map[string]any{
|
|
model.PostPropsAttachments: []*model.MessageAttachment{
|
|
{
|
|
Text: ":" + emoji.Name + ":",
|
|
},
|
|
},
|
|
},
|
|
}, th.BasicChannel, model.CreatePostFlags{SetOnline: true})
|
|
require.Nil(t, err)
|
|
|
|
th.AddReactionToPost(t, post, th.BasicUser, "smile")
|
|
th.AddReactionToPost(t, post, th.BasicUser, "angry")
|
|
th.AddReactionToPost(t, post, th.BasicUser2, "angry")
|
|
post.HasReactions = true
|
|
|
|
clientPost := th.App.PreparePostForClient(th.Context, post, &model.PreparePostForClientOpts{})
|
|
|
|
t.Run("populates emojis", func(t *testing.T) {
|
|
assert.ElementsMatch(t, []*model.Emoji{}, clientPost.Metadata.Emojis, "should've populated empty Emojis")
|
|
})
|
|
|
|
t.Run("populates reaction counts", func(t *testing.T) {
|
|
reactions := clientPost.Metadata.Reactions
|
|
assert.Len(t, reactions, 3, "should've populated Reactions")
|
|
})
|
|
})
|
|
|
|
t.Run("emojis with custom emojis enabled", func(t *testing.T) {
|
|
th := setup(t)
|
|
|
|
th.App.UpdateConfig(func(cfg *model.Config) {
|
|
*cfg.ServiceSettings.EnableCustomEmoji = true
|
|
})
|
|
|
|
emoji1 := th.CreateEmoji(t)
|
|
emoji2 := th.CreateEmoji(t)
|
|
emoji3 := th.CreateEmoji(t)
|
|
emoji4 := th.CreateEmoji(t)
|
|
|
|
post, _, err := th.App.CreatePost(th.Context, &model.Post{
|
|
UserId: th.BasicUser.Id,
|
|
ChannelId: th.BasicChannel.Id,
|
|
Message: ":" + emoji3.Name + ": :taco:",
|
|
Props: map[string]any{
|
|
model.PostPropsAttachments: []*model.MessageAttachment{
|
|
{
|
|
Text: ":" + emoji4.Name + ":",
|
|
},
|
|
},
|
|
},
|
|
}, th.BasicChannel, model.CreatePostFlags{SetOnline: true})
|
|
require.Nil(t, err)
|
|
|
|
th.AddReactionToPost(t, post, th.BasicUser, emoji1.Name)
|
|
th.AddReactionToPost(t, post, th.BasicUser, emoji2.Name)
|
|
th.AddReactionToPost(t, post, th.BasicUser2, emoji2.Name)
|
|
th.AddReactionToPost(t, post, th.BasicUser2, "angry")
|
|
post.HasReactions = true
|
|
|
|
clientPost := th.App.PreparePostForClient(th.Context, post, &model.PreparePostForClientOpts{})
|
|
|
|
t.Run("populates emojis", func(t *testing.T) {
|
|
assert.ElementsMatch(t, []*model.Emoji{emoji1, emoji2, emoji3, emoji4}, clientPost.Metadata.Emojis, "should've populated post.Emojis")
|
|
})
|
|
|
|
t.Run("populates reaction counts", func(t *testing.T) {
|
|
reactions := clientPost.Metadata.Reactions
|
|
assert.Len(t, reactions, 4, "should've populated Reactions")
|
|
})
|
|
})
|
|
|
|
t.Run("emojis overriding profile icon", func(t *testing.T) {
|
|
th := setup(t)
|
|
|
|
prepare := func(override bool, url, emoji string) *model.Post {
|
|
th.App.UpdateConfig(func(cfg *model.Config) {
|
|
*cfg.ServiceSettings.EnablePostIconOverride = override
|
|
})
|
|
|
|
post, _, err := th.App.CreatePost(th.Context, &model.Post{
|
|
UserId: th.BasicUser.Id,
|
|
ChannelId: th.BasicChannel.Id,
|
|
Message: "Test",
|
|
}, th.BasicChannel, model.CreatePostFlags{SetOnline: true})
|
|
|
|
require.Nil(t, err)
|
|
|
|
post.AddProp(model.PostPropsOverrideIconURL, url)
|
|
post.AddProp(model.PostPropsOverrideIconEmoji, emoji)
|
|
|
|
return th.App.PreparePostForClient(th.Context, post, &model.PreparePostForClientOpts{})
|
|
}
|
|
|
|
emoji := "basketball"
|
|
url := "http://host.com/image.png"
|
|
overriddenURL := "/static/emoji/1f3c0.png"
|
|
|
|
t.Run("does not override icon URL", func(t *testing.T) {
|
|
clientPost := prepare(false, url, emoji)
|
|
|
|
s, ok := clientPost.GetProps()[model.PostPropsOverrideIconURL]
|
|
assert.True(t, ok)
|
|
assert.EqualValues(t, url, s)
|
|
s, ok = clientPost.GetProps()[model.PostPropsOverrideIconEmoji]
|
|
assert.True(t, ok)
|
|
assert.EqualValues(t, emoji, s)
|
|
})
|
|
|
|
t.Run("overrides icon URL", func(t *testing.T) {
|
|
clientPost := prepare(true, url, emoji)
|
|
|
|
s, ok := clientPost.GetProps()[model.PostPropsOverrideIconURL]
|
|
assert.True(t, ok)
|
|
assert.EqualValues(t, overriddenURL, s)
|
|
s, ok = clientPost.GetProps()[model.PostPropsOverrideIconEmoji]
|
|
assert.True(t, ok)
|
|
assert.EqualValues(t, emoji, s)
|
|
})
|
|
|
|
t.Run("overrides icon URL with name surrounded by colons", func(t *testing.T) {
|
|
colonEmoji := ":basketball:"
|
|
clientPost := prepare(true, url, colonEmoji)
|
|
|
|
s, ok := clientPost.GetProps()[model.PostPropsOverrideIconURL]
|
|
assert.True(t, ok)
|
|
assert.EqualValues(t, overriddenURL, s)
|
|
s, ok = clientPost.GetProps()[model.PostPropsOverrideIconEmoji]
|
|
assert.True(t, ok)
|
|
assert.EqualValues(t, colonEmoji, s)
|
|
})
|
|
})
|
|
|
|
t.Run("markdown image dimensions", func(t *testing.T) {
|
|
th := setup(t)
|
|
|
|
post, _, err := th.App.CreatePost(th.Context, &model.Post{
|
|
UserId: th.BasicUser.Id,
|
|
ChannelId: th.BasicChannel.Id,
|
|
Message: fmt.Sprintf("This is  and ", server.URL, server.URL),
|
|
}, th.BasicChannel, model.CreatePostFlags{SetOnline: true})
|
|
require.Nil(t, err)
|
|
|
|
clientPost := th.App.PreparePostForClient(th.Context, post, &model.PreparePostForClientOpts{})
|
|
|
|
t.Run("populates image dimensions", func(t *testing.T) {
|
|
imageDimensions := clientPost.Metadata.Images
|
|
require.Len(t, imageDimensions, 2)
|
|
assert.Equal(t, &model.PostImage{
|
|
Format: "png",
|
|
Width: 1280,
|
|
Height: 1780,
|
|
}, imageDimensions[server.URL+"/test-image2.png"])
|
|
assert.Equal(t, &model.PostImage{
|
|
Format: "png",
|
|
Width: 408,
|
|
Height: 336,
|
|
}, imageDimensions[server.URL+"/test-image1.png"])
|
|
})
|
|
})
|
|
|
|
t.Run("post props has invalid fields", func(t *testing.T) {
|
|
th := setup(t)
|
|
|
|
post, _, err := th.App.CreatePost(th.Context, &model.Post{
|
|
UserId: th.BasicUser.Id,
|
|
ChannelId: th.BasicChannel.Id,
|
|
Message: "some post",
|
|
}, th.BasicChannel, model.CreatePostFlags{SetOnline: true})
|
|
require.Nil(t, err)
|
|
|
|
// this value expected to be a string
|
|
post.AddProp(model.PostPropsOverrideIconEmoji, true)
|
|
|
|
require.NotPanics(t, func() {
|
|
_ = th.App.PreparePostForClient(th.Context, post, &model.PreparePostForClientOpts{})
|
|
})
|
|
})
|
|
|
|
t.Run("proxy linked images", func(t *testing.T) {
|
|
th := setup(t)
|
|
|
|
testProxyLinkedImage(t, th, false)
|
|
})
|
|
|
|
t.Run("proxy opengraph images", func(t *testing.T) {
|
|
th := setup(t)
|
|
|
|
testProxyOpenGraphImage(t, th, false)
|
|
})
|
|
|
|
t.Run("image embed", func(t *testing.T) {
|
|
th := setup(t)
|
|
|
|
post, _, err := th.App.CreatePost(th.Context, &model.Post{
|
|
UserId: th.BasicUser.Id,
|
|
ChannelId: th.BasicChannel.Id,
|
|
Message: `This is our logo: ` + server.URL + `/test-image2.png
|
|
And this is our icon: ` + server.URL + `/test-image1.png`,
|
|
}, th.BasicChannel, model.CreatePostFlags{SetOnline: true})
|
|
require.Nil(t, err)
|
|
post.Metadata.Embeds = nil
|
|
clientPost := th.App.PreparePostForClientWithEmbedsAndImages(th.Context, post, &model.PreparePostForClientOpts{})
|
|
|
|
// Reminder that only the first link gets an embed and dimensions
|
|
|
|
t.Run("populates embeds", func(t *testing.T) {
|
|
assert.ElementsMatch(t, []*model.PostEmbed{
|
|
{
|
|
Type: model.PostEmbedImage,
|
|
URL: server.URL + "/test-image2.png",
|
|
},
|
|
}, clientPost.Metadata.Embeds)
|
|
})
|
|
|
|
t.Run("populates image dimensions", func(t *testing.T) {
|
|
imageDimensions := clientPost.Metadata.Images
|
|
require.Len(t, imageDimensions, 1)
|
|
assert.Equal(t, &model.PostImage{
|
|
Format: "png",
|
|
Width: 1280,
|
|
Height: 1780,
|
|
}, imageDimensions[server.URL+"/test-image2.png"])
|
|
})
|
|
})
|
|
|
|
t.Run("opengraph embed", func(t *testing.T) {
|
|
th := setup(t)
|
|
|
|
post, _, err := th.App.CreatePost(th.Context, &model.Post{
|
|
UserId: th.BasicUser.Id,
|
|
ChannelId: th.BasicChannel.Id,
|
|
Message: `This is our web page: ` + server.URL,
|
|
}, th.BasicChannel, model.CreatePostFlags{SetOnline: true})
|
|
require.Nil(t, err)
|
|
|
|
clientPost := th.App.PreparePostForClient(th.Context, post, &model.PreparePostForClientOpts{})
|
|
firstEmbed := clientPost.Metadata.Embeds[0]
|
|
ogData := firstEmbed.Data.(*opengraph.OpenGraph)
|
|
|
|
t.Run("populates embeds", func(t *testing.T) {
|
|
assert.Equal(t, firstEmbed.Type, model.PostEmbedOpengraph)
|
|
assert.Equal(t, firstEmbed.URL, server.URL)
|
|
assert.Equal(t, ogData.Description, "Contribute to hmhealey/test-files development by creating an account on GitHub.")
|
|
assert.Equal(t, ogData.SiteName, "GitHub")
|
|
assert.Equal(t, ogData.Title, "hmhealey/test-files")
|
|
assert.Equal(t, ogData.Type, "object")
|
|
assert.Equal(t, ogData.URL, server.URL)
|
|
assert.Equal(t, ogData.Images[0].URL, server.URL+"/test-image3.png")
|
|
})
|
|
|
|
t.Run("populates image dimensions", func(t *testing.T) {
|
|
imageDimensions := clientPost.Metadata.Images
|
|
require.Len(t, imageDimensions, 1)
|
|
assert.Equal(t, &model.PostImage{
|
|
Format: "png",
|
|
Width: 1790,
|
|
Height: 1340,
|
|
}, imageDimensions[server.URL+"/test-image3.png"])
|
|
})
|
|
})
|
|
|
|
t.Run("opengraph unsafe links", func(t *testing.T) {
|
|
th := setup(t)
|
|
|
|
noAccessServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
assert.Fail(t, "acessed server")
|
|
}))
|
|
|
|
for _, tc := range []struct {
|
|
name string
|
|
link string
|
|
notImplemented bool
|
|
}{
|
|
{
|
|
name: "normal link",
|
|
link: "%s",
|
|
},
|
|
{
|
|
name: "normal image",
|
|
link: "%s/test-image1.png",
|
|
},
|
|
{
|
|
name: "markdown",
|
|
link: "[markdown](%s) link",
|
|
// This is because markdown links are not currently supported in the opengraph fetching code
|
|
// if you just implemented this, remove the `notImplemented` field
|
|
notImplemented: true,
|
|
},
|
|
{
|
|
name: "markdown image",
|
|
link: " link",
|
|
},
|
|
} {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
t.Run("prop set", func(t *testing.T) {
|
|
prepost := &model.Post{
|
|
UserId: th.BasicUser.Id,
|
|
ChannelId: th.BasicChannel.Id,
|
|
Message: `Bla bla bla: ` + fmt.Sprintf(tc.link, noAccessServer.URL),
|
|
}
|
|
prepost.AddProp(model.PostPropsUnsafeLinks, "true")
|
|
|
|
post, _, err := th.App.CreatePost(th.Context, prepost, th.BasicChannel, model.CreatePostFlags{SetOnline: true})
|
|
require.Nil(t, err)
|
|
|
|
clientPost := th.App.PreparePostForClient(th.Context, post, &model.PreparePostForClientOpts{})
|
|
|
|
assert.Len(t, clientPost.Metadata.Embeds, 0)
|
|
assert.Len(t, clientPost.Metadata.Images, 0)
|
|
})
|
|
if !tc.notImplemented {
|
|
t.Run("prop not set", func(t *testing.T) {
|
|
prepost := &model.Post{
|
|
UserId: th.BasicUser.Id,
|
|
ChannelId: th.BasicChannel.Id,
|
|
Message: `Bla bla bla: ` + fmt.Sprintf(tc.link, server.URL),
|
|
}
|
|
|
|
post, _, err := th.App.CreatePost(th.Context, prepost, th.BasicChannel, model.CreatePostFlags{SetOnline: true})
|
|
require.Nil(t, err)
|
|
|
|
clientPost := th.App.PreparePostForClient(th.Context, post, &model.PreparePostForClientOpts{})
|
|
|
|
assert.Greater(t, len(clientPost.Metadata.Embeds)+len(clientPost.Metadata.Images), 0)
|
|
})
|
|
}
|
|
})
|
|
}
|
|
})
|
|
|
|
t.Run("message attachment embed", func(t *testing.T) {
|
|
th := setup(t)
|
|
|
|
post, _, err := th.App.CreatePost(th.Context, &model.Post{
|
|
UserId: th.BasicUser.Id,
|
|
ChannelId: th.BasicChannel.Id,
|
|
Props: map[string]any{
|
|
model.PostPropsAttachments: []any{
|
|
map[string]any{
|
|
"text": "",
|
|
},
|
|
},
|
|
},
|
|
}, th.BasicChannel, model.CreatePostFlags{SetOnline: true})
|
|
require.Nil(t, err)
|
|
post.Metadata.Embeds = nil
|
|
clientPost := th.App.PreparePostForClientWithEmbedsAndImages(th.Context, post, &model.PreparePostForClientOpts{})
|
|
|
|
t.Run("populates embeds", func(t *testing.T) {
|
|
assert.ElementsMatch(t, []*model.PostEmbed{
|
|
{
|
|
Type: model.PostEmbedMessageAttachment,
|
|
},
|
|
}, clientPost.Metadata.Embeds)
|
|
})
|
|
|
|
t.Run("populates image dimensions", func(t *testing.T) {
|
|
imageDimensions := clientPost.Metadata.Images
|
|
require.Len(t, imageDimensions, 1)
|
|
assert.Equal(t, &model.PostImage{
|
|
Format: "png",
|
|
Width: 408,
|
|
Height: 336,
|
|
}, imageDimensions[server.URL+"/test-image1.png"])
|
|
})
|
|
})
|
|
|
|
t.Run("no metadata for deleted posts", func(t *testing.T) {
|
|
th := setup(t)
|
|
|
|
fileInfo, err := th.App.DoUploadFile(th.Context, time.Now(), th.BasicTeam.Id, th.BasicChannel.Id, th.BasicUser.Id, "test.txt", []byte("test"), true)
|
|
require.Nil(t, err)
|
|
|
|
post, _, err := th.App.CreatePost(th.Context, &model.Post{
|
|
Message: "test",
|
|
FileIds: []string{fileInfo.Id},
|
|
UserId: th.BasicUser.Id,
|
|
ChannelId: th.BasicChannel.Id,
|
|
}, th.BasicChannel, model.CreatePostFlags{SetOnline: true})
|
|
require.Nil(t, err)
|
|
post.Metadata.Embeds = nil
|
|
|
|
th.AddReactionToPost(t, post, th.BasicUser, "taco")
|
|
|
|
post, err = th.App.DeletePost(th.Context, post.Id, th.BasicUser.Id)
|
|
require.Nil(t, err)
|
|
|
|
// DeleteAt isn't set on the post returned by App.DeletePost
|
|
post.DeleteAt = model.GetMillis()
|
|
|
|
clientPost := th.App.PreparePostForClient(th.Context, post, &model.PreparePostForClientOpts{})
|
|
|
|
assert.NotEqual(t, nil, clientPost.Metadata, "should've populated Metadata“")
|
|
assert.Equal(t, "", clientPost.Message, "should've cleaned post content")
|
|
assert.Nil(t, clientPost.Metadata.Reactions, "should not have populated Reactions")
|
|
assert.Nil(t, clientPost.Metadata.Files, "should not have populated Files")
|
|
})
|
|
|
|
t.Run("permalink preview", func(t *testing.T) {
|
|
th := setup(t)
|
|
|
|
th.App.UpdateConfig(func(cfg *model.Config) {
|
|
*cfg.ServiceSettings.SiteURL = "http://mymattermost.com"
|
|
})
|
|
|
|
th.Context.Session().UserId = th.BasicUser.Id
|
|
|
|
referencedPost, _, err := th.App.CreatePost(th.Context, &model.Post{
|
|
UserId: th.BasicUser.Id,
|
|
ChannelId: th.BasicChannel.Id,
|
|
Message: "hello world",
|
|
}, th.BasicChannel, model.CreatePostFlags{SetOnline: true})
|
|
require.Nil(t, err)
|
|
referencedPost.Metadata.Embeds = nil
|
|
|
|
link := fmt.Sprintf("%s/%s/pl/%s", *th.App.Config().ServiceSettings.SiteURL, th.BasicTeam.Name, referencedPost.Id)
|
|
|
|
previewPost, _, err := th.App.CreatePost(th.Context, &model.Post{
|
|
UserId: th.BasicUser.Id,
|
|
ChannelId: th.BasicChannel.Id,
|
|
Message: link,
|
|
}, th.BasicChannel, model.CreatePostFlags{SetOnline: true})
|
|
require.Nil(t, err)
|
|
previewPost.Metadata.Embeds = nil
|
|
clientPost := th.App.PreparePostForClientWithEmbedsAndImages(th.Context, previewPost, &model.PreparePostForClientOpts{})
|
|
firstEmbed := clientPost.Metadata.Embeds[0]
|
|
preview := firstEmbed.Data.(*model.PreviewPost)
|
|
require.Equal(t, referencedPost.Id, preview.PostID)
|
|
})
|
|
|
|
t.Run("permalink preview includes post priority metadata", func(t *testing.T) {
|
|
th := setup(t)
|
|
|
|
th.App.UpdateConfig(func(cfg *model.Config) {
|
|
*cfg.ServiceSettings.SiteURL = "http://mymattermost.com"
|
|
*cfg.ServiceSettings.PostPriority = true
|
|
})
|
|
|
|
th.Context.Session().UserId = th.BasicUser.Id
|
|
|
|
referencedPost, _, err := th.App.CreatePost(th.Context, &model.Post{
|
|
UserId: th.BasicUser.Id,
|
|
ChannelId: th.BasicChannel.Id,
|
|
Message: "hello world with priority",
|
|
Metadata: &model.PostMetadata{
|
|
Priority: &model.PostPriority{
|
|
Priority: model.NewPointer(model.PostPriorityUrgent),
|
|
RequestedAck: new(true),
|
|
},
|
|
},
|
|
}, th.BasicChannel, model.CreatePostFlags{SetOnline: true})
|
|
require.Nil(t, err)
|
|
referencedPost.Metadata.Embeds = nil
|
|
|
|
link := fmt.Sprintf("%s/%s/pl/%s", *th.App.Config().ServiceSettings.SiteURL, th.BasicTeam.Name, referencedPost.Id)
|
|
|
|
previewPost, _, err := th.App.CreatePost(th.Context, &model.Post{
|
|
UserId: th.BasicUser.Id,
|
|
ChannelId: th.BasicChannel.Id,
|
|
Message: link,
|
|
}, th.BasicChannel, model.CreatePostFlags{SetOnline: true})
|
|
require.Nil(t, err)
|
|
previewPost.Metadata.Embeds = nil
|
|
|
|
clientPost := th.App.PreparePostForClientWithEmbedsAndImages(th.Context, previewPost, &model.PreparePostForClientOpts{})
|
|
firstEmbed := clientPost.Metadata.Embeds[0]
|
|
preview := firstEmbed.Data.(*model.PreviewPost)
|
|
|
|
require.Equal(t, referencedPost.Id, preview.PostID)
|
|
require.NotNil(t, preview.Post.Metadata)
|
|
require.NotNil(t, preview.Post.Metadata.Priority)
|
|
assert.Equal(t, model.PostPriorityUrgent, *preview.Post.Metadata.Priority.Priority)
|
|
assert.True(t, *preview.Post.Metadata.Priority.RequestedAck)
|
|
})
|
|
|
|
t.Run("permalink previews for direct and group messages", func(t *testing.T) {
|
|
th := setup(t)
|
|
|
|
th.App.UpdateConfig(func(cfg *model.Config) {
|
|
*cfg.ServiceSettings.SiteURL = "http://mymattermost.com"
|
|
})
|
|
|
|
th.Context.Session().UserId = th.BasicUser.Id
|
|
|
|
directChannel, err := th.App.createDirectChannel(th.Context, th.BasicUser.Id, th.BasicUser2.Id)
|
|
require.Nil(t, err)
|
|
|
|
groupChannel, err := th.App.createGroupChannel(th.Context, []string{th.BasicUser.Id, th.BasicUser2.Id, th.CreateUser(t).Id}, th.BasicUser.Id)
|
|
require.Nil(t, err)
|
|
|
|
testCases := []struct {
|
|
Description string
|
|
Channel *model.Channel
|
|
Expected model.ChannelType
|
|
}{
|
|
{
|
|
Description: "direct message permalink preview",
|
|
Channel: directChannel,
|
|
Expected: model.ChannelType("D"),
|
|
},
|
|
{
|
|
Description: "group message permalink preview",
|
|
Channel: groupChannel,
|
|
Expected: model.ChannelType("G"),
|
|
},
|
|
}
|
|
|
|
for _, testCase := range testCases {
|
|
t.Run(testCase.Description, func(t *testing.T) {
|
|
referencedPost, _, err := th.App.CreatePost(th.Context, &model.Post{
|
|
UserId: th.BasicUser.Id,
|
|
ChannelId: testCase.Channel.Id,
|
|
Message: "hello world",
|
|
}, th.BasicChannel, model.CreatePostFlags{SetOnline: true})
|
|
require.Nil(t, err)
|
|
referencedPost.Metadata.Embeds = nil
|
|
|
|
link := fmt.Sprintf("%s/%s/pl/%s", *th.App.Config().ServiceSettings.SiteURL, th.BasicTeam.Name, referencedPost.Id)
|
|
|
|
previewPost, _, err := th.App.CreatePost(th.Context, &model.Post{
|
|
UserId: th.BasicUser.Id,
|
|
ChannelId: th.BasicChannel.Id,
|
|
Message: link,
|
|
}, th.BasicChannel, model.CreatePostFlags{SetOnline: true})
|
|
require.Nil(t, err)
|
|
previewPost.Metadata.Embeds = nil
|
|
|
|
clientPost := th.App.PreparePostForClientWithEmbedsAndImages(th.Context, previewPost, &model.PreparePostForClientOpts{})
|
|
firstEmbed := clientPost.Metadata.Embeds[0]
|
|
preview := firstEmbed.Data.(*model.PreviewPost)
|
|
|
|
assert.Empty(t, preview.TeamName)
|
|
assert.Equal(t, testCase.Expected, preview.ChannelType)
|
|
})
|
|
}
|
|
})
|
|
|
|
t.Run("permalink with nested preview should have referenced post metadata", func(t *testing.T) {
|
|
th := setup(t)
|
|
|
|
th.App.UpdateConfig(func(cfg *model.Config) {
|
|
*cfg.ServiceSettings.SiteURL = "http://mymattermost.com"
|
|
})
|
|
|
|
th.Context.Session().UserId = th.BasicUser.Id
|
|
|
|
referencedPost, _, err := th.App.CreatePost(th.Context, &model.Post{
|
|
UserId: th.BasicUser.Id,
|
|
ChannelId: th.BasicChannel.Id,
|
|
Message: `This is our logo: ` + server.URL + `/test-image2.png`,
|
|
}, th.BasicChannel, model.CreatePostFlags{SetOnline: true})
|
|
require.Nil(t, err)
|
|
referencedPost.Metadata.Embeds = nil
|
|
|
|
link := fmt.Sprintf("%s/%s/pl/%s", *th.App.Config().ServiceSettings.SiteURL, th.BasicTeam.Name, referencedPost.Id)
|
|
|
|
previewPost, _, err := th.App.CreatePost(th.Context, &model.Post{
|
|
UserId: th.BasicUser.Id,
|
|
ChannelId: th.BasicChannel.Id,
|
|
Message: link,
|
|
}, th.BasicChannel, model.CreatePostFlags{SetOnline: true})
|
|
require.Nil(t, err)
|
|
previewPost.Metadata.Embeds = nil
|
|
|
|
clientPost := th.App.PreparePostForClientWithEmbedsAndImages(th.Context, previewPost, &model.PreparePostForClientOpts{})
|
|
firstEmbed := clientPost.Metadata.Embeds[0]
|
|
preview := firstEmbed.Data.(*model.PreviewPost)
|
|
referencedPostFirstEmbed := preview.Post.Metadata.Embeds[0]
|
|
|
|
require.Equal(t, referencedPost.Id, preview.PostID)
|
|
require.Equal(t, referencedPostFirstEmbed.URL, serverURL+`/test-image2.png`)
|
|
})
|
|
|
|
t.Run("permalink with nested permalink should not have referenced post metadata", func(t *testing.T) {
|
|
th := setup(t)
|
|
|
|
th.App.UpdateConfig(func(cfg *model.Config) {
|
|
*cfg.ServiceSettings.SiteURL = "http://mymattermost.com"
|
|
})
|
|
|
|
th.Context.Session().UserId = th.BasicUser.Id
|
|
|
|
nestedPermalinkPost, _, err := th.App.CreatePost(th.Context, &model.Post{
|
|
UserId: th.BasicUser.Id,
|
|
ChannelId: th.BasicChannel.Id,
|
|
Message: `This is our logo: ` + server.URL + `/test-image2.png`,
|
|
}, th.BasicChannel, model.CreatePostFlags{SetOnline: true})
|
|
require.Nil(t, err)
|
|
nestedPermalinkPost.Metadata.Embeds = nil
|
|
|
|
nestedLink := fmt.Sprintf("%s/%s/pl/%s", *th.App.Config().ServiceSettings.SiteURL, th.BasicTeam.Name, nestedPermalinkPost.Id)
|
|
|
|
referencedPost, _, err := th.App.CreatePost(th.Context, &model.Post{
|
|
UserId: th.BasicUser.Id,
|
|
ChannelId: th.BasicChannel.Id,
|
|
Message: nestedLink,
|
|
}, th.BasicChannel, model.CreatePostFlags{SetOnline: true})
|
|
require.Nil(t, err)
|
|
referencedPost.Metadata.Embeds = nil
|
|
|
|
link := fmt.Sprintf("%s/%s/pl/%s", *th.App.Config().ServiceSettings.SiteURL, th.BasicTeam.Name, referencedPost.Id)
|
|
|
|
previewPost, _, err := th.App.CreatePost(th.Context, &model.Post{
|
|
UserId: th.BasicUser.Id,
|
|
ChannelId: th.BasicChannel.Id,
|
|
Message: link,
|
|
}, th.BasicChannel, model.CreatePostFlags{SetOnline: true})
|
|
require.Nil(t, err)
|
|
previewPost.Metadata.Embeds = nil
|
|
|
|
clientPost := th.App.PreparePostForClientWithEmbedsAndImages(th.Context, previewPost, &model.PreparePostForClientOpts{})
|
|
firstEmbed := clientPost.Metadata.Embeds[0]
|
|
preview := firstEmbed.Data.(*model.PreviewPost)
|
|
referencedPostMetadata := preview.Post.Metadata
|
|
|
|
require.Equal(t, referencedPost.Id, preview.PostID)
|
|
require.Equal(t, referencedPostMetadata, (*model.PostMetadata)(nil))
|
|
})
|
|
|
|
t.Run("permalink preview renders after toggling off the feature", func(t *testing.T) {
|
|
th := setup(t)
|
|
|
|
th.App.UpdateConfig(func(cfg *model.Config) {
|
|
*cfg.ServiceSettings.SiteURL = "http://mymattermost.com"
|
|
})
|
|
|
|
th.Context.Session().UserId = th.BasicUser.Id
|
|
|
|
referencedPost, _, err := th.App.CreatePost(th.Context, &model.Post{
|
|
UserId: th.BasicUser.Id,
|
|
ChannelId: th.BasicChannel.Id,
|
|
Message: "hello world",
|
|
}, th.BasicChannel, model.CreatePostFlags{SetOnline: true})
|
|
require.Nil(t, err)
|
|
|
|
link := fmt.Sprintf("%s/%s/pl/%s", *th.App.Config().ServiceSettings.SiteURL, th.BasicTeam.Name, referencedPost.Id)
|
|
|
|
previewPost, _, err := th.App.CreatePost(th.Context, &model.Post{
|
|
UserId: th.BasicUser.Id,
|
|
ChannelId: th.BasicChannel.Id,
|
|
Message: link,
|
|
}, th.BasicChannel, model.CreatePostFlags{SetOnline: true})
|
|
require.Nil(t, err)
|
|
|
|
clientPost := th.App.PreparePostForClient(th.Context, previewPost, &model.PreparePostForClientOpts{})
|
|
firstEmbed := clientPost.Metadata.Embeds[0]
|
|
preview := firstEmbed.Data.(*model.PreviewPost)
|
|
require.Equal(t, referencedPost.Id, preview.PostID)
|
|
|
|
th.App.UpdateConfig(func(cfg *model.Config) {
|
|
*cfg.ServiceSettings.EnablePermalinkPreviews = false
|
|
})
|
|
|
|
th.App.PreparePostForClient(th.Context, previewPost, &model.PreparePostForClientOpts{})
|
|
|
|
th.App.UpdateConfig(func(cfg *model.Config) {
|
|
*cfg.ServiceSettings.EnablePermalinkPreviews = true
|
|
})
|
|
|
|
clientPost2 := th.App.PreparePostForClient(th.Context, previewPost, &model.PreparePostForClientOpts{})
|
|
firstEmbed2 := clientPost2.Metadata.Embeds[0]
|
|
preview2 := firstEmbed2.Data.(*model.PreviewPost)
|
|
require.Equal(t, referencedPost.Id, preview2.PostID)
|
|
})
|
|
}
|
|
|
|
func TestPreparePostForClientWithImageProxy(t *testing.T) {
|
|
mainHelper.Parallel(t)
|
|
setup := func(t *testing.T) *TestHelper {
|
|
th := Setup(t).InitBasic(t)
|
|
|
|
th.App.UpdateConfig(func(cfg *model.Config) {
|
|
*cfg.ServiceSettings.EnableLinkPreviews = true
|
|
*cfg.ServiceSettings.SiteURL = "http://mymattermost.com"
|
|
*cfg.ServiceSettings.AllowedUntrustedInternalConnections = "localhost,127.0.0.1"
|
|
*cfg.ImageProxySettings.Enable = true
|
|
*cfg.ImageProxySettings.ImageProxyType = "atmos/camo"
|
|
*cfg.ImageProxySettings.RemoteImageProxyURL = "https://127.0.0.1"
|
|
*cfg.ImageProxySettings.RemoteImageProxyOptions = model.NewTestPassword()
|
|
})
|
|
|
|
th.App.ch.imageProxy = imageproxy.MakeImageProxy(th.Server.platform, th.Server.HTTPService(), th.Server.Log())
|
|
|
|
return th
|
|
}
|
|
|
|
t.Run("proxy linked images", func(t *testing.T) {
|
|
th := setup(t)
|
|
|
|
testProxyLinkedImage(t, th, true)
|
|
})
|
|
|
|
t.Run("proxy opengraph images", func(t *testing.T) {
|
|
th := setup(t)
|
|
|
|
testProxyOpenGraphImage(t, th, true)
|
|
})
|
|
}
|
|
|
|
func testProxyLinkedImage(t *testing.T, th *TestHelper, shouldProxy bool) {
|
|
postTemplate := ""
|
|
imageURL := "http://mydomain.com/myimage"
|
|
proxiedImageURL := "http://mymattermost.com/api/v4/image?url=http%3A%2F%2Fmydomain.com%2Fmyimage"
|
|
|
|
post := &model.Post{
|
|
UserId: th.BasicUser.Id,
|
|
ChannelId: th.BasicChannel.Id,
|
|
Message: fmt.Sprintf(postTemplate, imageURL),
|
|
}
|
|
|
|
clientPost := th.App.PreparePostForClient(th.Context, post, &model.PreparePostForClientOpts{})
|
|
|
|
if shouldProxy {
|
|
assert.Equal(t, fmt.Sprintf(postTemplate, imageURL), post.Message, "should not have mutated original post")
|
|
assert.Equal(t, fmt.Sprintf(postTemplate, proxiedImageURL), clientPost.Message, "should've replaced linked image URLs")
|
|
} else {
|
|
assert.Equal(t, fmt.Sprintf(postTemplate, imageURL), clientPost.Message, "shouldn't have replaced linked image URLs")
|
|
}
|
|
}
|
|
|
|
func testProxyOpenGraphImage(t *testing.T, th *TestHelper, shouldProxy bool) {
|
|
var serverURL string
|
|
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
switch r.URL.Path {
|
|
case "/":
|
|
w.Header().Set("Content-Type", "text/html")
|
|
_, err := w.Write([]byte(`
|
|
<html>
|
|
<head>
|
|
<meta property="og:image" content="` + serverURL + `/test-image3.png" />
|
|
<meta property="og:site_name" content="GitHub" />
|
|
<meta property="og:type" content="object" />
|
|
<meta property="og:title" content="hmhealey/test-files" />
|
|
<meta property="og:url" content="https://github.com/hmhealey/test-files" />
|
|
<meta property="og:description" content="Contribute to hmhealey/test-files development by creating an account on GitHub." />
|
|
</head>
|
|
</html>`))
|
|
require.NoError(t, err)
|
|
case "/test-image3.png":
|
|
file, err := testutils.ReadTestFile("qa-data-graph.png")
|
|
require.NoError(t, err)
|
|
|
|
w.Header().Set("Content-Type", "image/png")
|
|
_, err = w.Write(file)
|
|
require.NoError(t, err)
|
|
default:
|
|
require.Fail(t, "Invalid path", r.URL.Path)
|
|
}
|
|
}))
|
|
serverURL = server.URL
|
|
defer server.Close()
|
|
|
|
post, _, err := th.App.CreatePost(th.Context, &model.Post{
|
|
UserId: th.BasicUser.Id,
|
|
ChannelId: th.BasicChannel.Id,
|
|
Message: `This is our web page: ` + server.URL,
|
|
}, th.BasicChannel, model.CreatePostFlags{SetOnline: true})
|
|
require.Nil(t, err)
|
|
|
|
post.Metadata.Embeds = nil
|
|
embeds := th.App.PreparePostForClientWithEmbedsAndImages(th.Context, post, &model.PreparePostForClientOpts{}).Metadata.Embeds
|
|
require.Len(t, embeds, 1, "should have one embed")
|
|
|
|
embed := embeds[0]
|
|
assert.Equal(t, model.PostEmbedOpengraph, embed.Type, "embed type should be OpenGraph")
|
|
assert.Equal(t, server.URL, embed.URL, "embed URL should be correct")
|
|
|
|
og, ok := embed.Data.(*opengraph.OpenGraph)
|
|
assert.True(t, ok, "data should be non-nil OpenGraph data")
|
|
assert.NotNil(t, og, "data should be non-nil OpenGraph data")
|
|
assert.Equal(t, "GitHub", og.SiteName, "OpenGraph data should be correctly populated")
|
|
|
|
require.Len(t, og.Images, 1, "OpenGraph data should have one image")
|
|
|
|
image := og.Images[0]
|
|
if shouldProxy {
|
|
assert.Equal(t, "", image.URL, "image URL should not be set with proxy")
|
|
assert.Equal(t, "http://mymattermost.com/api/v4/image?url="+url.QueryEscape(server.URL+"/test-image3.png"), image.SecureURL, "secure image URL should be sent through proxy")
|
|
} else {
|
|
assert.Equal(t, server.URL+"/test-image3.png", image.URL, "image URL should be set")
|
|
assert.Equal(t, "", image.SecureURL, "secure image URL should not be set")
|
|
}
|
|
}
|
|
|
|
func TestGetEmbedForPost(t *testing.T) {
|
|
mainHelper.Parallel(t)
|
|
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
if r.URL.Path == "/index.html" {
|
|
w.Header().Set("Content-Type", "text/html")
|
|
if r.Header.Get("Accept-Language") == "fr" {
|
|
w.Header().Set("Content-Language", "fr")
|
|
_, err := w.Write([]byte(`
|
|
<html>
|
|
<head>
|
|
<meta property="og:title" content="Title-FR" />
|
|
<meta property="og:description" content="Bonjour le monde" />
|
|
</head>
|
|
</html>`))
|
|
require.NoError(t, err)
|
|
} else {
|
|
_, err := w.Write([]byte(`
|
|
<html>
|
|
<head>
|
|
<meta property="og:title" content="Title" />
|
|
<meta property="og:description" content="Hello world" />
|
|
</head>
|
|
</html>`))
|
|
require.NoError(t, err)
|
|
}
|
|
} else if r.URL.Path == "/image.png" {
|
|
file, err := testutils.ReadTestFile("test.png")
|
|
require.NoError(t, err)
|
|
|
|
w.Header().Set("Content-Type", "image/png")
|
|
_, err = w.Write(file)
|
|
require.NoError(t, err)
|
|
} else if r.URL.Path == "/other" {
|
|
w.Header().Set("Content-Type", "text/html")
|
|
_, err := w.Write([]byte(`
|
|
<html>
|
|
<head>
|
|
</head>
|
|
</html>`))
|
|
require.NoError(t, err)
|
|
} else {
|
|
require.Fail(t, "Invalid path", r.URL.Path)
|
|
}
|
|
}))
|
|
defer server.Close()
|
|
|
|
ogURL := server.URL + "/index.html"
|
|
imageURL := server.URL + "/image.png"
|
|
otherURL := server.URL + "/other"
|
|
|
|
t.Run("with link previews enabled", func(t *testing.T) {
|
|
th := Setup(t)
|
|
|
|
th.App.UpdateConfig(func(cfg *model.Config) {
|
|
*cfg.ServiceSettings.AllowedUntrustedInternalConnections = "127.0.0.1"
|
|
*cfg.ServiceSettings.EnableLinkPreviews = true
|
|
})
|
|
|
|
t.Run("should return a message attachment when the post has one", func(t *testing.T) {
|
|
embed, err := th.App.getEmbedForPost(th.Context, &model.Post{
|
|
Props: model.StringInterface{
|
|
model.PostPropsAttachments: []*model.MessageAttachment{
|
|
{
|
|
Text: "test",
|
|
},
|
|
},
|
|
},
|
|
}, "", false)
|
|
|
|
assert.Equal(t, &model.PostEmbed{
|
|
Type: model.PostEmbedMessageAttachment,
|
|
}, embed)
|
|
assert.NoError(t, err)
|
|
})
|
|
|
|
t.Run("should return an image embed when the first link is an image", func(t *testing.T) {
|
|
embed, err := th.App.getEmbedForPost(th.Context, &model.Post{}, imageURL, false)
|
|
|
|
assert.Equal(t, &model.PostEmbed{
|
|
Type: model.PostEmbedImage,
|
|
URL: imageURL,
|
|
}, embed)
|
|
assert.NoError(t, err)
|
|
})
|
|
|
|
t.Run("should return an opengraph embed", func(t *testing.T) {
|
|
embed, err := th.App.getEmbedForPost(th.Context, &model.Post{}, ogURL, false)
|
|
|
|
assert.Equal(t, &model.PostEmbed{
|
|
Type: model.PostEmbedOpengraph,
|
|
URL: ogURL,
|
|
Data: &opengraph.OpenGraph{
|
|
Title: "Title",
|
|
Description: "Hello world",
|
|
},
|
|
}, embed)
|
|
assert.NoError(t, err)
|
|
})
|
|
|
|
t.Run("should return an opengraph embed in different Server Language", func(t *testing.T) {
|
|
th.App.UpdateConfig(func(cfg *model.Config) {
|
|
*cfg.LocalizationSettings.DefaultServerLocale = "fr"
|
|
})
|
|
embed, err := th.App.getEmbedForPost(th.Context, &model.Post{}, ogURL, false)
|
|
|
|
assert.Equal(t, &model.PostEmbed{
|
|
Type: model.PostEmbedOpengraph,
|
|
URL: ogURL,
|
|
Data: &opengraph.OpenGraph{
|
|
Title: "Title-FR",
|
|
Description: "Bonjour le monde",
|
|
},
|
|
}, embed)
|
|
assert.NoError(t, err)
|
|
})
|
|
|
|
t.Run("should return a link embed", func(t *testing.T) {
|
|
embed, err := th.App.getEmbedForPost(th.Context, &model.Post{}, otherURL, false)
|
|
|
|
assert.Equal(t, &model.PostEmbed{
|
|
Type: model.PostEmbedLink,
|
|
URL: otherURL,
|
|
}, embed)
|
|
assert.NoError(t, err)
|
|
})
|
|
})
|
|
|
|
t.Run("with link previews disabled", func(t *testing.T) {
|
|
th := Setup(t)
|
|
|
|
th.App.UpdateConfig(func(cfg *model.Config) {
|
|
*cfg.ServiceSettings.AllowedUntrustedInternalConnections = "127.0.0.1"
|
|
*cfg.ServiceSettings.EnableLinkPreviews = false
|
|
})
|
|
|
|
t.Run("should return an embedded message attachment", func(t *testing.T) {
|
|
embed, err := th.App.getEmbedForPost(th.Context, &model.Post{
|
|
Props: model.StringInterface{
|
|
model.PostPropsAttachments: []*model.MessageAttachment{
|
|
{
|
|
Text: "test",
|
|
},
|
|
},
|
|
},
|
|
}, "", false)
|
|
|
|
assert.Equal(t, &model.PostEmbed{
|
|
Type: model.PostEmbedMessageAttachment,
|
|
}, embed)
|
|
assert.NoError(t, err)
|
|
})
|
|
|
|
t.Run("should not return an opengraph embed", func(t *testing.T) {
|
|
embed, err := th.App.getEmbedForPost(th.Context, &model.Post{}, ogURL, false)
|
|
|
|
assert.Nil(t, embed)
|
|
assert.NoError(t, err)
|
|
})
|
|
|
|
t.Run("should not return an image embed", func(t *testing.T) {
|
|
embed, err := th.App.getEmbedForPost(th.Context, &model.Post{}, imageURL, false)
|
|
|
|
assert.Nil(t, embed)
|
|
assert.NoError(t, err)
|
|
})
|
|
|
|
t.Run("should not return a link embed", func(t *testing.T) {
|
|
embed, err := th.App.getEmbedForPost(th.Context, &model.Post{}, otherURL, false)
|
|
|
|
assert.Nil(t, embed)
|
|
assert.NoError(t, err)
|
|
})
|
|
})
|
|
}
|
|
|
|
func TestGetImagesForPost(t *testing.T) {
|
|
mainHelper.Parallel(t)
|
|
t.Run("with an image link", func(t *testing.T) {
|
|
th := Setup(t)
|
|
|
|
th.App.UpdateConfig(func(cfg *model.Config) {
|
|
*cfg.ServiceSettings.AllowedUntrustedInternalConnections = "127.0.0.1"
|
|
})
|
|
|
|
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
file, err := testutils.ReadTestFile("test.png")
|
|
require.NoError(t, err)
|
|
|
|
w.Header().Set("Content-Type", "image/png")
|
|
_, err = w.Write(file)
|
|
require.NoError(t, err)
|
|
}))
|
|
|
|
imageURL := server.URL + "/image.png"
|
|
post := &model.Post{
|
|
Message: "",
|
|
Metadata: &model.PostMetadata{},
|
|
}
|
|
|
|
images := th.App.getImagesForPost(th.Context, post, false)
|
|
|
|
assert.Equal(t, images, map[string]*model.PostImage{
|
|
imageURL: {
|
|
Format: "png",
|
|
Width: 408,
|
|
Height: 336,
|
|
},
|
|
})
|
|
})
|
|
|
|
t.Run("with an invalid image link", func(t *testing.T) {
|
|
th := Setup(t)
|
|
|
|
th.App.UpdateConfig(func(cfg *model.Config) {
|
|
*cfg.ServiceSettings.AllowedUntrustedInternalConnections = "127.0.0.1"
|
|
})
|
|
|
|
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
w.WriteHeader(http.StatusInternalServerError)
|
|
}))
|
|
|
|
imageURL := server.URL + "/bad_image.png"
|
|
post := &model.Post{
|
|
Message: "",
|
|
Metadata: &model.PostMetadata{},
|
|
}
|
|
|
|
images := th.App.getImagesForPost(th.Context, post, false)
|
|
|
|
assert.Equal(t, images, map[string]*model.PostImage{})
|
|
})
|
|
|
|
t.Run("for an OpenGraph image", func(t *testing.T) {
|
|
th := Setup(t)
|
|
|
|
th.App.UpdateConfig(func(cfg *model.Config) {
|
|
*cfg.ServiceSettings.AllowedUntrustedInternalConnections = "127.0.0.1"
|
|
})
|
|
|
|
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
if r.URL.Path == "/image.png" {
|
|
w.Header().Set("Content-Type", "image/png")
|
|
|
|
img := image.NewGray(image.Rect(0, 0, 200, 300))
|
|
|
|
var encoder png.Encoder
|
|
err := encoder.Encode(w, img)
|
|
require.NoError(t, err)
|
|
} else {
|
|
w.WriteHeader(http.StatusNotFound)
|
|
}
|
|
}))
|
|
defer server.Close()
|
|
|
|
ogURL := server.URL + "/index.html"
|
|
imageURL := server.URL + "/image.png"
|
|
|
|
post := &model.Post{
|
|
Metadata: &model.PostMetadata{
|
|
Embeds: []*model.PostEmbed{
|
|
{
|
|
Type: model.PostEmbedOpengraph,
|
|
URL: ogURL,
|
|
Data: &opengraph.OpenGraph{
|
|
Images: []*ogimage.Image{
|
|
{
|
|
URL: imageURL,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
|
|
images := th.App.getImagesForPost(th.Context, post, false)
|
|
|
|
assert.Equal(t, images, map[string]*model.PostImage{
|
|
imageURL: {
|
|
Format: "png",
|
|
Width: 200,
|
|
Height: 300,
|
|
},
|
|
})
|
|
})
|
|
|
|
t.Run("with an OpenGraph image with a secure_url", func(t *testing.T) {
|
|
th := Setup(t)
|
|
|
|
th.App.UpdateConfig(func(cfg *model.Config) {
|
|
*cfg.ServiceSettings.AllowedUntrustedInternalConnections = "127.0.0.1"
|
|
})
|
|
|
|
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
if r.URL.Path == "/secure_image.png" {
|
|
w.Header().Set("Content-Type", "image/png")
|
|
|
|
img := image.NewGray(image.Rect(0, 0, 300, 400))
|
|
|
|
var encoder png.Encoder
|
|
err := encoder.Encode(w, img)
|
|
require.NoError(t, err)
|
|
} else {
|
|
w.WriteHeader(http.StatusNotFound)
|
|
}
|
|
}))
|
|
defer server.Close()
|
|
|
|
ogURL := server.URL + "/index.html"
|
|
imageURL := server.URL + "/secure_image.png"
|
|
|
|
post := &model.Post{
|
|
Metadata: &model.PostMetadata{
|
|
Embeds: []*model.PostEmbed{
|
|
{
|
|
Type: model.PostEmbedOpengraph,
|
|
URL: ogURL,
|
|
Data: &opengraph.OpenGraph{
|
|
Images: []*ogimage.Image{
|
|
{
|
|
SecureURL: imageURL,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
|
|
images := th.App.getImagesForPost(th.Context, post, false)
|
|
|
|
assert.Equal(t, images, map[string]*model.PostImage{
|
|
imageURL: {
|
|
Format: "png",
|
|
Width: 300,
|
|
Height: 400,
|
|
},
|
|
})
|
|
})
|
|
|
|
t.Run("with an OpenGraph image with a secure_url and no dimensions", func(t *testing.T) {
|
|
th := Setup(t)
|
|
|
|
th.App.UpdateConfig(func(cfg *model.Config) {
|
|
*cfg.ServiceSettings.AllowedUntrustedInternalConnections = "127.0.0.1"
|
|
})
|
|
|
|
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
if r.URL.Path == "/secure_image.png" {
|
|
w.Header().Set("Content-Type", "image/png")
|
|
|
|
img := image.NewGray(image.Rect(0, 0, 400, 500))
|
|
|
|
var encoder png.Encoder
|
|
err := encoder.Encode(w, img)
|
|
require.NoError(t, err)
|
|
} else {
|
|
w.WriteHeader(http.StatusNotFound)
|
|
}
|
|
}))
|
|
|
|
ogURL := server.URL + "/index.html"
|
|
imageURL := server.URL + "/secure_image.png"
|
|
|
|
post := &model.Post{
|
|
Metadata: &model.PostMetadata{
|
|
Embeds: []*model.PostEmbed{
|
|
{
|
|
Type: model.PostEmbedOpengraph,
|
|
URL: ogURL,
|
|
Data: &opengraph.OpenGraph{
|
|
Images: []*ogimage.Image{
|
|
{
|
|
URL: server.URL + "/image.png",
|
|
SecureURL: imageURL,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
|
|
images := th.App.getImagesForPost(th.Context, post, false)
|
|
|
|
assert.Equal(t, images, map[string]*model.PostImage{
|
|
imageURL: {
|
|
Format: "png",
|
|
Width: 400,
|
|
Height: 500,
|
|
},
|
|
})
|
|
})
|
|
|
|
t.Run("with an invalid OpenGraph image data", func(t *testing.T) {
|
|
th := Setup(t)
|
|
|
|
th.App.UpdateConfig(func(cfg *model.Config) {
|
|
*cfg.ServiceSettings.AllowedUntrustedInternalConnections = "127.0.0.1"
|
|
})
|
|
|
|
post := &model.Post{
|
|
Metadata: &model.PostMetadata{
|
|
Embeds: []*model.PostEmbed{
|
|
{
|
|
Type: model.PostEmbedOpengraph,
|
|
Data: map[string]any{},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
|
|
images := th.App.getImagesForPost(th.Context, post, false)
|
|
assert.Equal(t, images, map[string]*model.PostImage{})
|
|
})
|
|
|
|
t.Run("should not process OpenGraph image that's a Mattermost permalink", func(t *testing.T) {
|
|
th := SetupWithStoreMock(t)
|
|
|
|
ogURL := "https://example.com/index.html"
|
|
imageURL := th.App.GetSiteURL() + "/team/pl/qwertyuiopasdfghjklzxcvbnm"
|
|
|
|
post := &model.Post{
|
|
Id: "qwertyuiopasdfghjklzxcvbnm",
|
|
Metadata: &model.PostMetadata{
|
|
Embeds: []*model.PostEmbed{
|
|
{
|
|
Type: model.PostEmbedOpengraph,
|
|
URL: ogURL,
|
|
Data: &opengraph.OpenGraph{
|
|
Images: []*ogimage.Image{
|
|
{
|
|
URL: imageURL,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
|
|
mockPostStore := mocks.PostStore{}
|
|
mockPostStore.On("GetSingle", "qwertyuiopasdfghjklzxcvbnm", false).RunFn = func(args mock.Arguments) {
|
|
assert.Fail(t, "should not have tried to process Mattermost permalink in OG image URL")
|
|
}
|
|
|
|
mockLinkMetadataStore := mocks.LinkMetadataStore{}
|
|
mockLinkMetadataStore.On("Get", mock.Anything, mock.Anything).Return(nil, store.NewErrNotFound("mock resource", "mock ID"))
|
|
|
|
mockStore := th.App.Srv().Store().(*mocks.Store)
|
|
mockStore.On("Post").Return(&mockPostStore)
|
|
mockStore.On("LinkMetadata").Return(&mockLinkMetadataStore)
|
|
|
|
images := th.App.getImagesForPost(th.Context, post, false)
|
|
assert.Equal(t, 0, len(images))
|
|
assert.Equal(t, images, map[string]*model.PostImage{})
|
|
})
|
|
|
|
t.Run("with message attachment image URLs", func(t *testing.T) {
|
|
th := Setup(t)
|
|
|
|
th.App.UpdateConfig(func(cfg *model.Config) {
|
|
*cfg.ServiceSettings.AllowedUntrustedInternalConnections = "127.0.0.1"
|
|
})
|
|
|
|
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
file, err := testutils.ReadTestFile("test.png")
|
|
require.NoError(t, err)
|
|
|
|
w.Header().Set("Content-Type", "image/png")
|
|
_, err = w.Write(file)
|
|
require.NoError(t, err)
|
|
}))
|
|
defer server.Close()
|
|
|
|
imageURL := server.URL + "/attachment.png"
|
|
thumbURL := server.URL + "/thumb.png"
|
|
authorIconURL := server.URL + "/author.png"
|
|
footerIconURL := server.URL + "/footer.png"
|
|
post := &model.Post{
|
|
Metadata: &model.PostMetadata{},
|
|
Props: model.StringInterface{
|
|
model.PostPropsAttachments: []*model.MessageAttachment{
|
|
{
|
|
ImageURL: imageURL,
|
|
ThumbURL: thumbURL,
|
|
AuthorIcon: authorIconURL,
|
|
FooterIcon: footerIconURL,
|
|
},
|
|
},
|
|
},
|
|
}
|
|
|
|
images := th.App.getImagesForPost(th.Context, post, false)
|
|
|
|
expected := &model.PostImage{
|
|
Format: "png",
|
|
Width: 408,
|
|
Height: 336,
|
|
}
|
|
assert.Equal(t, map[string]*model.PostImage{
|
|
imageURL: expected,
|
|
thumbURL: expected,
|
|
authorIconURL: expected,
|
|
footerIconURL: expected,
|
|
}, images)
|
|
})
|
|
|
|
t.Run("skips all when unsafe links including interactive props", func(t *testing.T) {
|
|
th := Setup(t)
|
|
|
|
th.App.UpdateConfig(func(cfg *model.Config) {
|
|
*cfg.ServiceSettings.AllowedUntrustedInternalConnections = "127.0.0.1"
|
|
})
|
|
|
|
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
t.Fatalf("unexpected HTTP request to test server: %s %s", r.Method, r.URL.String())
|
|
}))
|
|
t.Cleanup(server.Close)
|
|
|
|
post := &model.Post{
|
|
Metadata: &model.PostMetadata{
|
|
Embeds: []*model.PostEmbed{
|
|
{Type: model.PostEmbedImage, URL: server.URL + "/embed.png"},
|
|
},
|
|
},
|
|
Props: model.StringInterface{
|
|
model.PostPropsUnsafeLinks: "true",
|
|
model.PostPropsMmBlocks: []any{
|
|
map[string]any{"type": "image", "url": server.URL + "/mm1.png"},
|
|
map[string]any{"type": "image", "url": server.URL + "/mm2.png"},
|
|
},
|
|
model.PostPropsBlockKitBlocks: []any{
|
|
map[string]any{"type": "image", "image_url": server.URL + "/bk1.png"},
|
|
map[string]any{
|
|
"type": "section",
|
|
"accessory": map[string]any{
|
|
"type": "image",
|
|
"image_url": server.URL + "/bk2.png",
|
|
},
|
|
},
|
|
},
|
|
model.PostPropsAdaptiveCards: []any{
|
|
map[string]any{
|
|
"type": "AdaptiveCard",
|
|
"body": []any{
|
|
map[string]any{"type": "Image", "url": server.URL + "/ac1.png"},
|
|
map[string]any{"type": "Image", "url": server.URL + "/ac2.png"},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
|
|
out := th.App.getImagesForPost(th.Context, post, false)
|
|
assert.Empty(t, out)
|
|
})
|
|
|
|
t.Run("skips interactive prop images when mmBlocksEnabled is false", func(t *testing.T) {
|
|
th := Setup(t)
|
|
|
|
th.ConfigStore.SetReadOnlyFF(false)
|
|
defer th.ConfigStore.SetReadOnlyFF(true)
|
|
|
|
th.App.UpdateConfig(func(cfg *model.Config) {
|
|
*cfg.ServiceSettings.AllowedUntrustedInternalConnections = "127.0.0.1"
|
|
cfg.FeatureFlags.MmBlocksEnabled = false
|
|
})
|
|
defer th.App.UpdateConfig(func(cfg *model.Config) { cfg.FeatureFlags.MmBlocksEnabled = true })
|
|
|
|
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
switch r.URL.Path {
|
|
case "/attach.png":
|
|
file, err := testutils.ReadTestFile("test.png")
|
|
require.NoError(t, err)
|
|
w.Header().Set("Content-Type", "image/png")
|
|
_, err = w.Write(file)
|
|
require.NoError(t, err)
|
|
default:
|
|
t.Fatalf("unexpected HTTP request: %s", r.URL.String())
|
|
}
|
|
}))
|
|
defer server.Close()
|
|
|
|
attachURL := server.URL + "/attach.png"
|
|
post := &model.Post{
|
|
Metadata: &model.PostMetadata{},
|
|
Props: model.StringInterface{
|
|
model.PostPropsMmBlocks: []any{
|
|
map[string]any{"type": "image", "url": server.URL + "/mm.png"},
|
|
},
|
|
model.PostPropsBlockKitBlocks: []any{
|
|
map[string]any{"type": "image", "image_url": server.URL + "/bk.png"},
|
|
},
|
|
model.PostPropsAdaptiveCards: []any{
|
|
map[string]any{
|
|
"type": "AdaptiveCard",
|
|
"body": []any{
|
|
map[string]any{"type": "Image", "url": server.URL + "/ac.png"},
|
|
},
|
|
},
|
|
},
|
|
model.PostPropsAttachments: []*model.MessageAttachment{
|
|
{ImageURL: attachURL},
|
|
},
|
|
},
|
|
}
|
|
|
|
images := th.App.getImagesForPost(th.Context, post, false)
|
|
require.Len(t, images, 1)
|
|
require.Contains(t, images, attachURL)
|
|
})
|
|
}
|
|
|
|
func TestGetEmojiNamesForString(t *testing.T) {
|
|
mainHelper.Parallel(t)
|
|
testCases := []struct {
|
|
Description string
|
|
Input string
|
|
Expected []string
|
|
}{
|
|
{
|
|
Description: "no emojis",
|
|
Input: "this is a string",
|
|
Expected: []string{},
|
|
},
|
|
{
|
|
Description: "one emoji",
|
|
Input: "this is an :emoji1: string",
|
|
Expected: []string{"emoji1"},
|
|
},
|
|
{
|
|
Description: "two emojis",
|
|
Input: "this is a :emoji3: :emoji2: string",
|
|
Expected: []string{"emoji3", "emoji2"},
|
|
},
|
|
{
|
|
Description: "punctuation around emojis",
|
|
Input: ":emoji3:/:emoji1: (:emoji2:)",
|
|
Expected: []string{"emoji3", "emoji1", "emoji2"},
|
|
},
|
|
{
|
|
Description: "adjacent emojis",
|
|
Input: ":emoji3::emoji1:",
|
|
Expected: []string{"emoji3", "emoji1"},
|
|
},
|
|
{
|
|
Description: "duplicate emojis",
|
|
Input: ":emoji1: :emoji1: :emoji1::emoji2::emoji2: :emoji1:",
|
|
Expected: []string{"emoji1", "emoji1", "emoji1", "emoji2", "emoji2", "emoji1"},
|
|
},
|
|
{
|
|
Description: "fake emojis",
|
|
Input: "these don't exist :tomato: :potato: :rotato:",
|
|
Expected: []string{"tomato", "potato", "rotato"},
|
|
},
|
|
}
|
|
|
|
for _, testCase := range testCases {
|
|
t.Run(testCase.Description, func(t *testing.T) {
|
|
emojis := getEmojiNamesForString(testCase.Input)
|
|
assert.ElementsMatch(t, emojis, testCase.Expected, "received incorrect emoji names")
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestGetEmojiNamesForPost(t *testing.T) {
|
|
mainHelper.Parallel(t)
|
|
testCases := []struct {
|
|
Description string
|
|
Post *model.Post
|
|
Reactions []*model.Reaction
|
|
Expected []string
|
|
}{
|
|
{
|
|
Description: "no emojis",
|
|
Post: &model.Post{
|
|
Message: "this is a post",
|
|
},
|
|
Expected: []string{},
|
|
},
|
|
{
|
|
Description: "in post message",
|
|
Post: &model.Post{
|
|
Message: "this is :emoji:",
|
|
},
|
|
Expected: []string{"emoji"},
|
|
},
|
|
{
|
|
Description: "in reactions",
|
|
Post: &model.Post{},
|
|
Reactions: []*model.Reaction{
|
|
{
|
|
EmojiName: "emoji1",
|
|
},
|
|
{
|
|
EmojiName: "emoji2",
|
|
},
|
|
},
|
|
Expected: []string{"emoji1", "emoji2"},
|
|
},
|
|
{
|
|
Description: "in message attachments",
|
|
Post: &model.Post{
|
|
Message: "this is a post",
|
|
Props: map[string]any{
|
|
model.PostPropsAttachments: []*model.MessageAttachment{
|
|
{
|
|
Text: ":emoji1:",
|
|
Pretext: ":emoji2:",
|
|
},
|
|
{
|
|
Fields: []*model.MessageAttachmentField{
|
|
{
|
|
Value: ":emoji3:",
|
|
},
|
|
{
|
|
Value: ":emoji4:",
|
|
},
|
|
},
|
|
},
|
|
{
|
|
Title: "This is the title: :emoji5:",
|
|
},
|
|
},
|
|
},
|
|
},
|
|
Expected: []string{"emoji1", "emoji2", "emoji3", "emoji4", "emoji5"},
|
|
},
|
|
{
|
|
Description: "with duplicates",
|
|
Post: &model.Post{
|
|
Message: "this is :emoji1",
|
|
Props: map[string]any{
|
|
model.PostPropsAttachments: []*model.MessageAttachment{
|
|
{
|
|
Text: ":emoji2:",
|
|
Pretext: ":emoji2:",
|
|
Fields: []*model.MessageAttachmentField{
|
|
{
|
|
Value: ":emoji3:",
|
|
},
|
|
{
|
|
Value: ":emoji1:",
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
Expected: []string{"emoji1", "emoji2", "emoji3"},
|
|
},
|
|
{
|
|
Description: "in interactive mm_blocks, blocks, and cards",
|
|
Post: &model.Post{
|
|
Message: "body :smile:",
|
|
Props: map[string]any{
|
|
model.PostPropsMmBlocks: []any{
|
|
map[string]any{"type": "text", "text": "mm :coffee:"},
|
|
map[string]any{"type": "button", "text": "btn :thumbsup:", "action_id": "a"},
|
|
},
|
|
model.PostPropsBlockKitBlocks: []any{
|
|
map[string]any{
|
|
"type": "section",
|
|
"text": map[string]any{
|
|
"type": "mrkdwn",
|
|
"text": "block kit :rocket:",
|
|
},
|
|
},
|
|
},
|
|
model.PostPropsAdaptiveCards: []any{
|
|
map[string]any{
|
|
"type": "AdaptiveCard",
|
|
"body": []any{
|
|
map[string]any{"type": "TextBlock", "text": "card :tada:"},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
// Button labels are not included in Post.AllStrings (see model.TestPost_AllStrings_interactiveProps).
|
|
Expected: []string{"smile", "coffee", "rocket", "tada"},
|
|
},
|
|
}
|
|
|
|
for _, testCase := range testCases {
|
|
t.Run(testCase.Description, func(t *testing.T) {
|
|
emojis := getEmojiNamesForPost(testCase.Post, testCase.Reactions, true)
|
|
assert.ElementsMatch(t, emojis, testCase.Expected, "received incorrect emoji names")
|
|
})
|
|
}
|
|
|
|
t.Run("omits interactive block emojis when mmBlocksEnabled is false", func(t *testing.T) {
|
|
post := &model.Post{
|
|
Message: "body :smile:",
|
|
Props: map[string]any{
|
|
model.PostPropsMmBlocks: []any{
|
|
map[string]any{"type": "text", "text": "mm :coffee:"},
|
|
},
|
|
model.PostPropsBlockKitBlocks: []any{
|
|
map[string]any{
|
|
"type": "section",
|
|
"text": map[string]any{
|
|
"type": "mrkdwn",
|
|
"text": "block kit :rocket:",
|
|
},
|
|
},
|
|
},
|
|
model.PostPropsAdaptiveCards: []any{
|
|
map[string]any{
|
|
"type": "AdaptiveCard",
|
|
"body": []any{
|
|
map[string]any{"type": "TextBlock", "text": "card :tada:"},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
reactions := []*model.Reaction{{EmojiName: "wave"}}
|
|
|
|
emojis := getEmojiNamesForPost(post, reactions, false)
|
|
|
|
assert.ElementsMatch(t, []string{"smile", "wave"}, emojis)
|
|
})
|
|
}
|
|
|
|
func TestGetCustomEmojisForPost(t *testing.T) {
|
|
mainHelper.Parallel(t)
|
|
th := Setup(t).InitBasic(t)
|
|
|
|
th.App.UpdateConfig(func(cfg *model.Config) {
|
|
*cfg.ServiceSettings.EnableCustomEmoji = true
|
|
})
|
|
|
|
emojis := []*model.Emoji{
|
|
th.CreateEmoji(t),
|
|
th.CreateEmoji(t),
|
|
th.CreateEmoji(t),
|
|
th.CreateEmoji(t),
|
|
th.CreateEmoji(t),
|
|
th.CreateEmoji(t),
|
|
}
|
|
|
|
t.Run("from different parts of the post", func(t *testing.T) {
|
|
reactions := []*model.Reaction{
|
|
{
|
|
UserId: th.BasicUser.Id,
|
|
EmojiName: emojis[0].Name,
|
|
},
|
|
}
|
|
|
|
post := &model.Post{
|
|
Message: ":" + emojis[1].Name + ":",
|
|
Props: map[string]any{
|
|
model.PostPropsAttachments: []*model.MessageAttachment{
|
|
{
|
|
Pretext: ":" + emojis[2].Name + ":",
|
|
Text: ":" + emojis[3].Name + ":",
|
|
Fields: []*model.MessageAttachmentField{
|
|
{
|
|
Value: ":" + emojis[4].Name + ":",
|
|
},
|
|
{
|
|
Value: ":" + emojis[5].Name + ":",
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
|
|
emojisForPost, err := th.App.getCustomEmojisForPost(th.Context, post, reactions)
|
|
assert.Nil(t, err, "failed to get emojis for post")
|
|
assert.ElementsMatch(t, emojisForPost, emojis, "received incorrect emojis")
|
|
})
|
|
|
|
t.Run("with emojis that don't exist", func(t *testing.T) {
|
|
post := &model.Post{
|
|
Message: ":secret: :" + emojis[0].Name + ":",
|
|
Props: map[string]any{
|
|
model.PostPropsAttachments: []*model.MessageAttachment{
|
|
{
|
|
Text: ":imaginary:",
|
|
},
|
|
},
|
|
},
|
|
}
|
|
|
|
emojisForPost, err := th.App.getCustomEmojisForPost(th.Context, post, nil)
|
|
assert.Nil(t, err, "failed to get emojis for post")
|
|
assert.ElementsMatch(t, emojisForPost, []*model.Emoji{emojis[0]}, "received incorrect emojis")
|
|
})
|
|
|
|
t.Run("with no emojis", func(t *testing.T) {
|
|
post := &model.Post{
|
|
Message: "this post is boring",
|
|
Props: map[string]any{},
|
|
}
|
|
|
|
emojisForPost, err := th.App.getCustomEmojisForPost(th.Context, post, nil)
|
|
assert.Nil(t, err, "failed to get emojis for post")
|
|
assert.ElementsMatch(t, emojisForPost, []*model.Emoji{}, "should have received no emojis")
|
|
})
|
|
|
|
t.Run("includes interactive block emojis when mmBlocksEnabled is true", func(t *testing.T) {
|
|
th.ConfigStore.SetReadOnlyFF(false)
|
|
defer th.ConfigStore.SetReadOnlyFF(true)
|
|
|
|
th.App.UpdateConfig(func(cfg *model.Config) { cfg.FeatureFlags.MmBlocksEnabled = true })
|
|
defer th.App.UpdateConfig(func(cfg *model.Config) { cfg.FeatureFlags.MmBlocksEnabled = true })
|
|
|
|
messageEmoji := emojis[0]
|
|
blockEmoji := th.CreateEmoji(t)
|
|
|
|
post := &model.Post{
|
|
Message: ":" + messageEmoji.Name + ":",
|
|
Props: map[string]any{
|
|
model.PostPropsMmBlocks: []any{
|
|
map[string]any{"type": "text", "text": "mm :" + blockEmoji.Name + ":"},
|
|
},
|
|
},
|
|
}
|
|
|
|
emojisForPost, err := th.App.getCustomEmojisForPost(th.Context, post, nil)
|
|
require.Nil(t, err)
|
|
assert.ElementsMatch(t, []*model.Emoji{messageEmoji, blockEmoji}, emojisForPost)
|
|
})
|
|
|
|
t.Run("omits interactive block emojis when mmBlocksEnabled is false", func(t *testing.T) {
|
|
th.ConfigStore.SetReadOnlyFF(false)
|
|
defer th.ConfigStore.SetReadOnlyFF(true)
|
|
|
|
th.App.UpdateConfig(func(cfg *model.Config) { cfg.FeatureFlags.MmBlocksEnabled = false })
|
|
defer th.App.UpdateConfig(func(cfg *model.Config) { cfg.FeatureFlags.MmBlocksEnabled = true })
|
|
|
|
messageEmoji := emojis[0]
|
|
blockEmoji := th.CreateEmoji(t)
|
|
|
|
post := &model.Post{
|
|
Message: ":" + messageEmoji.Name + ":",
|
|
Props: map[string]any{
|
|
model.PostPropsMmBlocks: []any{
|
|
map[string]any{"type": "text", "text": "mm :" + blockEmoji.Name + ":"},
|
|
},
|
|
},
|
|
}
|
|
|
|
emojisForPost, err := th.App.getCustomEmojisForPost(th.Context, post, nil)
|
|
require.Nil(t, err)
|
|
assert.ElementsMatch(t, []*model.Emoji{messageEmoji}, emojisForPost)
|
|
})
|
|
}
|
|
|
|
func TestGetFirstLink(t *testing.T) {
|
|
mainHelper.Parallel(t)
|
|
th := Setup(t).InitBasic(t)
|
|
|
|
for name, testCase := range map[string]struct {
|
|
Input string
|
|
ExpectedFirstLink string
|
|
ExpectedImages []string
|
|
}{
|
|
"no links or images": {
|
|
Input: "this is a string",
|
|
ExpectedFirstLink: "",
|
|
ExpectedImages: []string{},
|
|
},
|
|
"http link": {
|
|
Input: "this is a http://example.com",
|
|
ExpectedFirstLink: "http://example.com",
|
|
ExpectedImages: []string{},
|
|
},
|
|
"www link": {
|
|
Input: "this is a www.example.com",
|
|
ExpectedFirstLink: "http://www.example.com",
|
|
ExpectedImages: []string{},
|
|
},
|
|
"image": {
|
|
Input: "this is a ",
|
|
ExpectedFirstLink: "",
|
|
ExpectedImages: []string{"http://example.com/logo"},
|
|
},
|
|
"multiple images": {
|
|
Input: "this is a  and  and ",
|
|
ExpectedFirstLink: "",
|
|
ExpectedImages: []string{"http://example.com/logo", "http://example.com/logo2", "http://example.com/logo3"},
|
|
},
|
|
"multiple images with duplicate": {
|
|
Input: "this is a  and  and ",
|
|
ExpectedFirstLink: "",
|
|
ExpectedImages: []string{"http://example.com/logo", "http://example.com/logo2", "http://example.com/logo2"},
|
|
},
|
|
"reference image": {
|
|
Input: `this is a ![our logo][logo]
|
|
|
|
[logo]: http://example.com/logo`,
|
|
ExpectedFirstLink: "",
|
|
ExpectedImages: []string{"http://example.com/logo"},
|
|
},
|
|
"image and link": {
|
|
Input: "this is a https://example.com and ",
|
|
ExpectedFirstLink: "https://example.com",
|
|
ExpectedImages: []string{"https://example.com/logo"},
|
|
},
|
|
"markdown links (not returned)": {
|
|
Input: `this is a [our page](http://example.com) and [another page][]
|
|
|
|
[another page]: http://www.example.com/another_page`,
|
|
ExpectedFirstLink: "",
|
|
ExpectedImages: []string{},
|
|
},
|
|
"http link in angle brackets": {
|
|
Input: "this is a <http://example.com>",
|
|
ExpectedFirstLink: "http://example.com",
|
|
ExpectedImages: []string{},
|
|
},
|
|
"http link with only opening angle bracket": {
|
|
Input: "this is a <http://example.com",
|
|
ExpectedFirstLink: "http://example.com",
|
|
ExpectedImages: []string{},
|
|
},
|
|
"http link with only closing angle bracket": {
|
|
Input: "this is a http://example.com>",
|
|
ExpectedFirstLink: "http://example.com",
|
|
ExpectedImages: []string{},
|
|
},
|
|
} {
|
|
t.Run(name, func(t *testing.T) {
|
|
assert.Equal(t, testCase.ExpectedFirstLink, th.App.getFirstLink(th.Context, testCase.Input))
|
|
assert.Equal(t, testCase.ExpectedImages, th.App.getImages(th.Context, testCase.Input))
|
|
})
|
|
}
|
|
|
|
for name, testCase := range map[string]struct {
|
|
Input string
|
|
ExpectedFirstLink string
|
|
ExpectedImages []string
|
|
}{
|
|
"http link domain is restricted": {
|
|
Input: "this is a http://example.com",
|
|
ExpectedFirstLink: "",
|
|
ExpectedImages: []string{},
|
|
},
|
|
"http link domain is not restricted": {
|
|
Input: "this is a http://example1.com",
|
|
ExpectedFirstLink: "http://example1.com",
|
|
ExpectedImages: []string{},
|
|
},
|
|
"www link domain is restricted": {
|
|
Input: "this is a www.example.com",
|
|
ExpectedFirstLink: "",
|
|
ExpectedImages: []string{},
|
|
},
|
|
"image domain is restricted": {
|
|
Input: "this is a ",
|
|
ExpectedFirstLink: "",
|
|
ExpectedImages: []string{},
|
|
},
|
|
"image domain is not restricted": {
|
|
Input: "this is a ",
|
|
ExpectedFirstLink: "",
|
|
ExpectedImages: []string{"http://example1.com/logo"},
|
|
},
|
|
"multiple images is domain restricted": {
|
|
Input: "this is a  and  and ",
|
|
ExpectedFirstLink: "",
|
|
ExpectedImages: []string{},
|
|
},
|
|
"multiple images domain is not restricted": {
|
|
Input: "this is a  and  and ",
|
|
ExpectedFirstLink: "",
|
|
ExpectedImages: []string{"http://example1.com/logo", "http://example1.com/logo2", "http://example1.com/logo3"},
|
|
},
|
|
"multiple images with duplicate domain is restricted": {
|
|
Input: "this is a  and  and ",
|
|
ExpectedFirstLink: "",
|
|
ExpectedImages: []string{},
|
|
},
|
|
"reference image domain is restricted": {
|
|
Input: `this is a ![our logo][logo]
|
|
|
|
[logo]: http://example.com/logo`,
|
|
ExpectedFirstLink: "",
|
|
ExpectedImages: []string{},
|
|
},
|
|
"image and link domain is restricted": {
|
|
Input: "this is a https://example.com and ",
|
|
ExpectedFirstLink: "",
|
|
ExpectedImages: []string{},
|
|
},
|
|
"test with denied domain as username, password": {
|
|
Input: "link as http://example.com:[email protected]/link1",
|
|
ExpectedFirstLink: "http://example.com:[email protected]/link1",
|
|
ExpectedImages: []string{},
|
|
},
|
|
"test with URL encoded": {
|
|
Input: "link as https://example%E3%80%82com/link1",
|
|
ExpectedFirstLink: "",
|
|
ExpectedImages: []string{},
|
|
},
|
|
"test with unicode": {
|
|
Input: "link as https://example。com/link1",
|
|
ExpectedFirstLink: "",
|
|
ExpectedImages: []string{},
|
|
},
|
|
} {
|
|
th.App.UpdateConfig(func(cfg *model.Config) {
|
|
*cfg.ServiceSettings.RestrictLinkPreviews = "example.com, test.com"
|
|
})
|
|
|
|
t.Run(name, func(t *testing.T) {
|
|
assert.Equal(t, testCase.ExpectedFirstLink, th.App.getFirstLink(th.Context, testCase.Input))
|
|
assert.Equal(t, testCase.ExpectedImages, th.App.getImages(th.Context, testCase.Input))
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestGetLinkMetadata(t *testing.T) {
|
|
setup := func(t *testing.T) *TestHelper {
|
|
th := Setup(t)
|
|
|
|
th.App.UpdateConfig(func(cfg *model.Config) {
|
|
*cfg.ServiceSettings.AllowedUntrustedInternalConnections = "127.0.0.1"
|
|
})
|
|
|
|
err := platform.PurgeLinkCache()
|
|
require.NoError(t, err)
|
|
|
|
return th
|
|
}
|
|
|
|
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
params := r.URL.Query()
|
|
|
|
writeImage := func(height, width int) {
|
|
img := image.NewGray(image.Rect(0, 0, height, width))
|
|
|
|
var encoder png.Encoder
|
|
|
|
err := encoder.Encode(w, img)
|
|
require.NoError(t, err)
|
|
}
|
|
|
|
writeHTML := func(title string) {
|
|
w.Header().Set("Content-Type", "text/html")
|
|
|
|
_, err := w.Write([]byte(`
|
|
<html prefix="og:http://ogp.me/ns#">
|
|
<head>
|
|
<meta property="og:title" content="` + title + `" />
|
|
</head>
|
|
<body>
|
|
</body>
|
|
</html>`))
|
|
require.NoError(t, err)
|
|
}
|
|
|
|
if strings.HasPrefix(r.URL.Path, "/image") {
|
|
height, _ := strconv.ParseInt(params["height"][0], 10, 0)
|
|
width, _ := strconv.ParseInt(params["width"][0], 10, 0)
|
|
|
|
writeImage(int(height), int(width))
|
|
} else if strings.HasPrefix(r.URL.Path, "/opengraph") {
|
|
writeHTML(params["title"][0])
|
|
} else if strings.HasPrefix(r.URL.Path, "/json") {
|
|
w.Header().Set("Content-Type", "application/json")
|
|
|
|
_, err := w.Write([]byte("true"))
|
|
require.NoError(t, err)
|
|
} else if strings.HasPrefix(r.URL.Path, "/timeout") {
|
|
w.Header().Set("Content-Type", "text/html")
|
|
|
|
_, err := w.Write([]byte("<html>"))
|
|
require.NoError(t, err)
|
|
select {
|
|
case <-time.After(60 * time.Second):
|
|
case <-r.Context().Done():
|
|
}
|
|
_, err = w.Write([]byte("</html>"))
|
|
require.NoError(t, err)
|
|
} else if strings.HasPrefix(r.URL.Path, "/mixed") {
|
|
for _, acceptedType := range r.Header["Accept"] {
|
|
if strings.HasPrefix(acceptedType, "image/*") || strings.HasPrefix(acceptedType, "image/png") {
|
|
writeImage(10, 10)
|
|
} else if strings.HasPrefix(acceptedType, "text/html") {
|
|
writeHTML("mixed")
|
|
}
|
|
}
|
|
} else {
|
|
w.WriteHeader(http.StatusInternalServerError)
|
|
}
|
|
}))
|
|
defer server.Close()
|
|
|
|
t.Run("in-memory cache", func(t *testing.T) {
|
|
th := setup(t)
|
|
|
|
requestURL := server.URL + "/cached"
|
|
timestamp := int64(1547510400000)
|
|
title := "from cache"
|
|
|
|
cacheLinkMetadata(th.Context, requestURL, timestamp, &opengraph.OpenGraph{Title: title}, nil, nil)
|
|
|
|
t.Run("should use cache if cached entry exists", func(t *testing.T) {
|
|
_, _, _, ok := getLinkMetadataFromCache(requestURL, timestamp)
|
|
require.True(t, ok, "data should already exist in in-memory cache")
|
|
|
|
_, _, ok = th.App.getLinkMetadataFromDatabase(requestURL, timestamp)
|
|
require.False(t, ok, "data should not exist in database")
|
|
|
|
og, img, _, err := th.App.getLinkMetadata(th.Context, requestURL, timestamp, false, "")
|
|
|
|
require.NotNil(t, og)
|
|
assert.Nil(t, img)
|
|
assert.NoError(t, err)
|
|
assert.Equal(t, title, og.Title)
|
|
})
|
|
|
|
t.Run("should use cache if cached entry exists near time", func(t *testing.T) {
|
|
_, _, _, ok := getLinkMetadataFromCache(requestURL, timestamp)
|
|
require.True(t, ok, "data should already exist in in-memory cache")
|
|
|
|
_, _, ok = th.App.getLinkMetadataFromDatabase(requestURL, timestamp)
|
|
require.False(t, ok, "data should not exist in database")
|
|
|
|
og, img, _, err := th.App.getLinkMetadata(th.Context, requestURL, timestamp+60*1000, false, "")
|
|
|
|
require.NotNil(t, og)
|
|
assert.Nil(t, img)
|
|
assert.NoError(t, err)
|
|
assert.Equal(t, title, og.Title)
|
|
})
|
|
|
|
t.Run("should not use cache if URL is different", func(t *testing.T) {
|
|
differentURL := server.URL + "/other"
|
|
|
|
_, _, _, ok := getLinkMetadataFromCache(differentURL, timestamp)
|
|
require.False(t, ok, "data should not exist in in-memory cache")
|
|
|
|
_, _, ok = th.App.getLinkMetadataFromDatabase(differentURL, timestamp)
|
|
require.False(t, ok, "data should not exist in database")
|
|
|
|
og, img, _, err := th.App.getLinkMetadata(th.Context, differentURL, timestamp, false, "")
|
|
|
|
assert.Nil(t, og)
|
|
assert.Nil(t, img)
|
|
assert.NoError(t, err)
|
|
})
|
|
|
|
t.Run("should not use cache if timestamp is different", func(t *testing.T) {
|
|
differentTimestamp := timestamp + 60*60*1000
|
|
|
|
_, _, _, ok := getLinkMetadataFromCache(requestURL, differentTimestamp)
|
|
require.False(t, ok, "data should not exist in in-memory cache")
|
|
|
|
_, _, ok = th.App.getLinkMetadataFromDatabase(requestURL, differentTimestamp)
|
|
require.False(t, ok, "data should not exist in database")
|
|
|
|
og, img, _, err := th.App.getLinkMetadata(th.Context, requestURL, differentTimestamp, false, "")
|
|
|
|
assert.Nil(t, og)
|
|
assert.Nil(t, img)
|
|
assert.NoError(t, err)
|
|
})
|
|
})
|
|
|
|
t.Run("database cache", func(t *testing.T) {
|
|
th := setup(t)
|
|
|
|
requestURL := server.URL
|
|
timestamp := int64(1547510400000)
|
|
title := "from database"
|
|
|
|
th.App.saveLinkMetadataToDatabase(requestURL, timestamp, &opengraph.OpenGraph{Title: title}, nil)
|
|
|
|
t.Run("should use database if saved entry exists", func(t *testing.T) {
|
|
err := platform.PurgeLinkCache()
|
|
require.NoError(t, err)
|
|
|
|
_, _, _, ok := getLinkMetadataFromCache(requestURL, timestamp)
|
|
require.False(t, ok, "data should not exist in in-memory cache")
|
|
|
|
_, _, ok = th.App.getLinkMetadataFromDatabase(requestURL, timestamp)
|
|
require.True(t, ok, "data should already exist in database")
|
|
|
|
og, img, _, err := th.App.getLinkMetadata(th.Context, requestURL, timestamp, false, "")
|
|
|
|
require.NotNil(t, og)
|
|
assert.Nil(t, img)
|
|
assert.NoError(t, err)
|
|
assert.Equal(t, title, og.Title)
|
|
})
|
|
|
|
t.Run("should use database if saved entry exists near time", func(t *testing.T) {
|
|
err := platform.PurgeLinkCache()
|
|
require.NoError(t, err)
|
|
|
|
_, _, _, ok := getLinkMetadataFromCache(requestURL, timestamp)
|
|
require.False(t, ok, "data should not exist in in-memory cache")
|
|
|
|
_, _, ok = th.App.getLinkMetadataFromDatabase(requestURL, timestamp)
|
|
require.True(t, ok, "data should already exist in database")
|
|
|
|
og, img, _, err := th.App.getLinkMetadata(th.Context, requestURL, timestamp+60*1000, false, "")
|
|
|
|
require.NotNil(t, og)
|
|
assert.Nil(t, img)
|
|
assert.NoError(t, err)
|
|
assert.Equal(t, title, og.Title)
|
|
})
|
|
|
|
t.Run("should not use database if URL is different", func(t *testing.T) {
|
|
err := platform.PurgeLinkCache()
|
|
require.NoError(t, err)
|
|
|
|
differentURL := requestURL + "/other"
|
|
|
|
_, _, _, ok := getLinkMetadataFromCache(requestURL, timestamp)
|
|
require.False(t, ok, "data should not exist in in-memory cache")
|
|
|
|
_, _, ok = th.App.getLinkMetadataFromDatabase(differentURL, timestamp)
|
|
require.False(t, ok, "data should not exist in database")
|
|
|
|
og, img, _, err := th.App.getLinkMetadata(th.Context, differentURL, timestamp, false, "")
|
|
|
|
assert.Nil(t, og)
|
|
assert.Nil(t, img)
|
|
assert.NoError(t, err)
|
|
})
|
|
|
|
t.Run("should not use database if timestamp is different", func(t *testing.T) {
|
|
err := platform.PurgeLinkCache()
|
|
require.NoError(t, err)
|
|
|
|
differentTimestamp := timestamp + 60*60*1000
|
|
|
|
_, _, _, ok := getLinkMetadataFromCache(requestURL, timestamp)
|
|
require.False(t, ok, "data should not exist in in-memory cache")
|
|
|
|
_, _, ok = th.App.getLinkMetadataFromDatabase(requestURL, differentTimestamp)
|
|
require.False(t, ok, "data should not exist in database")
|
|
|
|
og, img, _, err := th.App.getLinkMetadata(th.Context, requestURL, differentTimestamp, false, "")
|
|
|
|
assert.Nil(t, og)
|
|
assert.Nil(t, img)
|
|
assert.NoError(t, err)
|
|
})
|
|
})
|
|
|
|
t.Run("should get data from remote source", func(t *testing.T) {
|
|
th := setup(t)
|
|
|
|
requestURL := server.URL + "/opengraph?title=Remote&name=" + t.Name()
|
|
timestamp := int64(1547510400000)
|
|
|
|
_, _, _, ok := getLinkMetadataFromCache(requestURL, timestamp)
|
|
require.False(t, ok, "data should not exist in in-memory cache")
|
|
|
|
_, _, ok = th.App.getLinkMetadataFromDatabase(requestURL, timestamp)
|
|
require.False(t, ok, "data should not exist in database")
|
|
|
|
og, img, _, err := th.App.getLinkMetadata(th.Context, requestURL, timestamp, false, "")
|
|
|
|
assert.NotNil(t, og)
|
|
assert.Nil(t, img)
|
|
assert.NoError(t, err)
|
|
})
|
|
|
|
t.Run("should cache OpenGraph results", func(t *testing.T) {
|
|
th := setup(t)
|
|
|
|
requestURL := server.URL + "/opengraph?title=Remote&name=" + t.Name()
|
|
timestamp := int64(1547510400000)
|
|
|
|
_, _, _, ok := getLinkMetadataFromCache(requestURL, timestamp)
|
|
require.False(t, ok, "data should not exist in in-memory cache")
|
|
|
|
_, _, ok = th.App.getLinkMetadataFromDatabase(requestURL, timestamp)
|
|
require.False(t, ok, "data should not exist in database")
|
|
|
|
og, img, _, err := th.App.getLinkMetadata(th.Context, requestURL, timestamp, false, "")
|
|
|
|
assert.NotNil(t, og)
|
|
assert.Nil(t, img)
|
|
assert.NoError(t, err)
|
|
|
|
fromCache, _, _, ok := getLinkMetadataFromCache(requestURL, timestamp)
|
|
assert.True(t, ok)
|
|
assert.Exactly(t, og, fromCache)
|
|
|
|
fromDatabase, _, ok := th.App.getLinkMetadataFromDatabase(requestURL, timestamp)
|
|
assert.True(t, ok)
|
|
assert.Exactly(t, og, fromDatabase)
|
|
})
|
|
|
|
t.Run("should cache image results", func(t *testing.T) {
|
|
th := setup(t)
|
|
|
|
requestURL := server.URL + "/image?height=300&width=400&name=" + t.Name()
|
|
timestamp := int64(1547510400000)
|
|
|
|
_, _, _, ok := getLinkMetadataFromCache(requestURL, timestamp)
|
|
require.False(t, ok, "data should not exist in in-memory cache")
|
|
|
|
_, _, ok = th.App.getLinkMetadataFromDatabase(requestURL, timestamp)
|
|
require.False(t, ok, "data should not exist in database")
|
|
|
|
og, img, _, err := th.App.getLinkMetadata(th.Context, requestURL, timestamp, false, "")
|
|
|
|
assert.Nil(t, og)
|
|
assert.NotNil(t, img)
|
|
assert.NoError(t, err)
|
|
|
|
_, fromCache, _, ok := getLinkMetadataFromCache(requestURL, timestamp)
|
|
assert.True(t, ok)
|
|
assert.Exactly(t, img, fromCache)
|
|
|
|
_, fromDatabase, ok := th.App.getLinkMetadataFromDatabase(requestURL, timestamp)
|
|
assert.True(t, ok)
|
|
assert.Exactly(t, img, fromDatabase)
|
|
})
|
|
|
|
t.Run("should cache general errors", func(t *testing.T) {
|
|
th := setup(t)
|
|
|
|
requestURL := server.URL + "/error"
|
|
timestamp := int64(1547510400000)
|
|
|
|
_, _, _, ok := getLinkMetadataFromCache(requestURL, timestamp)
|
|
require.False(t, ok, "data should not exist in in-memory cache")
|
|
|
|
_, _, ok = th.App.getLinkMetadataFromDatabase(requestURL, timestamp)
|
|
require.False(t, ok, "data should not exist in database")
|
|
|
|
og, img, _, err := th.App.getLinkMetadata(th.Context, requestURL, timestamp, false, "")
|
|
|
|
assert.Nil(t, og)
|
|
assert.Nil(t, img)
|
|
assert.NoError(t, err)
|
|
|
|
ogFromCache, imgFromCache, _, ok := getLinkMetadataFromCache(requestURL, timestamp)
|
|
assert.True(t, ok)
|
|
assert.Nil(t, ogFromCache)
|
|
assert.Nil(t, imgFromCache)
|
|
|
|
ogFromDatabase, imageFromDatabase, ok := th.App.getLinkMetadataFromDatabase(requestURL, timestamp)
|
|
assert.True(t, ok)
|
|
assert.Nil(t, ogFromDatabase)
|
|
assert.Nil(t, imageFromDatabase)
|
|
})
|
|
|
|
t.Run("should cache invalid URL errors", func(t *testing.T) {
|
|
th := setup(t)
|
|
|
|
requestURL := "http://notarealdomainthatactuallyexists.ca/?name=" + t.Name()
|
|
timestamp := int64(1547510400000)
|
|
|
|
_, _, _, ok := getLinkMetadataFromCache(requestURL, timestamp)
|
|
require.False(t, ok, "data should not exist in in-memory cache")
|
|
|
|
_, _, ok = th.App.getLinkMetadataFromDatabase(requestURL, timestamp)
|
|
require.False(t, ok, "data should not exist in database")
|
|
|
|
og, img, _, err := th.App.getLinkMetadata(th.Context, requestURL, timestamp, false, "")
|
|
|
|
assert.Nil(t, og)
|
|
assert.Nil(t, img)
|
|
assert.IsType(t, &url.Error{}, err)
|
|
|
|
ogFromCache, imgFromCache, _, ok := getLinkMetadataFromCache(requestURL, timestamp)
|
|
assert.True(t, ok)
|
|
assert.Nil(t, ogFromCache)
|
|
assert.Nil(t, imgFromCache)
|
|
|
|
ogFromDatabase, imageFromDatabase, ok := th.App.getLinkMetadataFromDatabase(requestURL, timestamp)
|
|
assert.True(t, ok)
|
|
assert.Nil(t, ogFromDatabase)
|
|
assert.Nil(t, imageFromDatabase)
|
|
})
|
|
|
|
t.Run("should cache timeout errors", func(t *testing.T) {
|
|
th := setup(t)
|
|
|
|
th.App.UpdateConfig(func(cfg *model.Config) {
|
|
*cfg.ExperimentalSettings.LinkMetadataTimeoutMilliseconds = 100
|
|
})
|
|
|
|
requestURL := server.URL + "/timeout?name=" + t.Name()
|
|
timestamp := int64(1547510400000)
|
|
|
|
_, _, _, ok := getLinkMetadataFromCache(requestURL, timestamp)
|
|
require.False(t, ok, "data should not exist in in-memory cache")
|
|
|
|
_, _, ok = th.App.getLinkMetadataFromDatabase(requestURL, timestamp)
|
|
require.False(t, ok, "data should not exist in database")
|
|
|
|
og, img, _, err := th.App.getLinkMetadata(th.Context, requestURL, timestamp, false, "")
|
|
|
|
assert.Nil(t, og)
|
|
assert.Nil(t, img)
|
|
assert.Error(t, err)
|
|
assert.True(t, os.IsTimeout(err))
|
|
|
|
ogFromCache, imgFromCache, _, ok := getLinkMetadataFromCache(requestURL, timestamp)
|
|
assert.True(t, ok)
|
|
assert.Nil(t, ogFromCache)
|
|
assert.Nil(t, imgFromCache)
|
|
|
|
ogFromDatabase, imageFromDatabase, ok := th.App.getLinkMetadataFromDatabase(requestURL, timestamp)
|
|
assert.True(t, ok)
|
|
assert.Nil(t, ogFromDatabase)
|
|
assert.Nil(t, imageFromDatabase)
|
|
})
|
|
|
|
t.Run("should cache database results in memory", func(t *testing.T) {
|
|
th := setup(t)
|
|
|
|
requestURL := server.URL + "/image?height=300&width=400&name=" + t.Name()
|
|
timestamp := int64(1547510400000)
|
|
|
|
_, _, _, ok := getLinkMetadataFromCache(requestURL, timestamp)
|
|
require.False(t, ok, "data should not exist in in-memory cache")
|
|
|
|
_, _, ok = th.App.getLinkMetadataFromDatabase(requestURL, timestamp)
|
|
require.False(t, ok, "data should not exist in database")
|
|
|
|
_, img, _, err := th.App.getLinkMetadata(th.Context, requestURL, timestamp, false, "")
|
|
require.NoError(t, err)
|
|
|
|
_, _, _, ok = getLinkMetadataFromCache(requestURL, timestamp)
|
|
require.True(t, ok, "data should now exist in in-memory cache")
|
|
|
|
err = platform.PurgeLinkCache()
|
|
require.NoError(t, err)
|
|
_, _, _, ok = getLinkMetadataFromCache(requestURL, timestamp)
|
|
require.False(t, ok, "data should no longer exist in in-memory cache")
|
|
|
|
_, fromDatabase, ok := th.App.getLinkMetadataFromDatabase(requestURL, timestamp)
|
|
assert.True(t, ok, "data should be be in in-memory cache again")
|
|
assert.Exactly(t, img, fromDatabase)
|
|
})
|
|
|
|
t.Run("should reject non-html, non-image response", func(t *testing.T) {
|
|
th := setup(t)
|
|
|
|
requestURL := server.URL + "/json?name=" + t.Name()
|
|
timestamp := int64(1547510400000)
|
|
|
|
og, img, _, err := th.App.getLinkMetadata(th.Context, requestURL, timestamp, false, "")
|
|
assert.Nil(t, og)
|
|
assert.Nil(t, img)
|
|
assert.NoError(t, err)
|
|
})
|
|
|
|
t.Run("should check in-memory cache for new post", func(t *testing.T) {
|
|
th := setup(t)
|
|
|
|
requestURL := server.URL + "/error?name=" + t.Name()
|
|
timestamp := int64(1547510400000)
|
|
|
|
cacheLinkMetadata(th.Context, requestURL, timestamp, &opengraph.OpenGraph{Title: "cached"}, nil, nil)
|
|
|
|
og, img, _, err := th.App.getLinkMetadata(th.Context, requestURL, timestamp, true, "")
|
|
assert.NotNil(t, og)
|
|
assert.Nil(t, img)
|
|
assert.NoError(t, err)
|
|
})
|
|
|
|
t.Run("should skip database cache for new post", func(t *testing.T) {
|
|
th := setup(t)
|
|
|
|
requestURL := server.URL + "/error?name=" + t.Name()
|
|
timestamp := int64(1547510400000)
|
|
|
|
th.App.saveLinkMetadataToDatabase(requestURL, timestamp, &opengraph.OpenGraph{Title: "cached"}, nil)
|
|
|
|
og, img, _, err := th.App.getLinkMetadata(th.Context, requestURL, timestamp, true, "")
|
|
assert.Nil(t, og)
|
|
assert.Nil(t, img)
|
|
assert.NoError(t, err)
|
|
})
|
|
|
|
t.Run("should resolve relative URL", func(t *testing.T) {
|
|
th := setup(t)
|
|
|
|
// Fake the SiteURL to have the relative URL resolve to the external server
|
|
oldSiteURL := *th.App.Config().ServiceSettings.SiteURL
|
|
defer th.App.UpdateConfig(func(cfg *model.Config) {
|
|
*cfg.ServiceSettings.SiteURL = oldSiteURL
|
|
})
|
|
|
|
th.App.UpdateConfig(func(cfg *model.Config) {
|
|
*cfg.ServiceSettings.SiteURL = server.URL
|
|
})
|
|
|
|
requestURL := "/image?height=200&width=300&name=" + t.Name()
|
|
timestamp := int64(1547510400000)
|
|
|
|
og, img, _, err := th.App.getLinkMetadata(th.Context, requestURL, timestamp, false, "")
|
|
assert.Nil(t, og)
|
|
assert.NotNil(t, img)
|
|
assert.NoError(t, err)
|
|
})
|
|
|
|
t.Run("should error on local addresses other than the image proxy", func(t *testing.T) {
|
|
th := setup(t)
|
|
|
|
// Disable AllowedUntrustedInternalConnections since it's turned on for the previous tests
|
|
oldAllowUntrusted := *th.App.Config().ServiceSettings.AllowedUntrustedInternalConnections
|
|
oldSiteURL := *th.App.Config().ServiceSettings.SiteURL
|
|
defer th.App.UpdateConfig(func(cfg *model.Config) {
|
|
*cfg.ServiceSettings.AllowedUntrustedInternalConnections = oldAllowUntrusted
|
|
*cfg.ServiceSettings.SiteURL = oldSiteURL
|
|
})
|
|
|
|
th.App.UpdateConfig(func(cfg *model.Config) {
|
|
*cfg.ServiceSettings.AllowedUntrustedInternalConnections = ""
|
|
*cfg.ServiceSettings.SiteURL = "http://mattermost.example.com"
|
|
*cfg.ImageProxySettings.Enable = true
|
|
*cfg.ImageProxySettings.ImageProxyType = "local"
|
|
})
|
|
|
|
requestURL := server.URL + "/image?height=200&width=300&name=" + t.Name()
|
|
timestamp := int64(1547510400000)
|
|
|
|
og, img, _, err := th.App.getLinkMetadata(th.Context, requestURL, timestamp, false, "")
|
|
assert.Nil(t, og)
|
|
assert.Nil(t, img)
|
|
assert.Error(t, err)
|
|
assert.IsType(t, &url.Error{}, err)
|
|
assert.ErrorContains(t, err, httpservice.ErrAddressForbidden.Error())
|
|
|
|
requestURL = th.App.GetSiteURL() + "/api/v4/image?url=" + url.QueryEscape(requestURL)
|
|
|
|
// Note that this request still fails while testing because the request made by the image proxy is blocked
|
|
og, img, _, err = th.App.getLinkMetadata(th.Context, requestURL, timestamp, false, "")
|
|
assert.Nil(t, og)
|
|
assert.Nil(t, img)
|
|
assert.Error(t, err)
|
|
assert.IsType(t, imageproxy.Error{}, err)
|
|
})
|
|
|
|
t.Run("should prefer images for mixed content", func(t *testing.T) {
|
|
th := setup(t)
|
|
|
|
requestURL := server.URL + "/mixed?name=" + t.Name()
|
|
timestamp := int64(1547510400000)
|
|
|
|
og, img, _, err := th.App.getLinkMetadata(th.Context, requestURL, timestamp, true, "")
|
|
assert.Nil(t, og)
|
|
assert.NotNil(t, img)
|
|
assert.NoError(t, err)
|
|
})
|
|
|
|
t.Run("should throw error if post doesn't exist", func(t *testing.T) {
|
|
th := setup(t)
|
|
|
|
th.App.UpdateConfig(func(cfg *model.Config) {
|
|
*cfg.ServiceSettings.EnablePermalinkPreviews = true
|
|
*cfg.ServiceSettings.SiteURL = server.URL
|
|
})
|
|
|
|
requestURL := server.URL + "/team/pl/5rpoy4o3nbgwjm7gs4cm71h6ho"
|
|
timestamp := int64(1547510400000)
|
|
|
|
_, _, _, err := th.App.getLinkMetadata(th.Context, requestURL, timestamp, true, "")
|
|
assert.Error(t, err)
|
|
})
|
|
|
|
t.Run("from cache", func(t *testing.T) {
|
|
testURL := "https://example.com/test"
|
|
testTimestamp := int64(1640995200000) // 2022-01-01 00:00:00 UTC
|
|
|
|
setup := func(t *testing.T) {
|
|
err := platform.PurgeLinkCache()
|
|
require.NoError(t, err)
|
|
}
|
|
|
|
assertCached := func(t *testing.T, url string, expectedOG *opengraph.OpenGraph, expectedImage *model.PostImage, expectedPermalink *model.Permalink) {
|
|
og, image, permalink, found := getLinkMetadataFromCache(url, testTimestamp)
|
|
assert.True(t, found)
|
|
assert.Equal(t, expectedOG, og)
|
|
assert.Equal(t, expectedImage, image)
|
|
assert.Equal(t, expectedPermalink, permalink)
|
|
}
|
|
|
|
assertNotCached := func(t *testing.T, url string) {
|
|
og, image, permalink, found := getLinkMetadataFromCache(url, testTimestamp)
|
|
assert.False(t, found)
|
|
assert.Nil(t, og)
|
|
assert.Nil(t, image)
|
|
assert.Nil(t, permalink)
|
|
}
|
|
|
|
t.Run("should return false when cache is empty", func(t *testing.T) {
|
|
setup(t)
|
|
assertNotCached(t, testURL)
|
|
})
|
|
|
|
t.Run("should return cached data when URL matches", func(t *testing.T) {
|
|
setup(t)
|
|
expectedOG := &opengraph.OpenGraph{
|
|
Title: "Test Title",
|
|
URL: testURL,
|
|
}
|
|
expectedImage := &model.PostImage{
|
|
Width: 100,
|
|
Height: 200,
|
|
}
|
|
expectedPermalink := &model.Permalink{
|
|
PreviewPost: &model.PreviewPost{
|
|
PostID: "test-post-id",
|
|
},
|
|
}
|
|
|
|
ctx := request.TestContext(t)
|
|
cacheLinkMetadata(ctx, testURL, testTimestamp, expectedOG, expectedImage, expectedPermalink)
|
|
|
|
assertCached(t, testURL, expectedOG, expectedImage, expectedPermalink)
|
|
})
|
|
|
|
t.Run("should return false when different url not cached", func(t *testing.T) {
|
|
setup(t)
|
|
|
|
cachedURL := "https://example.com/cached"
|
|
requestedURL := "https://example.com/different"
|
|
|
|
expectedOG := &opengraph.OpenGraph{
|
|
Title: "Cached Title",
|
|
URL: cachedURL,
|
|
}
|
|
ctx := request.TestContext(t)
|
|
cacheLinkMetadata(ctx, cachedURL, testTimestamp, expectedOG, nil, nil)
|
|
|
|
assertNotCached(t, requestedURL)
|
|
})
|
|
|
|
t.Run("should return false when different url not cached, even if hash collides with a cached url", func(t *testing.T) {
|
|
setup(t)
|
|
|
|
url1 := "http://test.com/w4xg6hpvomau9j5iz371"
|
|
url2 := "http://collision.comupio5zw28x1m36c"
|
|
|
|
hash1 := model.GenerateLinkMetadataHash(url1, testTimestamp)
|
|
hash2 := model.GenerateLinkMetadataHash(url2, testTimestamp)
|
|
assert.Equal(t, hash1, hash2, "URLs should have colliding hashes")
|
|
|
|
og1 := &opengraph.OpenGraph{
|
|
Title: "First URL Title",
|
|
URL: url1,
|
|
}
|
|
ctx := request.TestContext(t)
|
|
cacheLinkMetadata(ctx, url1, testTimestamp, og1, nil, nil)
|
|
|
|
assertCached(t, url1, og1, nil, nil)
|
|
assertNotCached(t, url2)
|
|
})
|
|
|
|
t.Run("should handle cached nil values correctly", func(t *testing.T) {
|
|
setup(t)
|
|
|
|
nilURL := "https://example.com/nil-test"
|
|
|
|
ctx := request.TestContext(t)
|
|
cacheLinkMetadata(ctx, nilURL, testTimestamp, nil, nil, nil)
|
|
|
|
assertCached(t, nilURL, nil, nil, nil)
|
|
})
|
|
})
|
|
}
|
|
|
|
func TestResolveMetadataURL(t *testing.T) {
|
|
mainHelper.Parallel(t)
|
|
for _, test := range []struct {
|
|
Name string
|
|
RequestURL string
|
|
SiteURL string
|
|
Expected string
|
|
}{
|
|
{
|
|
Name: "with HTTPS",
|
|
RequestURL: "https://example.com/file?param=1",
|
|
Expected: "https://example.com/file?param=1",
|
|
},
|
|
{
|
|
Name: "with HTTP",
|
|
RequestURL: "http://example.com/file?param=1",
|
|
Expected: "http://example.com/file?param=1",
|
|
},
|
|
{
|
|
Name: "with FTP",
|
|
RequestURL: "ftp://example.com/file?param=1",
|
|
Expected: "ftp://example.com/file?param=1",
|
|
},
|
|
{
|
|
Name: "relative to root",
|
|
RequestURL: "/file?param=1",
|
|
SiteURL: "https://mattermost.example.com:123",
|
|
Expected: "https://mattermost.example.com:123/file?param=1",
|
|
},
|
|
{
|
|
Name: "relative to root with subpath",
|
|
RequestURL: "/file?param=1",
|
|
SiteURL: "https://mattermost.example.com:123/subpath",
|
|
Expected: "https://mattermost.example.com:123/file?param=1",
|
|
},
|
|
} {
|
|
t.Run(test.Name, func(t *testing.T) {
|
|
assert.Equal(t, resolveMetadataURL(test.RequestURL, test.SiteURL), test.Expected)
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestParseLinkMetadata(t *testing.T) {
|
|
mainHelper.Parallel(t)
|
|
th := Setup(t)
|
|
|
|
imageURL := "http://example.com/test.png"
|
|
file, err := testutils.ReadTestFile("test.png")
|
|
require.NoError(t, err)
|
|
|
|
ogURL := "https://example.com/hello"
|
|
html := `
|
|
<html>
|
|
<head>
|
|
<meta property="og:title" content="Hello, World!">
|
|
<meta property="og:type" content="object">
|
|
<meta property="og:url" content="` + ogURL + `">
|
|
</head>
|
|
</html>`
|
|
|
|
makeImageReader := func() io.Reader {
|
|
return bytes.NewReader(file)
|
|
}
|
|
|
|
makeOpenGraphReader := func() io.Reader {
|
|
return strings.NewReader(html)
|
|
}
|
|
|
|
t.Run("image", func(t *testing.T) {
|
|
og, dimensions, err := th.App.parseLinkMetadata(th.Context, imageURL, makeImageReader(), "image/png")
|
|
assert.NoError(t, err)
|
|
|
|
assert.Nil(t, og)
|
|
assert.Equal(t, &model.PostImage{
|
|
Format: "png",
|
|
Width: 408,
|
|
Height: 336,
|
|
}, dimensions)
|
|
})
|
|
|
|
t.Run("image with no content-type given", func(t *testing.T) {
|
|
og, dimensions, err := th.App.parseLinkMetadata(th.Context, imageURL, makeImageReader(), "")
|
|
assert.NoError(t, err)
|
|
|
|
assert.Nil(t, og)
|
|
assert.Equal(t, &model.PostImage{
|
|
Format: "png",
|
|
Width: 408,
|
|
Height: 336,
|
|
}, dimensions)
|
|
})
|
|
|
|
t.Run("malformed image", func(t *testing.T) {
|
|
og, dimensions, err := th.App.parseLinkMetadata(th.Context, imageURL, makeOpenGraphReader(), "image/png")
|
|
assert.Error(t, err)
|
|
|
|
assert.Nil(t, og)
|
|
assert.Nil(t, dimensions)
|
|
})
|
|
|
|
t.Run("opengraph", func(t *testing.T) {
|
|
og, dimensions, err := th.App.parseLinkMetadata(th.Context, ogURL, makeOpenGraphReader(), "text/html; charset=utf-8")
|
|
assert.NoError(t, err)
|
|
|
|
assert.NotNil(t, og)
|
|
assert.Equal(t, og.Title, "Hello, World!")
|
|
assert.Equal(t, og.Type, "object")
|
|
assert.Equal(t, og.URL, ogURL)
|
|
assert.Nil(t, dimensions)
|
|
})
|
|
|
|
t.Run("malformed opengraph", func(t *testing.T) {
|
|
og, dimensions, err := th.App.parseLinkMetadata(th.Context, ogURL, makeImageReader(), "text/html; charset=utf-8")
|
|
assert.NoError(t, err)
|
|
|
|
assert.Nil(t, og)
|
|
assert.Nil(t, dimensions)
|
|
})
|
|
|
|
t.Run("neither", func(t *testing.T) {
|
|
og, dimensions, err := th.App.parseLinkMetadata(th.Context, "http://example.com/test.wad", strings.NewReader("garbage"), "application/x-doom")
|
|
assert.NoError(t, err)
|
|
|
|
assert.Nil(t, og)
|
|
assert.Nil(t, dimensions)
|
|
})
|
|
|
|
t.Run("svg", func(t *testing.T) {
|
|
og, dimensions, err := th.App.parseLinkMetadata(th.Context, "http://example.com/image.svg", nil, "image/svg+xml")
|
|
assert.NoError(t, err)
|
|
assert.Nil(t, og)
|
|
assert.Nil(t, dimensions)
|
|
})
|
|
|
|
t.Run("svg with charset", func(t *testing.T) {
|
|
og, dimensions, err := th.App.parseLinkMetadata(th.Context, "http://example.com/image.svg", nil, "image/svg+xml; charset=utf-8")
|
|
assert.NoError(t, err)
|
|
assert.Nil(t, og)
|
|
assert.Nil(t, dimensions)
|
|
})
|
|
}
|
|
|
|
func TestParseImages(t *testing.T) {
|
|
mainHelper.Parallel(t)
|
|
for name, testCase := range map[string]struct {
|
|
FileName string
|
|
Expected *model.PostImage
|
|
ExpectError bool
|
|
}{
|
|
"png": {
|
|
FileName: "test.png",
|
|
Expected: &model.PostImage{
|
|
Width: 408,
|
|
Height: 336,
|
|
Format: "png",
|
|
},
|
|
},
|
|
"jpg-1": {
|
|
FileName: "orientation_test_1.jpeg",
|
|
Expected: &model.PostImage{
|
|
Width: 2860,
|
|
Height: 1578,
|
|
Format: "jpeg",
|
|
},
|
|
},
|
|
"jpg-2": {
|
|
FileName: "orientation_test_2.jpeg",
|
|
Expected: &model.PostImage{
|
|
Width: 2860,
|
|
Height: 1578,
|
|
Format: "jpeg",
|
|
},
|
|
},
|
|
"jpg-3": {
|
|
FileName: "orientation_test_3.jpeg",
|
|
Expected: &model.PostImage{
|
|
Width: 2860,
|
|
Height: 1578,
|
|
Format: "jpeg",
|
|
},
|
|
},
|
|
"jpg-4": {
|
|
FileName: "orientation_test_4.jpeg",
|
|
Expected: &model.PostImage{
|
|
Width: 2860,
|
|
Height: 1578,
|
|
Format: "jpeg",
|
|
},
|
|
},
|
|
"jpg-5": {
|
|
FileName: "orientation_test_5.jpeg",
|
|
Expected: &model.PostImage{
|
|
Width: 2860,
|
|
Height: 1578,
|
|
Format: "jpeg",
|
|
},
|
|
},
|
|
"jpg-6": {
|
|
FileName: "orientation_test_6.jpeg",
|
|
Expected: &model.PostImage{
|
|
Width: 2860,
|
|
Height: 1578,
|
|
Format: "jpeg",
|
|
},
|
|
},
|
|
"jpg-7": {
|
|
FileName: "orientation_test_7.jpeg",
|
|
Expected: &model.PostImage{
|
|
Width: 2860,
|
|
Height: 1578,
|
|
Format: "jpeg",
|
|
},
|
|
},
|
|
"jpg-8": {
|
|
FileName: "orientation_test_8.jpeg",
|
|
Expected: &model.PostImage{
|
|
Width: 2860,
|
|
Height: 1578,
|
|
Format: "jpeg",
|
|
},
|
|
},
|
|
"jpg-9": {
|
|
FileName: "orientation_test_9.jpeg",
|
|
Expected: &model.PostImage{
|
|
Width: 4000,
|
|
Height: 2667,
|
|
Format: "jpeg",
|
|
},
|
|
},
|
|
"animated gif": {
|
|
FileName: "testgif.gif",
|
|
Expected: &model.PostImage{
|
|
Width: 118,
|
|
Height: 118,
|
|
Format: "gif",
|
|
FrameCount: 4,
|
|
},
|
|
},
|
|
"tiff": {
|
|
FileName: "test.tiff",
|
|
Expected: (*model.PostImage)(nil),
|
|
},
|
|
"not an image": {
|
|
FileName: "README.md",
|
|
ExpectError: true,
|
|
},
|
|
} {
|
|
t.Run(name, func(t *testing.T) {
|
|
file, err := testutils.ReadTestFile(testCase.FileName)
|
|
require.NoError(t, err)
|
|
|
|
result, err := parseImages(request.EmptyContext(mlog.CreateConsoleTestLogger(t)), "", bytes.NewReader(file))
|
|
if testCase.ExpectError {
|
|
assert.Error(t, err)
|
|
} else {
|
|
assert.NoError(t, err)
|
|
assert.Equal(t, testCase.Expected, result)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestLooksLikeAPermalink(t *testing.T) {
|
|
mainHelper.Parallel(t)
|
|
const siteURLWithSubpath = "http://localhost:8065/foo"
|
|
const siteURLWithTrailingSlash = "http://test.com/"
|
|
const siteURL = "http://test.com"
|
|
tests := map[string]struct {
|
|
input string
|
|
siteURL string
|
|
expect bool
|
|
}{
|
|
"happy path": {input: fmt.Sprintf("%s/private-core/pl/dppezk51jp8afbhwxf1jpag66r", siteURLWithSubpath), siteURL: siteURLWithSubpath, expect: true},
|
|
"happy path redirect": {input: fmt.Sprintf("%s/_redirect/pl/dppezk51jp8afbhwxf1jpag66r", siteURLWithSubpath), siteURL: siteURLWithSubpath, expect: true},
|
|
"looks nothing like a permalink": {input: "foobar", siteURL: siteURLWithSubpath, expect: false},
|
|
"link has no subpath": {input: fmt.Sprintf("%s/private-core/pl/dppezk51jp8afbhwxf1jpag66r", "http://localhost:8065"), siteURL: siteURLWithSubpath, expect: false},
|
|
"without port": {input: fmt.Sprintf("%s/private-core/pl/dppezk51jp8afbhwxf1jpag66r", "http://localhost/foo"), siteURL: siteURLWithSubpath, expect: false},
|
|
"wrong port": {input: fmt.Sprintf("%s/private-core/pl/dppezk51jp8afbhwxf1jpag66r", "http://localhost:8066"), siteURL: siteURLWithSubpath, expect: false},
|
|
"invalid post ID length": {input: fmt.Sprintf("%s/private-core/pl/dppezk51jp8afbhwxf1jpag66", siteURLWithSubpath), siteURL: siteURLWithSubpath, expect: false},
|
|
"invalid post ID character": {input: fmt.Sprintf("%s/private-core/pl/dppezk51jp8$fbhwxf1jpag66r", siteURLWithSubpath), siteURL: siteURLWithSubpath, expect: false},
|
|
"leading whitespace": {input: fmt.Sprintf(" %s/private-core/pl/dppezk51jp8afbhwxf1jpag66r", siteURLWithSubpath), siteURL: siteURLWithSubpath, expect: true},
|
|
"trailing whitespace": {input: fmt.Sprintf("%s/private-core/pl/dppezk51jp8afbhwxf1jpag66r ", siteURLWithSubpath), siteURL: siteURLWithSubpath, expect: true},
|
|
"siteURL without a subpath": {input: fmt.Sprintf("%sprivate-core/pl/dppezk51jp8afbhwxf1jpag66r", siteURLWithTrailingSlash), siteURL: siteURLWithTrailingSlash, expect: true},
|
|
"siteURL without a trailing slash": {input: fmt.Sprintf("%s/private-core/pl/dppezk51jp8afbhwxf1jpag66r", siteURL), siteURL: siteURL, expect: true},
|
|
}
|
|
|
|
for name, tc := range tests {
|
|
t.Run(name, func(t *testing.T) {
|
|
actual := looksLikeAPermalink(tc.input, tc.siteURL)
|
|
assert.Equal(t, tc.expect, actual)
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestContainsPermalink(t *testing.T) {
|
|
mainHelper.Parallel(t)
|
|
th := Setup(t)
|
|
|
|
const siteURLWithSubpath = "http://localhost:8065/foo"
|
|
|
|
th.App.UpdateConfig(func(cfg *model.Config) {
|
|
*cfg.ServiceSettings.SiteURL = siteURLWithSubpath
|
|
})
|
|
|
|
testCases := []struct {
|
|
Description string
|
|
Post *model.Post
|
|
Expected bool
|
|
}{
|
|
{
|
|
Description: "contains a permalink",
|
|
Post: &model.Post{
|
|
Message: fmt.Sprintf("%s/private-core/pl/dppezk51jp8afbhwxf1jpag66r", siteURLWithSubpath),
|
|
},
|
|
Expected: true,
|
|
},
|
|
{
|
|
Description: "does not contain a permalink",
|
|
Post: &model.Post{
|
|
Message: "foobar",
|
|
},
|
|
Expected: false,
|
|
},
|
|
}
|
|
|
|
for _, testCase := range testCases {
|
|
t.Run(testCase.Description, func(t *testing.T) {
|
|
actual := th.App.containsPermalink(th.Context, testCase.Post)
|
|
assert.Equal(t, testCase.Expected, actual)
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestSanitizePostMetaDataForAudit(t *testing.T) {
|
|
mainHelper.Parallel(t)
|
|
th := Setup(t).InitBasic(t)
|
|
|
|
th.App.UpdateConfig(func(cfg *model.Config) {
|
|
*cfg.ServiceSettings.SiteURL = "http://mymattermost.com"
|
|
})
|
|
|
|
th.Context.Session().UserId = th.BasicUser.Id
|
|
|
|
referencedPost, _, err := th.App.CreatePost(th.Context, &model.Post{
|
|
UserId: th.BasicUser.Id,
|
|
ChannelId: th.BasicChannel.Id,
|
|
Message: "hello world",
|
|
}, th.BasicChannel, model.CreatePostFlags{SetOnline: true})
|
|
require.Nil(t, err)
|
|
referencedPost.Metadata.Embeds = nil
|
|
|
|
link := fmt.Sprintf("%s/%s/pl/%s", *th.App.Config().ServiceSettings.SiteURL, th.BasicTeam.Name, referencedPost.Id)
|
|
|
|
previewPost, _, err := th.App.CreatePost(th.Context, &model.Post{
|
|
UserId: th.BasicUser.Id,
|
|
ChannelId: th.BasicChannel.Id,
|
|
Message: link,
|
|
}, th.BasicChannel, model.CreatePostFlags{SetOnline: true})
|
|
require.Nil(t, err)
|
|
previewPost.Metadata.Embeds = nil
|
|
clientPost := th.App.PreparePostForClientWithEmbedsAndImages(th.Context, previewPost, &model.PreparePostForClientOpts{})
|
|
firstEmbed := clientPost.Metadata.Embeds[0]
|
|
preview := firstEmbed.Data.(*model.PreviewPost)
|
|
require.Equal(t, referencedPost.Id, preview.PostID)
|
|
|
|
// ensure the permalink metadata is sanitized for audit logging
|
|
m := clientPost.Auditable()
|
|
metaDataI, ok := m["metadata"]
|
|
require.True(t, ok)
|
|
metaData, ok := metaDataI.(map[string]any)
|
|
require.True(t, ok)
|
|
embedsI, ok := metaData["embeds"]
|
|
require.True(t, ok)
|
|
embeds, ok := embedsI.([]map[string]any)
|
|
require.True(t, ok)
|
|
for _, pe := range embeds {
|
|
// ensure all the PostEmbed maps only contain `type` and `url`
|
|
for k := range pe {
|
|
if k != "type" && k != "url" {
|
|
require.Fail(t, "PostEmbed should only contain 'type' and 'url fields'")
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestSanitizePostMetadataForUser(t *testing.T) {
|
|
mainHelper.Parallel(t)
|
|
th := Setup(t).InitBasic(t)
|
|
|
|
enableLinkPreviews := *th.App.Config().ServiceSettings.EnableLinkPreviews
|
|
defer func() {
|
|
th.App.UpdateConfig(func(cfg *model.Config) {
|
|
cfg.ServiceSettings.EnableLinkPreviews = &enableLinkPreviews
|
|
})
|
|
}()
|
|
th.App.UpdateConfig(func(cfg *model.Config) {
|
|
*cfg.ServiceSettings.EnableLinkPreviews = true
|
|
})
|
|
|
|
t.Run("should remove embeds for not accessible channels", func(t *testing.T) {
|
|
privateChannel, err := th.App.CreateChannel(th.Context, &model.Channel{
|
|
Name: model.NewId(),
|
|
Type: model.ChannelTypePrivate,
|
|
TeamId: th.BasicTeam.Id,
|
|
CreatorId: th.SystemAdminUser.Id,
|
|
}, true)
|
|
|
|
require.Nil(t, err)
|
|
require.NotEmpty(t, privateChannel.Id)
|
|
|
|
post := &model.Post{
|
|
Id: "post_id_1",
|
|
UserId: th.BasicUser.Id,
|
|
Metadata: &model.PostMetadata{
|
|
Embeds: []*model.PostEmbed{
|
|
{
|
|
Type: model.PostEmbedPermalink,
|
|
Data: &model.PreviewPost{
|
|
PostID: "permalink_post_id",
|
|
Post: &model.Post{
|
|
Id: "permalink_post_id",
|
|
Message: "permalink post message",
|
|
ChannelId: privateChannel.Id,
|
|
},
|
|
},
|
|
},
|
|
{
|
|
Type: model.PostEmbedPermalink,
|
|
Data: &model.PreviewPost{
|
|
PostID: "permalink_post_id_2",
|
|
Post: &model.Post{
|
|
Id: "permalink_post_id_2",
|
|
Message: "permalink post message 2",
|
|
ChannelId: privateChannel.Id,
|
|
},
|
|
},
|
|
},
|
|
{
|
|
Type: model.PostEmbedLink,
|
|
URL: "https://mattermost.com",
|
|
},
|
|
},
|
|
},
|
|
}
|
|
|
|
sanitizedPost, isMemberForPreviews, err := th.App.SanitizePostMetadataForUser(th.Context, post, th.BasicUser.Id)
|
|
require.Nil(t, err)
|
|
require.NotNil(t, sanitizedPost)
|
|
|
|
require.Equal(t, 1, len(sanitizedPost.Metadata.Embeds))
|
|
require.Equal(t, model.PostEmbedLink, sanitizedPost.Metadata.Embeds[0].Type)
|
|
require.True(t, isMemberForPreviews)
|
|
})
|
|
|
|
t.Run("should not remove embeds for archived channels", func(t *testing.T) {
|
|
publicChannel, err := th.App.CreateChannel(th.Context, &model.Channel{
|
|
Name: model.NewId(),
|
|
Type: model.ChannelTypeOpen,
|
|
TeamId: th.BasicTeam.Id,
|
|
CreatorId: th.SystemAdminUser.Id,
|
|
}, true)
|
|
|
|
require.Nil(t, err)
|
|
require.NotEmpty(t, publicChannel.Id)
|
|
|
|
err = th.App.DeleteChannel(th.Context, publicChannel, th.SystemAdminUser.Id)
|
|
require.Nil(t, err)
|
|
|
|
publicChannel, err = th.App.GetChannel(th.Context, publicChannel.Id)
|
|
require.Nil(t, err)
|
|
require.NotEmpty(t, publicChannel.Id)
|
|
require.NotEqual(t, 0, publicChannel.DeleteAt)
|
|
|
|
post := &model.Post{
|
|
Id: "post_id_1",
|
|
UserId: th.BasicUser.Id,
|
|
Metadata: &model.PostMetadata{
|
|
Embeds: []*model.PostEmbed{
|
|
{
|
|
Type: model.PostEmbedPermalink,
|
|
Data: &model.PreviewPost{
|
|
PostID: "permalink_post_id",
|
|
Post: &model.Post{
|
|
Id: "permalink_post_id",
|
|
Message: "permalink post message",
|
|
ChannelId: publicChannel.Id,
|
|
},
|
|
},
|
|
},
|
|
{
|
|
Type: model.PostEmbedLink,
|
|
URL: "https://mattermost.com",
|
|
},
|
|
},
|
|
},
|
|
}
|
|
|
|
sanitizedPost, isMemberForPreviews, err := th.App.SanitizePostMetadataForUser(th.Context, post, th.BasicUser.Id)
|
|
require.Nil(t, err)
|
|
require.NotNil(t, sanitizedPost)
|
|
|
|
require.Equal(t, 2, len(sanitizedPost.Metadata.Embeds))
|
|
require.Equal(t, model.PostEmbedPermalink, sanitizedPost.Metadata.Embeds[0].Type)
|
|
require.False(t, isMemberForPreviews)
|
|
})
|
|
|
|
t.Run("permalink embed with PreviewPost type does not panic", func(t *testing.T) {
|
|
// Verify that the code correctly type-asserts embed.Data as *model.PreviewPost
|
|
// (not *model.Permalink) and does not panic.
|
|
refChannelID := th.BasicChannel.Id
|
|
fileID := model.NewId()
|
|
|
|
post := &model.Post{
|
|
Id: model.NewId(),
|
|
ChannelId: th.BasicChannel.Id,
|
|
UserId: th.BasicUser.Id,
|
|
Metadata: &model.PostMetadata{
|
|
Embeds: []*model.PostEmbed{
|
|
{
|
|
Type: model.PostEmbedPermalink,
|
|
Data: &model.PreviewPost{
|
|
PostID: model.NewId(),
|
|
Post: &model.Post{
|
|
Id: model.NewId(),
|
|
ChannelId: refChannelID,
|
|
FileIds: model.StringArray{fileID},
|
|
Metadata: &model.PostMetadata{
|
|
Files: []*model.FileInfo{
|
|
{Id: fileID, Name: "photo.png", Extension: "png"},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
|
|
// With AccessControl == nil (no enterprise), sanitizeFileAttachmentsForUser returns early.
|
|
// The key assertion is that the function does NOT panic on the *model.PreviewPost type assertion.
|
|
sanitizedPost, isMemberForPreviews, appErr := th.App.SanitizePostMetadataForUser(th.Context, post, th.BasicUser.Id)
|
|
require.Nil(t, appErr)
|
|
require.NotNil(t, sanitizedPost)
|
|
require.True(t, isMemberForPreviews)
|
|
|
|
// Without enterprise ABAC, files should remain untouched.
|
|
require.Len(t, sanitizedPost.Metadata.Embeds, 1)
|
|
previewData, ok := sanitizedPost.Metadata.Embeds[0].Data.(*model.PreviewPost)
|
|
require.True(t, ok)
|
|
assert.Len(t, previewData.Post.Metadata.Files, 1, "files should be preserved when ABAC is not active")
|
|
assert.Len(t, previewData.Post.FileIds, 1)
|
|
})
|
|
|
|
t.Run("post with no top-level files still reaches embed sanitization without error", func(t *testing.T) {
|
|
// Verifies that the guard removal in SanitizePostMetadataForUser allows sanitization
|
|
// to proceed even when the post itself has no files, only permalink embeds with files.
|
|
post := &model.Post{
|
|
Id: model.NewId(),
|
|
ChannelId: th.BasicChannel.Id,
|
|
UserId: th.BasicUser.Id,
|
|
FileIds: model.StringArray{},
|
|
Metadata: &model.PostMetadata{
|
|
Files: nil,
|
|
Embeds: []*model.PostEmbed{
|
|
{
|
|
Type: model.PostEmbedPermalink,
|
|
Data: &model.PreviewPost{
|
|
PostID: model.NewId(),
|
|
Post: &model.Post{
|
|
Id: model.NewId(),
|
|
ChannelId: th.BasicChannel.Id,
|
|
FileIds: model.StringArray{model.NewId()},
|
|
Metadata: &model.PostMetadata{
|
|
Files: []*model.FileInfo{
|
|
{Id: model.NewId(), Name: "doc.pdf", Extension: "pdf"},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
|
|
sanitizedPost, _, appErr := th.App.SanitizePostMetadataForUser(th.Context, post, th.BasicUser.Id)
|
|
require.Nil(t, appErr)
|
|
require.NotNil(t, sanitizedPost)
|
|
|
|
// Without enterprise ABAC, the embed files should remain.
|
|
previewData, ok := sanitizedPost.Metadata.Embeds[0].Data.(*model.PreviewPost)
|
|
require.True(t, ok)
|
|
assert.NotNil(t, previewData.Post.Metadata.Files, "embed files should not be stripped without ABAC")
|
|
})
|
|
}
|
|
|
|
func TestPreparePostForClient_BurnOnReadSenderExpireAt(t *testing.T) {
|
|
th := Setup(t).InitBasic(t)
|
|
|
|
// Enable BurnOnRead feature flag
|
|
th.App.UpdateConfig(func(cfg *model.Config) { cfg.FeatureFlags.BurnOnRead = true })
|
|
|
|
// Enable Enterprise Advanced license and BoR config
|
|
th.App.Srv().SetLicense(model.NewTestLicenseSKU(model.LicenseShortSkuEnterpriseAdvanced))
|
|
th.App.UpdateConfig(func(cfg *model.Config) {
|
|
cfg.ServiceSettings.EnableBurnOnRead = new(true)
|
|
})
|
|
|
|
// Create a burn-on-read post
|
|
borPost := &model.Post{
|
|
ChannelId: th.BasicChannel.Id,
|
|
UserId: th.BasicUser.Id,
|
|
Message: "test burn on read",
|
|
Type: model.PostTypeBurnOnRead,
|
|
}
|
|
borPost.AddProp(model.PostPropsExpireAt, model.GetMillis()+int64(10*60*1000))
|
|
|
|
post, _, appErr := th.App.CreatePost(th.Context, borPost, th.BasicChannel, model.CreatePostFlags{})
|
|
require.Nil(t, appErr)
|
|
|
|
// Simulate recipient revealing the post
|
|
user2 := th.CreateUser(t)
|
|
th.LinkUserToTeam(t, user2, th.BasicTeam)
|
|
th.AddUserToChannel(t, user2, th.BasicChannel)
|
|
|
|
_, appErr = th.App.RevealPost(th.Context, post, user2.Id, "")
|
|
require.Nil(t, appErr)
|
|
|
|
// Get the read receipt to get the expireAt
|
|
receipt, err := th.App.Srv().Store().ReadReceipt().Get(th.Context, post.Id, user2.Id)
|
|
require.NoError(t, err)
|
|
|
|
// Update temporary post with sender's expireAt (simulating all recipients revealed)
|
|
tmpPost, err := th.App.Srv().Store().TemporaryPost().Get(th.Context, post.Id, true)
|
|
require.NoError(t, err)
|
|
tmpPost.ExpireAt = receipt.ExpireAt
|
|
_, err = th.App.Srv().Store().TemporaryPost().Save(th.Context, tmpPost)
|
|
require.NoError(t, err)
|
|
|
|
// Create a new context for the sender (post author) with proper session
|
|
senderContext := th.Context
|
|
senderContext.Session().UserId = th.BasicUser.Id
|
|
|
|
// Fetch the post without ExpireAt in metadata (simulating page reload)
|
|
postFromDB, appErr := th.App.GetSinglePost(th.Context, post.Id, false)
|
|
require.Nil(t, appErr)
|
|
postFromDB.Metadata = &model.PostMetadata{} // Reset metadata to simulate fresh fetch
|
|
|
|
// PreparePostForClient should fill ExpireAt from TemporaryPost for the sender
|
|
preparedPost := th.App.PreparePostForClient(senderContext, postFromDB, &model.PreparePostForClientOpts{})
|
|
|
|
// Verify that ExpireAt is now set in metadata for the sender
|
|
require.NotNil(t, preparedPost.Metadata)
|
|
require.NotZero(t, preparedPost.Metadata.ExpireAt)
|
|
require.Equal(t, receipt.ExpireAt, preparedPost.Metadata.ExpireAt)
|
|
}
|
|
func TestSanitizeChannelMentionsForUser(t *testing.T) {
|
|
mainHelper.Parallel(t)
|
|
|
|
t.Run("post with no channel mentions", func(t *testing.T) {
|
|
th := Setup(t).InitBasic(t)
|
|
|
|
post := &model.Post{
|
|
Message: "Regular post with no mentions",
|
|
}
|
|
|
|
result, _, err := th.App.SanitizePostMetadataForUser(th.Context, post, th.BasicUser.Id)
|
|
require.Nil(t, err)
|
|
require.NotNil(t, result)
|
|
require.Nil(t, result.GetProp(model.PostPropsChannelMentions))
|
|
})
|
|
|
|
t.Run("removes inaccessible channel mentions", func(t *testing.T) {
|
|
th := Setup(t).InitBasic(t)
|
|
|
|
// Create a channel on a different team user is not member of
|
|
team2, err := th.App.CreateTeam(th.Context, &model.Team{
|
|
DisplayName: "Team 2",
|
|
Name: "team-2",
|
|
Type: model.TeamOpen,
|
|
})
|
|
require.Nil(t, err)
|
|
|
|
channel, err := th.App.CreateChannel(th.Context, &model.Channel{
|
|
TeamId: team2.Id,
|
|
Name: "test-channel",
|
|
DisplayName: "Test Channel",
|
|
Type: model.ChannelTypeOpen,
|
|
}, false)
|
|
require.Nil(t, err)
|
|
|
|
post := &model.Post{
|
|
Message: "Check ~test-channel",
|
|
}
|
|
post.AddProp(model.PostPropsChannelMentions, map[string]any{
|
|
channel.Name: map[string]any{
|
|
"display_name": channel.DisplayName,
|
|
"team_name": team2.Name,
|
|
},
|
|
})
|
|
|
|
result, _, err := th.App.SanitizePostMetadataForUser(th.Context, post, th.BasicUser.Id)
|
|
require.Nil(t, err)
|
|
require.NotNil(t, result)
|
|
// Should remove the mention since user can't access the team
|
|
require.Nil(t, result.GetProp(model.PostPropsChannelMentions))
|
|
})
|
|
|
|
t.Run("includes accessible public channel mentions on retrieval", func(t *testing.T) {
|
|
th := Setup(t).InitBasic(t)
|
|
|
|
post := &model.Post{
|
|
Message: "Check ~" + th.BasicChannel.Name,
|
|
}
|
|
post.AddProp(model.PostPropsChannelMentions, map[string]any{
|
|
th.BasicChannel.Name: map[string]any{
|
|
"display_name": th.BasicChannel.DisplayName,
|
|
"team_name": th.BasicTeam.Name,
|
|
},
|
|
})
|
|
|
|
result, _, err := th.App.SanitizePostMetadataForUser(th.Context, post, th.BasicUser.Id)
|
|
require.Nil(t, err)
|
|
require.NotNil(t, result)
|
|
|
|
mentions := result.GetProp(model.PostPropsChannelMentions)
|
|
require.NotNil(t, mentions)
|
|
mentionsMap, ok := mentions.(map[string]any)
|
|
require.True(t, ok)
|
|
require.Contains(t, mentionsMap, th.BasicChannel.Name)
|
|
})
|
|
|
|
t.Run("uses fresh display name from database on retrieval", func(t *testing.T) {
|
|
th := Setup(t).InitBasic(t)
|
|
|
|
// Post has stale data
|
|
post := &model.Post{
|
|
Message: "Check ~" + th.BasicChannel.Name,
|
|
}
|
|
post.AddProp(model.PostPropsChannelMentions, map[string]any{
|
|
th.BasicChannel.Name: map[string]any{
|
|
"display_name": "Stale Display Name",
|
|
"team_name": th.BasicTeam.Name,
|
|
},
|
|
})
|
|
|
|
result, _, err := th.App.SanitizePostMetadataForUser(th.Context, post, th.BasicUser.Id)
|
|
require.Nil(t, err)
|
|
require.NotNil(t, result)
|
|
|
|
mentions := result.GetProp(model.PostPropsChannelMentions)
|
|
require.NotNil(t, mentions)
|
|
mentionsMap, ok := mentions.(map[string]any)
|
|
require.True(t, ok)
|
|
|
|
channelData, ok := mentionsMap[th.BasicChannel.Name].(map[string]any)
|
|
require.True(t, ok)
|
|
// Should have current display name from database, not stale data
|
|
require.Equal(t, th.BasicChannel.DisplayName, channelData["display_name"])
|
|
})
|
|
|
|
t.Run("retains same-team public channel mention for non-channel-member under compliance", func(t *testing.T) {
|
|
th := Setup(t).InitBasic(t)
|
|
|
|
th.App.UpdateConfig(func(cfg *model.Config) {
|
|
cfg.ComplianceSettings.Enable = model.NewPointer(true)
|
|
})
|
|
|
|
// BasicUser2 is a member of BasicTeam (via InitBasic) but NOT of this fresh channel.
|
|
ch := th.CreateChannel(t, th.BasicTeam)
|
|
|
|
post := &model.Post{
|
|
Message: "Check ~" + ch.Name,
|
|
}
|
|
post.AddProp(model.PostPropsChannelMentions, map[string]any{
|
|
ch.Name: map[string]any{
|
|
"display_name": ch.DisplayName,
|
|
"team_name": th.BasicTeam.Name,
|
|
},
|
|
})
|
|
|
|
result, _, err := th.App.SanitizePostMetadataForUser(th.Context, post, th.BasicUser2.Id)
|
|
require.Nil(t, err)
|
|
require.NotNil(t, result)
|
|
|
|
mentions := result.GetProp(model.PostPropsChannelMentions)
|
|
require.NotNil(t, mentions)
|
|
mentionsMap, ok := mentions.(map[string]any)
|
|
require.True(t, ok)
|
|
require.Contains(t, mentionsMap, ch.Name)
|
|
})
|
|
|
|
t.Run("strips cross-team public channel mention for non-member regardless of compliance", func(t *testing.T) {
|
|
for _, compliance := range []bool{true, false} {
|
|
t.Run(fmt.Sprintf("compliance=%t", compliance), func(t *testing.T) {
|
|
th := Setup(t).InitBasic(t)
|
|
|
|
th.App.UpdateConfig(func(cfg *model.Config) {
|
|
cfg.ComplianceSettings.Enable = model.NewPointer(compliance)
|
|
})
|
|
|
|
team2 := th.CreateTeam(t)
|
|
ch := th.CreateChannel(t, team2)
|
|
|
|
post := &model.Post{
|
|
Message: "Check ~" + ch.Name,
|
|
}
|
|
post.AddProp(model.PostPropsChannelMentions, map[string]any{
|
|
ch.Name: map[string]any{
|
|
"display_name": ch.DisplayName,
|
|
"team_name": team2.Name,
|
|
},
|
|
})
|
|
|
|
result, _, err := th.App.SanitizePostMetadataForUser(th.Context, post, th.BasicUser2.Id)
|
|
require.Nil(t, err)
|
|
require.NotNil(t, result)
|
|
require.Nil(t, result.GetProp(model.PostPropsChannelMentions))
|
|
})
|
|
}
|
|
})
|
|
|
|
t.Run("strips private channel mention for non-member regardless of compliance", func(t *testing.T) {
|
|
for _, compliance := range []bool{true, false} {
|
|
t.Run(fmt.Sprintf("compliance=%t", compliance), func(t *testing.T) {
|
|
th := Setup(t).InitBasic(t)
|
|
|
|
th.App.UpdateConfig(func(cfg *model.Config) {
|
|
cfg.ComplianceSettings.Enable = model.NewPointer(compliance)
|
|
})
|
|
|
|
priv := th.CreatePrivateChannel(t, th.BasicTeam)
|
|
_ = th.App.RemoveUserFromChannel(th.Context, th.BasicUser2.Id, "", priv)
|
|
|
|
post := &model.Post{
|
|
Message: "Check ~" + priv.Name,
|
|
}
|
|
post.AddProp(model.PostPropsChannelMentions, map[string]any{
|
|
priv.Name: map[string]any{
|
|
"display_name": priv.DisplayName,
|
|
"team_name": th.BasicTeam.Name,
|
|
},
|
|
})
|
|
|
|
result, _, err := th.App.SanitizePostMetadataForUser(th.Context, post, th.BasicUser2.Id)
|
|
require.Nil(t, err)
|
|
require.NotNil(t, result)
|
|
require.Nil(t, result.GetProp(model.PostPropsChannelMentions))
|
|
})
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestFillInPostPropsWithCurrentTeamId(t *testing.T) {
|
|
mainHelper.Parallel(t)
|
|
|
|
t.Run("uses current_team_id for DM channel mention resolution", func(t *testing.T) {
|
|
th := Setup(t).InitBasic(t)
|
|
|
|
// Create a second team with a channel named "engineering"
|
|
team2, err := th.App.CreateTeam(th.Context, &model.Team{
|
|
DisplayName: "Team 2",
|
|
Name: "team2",
|
|
Type: model.TeamOpen,
|
|
})
|
|
require.Nil(t, err)
|
|
|
|
th.LinkUserToTeam(t, th.BasicUser, team2)
|
|
|
|
channel2, err := th.App.CreateChannel(th.Context, &model.Channel{
|
|
TeamId: team2.Id,
|
|
Name: "engineering",
|
|
DisplayName: "Engineering Team 2",
|
|
Type: model.ChannelTypeOpen,
|
|
}, false)
|
|
require.Nil(t, err)
|
|
|
|
// Also create channel with same name on BasicTeam
|
|
channel1, err := th.App.CreateChannel(th.Context, &model.Channel{
|
|
TeamId: th.BasicTeam.Id,
|
|
Name: "engineering",
|
|
DisplayName: "Engineering Team 1",
|
|
Type: model.ChannelTypeOpen,
|
|
}, false)
|
|
require.Nil(t, err)
|
|
|
|
// Create a DM channel
|
|
dmChannel, err := th.App.GetOrCreateDirectChannel(th.Context, th.BasicUser.Id, th.BasicUser2.Id)
|
|
require.Nil(t, err)
|
|
|
|
// Create post in DM with channel mention and current_team_id
|
|
post := &model.Post{
|
|
ChannelId: dmChannel.Id,
|
|
UserId: th.BasicUser.Id,
|
|
Message: "Check ~engineering",
|
|
}
|
|
post.AddProp(model.PostPropsCurrentTeamId, team2.Id)
|
|
|
|
// Fill in post props
|
|
appErr := th.App.FillInPostProps(th.Context, post, dmChannel)
|
|
require.Nil(t, appErr)
|
|
|
|
// Verify current_team_id was removed
|
|
require.Nil(t, post.GetProp(model.PostPropsCurrentTeamId))
|
|
|
|
// Verify channel mentions were populated from team2, not BasicTeam
|
|
mentions := post.GetProp(model.PostPropsChannelMentions)
|
|
require.NotNil(t, mentions)
|
|
mentionsMap, ok := mentions.(map[string]any)
|
|
require.True(t, ok)
|
|
|
|
channelData, ok := mentionsMap["engineering"].(map[string]any)
|
|
require.True(t, ok)
|
|
require.Equal(t, channel2.DisplayName, channelData["display_name"])
|
|
require.Equal(t, team2.Name, channelData["team_name"])
|
|
require.NotEqual(t, channel1.DisplayName, channelData["display_name"]) // Should NOT be from team1
|
|
})
|
|
|
|
t.Run("omits channel mentions from interactive blocks when mmBlocksEnabled is false", func(t *testing.T) {
|
|
th := Setup(t).InitBasic(t)
|
|
|
|
th.ConfigStore.SetReadOnlyFF(false)
|
|
defer th.ConfigStore.SetReadOnlyFF(true)
|
|
|
|
th.App.UpdateConfig(func(cfg *model.Config) { cfg.FeatureFlags.MmBlocksEnabled = false })
|
|
defer th.App.UpdateConfig(func(cfg *model.Config) { cfg.FeatureFlags.MmBlocksEnabled = true })
|
|
|
|
post := &model.Post{
|
|
ChannelId: th.BasicChannel.Id,
|
|
UserId: th.BasicUser.Id,
|
|
Message: "plain text only",
|
|
Props: model.StringInterface{
|
|
model.PostPropsMmBlocks: []any{
|
|
map[string]any{"type": "text", "text": "see ~off-topic in blocks"},
|
|
},
|
|
},
|
|
}
|
|
|
|
appErr := th.App.FillInPostProps(th.Context, post, th.BasicChannel)
|
|
require.Nil(t, appErr)
|
|
assert.Nil(t, post.GetProp(model.PostPropsChannelMentions))
|
|
})
|
|
|
|
t.Run("uses channel team_id for regular channels, ignores current_team_id", func(t *testing.T) {
|
|
th := Setup(t).InitBasic(t)
|
|
|
|
// Create a post in a regular team channel
|
|
post := &model.Post{
|
|
ChannelId: th.BasicChannel.Id,
|
|
UserId: th.BasicUser.Id,
|
|
Message: "Check ~off-topic",
|
|
}
|
|
// Add current_team_id (should be ignored for team channels)
|
|
post.AddProp(model.PostPropsCurrentTeamId, "some-other-team")
|
|
|
|
// Fill in post props
|
|
appErr := th.App.FillInPostProps(th.Context, post, th.BasicChannel)
|
|
require.Nil(t, appErr)
|
|
|
|
// Verify current_team_id was removed
|
|
require.Nil(t, post.GetProp(model.PostPropsCurrentTeamId))
|
|
|
|
// Should use BasicChannel's team_id, not current_team_id
|
|
mentions := post.GetProp(model.PostPropsChannelMentions)
|
|
if mentions != nil {
|
|
mentionsMap, ok := mentions.(map[string]any)
|
|
require.True(t, ok)
|
|
// If off-topic was found, it should be from BasicTeam
|
|
if channelData, exists := mentionsMap["off-topic"].(map[string]any); exists {
|
|
require.Equal(t, th.BasicTeam.Name, channelData["team_name"])
|
|
}
|
|
}
|
|
})
|
|
}
|