github.com/haalcala/mattermost-server-change-repo/v5@v5.33.2/app/post_metadata_test.go (about)

     1  // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
     2  // See LICENSE.txt for license information.
     3  
     4  package app
     5  
     6  import (
     7  	"bytes"
     8  	"fmt"
     9  	"image"
    10  	"image/png"
    11  	"io"
    12  	"net/http"
    13  	"net/http/httptest"
    14  	"net/url"
    15  	"os"
    16  	"strconv"
    17  	"strings"
    18  	"testing"
    19  	"time"
    20  
    21  	"github.com/dyatlov/go-opengraph/opengraph"
    22  	"github.com/stretchr/testify/assert"
    23  	"github.com/stretchr/testify/require"
    24  
    25  	"github.com/mattermost/mattermost-server/v5/model"
    26  	"github.com/mattermost/mattermost-server/v5/services/httpservice"
    27  	"github.com/mattermost/mattermost-server/v5/services/imageproxy"
    28  	"github.com/mattermost/mattermost-server/v5/utils/testutils"
    29  )
    30  
    31  func TestPreparePostListForClient(t *testing.T) {
    32  	// Most of this logic is covered by TestPreparePostForClient, so this just tests handling of multiple posts
    33  
    34  	th := Setup(t)
    35  	defer th.TearDown()
    36  
    37  	postList := model.NewPostList()
    38  	for i := 0; i < 5; i++ {
    39  		postList.AddPost(&model.Post{})
    40  	}
    41  
    42  	clientPostList := th.App.PreparePostListForClient(postList)
    43  
    44  	t.Run("doesn't mutate provided post list", func(t *testing.T) {
    45  		assert.NotEqual(t, clientPostList, postList, "should've returned a new post list")
    46  		assert.NotEqual(t, clientPostList.Posts, postList.Posts, "should've returned a new PostList.Posts")
    47  		assert.Equal(t, clientPostList.Order, postList.Order, "should've returned the existing PostList.Order")
    48  
    49  		for id, originalPost := range postList.Posts {
    50  			assert.NotEqual(t, clientPostList.Posts[id], originalPost, "should've returned new post objects")
    51  			assert.Equal(t, clientPostList.Posts[id].Id, originalPost.Id, "should've returned the same posts")
    52  		}
    53  	})
    54  
    55  	t.Run("adds metadata to each post", func(t *testing.T) {
    56  		for _, clientPost := range clientPostList.Posts {
    57  			assert.NotNil(t, clientPost.Metadata, "should've populated metadata for each post")
    58  		}
    59  	})
    60  }
    61  
    62  func TestPreparePostForClient(t *testing.T) {
    63  	var serverURL string
    64  	server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
    65  		switch r.URL.Path {
    66  		case "/":
    67  			w.Header().Set("Content-Type", "text/html")
    68  			w.Write([]byte(`
    69  			<html>
    70  			<head>
    71  			<meta property="og:image" content="` + serverURL + `/test-image3.png" />
    72  			<meta property="og:site_name" content="GitHub" />
    73  			<meta property="og:type" content="object" />
    74  			<meta property="og:title" content="hmhealey/test-files" />
    75  			<meta property="og:url" content="https://github.com/hmhealey/test-files" />
    76  			<meta property="og:description" content="Contribute to hmhealey/test-files development by creating an account on GitHub." />
    77  			</head>
    78  			</html>`))
    79  		case "/test-image1.png":
    80  			file, err := testutils.ReadTestFile("test.png")
    81  			require.NoError(t, err)
    82  
    83  			w.Header().Set("Content-Type", "image/png")
    84  			w.Write(file)
    85  		case "/test-image2.png":
    86  			file, err := testutils.ReadTestFile("test-data-graph.png")
    87  			require.NoError(t, err)
    88  
    89  			w.Header().Set("Content-Type", "image/png")
    90  			w.Write(file)
    91  		case "/test-image3.png":
    92  			file, err := testutils.ReadTestFile("qa-data-graph.png")
    93  			require.NoError(t, err)
    94  
    95  			w.Header().Set("Content-Type", "image/png")
    96  			w.Write(file)
    97  		default:
    98  			require.Fail(t, "Invalid path", r.URL.Path)
    99  		}
   100  	}))
   101  	serverURL = server.URL
   102  	defer server.Close()
   103  
   104  	setup := func(t *testing.T) *TestHelper {
   105  		th := Setup(t).InitBasic()
   106  
   107  		th.App.UpdateConfig(func(cfg *model.Config) {
   108  			*cfg.ServiceSettings.EnableLinkPreviews = true
   109  			*cfg.ImageProxySettings.Enable = false
   110  			*cfg.ServiceSettings.AllowedUntrustedInternalConnections = "localhost,127.0.0.1"
   111  		})
   112  
   113  		return th
   114  	}
   115  
   116  	t.Run("no metadata needed", func(t *testing.T) {
   117  		th := setup(t)
   118  		defer th.TearDown()
   119  
   120  		message := model.NewId()
   121  		post := &model.Post{
   122  			Message: message,
   123  		}
   124  
   125  		clientPost := th.App.PreparePostForClient(post, false, false)
   126  
   127  		t.Run("doesn't mutate provided post", func(t *testing.T) {
   128  			assert.NotEqual(t, clientPost, post, "should've returned a new post")
   129  
   130  			assert.Equal(t, message, post.Message, "shouldn't have mutated post.Message")
   131  			assert.Equal(t, (*model.PostMetadata)(nil), post.Metadata, "shouldn't have mutated post.Metadata")
   132  		})
   133  
   134  		t.Run("populates all fields", func(t *testing.T) {
   135  			assert.Equal(t, message, clientPost.Message, "shouldn't have changed Message")
   136  			assert.NotEqual(t, nil, clientPost.Metadata, "should've populated Metadata")
   137  			assert.Empty(t, clientPost.Metadata.Embeds, "should've populated Embeds")
   138  			assert.Empty(t, clientPost.Metadata.Reactions, "should've populated Reactions")
   139  			assert.Empty(t, clientPost.Metadata.Files, "should've populated Files")
   140  			assert.Empty(t, clientPost.Metadata.Emojis, "should've populated Emojis")
   141  			assert.Empty(t, clientPost.Metadata.Images, "should've populated Images")
   142  		})
   143  	})
   144  
   145  	t.Run("metadata already set", func(t *testing.T) {
   146  		th := setup(t)
   147  		defer th.TearDown()
   148  
   149  		post := th.CreatePost(th.BasicChannel)
   150  
   151  		clientPost := th.App.PreparePostForClient(post, false, false)
   152  
   153  		assert.False(t, clientPost == post, "should've returned a new post")
   154  		assert.Equal(t, clientPost, post, "shouldn't have changed any metadata")
   155  	})
   156  
   157  	t.Run("reactions", func(t *testing.T) {
   158  		th := setup(t)
   159  		defer th.TearDown()
   160  
   161  		post := th.CreatePost(th.BasicChannel)
   162  		reaction1 := th.AddReactionToPost(post, th.BasicUser, "smile")
   163  		reaction2 := th.AddReactionToPost(post, th.BasicUser2, "smile")
   164  		reaction3 := th.AddReactionToPost(post, th.BasicUser2, "ice_cream")
   165  		post.HasReactions = true
   166  
   167  		clientPost := th.App.PreparePostForClient(post, false, false)
   168  
   169  		assert.Len(t, clientPost.Metadata.Reactions, 3, "should've populated Reactions")
   170  		assert.Equal(t, reaction1, clientPost.Metadata.Reactions[0], "first reaction is incorrect")
   171  		assert.Equal(t, reaction2, clientPost.Metadata.Reactions[1], "second reaction is incorrect")
   172  		assert.Equal(t, reaction3, clientPost.Metadata.Reactions[2], "third reaction is incorrect")
   173  	})
   174  
   175  	t.Run("files", func(t *testing.T) {
   176  		th := setup(t)
   177  		defer th.TearDown()
   178  
   179  		fileInfo, err := th.App.DoUploadFile(time.Now(), th.BasicTeam.Id, th.BasicChannel.Id, th.BasicUser.Id, "test.txt", []byte("test"))
   180  		require.Nil(t, err)
   181  
   182  		post, err := th.App.CreatePost(&model.Post{
   183  			UserId:    th.BasicUser.Id,
   184  			ChannelId: th.BasicChannel.Id,
   185  			FileIds:   []string{fileInfo.Id},
   186  		}, th.BasicChannel, false, true)
   187  		require.Nil(t, err)
   188  
   189  		fileInfo.PostId = post.Id
   190  
   191  		clientPost := th.App.PreparePostForClient(post, false, false)
   192  
   193  		assert.Equal(t, []*model.FileInfo{fileInfo}, clientPost.Metadata.Files, "should've populated Files")
   194  	})
   195  
   196  	t.Run("emojis without custom emojis enabled", func(t *testing.T) {
   197  		th := setup(t)
   198  		defer th.TearDown()
   199  
   200  		th.App.UpdateConfig(func(cfg *model.Config) {
   201  			*cfg.ServiceSettings.EnableCustomEmoji = false
   202  		})
   203  
   204  		emoji := th.CreateEmoji()
   205  
   206  		post, err := th.App.CreatePost(&model.Post{
   207  			UserId:    th.BasicUser.Id,
   208  			ChannelId: th.BasicChannel.Id,
   209  			Message:   ":" + emoji.Name + ": :taco:",
   210  			Props: map[string]interface{}{
   211  				"attachments": []*model.SlackAttachment{
   212  					{
   213  						Text: ":" + emoji.Name + ":",
   214  					},
   215  				},
   216  			},
   217  		}, th.BasicChannel, false, true)
   218  		require.Nil(t, err)
   219  
   220  		th.AddReactionToPost(post, th.BasicUser, "smile")
   221  		th.AddReactionToPost(post, th.BasicUser, "angry")
   222  		th.AddReactionToPost(post, th.BasicUser2, "angry")
   223  		post.HasReactions = true
   224  
   225  		clientPost := th.App.PreparePostForClient(post, false, false)
   226  
   227  		t.Run("populates emojis", func(t *testing.T) {
   228  			assert.ElementsMatch(t, []*model.Emoji{}, clientPost.Metadata.Emojis, "should've populated empty Emojis")
   229  		})
   230  
   231  		t.Run("populates reaction counts", func(t *testing.T) {
   232  			reactions := clientPost.Metadata.Reactions
   233  			assert.Len(t, reactions, 3, "should've populated Reactions")
   234  		})
   235  	})
   236  
   237  	t.Run("emojis with custom emojis enabled", func(t *testing.T) {
   238  		th := setup(t)
   239  		defer th.TearDown()
   240  
   241  		th.App.UpdateConfig(func(cfg *model.Config) {
   242  			*cfg.ServiceSettings.EnableCustomEmoji = true
   243  		})
   244  
   245  		emoji1 := th.CreateEmoji()
   246  		emoji2 := th.CreateEmoji()
   247  		emoji3 := th.CreateEmoji()
   248  		emoji4 := th.CreateEmoji()
   249  
   250  		post, err := th.App.CreatePost(&model.Post{
   251  			UserId:    th.BasicUser.Id,
   252  			ChannelId: th.BasicChannel.Id,
   253  			Message:   ":" + emoji3.Name + ": :taco:",
   254  			Props: map[string]interface{}{
   255  				"attachments": []*model.SlackAttachment{
   256  					{
   257  						Text: ":" + emoji4.Name + ":",
   258  					},
   259  				},
   260  			},
   261  		}, th.BasicChannel, false, true)
   262  		require.Nil(t, err)
   263  
   264  		th.AddReactionToPost(post, th.BasicUser, emoji1.Name)
   265  		th.AddReactionToPost(post, th.BasicUser, emoji2.Name)
   266  		th.AddReactionToPost(post, th.BasicUser2, emoji2.Name)
   267  		th.AddReactionToPost(post, th.BasicUser2, "angry")
   268  		post.HasReactions = true
   269  
   270  		clientPost := th.App.PreparePostForClient(post, false, false)
   271  
   272  		t.Run("populates emojis", func(t *testing.T) {
   273  			assert.ElementsMatch(t, []*model.Emoji{emoji1, emoji2, emoji3, emoji4}, clientPost.Metadata.Emojis, "should've populated post.Emojis")
   274  		})
   275  
   276  		t.Run("populates reaction counts", func(t *testing.T) {
   277  			reactions := clientPost.Metadata.Reactions
   278  			assert.Len(t, reactions, 4, "should've populated Reactions")
   279  		})
   280  	})
   281  
   282  	t.Run("emojis overriding profile icon", func(t *testing.T) {
   283  		th := setup(t)
   284  		defer th.TearDown()
   285  
   286  		prepare := func(override bool, url, emoji string) *model.Post {
   287  			th.App.UpdateConfig(func(cfg *model.Config) {
   288  				*cfg.ServiceSettings.EnablePostIconOverride = override
   289  			})
   290  
   291  			post, err := th.App.CreatePost(&model.Post{
   292  				UserId:    th.BasicUser.Id,
   293  				ChannelId: th.BasicChannel.Id,
   294  				Message:   "Test",
   295  			}, th.BasicChannel, false, true)
   296  
   297  			require.Nil(t, err)
   298  
   299  			post.AddProp(model.POST_PROPS_OVERRIDE_ICON_URL, url)
   300  			post.AddProp(model.POST_PROPS_OVERRIDE_ICON_EMOJI, emoji)
   301  
   302  			return th.App.PreparePostForClient(post, false, false)
   303  		}
   304  
   305  		emoji := "basketball"
   306  		url := "http://host.com/image.png"
   307  		overridenUrl := "/static/emoji/1f3c0.png"
   308  
   309  		t.Run("does not override icon URL", func(t *testing.T) {
   310  			clientPost := prepare(false, url, emoji)
   311  
   312  			s, ok := clientPost.GetProps()[model.POST_PROPS_OVERRIDE_ICON_URL]
   313  			assert.True(t, ok)
   314  			assert.EqualValues(t, url, s)
   315  			s, ok = clientPost.GetProps()[model.POST_PROPS_OVERRIDE_ICON_EMOJI]
   316  			assert.True(t, ok)
   317  			assert.EqualValues(t, emoji, s)
   318  		})
   319  
   320  		t.Run("overrides icon URL", func(t *testing.T) {
   321  			clientPost := prepare(true, url, emoji)
   322  
   323  			s, ok := clientPost.GetProps()[model.POST_PROPS_OVERRIDE_ICON_URL]
   324  			assert.True(t, ok)
   325  			assert.EqualValues(t, overridenUrl, s)
   326  			s, ok = clientPost.GetProps()[model.POST_PROPS_OVERRIDE_ICON_EMOJI]
   327  			assert.True(t, ok)
   328  			assert.EqualValues(t, emoji, s)
   329  		})
   330  
   331  		t.Run("overrides icon URL with name surrounded by colons", func(t *testing.T) {
   332  			colonEmoji := ":basketball:"
   333  			clientPost := prepare(true, url, colonEmoji)
   334  
   335  			s, ok := clientPost.GetProps()[model.POST_PROPS_OVERRIDE_ICON_URL]
   336  			assert.True(t, ok)
   337  			assert.EqualValues(t, overridenUrl, s)
   338  			s, ok = clientPost.GetProps()[model.POST_PROPS_OVERRIDE_ICON_EMOJI]
   339  			assert.True(t, ok)
   340  			assert.EqualValues(t, colonEmoji, s)
   341  		})
   342  
   343  	})
   344  
   345  	t.Run("markdown image dimensions", func(t *testing.T) {
   346  		th := setup(t)
   347  		defer th.TearDown()
   348  
   349  		post, err := th.App.CreatePost(&model.Post{
   350  			UserId:    th.BasicUser.Id,
   351  			ChannelId: th.BasicChannel.Id,
   352  			Message:   fmt.Sprintf("This is ![our logo](%s/test-image2.png) and ![our icon](%s/test-image1.png)", server.URL, server.URL),
   353  		}, th.BasicChannel, false, true)
   354  		require.Nil(t, err)
   355  
   356  		clientPost := th.App.PreparePostForClient(post, false, false)
   357  
   358  		t.Run("populates image dimensions", func(t *testing.T) {
   359  			imageDimensions := clientPost.Metadata.Images
   360  			require.Len(t, imageDimensions, 2)
   361  			assert.Equal(t, &model.PostImage{
   362  				Format: "png",
   363  				Width:  1280,
   364  				Height: 1780,
   365  			}, imageDimensions[server.URL+"/test-image2.png"])
   366  			assert.Equal(t, &model.PostImage{
   367  				Format: "png",
   368  				Width:  408,
   369  				Height: 336,
   370  			}, imageDimensions[server.URL+"/test-image1.png"])
   371  		})
   372  	})
   373  
   374  	t.Run("post props has invalid fields", func(t *testing.T) {
   375  		th := setup(t)
   376  		defer th.TearDown()
   377  
   378  		post, err := th.App.CreatePost(&model.Post{
   379  			UserId:    th.BasicUser.Id,
   380  			ChannelId: th.BasicChannel.Id,
   381  			Message:   "some post",
   382  		}, th.BasicChannel, false, true)
   383  		require.Nil(t, err)
   384  
   385  		// this value expected to be a string
   386  		post.AddProp(model.POST_PROPS_OVERRIDE_ICON_EMOJI, true)
   387  
   388  		require.NotPanics(t, func() {
   389  			_ = th.App.PreparePostForClient(post, false, false)
   390  		})
   391  	})
   392  
   393  	t.Run("proxy linked images", func(t *testing.T) {
   394  		th := setup(t)
   395  		defer th.TearDown()
   396  
   397  		testProxyLinkedImage(t, th, false)
   398  	})
   399  
   400  	t.Run("proxy opengraph images", func(t *testing.T) {
   401  		th := setup(t)
   402  		defer th.TearDown()
   403  
   404  		testProxyOpenGraphImage(t, th, false)
   405  	})
   406  
   407  	t.Run("image embed", func(t *testing.T) {
   408  		th := setup(t)
   409  		defer th.TearDown()
   410  
   411  		post, err := th.App.CreatePost(&model.Post{
   412  			UserId:    th.BasicUser.Id,
   413  			ChannelId: th.BasicChannel.Id,
   414  			Message: `This is our logo: ` + server.URL + `/test-image2.png
   415  	And this is our icon: ` + server.URL + `/test-image1.png`,
   416  		}, th.BasicChannel, false, true)
   417  		require.Nil(t, err)
   418  
   419  		clientPost := th.App.PreparePostForClient(post, false, false)
   420  
   421  		// Reminder that only the first link gets an embed and dimensions
   422  
   423  		t.Run("populates embeds", func(t *testing.T) {
   424  			assert.ElementsMatch(t, []*model.PostEmbed{
   425  				{
   426  					Type: model.POST_EMBED_IMAGE,
   427  					URL:  server.URL + "/test-image2.png",
   428  				},
   429  			}, clientPost.Metadata.Embeds)
   430  		})
   431  
   432  		t.Run("populates image dimensions", func(t *testing.T) {
   433  			imageDimensions := clientPost.Metadata.Images
   434  			require.Len(t, imageDimensions, 1)
   435  			assert.Equal(t, &model.PostImage{
   436  				Format: "png",
   437  				Width:  1280,
   438  				Height: 1780,
   439  			}, imageDimensions[server.URL+"/test-image2.png"])
   440  		})
   441  	})
   442  
   443  	t.Run("opengraph embed", func(t *testing.T) {
   444  		th := setup(t)
   445  		defer th.TearDown()
   446  
   447  		post, err := th.App.CreatePost(&model.Post{
   448  			UserId:    th.BasicUser.Id,
   449  			ChannelId: th.BasicChannel.Id,
   450  			Message:   `This is our web page: ` + server.URL,
   451  		}, th.BasicChannel, false, true)
   452  		require.Nil(t, err)
   453  
   454  		clientPost := th.App.PreparePostForClient(post, false, false)
   455  		firstEmbed := clientPost.Metadata.Embeds[0]
   456  		ogData := firstEmbed.Data.(*opengraph.OpenGraph)
   457  
   458  		t.Run("populates embeds", func(t *testing.T) {
   459  			assert.Equal(t, firstEmbed.Type, model.POST_EMBED_OPENGRAPH)
   460  			assert.Equal(t, firstEmbed.URL, server.URL)
   461  			assert.Equal(t, ogData.Description, "Contribute to hmhealey/test-files development by creating an account on GitHub.")
   462  			assert.Equal(t, ogData.SiteName, "GitHub")
   463  			assert.Equal(t, ogData.Title, "hmhealey/test-files")
   464  			assert.Equal(t, ogData.Type, "object")
   465  			assert.Equal(t, ogData.URL, server.URL)
   466  			assert.Equal(t, ogData.Images[0].URL, server.URL+"/test-image3.png")
   467  		})
   468  
   469  		t.Run("populates image dimensions", func(t *testing.T) {
   470  			imageDimensions := clientPost.Metadata.Images
   471  			require.Len(t, imageDimensions, 1)
   472  			assert.Equal(t, &model.PostImage{
   473  				Format: "png",
   474  				Width:  1790,
   475  				Height: 1340,
   476  			}, imageDimensions[server.URL+"/test-image3.png"])
   477  		})
   478  	})
   479  
   480  	t.Run("message attachment embed", func(t *testing.T) {
   481  		th := setup(t)
   482  		defer th.TearDown()
   483  
   484  		post, err := th.App.CreatePost(&model.Post{
   485  			UserId:    th.BasicUser.Id,
   486  			ChannelId: th.BasicChannel.Id,
   487  			Props: map[string]interface{}{
   488  				"attachments": []interface{}{
   489  					map[string]interface{}{
   490  						"text": "![icon](" + server.URL + "/test-image1.png)",
   491  					},
   492  				},
   493  			},
   494  		}, th.BasicChannel, false, true)
   495  		require.Nil(t, err)
   496  
   497  		clientPost := th.App.PreparePostForClient(post, false, false)
   498  
   499  		t.Run("populates embeds", func(t *testing.T) {
   500  			assert.ElementsMatch(t, []*model.PostEmbed{
   501  				{
   502  					Type: model.POST_EMBED_MESSAGE_ATTACHMENT,
   503  				},
   504  			}, clientPost.Metadata.Embeds)
   505  		})
   506  
   507  		t.Run("populates image dimensions", func(t *testing.T) {
   508  			imageDimensions := clientPost.Metadata.Images
   509  			require.Len(t, imageDimensions, 1)
   510  			assert.Equal(t, &model.PostImage{
   511  				Format: "png",
   512  				Width:  408,
   513  				Height: 336,
   514  			}, imageDimensions[server.URL+"/test-image1.png"])
   515  		})
   516  	})
   517  
   518  	t.Run("no metadata for deleted posts", func(t *testing.T) {
   519  		th := setup(t)
   520  		defer th.TearDown()
   521  
   522  		fileInfo, err := th.App.DoUploadFile(time.Now(), th.BasicTeam.Id, th.BasicChannel.Id, th.BasicUser.Id, "test.txt", []byte("test"))
   523  		require.Nil(t, err)
   524  
   525  		post, err := th.App.CreatePost(&model.Post{
   526  			Message:   "test",
   527  			FileIds:   []string{fileInfo.Id},
   528  			UserId:    th.BasicUser.Id,
   529  			ChannelId: th.BasicChannel.Id,
   530  		}, th.BasicChannel, false, true)
   531  		require.Nil(t, err)
   532  
   533  		th.AddReactionToPost(post, th.BasicUser, "taco")
   534  
   535  		post, err = th.App.DeletePost(post.Id, th.BasicUser.Id)
   536  		require.Nil(t, err)
   537  
   538  		// DeleteAt isn't set on the post returned by App.DeletePost
   539  		post.DeleteAt = model.GetMillis()
   540  
   541  		clientPost := th.App.PreparePostForClient(post, false, false)
   542  
   543  		assert.NotEqual(t, nil, clientPost.Metadata, "should've populated Metadata“")
   544  		assert.Equal(t, "", clientPost.Message, "should've cleaned post content")
   545  		assert.Nil(t, clientPost.Metadata.Reactions, "should not have populated Reactions")
   546  		assert.Nil(t, clientPost.Metadata.Files, "should not have populated Files")
   547  	})
   548  }
   549  
   550  func TestPreparePostForClientWithImageProxy(t *testing.T) {
   551  	setup := func(t *testing.T) *TestHelper {
   552  		th := Setup(t).InitBasic()
   553  
   554  		th.App.UpdateConfig(func(cfg *model.Config) {
   555  			*cfg.ServiceSettings.EnableLinkPreviews = true
   556  			*cfg.ServiceSettings.SiteURL = "http://mymattermost.com"
   557  			*cfg.ServiceSettings.AllowedUntrustedInternalConnections = "localhost,127.0.0.1"
   558  			*cfg.ImageProxySettings.Enable = true
   559  			*cfg.ImageProxySettings.ImageProxyType = "atmos/camo"
   560  			*cfg.ImageProxySettings.RemoteImageProxyURL = "https://127.0.0.1"
   561  			*cfg.ImageProxySettings.RemoteImageProxyOptions = "foo"
   562  		})
   563  
   564  		th.Server.ImageProxy = imageproxy.MakeImageProxy(th.Server, th.Server.HTTPService, th.Server.Log)
   565  
   566  		return th
   567  	}
   568  
   569  	t.Run("proxy linked images", func(t *testing.T) {
   570  		th := setup(t)
   571  		defer th.TearDown()
   572  
   573  		testProxyLinkedImage(t, th, true)
   574  	})
   575  
   576  	t.Run("proxy opengraph images", func(t *testing.T) {
   577  		th := setup(t)
   578  		defer th.TearDown()
   579  
   580  		testProxyOpenGraphImage(t, th, true)
   581  	})
   582  }
   583  
   584  func testProxyLinkedImage(t *testing.T, th *TestHelper, shouldProxy bool) {
   585  	postTemplate := "![foo](%v)"
   586  	imageURL := "http://mydomain.com/myimage"
   587  	proxiedImageURL := "http://mymattermost.com/api/v4/image?url=http%3A%2F%2Fmydomain.com%2Fmyimage"
   588  
   589  	post := &model.Post{
   590  		UserId:    th.BasicUser.Id,
   591  		ChannelId: th.BasicChannel.Id,
   592  		Message:   fmt.Sprintf(postTemplate, imageURL),
   593  	}
   594  
   595  	clientPost := th.App.PreparePostForClient(post, false, false)
   596  
   597  	if shouldProxy {
   598  		assert.Equal(t, fmt.Sprintf(postTemplate, imageURL), post.Message, "should not have mutated original post")
   599  		assert.Equal(t, fmt.Sprintf(postTemplate, proxiedImageURL), clientPost.Message, "should've replaced linked image URLs")
   600  	} else {
   601  		assert.Equal(t, fmt.Sprintf(postTemplate, imageURL), clientPost.Message, "shouldn't have replaced linked image URLs")
   602  	}
   603  }
   604  
   605  func testProxyOpenGraphImage(t *testing.T, th *TestHelper, shouldProxy bool) {
   606  	var serverURL string
   607  	server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
   608  		switch r.URL.Path {
   609  		case "/":
   610  			w.Header().Set("Content-Type", "text/html")
   611  			w.Write([]byte(`
   612  			<html>
   613  			<head>
   614  			<meta property="og:image" content="` + serverURL + `/test-image3.png" />
   615  			<meta property="og:site_name" content="GitHub" />
   616  			<meta property="og:type" content="object" />
   617  			<meta property="og:title" content="hmhealey/test-files" />
   618  			<meta property="og:url" content="https://github.com/hmhealey/test-files" />
   619  			<meta property="og:description" content="Contribute to hmhealey/test-files development by creating an account on GitHub." />
   620  			</head>
   621  			</html>`))
   622  		case "/test-image3.png":
   623  			file, err := testutils.ReadTestFile("qa-data-graph.png")
   624  			require.NoError(t, err)
   625  
   626  			w.Header().Set("Content-Type", "image/png")
   627  			w.Write(file)
   628  		default:
   629  			require.Fail(t, "Invalid path", r.URL.Path)
   630  		}
   631  	}))
   632  	serverURL = server.URL
   633  	defer server.Close()
   634  
   635  	post, err := th.App.CreatePost(&model.Post{
   636  		UserId:    th.BasicUser.Id,
   637  		ChannelId: th.BasicChannel.Id,
   638  		Message:   `This is our web page: ` + server.URL,
   639  	}, th.BasicChannel, false, true)
   640  	require.Nil(t, err)
   641  
   642  	embeds := th.App.PreparePostForClient(post, false, false).Metadata.Embeds
   643  	require.Len(t, embeds, 1, "should have one embed")
   644  
   645  	embed := embeds[0]
   646  	assert.Equal(t, model.POST_EMBED_OPENGRAPH, embed.Type, "embed type should be OpenGraph")
   647  	assert.Equal(t, server.URL, embed.URL, "embed URL should be correct")
   648  
   649  	og, ok := embed.Data.(*opengraph.OpenGraph)
   650  	assert.True(t, ok, "data should be non-nil OpenGraph data")
   651  	assert.NotNil(t, og, "data should be non-nil OpenGraph data")
   652  	assert.Equal(t, "GitHub", og.SiteName, "OpenGraph data should be correctly populated")
   653  
   654  	require.Len(t, og.Images, 1, "OpenGraph data should have one image")
   655  
   656  	image := og.Images[0]
   657  	if shouldProxy {
   658  		assert.Equal(t, "", image.URL, "image URL should not be set with proxy")
   659  		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")
   660  	} else {
   661  		assert.Equal(t, server.URL+"/test-image3.png", image.URL, "image URL should be set")
   662  		assert.Equal(t, "", image.SecureURL, "secure image URL should not be set")
   663  	}
   664  }
   665  
   666  func TestGetEmbedForPost(t *testing.T) {
   667  	server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
   668  		if r.URL.Path == "/index.html" {
   669  			w.Header().Set("Content-Type", "text/html")
   670  			w.Write([]byte(`
   671  			<html>
   672  			<head>
   673  			<meta property="og:title" content="Title" />
   674  			</head>
   675  			</html>`))
   676  		} else if r.URL.Path == "/image.png" {
   677  			file, err := testutils.ReadTestFile("test.png")
   678  			require.NoError(t, err)
   679  
   680  			w.Header().Set("Content-Type", "image/png")
   681  			w.Write(file)
   682  		} else if r.URL.Path == "/other" {
   683  			w.Header().Set("Content-Type", "text/html")
   684  			w.Write([]byte(`
   685  			<html>
   686  			<head>
   687  			</head>
   688  			</html>`))
   689  		} else {
   690  			require.Fail(t, "Invalid path", r.URL.Path)
   691  		}
   692  	}))
   693  	defer server.Close()
   694  
   695  	ogURL := server.URL + "/index.html"
   696  	imageURL := server.URL + "/image.png"
   697  	otherURL := server.URL + "/other"
   698  
   699  	t.Run("with link previews enabled", func(t *testing.T) {
   700  		th := Setup(t)
   701  		defer th.TearDown()
   702  
   703  		th.App.UpdateConfig(func(cfg *model.Config) {
   704  			*cfg.ServiceSettings.AllowedUntrustedInternalConnections = "127.0.0.1"
   705  			*cfg.ServiceSettings.EnableLinkPreviews = true
   706  		})
   707  
   708  		t.Run("should return a message attachment when the post has one", func(t *testing.T) {
   709  			embed, err := th.App.getEmbedForPost(&model.Post{
   710  				Props: model.StringInterface{
   711  					"attachments": []*model.SlackAttachment{
   712  						{
   713  							Text: "test",
   714  						},
   715  					},
   716  				},
   717  			}, "", false)
   718  
   719  			assert.Equal(t, &model.PostEmbed{
   720  				Type: model.POST_EMBED_MESSAGE_ATTACHMENT,
   721  			}, embed)
   722  			assert.NoError(t, err)
   723  		})
   724  
   725  		t.Run("should return an image embed when the first link is an image", func(t *testing.T) {
   726  			embed, err := th.App.getEmbedForPost(&model.Post{}, imageURL, false)
   727  
   728  			assert.Equal(t, &model.PostEmbed{
   729  				Type: model.POST_EMBED_IMAGE,
   730  				URL:  imageURL,
   731  			}, embed)
   732  			assert.NoError(t, err)
   733  		})
   734  
   735  		t.Run("should return an image embed when the first link is an image", func(t *testing.T) {
   736  			embed, err := th.App.getEmbedForPost(&model.Post{}, ogURL, false)
   737  
   738  			assert.Equal(t, &model.PostEmbed{
   739  				Type: model.POST_EMBED_OPENGRAPH,
   740  				URL:  ogURL,
   741  				Data: &opengraph.OpenGraph{
   742  					Title: "Title",
   743  				},
   744  			}, embed)
   745  			assert.NoError(t, err)
   746  		})
   747  
   748  		t.Run("should return a link embed", func(t *testing.T) {
   749  			embed, err := th.App.getEmbedForPost(&model.Post{}, otherURL, false)
   750  
   751  			assert.Equal(t, &model.PostEmbed{
   752  				Type: model.POST_EMBED_LINK,
   753  				URL:  otherURL,
   754  			}, embed)
   755  			assert.NoError(t, err)
   756  		})
   757  	})
   758  
   759  	t.Run("with link previews disabled", func(t *testing.T) {
   760  		th := Setup(t)
   761  		defer th.TearDown()
   762  
   763  		th.App.UpdateConfig(func(cfg *model.Config) {
   764  			*cfg.ServiceSettings.AllowedUntrustedInternalConnections = "127.0.0.1"
   765  			*cfg.ServiceSettings.EnableLinkPreviews = false
   766  		})
   767  
   768  		t.Run("should return an embedded message attachment", func(t *testing.T) {
   769  			embed, err := th.App.getEmbedForPost(&model.Post{
   770  				Props: model.StringInterface{
   771  					"attachments": []*model.SlackAttachment{
   772  						{
   773  							Text: "test",
   774  						},
   775  					},
   776  				},
   777  			}, "", false)
   778  
   779  			assert.Equal(t, &model.PostEmbed{
   780  				Type: model.POST_EMBED_MESSAGE_ATTACHMENT,
   781  			}, embed)
   782  			assert.NoError(t, err)
   783  		})
   784  
   785  		t.Run("should not return an opengraph embed", func(t *testing.T) {
   786  			embed, err := th.App.getEmbedForPost(&model.Post{}, ogURL, false)
   787  
   788  			assert.Nil(t, embed)
   789  			assert.NoError(t, err)
   790  		})
   791  
   792  		t.Run("should not return an image embed", func(t *testing.T) {
   793  			embed, err := th.App.getEmbedForPost(&model.Post{}, imageURL, false)
   794  
   795  			assert.Nil(t, embed)
   796  			assert.NoError(t, err)
   797  		})
   798  
   799  		t.Run("should not return a link embed", func(t *testing.T) {
   800  			embed, err := th.App.getEmbedForPost(&model.Post{}, otherURL, false)
   801  
   802  			assert.Nil(t, embed)
   803  			assert.NoError(t, err)
   804  		})
   805  	})
   806  }
   807  
   808  func TestGetImagesForPost(t *testing.T) {
   809  	t.Run("with an image link", func(t *testing.T) {
   810  		th := Setup(t)
   811  		defer th.TearDown()
   812  
   813  		th.App.UpdateConfig(func(cfg *model.Config) {
   814  			*cfg.ServiceSettings.AllowedUntrustedInternalConnections = "127.0.0.1"
   815  		})
   816  
   817  		server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
   818  			file, err := testutils.ReadTestFile("test.png")
   819  			require.NoError(t, err)
   820  
   821  			w.Header().Set("Content-Type", "image/png")
   822  			w.Write(file)
   823  		}))
   824  
   825  		post := &model.Post{
   826  			Metadata: &model.PostMetadata{},
   827  		}
   828  		imageURL := server.URL + "/image.png"
   829  
   830  		images := th.App.getImagesForPost(post, []string{imageURL}, false)
   831  
   832  		assert.Equal(t, images, map[string]*model.PostImage{
   833  			imageURL: {
   834  				Format: "png",
   835  				Width:  408,
   836  				Height: 336,
   837  			},
   838  		})
   839  	})
   840  
   841  	t.Run("with an invalid image link", func(t *testing.T) {
   842  		th := Setup(t)
   843  		defer th.TearDown()
   844  
   845  		th.App.UpdateConfig(func(cfg *model.Config) {
   846  			*cfg.ServiceSettings.AllowedUntrustedInternalConnections = "127.0.0.1"
   847  		})
   848  
   849  		server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
   850  			w.WriteHeader(http.StatusInternalServerError)
   851  		}))
   852  
   853  		post := &model.Post{
   854  			Metadata: &model.PostMetadata{},
   855  		}
   856  		imageURL := server.URL + "/bad_image.png"
   857  
   858  		images := th.App.getImagesForPost(post, []string{imageURL}, false)
   859  
   860  		assert.Equal(t, images, map[string]*model.PostImage{})
   861  	})
   862  
   863  	t.Run("for an OpenGraph image", func(t *testing.T) {
   864  		th := Setup(t)
   865  		defer th.TearDown()
   866  
   867  		th.App.UpdateConfig(func(cfg *model.Config) {
   868  			*cfg.ServiceSettings.AllowedUntrustedInternalConnections = "127.0.0.1"
   869  		})
   870  
   871  		server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
   872  			if r.URL.Path == "/image.png" {
   873  				w.Header().Set("Content-Type", "image/png")
   874  
   875  				img := image.NewGray(image.Rect(0, 0, 200, 300))
   876  
   877  				var encoder png.Encoder
   878  				encoder.Encode(w, img)
   879  			} else {
   880  				w.WriteHeader(http.StatusNotFound)
   881  			}
   882  		}))
   883  		defer server.Close()
   884  
   885  		ogURL := server.URL + "/index.html"
   886  		imageURL := server.URL + "/image.png"
   887  
   888  		post := &model.Post{
   889  			Metadata: &model.PostMetadata{
   890  				Embeds: []*model.PostEmbed{
   891  					{
   892  						Type: model.POST_EMBED_OPENGRAPH,
   893  						URL:  ogURL,
   894  						Data: &opengraph.OpenGraph{
   895  							Images: []*opengraph.Image{
   896  								{
   897  									URL: imageURL,
   898  								},
   899  							},
   900  						},
   901  					},
   902  				},
   903  			},
   904  		}
   905  
   906  		images := th.App.getImagesForPost(post, []string{}, false)
   907  
   908  		assert.Equal(t, images, map[string]*model.PostImage{
   909  			imageURL: {
   910  				Format: "png",
   911  				Width:  200,
   912  				Height: 300,
   913  			},
   914  		})
   915  	})
   916  
   917  	t.Run("with an OpenGraph image with a secure_url", func(t *testing.T) {
   918  		th := Setup(t)
   919  		defer th.TearDown()
   920  
   921  		th.App.UpdateConfig(func(cfg *model.Config) {
   922  			*cfg.ServiceSettings.AllowedUntrustedInternalConnections = "127.0.0.1"
   923  		})
   924  
   925  		server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
   926  			if r.URL.Path == "/secure_image.png" {
   927  				w.Header().Set("Content-Type", "image/png")
   928  
   929  				img := image.NewGray(image.Rect(0, 0, 300, 400))
   930  
   931  				var encoder png.Encoder
   932  				encoder.Encode(w, img)
   933  			} else {
   934  				w.WriteHeader(http.StatusNotFound)
   935  			}
   936  		}))
   937  		defer server.Close()
   938  
   939  		ogURL := server.URL + "/index.html"
   940  		imageURL := server.URL + "/secure_image.png"
   941  
   942  		post := &model.Post{
   943  			Metadata: &model.PostMetadata{
   944  				Embeds: []*model.PostEmbed{
   945  					{
   946  						Type: model.POST_EMBED_OPENGRAPH,
   947  						URL:  ogURL,
   948  						Data: &opengraph.OpenGraph{
   949  							Images: []*opengraph.Image{
   950  								{
   951  									SecureURL: imageURL,
   952  								},
   953  							},
   954  						},
   955  					},
   956  				},
   957  			},
   958  		}
   959  
   960  		images := th.App.getImagesForPost(post, []string{}, false)
   961  
   962  		assert.Equal(t, images, map[string]*model.PostImage{
   963  			imageURL: {
   964  				Format: "png",
   965  				Width:  300,
   966  				Height: 400,
   967  			},
   968  		})
   969  	})
   970  
   971  	t.Run("with an OpenGraph image with a secure_url and no dimensions", func(t *testing.T) {
   972  		th := Setup(t)
   973  		defer th.TearDown()
   974  
   975  		th.App.UpdateConfig(func(cfg *model.Config) {
   976  			*cfg.ServiceSettings.AllowedUntrustedInternalConnections = "127.0.0.1"
   977  		})
   978  
   979  		server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
   980  			if r.URL.Path == "/secure_image.png" {
   981  				w.Header().Set("Content-Type", "image/png")
   982  
   983  				img := image.NewGray(image.Rect(0, 0, 400, 500))
   984  
   985  				var encoder png.Encoder
   986  				encoder.Encode(w, img)
   987  			} else {
   988  				w.WriteHeader(http.StatusNotFound)
   989  			}
   990  		}))
   991  
   992  		ogURL := server.URL + "/index.html"
   993  		imageURL := server.URL + "/secure_image.png"
   994  
   995  		post := &model.Post{
   996  			Metadata: &model.PostMetadata{
   997  				Embeds: []*model.PostEmbed{
   998  					{
   999  						Type: model.POST_EMBED_OPENGRAPH,
  1000  						URL:  ogURL,
  1001  						Data: &opengraph.OpenGraph{
  1002  							Images: []*opengraph.Image{
  1003  								{
  1004  									URL:       server.URL + "/image.png",
  1005  									SecureURL: imageURL,
  1006  								},
  1007  							},
  1008  						},
  1009  					},
  1010  				},
  1011  			},
  1012  		}
  1013  
  1014  		images := th.App.getImagesForPost(post, []string{}, false)
  1015  
  1016  		assert.Equal(t, images, map[string]*model.PostImage{
  1017  			imageURL: {
  1018  				Format: "png",
  1019  				Width:  400,
  1020  				Height: 500,
  1021  			},
  1022  		})
  1023  	})
  1024  }
  1025  
  1026  func TestGetEmojiNamesForString(t *testing.T) {
  1027  	testCases := []struct {
  1028  		Description string
  1029  		Input       string
  1030  		Expected    []string
  1031  	}{
  1032  		{
  1033  			Description: "no emojis",
  1034  			Input:       "this is a string",
  1035  			Expected:    []string{},
  1036  		},
  1037  		{
  1038  			Description: "one emoji",
  1039  			Input:       "this is an :emoji1: string",
  1040  			Expected:    []string{"emoji1"},
  1041  		},
  1042  		{
  1043  			Description: "two emojis",
  1044  			Input:       "this is a :emoji3: :emoji2: string",
  1045  			Expected:    []string{"emoji3", "emoji2"},
  1046  		},
  1047  		{
  1048  			Description: "punctuation around emojis",
  1049  			Input:       ":emoji3:/:emoji1: (:emoji2:)",
  1050  			Expected:    []string{"emoji3", "emoji1", "emoji2"},
  1051  		},
  1052  		{
  1053  			Description: "adjacent emojis",
  1054  			Input:       ":emoji3::emoji1:",
  1055  			Expected:    []string{"emoji3", "emoji1"},
  1056  		},
  1057  		{
  1058  			Description: "duplicate emojis",
  1059  			Input:       ":emoji1: :emoji1: :emoji1::emoji2::emoji2: :emoji1:",
  1060  			Expected:    []string{"emoji1", "emoji1", "emoji1", "emoji2", "emoji2", "emoji1"},
  1061  		},
  1062  		{
  1063  			Description: "fake emojis",
  1064  			Input:       "these don't exist :tomato: :potato: :rotato:",
  1065  			Expected:    []string{"tomato", "potato", "rotato"},
  1066  		},
  1067  	}
  1068  
  1069  	for _, testCase := range testCases {
  1070  		testCase := testCase
  1071  		t.Run(testCase.Description, func(t *testing.T) {
  1072  			emojis := getEmojiNamesForString(testCase.Input)
  1073  			assert.ElementsMatch(t, emojis, testCase.Expected, "received incorrect emoji names")
  1074  		})
  1075  	}
  1076  }
  1077  
  1078  func TestGetEmojiNamesForPost(t *testing.T) {
  1079  	testCases := []struct {
  1080  		Description string
  1081  		Post        *model.Post
  1082  		Reactions   []*model.Reaction
  1083  		Expected    []string
  1084  	}{
  1085  		{
  1086  			Description: "no emojis",
  1087  			Post: &model.Post{
  1088  				Message: "this is a post",
  1089  			},
  1090  			Expected: []string{},
  1091  		},
  1092  		{
  1093  			Description: "in post message",
  1094  			Post: &model.Post{
  1095  				Message: "this is :emoji:",
  1096  			},
  1097  			Expected: []string{"emoji"},
  1098  		},
  1099  		{
  1100  			Description: "in reactions",
  1101  			Post:        &model.Post{},
  1102  			Reactions: []*model.Reaction{
  1103  				{
  1104  					EmojiName: "emoji1",
  1105  				},
  1106  				{
  1107  					EmojiName: "emoji2",
  1108  				},
  1109  			},
  1110  			Expected: []string{"emoji1", "emoji2"},
  1111  		},
  1112  		{
  1113  			Description: "in message attachments",
  1114  			Post: &model.Post{
  1115  				Message: "this is a post",
  1116  				Props: map[string]interface{}{
  1117  					"attachments": []*model.SlackAttachment{
  1118  						{
  1119  							Text:    ":emoji1:",
  1120  							Pretext: ":emoji2:",
  1121  						},
  1122  						{
  1123  							Fields: []*model.SlackAttachmentField{
  1124  								{
  1125  									Value: ":emoji3:",
  1126  								},
  1127  								{
  1128  									Value: ":emoji4:",
  1129  								},
  1130  							},
  1131  						},
  1132  					},
  1133  				},
  1134  			},
  1135  			Expected: []string{"emoji1", "emoji2", "emoji3", "emoji4"},
  1136  		},
  1137  		{
  1138  			Description: "with duplicates",
  1139  			Post: &model.Post{
  1140  				Message: "this is :emoji1",
  1141  				Props: map[string]interface{}{
  1142  					"attachments": []*model.SlackAttachment{
  1143  						{
  1144  							Text:    ":emoji2:",
  1145  							Pretext: ":emoji2:",
  1146  							Fields: []*model.SlackAttachmentField{
  1147  								{
  1148  									Value: ":emoji3:",
  1149  								},
  1150  								{
  1151  									Value: ":emoji1:",
  1152  								},
  1153  							},
  1154  						},
  1155  					},
  1156  				},
  1157  			},
  1158  			Expected: []string{"emoji1", "emoji2", "emoji3"},
  1159  		},
  1160  	}
  1161  
  1162  	for _, testCase := range testCases {
  1163  		testCase := testCase
  1164  		t.Run(testCase.Description, func(t *testing.T) {
  1165  			emojis := getEmojiNamesForPost(testCase.Post, testCase.Reactions)
  1166  			assert.ElementsMatch(t, emojis, testCase.Expected, "received incorrect emoji names")
  1167  		})
  1168  	}
  1169  }
  1170  
  1171  func TestGetCustomEmojisForPost(t *testing.T) {
  1172  	th := Setup(t).InitBasic()
  1173  	defer th.TearDown()
  1174  
  1175  	th.App.UpdateConfig(func(cfg *model.Config) {
  1176  		*cfg.ServiceSettings.EnableCustomEmoji = true
  1177  	})
  1178  
  1179  	emojis := []*model.Emoji{
  1180  		th.CreateEmoji(),
  1181  		th.CreateEmoji(),
  1182  		th.CreateEmoji(),
  1183  		th.CreateEmoji(),
  1184  		th.CreateEmoji(),
  1185  		th.CreateEmoji(),
  1186  	}
  1187  
  1188  	t.Run("from different parts of the post", func(t *testing.T) {
  1189  		reactions := []*model.Reaction{
  1190  			{
  1191  				UserId:    th.BasicUser.Id,
  1192  				EmojiName: emojis[0].Name,
  1193  			},
  1194  		}
  1195  
  1196  		post := &model.Post{
  1197  			Message: ":" + emojis[1].Name + ":",
  1198  			Props: map[string]interface{}{
  1199  				"attachments": []*model.SlackAttachment{
  1200  					{
  1201  						Pretext: ":" + emojis[2].Name + ":",
  1202  						Text:    ":" + emojis[3].Name + ":",
  1203  						Fields: []*model.SlackAttachmentField{
  1204  							{
  1205  								Value: ":" + emojis[4].Name + ":",
  1206  							},
  1207  							{
  1208  								Value: ":" + emojis[5].Name + ":",
  1209  							},
  1210  						},
  1211  					},
  1212  				},
  1213  			},
  1214  		}
  1215  
  1216  		emojisForPost, err := th.App.getCustomEmojisForPost(post, reactions)
  1217  		assert.Nil(t, err, "failed to get emojis for post")
  1218  		assert.ElementsMatch(t, emojisForPost, emojis, "received incorrect emojis")
  1219  	})
  1220  
  1221  	t.Run("with emojis that don't exist", func(t *testing.T) {
  1222  		post := &model.Post{
  1223  			Message: ":secret: :" + emojis[0].Name + ":",
  1224  			Props: map[string]interface{}{
  1225  				"attachments": []*model.SlackAttachment{
  1226  					{
  1227  						Text: ":imaginary:",
  1228  					},
  1229  				},
  1230  			},
  1231  		}
  1232  
  1233  		emojisForPost, err := th.App.getCustomEmojisForPost(post, nil)
  1234  		assert.Nil(t, err, "failed to get emojis for post")
  1235  		assert.ElementsMatch(t, emojisForPost, []*model.Emoji{emojis[0]}, "received incorrect emojis")
  1236  	})
  1237  
  1238  	t.Run("with no emojis", func(t *testing.T) {
  1239  		post := &model.Post{
  1240  			Message: "this post is boring",
  1241  			Props:   map[string]interface{}{},
  1242  		}
  1243  
  1244  		emojisForPost, err := th.App.getCustomEmojisForPost(post, nil)
  1245  		assert.Nil(t, err, "failed to get emojis for post")
  1246  		assert.ElementsMatch(t, emojisForPost, []*model.Emoji{}, "should have received no emojis")
  1247  	})
  1248  }
  1249  
  1250  func TestGetFirstLinkAndImages(t *testing.T) {
  1251  	for name, testCase := range map[string]struct {
  1252  		Input             string
  1253  		ExpectedFirstLink string
  1254  		ExpectedImages    []string
  1255  	}{
  1256  		"no links or images": {
  1257  			Input:             "this is a string",
  1258  			ExpectedFirstLink: "",
  1259  			ExpectedImages:    []string{},
  1260  		},
  1261  		"http link": {
  1262  			Input:             "this is a http://example.com",
  1263  			ExpectedFirstLink: "http://example.com",
  1264  			ExpectedImages:    []string{},
  1265  		},
  1266  		"www link": {
  1267  			Input:             "this is a www.example.com",
  1268  			ExpectedFirstLink: "http://www.example.com",
  1269  			ExpectedImages:    []string{},
  1270  		},
  1271  		"image": {
  1272  			Input:             "this is a ![our logo](http://example.com/logo)",
  1273  			ExpectedFirstLink: "",
  1274  			ExpectedImages:    []string{"http://example.com/logo"},
  1275  		},
  1276  		"multiple images": {
  1277  			Input:             "this is a ![our logo](http://example.com/logo) and ![their logo](http://example.com/logo2) and ![my logo](http://example.com/logo3)",
  1278  			ExpectedFirstLink: "",
  1279  			ExpectedImages:    []string{"http://example.com/logo", "http://example.com/logo2", "http://example.com/logo3"},
  1280  		},
  1281  		"multiple images with duplicate": {
  1282  			Input:             "this is a ![our logo](http://example.com/logo) and ![their logo](http://example.com/logo2) and ![my logo which is their logo](http://example.com/logo2)",
  1283  			ExpectedFirstLink: "",
  1284  			ExpectedImages:    []string{"http://example.com/logo", "http://example.com/logo2", "http://example.com/logo2"},
  1285  		},
  1286  		"reference image": {
  1287  			Input: `this is a ![our logo][logo]
  1288  
  1289  [logo]: http://example.com/logo`,
  1290  			ExpectedFirstLink: "",
  1291  			ExpectedImages:    []string{"http://example.com/logo"},
  1292  		},
  1293  		"image and link": {
  1294  			Input:             "this is a https://example.com and ![our logo](https://example.com/logo)",
  1295  			ExpectedFirstLink: "https://example.com",
  1296  			ExpectedImages:    []string{"https://example.com/logo"},
  1297  		},
  1298  		"markdown links (not returned)": {
  1299  			Input: `this is a [our page](http://example.com) and [another page][]
  1300  
  1301  [another page]: http://www.exaple.com/another_page`,
  1302  			ExpectedFirstLink: "",
  1303  			ExpectedImages:    []string{},
  1304  		},
  1305  	} {
  1306  		t.Run(name, func(t *testing.T) {
  1307  			firstLink, images := getFirstLinkAndImages(testCase.Input)
  1308  
  1309  			assert.Equal(t, firstLink, testCase.ExpectedFirstLink)
  1310  			assert.Equal(t, images, testCase.ExpectedImages)
  1311  		})
  1312  	}
  1313  }
  1314  
  1315  func TestGetImagesInMessageAttachments(t *testing.T) {
  1316  	for _, test := range []struct {
  1317  		Name     string
  1318  		Post     *model.Post
  1319  		Expected []string
  1320  	}{
  1321  		{
  1322  			Name:     "no attachments",
  1323  			Post:     &model.Post{},
  1324  			Expected: []string{},
  1325  		},
  1326  		{
  1327  			Name: "empty attachments",
  1328  			Post: &model.Post{
  1329  				Props: map[string]interface{}{
  1330  					"attachments": []*model.SlackAttachment{},
  1331  				},
  1332  			},
  1333  			Expected: []string{},
  1334  		},
  1335  		{
  1336  			Name: "attachment with no fields that can contain images",
  1337  			Post: &model.Post{
  1338  				Props: map[string]interface{}{
  1339  					"attachments": []*model.SlackAttachment{
  1340  						{
  1341  							Title: "This is the title",
  1342  						},
  1343  					},
  1344  				},
  1345  			},
  1346  			Expected: []string{},
  1347  		},
  1348  		{
  1349  			Name: "images in text",
  1350  			Post: &model.Post{
  1351  				Props: map[string]interface{}{
  1352  					"attachments": []*model.SlackAttachment{
  1353  						{
  1354  							Text: "![logo](https://example.com/logo) and ![icon](https://example.com/icon)",
  1355  						},
  1356  					},
  1357  				},
  1358  			},
  1359  			Expected: []string{"https://example.com/logo", "https://example.com/icon"},
  1360  		},
  1361  		{
  1362  			Name: "images in pretext",
  1363  			Post: &model.Post{
  1364  				Props: map[string]interface{}{
  1365  					"attachments": []*model.SlackAttachment{
  1366  						{
  1367  							Pretext: "![logo](https://example.com/logo1) and ![icon](https://example.com/icon1)",
  1368  						},
  1369  					},
  1370  				},
  1371  			},
  1372  			Expected: []string{"https://example.com/logo1", "https://example.com/icon1"},
  1373  		},
  1374  		{
  1375  			Name: "images in fields",
  1376  			Post: &model.Post{
  1377  				Props: map[string]interface{}{
  1378  					"attachments": []*model.SlackAttachment{
  1379  						{
  1380  							Fields: []*model.SlackAttachmentField{
  1381  								{
  1382  									Value: "![logo](https://example.com/logo2) and ![icon](https://example.com/icon2)",
  1383  								},
  1384  							},
  1385  						},
  1386  					},
  1387  				},
  1388  			},
  1389  			Expected: []string{"https://example.com/logo2", "https://example.com/icon2"},
  1390  		},
  1391  		{
  1392  			Name: "image in author_icon",
  1393  			Post: &model.Post{
  1394  				Props: map[string]interface{}{
  1395  					"attachments": []*model.SlackAttachment{
  1396  						{
  1397  							AuthorIcon: "https://example.com/icon2",
  1398  						},
  1399  					},
  1400  				},
  1401  			},
  1402  			Expected: []string{"https://example.com/icon2"},
  1403  		},
  1404  		{
  1405  			Name: "image in image_url",
  1406  			Post: &model.Post{
  1407  				Props: map[string]interface{}{
  1408  					"attachments": []*model.SlackAttachment{
  1409  						{
  1410  							ImageURL: "https://example.com/image",
  1411  						},
  1412  					},
  1413  				},
  1414  			},
  1415  			Expected: []string{"https://example.com/image"},
  1416  		},
  1417  		{
  1418  			Name: "image in thumb_url",
  1419  			Post: &model.Post{
  1420  				Props: map[string]interface{}{
  1421  					"attachments": []*model.SlackAttachment{
  1422  						{
  1423  							ThumbURL: "https://example.com/image",
  1424  						},
  1425  					},
  1426  				},
  1427  			},
  1428  			Expected: []string{"https://example.com/image"},
  1429  		},
  1430  		{
  1431  			Name: "image in footer_icon",
  1432  			Post: &model.Post{
  1433  				Props: map[string]interface{}{
  1434  					"attachments": []*model.SlackAttachment{
  1435  						{
  1436  							FooterIcon: "https://example.com/image",
  1437  						},
  1438  					},
  1439  				},
  1440  			},
  1441  			Expected: []string{"https://example.com/image"},
  1442  		},
  1443  		{
  1444  			Name: "images in multiple fields",
  1445  			Post: &model.Post{
  1446  				Props: map[string]interface{}{
  1447  					"attachments": []*model.SlackAttachment{
  1448  						{
  1449  							Fields: []*model.SlackAttachmentField{
  1450  								{
  1451  									Value: "![logo](https://example.com/logo)",
  1452  								},
  1453  								{
  1454  									Value: "![icon](https://example.com/icon)",
  1455  								},
  1456  							},
  1457  						},
  1458  					},
  1459  				},
  1460  			},
  1461  			Expected: []string{"https://example.com/logo", "https://example.com/icon"},
  1462  		},
  1463  		{
  1464  			Name: "non-string field",
  1465  			Post: &model.Post{
  1466  				Props: map[string]interface{}{
  1467  					"attachments": []*model.SlackAttachment{
  1468  						{
  1469  							Fields: []*model.SlackAttachmentField{
  1470  								{
  1471  									Value: 77,
  1472  								},
  1473  							},
  1474  						},
  1475  					},
  1476  				},
  1477  			},
  1478  			Expected: []string{},
  1479  		},
  1480  		{
  1481  			Name: "images in multiple locations",
  1482  			Post: &model.Post{
  1483  				Props: map[string]interface{}{
  1484  					"attachments": []*model.SlackAttachment{
  1485  						{
  1486  							Text:    "![text](https://example.com/text)",
  1487  							Pretext: "![pretext](https://example.com/pretext)",
  1488  							Fields: []*model.SlackAttachmentField{
  1489  								{
  1490  									Value: "![field1](https://example.com/field1)",
  1491  								},
  1492  								{
  1493  									Value: "![field2](https://example.com/field2)",
  1494  								},
  1495  							},
  1496  						},
  1497  					},
  1498  				},
  1499  			},
  1500  			Expected: []string{"https://example.com/text", "https://example.com/pretext", "https://example.com/field1", "https://example.com/field2"},
  1501  		},
  1502  		{
  1503  			Name: "multiple attachments",
  1504  			Post: &model.Post{
  1505  				Props: map[string]interface{}{
  1506  					"attachments": []*model.SlackAttachment{
  1507  						{
  1508  							Text: "![logo](https://example.com/logo)",
  1509  						},
  1510  						{
  1511  							Text: "![icon](https://example.com/icon)",
  1512  						},
  1513  					},
  1514  				},
  1515  			},
  1516  			Expected: []string{"https://example.com/logo", "https://example.com/icon"},
  1517  		},
  1518  	} {
  1519  		t.Run(test.Name, func(t *testing.T) {
  1520  			images := getImagesInMessageAttachments(test.Post)
  1521  
  1522  			assert.ElementsMatch(t, images, test.Expected)
  1523  		})
  1524  	}
  1525  }
  1526  
  1527  func TestGetLinkMetadata(t *testing.T) {
  1528  	setup := func(t *testing.T) *TestHelper {
  1529  		th := Setup(t)
  1530  
  1531  		th.App.UpdateConfig(func(cfg *model.Config) {
  1532  			*cfg.ServiceSettings.AllowedUntrustedInternalConnections = "127.0.0.1"
  1533  		})
  1534  
  1535  		linkCache.Purge()
  1536  
  1537  		return th
  1538  	}
  1539  
  1540  	server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
  1541  		params := r.URL.Query()
  1542  
  1543  		writeImage := func(height, width int) {
  1544  
  1545  			img := image.NewGray(image.Rect(0, 0, height, width))
  1546  
  1547  			var encoder png.Encoder
  1548  
  1549  			encoder.Encode(w, img)
  1550  		}
  1551  
  1552  		writeHTML := func(title string) {
  1553  			w.Header().Set("Content-Type", "text/html")
  1554  
  1555  			w.Write([]byte(`
  1556  				<html prefix="og:http://ogp.me/ns#">
  1557  				<head>
  1558  				<meta property="og:title" content="` + title + `" />
  1559  				</head>
  1560  				<body>
  1561  				</body>
  1562  				</html>`))
  1563  		}
  1564  
  1565  		if strings.HasPrefix(r.URL.Path, "/image") {
  1566  			height, _ := strconv.ParseInt(params["height"][0], 10, 0)
  1567  			width, _ := strconv.ParseInt(params["width"][0], 10, 0)
  1568  
  1569  			writeImage(int(height), int(width))
  1570  		} else if strings.HasPrefix(r.URL.Path, "/opengraph") {
  1571  			writeHTML(params["title"][0])
  1572  		} else if strings.HasPrefix(r.URL.Path, "/json") {
  1573  			w.Header().Set("Content-Type", "application/json")
  1574  
  1575  			w.Write([]byte("true"))
  1576  		} else if strings.HasPrefix(r.URL.Path, "/timeout") {
  1577  			w.Header().Set("Content-Type", "text/html")
  1578  
  1579  			w.Write([]byte("<html>"))
  1580  			select {
  1581  			case <-time.After(60 * time.Second):
  1582  			case <-r.Context().Done():
  1583  			}
  1584  			w.Write([]byte("</html>"))
  1585  		} else if strings.HasPrefix(r.URL.Path, "/mixed") {
  1586  			for _, acceptedType := range r.Header["Accept"] {
  1587  				if strings.HasPrefix(acceptedType, "image/*") || strings.HasPrefix(acceptedType, "image/png") {
  1588  					writeImage(10, 10)
  1589  				} else if strings.HasPrefix(acceptedType, "text/html") {
  1590  					writeHTML("mixed")
  1591  				}
  1592  			}
  1593  		} else {
  1594  			w.WriteHeader(http.StatusInternalServerError)
  1595  		}
  1596  	}))
  1597  	defer server.Close()
  1598  
  1599  	t.Run("in-memory cache", func(t *testing.T) {
  1600  		th := setup(t)
  1601  		defer th.TearDown()
  1602  
  1603  		requestURL := server.URL + "/cached"
  1604  		timestamp := int64(1547510400000)
  1605  		title := "from cache"
  1606  
  1607  		cacheLinkMetadata(requestURL, timestamp, &opengraph.OpenGraph{Title: title}, nil)
  1608  
  1609  		t.Run("should use cache if cached entry exists", func(t *testing.T) {
  1610  			_, _, ok := getLinkMetadataFromCache(requestURL, timestamp)
  1611  			require.True(t, ok, "data should already exist in in-memory cache")
  1612  
  1613  			_, _, ok = th.App.getLinkMetadataFromDatabase(requestURL, timestamp)
  1614  			require.False(t, ok, "data should not exist in database")
  1615  
  1616  			og, img, err := th.App.getLinkMetadata(requestURL, timestamp, false)
  1617  
  1618  			require.NotNil(t, og)
  1619  			assert.Nil(t, img)
  1620  			assert.NoError(t, err)
  1621  			assert.Equal(t, title, og.Title)
  1622  		})
  1623  
  1624  		t.Run("should use cache if cached entry exists near time", func(t *testing.T) {
  1625  			_, _, ok := getLinkMetadataFromCache(requestURL, timestamp)
  1626  			require.True(t, ok, "data should already exist in in-memory cache")
  1627  
  1628  			_, _, ok = th.App.getLinkMetadataFromDatabase(requestURL, timestamp)
  1629  			require.False(t, ok, "data should not exist in database")
  1630  
  1631  			og, img, err := th.App.getLinkMetadata(requestURL, timestamp+60*1000, false)
  1632  
  1633  			require.NotNil(t, og)
  1634  			assert.Nil(t, img)
  1635  			assert.NoError(t, err)
  1636  			assert.Equal(t, title, og.Title)
  1637  		})
  1638  
  1639  		t.Run("should not use cache if URL is different", func(t *testing.T) {
  1640  			differentURL := server.URL + "/other"
  1641  
  1642  			_, _, ok := getLinkMetadataFromCache(differentURL, timestamp)
  1643  			require.False(t, ok, "data should not exist in in-memory cache")
  1644  
  1645  			_, _, ok = th.App.getLinkMetadataFromDatabase(differentURL, timestamp)
  1646  			require.False(t, ok, "data should not exist in database")
  1647  
  1648  			og, img, err := th.App.getLinkMetadata(differentURL, timestamp, false)
  1649  
  1650  			assert.Nil(t, og)
  1651  			assert.Nil(t, img)
  1652  			assert.NoError(t, err)
  1653  		})
  1654  
  1655  		t.Run("should not use cache if timestamp is different", func(t *testing.T) {
  1656  			differentTimestamp := timestamp + 60*60*1000
  1657  
  1658  			_, _, ok := getLinkMetadataFromCache(requestURL, differentTimestamp)
  1659  			require.False(t, ok, "data should not exist in in-memory cache")
  1660  
  1661  			_, _, ok = th.App.getLinkMetadataFromDatabase(requestURL, differentTimestamp)
  1662  			require.False(t, ok, "data should not exist in database")
  1663  
  1664  			og, img, err := th.App.getLinkMetadata(requestURL, differentTimestamp, false)
  1665  
  1666  			assert.Nil(t, og)
  1667  			assert.Nil(t, img)
  1668  			assert.NoError(t, err)
  1669  		})
  1670  	})
  1671  
  1672  	t.Run("database cache", func(t *testing.T) {
  1673  		th := setup(t)
  1674  		defer th.TearDown()
  1675  
  1676  		requestURL := server.URL
  1677  		timestamp := int64(1547510400000)
  1678  		title := "from database"
  1679  
  1680  		th.App.saveLinkMetadataToDatabase(requestURL, timestamp, &opengraph.OpenGraph{Title: title}, nil)
  1681  
  1682  		t.Run("should use database if saved entry exists", func(t *testing.T) {
  1683  			linkCache.Purge()
  1684  
  1685  			_, _, ok := getLinkMetadataFromCache(requestURL, timestamp)
  1686  			require.False(t, ok, "data should not exist in in-memory cache")
  1687  
  1688  			_, _, ok = th.App.getLinkMetadataFromDatabase(requestURL, timestamp)
  1689  			require.True(t, ok, "data should already exist in database")
  1690  
  1691  			og, img, err := th.App.getLinkMetadata(requestURL, timestamp, false)
  1692  
  1693  			require.NotNil(t, og)
  1694  			assert.Nil(t, img)
  1695  			assert.NoError(t, err)
  1696  			assert.Equal(t, title, og.Title)
  1697  		})
  1698  
  1699  		t.Run("should use database if saved entry exists near time", func(t *testing.T) {
  1700  			linkCache.Purge()
  1701  
  1702  			_, _, ok := getLinkMetadataFromCache(requestURL, timestamp)
  1703  			require.False(t, ok, "data should not exist in in-memory cache")
  1704  
  1705  			_, _, ok = th.App.getLinkMetadataFromDatabase(requestURL, timestamp)
  1706  			require.True(t, ok, "data should already exist in database")
  1707  
  1708  			og, img, err := th.App.getLinkMetadata(requestURL, timestamp+60*1000, false)
  1709  
  1710  			require.NotNil(t, og)
  1711  			assert.Nil(t, img)
  1712  			assert.NoError(t, err)
  1713  			assert.Equal(t, title, og.Title)
  1714  		})
  1715  
  1716  		t.Run("should not use database if URL is different", func(t *testing.T) {
  1717  			linkCache.Purge()
  1718  
  1719  			differentURL := requestURL + "/other"
  1720  
  1721  			_, _, ok := getLinkMetadataFromCache(requestURL, timestamp)
  1722  			require.False(t, ok, "data should not exist in in-memory cache")
  1723  
  1724  			_, _, ok = th.App.getLinkMetadataFromDatabase(differentURL, timestamp)
  1725  			require.False(t, ok, "data should not exist in database")
  1726  
  1727  			og, img, err := th.App.getLinkMetadata(differentURL, timestamp, false)
  1728  
  1729  			assert.Nil(t, og)
  1730  			assert.Nil(t, img)
  1731  			assert.NoError(t, err)
  1732  		})
  1733  
  1734  		t.Run("should not use database if timestamp is different", func(t *testing.T) {
  1735  			linkCache.Purge()
  1736  
  1737  			differentTimestamp := timestamp + 60*60*1000
  1738  
  1739  			_, _, ok := getLinkMetadataFromCache(requestURL, timestamp)
  1740  			require.False(t, ok, "data should not exist in in-memory cache")
  1741  
  1742  			_, _, ok = th.App.getLinkMetadataFromDatabase(requestURL, differentTimestamp)
  1743  			require.False(t, ok, "data should not exist in database")
  1744  
  1745  			og, img, err := th.App.getLinkMetadata(requestURL, differentTimestamp, false)
  1746  
  1747  			assert.Nil(t, og)
  1748  			assert.Nil(t, img)
  1749  			assert.NoError(t, err)
  1750  		})
  1751  	})
  1752  
  1753  	t.Run("should get data from remote source", func(t *testing.T) {
  1754  		th := setup(t)
  1755  		defer th.TearDown()
  1756  
  1757  		requestURL := server.URL + "/opengraph?title=Remote&name=" + t.Name()
  1758  		timestamp := int64(1547510400000)
  1759  
  1760  		_, _, ok := getLinkMetadataFromCache(requestURL, timestamp)
  1761  		require.False(t, ok, "data should not exist in in-memory cache")
  1762  
  1763  		_, _, ok = th.App.getLinkMetadataFromDatabase(requestURL, timestamp)
  1764  		require.False(t, ok, "data should not exist in database")
  1765  
  1766  		og, img, err := th.App.getLinkMetadata(requestURL, timestamp, false)
  1767  
  1768  		assert.NotNil(t, og)
  1769  		assert.Nil(t, img)
  1770  		assert.NoError(t, err)
  1771  	})
  1772  
  1773  	t.Run("should cache OpenGraph results", func(t *testing.T) {
  1774  		th := setup(t)
  1775  		defer th.TearDown()
  1776  
  1777  		requestURL := server.URL + "/opengraph?title=Remote&name=" + t.Name()
  1778  		timestamp := int64(1547510400000)
  1779  
  1780  		_, _, ok := getLinkMetadataFromCache(requestURL, timestamp)
  1781  		require.False(t, ok, "data should not exist in in-memory cache")
  1782  
  1783  		_, _, ok = th.App.getLinkMetadataFromDatabase(requestURL, timestamp)
  1784  		require.False(t, ok, "data should not exist in database")
  1785  
  1786  		og, img, err := th.App.getLinkMetadata(requestURL, timestamp, false)
  1787  
  1788  		assert.NotNil(t, og)
  1789  		assert.Nil(t, img)
  1790  		assert.NoError(t, err)
  1791  
  1792  		fromCache, _, ok := getLinkMetadataFromCache(requestURL, timestamp)
  1793  		assert.True(t, ok)
  1794  		assert.Exactly(t, og, fromCache)
  1795  
  1796  		fromDatabase, _, ok := th.App.getLinkMetadataFromDatabase(requestURL, timestamp)
  1797  		assert.True(t, ok)
  1798  		assert.Exactly(t, og, fromDatabase)
  1799  	})
  1800  
  1801  	t.Run("should cache image results", func(t *testing.T) {
  1802  		th := setup(t)
  1803  		defer th.TearDown()
  1804  
  1805  		requestURL := server.URL + "/image?height=300&width=400&name=" + t.Name()
  1806  		timestamp := int64(1547510400000)
  1807  
  1808  		_, _, ok := getLinkMetadataFromCache(requestURL, timestamp)
  1809  		require.False(t, ok, "data should not exist in in-memory cache")
  1810  
  1811  		_, _, ok = th.App.getLinkMetadataFromDatabase(requestURL, timestamp)
  1812  		require.False(t, ok, "data should not exist in database")
  1813  
  1814  		og, img, err := th.App.getLinkMetadata(requestURL, timestamp, false)
  1815  
  1816  		assert.Nil(t, og)
  1817  		assert.NotNil(t, img)
  1818  		assert.NoError(t, err)
  1819  
  1820  		_, fromCache, ok := getLinkMetadataFromCache(requestURL, timestamp)
  1821  		assert.True(t, ok)
  1822  		assert.Exactly(t, img, fromCache)
  1823  
  1824  		_, fromDatabase, ok := th.App.getLinkMetadataFromDatabase(requestURL, timestamp)
  1825  		assert.True(t, ok)
  1826  		assert.Exactly(t, img, fromDatabase)
  1827  	})
  1828  
  1829  	t.Run("should cache general errors", func(t *testing.T) {
  1830  		th := setup(t)
  1831  		defer th.TearDown()
  1832  
  1833  		requestURL := server.URL + "/error"
  1834  		timestamp := int64(1547510400000)
  1835  
  1836  		_, _, ok := getLinkMetadataFromCache(requestURL, timestamp)
  1837  		require.False(t, ok, "data should not exist in in-memory cache")
  1838  
  1839  		_, _, ok = th.App.getLinkMetadataFromDatabase(requestURL, timestamp)
  1840  		require.False(t, ok, "data should not exist in database")
  1841  
  1842  		og, img, err := th.App.getLinkMetadata(requestURL, timestamp, false)
  1843  
  1844  		assert.Nil(t, og)
  1845  		assert.Nil(t, img)
  1846  		assert.NoError(t, err)
  1847  
  1848  		ogFromCache, imgFromCache, ok := getLinkMetadataFromCache(requestURL, timestamp)
  1849  		assert.True(t, ok)
  1850  		assert.Nil(t, ogFromCache)
  1851  		assert.Nil(t, imgFromCache)
  1852  
  1853  		ogFromDatabase, imageFromDatabase, ok := th.App.getLinkMetadataFromDatabase(requestURL, timestamp)
  1854  		assert.True(t, ok)
  1855  		assert.Nil(t, ogFromDatabase)
  1856  		assert.Nil(t, imageFromDatabase)
  1857  	})
  1858  
  1859  	t.Run("should cache invalid URL errors", func(t *testing.T) {
  1860  		th := setup(t)
  1861  		defer th.TearDown()
  1862  
  1863  		requestURL := "http://notarealdomainthatactuallyexists.ca/?name=" + t.Name()
  1864  		timestamp := int64(1547510400000)
  1865  
  1866  		_, _, ok := getLinkMetadataFromCache(requestURL, timestamp)
  1867  		require.False(t, ok, "data should not exist in in-memory cache")
  1868  
  1869  		_, _, ok = th.App.getLinkMetadataFromDatabase(requestURL, timestamp)
  1870  		require.False(t, ok, "data should not exist in database")
  1871  
  1872  		og, img, err := th.App.getLinkMetadata(requestURL, timestamp, false)
  1873  
  1874  		assert.Nil(t, og)
  1875  		assert.Nil(t, img)
  1876  		assert.IsType(t, &url.Error{}, err)
  1877  
  1878  		ogFromCache, imgFromCache, ok := getLinkMetadataFromCache(requestURL, timestamp)
  1879  		assert.True(t, ok)
  1880  		assert.Nil(t, ogFromCache)
  1881  		assert.Nil(t, imgFromCache)
  1882  
  1883  		ogFromDatabase, imageFromDatabase, ok := th.App.getLinkMetadataFromDatabase(requestURL, timestamp)
  1884  		assert.True(t, ok)
  1885  		assert.Nil(t, ogFromDatabase)
  1886  		assert.Nil(t, imageFromDatabase)
  1887  	})
  1888  
  1889  	t.Run("should cache timeout errors", func(t *testing.T) {
  1890  		th := setup(t)
  1891  		defer th.TearDown()
  1892  
  1893  		th.App.UpdateConfig(func(cfg *model.Config) {
  1894  			*cfg.ExperimentalSettings.LinkMetadataTimeoutMilliseconds = 100
  1895  		})
  1896  
  1897  		requestURL := server.URL + "/timeout?name=" + t.Name()
  1898  		timestamp := int64(1547510400000)
  1899  
  1900  		_, _, ok := getLinkMetadataFromCache(requestURL, timestamp)
  1901  		require.False(t, ok, "data should not exist in in-memory cache")
  1902  
  1903  		_, _, ok = th.App.getLinkMetadataFromDatabase(requestURL, timestamp)
  1904  		require.False(t, ok, "data should not exist in database")
  1905  
  1906  		og, img, err := th.App.getLinkMetadata(requestURL, timestamp, false)
  1907  
  1908  		assert.Nil(t, og)
  1909  		assert.Nil(t, img)
  1910  		assert.Error(t, err)
  1911  		assert.True(t, os.IsTimeout(err))
  1912  
  1913  		ogFromCache, imgFromCache, ok := getLinkMetadataFromCache(requestURL, timestamp)
  1914  		assert.True(t, ok)
  1915  		assert.Nil(t, ogFromCache)
  1916  		assert.Nil(t, imgFromCache)
  1917  
  1918  		ogFromDatabase, imageFromDatabase, ok := th.App.getLinkMetadataFromDatabase(requestURL, timestamp)
  1919  		assert.True(t, ok)
  1920  		assert.Nil(t, ogFromDatabase)
  1921  		assert.Nil(t, imageFromDatabase)
  1922  	})
  1923  
  1924  	t.Run("should cache database results in memory", func(t *testing.T) {
  1925  		th := setup(t)
  1926  		defer th.TearDown()
  1927  
  1928  		requestURL := server.URL + "/image?height=300&width=400&name=" + t.Name()
  1929  		timestamp := int64(1547510400000)
  1930  
  1931  		_, _, ok := getLinkMetadataFromCache(requestURL, timestamp)
  1932  		require.False(t, ok, "data should not exist in in-memory cache")
  1933  
  1934  		_, _, ok = th.App.getLinkMetadataFromDatabase(requestURL, timestamp)
  1935  		require.False(t, ok, "data should not exist in database")
  1936  
  1937  		_, img, err := th.App.getLinkMetadata(requestURL, timestamp, false)
  1938  		require.NoError(t, err)
  1939  
  1940  		_, _, ok = getLinkMetadataFromCache(requestURL, timestamp)
  1941  		require.True(t, ok, "data should now exist in in-memory cache")
  1942  
  1943  		linkCache.Purge()
  1944  		_, _, ok = getLinkMetadataFromCache(requestURL, timestamp)
  1945  		require.False(t, ok, "data should no longer exist in in-memory cache")
  1946  
  1947  		_, fromDatabase, ok := th.App.getLinkMetadataFromDatabase(requestURL, timestamp)
  1948  		assert.True(t, ok, "data should be be in in-memory cache again")
  1949  		assert.Exactly(t, img, fromDatabase)
  1950  	})
  1951  
  1952  	t.Run("should reject non-html, non-image response", func(t *testing.T) {
  1953  		th := setup(t)
  1954  		defer th.TearDown()
  1955  
  1956  		requestURL := server.URL + "/json?name=" + t.Name()
  1957  		timestamp := int64(1547510400000)
  1958  
  1959  		og, img, err := th.App.getLinkMetadata(requestURL, timestamp, false)
  1960  		assert.Nil(t, og)
  1961  		assert.Nil(t, img)
  1962  		assert.NoError(t, err)
  1963  	})
  1964  
  1965  	t.Run("should check in-memory cache for new post", func(t *testing.T) {
  1966  		th := setup(t)
  1967  		defer th.TearDown()
  1968  
  1969  		requestURL := server.URL + "/error?name=" + t.Name()
  1970  		timestamp := int64(1547510400000)
  1971  
  1972  		cacheLinkMetadata(requestURL, timestamp, &opengraph.OpenGraph{Title: "cached"}, nil)
  1973  
  1974  		og, img, err := th.App.getLinkMetadata(requestURL, timestamp, true)
  1975  		assert.NotNil(t, og)
  1976  		assert.Nil(t, img)
  1977  		assert.NoError(t, err)
  1978  	})
  1979  
  1980  	t.Run("should skip database cache for new post", func(t *testing.T) {
  1981  		th := setup(t)
  1982  		defer th.TearDown()
  1983  
  1984  		requestURL := server.URL + "/error?name=" + t.Name()
  1985  		timestamp := int64(1547510400000)
  1986  
  1987  		th.App.saveLinkMetadataToDatabase(requestURL, timestamp, &opengraph.OpenGraph{Title: "cached"}, nil)
  1988  
  1989  		og, img, err := th.App.getLinkMetadata(requestURL, timestamp, true)
  1990  		assert.Nil(t, og)
  1991  		assert.Nil(t, img)
  1992  		assert.NoError(t, err)
  1993  	})
  1994  
  1995  	t.Run("should resolve relative URL", func(t *testing.T) {
  1996  		th := setup(t)
  1997  		defer th.TearDown()
  1998  
  1999  		// Fake the SiteURL to have the relative URL resolve to the external server
  2000  		oldSiteURL := *th.App.Config().ServiceSettings.SiteURL
  2001  		defer th.App.UpdateConfig(func(cfg *model.Config) {
  2002  			*cfg.ServiceSettings.SiteURL = oldSiteURL
  2003  		})
  2004  
  2005  		th.App.UpdateConfig(func(cfg *model.Config) {
  2006  			*cfg.ServiceSettings.SiteURL = server.URL
  2007  		})
  2008  
  2009  		requestURL := "/image?height=200&width=300&name=" + t.Name()
  2010  		timestamp := int64(1547510400000)
  2011  
  2012  		og, img, err := th.App.getLinkMetadata(requestURL, timestamp, false)
  2013  		assert.Nil(t, og)
  2014  		assert.NotNil(t, img)
  2015  		assert.NoError(t, err)
  2016  	})
  2017  
  2018  	t.Run("should error on local addresses other than the image proxy", func(t *testing.T) {
  2019  		th := setup(t)
  2020  		defer th.TearDown()
  2021  
  2022  		// Disable AllowedUntrustedInternalConnections since it's turned on for the previous tests
  2023  		oldAllowUntrusted := *th.App.Config().ServiceSettings.AllowedUntrustedInternalConnections
  2024  		oldSiteURL := *th.App.Config().ServiceSettings.SiteURL
  2025  		defer th.App.UpdateConfig(func(cfg *model.Config) {
  2026  			*cfg.ServiceSettings.AllowedUntrustedInternalConnections = oldAllowUntrusted
  2027  			*cfg.ServiceSettings.SiteURL = oldSiteURL
  2028  		})
  2029  
  2030  		th.App.UpdateConfig(func(cfg *model.Config) {
  2031  			*cfg.ServiceSettings.AllowedUntrustedInternalConnections = ""
  2032  			*cfg.ServiceSettings.SiteURL = "http://mattermost.example.com"
  2033  			*cfg.ImageProxySettings.Enable = true
  2034  			*cfg.ImageProxySettings.ImageProxyType = "local"
  2035  		})
  2036  
  2037  		requestURL := server.URL + "/image?height=200&width=300&name=" + t.Name()
  2038  		timestamp := int64(1547510400000)
  2039  
  2040  		og, img, err := th.App.getLinkMetadata(requestURL, timestamp, false)
  2041  		assert.Nil(t, og)
  2042  		assert.Nil(t, img)
  2043  		assert.Error(t, err)
  2044  		assert.IsType(t, &url.Error{}, err)
  2045  		assert.Equal(t, httpservice.AddressForbidden, err.(*url.Error).Err)
  2046  
  2047  		requestURL = th.App.GetSiteURL() + "/api/v4/image?url=" + url.QueryEscape(requestURL)
  2048  
  2049  		// Note that this request still fails while testing because the request made by the image proxy is blocked
  2050  		og, img, err = th.App.getLinkMetadata(requestURL, timestamp, false)
  2051  		assert.Nil(t, og)
  2052  		assert.Nil(t, img)
  2053  		assert.Error(t, err)
  2054  		assert.IsType(t, imageproxy.Error{}, err)
  2055  	})
  2056  
  2057  	t.Run("should prefer images for mixed content", func(t *testing.T) {
  2058  		th := setup(t)
  2059  		defer th.TearDown()
  2060  
  2061  		requestURL := server.URL + "/mixed?name=" + t.Name()
  2062  		timestamp := int64(1547510400000)
  2063  
  2064  		og, img, err := th.App.getLinkMetadata(requestURL, timestamp, true)
  2065  		assert.Nil(t, og)
  2066  		assert.NotNil(t, img)
  2067  		assert.NoError(t, err)
  2068  	})
  2069  }
  2070  
  2071  func TestResolveMetadataURL(t *testing.T) {
  2072  	for _, test := range []struct {
  2073  		Name       string
  2074  		RequestURL string
  2075  		SiteURL    string
  2076  		Expected   string
  2077  	}{
  2078  		{
  2079  			Name:       "with HTTPS",
  2080  			RequestURL: "https://example.com/file?param=1",
  2081  			Expected:   "https://example.com/file?param=1",
  2082  		},
  2083  		{
  2084  			Name:       "with HTTP",
  2085  			RequestURL: "http://example.com/file?param=1",
  2086  			Expected:   "http://example.com/file?param=1",
  2087  		},
  2088  		{
  2089  			Name:       "with FTP",
  2090  			RequestURL: "ftp://example.com/file?param=1",
  2091  			Expected:   "ftp://example.com/file?param=1",
  2092  		},
  2093  		{
  2094  			Name:       "relative to root",
  2095  			RequestURL: "/file?param=1",
  2096  			SiteURL:    "https://mattermost.example.com:123",
  2097  			Expected:   "https://mattermost.example.com:123/file?param=1",
  2098  		},
  2099  		{
  2100  			Name:       "relative to root with subpath",
  2101  			RequestURL: "/file?param=1",
  2102  			SiteURL:    "https://mattermost.example.com:123/subpath",
  2103  			Expected:   "https://mattermost.example.com:123/file?param=1",
  2104  		},
  2105  	} {
  2106  		t.Run(test.Name, func(t *testing.T) {
  2107  			assert.Equal(t, resolveMetadataURL(test.RequestURL, test.SiteURL), test.Expected)
  2108  		})
  2109  	}
  2110  }
  2111  
  2112  func TestParseLinkMetadata(t *testing.T) {
  2113  	th := Setup(t)
  2114  	defer th.TearDown()
  2115  
  2116  	imageURL := "http://example.com/test.png"
  2117  	file, err := testutils.ReadTestFile("test.png")
  2118  	require.NoError(t, err)
  2119  
  2120  	ogURL := "https://example.com/hello"
  2121  	html := `
  2122  		<html>
  2123  			<head>
  2124  				<meta property="og:title" content="Hello, World!">
  2125  				<meta property="og:type" content="object">
  2126  				<meta property="og:url" content="` + ogURL + `">
  2127  			</head>
  2128  		</html>`
  2129  
  2130  	makeImageReader := func() io.Reader {
  2131  		return bytes.NewReader(file)
  2132  	}
  2133  
  2134  	makeOpenGraphReader := func() io.Reader {
  2135  		return strings.NewReader(html)
  2136  	}
  2137  
  2138  	t.Run("image", func(t *testing.T) {
  2139  		og, dimensions, err := th.App.parseLinkMetadata(imageURL, makeImageReader(), "image/png")
  2140  		assert.NoError(t, err)
  2141  
  2142  		assert.Nil(t, og)
  2143  		assert.Equal(t, &model.PostImage{
  2144  			Format: "png",
  2145  			Width:  408,
  2146  			Height: 336,
  2147  		}, dimensions)
  2148  	})
  2149  
  2150  	t.Run("malformed image", func(t *testing.T) {
  2151  		og, dimensions, err := th.App.parseLinkMetadata(imageURL, makeOpenGraphReader(), "image/png")
  2152  		assert.Error(t, err)
  2153  
  2154  		assert.Nil(t, og)
  2155  		assert.Nil(t, dimensions)
  2156  	})
  2157  
  2158  	t.Run("opengraph", func(t *testing.T) {
  2159  		og, dimensions, err := th.App.parseLinkMetadata(ogURL, makeOpenGraphReader(), "text/html; charset=utf-8")
  2160  		assert.NoError(t, err)
  2161  
  2162  		assert.NotNil(t, og)
  2163  		assert.Equal(t, og.Title, "Hello, World!")
  2164  		assert.Equal(t, og.Type, "object")
  2165  		assert.Equal(t, og.URL, ogURL)
  2166  		assert.Nil(t, dimensions)
  2167  	})
  2168  
  2169  	t.Run("malformed opengraph", func(t *testing.T) {
  2170  		og, dimensions, err := th.App.parseLinkMetadata(ogURL, makeImageReader(), "text/html; charset=utf-8")
  2171  		assert.NoError(t, err)
  2172  
  2173  		assert.Nil(t, og)
  2174  		assert.Nil(t, dimensions)
  2175  	})
  2176  
  2177  	t.Run("neither", func(t *testing.T) {
  2178  		og, dimensions, err := th.App.parseLinkMetadata("http://example.com/test.wad", strings.NewReader("garbage"), "application/x-doom")
  2179  		assert.NoError(t, err)
  2180  
  2181  		assert.Nil(t, og)
  2182  		assert.Nil(t, dimensions)
  2183  	})
  2184  
  2185  	t.Run("svg", func(t *testing.T) {
  2186  		og, dimensions, err := th.App.parseLinkMetadata("http://example.com/image.svg", nil, "image/svg+xml")
  2187  		assert.NoError(t, err)
  2188  
  2189  		assert.Nil(t, og)
  2190  		assert.Equal(t, &model.PostImage{
  2191  			Format: "svg",
  2192  		}, dimensions)
  2193  	})
  2194  }
  2195  
  2196  func TestParseImages(t *testing.T) {
  2197  	for name, testCase := range map[string]struct {
  2198  		FileName    string
  2199  		Expected    *model.PostImage
  2200  		ExpectError bool
  2201  	}{
  2202  		"png": {
  2203  			FileName: "test.png",
  2204  			Expected: &model.PostImage{
  2205  				Width:  408,
  2206  				Height: 336,
  2207  				Format: "png",
  2208  			},
  2209  		},
  2210  		"animated gif": {
  2211  			FileName: "testgif.gif",
  2212  			Expected: &model.PostImage{
  2213  				Width:      118,
  2214  				Height:     118,
  2215  				Format:     "gif",
  2216  				FrameCount: 4,
  2217  			},
  2218  		},
  2219  		"tiff": {
  2220  			FileName: "test.tiff",
  2221  			Expected: (*model.PostImage)(nil),
  2222  		},
  2223  		"not an image": {
  2224  			FileName:    "README.md",
  2225  			ExpectError: true,
  2226  		},
  2227  	} {
  2228  		t.Run(name, func(t *testing.T) {
  2229  			file, err := testutils.ReadTestFile(testCase.FileName)
  2230  			require.NoError(t, err)
  2231  
  2232  			result, err := parseImages(bytes.NewReader(file))
  2233  			if testCase.ExpectError {
  2234  				assert.Error(t, err)
  2235  			} else {
  2236  				assert.NoError(t, err)
  2237  				assert.Equal(t, testCase.Expected, result)
  2238  			}
  2239  		})
  2240  	}
  2241  }