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

     1  package chat
     2  
     3  import (
     4  	"context"
     5  	"fmt"
     6  	"strings"
     7  	"testing"
     8  	"time"
     9  
    10  	"github.com/keybase/client/go/chat/flip"
    11  	"github.com/keybase/client/go/chat/globals"
    12  	"github.com/keybase/client/go/chat/types"
    13  	"github.com/keybase/client/go/externalstest"
    14  	"github.com/keybase/client/go/kbtest"
    15  	"github.com/keybase/client/go/protocol/chat1"
    16  	"github.com/keybase/client/go/protocol/gregor1"
    17  	"github.com/keybase/clockwork"
    18  	"github.com/stretchr/testify/require"
    19  )
    20  
    21  func consumeFlipToResult(t *testing.T, ui *kbtest.ChatUI, listener *serverChatListener,
    22  	gameID chat1.FlipGameIDStr, numUsers int) string {
    23  	timeout := 20 * time.Second
    24  	consumeNewMsgRemote(t, listener, chat1.MessageType_FLIP) // host msg
    25  	for {
    26  		select {
    27  		case updates := <-ui.CoinFlipUpdates:
    28  			require.Equal(t, 1, len(updates))
    29  			t.Logf("update: %v gameID: %s", updates[0].Phase, updates[0].GameID)
    30  			if updates[0].Phase == chat1.UICoinFlipPhase_COMPLETE {
    31  				if updates[0].GameID != gameID {
    32  					// it is possible for a game to produce more than one complete update
    33  					// so if we get one for a different game, just skip it
    34  					t.Logf("skipping complete: looking: %s found: %s", gameID, updates[0].GameID)
    35  					continue
    36  				}
    37  				require.Equal(t, numUsers, len(updates[0].Participants))
    38  				return updates[0].ResultText
    39  			}
    40  		case <-time.After(timeout):
    41  			require.Fail(t, "no complete")
    42  		}
    43  	}
    44  }
    45  func assertNoFlip(t *testing.T, ui *kbtest.ChatUI) {
    46  	select {
    47  	case <-ui.CoinFlipUpdates:
    48  		require.Fail(t, "unexpected coinflip update")
    49  	default:
    50  	}
    51  }
    52  
    53  func TestFlipManagerStartFlip(t *testing.T) {
    54  	t.Skip()
    55  	runWithMemberTypes(t, func(mt chat1.ConversationMembersType) {
    56  		runWithEphemeral(t, mt, func(ephemeralLifetime *gregor1.DurationSec) {
    57  			ctc := makeChatTestContext(t, "FlipManagerStartFlip", 3)
    58  			defer ctc.cleanup()
    59  
    60  			users := ctc.users()
    61  			numUsers := 3
    62  			flip.DefaultCommitmentWindowMsec = 2000
    63  
    64  			var ui0, ui1, ui2 *kbtest.ChatUI
    65  			ui0 = kbtest.NewChatUI()
    66  			ui1 = kbtest.NewChatUI()
    67  			ui2 = kbtest.NewChatUI()
    68  			ctc.as(t, users[0]).h.mockChatUI = ui0
    69  			ctc.as(t, users[1]).h.mockChatUI = ui1
    70  			ctc.as(t, users[2]).h.mockChatUI = ui2
    71  			ctc.world.Tcs[users[0].Username].G.UIRouter = kbtest.NewMockUIRouter(ui0)
    72  			ctc.world.Tcs[users[1].Username].G.UIRouter = kbtest.NewMockUIRouter(ui1)
    73  			ctc.world.Tcs[users[2].Username].G.UIRouter = kbtest.NewMockUIRouter(ui2)
    74  			listener0 := newServerChatListener()
    75  			listener1 := newServerChatListener()
    76  			listener2 := newServerChatListener()
    77  			ctc.as(t, users[0]).h.G().NotifyRouter.AddListener(listener0)
    78  			ctc.as(t, users[1]).h.G().NotifyRouter.AddListener(listener1)
    79  			ctc.as(t, users[2]).h.G().NotifyRouter.AddListener(listener2)
    80  
    81  			t.Logf("uid0: %s", users[0].GetUID())
    82  			t.Logf("uid1: %s", users[1].GetUID())
    83  			t.Logf("uid2: %s", users[2].GetUID())
    84  			conv := mustCreateConversationForTest(t, ctc, users[0], chat1.TopicType_CHAT,
    85  				mt, ctc.as(t, users[1]).user(), ctc.as(t, users[2]).user())
    86  			consumeNewConversation(t, listener0, conv.Id)
    87  			consumeNewConversation(t, listener1, conv.Id)
    88  			consumeNewConversation(t, listener2, conv.Id)
    89  			var policy *chat1.RetentionPolicy
    90  			if ephemeralLifetime != nil {
    91  				p := chat1.NewRetentionPolicyWithEphemeral(chat1.RpEphemeral{Age: *ephemeralLifetime})
    92  				policy = &p
    93  				mustSetConvRetentionLocal(t, ctc, users[0], conv.Id, p)
    94  				consumeNewMsgRemote(t, listener0, chat1.MessageType_SYSTEM)
    95  				consumeNewMsgRemote(t, listener1, chat1.MessageType_SYSTEM)
    96  				consumeNewMsgRemote(t, listener2, chat1.MessageType_SYSTEM)
    97  			}
    98  
    99  			var flipMsgs []chat1.UIMessage
   100  			expectedDevConvs := 0
   101  			// bool
   102  			expectedDevConvs++
   103  			mustPostLocalForTest(t, ctc, users[0], conv,
   104  				chat1.NewMessageBodyWithText(chat1.MessageText{
   105  					Body: "/flip",
   106  				}))
   107  			flipMsg := consumeNewMsgRemote(t, listener0, chat1.MessageType_FLIP)
   108  			flipMsgs = append(flipMsgs, flipMsg)
   109  			require.True(t, flipMsg.IsValid())
   110  			require.NotNil(t, flipMsg.Valid().FlipGameID)
   111  			gameID := *flipMsg.Valid().FlipGameID
   112  			consumeNewMsgRemote(t, listener1, chat1.MessageType_FLIP)
   113  			consumeNewMsgRemote(t, listener2, chat1.MessageType_FLIP)
   114  			res0 := consumeFlipToResult(t, ui0, listener0, gameID, numUsers)
   115  			t.Logf("res0 (coin): %s", res0)
   116  			require.True(t, res0 == "HEADS" || res0 == "TAILS")
   117  			res1 := consumeFlipToResult(t, ui1, listener1, gameID, numUsers)
   118  			require.Equal(t, res0, res1)
   119  			res2 := consumeFlipToResult(t, ui2, listener2, gameID, numUsers)
   120  			require.Equal(t, res0, res2)
   121  
   122  			// limit
   123  			expectedDevConvs++
   124  			mustPostLocalForTest(t, ctc, users[0], conv,
   125  				chat1.NewMessageBodyWithText(chat1.MessageText{
   126  					Body: "/flip 10",
   127  				}))
   128  			flipMsg = consumeNewMsgRemote(t, listener0, chat1.MessageType_FLIP)
   129  			flipMsgs = append(flipMsgs, flipMsg)
   130  			require.True(t, flipMsg.IsValid())
   131  			require.NotNil(t, flipMsg.Valid().FlipGameID)
   132  			gameID = *flipMsg.Valid().FlipGameID
   133  			consumeNewMsgRemote(t, listener1, chat1.MessageType_FLIP)
   134  			consumeNewMsgRemote(t, listener2, chat1.MessageType_FLIP)
   135  			res0 = consumeFlipToResult(t, ui0, listener0, gameID, numUsers)
   136  			found := false
   137  			t.Logf("res0 (limit): %s", res0)
   138  			for i := 1; i <= 10; i++ {
   139  				if res0 == fmt.Sprintf("%d", i) {
   140  					found = true
   141  					break
   142  				}
   143  			}
   144  			require.True(t, found)
   145  			res1 = consumeFlipToResult(t, ui1, listener1, gameID, numUsers)
   146  			require.Equal(t, res0, res1)
   147  			res2 = consumeFlipToResult(t, ui2, listener2, gameID, numUsers)
   148  			require.Equal(t, res0, res2)
   149  
   150  			// range
   151  			expectedDevConvs++
   152  			mustPostLocalForTest(t, ctc, users[0], conv,
   153  				chat1.NewMessageBodyWithText(chat1.MessageText{
   154  					Body: "/flip 10..15",
   155  				}))
   156  			flipMsg = consumeNewMsgRemote(t, listener0, chat1.MessageType_FLIP)
   157  			flipMsgs = append(flipMsgs, flipMsg)
   158  			require.True(t, flipMsg.IsValid())
   159  			require.NotNil(t, flipMsg.Valid().FlipGameID)
   160  			gameID = *flipMsg.Valid().FlipGameID
   161  			consumeNewMsgRemote(t, listener1, chat1.MessageType_FLIP)
   162  			consumeNewMsgRemote(t, listener2, chat1.MessageType_FLIP)
   163  			res0 = consumeFlipToResult(t, ui0, listener0, gameID, numUsers)
   164  			t.Logf("res0 (range): %s", res0)
   165  			found = false
   166  			for i := 10; i <= 15; i++ {
   167  				if res0 == fmt.Sprintf("%d", i) {
   168  					found = true
   169  					break
   170  				}
   171  			}
   172  			require.True(t, found)
   173  			res1 = consumeFlipToResult(t, ui1, listener1, gameID, numUsers)
   174  			require.Equal(t, res0, res1)
   175  			res2 = consumeFlipToResult(t, ui2, listener2, gameID, numUsers)
   176  			require.Equal(t, res0, res2)
   177  
   178  			// shuffle
   179  			ref := []string{"mike", "karen", "lisa", "sara", "anna"}
   180  			refMap := make(map[string]bool)
   181  			for _, r := range ref {
   182  				refMap[r] = true
   183  			}
   184  			expectedDevConvs++
   185  			mustPostLocalForTest(t, ctc, users[0], conv,
   186  				chat1.NewMessageBodyWithText(chat1.MessageText{
   187  					Body: fmt.Sprintf("/flip %s", strings.Join(ref, ",")),
   188  				}))
   189  			flipMsg = consumeNewMsgRemote(t, listener0, chat1.MessageType_FLIP)
   190  			flipMsgs = append(flipMsgs, flipMsg)
   191  			require.True(t, flipMsg.IsValid())
   192  			require.NotNil(t, flipMsg.Valid().FlipGameID)
   193  			gameID = *flipMsg.Valid().FlipGameID
   194  			consumeNewMsgRemote(t, listener1, chat1.MessageType_FLIP)
   195  			consumeNewMsgRemote(t, listener2, chat1.MessageType_FLIP)
   196  			res0 = consumeFlipToResult(t, ui0, listener0, gameID, numUsers)
   197  			t.Logf("res0 (shuffle): %s", res0)
   198  			toks := strings.Split(res0, ",")
   199  			for _, t := range toks {
   200  				delete(refMap, strings.Trim(t, " "))
   201  			}
   202  			require.Zero(t, len(refMap))
   203  			require.True(t, found)
   204  			res1 = consumeFlipToResult(t, ui1, listener1, gameID, numUsers)
   205  			require.Equal(t, res0, res1)
   206  			res2 = consumeFlipToResult(t, ui2, listener2, gameID, numUsers)
   207  			require.Equal(t, res0, res2)
   208  
   209  			uid := users[0].User.GetUID().ToBytes()
   210  			ttype := chat1.TopicType_DEV
   211  			ctx := ctc.as(t, users[0]).startCtx
   212  			ibox, _, err := ctc.as(t, users[0]).h.G().InboxSource.Read(ctx, uid,
   213  				types.ConversationLocalizerBlocking, types.InboxSourceDataSourceAll, nil,
   214  				&chat1.GetInboxLocalQuery{
   215  					TopicType: &ttype,
   216  				})
   217  			require.NoError(t, err)
   218  			numConvs := 0
   219  			for _, conv := range ibox.Convs {
   220  				if strings.HasPrefix(conv.Info.TopicName, gameIDTopicNamePrefix) {
   221  					numConvs++
   222  					require.Equal(t, policy, conv.ConvRetention)
   223  				}
   224  			}
   225  			require.Equal(t, expectedDevConvs, numConvs)
   226  			for _, flipMsg := range flipMsgs {
   227  				mustDeleteMsg(ctx, t, ctc, users[0], conv, flipMsg.GetMessageID())
   228  				consumeNewMsgRemote(t, listener1, chat1.MessageType_DELETE)
   229  				consumeNewMsgRemote(t, listener2, chat1.MessageType_DELETE)
   230  			}
   231  			ibox, _, err = ctc.as(t, users[0]).h.G().InboxSource.Read(ctx, uid,
   232  				types.ConversationLocalizerBlocking, types.InboxSourceDataSourceAll, nil,
   233  				&chat1.GetInboxLocalQuery{
   234  					TopicType: &ttype,
   235  				})
   236  			require.NoError(t, err)
   237  			require.Zero(t, len(ibox.Convs))
   238  		})
   239  	})
   240  }
   241  
   242  func TestFlipManagerChannelFlip(t *testing.T) {
   243  	// ensure only members of a channel are included in the flip
   244  	runWithMemberTypes(t, func(mt chat1.ConversationMembersType) {
   245  		switch mt {
   246  		case chat1.ConversationMembersType_TEAM:
   247  		default:
   248  			return
   249  		}
   250  		ctc := makeChatTestContext(t, "FlipManagerChannelFlip", 3)
   251  		defer ctc.cleanup()
   252  
   253  		users := ctc.users()
   254  		flip.DefaultCommitmentWindowMsec = 500
   255  
   256  		var ui0, ui1, ui2 *kbtest.ChatUI
   257  		ui0 = kbtest.NewChatUI()
   258  		ui1 = kbtest.NewChatUI()
   259  		ui2 = kbtest.NewChatUI()
   260  		ctc.as(t, users[0]).h.mockChatUI = ui0
   261  		ctc.as(t, users[1]).h.mockChatUI = ui1
   262  		ctc.as(t, users[2]).h.mockChatUI = ui2
   263  		ctc.world.Tcs[users[0].Username].G.UIRouter = kbtest.NewMockUIRouter(ui0)
   264  		ctc.world.Tcs[users[1].Username].G.UIRouter = kbtest.NewMockUIRouter(ui1)
   265  		ctc.world.Tcs[users[2].Username].G.UIRouter = kbtest.NewMockUIRouter(ui2)
   266  		listener0 := newServerChatListener()
   267  		listener1 := newServerChatListener()
   268  		listener2 := newServerChatListener()
   269  		ctc.as(t, users[0]).h.G().NotifyRouter.AddListener(listener0)
   270  		ctc.as(t, users[1]).h.G().NotifyRouter.AddListener(listener1)
   271  		ctc.as(t, users[2]).h.G().NotifyRouter.AddListener(listener2)
   272  
   273  		conv := mustCreateConversationForTest(t, ctc, users[0], chat1.TopicType_CHAT,
   274  			mt, ctc.as(t, users[1]).user(), ctc.as(t, users[2]).user())
   275  		consumeNewConversation(t, listener0, conv.Id)
   276  		consumeNewConversation(t, listener1, conv.Id)
   277  		consumeNewConversation(t, listener2, conv.Id)
   278  
   279  		topicName := "channel-1"
   280  		channel := mustCreateChannelForTest(t, ctc, users[0], chat1.TopicType_CHAT,
   281  			&topicName, mt, ctc.as(t, users[1]).user(), ctc.as(t, users[2]).user())
   282  		consumeNewMsgRemote(t, listener0, chat1.MessageType_JOIN)
   283  		consumeNewMsgRemote(t, listener0, chat1.MessageType_SYSTEM)
   284  		consumeNewMsgRemote(t, listener0, chat1.MessageType_SYSTEM)
   285  		consumeNewMsgRemote(t, listener1, chat1.MessageType_SYSTEM)
   286  		consumeNewMsgRemote(t, listener1, chat1.MessageType_SYSTEM)
   287  		consumeNewMsgRemote(t, listener2, chat1.MessageType_SYSTEM)
   288  		consumeNewMsgRemote(t, listener2, chat1.MessageType_SYSTEM)
   289  
   290  		mustJoinConversationByID(t, ctc, users[1], channel.Id)
   291  		consumeNewMsgRemote(t, listener0, chat1.MessageType_JOIN)
   292  		consumeNewMsgRemote(t, listener1, chat1.MessageType_JOIN)
   293  		mustJoinConversationByID(t, ctc, users[2], channel.Id)
   294  		_, err := ctc.as(t, users[2]).chatLocalHandler().LeaveConversationLocal(
   295  			ctc.as(t, users[0]).startCtx, channel.Id)
   296  		require.NoError(t, err)
   297  		consumeNewMsgRemote(t, listener0, chat1.MessageType_JOIN)
   298  		consumeNewMsgRemote(t, listener1, chat1.MessageType_JOIN)
   299  		consumeNewMsgRemote(t, listener2, chat1.MessageType_JOIN)
   300  		consumeNewMsgRemote(t, listener0, chat1.MessageType_LEAVE)
   301  		consumeNewMsgRemote(t, listener1, chat1.MessageType_LEAVE)
   302  
   303  		mustPostLocalForTest(t, ctc, users[0], channel,
   304  			chat1.NewMessageBodyWithText(chat1.MessageText{
   305  				Body: "/flip",
   306  			}))
   307  		flipMsg := consumeNewMsgRemote(t, listener0, chat1.MessageType_FLIP)
   308  		require.True(t, flipMsg.IsValid())
   309  		require.NotNil(t, flipMsg.Valid().FlipGameID)
   310  		gameID := *flipMsg.Valid().FlipGameID
   311  		consumeNewMsgRemote(t, listener1, chat1.MessageType_FLIP)
   312  		res0 := consumeFlipToResult(t, ui0, listener0, gameID, 2)
   313  		require.True(t, res0 == "HEADS" || res0 == "TAILS")
   314  		res1 := consumeFlipToResult(t, ui1, listener1, gameID, 2)
   315  		require.Equal(t, res0, res1)
   316  		assertNoFlip(t, ui2)
   317  	})
   318  }
   319  
   320  func TestFlipManagerParseEdges(t *testing.T) {
   321  	tc := externalstest.SetupTest(t, "flip", 0)
   322  	defer tc.Cleanup()
   323  
   324  	g := globals.NewContext(tc.G, &globals.ChatContext{})
   325  	fm := NewFlipManager(g, nil)
   326  	testCase := func(text string, ftyp flip.FlipType, refMetadata flipTextMetadata) {
   327  		start, metadata := fm.startFromText(text, nil)
   328  		ft, err := start.Params.T()
   329  		require.NoError(t, err)
   330  		require.Equal(t, ftyp, ft)
   331  		require.Equal(t, refMetadata, metadata)
   332  	}
   333  	deck := "2♠️,3♠️,4♠️,5♠️,6♠️,7♠️,8♠️,9♠️,10♠️,J♠️,Q♠️,K♠️,A♠️,2♣️,3♣️,4♣️,5♣️,6♣️,7♣️,8♣️,9♣️,10♣️,J♣️,Q♣️,K♣️,A♣️,2♦️,3♦️,4♦️,5♦️,6♦️,7♦️,8♦️,9♦️,10♦️,J♦️,Q♦️,K♦️,A♦️,2♥️,3♥️,4♥️,5♥️,6♥️,7♥️,8♥️,9♥️,10♥️,J♥️,Q♥️,K♥️,A♥️"
   334  	cards := strings.Split(deck, ",")
   335  	testCase("/flip 10", flip.FlipType_BIG, flipTextMetadata{LowerBound: "1"})
   336  	testCase("/flip 0", flip.FlipType_SHUFFLE, flipTextMetadata{ShuffleItems: []string{"0"}})
   337  	testCase("/flip -1", flip.FlipType_SHUFFLE, flipTextMetadata{ShuffleItems: []string{"-1"}})
   338  	testCase("/flip 1..5", flip.FlipType_BIG, flipTextMetadata{LowerBound: "1"})
   339  	testCase("/flip -20..20", flip.FlipType_BIG, flipTextMetadata{LowerBound: "-20"})
   340  	testCase("/flip -20..20,mike", flip.FlipType_SHUFFLE,
   341  		flipTextMetadata{ShuffleItems: []string{"-20..20", "mike"}})
   342  	testCase("/flip 1..1", flip.FlipType_BIG, flipTextMetadata{LowerBound: "1"})
   343  	testCase("/flip 1..0", flip.FlipType_SHUFFLE, flipTextMetadata{ShuffleItems: []string{"1..0"}})
   344  	testCase("/flip mike, karen,     jim", flip.FlipType_SHUFFLE,
   345  		flipTextMetadata{ShuffleItems: []string{"mike", "karen", "jim"}})
   346  	testCase("/flip mike,    jim bob    j  ,     jim", flip.FlipType_SHUFFLE,
   347  		flipTextMetadata{ShuffleItems: []string{"mike", "jim bob    j", "jim"}})
   348  	testCase("/flip 10...20", flip.FlipType_SHUFFLE, flipTextMetadata{ShuffleItems: []string{"10...20"}})
   349  	testCase("/flip 1,0", flip.FlipType_SHUFFLE, flipTextMetadata{ShuffleItems: []string{"1", "0"}})
   350  	testCase("/flip 1,0", flip.FlipType_SHUFFLE, flipTextMetadata{ShuffleItems: []string{"1", "0"}})
   351  	testCase("/flip cards", flip.FlipType_SHUFFLE, flipTextMetadata{
   352  		ShuffleItems: cards,
   353  		DeckShuffle:  true,
   354  	})
   355  	testCase("/flip cards 5 mikem, joshblum, chris", flip.FlipType_SHUFFLE, flipTextMetadata{
   356  		ShuffleItems:  cards,
   357  		DeckShuffle:   false,
   358  		HandCardCount: 5,
   359  		HandTargets:   []string{"mikem", "joshblum", "chris"},
   360  	})
   361  	testCase("/flip cards 5 mike maxim, lisa maxim, anna ", flip.FlipType_SHUFFLE, flipTextMetadata{
   362  		ShuffleItems:  cards,
   363  		DeckShuffle:   false,
   364  		HandCardCount: 5,
   365  		HandTargets:   []string{"mike maxim", "lisa maxim", "anna"},
   366  	})
   367  	testCase("/flip cards 5     mikem,  ,  ,       joshblum,        chris", flip.FlipType_SHUFFLE,
   368  		flipTextMetadata{
   369  			ShuffleItems:  cards,
   370  			DeckShuffle:   false,
   371  			HandCardCount: 5,
   372  			HandTargets:   []string{"mikem", "joshblum", "chris"},
   373  		})
   374  	testCase("/flip cards 5", flip.FlipType_SHUFFLE, flipTextMetadata{
   375  		ShuffleItems: cards,
   376  		DeckShuffle:  true,
   377  	})
   378  	testCase("/flip cards -5 chris mike", flip.FlipType_SHUFFLE, flipTextMetadata{
   379  		ShuffleItems: cards,
   380  		DeckShuffle:  true,
   381  	})
   382  }
   383  
   384  func TestFlipManagerLoadFlip(t *testing.T) {
   385  	t.Skip()
   386  	runWithMemberTypes(t, func(mt chat1.ConversationMembersType) {
   387  		ctc := makeChatTestContext(t, "FlipManager", 2)
   388  		defer ctc.cleanup()
   389  
   390  		users := ctc.users()
   391  		ui0 := kbtest.NewChatUI()
   392  		ui1 := kbtest.NewChatUI()
   393  		ctc.as(t, users[0]).h.mockChatUI = ui0
   394  		ctc.as(t, users[1]).h.mockChatUI = ui1
   395  		ctc.world.Tcs[users[0].Username].G.UIRouter = kbtest.NewMockUIRouter(ui0)
   396  		ctc.world.Tcs[users[1].Username].G.UIRouter = kbtest.NewMockUIRouter(ui1)
   397  		ctx := ctc.as(t, users[0]).startCtx
   398  		tc := ctc.world.Tcs[users[0].Username]
   399  		uid := users[0].User.GetUID().ToBytes()
   400  		listener0 := newServerChatListener()
   401  		listener1 := newServerChatListener()
   402  		ctc.as(t, users[0]).h.G().NotifyRouter.AddListener(listener0)
   403  		ctc.as(t, users[1]).h.G().NotifyRouter.AddListener(listener1)
   404  		flip.DefaultCommitmentWindowMsec = 500
   405  		timeout := 20 * time.Second
   406  		ctc.world.Tcs[users[0].Username].ChatG.Syncer.(*Syncer).isConnected = true
   407  
   408  		conv := mustCreateConversationForTest(t, ctc, users[0], chat1.TopicType_CHAT, mt,
   409  			ctc.as(t, users[1]).user())
   410  		mustPostLocalForTest(t, ctc, users[0], conv,
   411  			chat1.NewMessageBodyWithText(chat1.MessageText{
   412  				Body: "/flip",
   413  			}))
   414  		flipMsg := consumeNewMsgRemote(t, listener0, chat1.MessageType_FLIP)
   415  		require.True(t, flipMsg.IsValid())
   416  		require.NotNil(t, flipMsg.Valid().FlipGameID)
   417  		strGameID := *flipMsg.Valid().FlipGameID
   418  		consumeNewMsgRemote(t, listener1, chat1.MessageType_FLIP)
   419  		res := consumeFlipToResult(t, ui0, listener0, strGameID, 2)
   420  		require.True(t, res == "HEADS" || res == "TAILS")
   421  		res1 := consumeFlipToResult(t, ui1, listener1, strGameID, 2)
   422  		require.Equal(t, res, res1)
   423  
   424  		hostMsg, err := tc.Context().ConvSource.GetMessage(ctx, conv.Id, uid, 2, nil, nil, true)
   425  		require.NoError(t, err)
   426  		require.True(t, hostMsg.IsValid())
   427  		body := hostMsg.Valid().MessageBody
   428  		require.True(t, body.IsType(chat1.MessageType_FLIP))
   429  		gameID := body.Flip().GameID
   430  
   431  		testLoadFlip := func() {
   432  			tc.Context().CoinFlipManager.LoadFlip(ctx, uid, conv.Id, hostMsg.GetMessageID(),
   433  				body.Flip().FlipConvID, gameID)
   434  			select {
   435  			case updates := <-ui0.CoinFlipUpdates:
   436  				require.Equal(t, 1, len(updates))
   437  				require.Equal(t, chat1.UICoinFlipPhase_COMPLETE, updates[0].Phase)
   438  				require.Equal(t, res, updates[0].ResultText)
   439  			case <-time.After(timeout):
   440  				require.Fail(t, "no updates")
   441  			}
   442  		}
   443  		testLoadFlip()
   444  		err = tc.Context().ConvSource.Clear(ctx, conv.Id, uid, nil)
   445  		require.NoError(t, err)
   446  		tc.Context().CoinFlipManager.(*FlipManager).clearGameCache()
   447  		testLoadFlip()
   448  	})
   449  }
   450  
   451  func TestFlipManagerRateLimit(t *testing.T) {
   452  	t.Skip()
   453  	ctc := makeChatTestContext(t, "TestFlipManagerRateLimit", 2)
   454  	defer ctc.cleanup()
   455  	users := ctc.users()
   456  	useRemoteMock = false
   457  	defer func() { useRemoteMock = true }()
   458  
   459  	ui0 := kbtest.NewChatUI()
   460  	ui1 := kbtest.NewChatUI()
   461  	ctc.as(t, users[0]).h.mockChatUI = ui0
   462  	ctc.as(t, users[1]).h.mockChatUI = ui1
   463  	ctc.world.Tcs[users[0].Username].G.UIRouter = kbtest.NewMockUIRouter(ui0)
   464  	ctc.world.Tcs[users[1].Username].G.UIRouter = kbtest.NewMockUIRouter(ui1)
   465  	listener0 := newServerChatListener()
   466  	listener1 := newServerChatListener()
   467  	ctc.as(t, users[0]).h.G().NotifyRouter.AddListener(listener0)
   468  	ctc.as(t, users[1]).h.G().NotifyRouter.AddListener(listener1)
   469  	flip.DefaultCommitmentWindowMsec = 500
   470  	tc := ctc.world.Tcs[users[0].Username]
   471  	tc1 := ctc.world.Tcs[users[1].Username]
   472  	clock := clockwork.NewFakeClock()
   473  	flipmgr := tc.Context().CoinFlipManager.(*FlipManager)
   474  	flipmgr1 := tc1.Context().CoinFlipManager.(*FlipManager)
   475  	flipmgr.clock = clock
   476  	flipmgr1.clock = clock
   477  	flipmgr.testingServerClock = clock
   478  	flipmgr1.testingServerClock = clock
   479  	flipmgr.maxConvParticipations = 1
   480  	<-flipmgr.Stop(context.TODO())
   481  	<-flipmgr1.Stop(context.TODO())
   482  	flipmgr.Start(context.TODO(), gregor1.UID(users[0].GetUID().ToBytes()))
   483  	flipmgr1.Start(context.TODO(), gregor1.UID(users[1].GetUID().ToBytes()))
   484  	simRealClock := func(stopCh chan struct{}) {
   485  		t := time.NewTicker(100 * time.Millisecond)
   486  		for {
   487  			select {
   488  			case <-t.C:
   489  				clock.Advance(100 * time.Millisecond)
   490  			case <-stopCh:
   491  				return
   492  			}
   493  		}
   494  	}
   495  	t.Logf("uid0: %s", users[0].GetUID())
   496  	t.Logf("uid1: %s", users[1].GetUID())
   497  
   498  	conv := mustCreateConversationForTest(t, ctc, users[0], chat1.TopicType_CHAT,
   499  		chat1.ConversationMembersType_IMPTEAMNATIVE, ctc.as(t, users[1]).user())
   500  	mustPostLocalForTest(t, ctc, users[0], conv,
   501  		chat1.NewMessageBodyWithText(chat1.MessageText{
   502  			Body: "/flip",
   503  		}))
   504  	flipMsg := consumeNewMsgRemote(t, listener0, chat1.MessageType_FLIP)
   505  	require.True(t, flipMsg.IsValid())
   506  	require.NotNil(t, flipMsg.Valid().FlipGameID)
   507  	gameID := *flipMsg.Valid().FlipGameID
   508  	t.Logf("gameID: %s", gameID)
   509  	consumeNewMsgRemote(t, listener1, chat1.MessageType_FLIP)
   510  	stopCh := make(chan struct{})
   511  	go simRealClock(stopCh)
   512  	res := consumeFlipToResult(t, ui0, listener0, gameID, 2)
   513  	require.True(t, res == "HEADS" || res == "TAILS")
   514  	res1 := consumeFlipToResult(t, ui1, listener1, gameID, 2)
   515  	require.Equal(t, res, res1)
   516  	close(stopCh)
   517  
   518  	clock.Advance(time.Minute)
   519  	mustPostLocalForTest(t, ctc, users[1], conv,
   520  		chat1.NewMessageBodyWithText(chat1.MessageText{
   521  			Body: "/flip",
   522  		}))
   523  	select {
   524  	case <-ui0.CoinFlipUpdates:
   525  		require.Fail(t, "no update for 0")
   526  	default:
   527  	}
   528  	flipMsg = consumeNewMsgRemote(t, listener0, chat1.MessageType_FLIP)
   529  	require.True(t, flipMsg.IsValid())
   530  	require.NotNil(t, flipMsg.Valid().FlipGameID)
   531  	gameID = *flipMsg.Valid().FlipGameID
   532  	t.Logf("gameID: %s", gameID)
   533  	stopCh = make(chan struct{})
   534  	go simRealClock(stopCh)
   535  	consumeNewMsgRemote(t, listener0, chat1.MessageType_FLIP) // get host msg
   536  	res = consumeFlipToResult(t, ui1, listener1, gameID, 1)
   537  	require.True(t, res == "HEADS" || res == "TAILS")
   538  	close(stopCh)
   539  
   540  	clock.Advance(10 * time.Minute)
   541  	mustPostLocalForTest(t, ctc, users[1], conv,
   542  		chat1.NewMessageBodyWithText(chat1.MessageText{
   543  			Body: "/flip",
   544  		}))
   545  	flipMsg = consumeNewMsgRemote(t, listener0, chat1.MessageType_FLIP)
   546  	require.True(t, flipMsg.IsValid())
   547  	require.NotNil(t, flipMsg.Valid().FlipGameID)
   548  	gameID = *flipMsg.Valid().FlipGameID
   549  	t.Logf("gameID: %s", gameID)
   550  	consumeNewMsgRemote(t, listener1, chat1.MessageType_FLIP)
   551  	stopCh = make(chan struct{})
   552  	go simRealClock(stopCh)
   553  	res = consumeFlipToResult(t, ui0, listener0, gameID, 2)
   554  	require.True(t, res == "HEADS" || res == "TAILS")
   555  	res1 = consumeFlipToResult(t, ui1, listener1, gameID, 2)
   556  	require.Equal(t, res, res1)
   557  	close(stopCh)
   558  
   559  }