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 }