github.com/keybase/client/go@v0.0.0-20241007131713-f10651d043c8/chat/unfurl_test.go (about)

     1  package chat
     2  
     3  import (
     4  	"bytes"
     5  	"context"
     6  	"crypto/sha256"
     7  	"encoding/hex"
     8  	"fmt"
     9  	"io"
    10  	"net"
    11  	"net/http"
    12  	"os"
    13  	"path/filepath"
    14  	"sync"
    15  	"testing"
    16  	"time"
    17  
    18  	"github.com/keybase/client/go/externalstest"
    19  	"github.com/keybase/client/go/kbhttp/manager"
    20  
    21  	"github.com/keybase/client/go/chat/attachments"
    22  	"github.com/keybase/client/go/libkb"
    23  	"github.com/keybase/clockwork"
    24  
    25  	"github.com/keybase/client/go/chat/unfurl"
    26  	"github.com/keybase/client/go/protocol/chat1"
    27  	"github.com/keybase/client/go/protocol/gregor1"
    28  	"github.com/keybase/client/go/protocol/keybase1"
    29  	"github.com/stretchr/testify/require"
    30  )
    31  
    32  type dummyHTTPSrv struct {
    33  	sync.Mutex
    34  	t       *testing.T
    35  	srv     *http.Server
    36  	succeed bool
    37  }
    38  
    39  func newDummyHTTPSrv(t *testing.T) *dummyHTTPSrv {
    40  	return &dummyHTTPSrv{
    41  		t: t,
    42  	}
    43  }
    44  
    45  func (d *dummyHTTPSrv) Start() string {
    46  	localhost := "127.0.0.1"
    47  	listener, err := net.Listen("tcp", fmt.Sprintf("%s:0", localhost))
    48  	require.NoError(d.t, err)
    49  	port := listener.Addr().(*net.TCPAddr).Port
    50  	mux := http.NewServeMux()
    51  	mux.HandleFunc("/", d.handle)
    52  	mux.HandleFunc("/favicon.ico", d.handleFavicon)
    53  	mux.HandleFunc("/apple-touch-icon.png", d.handleApple)
    54  	d.srv = &http.Server{
    55  		Addr:    fmt.Sprintf("%s:%d", localhost, port),
    56  		Handler: mux,
    57  	}
    58  	go func() { _ = d.srv.Serve(listener) }()
    59  	return d.srv.Addr
    60  }
    61  
    62  func (d *dummyHTTPSrv) Stop() {
    63  	require.NoError(d.t, d.srv.Close())
    64  }
    65  
    66  func (d *dummyHTTPSrv) handleApple(w http.ResponseWriter, r *http.Request) {
    67  	d.Lock()
    68  	defer d.Unlock()
    69  	w.WriteHeader(404)
    70  }
    71  
    72  func (d *dummyHTTPSrv) handleFavicon(w http.ResponseWriter, r *http.Request) {
    73  	d.Lock()
    74  	defer d.Unlock()
    75  	w.WriteHeader(200)
    76  	f, err := os.Open(filepath.Join("unfurl", "testcases", "nytimes.ico"))
    77  	require.NoError(d.t, err)
    78  	_, err = io.Copy(w, f)
    79  	require.NoError(d.t, err)
    80  }
    81  
    82  func (d *dummyHTTPSrv) handle(w http.ResponseWriter, r *http.Request) {
    83  	d.Lock()
    84  	defer d.Unlock()
    85  	if d.succeed {
    86  		html := "<html><head><title>MIKE</title></head></html>"
    87  		w.WriteHeader(200)
    88  		_, err := io.Copy(w, bytes.NewBuffer([]byte(html)))
    89  		require.NoError(d.t, err)
    90  		return
    91  	}
    92  	w.WriteHeader(500)
    93  }
    94  
    95  func (d *dummyHTTPSrv) setSucceed(succeed bool) {
    96  	d.Lock()
    97  	defer d.Unlock()
    98  	d.succeed = succeed
    99  }
   100  
   101  type ptsigner struct{}
   102  
   103  func (p *ptsigner) Sign(payload []byte) ([]byte, error) {
   104  	s := sha256.Sum256(payload)
   105  	return s[:], nil
   106  }
   107  
   108  func TestChatSrvUnfurl(t *testing.T) {
   109  	runWithMemberTypes(t, func(mt chat1.ConversationMembersType) {
   110  		switch mt {
   111  		case chat1.ConversationMembersType_KBFS:
   112  			return
   113  		default:
   114  			// Fall through for other member types.
   115  		}
   116  
   117  		etc := externalstest.SetupTest(t, "unfurl", 1)
   118  		defer etc.Cleanup()
   119  
   120  		ctc := makeChatTestContext(t, "TestChatSrvUnfurl", 1)
   121  		defer ctc.cleanup()
   122  		users := ctc.users()
   123  
   124  		timeout := 20 * time.Second
   125  		ctx := ctc.as(t, users[0]).startCtx
   126  		tc := ctc.world.Tcs[users[0].Username]
   127  		ri := ctc.as(t, users[0]).ri
   128  		listener0 := newServerChatListener()
   129  		ctc.as(t, users[0]).h.G().NotifyRouter.AddListener(listener0)
   130  		httpSrv := newDummyHTTPSrv(t)
   131  		httpAddr := httpSrv.Start()
   132  		defer httpSrv.Stop()
   133  		storage := NewDevConversationBackedStorage(tc.Context(), func() chat1.RemoteInterface { return ri })
   134  		sender := NewNonblockingSender(tc.Context(),
   135  			NewBlockingSender(tc.Context(), NewBoxer(tc.Context()),
   136  				func() chat1.RemoteInterface { return ri }))
   137  		store := attachments.NewStoreTesting(tc.Context(), nil)
   138  		s3signer := &ptsigner{}
   139  		unfurler := unfurl.NewUnfurler(tc.Context(), store, s3signer, storage, sender,
   140  			func() chat1.RemoteInterface { return ri })
   141  		retryCh := make(chan struct{}, 5)
   142  		unfurlCh := make(chan *chat1.Unfurl, 5)
   143  		unfurler.SetTestingRetryCh(retryCh)
   144  		unfurler.SetTestingUnfurlCh(unfurlCh)
   145  		clock := clockwork.NewFakeClock()
   146  		unfurler.SetClock(clock)
   147  		tc.ChatG.Unfurler = unfurler
   148  		fetcher := NewRemoteAttachmentFetcher(tc.Context(), store)
   149  		tc.ChatG.AttachmentURLSrv = NewAttachmentHTTPSrv(tc.Context(),
   150  			manager.NewSrv(tc.Context().ExternalG()),
   151  			fetcher, func() chat1.RemoteInterface { return mockSigningRemote{} })
   152  
   153  		conv := mustCreateConversationForTest(t, ctc, users[0], chat1.TopicType_CHAT, mt)
   154  		recvSingleRetry := func() {
   155  			select {
   156  			case <-retryCh:
   157  			case <-time.After(timeout):
   158  				require.Fail(t, "no retry")
   159  			}
   160  			select {
   161  			case <-retryCh:
   162  				require.Fail(t, "unexpected retry")
   163  			default:
   164  			}
   165  		}
   166  		recvUnfurl := func() *chat1.Unfurl {
   167  			select {
   168  			case u := <-unfurlCh:
   169  				return u
   170  			case <-time.After(timeout):
   171  				require.Fail(t, "no unfurl")
   172  			}
   173  			return nil
   174  		}
   175  
   176  		recvAndCheckUnfurlMsg := func(msgID chat1.MessageID) {
   177  			var outboxID chat1.OutboxID
   178  			select {
   179  			case m := <-listener0.newMessageRemote:
   180  				require.Equal(t, conv.Id, m.ConvID)
   181  				require.True(t, m.Message.IsValid())
   182  				require.Equal(t, chat1.MessageType_UNFURL, m.Message.GetMessageType())
   183  				require.NotNil(t, m.Message.Valid().OutboxID)
   184  				b, err := hex.DecodeString(*m.Message.Valid().OutboxID)
   185  				require.NoError(t, err)
   186  				outboxID = chat1.OutboxID(b)
   187  			case <-time.After(timeout):
   188  				require.Fail(t, "no message")
   189  			}
   190  			_, _, err := unfurler.Status(ctx, outboxID)
   191  			require.Error(t, err)
   192  			require.IsType(t, libkb.NotFoundError{}, err)
   193  			select {
   194  			case <-listener0.newMessageRemote:
   195  				require.Fail(t, "no more messages")
   196  			default:
   197  			}
   198  			// We get two of these, one for local and remote, but its hard to know where they
   199  			// come from at the source, so just check twice.
   200  			for i := 0; i < 2; i++ {
   201  				select {
   202  				case mu := <-listener0.messagesUpdated:
   203  					require.Equal(t, 1, len(mu.Updates))
   204  					require.Equal(t, conv.Id, mu.ConvID)
   205  					require.Equal(t, msgID, mu.Updates[0].GetMessageID())
   206  					require.True(t, mu.Updates[0].IsValid())
   207  					require.Equal(t, 1, len(mu.Updates[0].Valid().Unfurls))
   208  					typ, err := mu.Updates[0].Valid().Unfurls[0].Unfurl.UnfurlType()
   209  					require.NoError(t, err)
   210  					require.Equal(t, chat1.UnfurlType_GENERIC, typ)
   211  					generic := mu.Updates[0].Valid().Unfurls[0].Unfurl.Generic()
   212  					require.Nil(t, generic.Media)
   213  					require.NotNil(t, generic.Favicon)
   214  					require.NotZero(t, len(generic.Favicon.Url))
   215  					resp, err := http.Get(generic.Favicon.Url)
   216  					require.NoError(t, err)
   217  					defer resp.Body.Close()
   218  					var buf bytes.Buffer
   219  					_, err = io.Copy(&buf, resp.Body)
   220  					require.NoError(t, err)
   221  					refBytes, err := os.ReadFile(filepath.Join("unfurl", "testcases", "nytimes_sol.ico"))
   222  					require.NoError(t, err)
   223  					require.True(t, bytes.Equal(refBytes, buf.Bytes()))
   224  					require.Equal(t, "MIKE", generic.Title)
   225  				case <-time.After(timeout):
   226  					require.Fail(t, "no message unfurl")
   227  				}
   228  			}
   229  			select {
   230  			case <-listener0.messagesUpdated:
   231  				require.Fail(t, "no more updates")
   232  			default:
   233  			}
   234  		}
   235  
   236  		t.Logf("send for prompt")
   237  		msg := chat1.NewMessageBodyWithText(chat1.MessageText{Body: fmt.Sprintf("http://%s", httpAddr)})
   238  		origID := mustPostLocalForTest(t, ctc, users[0], conv, msg)
   239  		t.Logf("origid: %v", origID)
   240  		consumeNewMsgRemote(t, listener0, chat1.MessageType_TEXT)
   241  		select {
   242  		case notificationID := <-listener0.unfurlPrompt:
   243  			require.Equal(t, origID, notificationID)
   244  		case <-time.After(timeout):
   245  			require.Fail(t, "no prompt")
   246  		}
   247  		t.Logf("whitelist and resolve")
   248  		require.NoError(t, ctc.as(t, users[0]).chatLocalHandler().ResolveUnfurlPrompt(ctx,
   249  			chat1.ResolveUnfurlPromptArg{
   250  				ConvID:           conv.Id,
   251  				MsgID:            origID,
   252  				Result:           chat1.NewUnfurlPromptResultWithAccept("0.1"),
   253  				IdentifyBehavior: keybase1.TLFIdentifyBehavior_GUI,
   254  			}))
   255  		consumeNewMsgRemote(t, listener0, chat1.MessageType_TEXT) // from whitelist add
   256  		select {
   257  		case <-listener0.newMessageRemote:
   258  			require.Fail(t, "no unfurl yet")
   259  		default:
   260  		}
   261  		recvSingleRetry()
   262  		require.Nil(t, recvUnfurl())
   263  
   264  		t.Logf("try it again and fail")
   265  		tc.Context().MessageDeliverer.ForceDeliverLoop(context.TODO())
   266  		recvSingleRetry()
   267  		require.Nil(t, recvUnfurl())
   268  
   269  		t.Logf("now work")
   270  		// now that we we can succeed
   271  		httpSrv.setSucceed(true)
   272  
   273  		var u *chat1.Unfurl
   274  		for i := 0; i < 10; i++ {
   275  			tc.Context().MessageDeliverer.ForceDeliverLoop(context.TODO())
   276  			recvSingleRetry()
   277  			u = recvUnfurl()
   278  			if u != nil {
   279  				break
   280  			}
   281  			t.Logf("retrying success unfurl, attempt: %d", i)
   282  		}
   283  		require.NotNil(t, u)
   284  		typ, err := u.UnfurlType()
   285  		require.NoError(t, err)
   286  		require.Equal(t, chat1.UnfurlType_GENERIC, typ)
   287  		require.Equal(t, "MIKE", u.Generic().Title)
   288  		recvAndCheckUnfurlMsg(origID)
   289  
   290  		t.Logf("make sure we don't unfurl twice")
   291  		require.NoError(t, ctc.as(t, users[0]).chatLocalHandler().ResolveUnfurlPrompt(ctx,
   292  			chat1.ResolveUnfurlPromptArg{
   293  				ConvID:           conv.Id,
   294  				MsgID:            origID,
   295  				Result:           chat1.NewUnfurlPromptResultWithAccept("0.1"),
   296  				IdentifyBehavior: keybase1.TLFIdentifyBehavior_GUI,
   297  			}))
   298  		time.Sleep(200 * time.Millisecond)
   299  		select {
   300  		case <-listener0.newMessageRemote:
   301  			require.Fail(t, "should not unfurl twice")
   302  		default:
   303  		}
   304  
   305  		t.Logf("delete an unfurl")
   306  		threadRes, err := ctc.as(t, users[0]).chatLocalHandler().GetThreadLocal(ctx, chat1.GetThreadLocalArg{
   307  			ConversationID: conv.Id,
   308  			Query: &chat1.GetThreadQuery{
   309  				MessageTypes: []chat1.MessageType{chat1.MessageType_TEXT},
   310  			},
   311  			IdentifyBehavior: keybase1.TLFIdentifyBehavior_GUI,
   312  		})
   313  		require.NoError(t, err)
   314  		require.Equal(t, 1, len(threadRes.Thread.Messages))
   315  		unfurlMsg := threadRes.Thread.Messages[0]
   316  		require.True(t, unfurlMsg.IsValid())
   317  		require.Equal(t, 1, len(unfurlMsg.Valid().Unfurls))
   318  		unfurlMsgID := func() chat1.MessageID {
   319  			for k := range unfurlMsg.Valid().Unfurls {
   320  				return k
   321  			}
   322  			return chat1.MessageID(0)
   323  		}()
   324  		t.Logf("deleting msgid: %v", unfurlMsgID)
   325  		_, err = ctc.as(t, users[0]).chatLocalHandler().PostDeleteNonblock(ctx, chat1.PostDeleteNonblockArg{
   326  			ConversationID:   conv.Id,
   327  			TlfName:          conv.TlfName,
   328  			Supersedes:       unfurlMsgID,
   329  			IdentifyBehavior: keybase1.TLFIdentifyBehavior_GUI,
   330  		})
   331  		require.NoError(t, err)
   332  		consumeNewMsgRemote(t, listener0, chat1.MessageType_DELETE)
   333  		threadRes, err = ctc.as(t, users[0]).chatLocalHandler().GetThreadLocal(ctx, chat1.GetThreadLocalArg{
   334  			ConversationID: conv.Id,
   335  			Query: &chat1.GetThreadQuery{
   336  				MessageTypes: []chat1.MessageType{chat1.MessageType_TEXT},
   337  			},
   338  			IdentifyBehavior: keybase1.TLFIdentifyBehavior_GUI,
   339  		})
   340  		require.NoError(t, err)
   341  		thread := filterOutboxMessages(threadRes.Thread)
   342  		require.Equal(t, 1, len(thread))
   343  		unfurlMsg = thread[0]
   344  		require.True(t, unfurlMsg.IsValid())
   345  		require.Zero(t, len(unfurlMsg.Valid().Unfurls))
   346  		select {
   347  		case mu := <-listener0.messagesUpdated:
   348  			require.Equal(t, 1, len(mu.Updates))
   349  			require.True(t, mu.Updates[0].IsValid())
   350  			require.Zero(t, len(mu.Updates[0].Valid().Unfurls))
   351  		case <-time.After(timeout):
   352  			require.Fail(t, "no update")
   353  		}
   354  		// only need one of these, since the second path through mergeMaybeNotify will have a deleted
   355  		// unfurl in play
   356  		select {
   357  		case <-listener0.messagesUpdated:
   358  			require.Fail(t, "no more updates")
   359  		default:
   360  		}
   361  
   362  		t.Logf("exploding unfurl: %v", ctc.world.Fc.Now())
   363  		dur := gregor1.ToDurationSec(120 * time.Minute)
   364  		g := ctc.as(t, users[0]).h.G()
   365  		err = g.GetEKLib().KeygenIfNeeded(g.MetaContext(context.Background()))
   366  		require.NoError(t, err)
   367  		origExplodeID := mustPostLocalEphemeralForTest(t, ctc, users[0], conv, msg, &dur)
   368  		consumeNewMsgRemote(t, listener0, chat1.MessageType_TEXT)
   369  		recvAndCheckUnfurlMsg(origExplodeID)
   370  
   371  		t.Logf("try get/set settings")
   372  		require.NoError(t, ctc.as(t, users[0]).chatLocalHandler().SaveUnfurlSettings(ctx,
   373  			chat1.SaveUnfurlSettingsArg{
   374  				Mode:      chat1.UnfurlMode_NEVER,
   375  				Whitelist: []string{"nytimes.com", "cnn.com"},
   376  			}))
   377  		settings, err := ctc.as(t, users[0]).chatLocalHandler().GetUnfurlSettings(ctx)
   378  		require.NoError(t, err)
   379  		require.Equal(t, chat1.UnfurlMode_NEVER, settings.Mode)
   380  		require.Equal(t, []string{"cnn.com", "nytimes.com"}, settings.Whitelist)
   381  
   382  	})
   383  }