github.com/keybase/client/go@v0.0.0-20240309051027-028f7c731f8b/chat/livelocation_test.go (about) 1 package chat 2 3 import ( 4 "context" 5 "net/url" 6 "strconv" 7 "testing" 8 "time" 9 10 "github.com/keybase/client/go/chat/commands" 11 "github.com/keybase/client/go/chat/globals" 12 "github.com/keybase/client/go/chat/maps" 13 "github.com/keybase/client/go/chat/storage" 14 "github.com/keybase/client/go/chat/types" 15 "github.com/keybase/client/go/chat/utils" 16 "github.com/keybase/client/go/kbtest" 17 "github.com/keybase/client/go/protocol/chat1" 18 "github.com/keybase/client/go/protocol/gregor1" 19 "github.com/keybase/client/go/protocol/keybase1" 20 "github.com/keybase/clockwork" 21 "github.com/stretchr/testify/require" 22 ) 23 24 type mockChatUI struct { 25 utils.NullChatUI 26 watchID chat1.LocationWatchID 27 watchCh chan chat1.LocationWatchID 28 clearCh chan chat1.LocationWatchID 29 } 30 31 func newMockChatUI() *mockChatUI { 32 return &mockChatUI{ 33 watchCh: make(chan chat1.LocationWatchID, 10), 34 clearCh: make(chan chat1.LocationWatchID, 10), 35 } 36 } 37 38 func (m *mockChatUI) ChatWatchPosition(context.Context, chat1.ConversationID, chat1.UIWatchPositionPerm) (chat1.LocationWatchID, error) { 39 m.watchID++ 40 m.watchCh <- m.watchID 41 return m.watchID, nil 42 } 43 44 func (m *mockChatUI) ChatClearWatch(ctx context.Context, watchID chat1.LocationWatchID) error { 45 m.clearCh <- watchID 46 return nil 47 } 48 49 func (m *mockChatUI) ChatCommandStatus(context.Context, chat1.ConversationID, string, 50 chat1.UICommandStatusDisplayTyp, []chat1.UICommandStatusActionTyp) error { 51 return nil 52 } 53 54 type unfurlData struct { 55 done bool 56 coords []chat1.Coordinate 57 } 58 59 type mockUnfurler struct { 60 globals.Contextified 61 types.DummyUnfurler 62 t *testing.T 63 unfurlCh chan unfurlData 64 } 65 66 var _ types.Unfurler = (*mockUnfurler)(nil) 67 68 func newMockUnfurler(g *globals.Context, t *testing.T) *mockUnfurler { 69 return &mockUnfurler{ 70 Contextified: globals.NewContextified(g), 71 t: t, 72 unfurlCh: make(chan unfurlData, 10), 73 } 74 } 75 76 func (m *mockUnfurler) Prefetch(ctx context.Context, uid gregor1.UID, convID chat1.ConversationID, 77 msgText string) int { 78 return 0 79 } 80 81 func (m *mockUnfurler) UnfurlAndSend(ctx context.Context, uid gregor1.UID, convID chat1.ConversationID, 82 msg chat1.MessageUnboxed) { 83 require.True(m.t, msg.IsValid()) 84 body := msg.Valid().MessageBody 85 require.True(m.t, body.IsType(chat1.MessageType_TEXT)) 86 mapurl := body.Text().Body 87 u, err := url.Parse(mapurl) 88 require.NoError(m.t, err) 89 livekey := u.Query().Get("livekey") 90 slat := u.Query().Get("lat") 91 slon := u.Query().Get("lon") 92 sdone := u.Query().Get("done") 93 shouldNotify := false 94 if len(livekey) > 0 { 95 done, err := strconv.ParseBool(sdone) 96 require.NoError(m.t, err) 97 shouldNotify = true 98 m.unfurlCh <- unfurlData{ 99 done: done, 100 coords: m.G().LiveLocationTracker.GetCoordinates(ctx, types.LiveLocationKey(livekey)), 101 } 102 } else if len(slat) > 0 { 103 shouldNotify = true 104 lat, err := strconv.ParseFloat(slat, 64) 105 require.NoError(m.t, err) 106 lon, err := strconv.ParseFloat(slon, 64) 107 require.NoError(m.t, err) 108 m.unfurlCh <- unfurlData{ 109 done: true, 110 coords: []chat1.Coordinate{ 111 { 112 Lat: lat, 113 Lon: lon, 114 }, 115 }} 116 } 117 if !shouldNotify { 118 return 119 } 120 outboxID := storage.GetOutboxIDFromURL(mapurl, convID, msg) 121 mvalid := msg.Valid() 122 mvalid.ClientHeader.OutboxID = &outboxID 123 notMsg := chat1.NewMessageUnboxedWithValid(mvalid) 124 activity := chat1.NewChatActivityWithIncomingMessage(chat1.IncomingMessage{ 125 Message: utils.PresentMessageUnboxed(ctx, m.G(), notMsg, uid, convID), 126 ConvID: convID, 127 }) 128 m.G().NotifyRouter.HandleNewChatActivity(ctx, keybase1.UID(uid.String()), chat1.TopicType_CHAT, 129 &activity, chat1.ChatActivitySource_LOCAL, false) 130 } 131 132 func checkCoords(t *testing.T, unfurler *mockUnfurler, refcoords []chat1.Coordinate, timeout time.Duration) bool { 133 var dat unfurlData 134 select { 135 case dat = <-unfurler.unfurlCh: 136 require.Equal(t, refcoords, dat.coords) 137 case <-time.After(timeout): 138 require.Fail(t, "no map unfurl") 139 } 140 return dat.done 141 } 142 143 func updateCoords(t *testing.T, livelocation *maps.LiveLocationTracker, coords []chat1.Coordinate, 144 allCoords []chat1.Coordinate, coordsCh chan struct{}) []chat1.Coordinate { 145 for _, c := range coords { 146 livelocation.LocationUpdate(context.TODO(), c) 147 allCoords = append(allCoords, c) 148 } 149 for i := 0; i < len(coords); i++ { 150 select { 151 case <-coordsCh: 152 case <-time.After(20 * time.Second): 153 require.Fail(t, "no coords ack") 154 } 155 } 156 return allCoords 157 } 158 159 func TestChatSrvLiveLocationCurrent(t *testing.T) { 160 useRemoteMock = false 161 defer func() { useRemoteMock = true }() 162 ctc := makeChatTestContext(t, "TestChatSrvLiveLocationCurrent", 1) 163 defer ctc.cleanup() 164 165 users := ctc.users() 166 tc := ctc.world.Tcs[users[0].Username] 167 chatUI := newMockChatUI() 168 clock := clockwork.NewFakeClock() 169 tc.G.UIRouter = kbtest.NewMockUIRouter(chatUI) 170 timeout := 20 * time.Second 171 172 conv := mustCreateConversationForTest(t, ctc, users[0], chat1.TopicType_CHAT, 173 chat1.ConversationMembersType_IMPTEAMNATIVE) 174 175 coordsCh := make(chan struct{}, 10) 176 unfurler := newMockUnfurler(tc.Context(), t) 177 tc.ChatG.Unfurler = unfurler 178 livelocation := maps.NewLiveLocationTracker(tc.Context()) 179 livelocation.SetClock(clock) 180 livelocation.TestingCoordsAddedCh = coordsCh 181 tc.ChatG.LiveLocationTracker = livelocation 182 tc.ChatG.CommandsSource.(*commands.Source).SetClock(clock) 183 184 mustPostLocalForTest(t, ctc, users[0], conv, chat1.NewMessageBodyWithText(chat1.MessageText{ 185 Body: "/location", 186 })) 187 select { 188 case <-chatUI.watchCh: 189 case <-time.After(timeout): 190 require.Fail(t, "no watch position call") 191 } 192 193 coords := []chat1.Coordinate{ 194 { 195 Lat: 40.800348, 196 Lon: -73.968784, 197 }, 198 } 199 updateCoords(t, livelocation, coords, nil, coordsCh) 200 checkCoords(t, unfurler, []chat1.Coordinate{coords[0]}, timeout) 201 clock.Advance(10 * time.Second) 202 select { 203 case <-unfurler.unfurlCh: 204 require.Fail(t, "should not have updated yet") 205 default: 206 } 207 select { 208 case <-chatUI.clearCh: 209 case <-time.After(timeout): 210 require.Fail(t, "no clear call") 211 } 212 } 213 214 func TestChatSrvLiveLocation(t *testing.T) { 215 useRemoteMock = false 216 defer func() { useRemoteMock = true }() 217 ctc := makeChatTestContext(t, "TestChatSrvLiveLocation", 1) 218 defer ctc.cleanup() 219 220 users := ctc.users() 221 tc := ctc.world.Tcs[users[0].Username] 222 chatUI := newMockChatUI() 223 clock := clockwork.NewFakeClock() 224 tc.G.UIRouter = kbtest.NewMockUIRouter(chatUI) 225 timeout := 20 * time.Second 226 227 conv := mustCreateConversationForTest(t, ctc, users[0], chat1.TopicType_CHAT, 228 chat1.ConversationMembersType_IMPTEAMNATIVE) 229 230 coordsCh := make(chan struct{}, 10) 231 unfurler := newMockUnfurler(tc.Context(), t) 232 tc.ChatG.Unfurler = unfurler 233 livelocation := maps.NewLiveLocationTracker(tc.Context()) 234 livelocation.SetClock(clock) 235 livelocation.TestingCoordsAddedCh = coordsCh 236 tc.ChatG.LiveLocationTracker = livelocation 237 tc.ChatG.CommandsSource.(*commands.Source).SetClock(clock) 238 239 // Start up a live location session 240 mustPostLocalForTest(t, ctc, users[0], conv, chat1.NewMessageBodyWithText(chat1.MessageText{ 241 Body: "/location live 1h", 242 })) 243 select { 244 case <-chatUI.watchCh: 245 case <-time.After(timeout): 246 require.Fail(t, "no watch position call") 247 } 248 // First update always comes through 249 var allCoords []chat1.Coordinate 250 coords := []chat1.Coordinate{{ 251 Lat: 40.800348, 252 Lon: -73.968784, 253 }} 254 allCoords = updateCoords(t, livelocation, coords, allCoords, coordsCh) 255 checkCoords(t, unfurler, coords, timeout) 256 257 // Throw some updates in 258 coords = []chat1.Coordinate{ 259 { 260 Lat: 40.798688, 261 Lon: -73.973716, 262 }, 263 { 264 Lat: 40.795234, 265 Lon: -73.976237, 266 }, 267 } 268 allCoords = updateCoords(t, livelocation, coords, allCoords, coordsCh) 269 // no new map yet 270 select { 271 case <-unfurler.unfurlCh: 272 require.Fail(t, "should not have updated yet") 273 default: 274 } 275 // advance clock to get a new map 276 clock.Advance(time.Minute) 277 checkCoords(t, unfurler, allCoords, timeout) 278 279 // make sure we clear after finishing 280 clock.Advance(2 * time.Hour) 281 select { 282 case <-chatUI.clearCh: 283 case <-time.After(timeout): 284 require.Fail(t, "no clear call") 285 } 286 } 287 288 func TestChatSrvLiveLocationMultiple(t *testing.T) { 289 useRemoteMock = false 290 defer func() { useRemoteMock = true }() 291 ctc := makeChatTestContext(t, "TestChatSrvLiveLocation", 1) 292 defer ctc.cleanup() 293 294 users := ctc.users() 295 tc := ctc.world.Tcs[users[0].Username] 296 chatUI := newMockChatUI() 297 clock := clockwork.NewFakeClock() 298 tc.G.UIRouter = kbtest.NewMockUIRouter(chatUI) 299 timeout := 20 * time.Second 300 301 conv := mustCreateConversationForTest(t, ctc, users[0], chat1.TopicType_CHAT, 302 chat1.ConversationMembersType_IMPTEAMNATIVE) 303 304 coordsCh := make(chan struct{}, 10) 305 unfurler := newMockUnfurler(tc.Context(), t) 306 tc.ChatG.Unfurler = unfurler 307 livelocation := maps.NewLiveLocationTracker(tc.Context()) 308 livelocation.SetClock(clock) 309 livelocation.TestingCoordsAddedCh = coordsCh 310 tc.ChatG.LiveLocationTracker = livelocation 311 tc.ChatG.CommandsSource.(*commands.Source).SetClock(clock) 312 313 var tracker1, tracker2 chat1.LocationWatchID 314 mustPostLocalForTest(t, ctc, users[0], conv, chat1.NewMessageBodyWithText(chat1.MessageText{ 315 Body: "/location live 1h", 316 })) 317 select { 318 case tracker1 = <-chatUI.watchCh: 319 case <-time.After(timeout): 320 require.Fail(t, "no watch position call") 321 } 322 323 mustPostLocalForTest(t, ctc, users[0], conv, chat1.NewMessageBodyWithText(chat1.MessageText{ 324 Body: "/location live 3h", 325 })) 326 select { 327 case tracker2 = <-chatUI.watchCh: 328 case <-time.After(timeout): 329 require.Fail(t, "no watch position call") 330 } 331 332 var allCoords []chat1.Coordinate 333 coords := []chat1.Coordinate{{ 334 Lat: 40.800348, 335 Lon: -73.968784, 336 }} 337 allCoords = updateCoords(t, livelocation, coords, allCoords, coordsCh) 338 checkCoords(t, unfurler, coords, timeout) 339 checkCoords(t, unfurler, coords, timeout) 340 341 clock.Advance(2 * time.Hour) 342 select { 343 case watchID := <-chatUI.clearCh: 344 require.Equal(t, tracker1, watchID) 345 case <-time.After(timeout): 346 require.Fail(t, "no clear call") 347 } 348 select { 349 case <-chatUI.clearCh: 350 require.Fail(t, "only one tracker should die") 351 default: 352 } 353 // trackers fire after time moves up 354 done := checkCoords(t, unfurler, coords, timeout) 355 if !done { 356 checkCoords(t, unfurler, coords, timeout) // tracker 1 expires and posts again 357 } 358 select { 359 case <-unfurler.unfurlCh: 360 require.Fail(t, "no more unfurls here") 361 default: 362 } 363 364 coords = []chat1.Coordinate{ 365 { 366 Lat: 40.798688, 367 Lon: -73.973716, 368 }, 369 { 370 Lat: 40.795234, 371 Lon: -73.976237, 372 }, 373 } 374 allCoords = updateCoords(t, livelocation, coords, allCoords, coordsCh) 375 clock.Advance(time.Minute) 376 checkCoords(t, unfurler, allCoords, timeout) 377 select { 378 case <-unfurler.unfurlCh: 379 require.Fail(t, "tracker 1 is done, no update from it") 380 default: 381 } 382 383 clock.Advance(2 * time.Hour) 384 select { 385 case watchID := <-chatUI.clearCh: 386 require.Equal(t, tracker2, watchID) 387 case <-time.After(timeout): 388 require.Fail(t, "no clear call") 389 } 390 } 391 392 func TestChatSrvLiveLocationStopTracking(t *testing.T) { 393 useRemoteMock = false 394 defer func() { useRemoteMock = true }() 395 ctc := makeChatTestContext(t, "TestChatSrvLiveLocationStopTracking", 1) 396 defer ctc.cleanup() 397 398 users := ctc.users() 399 tc := ctc.world.Tcs[users[0].Username] 400 chatUI := newMockChatUI() 401 clock := clockwork.NewFakeClock() 402 tc.G.UIRouter = kbtest.NewMockUIRouter(chatUI) 403 timeout := 20 * time.Second 404 405 conv := mustCreateConversationForTest(t, ctc, users[0], chat1.TopicType_CHAT, 406 chat1.ConversationMembersType_IMPTEAMNATIVE) 407 408 coordsCh := make(chan struct{}, 10) 409 unfurler := newMockUnfurler(tc.Context(), t) 410 tc.ChatG.Unfurler = unfurler 411 livelocation := maps.NewLiveLocationTracker(tc.Context()) 412 livelocation.SetClock(clock) 413 livelocation.TestingCoordsAddedCh = coordsCh 414 tc.ChatG.LiveLocationTracker = livelocation 415 tc.ChatG.CommandsSource.(*commands.Source).SetClock(clock) 416 livelocation.Start(context.TODO(), users[0].User.GetUID().ToBytes()) 417 require.False(t, livelocation.ActivelyTracking(context.TODO())) 418 419 mustPostLocalForTest(t, ctc, users[0], conv, chat1.NewMessageBodyWithText(chat1.MessageText{ 420 Body: "/location live 1h", 421 })) 422 coords := []chat1.Coordinate{{ 423 Lat: 40.800348, 424 Lon: -73.968784, 425 }} 426 updateCoords(t, livelocation, coords, nil, coordsCh) 427 checkCoords(t, unfurler, coords, timeout) 428 429 livelocation.StopAllTracking(context.TODO()) 430 checkCoords(t, unfurler, coords, timeout) 431 432 livelocation.Start(context.TODO(), users[0].User.GetUID().ToBytes()) 433 require.False(t, livelocation.ActivelyTracking(context.TODO())) 434 }