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  }