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

     1  package chat
     2  
     3  import (
     4  	"context"
     5  	"crypto/rand"
     6  	"testing"
     7  	"time"
     8  
     9  	"github.com/keybase/client/go/chat/storage"
    10  	"github.com/keybase/client/go/chat/types"
    11  	"github.com/keybase/client/go/gregor"
    12  	"github.com/keybase/client/go/kbtest"
    13  	"github.com/keybase/client/go/protocol/chat1"
    14  	"github.com/keybase/client/go/protocol/gregor1"
    15  	"github.com/keybase/client/go/protocol/keybase1"
    16  	"github.com/keybase/go-codec/codec"
    17  	"github.com/stretchr/testify/require"
    18  )
    19  
    20  func randBytes(t *testing.T, n int) []byte {
    21  	buf := make([]byte, n)
    22  	if _, err := rand.Read(buf); err != nil {
    23  		t.Fatal(err)
    24  	}
    25  	return buf
    26  }
    27  
    28  func sendSimple(ctx context.Context, t *testing.T, tc *kbtest.ChatTestContext, ph *PushHandler,
    29  	sender types.Sender, conv chat1.Conversation, user *kbtest.FakeUser,
    30  	iboxXform func(chat1.InboxVers) chat1.InboxVers) {
    31  	uid := gregor1.UID(user.User.GetUID().ToBytes())
    32  	convID := conv.GetConvID()
    33  	outboxID := chat1.OutboxID(randBytes(t, 8))
    34  	nr := tc.G.NotifyRouter
    35  	tc.G.NotifyRouter = nil
    36  	pt := chat1.MessagePlaintext{
    37  		ClientHeader: chat1.MessageClientHeader{
    38  			Conv:        conv.Metadata.IdTriple,
    39  			Sender:      uid,
    40  			TlfName:     user.Username,
    41  			TlfPublic:   false,
    42  			MessageType: chat1.MessageType_TEXT,
    43  			OutboxID:    &outboxID,
    44  		},
    45  		MessageBody: chat1.NewMessageBodyWithText(chat1.MessageText{
    46  			Body: "hi",
    47  		}),
    48  	}
    49  	_, boxed, err := sender.Send(ctx, convID, pt, 0, nil, nil, nil)
    50  	require.NoError(t, err)
    51  
    52  	ibox := storage.NewInbox(tc.Context())
    53  	vers, err := ibox.Version(ctx, uid)
    54  	if err != nil {
    55  		require.IsType(t, storage.MissError{}, err)
    56  		vers = 0
    57  	}
    58  	newVers := iboxXform(vers)
    59  	t.Logf("newVers: %d vers: %d", newVers, vers)
    60  	nm := chat1.NewMessagePayload{
    61  		Action:    types.ActionNewMessage,
    62  		ConvID:    conv.GetConvID(),
    63  		Message:   *boxed,
    64  		InboxVers: iboxXform(vers),
    65  		TopicType: chat1.TopicType_CHAT,
    66  	}
    67  	var data []byte
    68  	enc := codec.NewEncoderBytes(&data, &codec.MsgpackHandle{WriteExt: true})
    69  	require.NoError(t, enc.Encode(nm))
    70  	m := gregor1.OutOfBandMessage{
    71  		Uid_:    uid,
    72  		System_: "chat.activity",
    73  		Body_:   data,
    74  	}
    75  
    76  	tc.G.NotifyRouter = nr
    77  	require.NoError(t, ph.Activity(ctx, m))
    78  }
    79  
    80  func TestPushOrdering(t *testing.T) {
    81  	ctx, world, ri2, _, sender, list := setupTest(t, 1)
    82  	defer world.Cleanup()
    83  
    84  	ri := ri2.(*kbtest.ChatRemoteMock)
    85  	u := world.GetUsers()[0]
    86  	uid := u.User.GetUID().ToBytes()
    87  	tc := world.Tcs[u.Username]
    88  	handler := NewPushHandler(tc.Context())
    89  	handler.Start(context.TODO(), nil)
    90  	defer func() { <-handler.Stop(context.TODO()) }()
    91  	handler.SetClock(world.Fc)
    92  	timeout := 2 * time.Second
    93  
    94  	conv := newBlankConv(ctx, t, tc, uid, ri, sender, u.Username)
    95  	sendSimple(ctx, t, tc, handler, sender, conv, u,
    96  		func(vers chat1.InboxVers) chat1.InboxVers { return vers + 1 })
    97  
    98  	select {
    99  	case <-list.incomingRemote:
   100  	case <-time.After(timeout):
   101  		require.Fail(t, "no notification received")
   102  	}
   103  
   104  	sendSimple(ctx, t, tc, handler, sender, conv, u,
   105  		func(vers chat1.InboxVers) chat1.InboxVers { return vers + 2 })
   106  	select {
   107  	case <-list.incomingRemote:
   108  		require.Fail(t, "should not have gotten one of these")
   109  	default:
   110  	}
   111  
   112  	sendSimple(ctx, t, tc, handler, sender, conv, u,
   113  		func(vers chat1.InboxVers) chat1.InboxVers { return vers + 1 })
   114  	select {
   115  	case <-list.incomingRemote:
   116  	case <-time.After(timeout):
   117  		require.Fail(t, "no notification received")
   118  	}
   119  	select {
   120  	case <-list.incomingRemote:
   121  	case <-time.After(timeout):
   122  		require.Fail(t, "no notification received")
   123  	}
   124  	handler.orderer.Lock()
   125  	require.Zero(t, len(handler.orderer.waiters))
   126  	handler.orderer.Unlock()
   127  
   128  	sendSimple(ctx, t, tc, handler, sender, conv, u,
   129  		func(vers chat1.InboxVers) chat1.InboxVers { return vers + 2 })
   130  	select {
   131  	case <-list.incomingRemote:
   132  		require.Fail(t, "should not have gotten one of these")
   133  	default:
   134  	}
   135  
   136  	t.Logf("advancing clock")
   137  	world.Fc.Advance(time.Second)
   138  	select {
   139  	case <-list.incomingRemote:
   140  		require.Fail(t, "not notification expected")
   141  	default:
   142  	}
   143  	world.Fc.Advance(time.Second)
   144  	select {
   145  	case <-list.incomingRemote:
   146  	case <-time.After(timeout):
   147  		require.Fail(t, "no notification received")
   148  	}
   149  	handler.orderer.Lock()
   150  	require.Zero(t, len(handler.orderer.waiters))
   151  	handler.orderer.Unlock()
   152  }
   153  
   154  func TestPushAppState(t *testing.T) {
   155  	ctx, world, ri2, _, sender, list := setupTest(t, 1)
   156  	defer world.Cleanup()
   157  
   158  	ri := ri2.(*kbtest.ChatRemoteMock)
   159  	u := world.GetUsers()[0]
   160  	uid := u.User.GetUID().ToBytes()
   161  	tc := world.Tcs[u.Username]
   162  	handler := NewPushHandler(tc.Context())
   163  	handler.Start(context.TODO(), nil)
   164  	defer func() { <-handler.Stop(context.TODO()) }()
   165  	handler.SetClock(world.Fc)
   166  	conv := newBlankConv(ctx, t, tc, uid, ri, sender, u.Username)
   167  
   168  	tc.G.MobileAppState.Update(keybase1.MobileAppState_BACKGROUND)
   169  	sendSimple(ctx, t, tc, handler, sender, conv, u,
   170  		func(vers chat1.InboxVers) chat1.InboxVers { return vers + 1 })
   171  	select {
   172  	case <-list.incomingRemote:
   173  	case <-time.After(20 * time.Second):
   174  		require.Fail(t, "no message received")
   175  	}
   176  	tc.G.MobileAppState.Update(keybase1.MobileAppState_FOREGROUND)
   177  	sendSimple(ctx, t, tc, handler, sender, conv, u,
   178  		func(vers chat1.InboxVers) chat1.InboxVers { return vers + 1 })
   179  	select {
   180  	case <-list.incomingRemote:
   181  	case <-time.After(20 * time.Second):
   182  		require.Fail(t, "no message received")
   183  	}
   184  }
   185  
   186  func makeTypingNotification(t *testing.T, uid gregor1.UID, convID chat1.ConversationID, typing bool) gregor.OutOfBandMessage {
   187  
   188  	nm := chat1.RemoteUserTypingUpdate{
   189  		Uid:    uid,
   190  		ConvID: convID,
   191  		Typing: typing,
   192  	}
   193  	var data []byte
   194  	enc := codec.NewEncoderBytes(&data, &codec.MsgpackHandle{WriteExt: true})
   195  	require.NoError(t, enc.Encode(nm))
   196  	m := gregor1.OutOfBandMessage{
   197  		Uid_:    uid,
   198  		System_: "chat.typing",
   199  		Body_:   data,
   200  	}
   201  	return m
   202  }
   203  
   204  func TestPushTyping(t *testing.T) {
   205  	ctx, world, ri2, _, sender, list := setupTest(t, 1)
   206  	defer world.Cleanup()
   207  
   208  	ri := ri2.(*kbtest.ChatRemoteMock)
   209  	u := world.GetUsers()[0]
   210  	uid := u.User.GetUID().ToBytes()
   211  	tc := world.Tcs[u.Username]
   212  	handler := NewPushHandler(tc.Context())
   213  	handler.Start(context.TODO(), nil)
   214  	defer func() { <-handler.Stop(context.TODO()) }()
   215  	handler.SetClock(world.Fc)
   216  	handler.typingMonitor.SetClock(world.Fc)
   217  	handler.typingMonitor.SetTimeout(time.Minute)
   218  
   219  	conv := newBlankConv(ctx, t, tc, uid, ri, sender, u.Username)
   220  
   221  	confirmTyping := func(list *chatListener) {
   222  		select {
   223  		case updates := <-list.typingUpdate:
   224  			require.Equal(t, 1, len(updates))
   225  			require.Equal(t, conv.GetConvID(), updates[0].ConvID)
   226  			require.Equal(t, 1, len(updates[0].Typers))
   227  			require.Equal(t, uid, updates[0].Typers[0].Uid.ToBytes())
   228  		case <-time.After(20 * time.Second):
   229  			require.Fail(t, "no typing notification")
   230  		}
   231  	}
   232  
   233  	confirmNotTyping := func(list *chatListener) {
   234  		select {
   235  		case updates := <-list.typingUpdate:
   236  			require.Equal(t, 1, len(updates))
   237  			require.Equal(t, conv.GetConvID(), updates[0].ConvID)
   238  			require.Zero(t, len(updates[0].Typers))
   239  		case <-time.After(2 * time.Second):
   240  			require.Fail(t, "no typing notification")
   241  		}
   242  	}
   243  
   244  	t.Logf("test basic")
   245  	err := handler.Typing(context.TODO(), makeTypingNotification(t, uid, conv.GetConvID(), true))
   246  	require.NoError(t, err)
   247  	confirmTyping(list)
   248  	err = handler.Typing(context.TODO(), makeTypingNotification(t, uid, conv.GetConvID(), false))
   249  	require.NoError(t, err)
   250  	confirmNotTyping(list)
   251  
   252  	t.Logf("test expiration")
   253  	err = handler.Typing(context.TODO(), makeTypingNotification(t, uid, conv.GetConvID(), true))
   254  	require.NoError(t, err)
   255  	confirmTyping(list)
   256  	world.Fc.Advance(time.Hour)
   257  	confirmNotTyping(list)
   258  
   259  	t.Logf("test extend")
   260  	extendCh := make(chan struct{})
   261  	handler.typingMonitor.extendCh = &extendCh
   262  	err = handler.Typing(context.TODO(), makeTypingNotification(t, uid, conv.GetConvID(), true))
   263  	require.NoError(t, err)
   264  	confirmTyping(list)
   265  	world.Fc.Advance(30 * time.Second)
   266  	err = handler.Typing(context.TODO(), makeTypingNotification(t, uid, conv.GetConvID(), true))
   267  	require.NoError(t, err)
   268  	select {
   269  	case <-list.typingUpdate:
   270  		require.Fail(t, "should have extended")
   271  	default:
   272  	}
   273  	select {
   274  	case <-extendCh:
   275  	case <-time.After(20 * time.Second):
   276  		require.Fail(t, "no extend callback")
   277  	}
   278  	world.Fc.Advance(40 * time.Second)
   279  	select {
   280  	case <-list.typingUpdate:
   281  		require.Fail(t, "not far enough")
   282  	default:
   283  	}
   284  	world.Fc.Advance(40 * time.Second)
   285  	confirmNotTyping(list)
   286  
   287  	require.Zero(t, len(handler.typingMonitor.typers))
   288  }