github.com/keybase/client/go@v0.0.0-20241007131713-f10651d043c8/chat/storage/outbox_test.go (about) 1 package storage 2 3 import ( 4 "context" 5 "fmt" 6 "testing" 7 8 "time" 9 10 "github.com/keybase/client/go/kbtest" 11 "github.com/keybase/client/go/libkb" 12 "github.com/keybase/client/go/protocol/chat1" 13 "github.com/keybase/client/go/protocol/gregor1" 14 "github.com/keybase/client/go/protocol/keybase1" 15 "github.com/keybase/clockwork" 16 "github.com/stretchr/testify/require" 17 ) 18 19 func setupOutboxTest(t testing.TB, name string) (kbtest.ChatTestContext, *Outbox, gregor1.UID, clockwork.FakeClock) { 20 ctc := setupCommonTest(t, name) 21 u, err := kbtest.CreateAndSignupFakeUser("ob", ctc.TestContext.G) 22 require.NoError(t, err) 23 uid := gregor1.UID(u.User.GetUID().ToBytes()) 24 cl := clockwork.NewFakeClock() 25 tp := ctc.Context().Env.Test 26 ctc.G.Env = libkb.NewEnv(libkb.AppConfig{ 27 HomeDir: ctc.Context().GetEnv().GetHome(), 28 RunMode: ctc.Context().GetRunMode(), 29 }, nil, ctc.Context().GetLog) 30 ctc.Context().Env.Test = tp 31 ob := NewOutbox(ctc.Context(), uid) 32 ob.SetClock(cl) 33 return ctc, ob, uid, cl 34 } 35 36 func makeMsgPlaintext(body string, uid gregor1.UID) chat1.MessagePlaintext { 37 return chat1.MessagePlaintext{ 38 ClientHeader: chat1.MessageClientHeader{ 39 Sender: uid, 40 OutboxInfo: &chat1.OutboxInfo{}, 41 }, 42 MessageBody: chat1.NewMessageBodyWithText(chat1.MessageText{Body: body}), 43 } 44 } 45 46 func makeMsgPlaintextEphemeral(body string, uid gregor1.UID, ephemeralMetadata *chat1.MsgEphemeralMetadata) chat1.MessagePlaintext { 47 msg := makeMsgPlaintext(body, uid) 48 msg.ClientHeader.EphemeralMetadata = ephemeralMetadata 49 return msg 50 } 51 52 func TestChatOutbox(t *testing.T) { 53 tc, ob, uid, cl := setupOutboxTest(t, "outbox") 54 defer tc.Cleanup() 55 56 var obrs []chat1.OutboxRecord 57 conv := makeConvo(gregor1.Time(5), 1, 1) 58 59 prevOrdinal := outboxOrdinalStart 60 for i := 0; i < 5; i++ { 61 obr, err := ob.PushMessage(context.TODO(), conv.GetConvID(), makeMsgPlaintext("hi", uid), 62 nil, nil, nil, keybase1.TLFIdentifyBehavior_CHAT_CLI) 63 require.Equal(t, obr.Ordinal, prevOrdinal) 64 prevOrdinal++ 65 require.NoError(t, err) 66 obrs = append(obrs, obr) 67 cl.Advance(time.Millisecond) 68 } 69 70 // Basic pull 71 res, err := ob.PullAllConversations(context.TODO(), true, false) 72 require.NoError(t, err) 73 require.Equal(t, obrs, res, "wrong obids") 74 75 // Pull with remove 76 res, err = ob.PullAllConversations(context.TODO(), true, true) 77 require.NoError(t, err) 78 require.Equal(t, obrs, res, "wrong obids") 79 emptyRes, err := ob.PullAllConversations(context.TODO(), true, true) 80 require.NoError(t, err) 81 require.Zero(t, len(emptyRes), "not empty") 82 83 // Record a failed attempt 84 require.NoError(t, ob.RecordFailedAttempt(context.TODO(), obrs[3])) 85 86 // Check to make sure this record now has a failure 87 res, err = ob.PullAllConversations(context.TODO(), true, false) 88 require.NoError(t, err) 89 require.Equal(t, 1, len(res), "wrong len") 90 state, err := res[0].State.State() 91 require.NoError(t, err) 92 require.Equal(t, chat1.OutboxStateType_SENDING, state, "wrong state") 93 require.Equal(t, 1, res[0].State.Sending(), "wrong attempts") 94 95 // Mark one as an error 96 newObr, err := ob.MarkAsError(context.TODO(), obrs[2], chat1.OutboxStateError{ 97 Message: "failed", 98 Typ: chat1.OutboxErrorType_MISC, 99 }) 100 require.NoError(t, err) 101 st, err := newObr.State.State() 102 require.NoError(t, err) 103 require.Equal(t, chat1.OutboxStateType_ERROR, st) 104 require.Equal(t, chat1.OutboxErrorType_MISC, newObr.State.Error().Typ) 105 106 // Check for correct order 107 res, err = ob.PullAllConversations(context.TODO(), true, false) 108 require.NoError(t, err) 109 require.Equal(t, 2, len(res), "wrong len") 110 state, err = res[0].State.State() 111 require.NoError(t, err) 112 require.Equal(t, chat1.OutboxStateType_ERROR, state, "wrong state") 113 state, err = res[1].State.State() 114 require.NoError(t, err) 115 require.Equal(t, chat1.OutboxStateType_SENDING, state, "wrong state") 116 117 // Pull without errors 118 res, err = ob.PullAllConversations(context.TODO(), false, false) 119 require.NoError(t, err) 120 require.Equal(t, 1, len(res), "wrong len") 121 122 // Retry the error 123 t.Logf("retrying the error: %s", obrs[2].OutboxID) 124 _, err = ob.RetryMessage(context.TODO(), obrs[2].OutboxID, nil) 125 require.NoError(t, err) 126 res, err = ob.PullAllConversations(context.TODO(), true, false) 127 require.NoError(t, err) 128 require.Equal(t, 2, len(res), "wrong len") 129 state, err = res[1].State.State() 130 require.NoError(t, err) 131 require.Equal(t, chat1.OutboxStateType_SENDING, state, "wrong state") 132 require.Equal(t, 0, res[1].State.Sending(), "wrong attempts") 133 134 // Remove 2 135 _, err = ob.RemoveMessage(context.TODO(), obrs[2].OutboxID) 136 require.NoError(t, err) 137 res, err = ob.PullAllConversations(context.TODO(), true, false) 138 require.NoError(t, err) 139 require.Equal(t, 1, len(res), "wrong len") 140 require.Equal(t, obrs[3].OutboxID, res[0].OutboxID, "wrong element") 141 142 var tv chat1.ThreadView 143 require.NoError(t, ob.AppendToThread(context.TODO(), conv.GetConvID(), &tv)) 144 require.Equal(t, 1, len(tv.Messages)) 145 newObr, err = ob.MarkAsError(context.TODO(), obrs[3], chat1.OutboxStateError{ 146 Message: "failed", 147 Typ: chat1.OutboxErrorType_DUPLICATE, 148 }) 149 require.NoError(t, err) 150 st, err = newObr.State.State() 151 require.NoError(t, err) 152 require.Equal(t, chat1.OutboxStateType_ERROR, st) 153 require.Equal(t, chat1.OutboxErrorType_DUPLICATE, newObr.State.Error().Typ) 154 tv.Messages = nil 155 require.NoError(t, ob.AppendToThread(context.TODO(), conv.GetConvID(), &tv)) 156 require.Zero(t, len(tv.Messages)) 157 } 158 159 func TestChatOutboxPurge(t *testing.T) { 160 tc, ob, uid, cl := setupOutboxTest(t, "outbox") 161 defer tc.Cleanup() 162 163 var obrs []chat1.OutboxRecord 164 conv := makeConvo(gregor1.Time(5), 1, 1) 165 166 prevOrdinal := outboxOrdinalStart 167 ephemeralMetadata := &chat1.MsgEphemeralMetadata{Lifetime: 0} 168 for i := 0; i < 9; i++ { 169 // send some exploding and some non exploding msgs 170 if i > 3 { 171 ephemeralMetadata = nil 172 } 173 obr, err := ob.PushMessage(context.TODO(), conv.GetConvID(), 174 makeMsgPlaintextEphemeral("hi", uid, ephemeralMetadata), 175 nil, nil, nil, keybase1.TLFIdentifyBehavior_CHAT_CLI) 176 require.Equal(t, obr.Ordinal, prevOrdinal) 177 prevOrdinal++ 178 require.NoError(t, err) 179 obrs = append(obrs, obr) 180 cl.Advance(time.Millisecond) 181 } 182 183 res, err := ob.PullAllConversations(context.TODO(), true, false) 184 require.NoError(t, err) 185 require.Equal(t, obrs, res, "wrong obids") 186 187 // Nothing is in the error state & expired, so we should not have purged anything 188 _, err = ob.OutboxPurge(context.TODO()) 189 require.NoError(t, err) 190 191 res, err = ob.PullAllConversations(context.TODO(), true, false) 192 require.NoError(t, err) 193 require.Equal(t, obrs, res, "wrong obids") 194 195 // Mark 6/9 records as an error, three of these are ephemeral message, 3 196 // regular messages. 197 for i := 0; i < 6; i++ { 198 errRec := chat1.OutboxStateError{ 199 Message: "failed", 200 Typ: chat1.OutboxErrorType_MISC, 201 } 202 obrs[i], err = ob.MarkAsError(context.TODO(), obrs[i], errRec) 203 require.NoError(t, err) 204 } 205 206 _, err = ob.OutboxPurge(context.TODO()) 207 require.NoError(t, err) 208 res, err = ob.PullAllConversations(context.TODO(), true, false) 209 require.NoError(t, err) 210 require.Equal(t, obrs, res, "wrong obids") 211 212 // move the clock forward the ephemeralPurgeCutoff duration and we'll 213 // remove the ephemeral records. 214 cl.Advance(ephemeralPurgeCutoff) 215 _, err = ob.OutboxPurge(context.TODO()) 216 require.NoError(t, err) 217 res, err = ob.PullAllConversations(context.TODO(), true, false) 218 require.NoError(t, err) 219 require.Equal(t, obrs[4:], res, "wrong obids") 220 221 // move the clock forward the errorPurgeCutoff duration and we'll remove 222 // the records that are marked as an error. 223 cl.Advance(errorPurgeCutoff) 224 _, err = ob.OutboxPurge(context.TODO()) 225 require.NoError(t, err) 226 res, err = ob.PullAllConversations(context.TODO(), true, false) 227 require.NoError(t, err) 228 require.Equal(t, obrs[6:], res, "wrong obids") 229 } 230 231 func TestChatOutboxMarkConv(t *testing.T) { 232 tc, ob, uid, cl := setupOutboxTest(t, "outbox") 233 defer tc.Cleanup() 234 235 var obrs []chat1.OutboxRecord 236 conv := makeConvo(gregor1.Time(5), 1, 1) 237 conv2 := makeConvo(gregor1.Time(5), 1, 1) 238 for i := 0; i < 5; i++ { 239 convID := conv.GetConvID() 240 if i%2 == 0 { 241 convID = conv2.GetConvID() 242 } 243 obr, err := ob.PushMessage(context.TODO(), convID, 244 makeMsgPlaintext("hi", uid), 245 nil, nil, nil, keybase1.TLFIdentifyBehavior_CHAT_CLI) 246 require.NoError(t, err) 247 obrs = append(obrs, obr) 248 cl.Advance(time.Millisecond) 249 } 250 251 errConvID := obrs[0].ConvID 252 newObr, err := ob.MarkAsError(context.TODO(), obrs[0], chat1.OutboxStateError{ 253 Message: "failed", 254 Typ: chat1.OutboxErrorType_MISC, 255 }) 256 require.NoError(t, err) 257 st, err := newObr.State.State() 258 require.NoError(t, err) 259 require.Equal(t, chat1.OutboxStateType_ERROR, st) 260 require.Equal(t, chat1.OutboxErrorType_MISC, newObr.State.Error().Typ) 261 262 newObrs, err := ob.MarkConvAsError(context.TODO(), errConvID, chat1.OutboxStateError{ 263 Message: "failed", 264 Typ: chat1.OutboxErrorType_MISC, 265 }) 266 require.NoError(t, err) 267 require.Equal(t, 2, len(newObrs)) 268 for _, newObr := range newObrs { 269 st, err := newObr.State.State() 270 require.NoError(t, err) 271 require.True(t, newObr.ConvID.Eq(errConvID)) 272 require.Equal(t, chat1.OutboxStateType_ERROR, st) 273 require.Equal(t, chat1.OutboxErrorType_MISC, newObr.State.Error().Typ) 274 } 275 276 obrs, err = ob.PullAllConversations(context.TODO(), true, false) 277 require.NoError(t, err) 278 require.Len(t, obrs, 5) 279 for _, obr := range obrs { 280 st, err := obr.State.State() 281 require.NoError(t, err) 282 if obr.ConvID.Eq(errConvID) { 283 require.Equal(t, chat1.OutboxStateType_ERROR, st) 284 require.Equal(t, chat1.OutboxErrorType_MISC, newObr.State.Error().Typ) 285 } else { 286 require.Equal(t, chat1.OutboxStateType_SENDING, st) 287 require.Equal(t, 0, obr.State.Sending()) 288 } 289 } 290 } 291 292 func TestChatOutboxCancelMessagesWithPredicate(t *testing.T) { 293 tc, ob, uid, cl := setupOutboxTest(t, "outbox") 294 defer tc.Cleanup() 295 ctx := context.TODO() 296 297 conv := makeConvo(gregor1.Time(5), 1, 1) 298 for i := 0; i < 5; i++ { 299 _, err := ob.PushMessage(ctx, conv.GetConvID(), 300 makeMsgPlaintext(fmt.Sprintf("hi%d", i), uid), 301 nil, nil, nil, keybase1.TLFIdentifyBehavior_CHAT_CLI) 302 require.NoError(t, err) 303 cl.Advance(time.Millisecond) 304 } 305 306 allFalse := func(obr chat1.OutboxRecord) bool { return false } 307 numCancelled, err := ob.CancelMessagesWithPredicate(ctx, allFalse) 308 require.NoError(t, err) 309 require.Zero(t, numCancelled) 310 res, err := ob.PullAllConversations(ctx, false, false) 311 require.NoError(t, err) 312 require.Len(t, res, 5) 313 314 ith := func(obr chat1.OutboxRecord) bool { 315 txt := obr.Msg.MessageBody.Text().Body 316 return txt == "hi0" || txt == "hi1" 317 } 318 t.Logf("DEBUG: ith") 319 numCancelled, err = ob.CancelMessagesWithPredicate(ctx, ith) 320 require.NoError(t, err) 321 require.Equal(t, 2, numCancelled) 322 res, err = ob.PullAllConversations(ctx, false, false) 323 require.NoError(t, err) 324 require.Len(t, res, 3) 325 326 allTrue := func(obr chat1.OutboxRecord) bool { return true } 327 numCancelled, err = ob.CancelMessagesWithPredicate(ctx, allTrue) 328 require.NoError(t, err) 329 require.Equal(t, 3, numCancelled) 330 res, err = ob.PullAllConversations(ctx, false, false) 331 require.NoError(t, err) 332 require.Zero(t, len(res)) 333 }