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 }