github.com/status-im/status-go@v1.1.0/protocol/messenger_linkpreview_test.go (about)

     1  package protocol
     2  
     3  import (
     4  	"bytes"
     5  	"fmt"
     6  	"io/ioutil"
     7  	"math"
     8  	"net/http"
     9  	"net/url"
    10  	"regexp"
    11  	"strings"
    12  	"testing"
    13  	"time"
    14  
    15  	"github.com/stretchr/testify/suite"
    16  
    17  	"github.com/status-im/status-go/eth-node/crypto"
    18  	"github.com/status-im/status-go/images"
    19  	"github.com/status-im/status-go/multiaccounts/accounts"
    20  	"github.com/status-im/status-go/multiaccounts/settings"
    21  	"github.com/status-im/status-go/protocol/common"
    22  	"github.com/status-im/status-go/protocol/protobuf"
    23  	"github.com/status-im/status-go/protocol/requests"
    24  )
    25  
    26  const (
    27  	exampleIdenticonURI = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADIAAAAyCAYAAAAeP4ixA" +
    28  		"AAAiklEQVR4nOzWwQmFQAwG4ffEXmzLIizDImzLarQBhSwSGH7mO+9hh0DI9AthCI0hNIbQGEJjCI0hNIbQxITM1YfHfl69X3m2bsu/8i5mI" +
    29  		"obQGEJjCI0hNIbQlG+tUW83UtfNFjMRQ2gMofm8tUa3U9c2i5mIITSGqEnMRAyhMYTGEBpDaO4AAAD//5POEGncqtj1AAAAAElFTkSuQmCC"
    30  )
    31  
    32  func TestMessengerLinkPreviews(t *testing.T) {
    33  	suite.Run(t, new(MessengerLinkPreviewsTestSuite))
    34  }
    35  
    36  type MessengerLinkPreviewsTestSuite struct {
    37  	MessengerBaseTestSuite
    38  }
    39  
    40  // StubMatcher should either return an http.Response or nil in case the request
    41  // doesn't match.
    42  type StubMatcher func(req *http.Request) *http.Response
    43  
    44  type StubTransport struct {
    45  	// fallbackToDefaultTransport when true will make the transport use
    46  	// http.DefaultTransport in case no matcher is found.
    47  	fallbackToDefaultTransport bool
    48  	// disabledStubs when true, will skip all matchers and use
    49  	// http.DefaultTransport.
    50  	//
    51  	// Useful while testing to toggle between the original and stubbed responses.
    52  	disabledStubs bool
    53  	// matchers are http.RoundTripper functions.
    54  	matchers []StubMatcher
    55  }
    56  
    57  // RoundTrip returns a stubbed response if any matcher returns a non-nil
    58  // http.Response. If no matcher is found and fallbackToDefaultTransport is true,
    59  // then it executes the HTTP request using the default http transport.
    60  //
    61  // If StubTransport#disabledStubs is true, the default http transport is used.
    62  func (t *StubTransport) RoundTrip(req *http.Request) (*http.Response, error) {
    63  	if t.disabledStubs {
    64  		return http.DefaultTransport.RoundTrip(req)
    65  	}
    66  
    67  	for _, matcher := range t.matchers {
    68  		res := matcher(req)
    69  		if res != nil {
    70  			return res, nil
    71  		}
    72  	}
    73  
    74  	if t.fallbackToDefaultTransport {
    75  		return http.DefaultTransport.RoundTrip(req)
    76  	}
    77  
    78  	return nil, fmt.Errorf("no HTTP matcher found")
    79  }
    80  
    81  func (t *StubTransport) AddURLMatcherRoundTrip(urlRegexp string, roundTrip func(r *http.Request) *http.Response) {
    82  	matcher := func(req *http.Request) *http.Response {
    83  		rx, err := regexp.Compile(regexp.QuoteMeta(urlRegexp))
    84  		if err != nil {
    85  			return nil
    86  		}
    87  		if !rx.MatchString(req.URL.String()) {
    88  			return nil
    89  		}
    90  		return roundTrip(req)
    91  	}
    92  	t.matchers = append(t.matchers, matcher)
    93  }
    94  
    95  // Add a matcher based on a URL regexp. If a given request URL matches the
    96  // regexp, then responseBody will be returned with a hardcoded 200 status code.
    97  // If headers is non-nil, use it as the value of http.Response.Header.
    98  func (t *StubTransport) AddURLMatcher(urlRegexp string, responseBody []byte, headers map[string]string) {
    99  	matcher := func(req *http.Request) *http.Response {
   100  		res := &http.Response{
   101  			StatusCode: http.StatusOK,
   102  			Body:       ioutil.NopCloser(bytes.NewBuffer(responseBody)),
   103  		}
   104  
   105  		if headers != nil {
   106  			res.Header = http.Header{}
   107  			for k, v := range headers {
   108  				res.Header.Set(k, v)
   109  			}
   110  		}
   111  
   112  		return res
   113  	}
   114  	t.AddURLMatcherRoundTrip(urlRegexp, matcher)
   115  }
   116  
   117  // assertContainsLongString verifies if actual contains a slice of expected and
   118  // correctly prints the cause of the failure. The default behavior of
   119  // require.Contains with long strings is to not print the formatted message
   120  // (varargs to require.Contains).
   121  func (s *MessengerLinkPreviewsTestSuite) assertContainsLongString(expected string, actual string, maxLength int) {
   122  	var safeIdx float64
   123  	var actualShort string
   124  	var expectedShort string
   125  
   126  	if len(actual) > 0 {
   127  		safeIdx = math.Min(float64(maxLength), float64(len(actual)-1))
   128  		actualShort = actual[:int(safeIdx)]
   129  	}
   130  
   131  	if len(expected) > 0 {
   132  		safeIdx = math.Min(float64(maxLength), float64(len(expected)-1))
   133  		expectedShort = expected[:int(safeIdx)]
   134  	}
   135  
   136  	s.Require().Contains(
   137  		actual, expected,
   138  		"'%s' should contain '%s'",
   139  		actualShort,
   140  		expectedShort,
   141  	)
   142  }
   143  
   144  func (s *MessengerLinkPreviewsTestSuite) Test_GetLinks() {
   145  	examples := []struct {
   146  		args     string
   147  		expected []string
   148  	}{
   149  		// Invalid URLs are not taken in consideration.
   150  		{args: "", expected: []string{}},
   151  		{args: "  ", expected: []string{}},
   152  		{args: "https", expected: []string{}},
   153  		{args: "https://", expected: []string{}},
   154  		{args: "https://status", expected: []string{}},
   155  		{args: "https://status.", expected: []string{}},
   156  		// URLs must include the sheme.
   157  		{args: "status.com", expected: []string{}},
   158  
   159  		{args: "https://status.im", expected: []string{"https://status.im"}},
   160  
   161  		// Only the host should be lowercased.
   162  		{args: "HTTPS://STATUS.IM/path/to?Q=AbCdE", expected: []string{"https://status.im/path/to?Q=AbCdE"}},
   163  
   164  		// Remove trailing forward slash.
   165  		{args: "https://github.com/", expected: []string{"https://github.com"}},
   166  		{args: "https://www.youtube.com/watch?v=mzOyYtfXkb0/", expected: []string{"https://www.youtube.com/watch?v=mzOyYtfXkb0"}},
   167  
   168  		// Valid URL.
   169  		{args: "https://status.c", expected: []string{"https://status.c"}},
   170  		{args: "https://status.im/test", expected: []string{"https://status.im/test"}},
   171  		{args: "https://192.168.0.100:9999/xyz", expected: []string{"https://192.168.0.100:9999/xyz"}},
   172  
   173  		// There is a bug in the code that builds the AST from markdown text,
   174  		// because it removes the closing parenthesis, which means it won't be
   175  		// possible to unfurl this URL.
   176  		{args: "https://en.wikipedia.org/wiki/Status_message_(instant_messaging)", expected: []string{"https://en.wikipedia.org/wiki/Status_message_(instant_messaging"}},
   177  
   178  		// Multiple URLs.
   179  		{
   180  			args:     "https://status.im/test https://www.youtube.com/watch?v=mzOyYtfXkb0",
   181  			expected: []string{"https://status.im/test", "https://www.youtube.com/watch?v=mzOyYtfXkb0"},
   182  		},
   183  		{
   184  			args:     "status.im https://www.youtube.com/watch?v=mzOyYtfXkb0",
   185  			expected: []string{"https://www.youtube.com/watch?v=mzOyYtfXkb0"},
   186  		},
   187  	}
   188  
   189  	for _, ex := range examples {
   190  		links := s.m.GetURLs(ex.args)
   191  		s.Require().Equal(ex.expected, links, "Failed for args: '%s'", ex.args)
   192  	}
   193  }
   194  
   195  func (s *MessengerLinkPreviewsTestSuite) readAsset(filename string) []byte {
   196  	b, err := ioutil.ReadFile("../_assets/tests/" + filename)
   197  	s.Require().NoError(err)
   198  	return b
   199  }
   200  
   201  func (s *MessengerLinkPreviewsTestSuite) Test_GetFavicon() {
   202  	goodHTMLPNG := []byte(
   203  		`
   204  	<html>
   205  		<head>
   206  			<link rel="shortcut icon" href="https://www.somehost.com/favicon.png">
   207  		</head>
   208  	</html>`)
   209  
   210  	goodHTMLSVG := []byte(
   211  		`
   212  	<html>
   213  		<head>
   214  			<link rel="shortcut icon" href="https://www.somehost.com/favicon.svg">
   215  		</head>
   216  	</html>`)
   217  
   218  	goodHTMLICO := []byte(
   219  		`
   220  	<html>
   221  		<head>
   222  			<link rel="shortcut icon" href="https://www.somehost.com/favicon.ico">
   223  		</head>
   224  	</html>`)
   225  
   226  	badHTMLNoRelAttr := []byte(
   227  		`
   228  	<html>
   229  		<head>
   230  			<link href="https://www.somehost.com/favicon.png">
   231  		</head>
   232  	</html>`)
   233  
   234  	GoodHTMLRelAttributeIcon := []byte(
   235  		`
   236  	<html>
   237  		<head>
   238  			<link rel="icon" href="https://www.somehost.com/favicon.png">
   239  		</head>
   240  	</html>`)
   241  
   242  	faviconPath := GetFavicon(goodHTMLPNG)
   243  	s.Require().Equal("https://www.somehost.com/favicon.png", faviconPath)
   244  
   245  	faviconPath = GetFavicon(goodHTMLSVG)
   246  	s.Require().Equal("https://www.somehost.com/favicon.svg", faviconPath)
   247  
   248  	faviconPath = GetFavicon(goodHTMLICO)
   249  	s.Require().Equal("https://www.somehost.com/favicon.ico", faviconPath)
   250  
   251  	faviconPath = GetFavicon(GoodHTMLRelAttributeIcon)
   252  	s.Require().Equal("https://www.somehost.com/favicon.png", faviconPath)
   253  
   254  	faviconPath = GetFavicon(badHTMLNoRelAttr)
   255  	s.Require().Equal("", faviconPath)
   256  }
   257  
   258  func (s *MessengerLinkPreviewsTestSuite) Test_UnfurlURLs_YouTube() {
   259  	u := "https://www.youtube.com/watch?v=lE4UXdJSJM4"
   260  	thumbnailURL := "https://i.ytimg.com/vi/lE4UXdJSJM4/maxresdefault.jpg"
   261  	expected := common.LinkPreview{
   262  		Type:        protobuf.UnfurledLink_LINK,
   263  		URL:         u,
   264  		Hostname:    "www.youtube.com",
   265  		Title:       "Interview with a GNU/Linux user - Partition 1",
   266  		Description: "GNU/Linux Operating SystemInterview with a GNU/Linux user with Richie Guix - aired on © The GNU Linux.Programmer humorLinux humorProgramming jokesProgramming...",
   267  		Thumbnail: common.LinkPreviewThumbnail{
   268  			Width:   1,
   269  			Height:  1,
   270  			DataURI: "data:image/webp;base64,UklGRiQAAABXRUJQVlA4IBgAAAAwAQCdASoBAAEAAQAaJaQAA3AA/vpMgAA",
   271  		},
   272  	}
   273  	favicon := "https://www.youtube.com/s/desktop/87423d78/img/favicon.ico"
   274  	transport := StubTransport{}
   275  	transport.AddURLMatcher(
   276  		u,
   277  		[]byte(fmt.Sprintf(`
   278  			<html>
   279  				<head>
   280  					<meta property="og:title" content="%s">
   281  					<meta property="og:description" content="%s">
   282  					<meta property="og:image" content="%s">
   283  					<link rel="shortcut icon" href="%s">
   284  				</head>
   285  			</html>
   286  		`, expected.Title, expected.Description, thumbnailURL, favicon)),
   287  		nil,
   288  	)
   289  	transport.AddURLMatcher(thumbnailURL, s.readAsset("1.jpg"), nil)
   290  	stubbedClient := http.Client{Transport: &transport}
   291  
   292  	response, err := s.m.UnfurlURLs(&stubbedClient, []string{u})
   293  	s.Require().NoError(err)
   294  	s.Require().Len(response.StatusLinkPreviews, 0)
   295  	s.Require().Len(response.LinkPreviews, 1)
   296  	preview := response.LinkPreviews[0]
   297  
   298  	s.Require().Equal(expected.Type, preview.Type)
   299  	s.Require().Equal(expected.URL, preview.URL)
   300  	s.Require().Equal(expected.Hostname, preview.Hostname)
   301  	s.Require().Equal(expected.Title, preview.Title)
   302  	s.Require().Equal(expected.Description, preview.Description)
   303  	s.Require().Equal(expected.Thumbnail.Width, preview.Thumbnail.Width)
   304  	s.Require().Equal(expected.Thumbnail.Height, preview.Thumbnail.Height)
   305  	s.Require().Equal(expected.Thumbnail.URL, preview.Thumbnail.URL)
   306  	s.Require().NotNil(preview.Favicon)
   307  	s.assertContainsLongString(expected.Thumbnail.DataURI, preview.Thumbnail.DataURI, 100)
   308  }
   309  
   310  func (s *MessengerLinkPreviewsTestSuite) Test_UnfurlURLs_Reddit() {
   311  	u := "https://www.reddit.com/r/Bitcoin/comments/13j0tzr/the_best_bitcoin_explanation_of_all_times/?utm_source=share"
   312  	expected := common.LinkPreview{
   313  		Type:        protobuf.UnfurledLink_LINK,
   314  		URL:         u,
   315  		Hostname:    "www.reddit.com",
   316  		Title:       "The best bitcoin explanation of all times.",
   317  		Description: "",
   318  		Thumbnail:   common.LinkPreviewThumbnail{},
   319  	}
   320  
   321  	transport := StubTransport{}
   322  	transport.AddURLMatcher(
   323  		"https://www.reddit.com/oembed",
   324  		[]byte(`
   325  			{
   326  				"provider_url": "https://www.reddit.com/",
   327  				"version": "1.0",
   328  				"title": "The best bitcoin explanation of all times.",
   329  				"provider_name": "reddit",
   330  				"type": "rich",
   331  				"author_name": "DTheDev"
   332  			}
   333  		`),
   334  		nil,
   335  	)
   336  	stubbedClient := http.Client{Transport: &transport}
   337  
   338  	response, err := s.m.UnfurlURLs(&stubbedClient, []string{u})
   339  	s.Require().NoError(err)
   340  	s.Require().Len(response.StatusLinkPreviews, 0)
   341  	s.Require().Len(response.LinkPreviews, 1)
   342  	preview := response.LinkPreviews[0]
   343  
   344  	s.Require().Equal(expected.Type, preview.Type)
   345  	s.Require().Equal(expected.URL, preview.URL)
   346  	s.Require().Equal(expected.Hostname, preview.Hostname)
   347  	s.Require().Equal(expected.Title, preview.Title)
   348  	s.Require().Equal(expected.Description, preview.Description)
   349  	s.Require().Equal(expected.Thumbnail, preview.Thumbnail)
   350  }
   351  
   352  func (s *MessengerLinkPreviewsTestSuite) Test_UnfurlURLs_Timeout() {
   353  	httpClient := http.Client{Timeout: time.Nanosecond}
   354  	response, err := s.m.UnfurlURLs(&httpClient, []string{"https://status.im"})
   355  	s.Require().NoError(err)
   356  	s.Require().Len(response.StatusLinkPreviews, 0)
   357  	s.Require().Empty(response.LinkPreviews)
   358  }
   359  
   360  func (s *MessengerLinkPreviewsTestSuite) Test_UnfurlURLs_CommonFailures() {
   361  	httpClient := http.Client{}
   362  
   363  	// Test URL that doesn't return any OpenGraph title.
   364  	transport := StubTransport{}
   365  	transport.AddURLMatcher(
   366  		"https://wikipedia.org",
   367  		[]byte("<html><head></head></html>"),
   368  		nil,
   369  	)
   370  	stubbedClient := http.Client{Transport: &transport}
   371  	response, err := s.m.UnfurlURLs(&stubbedClient, []string{"https://wikipedia.org"})
   372  	s.Require().NoError(err)
   373  	s.Require().Len(response.StatusLinkPreviews, 0)
   374  	s.Require().Empty(response.LinkPreviews)
   375  
   376  	// Test 404.
   377  	response, err = s.m.UnfurlURLs(&httpClient, []string{"https://github.com/status-im/i_do_not_exist"})
   378  	s.Require().NoError(err)
   379  	s.Require().Len(response.StatusLinkPreviews, 0)
   380  	s.Require().Empty(response.LinkPreviews)
   381  
   382  	// Test no response when trying to get OpenGraph metadata.
   383  	response, err = s.m.UnfurlURLs(&httpClient, []string{"https://wikipedia.o"})
   384  	s.Require().NoError(err)
   385  	s.Require().Len(response.StatusLinkPreviews, 0)
   386  	s.Require().Empty(response.LinkPreviews)
   387  }
   388  
   389  func (s *MessengerLinkPreviewsTestSuite) Test_isSupportedImageURL() {
   390  	examples := []struct {
   391  		url      string
   392  		expected bool
   393  	}{
   394  		{url: "https://placehold.co/600x400@2x.png", expected: true},
   395  		{url: "https://placehold.co/600x400@2x.PNG", expected: true},
   396  		{url: "https://placehold.co/600x400@2x.jpg", expected: true},
   397  		{url: "https://placehold.co/600x400@2x.JPG", expected: true},
   398  		{url: "https://placehold.co/600x400@2x.jpeg", expected: true},
   399  		{url: "https://placehold.co/600x400@2x.Jpeg", expected: true},
   400  		{url: "https://placehold.co/600x400@2x.webp", expected: true},
   401  		{url: "https://placehold.co/600x400@2x.WebP", expected: true},
   402  		{url: "https://placehold.co/600x400@2x.PnGs", expected: false},
   403  		{url: "https://placehold.co/600x400@2x.tiff", expected: false},
   404  	}
   405  
   406  	for _, e := range examples {
   407  		parsedURL, err := url.Parse(e.url)
   408  		s.Require().NoError(err, e)
   409  		s.Require().Equal(e.expected, IsSupportedImageURL(parsedURL), e.url)
   410  	}
   411  }
   412  
   413  func (s *MessengerLinkPreviewsTestSuite) Test_UnfurlURLs_Image() {
   414  	u := "https://placehold.co/600x400@3x.png"
   415  	expected := common.LinkPreview{
   416  		Type:        protobuf.UnfurledLink_IMAGE,
   417  		URL:         u,
   418  		Hostname:    "placehold.co",
   419  		Title:       "600x400@3x.png",
   420  		Description: "",
   421  		Thumbnail: common.LinkPreviewThumbnail{
   422  			Width:   1293,
   423  			Height:  1900,
   424  			DataURI: "data:image/jpeg;base64,/9j/2wCEABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ",
   425  		},
   426  	}
   427  
   428  	transport := StubTransport{}
   429  	// Use a larger image to verify Thumbnail.DataURI is compressed.
   430  	transport.AddURLMatcher(u, s.readAsset("IMG_1205.HEIC.jpg"), nil)
   431  	stubbedClient := http.Client{Transport: &transport}
   432  
   433  	response, err := s.m.UnfurlURLs(&stubbedClient, []string{u})
   434  	s.Require().NoError(err)
   435  	s.Require().Len(response.StatusLinkPreviews, 0)
   436  	s.Require().Len(response.LinkPreviews, 1)
   437  	preview := response.LinkPreviews[0]
   438  
   439  	s.Require().Equal(expected.Type, preview.Type)
   440  	s.Require().Equal(expected.URL, preview.URL)
   441  	s.Require().Equal(expected.Hostname, preview.Hostname)
   442  	s.Require().Equal(expected.Title, preview.Title)
   443  	s.Require().Equal(expected.Description, preview.Description)
   444  	s.Require().Equal(expected.Thumbnail.Width, preview.Thumbnail.Width)
   445  	s.Require().Equal(expected.Thumbnail.Height, preview.Thumbnail.Height)
   446  	s.Require().Equal(expected.Thumbnail.URL, preview.Thumbnail.URL)
   447  	s.assertContainsLongString(expected.Thumbnail.DataURI, preview.Thumbnail.DataURI, 100)
   448  }
   449  
   450  func (s *MessengerLinkPreviewsTestSuite) Test_UnfurlURLs_StatusContactAdded() {
   451  	identity, err := crypto.GenerateKey()
   452  	s.Require().NoError(err)
   453  
   454  	c, err := BuildContactFromPublicKey(&identity.PublicKey)
   455  	s.Require().NoError(err)
   456  	s.Require().NotNil(c)
   457  
   458  	payload, err := images.GetPayloadFromURI(exampleIdenticonURI)
   459  	s.Require().NoError(err)
   460  
   461  	icon := images.IdentityImage{
   462  		Width:   50,
   463  		Height:  50,
   464  		Payload: payload,
   465  	}
   466  
   467  	c.Bio = "TestBio_1"
   468  	c.DisplayName = "TestDisplayName_1"
   469  	c.Images = map[string]images.IdentityImage{}
   470  	c.Images[images.SmallDimName] = icon
   471  	s.m.allContacts.Store(c.ID, c)
   472  
   473  	// Generate a shared URL
   474  	u, err := s.m.ShareUserURLWithData(c.ID)
   475  	s.Require().NoError(err)
   476  
   477  	// Update contact info locally after creating the shared URL
   478  	// This is required to test that URL-decoded data is not used in the preview.
   479  	c.Bio = "TestBio_2"
   480  	c.DisplayName = "TestDisplayName_2"
   481  	s.m.allContacts.Store(c.ID, c)
   482  
   483  	r, err := s.m.UnfurlURLs(nil, []string{u})
   484  	s.Require().NoError(err)
   485  	s.Require().Len(r.StatusLinkPreviews, 1)
   486  	s.Require().Len(r.LinkPreviews, 0)
   487  
   488  	preview := r.StatusLinkPreviews[0]
   489  	s.Require().Equal(u, preview.URL)
   490  	s.Require().Nil(preview.Community)
   491  	s.Require().Nil(preview.Channel)
   492  	s.Require().NotNil(preview.Contact)
   493  	s.Require().Equal(c.ID, preview.Contact.PublicKey)
   494  	s.Require().Equal(c.DisplayName, preview.Contact.DisplayName)
   495  	s.Require().Equal(c.Bio, preview.Contact.Description)
   496  	s.Require().Equal(icon.Width, preview.Contact.Icon.Width)
   497  	s.Require().Equal(icon.Height, preview.Contact.Icon.Height)
   498  	s.Require().Equal("", preview.Contact.Icon.URL)
   499  
   500  	expectedDataURI, err := images.GetPayloadDataURI(icon.Payload)
   501  	s.Require().NoError(err)
   502  	s.Require().Equal(expectedDataURI, preview.Contact.Icon.DataURI)
   503  }
   504  
   505  func (s *MessengerLinkPreviewsTestSuite) setProfileParameters(messenger *Messenger, displayName string, bio string, identityImages []images.IdentityImage) {
   506  	const timeout = 1 * time.Second
   507  
   508  	changes := SelfContactChangeEvent{}
   509  
   510  	SetSettingsAndWaitForChange(&s.Suite, messenger, timeout, func() {
   511  		err := messenger.SetDisplayName(displayName)
   512  		s.Require().NoError(err)
   513  		err = messenger.SetBio(bio)
   514  		s.Require().NoError(err)
   515  	}, func(event *SelfContactChangeEvent) bool {
   516  		if event.DisplayNameChanged {
   517  			changes.DisplayNameChanged = true
   518  		}
   519  		if event.BioChanged {
   520  			changes.BioChanged = true
   521  		}
   522  		return changes.DisplayNameChanged && changes.BioChanged
   523  	})
   524  
   525  	SetIdentityImagesAndWaitForChange(&s.Suite, messenger, timeout, func() {
   526  		err := messenger.multiAccounts.StoreIdentityImages(messenger.account.KeyUID, identityImages, false)
   527  		s.Require().NoError(err)
   528  	})
   529  
   530  	selfContact := messenger.GetSelfContact()
   531  	s.Require().Equal(selfContact.DisplayName, displayName)
   532  	s.Require().Equal(selfContact.Bio, bio)
   533  
   534  	for _, image := range identityImages {
   535  		saved, ok := selfContact.Images[image.Name]
   536  		s.Require().True(ok)
   537  		s.Require().Equal(saved, image)
   538  	}
   539  	s.Require().Equal(selfContact.DisplayName, displayName)
   540  }
   541  
   542  func (s *MessengerLinkPreviewsTestSuite) Test_UnfurlURLs_SelfLink() {
   543  	profileKp := accounts.GetProfileKeypairForTest(true, false, false)
   544  	profileKp.KeyUID = s.m.account.KeyUID
   545  	profileKp.Accounts[0].KeyUID = s.m.account.KeyUID
   546  
   547  	err := s.m.settings.SaveOrUpdateKeypair(profileKp)
   548  	s.Require().NoError(err)
   549  
   550  	// Set initial profile parameters
   551  	identityImages := images.SampleIdentityImages()
   552  	s.setProfileParameters(s.m, "TestDisplayName_3", "TestBio_3", identityImages)
   553  
   554  	// Generate a shared URL
   555  	u, err := s.m.ShareUserURLWithData(s.m.IdentityPublicKeyString())
   556  	s.Require().NoError(err)
   557  
   558  	// Update contact info locally after creating the shared URL
   559  	// This is required to test that URL-decoded data is not used in the preview.
   560  	iconPayload, err := images.GetPayloadFromURI(exampleIdenticonURI)
   561  	s.Require().NoError(err)
   562  	icon := images.IdentityImage{
   563  		Name:    images.SmallDimName,
   564  		Width:   50,
   565  		Height:  50,
   566  		Payload: iconPayload,
   567  	}
   568  	s.setProfileParameters(s.m, "TestDisplayName_4", "TestBio_4", []images.IdentityImage{icon})
   569  
   570  	r, err := s.m.UnfurlURLs(nil, []string{u})
   571  	s.Require().NoError(err)
   572  	s.Require().Len(r.StatusLinkPreviews, 1)
   573  	s.Require().Len(r.LinkPreviews, 0)
   574  
   575  	userSettings, err := s.m.getSettings()
   576  	s.Require().NoError(err)
   577  
   578  	preview := r.StatusLinkPreviews[0]
   579  	s.Require().Equal(u, preview.URL)
   580  	s.Require().Nil(preview.Community)
   581  	s.Require().Nil(preview.Channel)
   582  	s.Require().NotNil(preview.Contact)
   583  	s.Require().Equal(s.m.IdentityPublicKeyString(), preview.Contact.PublicKey)
   584  	s.Require().Equal(userSettings.DisplayName, preview.Contact.DisplayName)
   585  	s.Require().Equal(userSettings.Bio, preview.Contact.Description)
   586  
   587  	s.Require().Equal(icon.Width, preview.Contact.Icon.Width)
   588  	s.Require().Equal(icon.Height, preview.Contact.Icon.Height)
   589  	s.Require().Equal("", preview.Contact.Icon.URL)
   590  
   591  	expectedDataURI, err := images.GetPayloadDataURI(icon.Payload)
   592  	s.Require().NoError(err)
   593  	s.Require().Equal(expectedDataURI, preview.Contact.Icon.DataURI)
   594  }
   595  
   596  func (s *MessengerLinkPreviewsTestSuite) Test_UnfurlURLs_StatusCommunityJoined() {
   597  
   598  	description := &requests.CreateCommunity{
   599  		Membership:  protobuf.CommunityPermissions_AUTO_ACCEPT,
   600  		Name:        "status",
   601  		Description: "status community description",
   602  		Color:       "#123456",
   603  		Image:       "../_assets/tests/status.png", // 256*256 px
   604  		ImageAx:     0,
   605  		ImageAy:     0,
   606  		ImageBx:     256,
   607  		ImageBy:     256,
   608  		Banner: images.CroppedImage{
   609  			ImagePath: "../_assets/tests/IMG_1205.HEIC.jpg", // 2282*3352 px
   610  			X:         0,
   611  			Y:         0,
   612  			Width:     160,
   613  			Height:    90,
   614  		},
   615  	}
   616  
   617  	response, err := s.m.CreateCommunity(description, false)
   618  	s.Require().NoError(err)
   619  	s.Require().NotNil(response)
   620  
   621  	community := response.Communities()[0]
   622  	communityImages := community.Images()
   623  	s.Require().Len(communityImages, 3)
   624  
   625  	// Get icon data
   626  	icon, ok := communityImages[images.SmallDimName]
   627  	s.Require().True(ok)
   628  	iconWidth, iconHeight, err := images.GetImageDimensions(icon.Payload)
   629  	s.Require().NoError(err)
   630  	iconDataURI, err := images.GetPayloadDataURI(icon.Payload)
   631  	s.Require().NoError(err)
   632  
   633  	// Get banner data
   634  	banner, ok := communityImages[images.BannerIdentityName]
   635  	s.Require().True(ok)
   636  	bannerWidth, bannerHeight, err := images.GetImageDimensions(banner.Payload)
   637  	s.Require().NoError(err)
   638  	bannerDataURI, err := images.GetPayloadDataURI(banner.Payload)
   639  	s.Require().NoError(err)
   640  
   641  	// Create shared URL
   642  	u, err := s.m.ShareCommunityURLWithData(community.ID())
   643  	s.Require().NoError(err)
   644  
   645  	// Unfurl community shared URL
   646  	r, err := s.m.UnfurlURLs(nil, []string{u})
   647  	s.Require().NoError(err)
   648  	s.Require().Len(r.StatusLinkPreviews, 1)
   649  	s.Require().Len(r.LinkPreviews, 0)
   650  
   651  	preview := r.StatusLinkPreviews[0]
   652  	s.Require().Equal(u, preview.URL)
   653  	s.Require().NotNil(preview.Community)
   654  	s.Require().Nil(preview.Channel)
   655  	s.Require().Nil(preview.Contact)
   656  
   657  	s.Require().Equal(community.IDString(), preview.Community.CommunityID)
   658  	s.Require().Equal(community.Name(), preview.Community.DisplayName)
   659  	s.Require().Equal(community.Identity().Description, preview.Community.Description)
   660  	s.Require().Equal(iconWidth, preview.Community.Icon.Width)
   661  	s.Require().Equal(iconHeight, preview.Community.Icon.Height)
   662  	s.Require().Equal(iconDataURI, preview.Community.Icon.DataURI)
   663  	s.Require().Equal(bannerWidth, preview.Community.Banner.Width)
   664  	s.Require().Equal(bannerHeight, preview.Community.Banner.Height)
   665  	s.Require().Equal(bannerDataURI, preview.Community.Banner.DataURI)
   666  }
   667  
   668  func (s *MessengerLinkPreviewsTestSuite) Test_UnfurlURLs_Settings() {
   669  	// Create website stub
   670  	const ogLink = "https://github.com"
   671  	const statusUserLink = "https://status.app/c#zQ3shYSHp7GoiXaauJMnDcjwU2yNjdzpXLosAWapPS4CFxc11"
   672  	const gifLink = "https://media1.giphy.com/media/lcG3qwtTKSNI2i5vst/giphy.gif"
   673  
   674  	linksToUnfurl := []string{ogLink, statusUserLink, gifLink}
   675  	text := strings.Join(linksToUnfurl, " ")
   676  
   677  	// Test `AlwaysAsk`
   678  
   679  	err := s.m.settings.SaveSettingField(settings.URLUnfurlingMode, settings.URLUnfurlingAlwaysAsk)
   680  	s.Require().NoError(err)
   681  
   682  	plan := s.m.GetTextURLsToUnfurl(text)
   683  	s.Require().Len(plan.URLs, len(linksToUnfurl))
   684  
   685  	s.Require().Equal(plan.URLs[0].URL, ogLink)
   686  	s.Require().Equal(plan.URLs[0].IsStatusSharedURL, false)
   687  	s.Require().Equal(plan.URLs[0].Permission, URLUnfurlingAskUser)
   688  
   689  	s.Require().Equal(plan.URLs[1].URL, statusUserLink)
   690  	s.Require().Equal(plan.URLs[1].IsStatusSharedURL, true)
   691  	s.Require().Equal(plan.URLs[1].Permission, URLUnfurlingAllowed)
   692  
   693  	s.Require().Equal(plan.URLs[2].URL, gifLink)
   694  	s.Require().Equal(plan.URLs[2].IsStatusSharedURL, false)
   695  	s.Require().Equal(plan.URLs[2].Permission, URLUnfurlingNotSupported)
   696  
   697  	// Test `EnableAll`
   698  	err = s.m.settings.SaveSettingField(settings.URLUnfurlingMode, settings.URLUnfurlingEnableAll)
   699  	s.Require().NoError(err)
   700  
   701  	plan = s.m.GetTextURLsToUnfurl(text)
   702  	s.Require().Len(plan.URLs, len(linksToUnfurl))
   703  
   704  	s.Require().Equal(plan.URLs[0].URL, ogLink)
   705  	s.Require().Equal(plan.URLs[0].IsStatusSharedURL, false)
   706  	s.Require().Equal(plan.URLs[0].Permission, URLUnfurlingAllowed)
   707  
   708  	s.Require().Equal(plan.URLs[1].URL, statusUserLink)
   709  	s.Require().Equal(plan.URLs[1].IsStatusSharedURL, true)
   710  	s.Require().Equal(plan.URLs[1].Permission, URLUnfurlingAllowed)
   711  
   712  	s.Require().Equal(plan.URLs[2].URL, gifLink)
   713  	s.Require().Equal(plan.URLs[2].IsStatusSharedURL, false)
   714  	s.Require().Equal(plan.URLs[2].Permission, URLUnfurlingNotSupported)
   715  
   716  	// Test `DisableAll`
   717  	err = s.m.settings.SaveSettingField(settings.URLUnfurlingMode, settings.URLUnfurlingDisableAll)
   718  	s.Require().NoError(err)
   719  
   720  	plan = s.m.GetTextURLsToUnfurl(text)
   721  	s.Require().Len(plan.URLs, len(linksToUnfurl))
   722  
   723  	s.Require().Equal(plan.URLs[0].URL, ogLink)
   724  	s.Require().Equal(plan.URLs[0].IsStatusSharedURL, false)
   725  	s.Require().Equal(plan.URLs[0].Permission, URLUnfurlingForbiddenBySettings)
   726  
   727  	s.Require().Equal(plan.URLs[1].URL, statusUserLink)
   728  	s.Require().Equal(plan.URLs[1].IsStatusSharedURL, true)
   729  	s.Require().Equal(plan.URLs[1].Permission, URLUnfurlingAllowed)
   730  
   731  	s.Require().Equal(plan.URLs[2].URL, gifLink)
   732  	s.Require().Equal(plan.URLs[2].IsStatusSharedURL, false)
   733  	s.Require().Equal(plan.URLs[2].Permission, URLUnfurlingNotSupported)
   734  }
   735  
   736  func (s *MessengerLinkPreviewsTestSuite) Test_UnfurlURLs_Limit() {
   737  	text := "https://www.youtube.com/watch?v=6dkDepLX0rk " +
   738  		"https://www.youtube.com/watch?v=ferZnZ0_rSM " +
   739  		"https://www.youtube.com/watch?v=bdneye4pzMw " +
   740  		"https://www.youtube.com/watch?v=pRERgcQe-fQ " +
   741  		"https://www.youtube.com/watch?v=j82L3pLjb_0 " +
   742  		"https://www.youtube.com/watch?v=hxsJvKYyVyg " +
   743  		"https://www.youtube.com/watch?v=jIIuzB11dsA "
   744  
   745  	urls := s.m.GetURLs(text)
   746  	s.Require().Equal(UnfurledLinksPerMessageLimit, len(urls))
   747  }