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