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  }