github.com/keybase/client/go@v0.0.0-20240309051027-028f7c731f8b/chat/journey_card_test.go (about)

     1  package chat
     2  
     3  import (
     4  	"fmt"
     5  	"testing"
     6  	"time"
     7  
     8  	"github.com/keybase/client/go/kbtest"
     9  	"github.com/keybase/client/go/protocol/chat1"
    10  	"github.com/keybase/client/go/protocol/keybase1"
    11  	"github.com/keybase/client/go/teams"
    12  	"github.com/keybase/clockwork"
    13  
    14  	"github.com/keybase/client/go/protocol/gregor1"
    15  	"github.com/stretchr/testify/require"
    16  )
    17  
    18  func TestJourneycardStorage(t *testing.T) {
    19  	useRemoteMock = false
    20  	defer func() { useRemoteMock = true }()
    21  	ctc := makeChatTestContext(t, t.Name(), 1)
    22  	defer ctc.cleanup()
    23  
    24  	users := ctc.users()
    25  	tc0 := ctc.world.Tcs[users[0].Username]
    26  	ctx0 := ctc.as(t, users[0]).startCtx
    27  	uid0 := gregor1.UID(users[0].GetUID().ToBytes())
    28  	t.Logf("uid0: %s", uid0)
    29  
    30  	teamConv := mustCreateConversationForTest(t, ctc, users[0], chat1.TopicType_CHAT,
    31  		chat1.ConversationMembersType_TEAM)
    32  	t.Logf("teamconv: %x", teamConv.Id.DbShortForm())
    33  	teamID, err := keybase1.TeamIDFromString(teamConv.Triple.Tlfid.String())
    34  	require.NoError(t, err)
    35  	convID := teamConv.Id
    36  
    37  	t.Logf("setup complete")
    38  	tc0.ChatG.JourneyCardManager.SentMessage(ctx0, uid0, teamID, convID)
    39  	t.Logf("sent message")
    40  	js, err := tc0.ChatG.JourneyCardManager.(*JourneyCardManager).get(ctx0, uid0)
    41  	require.NoError(t, err)
    42  	jcd, err := js.getTeamData(ctx0, teamID)
    43  	require.NoError(t, err)
    44  	require.True(t, jcd.Convs[convID.ConvIDStr()].SentMessage)
    45  
    46  	t.Logf("switch users")
    47  	uid2kb, err := keybase1.UIDFromString("295a7eea607af32040647123732bc819")
    48  	require.NoError(t, err)
    49  	uid2 := gregor1.UID(uid2kb.ToBytes())
    50  	js, err = tc0.ChatG.JourneyCardManager.(*JourneyCardManager).get(ctx0, uid2)
    51  	require.NoError(t, err)
    52  	jcd, err = js.getTeamData(ctx0, teamID)
    53  	require.NoError(t, err)
    54  	require.False(t, jcd.Convs[convID.ConvIDStr()].SentMessage)
    55  
    56  	t.Logf("switch back")
    57  	js, err = tc0.ChatG.JourneyCardManager.(*JourneyCardManager).get(ctx0, uid0)
    58  	require.NoError(t, err)
    59  	jcd, err = js.getTeamData(ctx0, teamID)
    60  	require.NoError(t, err)
    61  	require.True(t, jcd.Convs[convID.ConvIDStr()].SentMessage)
    62  }
    63  
    64  func TestJourneycardDismiss(t *testing.T) {
    65  	useRemoteMock = false
    66  	defer func() { useRemoteMock = true }()
    67  	ctc := makeChatTestContext(t, t.Name(), 2)
    68  	defer ctc.cleanup()
    69  
    70  	users := ctc.users()
    71  	tc0 := ctc.world.Tcs[users[0].Username]
    72  	ctx0 := ctc.as(t, users[0]).startCtx
    73  	uid0 := gregor1.UID(users[0].GetUID().ToBytes())
    74  	t.Logf("uid0: %s", uid0)
    75  	tc1 := ctc.world.Tcs[users[1].Username]
    76  	ctx1 := ctc.as(t, users[1]).startCtx
    77  	uid1 := gregor1.UID(users[1].GetUID().ToBytes())
    78  	_ = tc1
    79  	_ = ctx1
    80  	t.Logf("uid1: %s", uid1)
    81  
    82  	teamConv := mustCreateConversationForTest(t, ctc, users[0], chat1.TopicType_CHAT,
    83  		chat1.ConversationMembersType_TEAM)
    84  	t.Logf("teamconv: %x", teamConv.Id.DbShortForm())
    85  	teamID, err := keybase1.TeamIDFromString(teamConv.Triple.Tlfid.String())
    86  	require.NoError(t, err)
    87  	convID := teamConv.Id
    88  
    89  	_, err = teams.AddMemberByID(ctx0, tc0.G, teamID, users[1].Username, keybase1.TeamRole_OWNER, nil, nil /* emailInviteMsg */)
    90  	require.NoError(t, err)
    91  
    92  	// In real app usage a SYSTEM message is sent to a team on creation. That doesn't seem to happen in this test jig.
    93  	// Journeycard needs a message to glom onto. Send a TEXT message pretending to be the would-be system message.
    94  	mustPostLocalForTest(t, ctc, users[0], teamConv, chat1.NewMessageBodyWithText(chat1.MessageText{
    95  		Body: "Where there's life there's hope, and need of vittles.",
    96  	}))
    97  	ui := kbtest.NewChatUI()
    98  	ctc.as(t, users[1]).h.mockChatUI = ui
    99  	_, err = ctc.as(t, users[1]).chatLocalHandler().GetThreadNonblock(ctx1,
   100  		chat1.GetThreadNonblockArg{
   101  			ConversationID:   teamConv.Id,
   102  			IdentifyBehavior: keybase1.TLFIdentifyBehavior_CHAT_CLI,
   103  		},
   104  	)
   105  	require.NoError(t, err)
   106  
   107  	requireJourneycard := func(toExist bool) {
   108  		thread, err := tc1.ChatG.ConvSource.Pull(ctx1, convID, uid1,
   109  			chat1.GetThreadReason_GENERAL, nil, nil, nil)
   110  		require.NoError(t, err)
   111  		t.Logf("the messages: %v", chat1.MessageUnboxedDebugList(thread.Messages))
   112  		require.True(t, len(thread.Messages) >= 1)
   113  		if toExist {
   114  			require.NotNil(t, thread.Messages[0].Journeycard__)
   115  		} else {
   116  			for _, msg := range thread.Messages {
   117  				require.Nil(t, msg.Journeycard__)
   118  			}
   119  		}
   120  	}
   121  
   122  	requireJourneycard(true)
   123  	t.Logf("dismiss arbitrary other type that does not appear")
   124  	err = ctc.as(t, users[1]).chatLocalHandler().DismissJourneycard(ctx1, chat1.DismissJourneycardArg{ConvID: convID, CardType: chat1.JourneycardType_ADD_PEOPLE})
   125  	require.NoError(t, err)
   126  	requireJourneycard(true)
   127  	t.Logf("dismiss welcome card")
   128  	err = ctc.as(t, users[1]).chatLocalHandler().DismissJourneycard(ctx1, chat1.DismissJourneycardArg{ConvID: convID, CardType: chat1.JourneycardType_WELCOME})
   129  	require.NoError(t, err)
   130  	requireJourneycard(false)
   131  }
   132  
   133  // Test that dismissing a CHANNEL_INACTIVE in one conv actually dismisses
   134  // CHANNEL_INACTIVE in all convs in he team.
   135  func TestJourneycardDismissTeamwide(t *testing.T) {
   136  	useRemoteMock = false
   137  	defer func() { useRemoteMock = true }()
   138  	ctc := makeChatTestContext(t, t.Name(), 2)
   139  	defer ctc.cleanup()
   140  
   141  	users := ctc.users()
   142  	tc0 := ctc.world.Tcs[users[0].Username]
   143  	ctx0 := ctc.as(t, users[0]).startCtx
   144  	uid0 := gregor1.UID(users[0].GetUID().ToBytes())
   145  	t.Logf("uid0: %s", uid0)
   146  	tc1 := ctc.world.Tcs[users[1].Username]
   147  	ctx1 := ctc.as(t, users[1]).startCtx
   148  	uid1 := gregor1.UID(users[1].GetUID().ToBytes())
   149  	_ = tc1
   150  	_ = ctx1
   151  	t.Logf("uid1: %s", uid1)
   152  
   153  	teamConv := mustCreateConversationForTest(t, ctc, users[0], chat1.TopicType_CHAT,
   154  		chat1.ConversationMembersType_TEAM, users[1])
   155  	t.Logf("teamconv: %x", teamConv.Id.DbShortForm())
   156  	teamID, err := keybase1.TeamIDFromString(teamConv.Triple.Tlfid.String())
   157  	_ = teamID
   158  	require.NoError(t, err)
   159  
   160  	t.Logf("[User u1] create other channels to make POPULAR_CHANNELS eligible for User u0")
   161  	topicNames := []string{"c-a", "c-b", "c-c"}
   162  	allConvIDs := []chat1.ConversationID{teamConv.Id}
   163  	_ = allConvIDs
   164  	allConvInfos := []chat1.ConversationInfoLocal{teamConv}
   165  	for _, topicName := range topicNames {
   166  		res, err := ctc.as(t, users[1]).chatLocalHandler().NewConversationLocal(ctx1,
   167  			chat1.NewConversationLocalArg{
   168  				TlfName:       teamConv.TlfName,
   169  				TopicName:     &topicName,
   170  				TopicType:     chat1.TopicType_CHAT,
   171  				TlfVisibility: keybase1.TLFVisibility_PRIVATE,
   172  				MembersType:   chat1.ConversationMembersType_TEAM,
   173  			})
   174  		require.NoError(t, err)
   175  		allConvIDs = append(allConvIDs, res.Conv.GetConvID())
   176  		allConvInfos = append(allConvInfos, res.Conv.Info)
   177  	}
   178  
   179  	// [User u0] Send a message to make POPULAR_CHANNELS eligible later by SentMessage.
   180  	// [User u1] Send a text message for cards to glom onto.
   181  	for i, convInfo := range allConvInfos {
   182  		var whichUser int
   183  		if i > 0 {
   184  			whichUser = 1
   185  		}
   186  		mustPostLocalForTest(t, ctc, users[whichUser], convInfo, chat1.NewMessageBodyWithText(chat1.MessageText{
   187  			Body: "Fruit flies like a banana.",
   188  		}))
   189  	}
   190  
   191  	requireNoJourneycard := func(convID chat1.ConversationID) {
   192  		thread, err := tc0.ChatG.ConvSource.Pull(ctx0, convID, uid0,
   193  			chat1.GetThreadReason_GENERAL, nil, nil, nil)
   194  		require.NoError(t, err)
   195  		t.Logf("the messages: %v", chat1.MessageUnboxedDebugList(thread.Messages))
   196  		require.True(t, len(thread.Messages) >= 1)
   197  		for _, msg := range thread.Messages {
   198  			require.Nil(t, msg.Journeycard__)
   199  		}
   200  	}
   201  
   202  	requireJourneycard := func(convID chat1.ConversationID, cardType chat1.JourneycardType) {
   203  		thread, err := tc0.ChatG.ConvSource.Pull(ctx0, convID, uid0,
   204  			chat1.GetThreadReason_GENERAL, nil, nil, nil)
   205  		require.NoError(t, err)
   206  		t.Logf("the messages: %v", chat1.MessageUnboxedDebugList(thread.Messages))
   207  		require.True(t, len(thread.Messages) >= 1)
   208  		msg := thread.Messages[0]
   209  		require.NotNil(t, msg.Journeycard__, "requireJourneycard expects a journeycard")
   210  		require.Equal(t, cardType, msg.Journeycard().CardType, "card type")
   211  	}
   212  
   213  	// Wait for journeycardmanager to find out about the team. Calls to SentMessage happen
   214  	// in background goroutines. So sometimes on CI this must be waited for.
   215  	pollFor(t, "hasTeam", 10*time.Second, func(_ int) bool {
   216  		jcm, err := tc0.ChatG.JourneyCardManager.(*JourneyCardManager).get(ctx0, uid0)
   217  		require.NoError(t, err)
   218  		found, nConvs, err := jcm.hasTeam(ctx0, teamID)
   219  		require.NoError(t, err)
   220  		return found && nConvs >= 1
   221  	})
   222  
   223  	requireJourneycard(allConvIDs[0], chat1.JourneycardType_POPULAR_CHANNELS)
   224  	t.Logf("POPULAR_CHANNELS appears only in #general")
   225  	for _, convID := range allConvIDs[1:] {
   226  		requireNoJourneycard(convID)
   227  	}
   228  
   229  	t.Logf("Dismiss POPULAR_CHANNELS")
   230  	err = ctc.as(t, users[0]).chatLocalHandler().DismissJourneycard(ctx0, chat1.DismissJourneycardArg{ConvID: allConvIDs[0], CardType: chat1.JourneycardType_POPULAR_CHANNELS})
   231  	require.NoError(t, err)
   232  	for _, convID := range allConvIDs {
   233  		requireNoJourneycard(convID)
   234  	}
   235  
   236  	t.Logf("Join all conversations")
   237  	for i := 1; i < len(allConvIDs); i++ {
   238  		_, err := ctc.as(t, users[0]).chatLocalHandler().JoinConversationLocal(ctx0, chat1.JoinConversationLocalArg{
   239  			TlfName:    allConvInfos[i].TLFNameExpanded(),
   240  			TopicType:  chat1.TopicType_CHAT,
   241  			Visibility: keybase1.TLFVisibility_PRIVATE,
   242  			TopicName:  allConvInfos[i].TopicName,
   243  		})
   244  		require.NoError(t, err)
   245  	}
   246  
   247  	t.Logf("Advanced time forward enough for CHANNEL_INACTIVE to be eligible")
   248  	nTeams, nConvs, err := tc0.ChatG.JourneyCardManager.(*JourneyCardManager).TimeTravel(ctx0, uid0, time.Hour*24*40+1)
   249  	require.NoError(t, err)
   250  	require.GreaterOrEqual(t, nTeams, 1, "expected known teams to time travel")
   251  	require.GreaterOrEqual(t, nConvs, 1, "expected known convs to time travel")
   252  	for _, convID := range allConvIDs {
   253  		requireJourneycard(convID, chat1.JourneycardType_CHANNEL_INACTIVE)
   254  	}
   255  
   256  	t.Logf("Dismiss CHANNEL_INACTIVE")
   257  	err = ctc.as(t, users[0]).chatLocalHandler().DismissJourneycard(ctx0, chat1.DismissJourneycardArg{ConvID: allConvIDs[0], CardType: chat1.JourneycardType_CHANNEL_INACTIVE})
   258  	require.NoError(t, err)
   259  	for _, convID := range allConvIDs {
   260  		requireNoJourneycard(convID)
   261  	}
   262  }
   263  
   264  // A journeycard sticks in its position in the conv.
   265  // And survives a reboot.
   266  func TestJourneycardPersist(t *testing.T) {
   267  	useRemoteMock = false
   268  	defer func() { useRemoteMock = true }()
   269  	ctc := makeChatTestContext(t, t.Name(), 2)
   270  	defer ctc.cleanup()
   271  
   272  	users := ctc.users()
   273  	tc0 := ctc.world.Tcs[users[0].Username]
   274  	ctx0 := ctc.as(t, users[0]).startCtx
   275  	uid0 := gregor1.UID(users[0].GetUID().ToBytes())
   276  	t.Logf("uid0: %s", uid0)
   277  
   278  	teamConv := mustCreateConversationForTest(t, ctc, users[0], chat1.TopicType_CHAT,
   279  		chat1.ConversationMembersType_TEAM)
   280  	t.Logf("teamconv: %x", teamConv.Id.DbShortForm())
   281  	teamID, err := keybase1.TeamIDFromString(teamConv.Triple.Tlfid.String())
   282  	_ = teamID
   283  	require.NoError(t, err)
   284  
   285  	// Send a text message for cards to glom onto.
   286  	mustPostLocalForTest(t, ctc, users[0], teamConv, chat1.NewMessageBodyWithText(chat1.MessageText{
   287  		Body: "Henry [Thoreau]’s annual melon party, featuring his own delicious watermelons, was a popular event among his neighbors.",
   288  	}))
   289  
   290  	requireJourneycard := func(convID chat1.ConversationID, cardType chat1.JourneycardType, skipMessages int) chat1.MessageUnboxedJourneycard {
   291  		thread, err := tc0.ChatG.ConvSource.Pull(ctx0, convID, uid0,
   292  			chat1.GetThreadReason_GENERAL, nil, nil, nil)
   293  		require.NoError(t, err)
   294  		t.Logf("the messages: %v", chat1.MessageUnboxedDebugList(thread.Messages))
   295  		require.True(t, len(thread.Messages) >= 1+skipMessages)
   296  		msg := thread.Messages[skipMessages]
   297  		require.NotNil(t, msg.Journeycard__, "requireJourneycard expects a journeycard")
   298  		require.Equal(t, cardType, msg.Journeycard().CardType, "card type")
   299  		return msg.Journeycard()
   300  	}
   301  
   302  	// Wait for journeycardmanager to find out about the team. Calls to SentMessage happen
   303  	// in background goroutines. So sometimes on CI this must be waited for.
   304  	pollFor(t, "hasTeam", 10*time.Second, func(_ int) bool {
   305  		jcm, err := tc0.ChatG.JourneyCardManager.(*JourneyCardManager).get(ctx0, uid0)
   306  		require.NoError(t, err)
   307  		found, nConvs, err := jcm.hasTeam(ctx0, teamID)
   308  		require.NoError(t, err)
   309  		return found && nConvs >= 1
   310  	})
   311  
   312  	t.Logf("Advanced time forward enough for ADD_PEOPLE to be eligible")
   313  	nTeams, nConvs, err := tc0.ChatG.JourneyCardManager.(*JourneyCardManager).TimeTravel(ctx0, uid0, time.Hour*24*4+1)
   314  	require.NoError(t, err)
   315  	require.GreaterOrEqual(t, nTeams, 1, "expected known teams to time travel")
   316  	require.GreaterOrEqual(t, nConvs, 1, "expected known convs to time travel")
   317  	jc1 := requireJourneycard(teamConv.Id, chat1.JourneycardType_ADD_PEOPLE, 0)
   318  
   319  	t.Logf("After sending another message the journeycard stays in its original location (ordinal)")
   320  	mustPostLocalForTest(t, ctc, users[0], teamConv, chat1.NewMessageBodyWithText(chat1.MessageText{
   321  		Body: "Henry does not pretend to be totally isolated, but tells his readers from the start that he was only half a mile (0.8 kilometers) from the railroad station and a fifth of a mile (300 meters) to the main road to Concord.",
   322  	}))
   323  	jc2 := requireJourneycard(teamConv.Id, chat1.JourneycardType_ADD_PEOPLE, 1)
   324  	require.Equal(t, jc1.PrevID, jc2.PrevID)
   325  	require.Equal(t, jc1.Ordinal, jc2.Ordinal)
   326  
   327  	t.Logf("After deleting in-memory cache the journeycard statys in its original location")
   328  	js, err := tc0.ChatG.JourneyCardManager.(*JourneyCardManager).get(ctx0, uid0)
   329  	require.NoError(t, err)
   330  	js.lru.Purge()
   331  	jc3 := requireJourneycard(teamConv.Id, chat1.JourneycardType_ADD_PEOPLE, 1)
   332  	require.Equal(t, jc1.PrevID, jc3.PrevID)
   333  	require.Equal(t, jc1.Ordinal, jc3.Ordinal)
   334  }
   335  
   336  func pollFor(t *testing.T, label string, totalTime time.Duration, poller func(i int) bool) {
   337  	t.Logf("pollFor '%s'", label)
   338  	clock := clockwork.NewRealClock()
   339  	start := clock.Now()
   340  	endCh := clock.After(totalTime)
   341  	wait := 10 * time.Millisecond
   342  	var i int
   343  	for {
   344  		satisfied := poller(i)
   345  		since := clock.Since(start)
   346  		t.Logf("pollFor '%s' round:%v -> %v running:%v", label, i, satisfied, since)
   347  		if satisfied {
   348  			t.Logf("pollFor '%s' succeeded after %v attempts over %v", label, i, since)
   349  			return
   350  		}
   351  		if since > totalTime {
   352  			// Game over
   353  			msg := fmt.Sprintf("pollFor '%s' timed out after %v attempts over %v", label, i, since)
   354  			t.Logf(msg)
   355  			require.Fail(t, msg)
   356  			require.FailNow(t, msg)
   357  			return
   358  		}
   359  		t.Logf("pollFor '%s' wait:%v", label, wait)
   360  		select {
   361  		case <-endCh:
   362  		case <-clock.After(wait):
   363  		}
   364  		wait *= 2
   365  		i++
   366  	}
   367  }