github.com/keybase/client/go@v0.0.0-20241007131713-f10651d043c8/chat/attachments/uploader_test.go (about) 1 package attachments 2 3 import ( 4 "errors" 5 "io" 6 "path/filepath" 7 "runtime" 8 "testing" 9 "time" 10 11 "github.com/keybase/client/go/chat/globals" 12 "github.com/keybase/client/go/chat/storage" 13 "github.com/keybase/client/go/chat/types" 14 "github.com/keybase/client/go/kbtest" 15 "github.com/keybase/client/go/libkb" 16 "github.com/stretchr/testify/require" 17 18 "github.com/keybase/client/go/protocol/chat1" 19 "github.com/keybase/client/go/protocol/gregor1" 20 "golang.org/x/net/context" 21 ) 22 23 type mockStore struct { 24 Store 25 uploadFn func(context.Context, *UploadTask) (chat1.Asset, error) 26 } 27 28 func (m *mockStore) UploadAsset(ctx context.Context, task *UploadTask, encryptedOut io.Writer) (chat1.Asset, error) { 29 return m.uploadFn(ctx, task) 30 } 31 32 type mockRemote struct { 33 chat1.RemoteInterface 34 } 35 36 func (r mockRemote) GetS3Params(context.Context, chat1.GetS3ParamsArg) (chat1.S3Params, error) { 37 return chat1.S3Params{}, nil 38 } 39 40 func (r mockRemote) S3Sign(context.Context, chat1.S3SignArg) ([]byte, error) { 41 return nil, nil 42 } 43 44 type mockActivityNotifier struct { 45 types.ActivityNotifier 46 startCh chan chat1.OutboxID 47 } 48 49 func newMockActivityNotifier() *mockActivityNotifier { 50 return &mockActivityNotifier{ 51 startCh: make(chan chat1.OutboxID, 1000), 52 } 53 } 54 55 func (a *mockActivityNotifier) AttachmentUploadStart(ctx context.Context, uid gregor1.UID, 56 convID chat1.ConversationID, outboxID chat1.OutboxID) { 57 a.startCh <- outboxID 58 } 59 60 func (a *mockActivityNotifier) AttachmentUploadProgress(ctx context.Context, uid gregor1.UID, 61 convID chat1.ConversationID, outboxID chat1.OutboxID, bytesComplete, bytesTotal int64) { 62 63 } 64 65 type mockDeliverer struct { 66 types.MessageDeliverer 67 forceCh chan struct{} 68 } 69 70 func newMockDeliverer() *mockDeliverer { 71 return &mockDeliverer{ 72 forceCh: make(chan struct{}, 1000), 73 } 74 } 75 76 func (m *mockDeliverer) ForceDeliverLoop(context.Context) { 77 m.forceCh <- struct{}{} 78 } 79 80 func (m *mockDeliverer) Stop(context.Context) chan struct{} { 81 ch := make(chan struct{}) 82 close(ch) 83 return ch 84 } 85 86 type mockInboxSource struct { 87 types.InboxSource 88 } 89 90 func (m mockInboxSource) ReadUnverified(ctx context.Context, uid gregor1.UID, 91 dataSource types.InboxSourceDataSourceTyp, rquery *chat1.GetInboxQuery) (types.Inbox, error) { 92 return types.Inbox{ 93 ConvsUnverified: []types.RemoteConversation{{ 94 Conv: chat1.Conversation{ 95 Metadata: chat1.ConversationMetadata{ 96 ConversationID: chat1.ConversationID([]byte{0, 1, 0}), 97 IdTriple: chat1.ConversationIDTriple{ 98 TopicType: chat1.TopicType_CHAT, 99 }, 100 }, 101 }, 102 }, 103 }, 104 }, nil 105 } 106 func (m mockInboxSource) Stop(context.Context) chan struct{} { 107 ch := make(chan struct{}) 108 close(ch) 109 return ch 110 } 111 112 func TestAttachmentUploader(t *testing.T) { 113 world := kbtest.NewChatMockWorld(t, "uploader", 1) 114 defer world.Cleanup() 115 116 u := world.GetUsers()[0] 117 uid := gregor1.UID(u.User.GetUID().ToBytes()) 118 tc := world.Tcs[u.Username] 119 g := globals.NewContext(tc.G, tc.ChatG) 120 notifier := newMockActivityNotifier() 121 store := &mockStore{} 122 ri := mockRemote{} 123 deliverer := newMockDeliverer() 124 g.AttachmentURLSrv = types.DummyAttachmentHTTPSrv{} 125 g.InboxSource = mockInboxSource{} 126 g.ActivityNotifier = notifier 127 g.MessageDeliverer = deliverer 128 getRi := func() chat1.RemoteInterface { return ri } 129 cacheSize := 1 130 uploader := NewUploader(g, store, NewS3Signer(getRi), getRi, cacheSize) 131 convID := chat1.ConversationID([]byte{0, 1, 0}) 132 md, err := libkb.RandBytes(10) 133 require.NoError(t, err) 134 135 uploadStartCheck := func(shouldHappen bool, outboxID chat1.OutboxID) { 136 if shouldHappen { 137 select { 138 case obid := <-notifier.startCh: 139 require.Equal(t, outboxID, obid) 140 case <-time.After(20 * time.Second): 141 require.Fail(t, "no start") 142 } 143 } else { 144 select { 145 case <-notifier.startCh: 146 require.Fail(t, "start not supposed to happen") 147 default: 148 } 149 } 150 } 151 deliverCheck := func(shouldHappen bool) { 152 if shouldHappen { 153 select { 154 case <-deliverer.forceCh: 155 case <-time.After(20 * time.Second): 156 require.Fail(t, "no start") 157 } 158 } else { 159 select { 160 case <-deliverer.forceCh: 161 require.Fail(t, "start not supposed to happen") 162 default: 163 } 164 } 165 } 166 successCheck := func(cb types.AttachmentUploaderResultCb) { 167 ch := cb.Wait() 168 select { 169 case res := <-ch: 170 require.Nil(t, res.Error) 171 require.Equal(t, md, res.Metadata) 172 require.NotNil(t, res.Preview) 173 require.Equal(t, "image/jpeg", res.Preview.MimeType) 174 require.Equal(t, "image/jpeg", res.Object.MimeType) 175 case <-time.After(20 * time.Second): 176 require.Fail(t, "no upload") 177 } 178 } 179 // On non darwin we don't covert the heic. 180 successCheckNoHeicConvert := func(cb types.AttachmentUploaderResultCb) { 181 ch := cb.Wait() 182 select { 183 case res := <-ch: 184 require.Nil(t, res.Error) 185 require.Equal(t, md, res.Metadata) 186 require.Nil(t, res.Preview) 187 require.Equal(t, "image/heif", res.Object.MimeType) 188 case <-time.After(20 * time.Second): 189 require.Fail(t, "no upload") 190 } 191 } 192 193 successCheckEmpty := func(cb types.AttachmentUploaderResultCb) { 194 ch := cb.Wait() 195 select { 196 case res := <-ch: 197 require.Nil(t, res.Error) 198 require.Equal(t, md, res.Metadata) 199 require.Nil(t, res.Preview) 200 require.Equal(t, "", res.Object.MimeType) 201 case <-time.After(20 * time.Second): 202 require.Fail(t, "no upload") 203 } 204 } 205 206 // Basic test to see if it works 207 store.uploadFn = func(context.Context, *UploadTask) (chat1.Asset, error) { 208 return chat1.Asset{}, nil 209 } 210 211 outboxID, err := storage.NewOutboxID() 212 require.NoError(t, err) 213 filename := "../testdata/empty.txt" 214 resChan, err := uploader.Register(context.TODO(), uid, convID, outboxID, "empty", filename, md, nil) 215 require.NoError(t, err) 216 deliverCheck(true) 217 uploadStartCheck(true, outboxID) 218 successCheckEmpty(resChan) 219 220 outboxID, err = storage.NewOutboxID() 221 require.NoError(t, err) 222 filename = "../testdata/mysql.heic" 223 resChan, err = uploader.Register(context.TODO(), uid, convID, outboxID, "mysql", filename, md, nil) 224 require.NoError(t, err) 225 deliverCheck(true) 226 uploadStartCheck(true, outboxID) 227 if runtime.GOOS == "darwin" { 228 successCheck(resChan) 229 } else { 230 successCheckNoHeicConvert(resChan) 231 } 232 233 outboxID, err = storage.NewOutboxID() 234 require.NoError(t, err) 235 filename = "../testdata/ship.jpg" 236 resChan, err = uploader.Register(context.TODO(), uid, convID, outboxID, "ship", filename, md, nil) 237 require.NoError(t, err) 238 deliverCheck(true) 239 uploadStartCheck(true, outboxID) 240 successCheck(resChan) 241 242 // Broken store 243 outboxID, err = storage.NewOutboxID() 244 require.NoError(t, err) 245 store.uploadFn = func(context.Context, *UploadTask) (chat1.Asset, error) { 246 return chat1.Asset{}, errors.New("i dont work") 247 } 248 resChan, err = uploader.Register(context.TODO(), uid, convID, outboxID, "ship", filename, md, nil) 249 require.NoError(t, err) 250 uploadStartCheck(true, outboxID) 251 select { 252 case res := <-resChan.Wait(): 253 require.NotNil(t, res.Error) 254 case <-time.After(20 * time.Second): 255 require.Fail(t, "no upload") 256 } 257 deliverCheck(true) 258 259 // block until the upload is marked as done 260 for count := 0; count <= 5; count++ { 261 uploader.Lock() 262 upload, ok := uploader.uploads[outboxID.String()] 263 uploader.Unlock() 264 if !ok && upload == nil { 265 break 266 } 267 time.Sleep(time.Millisecond * 200) 268 if count == 5 { 269 require.Fail(t, "upload not marked as done") 270 } 271 t.Logf("upload not done, checking again") 272 } 273 t.Logf("upload done") 274 275 // Retry after fixing store 276 store.uploadFn = func(context.Context, *UploadTask) (chat1.Asset, error) { 277 return chat1.Asset{}, nil 278 } 279 resChan, err = uploader.Retry(context.TODO(), outboxID) 280 require.NoError(t, err) 281 uploadStartCheck(true, outboxID) 282 successCheck(resChan) 283 deliverCheck(true) 284 285 // Slow store to test concurrent retry 286 outboxID, err = storage.NewOutboxID() 287 require.NoError(t, err) 288 slowCh := make(chan struct{}) 289 store.uploadFn = func(context.Context, *UploadTask) (chat1.Asset, error) { 290 <-slowCh 291 return chat1.Asset{}, nil 292 } 293 resChan, err = uploader.Register(context.TODO(), uid, convID, outboxID, "ship", filename, md, nil) 294 require.NoError(t, err) 295 uploadStartCheck(true, outboxID) 296 deliverCheck(false) 297 select { 298 case <-resChan.Wait(): 299 require.Fail(t, "no res") 300 default: 301 } 302 retryChan, err := uploader.Retry(context.TODO(), outboxID) 303 require.NoError(t, err) 304 uploadStartCheck(false, outboxID) 305 close(slowCh) 306 deliverCheck(true) 307 // Should get results on both of these 308 successCheck(retryChan) 309 successCheck(resChan) 310 311 uploader.Complete(context.TODO(), outboxID) 312 _, _, err = uploader.Status(context.TODO(), outboxID) 313 require.Error(t, err) 314 315 // Test cancel 316 outboxID, err = storage.NewOutboxID() 317 require.NoError(t, err) 318 slowCh = make(chan struct{}) 319 store.uploadFn = func(ctx context.Context, task *UploadTask) (chat1.Asset, error) { 320 select { 321 case <-slowCh: 322 case <-ctx.Done(): 323 return chat1.Asset{}, ctx.Err() 324 } 325 return chat1.Asset{}, nil 326 } 327 resChan, err = uploader.Register(context.TODO(), uid, convID, outboxID, "ship", filename, md, nil) 328 require.NoError(t, err) 329 uploadStartCheck(true, outboxID) 330 deliverCheck(false) 331 select { 332 case <-resChan.Wait(): 333 require.Fail(t, "no res") 334 default: 335 } 336 require.NoError(t, uploader.Cancel(context.TODO(), outboxID)) 337 _, _, err = uploader.Status(context.TODO(), outboxID) 338 require.Error(t, err) 339 res := <-resChan.Wait() 340 require.NotNil(t, res.Error) 341 342 // verify uploadedPreviewsDir respects the cache size 343 baseDir := uploader.getBaseDir() 344 uploadedPreviews, err := filepath.Glob(filepath.Join(baseDir, uploadedPreviewsDir, "*")) 345 require.NoError(t, err) 346 require.Len(t, uploadedPreviews, 1) 347 348 // verify uploadedFullsDir is respects the cache size 349 uploadedFulls, err := filepath.Glob(filepath.Join(baseDir, uploadedFullsDir, "*")) 350 require.NoError(t, err) 351 require.Len(t, uploadedFulls, 1) 352 mctx := kbtest.NewMetaContextForTest(*tc) 353 354 // verify db nuke 355 _, err = g.LocalDb.Nuke() 356 require.NoError(t, err) 357 err = uploader.OnDbNuke(mctx) 358 require.NoError(t, err) 359 360 uploadedPreviews, err = filepath.Glob(filepath.Join(baseDir, uploadedPreviewsDir, "*")) 361 require.NoError(t, err) 362 require.Zero(t, len(uploadedPreviews)) 363 364 uploadedFulls, err = filepath.Glob(filepath.Join(baseDir, uploadedFullsDir, "*")) 365 require.NoError(t, err) 366 require.Zero(t, len(uploadedFulls)) 367 }