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 }