github.com/keybase/client/go@v0.0.0-20241007131713-f10651d043c8/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 }