github.com/keybase/client/go@v0.0.0-20241007131713-f10651d043c8/chat/unfurl/scraper_test.go (about) 1 package unfurl 2 3 import ( 4 "bytes" 5 "context" 6 "fmt" 7 "io" 8 "net" 9 "net/http" 10 "os" 11 "path/filepath" 12 "strings" 13 "testing" 14 "time" 15 16 "github.com/keybase/client/go/chat/globals" 17 "github.com/keybase/client/go/chat/types" 18 19 "github.com/keybase/client/go/chat/maps" 20 "github.com/keybase/client/go/libkb" 21 "github.com/keybase/client/go/protocol/chat1" 22 "github.com/keybase/client/go/protocol/gregor1" 23 "github.com/keybase/clockwork" 24 "github.com/stretchr/testify/require" 25 ) 26 27 type dummyHTTPSrv struct { 28 t *testing.T 29 srv *http.Server 30 shouldServeAppleTouchIcon bool 31 handler func(w http.ResponseWriter, r *http.Request) 32 } 33 34 func newDummyHTTPSrv(t *testing.T, handler func(w http.ResponseWriter, r *http.Request)) *dummyHTTPSrv { 35 return &dummyHTTPSrv{ 36 t: t, 37 handler: handler, 38 } 39 } 40 41 func (d *dummyHTTPSrv) Start() string { 42 localhost := "127.0.0.1" 43 listener, err := net.Listen("tcp", fmt.Sprintf("%s:0", localhost)) 44 require.NoError(d.t, err) 45 port := listener.Addr().(*net.TCPAddr).Port 46 mux := http.NewServeMux() 47 mux.HandleFunc("/", d.handler) 48 mux.HandleFunc("/apple-touch-icon.png", d.serveAppleTouchIcon) 49 d.srv = &http.Server{ 50 Addr: fmt.Sprintf("%s:%d", localhost, port), 51 Handler: mux, 52 } 53 go func() { _ = d.srv.Serve(listener) }() 54 return d.srv.Addr 55 } 56 57 func (d *dummyHTTPSrv) Stop() { 58 require.NoError(d.t, d.srv.Close()) 59 } 60 61 func (d *dummyHTTPSrv) serveAppleTouchIcon(w http.ResponseWriter, r *http.Request) { 62 if d.shouldServeAppleTouchIcon { 63 w.WriteHeader(200) 64 dat, _ := os.ReadFile(filepath.Join("testcases", "github.png")) 65 _, _ = io.Copy(w, bytes.NewBuffer(dat)) 66 return 67 } 68 w.WriteHeader(404) 69 } 70 71 func strPtr(s string) *string { 72 return &s 73 } 74 75 func intPtr(i int) *int { 76 return &i 77 } 78 79 func createTestCaseHTTPSrv(t *testing.T) *dummyHTTPSrv { 80 return newDummyHTTPSrv(t, func(w http.ResponseWriter, r *http.Request) { 81 w.WriteHeader(200) 82 name := r.URL.Query().Get("name") 83 contentType := r.URL.Query().Get("content_type") 84 if len(contentType) > 0 { 85 w.Header().Set("Content-Type", contentType) 86 } 87 dat, err := os.ReadFile(filepath.Join("testcases", name)) 88 require.NoError(t, err) 89 _, err = io.Copy(w, bytes.NewBuffer(dat)) 90 require.NoError(t, err) 91 }) 92 } 93 94 func TestScraper(t *testing.T) { 95 tc := libkb.SetupTest(t, "scraper", 1) 96 defer tc.Cleanup() 97 g := globals.NewContext(tc.G, &globals.ChatContext{}) 98 scraper := NewScraper(g) 99 100 clock := clockwork.NewFakeClock() 101 scraper.cache.setClock(clock) 102 103 srv := createTestCaseHTTPSrv(t) 104 addr := srv.Start() 105 defer srv.Stop() 106 forceGiphy := new(chat1.UnfurlType) 107 *forceGiphy = chat1.UnfurlType_GIPHY 108 testCase := func(name string, expected chat1.UnfurlRaw, success bool, contentType *string, 109 forceTyp *chat1.UnfurlType) { 110 uri := fmt.Sprintf("http://%s/?name=%s", addr, name) 111 if contentType != nil { 112 uri += fmt.Sprintf("&content_type=%s", *contentType) 113 } 114 res, err := scraper.Scrape(context.TODO(), uri, forceTyp) 115 if !success { 116 require.Error(t, err) 117 return 118 } 119 require.NoError(t, err) 120 etyp, err := expected.UnfurlType() 121 require.NoError(t, err) 122 rtyp, err := res.UnfurlType() 123 require.NoError(t, err) 124 require.Equal(t, etyp, rtyp) 125 126 t.Logf("expected:\n%v\n\nactual:\n%v", expected, res) 127 switch rtyp { 128 case chat1.UnfurlType_GENERIC: 129 e := expected.Generic() 130 r := res.Generic() 131 require.Equal(t, e.Title, r.Title) 132 require.Equal(t, e.SiteName, r.SiteName) 133 require.True(t, (e.Description == nil && r.Description == nil) || (e.Description != nil && r.Description != nil)) 134 if e.Description != nil { 135 require.Equal(t, *e.Description, *r.Description) 136 } 137 require.True(t, (e.PublishTime == nil && r.PublishTime == nil) || (e.PublishTime != nil && r.PublishTime != nil)) 138 if e.PublishTime != nil { 139 require.Equal(t, *e.PublishTime, *r.PublishTime) 140 } 141 142 require.True(t, (e.ImageUrl == nil && r.ImageUrl == nil) || (e.ImageUrl != nil && r.ImageUrl != nil)) 143 if e.ImageUrl != nil { 144 require.Equal(t, *e.ImageUrl, *r.ImageUrl) 145 } 146 147 require.True(t, (e.FaviconUrl == nil && r.FaviconUrl == nil) || (e.FaviconUrl != nil && r.FaviconUrl != nil)) 148 if e.FaviconUrl != nil { 149 require.Equal(t, *e.FaviconUrl, *r.FaviconUrl) 150 } 151 152 require.True(t, (e.Video == nil && r.Video == nil) || (e.Video != nil && r.Video != nil)) 153 if e.Video != nil { 154 require.Equal(t, e.Video.Url, r.Video.Url) 155 require.Equal(t, e.Video.Height, r.Video.Height) 156 require.Equal(t, e.Video.Width, r.Video.Width) 157 } 158 case chat1.UnfurlType_GIPHY: 159 e := expected.Giphy() 160 r := res.Giphy() 161 require.Equal(t, e.ImageUrl, r.ImageUrl) 162 require.NotNil(t, r.FaviconUrl) 163 require.NotNil(t, e.FaviconUrl) 164 require.Equal(t, *e.FaviconUrl, *r.FaviconUrl) 165 require.NotNil(t, r.Video) 166 require.NotNil(t, e.Video) 167 require.Equal(t, e.Video.Url, r.Video.Url) 168 require.Equal(t, e.Video.Height, r.Video.Height) 169 require.Equal(t, e.Video.Width, r.Video.Width) 170 default: 171 require.Fail(t, "unknown unfurl typ") 172 } 173 174 // test caching 175 cachedRes, valid := scraper.cache.get(uri) 176 require.True(t, valid) 177 require.Equal(t, res, cachedRes.data.(chat1.UnfurlRaw)) 178 179 clock.Advance(defaultCacheLifetime * 2) 180 cachedRes, valid = scraper.cache.get(uri) 181 require.False(t, valid) 182 } 183 184 testCase("cnn0.html", chat1.NewUnfurlRawWithGeneric(chat1.UnfurlGenericRaw{ 185 Title: "Kanye West seeks separation from politics", 186 Url: "https://www.cnn.com/2018/10/30/entertainment/kanye-west-politics/index.html", 187 SiteName: "CNN", 188 Description: strPtr("Just weeks after visiting the White House, Kanye West appears to be a little tired of politics."), 189 PublishTime: intPtr(1540941044), 190 ImageUrl: strPtr("https://cdn.cnn.com/cnnnext/dam/assets/181011162312-11-week-in-photos-1011-super-tease.jpg"), 191 FaviconUrl: strPtr("http://cdn.cnn.com/cnn/.e/img/3.0/global/misc/apple-touch-icon.png"), 192 }), true, nil, nil) 193 testCase("wsj0.html", chat1.NewUnfurlRawWithGeneric(chat1.UnfurlGenericRaw{ 194 Title: "U.S. Stocks Jump as Tough Month Sets to Wrap", 195 Url: "https://www.wsj.com/articles/global-stocks-rally-to-end-a-tough-month-1540976261", 196 SiteName: "WSJ", 197 Description: strPtr("A surge in technology shares following Facebook’s latest earnings lifted U.S. stocks, helping major indexes trim some of their October declines following a punishing period for global investors."), 198 PublishTime: intPtr(1541004540), 199 ImageUrl: strPtr("https://images.wsj.net/im-33925/social"), 200 FaviconUrl: strPtr("https://s.wsj.net/media/wsj_apple-touch-icon-180x180.png"), 201 }), true, nil, nil) 202 testCase("nytimes0.html", chat1.NewUnfurlRawWithGeneric(chat1.UnfurlGenericRaw{ 203 Title: "First Up if Democrats Win: Campaign and Ethics Changes, Infrastructure and Drug Prices", 204 Url: "https://www.nytimes.com/2018/10/31/us/politics/democrats-midterm-elections.html", 205 SiteName: "0.1", // the default for these tests (from the localhost domain) 206 Description: strPtr("House Democratic leaders, for the first time, laid out an ambitious opening salvo of bills for a majority, including an overhaul of campaign and ethics laws."), 207 PublishTime: intPtr(1540990881), 208 ImageUrl: strPtr("https://static01.nyt.com/images/2018/10/31/us/politics/31dc-dems/31dc-dems-facebookJumbo.jpg"), 209 FaviconUrl: strPtr("http://127.0.0.1/vi-assets/static-assets/apple-touch-icon-319373aaf4524d94d38aa599c56b8655.png"), 210 }), true, nil, nil) 211 srv.shouldServeAppleTouchIcon = true 212 testCase("github0.html", chat1.NewUnfurlRawWithGeneric(chat1.UnfurlGenericRaw{ 213 Title: "keybase/client", 214 Url: "https://github.com/keybase/client", 215 SiteName: "GitHub", 216 Description: strPtr("Keybase Go Library, Client, Service, OS X, iOS, Android, Electron - keybase/client"), 217 ImageUrl: strPtr("https://avatars1.githubusercontent.com/u/5400834?s=400&v=4"), 218 FaviconUrl: strPtr(fmt.Sprintf("http://%s/apple-touch-icon.png", addr)), 219 }), true, nil, nil) 220 srv.shouldServeAppleTouchIcon = false 221 testCase("youtube0.html", chat1.NewUnfurlRawWithGeneric(chat1.UnfurlGenericRaw{ 222 Title: "Mario Kart Wii: The History of the Ultra Shortcut", 223 Url: "https://www.youtube.com/watch?v=mmJ_LT8bUj0", 224 SiteName: "YouTube", 225 Description: strPtr("https://www.twitch.tv/summoningsalt https://twitter.com/summoningsalt Music List- https://docs.google.com/document/d/1p2qV31ZhtNuP7AAXtRjGNZr2QwMSolzuz2wX6wu..."), 226 ImageUrl: strPtr("https://i.ytimg.com/vi/mmJ_LT8bUj0/hqdefault.jpg"), 227 FaviconUrl: strPtr("https://s.ytimg.com/yts/img/favicon-vfl8qSV2F.ico"), 228 }), true, nil, nil) 229 testCase("youtube1.html", chat1.NewUnfurlRawWithGeneric(chat1.UnfurlGenericRaw{ 230 Title: "Pumped to Be Here: Brazil's Game Fans", 231 Url: "https://www.youtube.com/watch?v=mmJ_LT8bUj0", 232 SiteName: "YouTube", 233 Description: strPtr("Brazil's games, consoles, and markets may seem strange, but there's plenty that's familiar, too. SUPPORT US ON PATREON! https://patreon.com/clothmap Patrons ..."), 234 ImageUrl: strPtr("https://i.ytimg.com/vi/6IIQFBb4exU/maxresdefault.jpg"), 235 FaviconUrl: strPtr("https://s.ytimg.com/yts/img/favicon-vfl8qSV2F.ico"), 236 }), true, nil, nil) 237 testCase("twitter0.html", chat1.NewUnfurlRawWithGeneric(chat1.UnfurlGenericRaw{ 238 Title: "Ars Technica on Twitter", 239 Url: "https://twitter.com/arstechnica/status/1057679097869094917", 240 SiteName: "Twitter", 241 Description: strPtr("“Nintendo recommits to “keep the business going” for 3DS https://t.co/wTIJxmGTJH by @KyleOrl”"), 242 ImageUrl: strPtr("https://pbs.twimg.com/profile_images/2215576731/ars-logo_400x400.png"), 243 FaviconUrl: strPtr("https://abs.twimg.com/icons/apple-touch-icon-192x192.png"), 244 }), true, nil, nil) 245 testCase("pinterest0.html", chat1.NewUnfurlRawWithGeneric(chat1.UnfurlGenericRaw{ 246 Title: "Halloween", 247 Url: "https://www.pinterest.com/pinterest/halloween/", 248 SiteName: "Pinterest", 249 Description: strPtr("Dracula dentures, kitten costumes, no-carve pumpkins—find your next killer idea on Pinterest."), 250 ImageUrl: strPtr("https://i.pinimg.com/custom_covers/200x150/424605139807203572_1414340303.jpg"), 251 FaviconUrl: strPtr("https://s.pinimg.com/webapp/style/images/logo_trans_144x144-642179a1.png"), 252 }), true, nil, nil) 253 testCase("wikipedia0.html", chat1.NewUnfurlRawWithGeneric(chat1.UnfurlGenericRaw{ 254 Title: "Merkle tree - Wikipedia", 255 SiteName: "0.1", 256 Description: nil, 257 ImageUrl: strPtr("https://upload.wikimedia.org/wikipedia/commons/thumb/9/95/Hash_Tree.svg/1200px-Hash_Tree.svg.png"), 258 FaviconUrl: strPtr("http://127.0.0.1/static/apple-touch/wikipedia.png"), 259 }), true, nil, nil) 260 testCase("reddit0.html", chat1.NewUnfurlRawWithGeneric(chat1.UnfurlGenericRaw{ 261 Title: "r/Stellar", 262 Url: "https://www.reddit.com/r/Stellar/", 263 SiteName: "reddit", 264 Description: strPtr("r/Stellar: Stellar is a decentralized protocol that enables you to send money to anyone in the world, for fractions of a penny, instantly, and in any currency. \n\n/r/Stellar is for news, announcements and discussion related to Stellar.\n\nPlease focus on community-oriented content, such as news and discussions, instead of individual-oriented content, such as questions and help. Follow the [Stellar Community Guidelines](https://www.stellar.org/community-guidelines/) ."), 265 ImageUrl: strPtr("https://b.thumbs.redditmedia.com/D857u25iiE2ORpt8yVx7fCuiMlLVP-b5fwSUjaw4lVU.png"), 266 FaviconUrl: strPtr("https://www.redditstatic.com/desktop2x/img/favicon/apple-icon-180x180.png"), 267 }), true, nil, nil) 268 testCase("etsy0.html", chat1.NewUnfurlRawWithGeneric(chat1.UnfurlGenericRaw{ 269 Title: "The Beatles - Minimalist Poster - Sgt Pepper", 270 Url: "https://www.etsy.com/listing/602032869/the-beatles-minimalist-poster-sgt-pepper?utm_source=OpenGraph&utm_medium=PageTools&utm_campaign=Share", 271 SiteName: "Etsy", 272 Description: strPtr("The Beatles Sgt Peppers Lonely Hearts Club Ban Created using mixed media Fits a 10 x 8 inch frame aperture - photograph shows item framed in a 12 x 10 inch frame Choose from: high lustre paper - 210g which produces very vibrant colours; textured watercolour paper - 190g - which looks"), 273 ImageUrl: strPtr("https://i.etsystatic.com/12686588/r/il/c3b4bc/1458062296/il_570xN.1458062296_rary.jpg"), 274 FaviconUrl: strPtr("http://127.0.0.1/images/favicon.ico"), 275 }), true, nil, nil) 276 testCase("giphy0.html", chat1.NewUnfurlRawWithGiphy(chat1.UnfurlGiphyRaw{ 277 ImageUrl: strPtr("https://media.giphy.com/media/5C3Zrs5xUg5fHV4Kcf/giphy-downsized-large.gif"), 278 FaviconUrl: strPtr("https://giphy.com/static/img/icons/apple-touch-icon-180px.png"), 279 Video: &chat1.UnfurlVideo{ 280 Url: "https://media.giphy.com/media/5C3Zrs5xUg5fHV4Kcf/giphy.mp4", 281 Height: 480, 282 Width: 480, 283 }, 284 }), true, nil, forceGiphy) 285 testCase("imgur0.html", chat1.NewUnfurlRawWithGeneric(chat1.UnfurlGenericRaw{ 286 Title: "Amazing Just Cause 4 Easter egg", 287 Url: "https://i.imgur.com/lXDyzHY.gifv", 288 SiteName: "Imgur", 289 Description: strPtr("2433301 views and 2489 votes on Imgur"), 290 ImageUrl: strPtr("https://i.imgur.com/lXDyzHY.jpg?play"), 291 FaviconUrl: strPtr(fmt.Sprintf("http://%s/favicon.ico", addr)), 292 Video: &chat1.UnfurlVideo{ 293 Url: "https://i.imgur.com/lXDyzHY.mp4", 294 Height: 408, 295 Width: 728, 296 }, 297 }), true, nil, nil) 298 srv.shouldServeAppleTouchIcon = false 299 testCase("nytogimage.jpg", chat1.NewUnfurlRawWithGeneric(chat1.UnfurlGenericRaw{ 300 SiteName: "0.1", 301 FaviconUrl: strPtr(fmt.Sprintf("http://%s/favicon.ico", addr)), 302 ImageUrl: strPtr(fmt.Sprintf("http://%s/?name=nytogimage.jpg&content_type=image/jpeg", addr)), 303 }), true, strPtr("image/jpeg"), nil) 304 srv.shouldServeAppleTouchIcon = true 305 testCase("slim.html", chat1.NewUnfurlRawWithGeneric(chat1.UnfurlGenericRaw{}), false, nil, nil) 306 307 } 308 309 func TestGiphySearchScrape(t *testing.T) { 310 tc := libkb.SetupTest(t, "giphyScraper", 1) 311 defer tc.Cleanup() 312 g := globals.NewContext(tc.G, &globals.ChatContext{}) 313 scraper := NewScraper(g) 314 315 clock := clockwork.NewFakeClock() 316 scraper.cache.setClock(clock) 317 318 url := "https://media0.giphy.com/media/iJDLBX5GY8niCpZYkR/giphy.mp4#height=360&width=640&isvideo=true" 319 res, err := scraper.Scrape(context.TODO(), url, nil) 320 require.NoError(t, err) 321 typ, err := res.UnfurlType() 322 require.NoError(t, err) 323 require.Equal(t, chat1.UnfurlType_GIPHY, typ) 324 require.Nil(t, res.Giphy().ImageUrl) 325 require.NotNil(t, res.Giphy().Video) 326 require.Equal(t, res.Giphy().Video.Url, url) 327 require.Equal(t, 360, res.Giphy().Video.Height) 328 require.Equal(t, 640, res.Giphy().Video.Width) 329 330 url = "https://media0.giphy.com/media/iJDLBX5GY8niCpZYkR/giphy.mp4#height=360&width=640&isvideo=false" 331 res, err = scraper.Scrape(context.TODO(), url, nil) 332 require.NoError(t, err) 333 typ, err = res.UnfurlType() 334 require.NoError(t, err) 335 require.Equal(t, chat1.UnfurlType_GIPHY, typ) 336 require.NotNil(t, res.Giphy().ImageUrl) 337 require.Nil(t, res.Giphy().Video) 338 require.Equal(t, *res.Giphy().ImageUrl, url) 339 340 url = "https://giphy.com/gifs/culture--think-hmm-d3mlE7uhX8KFgEmY" 341 res, err = scraper.Scrape(context.TODO(), url, nil) 342 require.NoError(t, err) 343 typ, err = res.UnfurlType() 344 require.NoError(t, err) 345 require.Equal(t, chat1.UnfurlType_GIPHY, typ) 346 require.NotNil(t, res.Giphy().ImageUrl) 347 require.NotNil(t, res.Giphy().Video) 348 } 349 350 func TestMapScraper(t *testing.T) { 351 tc := libkb.SetupTest(t, "mapScraper", 1) 352 defer tc.Cleanup() 353 g := globals.NewContext(tc.G, &globals.ChatContext{ 354 ExternalAPIKeySource: types.DummyExternalAPIKeySource{}, 355 }) 356 scraper := NewScraper(g) 357 lat := 40.800099 358 lon := -73.969341 359 acc := 65.00 360 url := fmt.Sprintf("https://%s/?lat=%f&lon=%f&acc=%f&done=true", types.MapsDomain, lat, lon, acc) 361 unfurl, err := scraper.Scrape(context.TODO(), url, nil) 362 require.NoError(t, err) 363 typ, err := unfurl.UnfurlType() 364 require.NoError(t, err) 365 require.Equal(t, chat1.UnfurlType_MAPS, typ) 366 require.True(t, strings.Contains(unfurl.Maps().Url, fmt.Sprintf("%f", lat))) 367 require.True(t, strings.Contains(unfurl.Maps().Url, fmt.Sprintf("%f", lon))) 368 require.NotNil(t, unfurl.Maps().ImageUrl) 369 require.True(t, strings.Contains(unfurl.Maps().ImageUrl, maps.MapsProxy)) 370 require.True(t, strings.Contains(unfurl.Maps().ImageUrl, fmt.Sprintf("%f", lat))) 371 require.True(t, strings.Contains(unfurl.Maps().ImageUrl, fmt.Sprintf("%f", lon))) 372 } 373 374 type testingLiveLocationTracker struct { 375 coords []chat1.Coordinate 376 } 377 378 func (t *testingLiveLocationTracker) Start(ctx context.Context, uid gregor1.UID) {} 379 func (t *testingLiveLocationTracker) Stop(ctx context.Context) chan struct{} { 380 ch := make(chan struct{}) 381 close(ch) 382 return ch 383 } 384 385 func (t *testingLiveLocationTracker) StartTracking(ctx context.Context, convID chat1.ConversationID, 386 msgID chat1.MessageID, endTime time.Time) { 387 } 388 389 func (t *testingLiveLocationTracker) GetCurrentPosition(ctx context.Context, convID chat1.ConversationID, 390 msgID chat1.MessageID) { 391 } 392 393 func (t *testingLiveLocationTracker) LocationUpdate(ctx context.Context, coord chat1.Coordinate) { 394 t.coords = append(t.coords, coord) 395 } 396 397 func (t *testingLiveLocationTracker) GetCoordinates(ctx context.Context, key types.LiveLocationKey) []chat1.Coordinate { 398 return t.coords 399 } 400 401 func (t *testingLiveLocationTracker) GetEndTime(ctx context.Context, key types.LiveLocationKey) *time.Time { 402 return nil 403 } 404 405 func (t *testingLiveLocationTracker) ActivelyTracking(ctx context.Context) bool { 406 return false 407 } 408 409 func (t *testingLiveLocationTracker) StopAllTracking(ctx context.Context) {} 410 411 func TestLiveMapScraper(t *testing.T) { 412 tc := libkb.SetupTest(t, "liveMapScraper", 1) 413 defer tc.Cleanup() 414 liveLocation := &testingLiveLocationTracker{} 415 g := globals.NewContext(tc.G, &globals.ChatContext{ 416 ExternalAPIKeySource: types.DummyExternalAPIKeySource{}, 417 LiveLocationTracker: liveLocation, 418 }) 419 scraper := NewScraper(g) 420 first := chat1.Coordinate{ 421 Lat: 40.800099, 422 Lon: -73.969341, 423 Accuracy: 65.00, 424 } 425 watchID := chat1.LocationWatchID(20) 426 liveLocation.LocationUpdate(context.TODO(), chat1.Coordinate{ 427 Lat: 40.756325, 428 Lon: -73.992533, 429 Accuracy: 65, 430 }) 431 liveLocation.LocationUpdate(context.TODO(), chat1.Coordinate{ 432 Lat: 40.704454, 433 Lon: -74.010893, 434 Accuracy: 65, 435 }) 436 url := fmt.Sprintf("https://%s/?lat=%f&lon=%f&acc=%f&watchID=%d&done=false&livekey=mike", 437 types.MapsDomain, first.Lat, first.Lon, first.Accuracy, watchID) 438 unfurl, err := scraper.Scrape(context.TODO(), url, nil) 439 require.NoError(t, err) 440 typ, err := unfurl.UnfurlType() 441 require.NoError(t, err) 442 require.Equal(t, chat1.UnfurlType_MAPS, typ) 443 require.NotZero(t, len(unfurl.Maps().ImageUrl)) 444 require.Equal(t, "Live Location Share", unfurl.Maps().SiteName) 445 446 url = fmt.Sprintf("https://%s/?lat=%f&lon=%f&acc=%f&watchID=%d&done=true&livekey=mike", types.MapsDomain, 447 first.Lat, first.Lon, first.Accuracy, watchID) 448 unfurl, err = scraper.Scrape(context.TODO(), url, nil) 449 require.NoError(t, err) 450 typ, err = unfurl.UnfurlType() 451 require.NoError(t, err) 452 require.Equal(t, chat1.UnfurlType_MAPS, typ) 453 require.Equal(t, "Live Location Share", unfurl.Maps().SiteName) 454 require.Equal(t, "Location share ended", unfurl.Maps().Title) 455 }