github.com/keybase/client/go@v0.0.0-20240309051027-028f7c731f8b/chat/push_test.go (about) 1 package chat 2 3 import ( 4 "context" 5 "crypto/rand" 6 "testing" 7 "time" 8 9 "github.com/keybase/client/go/chat/storage" 10 "github.com/keybase/client/go/chat/types" 11 "github.com/keybase/client/go/gregor" 12 "github.com/keybase/client/go/kbtest" 13 "github.com/keybase/client/go/protocol/chat1" 14 "github.com/keybase/client/go/protocol/gregor1" 15 "github.com/keybase/client/go/protocol/keybase1" 16 "github.com/keybase/go-codec/codec" 17 "github.com/stretchr/testify/require" 18 ) 19 20 func randBytes(t *testing.T, n int) []byte { 21 buf := make([]byte, n) 22 if _, err := rand.Read(buf); err != nil { 23 t.Fatal(err) 24 } 25 return buf 26 } 27 28 func sendSimple(ctx context.Context, t *testing.T, tc *kbtest.ChatTestContext, ph *PushHandler, 29 sender types.Sender, conv chat1.Conversation, user *kbtest.FakeUser, 30 iboxXform func(chat1.InboxVers) chat1.InboxVers) { 31 uid := gregor1.UID(user.User.GetUID().ToBytes()) 32 convID := conv.GetConvID() 33 outboxID := chat1.OutboxID(randBytes(t, 8)) 34 nr := tc.G.NotifyRouter 35 tc.G.NotifyRouter = nil 36 pt := chat1.MessagePlaintext{ 37 ClientHeader: chat1.MessageClientHeader{ 38 Conv: conv.Metadata.IdTriple, 39 Sender: uid, 40 TlfName: user.Username, 41 TlfPublic: false, 42 MessageType: chat1.MessageType_TEXT, 43 OutboxID: &outboxID, 44 }, 45 MessageBody: chat1.NewMessageBodyWithText(chat1.MessageText{ 46 Body: "hi", 47 }), 48 } 49 _, boxed, err := sender.Send(ctx, convID, pt, 0, nil, nil, nil) 50 require.NoError(t, err) 51 52 ibox := storage.NewInbox(tc.Context()) 53 vers, err := ibox.Version(ctx, uid) 54 if err != nil { 55 require.IsType(t, storage.MissError{}, err) 56 vers = 0 57 } 58 newVers := iboxXform(vers) 59 t.Logf("newVers: %d vers: %d", newVers, vers) 60 nm := chat1.NewMessagePayload{ 61 Action: types.ActionNewMessage, 62 ConvID: conv.GetConvID(), 63 Message: *boxed, 64 InboxVers: iboxXform(vers), 65 TopicType: chat1.TopicType_CHAT, 66 } 67 var data []byte 68 enc := codec.NewEncoderBytes(&data, &codec.MsgpackHandle{WriteExt: true}) 69 require.NoError(t, enc.Encode(nm)) 70 m := gregor1.OutOfBandMessage{ 71 Uid_: uid, 72 System_: "chat.activity", 73 Body_: data, 74 } 75 76 tc.G.NotifyRouter = nr 77 require.NoError(t, ph.Activity(ctx, m)) 78 } 79 80 func TestPushOrdering(t *testing.T) { 81 ctx, world, ri2, _, sender, list := setupTest(t, 1) 82 defer world.Cleanup() 83 84 ri := ri2.(*kbtest.ChatRemoteMock) 85 u := world.GetUsers()[0] 86 uid := u.User.GetUID().ToBytes() 87 tc := world.Tcs[u.Username] 88 handler := NewPushHandler(tc.Context()) 89 handler.Start(context.TODO(), nil) 90 defer func() { <-handler.Stop(context.TODO()) }() 91 handler.SetClock(world.Fc) 92 timeout := 2 * time.Second 93 94 conv := newBlankConv(ctx, t, tc, uid, ri, sender, u.Username) 95 sendSimple(ctx, t, tc, handler, sender, conv, u, 96 func(vers chat1.InboxVers) chat1.InboxVers { return vers + 1 }) 97 98 select { 99 case <-list.incomingRemote: 100 case <-time.After(timeout): 101 require.Fail(t, "no notification received") 102 } 103 104 sendSimple(ctx, t, tc, handler, sender, conv, u, 105 func(vers chat1.InboxVers) chat1.InboxVers { return vers + 2 }) 106 select { 107 case <-list.incomingRemote: 108 require.Fail(t, "should not have gotten one of these") 109 default: 110 } 111 112 sendSimple(ctx, t, tc, handler, sender, conv, u, 113 func(vers chat1.InboxVers) chat1.InboxVers { return vers + 1 }) 114 select { 115 case <-list.incomingRemote: 116 case <-time.After(timeout): 117 require.Fail(t, "no notification received") 118 } 119 select { 120 case <-list.incomingRemote: 121 case <-time.After(timeout): 122 require.Fail(t, "no notification received") 123 } 124 handler.orderer.Lock() 125 require.Zero(t, len(handler.orderer.waiters)) 126 handler.orderer.Unlock() 127 128 sendSimple(ctx, t, tc, handler, sender, conv, u, 129 func(vers chat1.InboxVers) chat1.InboxVers { return vers + 2 }) 130 select { 131 case <-list.incomingRemote: 132 require.Fail(t, "should not have gotten one of these") 133 default: 134 } 135 136 t.Logf("advancing clock") 137 world.Fc.Advance(time.Second) 138 select { 139 case <-list.incomingRemote: 140 require.Fail(t, "not notification expected") 141 default: 142 } 143 world.Fc.Advance(time.Second) 144 select { 145 case <-list.incomingRemote: 146 case <-time.After(timeout): 147 require.Fail(t, "no notification received") 148 } 149 handler.orderer.Lock() 150 require.Zero(t, len(handler.orderer.waiters)) 151 handler.orderer.Unlock() 152 } 153 154 func TestPushAppState(t *testing.T) { 155 ctx, world, ri2, _, sender, list := setupTest(t, 1) 156 defer world.Cleanup() 157 158 ri := ri2.(*kbtest.ChatRemoteMock) 159 u := world.GetUsers()[0] 160 uid := u.User.GetUID().ToBytes() 161 tc := world.Tcs[u.Username] 162 handler := NewPushHandler(tc.Context()) 163 handler.Start(context.TODO(), nil) 164 defer func() { <-handler.Stop(context.TODO()) }() 165 handler.SetClock(world.Fc) 166 conv := newBlankConv(ctx, t, tc, uid, ri, sender, u.Username) 167 168 tc.G.MobileAppState.Update(keybase1.MobileAppState_BACKGROUND) 169 sendSimple(ctx, t, tc, handler, sender, conv, u, 170 func(vers chat1.InboxVers) chat1.InboxVers { return vers + 1 }) 171 select { 172 case <-list.incomingRemote: 173 case <-time.After(20 * time.Second): 174 require.Fail(t, "no message received") 175 } 176 tc.G.MobileAppState.Update(keybase1.MobileAppState_FOREGROUND) 177 sendSimple(ctx, t, tc, handler, sender, conv, u, 178 func(vers chat1.InboxVers) chat1.InboxVers { return vers + 1 }) 179 select { 180 case <-list.incomingRemote: 181 case <-time.After(20 * time.Second): 182 require.Fail(t, "no message received") 183 } 184 } 185 186 func makeTypingNotification(t *testing.T, uid gregor1.UID, convID chat1.ConversationID, typing bool) gregor.OutOfBandMessage { 187 188 nm := chat1.RemoteUserTypingUpdate{ 189 Uid: uid, 190 ConvID: convID, 191 Typing: typing, 192 } 193 var data []byte 194 enc := codec.NewEncoderBytes(&data, &codec.MsgpackHandle{WriteExt: true}) 195 require.NoError(t, enc.Encode(nm)) 196 m := gregor1.OutOfBandMessage{ 197 Uid_: uid, 198 System_: "chat.typing", 199 Body_: data, 200 } 201 return m 202 } 203 204 func TestPushTyping(t *testing.T) { 205 ctx, world, ri2, _, sender, list := setupTest(t, 1) 206 defer world.Cleanup() 207 208 ri := ri2.(*kbtest.ChatRemoteMock) 209 u := world.GetUsers()[0] 210 uid := u.User.GetUID().ToBytes() 211 tc := world.Tcs[u.Username] 212 handler := NewPushHandler(tc.Context()) 213 handler.Start(context.TODO(), nil) 214 defer func() { <-handler.Stop(context.TODO()) }() 215 handler.SetClock(world.Fc) 216 handler.typingMonitor.SetClock(world.Fc) 217 handler.typingMonitor.SetTimeout(time.Minute) 218 219 conv := newBlankConv(ctx, t, tc, uid, ri, sender, u.Username) 220 221 confirmTyping := func(list *chatListener) { 222 select { 223 case updates := <-list.typingUpdate: 224 require.Equal(t, 1, len(updates)) 225 require.Equal(t, conv.GetConvID(), updates[0].ConvID) 226 require.Equal(t, 1, len(updates[0].Typers)) 227 require.Equal(t, uid, updates[0].Typers[0].Uid.ToBytes()) 228 case <-time.After(20 * time.Second): 229 require.Fail(t, "no typing notification") 230 } 231 } 232 233 confirmNotTyping := func(list *chatListener) { 234 select { 235 case updates := <-list.typingUpdate: 236 require.Equal(t, 1, len(updates)) 237 require.Equal(t, conv.GetConvID(), updates[0].ConvID) 238 require.Zero(t, len(updates[0].Typers)) 239 case <-time.After(2 * time.Second): 240 require.Fail(t, "no typing notification") 241 } 242 } 243 244 t.Logf("test basic") 245 err := handler.Typing(context.TODO(), makeTypingNotification(t, uid, conv.GetConvID(), true)) 246 require.NoError(t, err) 247 confirmTyping(list) 248 err = handler.Typing(context.TODO(), makeTypingNotification(t, uid, conv.GetConvID(), false)) 249 require.NoError(t, err) 250 confirmNotTyping(list) 251 252 t.Logf("test expiration") 253 err = handler.Typing(context.TODO(), makeTypingNotification(t, uid, conv.GetConvID(), true)) 254 require.NoError(t, err) 255 confirmTyping(list) 256 world.Fc.Advance(time.Hour) 257 confirmNotTyping(list) 258 259 t.Logf("test extend") 260 extendCh := make(chan struct{}) 261 handler.typingMonitor.extendCh = &extendCh 262 err = handler.Typing(context.TODO(), makeTypingNotification(t, uid, conv.GetConvID(), true)) 263 require.NoError(t, err) 264 confirmTyping(list) 265 world.Fc.Advance(30 * time.Second) 266 err = handler.Typing(context.TODO(), makeTypingNotification(t, uid, conv.GetConvID(), true)) 267 require.NoError(t, err) 268 select { 269 case <-list.typingUpdate: 270 require.Fail(t, "should have extended") 271 default: 272 } 273 select { 274 case <-extendCh: 275 case <-time.After(20 * time.Second): 276 require.Fail(t, "no extend callback") 277 } 278 world.Fc.Advance(40 * time.Second) 279 select { 280 case <-list.typingUpdate: 281 require.Fail(t, "not far enough") 282 default: 283 } 284 world.Fc.Advance(40 * time.Second) 285 confirmNotTyping(list) 286 287 require.Zero(t, len(handler.typingMonitor.typers)) 288 }