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

     1  package chat
     2  
     3  import (
     4  	"bytes"
     5  	"context"
     6  	"errors"
     7  	"io"
     8  	"net/http"
     9  	"os"
    10  	"path/filepath"
    11  	"testing"
    12  	"time"
    13  
    14  	"github.com/keybase/client/go/kbhttp/manager"
    15  
    16  	"github.com/keybase/client/go/chat/attachments"
    17  	"github.com/keybase/client/go/chat/utils"
    18  	"github.com/keybase/client/go/libkb"
    19  
    20  	"github.com/keybase/client/go/chat/s3"
    21  	"github.com/keybase/client/go/chat/types"
    22  	"github.com/keybase/client/go/protocol/chat1"
    23  	"github.com/keybase/client/go/protocol/gregor1"
    24  	"github.com/keybase/client/go/protocol/keybase1"
    25  	"github.com/stretchr/testify/require"
    26  )
    27  
    28  type nopCloser struct {
    29  	io.Reader
    30  }
    31  
    32  func (nopCloser) Close() error { return nil }
    33  
    34  type mockAttachmentRemoteStore struct {
    35  	decryptCh     chan struct{}
    36  	assetReaderCh chan struct{}
    37  }
    38  
    39  func (m mockAttachmentRemoteStore) DecryptAsset(ctx context.Context, w io.Writer, body io.Reader,
    40  	asset chat1.Asset, progress types.ProgressReporter) error {
    41  	if m.decryptCh != nil {
    42  		m.decryptCh <- struct{}{}
    43  	}
    44  	_, err := io.Copy(w, body)
    45  	return err
    46  }
    47  
    48  func (m mockAttachmentRemoteStore) DeleteAssets(ctx context.Context, params chat1.S3Params, signer s3.Signer,
    49  	assets []chat1.Asset) error {
    50  	return nil
    51  }
    52  
    53  func (m mockAttachmentRemoteStore) DeleteAsset(ctx context.Context, params chat1.S3Params, signer s3.Signer,
    54  	asset chat1.Asset) error {
    55  	return nil
    56  }
    57  
    58  func (m mockAttachmentRemoteStore) DownloadAsset(ctx context.Context, params chat1.S3Params,
    59  	asset chat1.Asset, w io.Writer, signer s3.Signer, progress types.ProgressReporter) error {
    60  	return errors.New("not implemented")
    61  }
    62  
    63  func (m mockAttachmentRemoteStore) UploadAsset(ctx context.Context, task *attachments.UploadTask,
    64  	encryptedOut io.Writer) (chat1.Asset, error) {
    65  	return chat1.Asset{}, errors.New("not implemented")
    66  }
    67  
    68  func (m mockAttachmentRemoteStore) StreamAsset(ctx context.Context, params chat1.S3Params, asset chat1.Asset,
    69  	signer s3.Signer) (io.ReadSeeker, error) {
    70  	return nil, errors.New("not implemented")
    71  }
    72  
    73  func (m mockAttachmentRemoteStore) GetAssetReader(ctx context.Context, params chat1.S3Params, asset chat1.Asset,
    74  	signer s3.Signer) (io.ReadCloser, error) {
    75  	if m.assetReaderCh != nil {
    76  		m.assetReaderCh <- struct{}{}
    77  	}
    78  	return nopCloser{Reader: bytes.NewBufferString("HI")}, nil
    79  }
    80  
    81  type mockSigningRemote struct {
    82  	chat1.RemoteInterface
    83  }
    84  
    85  func (m mockSigningRemote) Sign(payload []byte) ([]byte, error) {
    86  	return nil, nil
    87  }
    88  
    89  func (m mockSigningRemote) GetS3Params(ctx context.Context, convID chat1.ConversationID) (res chat1.S3Params, err error) {
    90  	return res, nil
    91  }
    92  
    93  func TestChatSrvAttachmentHTTPSrv(t *testing.T) {
    94  	ctc := makeChatTestContext(t, "TestChatSrvAttachmentHTTPSrv", 1)
    95  	defer ctc.cleanup()
    96  	users := ctc.users()
    97  
    98  	defer func() {
    99  		useRemoteMock = true
   100  	}()
   101  	useRemoteMock = false
   102  	tc := ctc.world.Tcs[users[0].Username]
   103  	decryptCh := make(chan struct{}, 10)
   104  	assetReaderCh := make(chan struct{}, 10)
   105  	store := mockAttachmentRemoteStore{
   106  		decryptCh:     decryptCh,
   107  		assetReaderCh: assetReaderCh,
   108  	}
   109  	fetcher := NewCachingAttachmentFetcher(tc.Context(), store, 1)
   110  	d, err := libkb.RandHexString("", 8)
   111  	require.NoError(t, err)
   112  	fetcher.tempDir = filepath.Join(os.TempDir(), d)
   113  
   114  	uid := gregor1.UID(users[0].GetUID().ToBytes())
   115  	conv := mustCreateConversationForTest(t, ctc, users[0], chat1.TopicType_CHAT,
   116  		chat1.ConversationMembersType_IMPTEAMNATIVE)
   117  	tc.ChatG.AttachmentURLSrv = NewAttachmentHTTPSrv(tc.Context(),
   118  		manager.NewSrv(tc.Context().ExternalG()), fetcher,
   119  		func() chat1.RemoteInterface { return mockSigningRemote{} })
   120  
   121  	_, err = postLocalForTest(t, ctc, users[0], conv, chat1.NewMessageBodyWithAttachment(chat1.MessageAttachment{
   122  		Object: chat1.Asset{
   123  			Path: "m0",
   124  		},
   125  	}))
   126  	require.NoError(t, err)
   127  	_, err = postLocalForTest(t, ctc, users[0], conv, chat1.NewMessageBodyWithAttachment(chat1.MessageAttachment{
   128  		Object: chat1.Asset{
   129  			Path: "m1",
   130  		},
   131  	}))
   132  	require.NoError(t, err)
   133  
   134  	tv, err := tc.Context().ConvSource.Pull(context.TODO(), conv.Id, uid,
   135  		chat1.GetThreadReason_GENERAL, nil,
   136  		&chat1.GetThreadQuery{
   137  			MessageTypes: []chat1.MessageType{chat1.MessageType_ATTACHMENT},
   138  		}, nil)
   139  	require.NoError(t, err)
   140  	require.Equal(t, 2, len(tv.Messages))
   141  
   142  	uiMsg := utils.PresentMessageUnboxed(context.TODO(), tc.Context(), tv.Messages[0], uid, conv.Id)
   143  	require.NotNil(t, uiMsg.Valid().AssetUrlInfo)
   144  	uiMsg2 := utils.PresentMessageUnboxed(context.TODO(), tc.Context(), tv.Messages[1], uid, conv.Id)
   145  	require.NotNil(t, uiMsg.Valid().AssetUrlInfo)
   146  
   147  	waitTime := 20 * time.Second
   148  	readAsset := func(msg chat1.UIMessage, cacheHit bool) {
   149  		httpRes, err := http.Get(msg.Valid().AssetUrlInfo.FullUrl)
   150  		require.NoError(t, err)
   151  		body, err := io.ReadAll(httpRes.Body)
   152  		require.NoError(t, err)
   153  		require.Equal(t, "HI", string(body))
   154  		if cacheHit {
   155  			select {
   156  			case <-assetReaderCh:
   157  				require.Fail(t, "should have hit cache")
   158  			default:
   159  			}
   160  		} else {
   161  			select {
   162  			case <-assetReaderCh:
   163  			case <-time.After(waitTime):
   164  				require.Fail(t, "should have read")
   165  			}
   166  		}
   167  		select {
   168  		case <-decryptCh:
   169  		case <-time.After(waitTime):
   170  			require.Fail(t, "should have decrypted")
   171  		}
   172  	}
   173  	readAsset(uiMsg, false)
   174  	readAsset(uiMsg, true)
   175  	readAsset(uiMsg2, false)
   176  	readAsset(uiMsg2, true)
   177  	readAsset(uiMsg, false) // make sure it got evicted
   178  	readAsset(uiMsg, true)
   179  
   180  	require.NoError(t, os.RemoveAll(fetcher.tempDir))
   181  	readAsset(uiMsg, false)
   182  	readAsset(uiMsg, true)
   183  
   184  	assets := utils.AssetsForMessage(tc.Context(), tv.Messages[0].Valid().MessageBody)
   185  	require.Len(t, assets, 1)
   186  
   187  	found, localPath, err := fetcher.localAssetPath(context.TODO(), assets[0])
   188  	require.NoError(t, err)
   189  	require.True(t, found)
   190  	_, err = os.Stat(localPath)
   191  	require.False(t, os.IsNotExist(err))
   192  
   193  	err = fetcher.DeleteAssets(context.TODO(), conv.Id, assets,
   194  		func() chat1.RemoteInterface { return mockSigningRemote{} }, mockSigningRemote{})
   195  	require.NoError(t, err)
   196  
   197  	// make sure we have purged the attachment from disk as well
   198  	_, err = os.Stat(localPath)
   199  	require.True(t, os.IsNotExist(err))
   200  }
   201  
   202  func TestChatSrvAttachmentUploadPreviewCached(t *testing.T) {
   203  	ctc := makeChatTestContext(t, "TestChatSrvAttachmentUploadPreviewCached", 1)
   204  	defer ctc.cleanup()
   205  	users := ctc.users()
   206  
   207  	defer func() {
   208  		useRemoteMock = true
   209  	}()
   210  	useRemoteMock = false
   211  	tc := ctc.world.Tcs[users[0].Username]
   212  	store := attachments.NewStoreTesting(tc.Context(), nil)
   213  	fetcher := NewCachingAttachmentFetcher(tc.Context(), store, 5)
   214  	ri := ctc.as(t, users[0]).ri
   215  	d, err := libkb.RandHexString("", 8)
   216  	require.NoError(t, err)
   217  	fetcher.tempDir = filepath.Join(os.TempDir(), d)
   218  
   219  	conv := mustCreateConversationForTest(t, ctc, users[0], chat1.TopicType_CHAT,
   220  		chat1.ConversationMembersType_IMPTEAMNATIVE)
   221  	tc.ChatG.AttachmentURLSrv = NewAttachmentHTTPSrv(tc.Context(),
   222  		manager.NewSrv(tc.Context().ExternalG()),
   223  		fetcher, func() chat1.RemoteInterface { return mockSigningRemote{} })
   224  	uploader := attachments.NewUploader(tc.Context(), store, mockSigningRemote{},
   225  		func() chat1.RemoteInterface { return ri }, 1)
   226  	uploader.SetPreviewTempDir(fetcher.tempDir)
   227  	tc.ChatG.AttachmentUploader = uploader
   228  
   229  	res, err := ctc.as(t, users[0]).chatLocalHandler().PostFileAttachmentLocal(context.TODO(),
   230  		chat1.PostFileAttachmentLocalArg{
   231  			Arg: chat1.PostFileAttachmentArg{
   232  				ConversationID: conv.Id,
   233  				TlfName:        conv.TlfName,
   234  				Visibility:     keybase1.TLFVisibility_PRIVATE,
   235  				Filename:       "testdata/ship.jpg",
   236  				Title:          "SHIP",
   237  			},
   238  		})
   239  	require.NoError(t, err)
   240  
   241  	msgRes, err := ctc.as(t, users[0]).chatLocalHandler().GetMessagesLocal(context.TODO(),
   242  		chat1.GetMessagesLocalArg{
   243  			ConversationID: conv.Id,
   244  			MessageIDs:     []chat1.MessageID{res.MessageID},
   245  		})
   246  	require.NoError(t, err)
   247  	require.Equal(t, 1, len(msgRes.Messages))
   248  	require.True(t, msgRes.Messages[0].IsValid())
   249  	body := msgRes.Messages[0].Valid().MessageBody
   250  	require.NotNil(t, body.Attachment().Preview)
   251  
   252  	t.Logf("remote preview path: %s", body.Attachment().Preview.Path)
   253  	t.Logf("remote object path: %s", body.Attachment().Object.Path)
   254  	found, path, err := fetcher.localAssetPath(context.TODO(), *body.Attachment().Preview)
   255  	require.NoError(t, err)
   256  	require.True(t, found)
   257  	_, err = os.Stat(path)
   258  	require.NoError(t, err)
   259  
   260  	t.Logf("found path: %s", path)
   261  
   262  	found, path, err = fetcher.localAssetPath(context.TODO(), body.Attachment().Object)
   263  	require.NoError(t, err)
   264  	require.True(t, found)
   265  	_, err = os.Stat(path)
   266  	require.NoError(t, err)
   267  	t.Logf("found path: %s", path)
   268  
   269  	// Try with an attachment with no preview
   270  	res, err = ctc.as(t, users[0]).chatLocalHandler().PostFileAttachmentLocal(context.TODO(),
   271  		chat1.PostFileAttachmentLocalArg{
   272  			Arg: chat1.PostFileAttachmentArg{
   273  				ConversationID: conv.Id,
   274  				TlfName:        conv.TlfName,
   275  				Visibility:     keybase1.TLFVisibility_PRIVATE,
   276  				Filename:       "testdata/weather.pdf",
   277  				Title:          "WEATHER",
   278  			},
   279  		})
   280  	require.NoError(t, err)
   281  	msgRes, err = ctc.as(t, users[0]).chatLocalHandler().GetMessagesLocal(context.TODO(),
   282  		chat1.GetMessagesLocalArg{
   283  			ConversationID: conv.Id,
   284  			MessageIDs:     []chat1.MessageID{res.MessageID},
   285  		})
   286  	require.NoError(t, err)
   287  	require.Equal(t, 1, len(msgRes.Messages))
   288  	require.True(t, msgRes.Messages[0].IsValid())
   289  	body = msgRes.Messages[0].Valid().MessageBody
   290  	require.Nil(t, body.Attachment().Preview)
   291  	found, _, err = fetcher.localAssetPath(context.TODO(), body.Attachment().Object)
   292  	require.NoError(t, err)
   293  	require.False(t, found)
   294  	// No preview is available, but the file is still on disk.
   295  	_, err = os.Stat(path)
   296  	require.NoError(t, err)
   297  }