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  }