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 }