index.test.js 8.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261
  1. import { beforeEach, describe, expect, it, jest } from '@jest/globals'
  2. import 'whatwg-fetch'
  3. import nock from 'nock'
  4. import Core from '@uppy/core'
  5. import AwsS3Multipart from './index.js'
  6. const KB = 1024
  7. const MB = KB * KB
  8. describe('AwsS3Multipart', () => {
  9. beforeEach(() => nock.disableNetConnect())
  10. it('Registers AwsS3Multipart upload plugin', () => {
  11. const core = new Core()
  12. core.use(AwsS3Multipart)
  13. const pluginNames = core[Symbol.for('uppy test: getPlugins')]('uploader').map((plugin) => plugin.constructor.name)
  14. expect(pluginNames).toContain('AwsS3Multipart')
  15. })
  16. describe('companionUrl assertion', () => {
  17. it('Throws an error for main functions if configured without companionUrl', () => {
  18. const core = new Core()
  19. core.use(AwsS3Multipart)
  20. const awsS3Multipart = core.getPlugin('AwsS3Multipart')
  21. const err = 'Expected a `companionUrl` option'
  22. const file = {}
  23. const opts = {}
  24. expect(() => awsS3Multipart.opts.createMultipartUpload(file)).toThrow(
  25. err,
  26. )
  27. expect(() => awsS3Multipart.opts.listParts(file, opts)).toThrow(err)
  28. expect(() => awsS3Multipart.opts.completeMultipartUpload(file, opts)).toThrow(err)
  29. expect(() => awsS3Multipart.opts.abortMultipartUpload(file, opts)).toThrow(err)
  30. expect(() => awsS3Multipart.opts.prepareUploadParts(file, opts)).toThrow(err)
  31. })
  32. })
  33. describe('without companionUrl (custom main functions)', () => {
  34. let core
  35. let awsS3Multipart
  36. beforeEach(() => {
  37. core = new Core()
  38. core.use(AwsS3Multipart, {
  39. createMultipartUpload: jest.fn(() => {
  40. return {
  41. uploadId: '6aeb1980f3fc7ce0b5454d25b71992',
  42. key: 'test/upload/multitest.dat',
  43. }
  44. }),
  45. completeMultipartUpload: jest.fn(async () => ({ location: 'test' })),
  46. abortMultipartUpload: jest.fn(),
  47. prepareUploadParts: jest.fn(async () => {
  48. const presignedUrls = {}
  49. const possiblePartNumbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
  50. possiblePartNumbers.forEach((partNumber) => {
  51. presignedUrls[
  52. partNumber
  53. ] = `https://bucket.s3.us-east-2.amazonaws.com/test/upload/multitest.dat?partNumber=${partNumber}&uploadId=6aeb1980f3fc7ce0b5454d25b71992&X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=AKIATEST%2F20210729%2Fus-east-2%2Fs3%2Faws4_request&X-Amz-Date=20210729T014044Z&X-Amz-Expires=600&X-Amz-SignedHeaders=host&X-Amz-Signature=test`
  54. })
  55. return { presignedUrls }
  56. }),
  57. })
  58. awsS3Multipart = core.getPlugin('AwsS3Multipart')
  59. })
  60. it('Calls the prepareUploadParts function totalChunks / limit times', async () => {
  61. const scope = nock(
  62. 'https://bucket.s3.us-east-2.amazonaws.com',
  63. ).defaultReplyHeaders({
  64. 'access-control-allow-method': 'PUT',
  65. 'access-control-allow-origin': '*',
  66. 'access-control-expose-headers': 'ETag',
  67. })
  68. // 6MB file will give us 2 chunks, so there will be 2 PUT and 2 OPTIONS
  69. // calls to the presigned URL from 1 prepareUploadParts calls
  70. const fileSize = 5 * MB + 1 * MB
  71. scope
  72. .options((uri) => uri.includes('test/upload/multitest.dat'))
  73. .reply(200, '')
  74. scope
  75. .options((uri) => uri.includes('test/upload/multitest.dat'))
  76. .reply(200, '')
  77. scope
  78. .put((uri) => uri.includes('test/upload/multitest.dat'))
  79. .reply(200, '', { ETag: 'test1' })
  80. scope
  81. .put((uri) => uri.includes('test/upload/multitest.dat'))
  82. .reply(200, '', { ETag: 'test2' })
  83. core.addFile({
  84. source: 'jest',
  85. name: 'multitest.dat',
  86. type: 'application/octet-stream',
  87. data: new File([new Uint8Array(fileSize)], {
  88. type: 'application/octet-stream',
  89. }),
  90. })
  91. await core.upload()
  92. expect(
  93. awsS3Multipart.opts.prepareUploadParts.mock.calls.length,
  94. ).toEqual(1)
  95. scope.done()
  96. })
  97. it('Calls prepareUploadParts with a Math.ceil(limit / 2) minimum, instead of one at a time for the remaining chunks after the first limit batch', async () => {
  98. const scope = nock(
  99. 'https://bucket.s3.us-east-2.amazonaws.com',
  100. ).defaultReplyHeaders({
  101. 'access-control-allow-method': 'PUT',
  102. 'access-control-allow-origin': '*',
  103. 'access-control-expose-headers': 'ETag',
  104. })
  105. // 50MB file will give us 10 chunks, so there will be 10 PUT and 10 OPTIONS
  106. // calls to the presigned URL from 3 prepareUploadParts calls
  107. //
  108. // The first prepareUploadParts call will be for 5 parts, the second
  109. // will be for 3 parts, the third will be for 2 parts.
  110. const fileSize = 50 * MB
  111. scope
  112. .options((uri) => uri.includes('test/upload/multitest.dat'))
  113. .reply(200, '')
  114. scope
  115. .put((uri) => uri.includes('test/upload/multitest.dat'))
  116. .reply(200, '', { ETag: 'test' })
  117. scope.persist()
  118. core.addFile({
  119. source: 'jest',
  120. name: 'multitest.dat',
  121. type: 'application/octet-stream',
  122. data: new File([new Uint8Array(fileSize)], {
  123. type: 'application/octet-stream',
  124. }),
  125. })
  126. await core.upload()
  127. function validatePartData ({ partNumbers, chunks }, expected) {
  128. expect(partNumbers).toEqual(expected)
  129. partNumbers.forEach(partNumber => {
  130. expect(chunks[partNumber]).toBeDefined()
  131. })
  132. }
  133. expect(awsS3Multipart.opts.prepareUploadParts.mock.calls.length).toEqual(3)
  134. validatePartData(awsS3Multipart.opts.prepareUploadParts.mock.calls[0][1], [1, 2, 3, 4, 5])
  135. validatePartData(awsS3Multipart.opts.prepareUploadParts.mock.calls[1][1], [6, 7, 8])
  136. validatePartData(awsS3Multipart.opts.prepareUploadParts.mock.calls[2][1], [9, 10])
  137. const completeCall = awsS3Multipart.opts.completeMultipartUpload.mock.calls[0][1]
  138. expect(completeCall.parts).toEqual([
  139. { ETag: 'test', PartNumber: 1 },
  140. { ETag: 'test', PartNumber: 2 },
  141. { ETag: 'test', PartNumber: 3 },
  142. { ETag: 'test', PartNumber: 4 },
  143. { ETag: 'test', PartNumber: 5 },
  144. { ETag: 'test', PartNumber: 6 },
  145. { ETag: 'test', PartNumber: 7 },
  146. { ETag: 'test', PartNumber: 8 },
  147. { ETag: 'test', PartNumber: 9 },
  148. { ETag: 'test', PartNumber: 10 },
  149. ])
  150. })
  151. })
  152. describe('MultipartUploader', () => {
  153. let core
  154. let awsS3Multipart
  155. beforeEach(() => {
  156. core = new Core()
  157. core.use(AwsS3Multipart, {
  158. createMultipartUpload: jest.fn(() => {
  159. return {
  160. uploadId: '6aeb1980f3fc7ce0b5454d25b71992',
  161. key: 'test/upload/multitest.dat',
  162. }
  163. }),
  164. completeMultipartUpload: jest.fn(async () => ({ location: 'test' })),
  165. abortMultipartUpload: jest.fn(),
  166. prepareUploadParts: jest
  167. .fn(async () => {
  168. const presignedUrls = {}
  169. const possiblePartNumbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
  170. possiblePartNumbers.forEach((partNumber) => {
  171. presignedUrls[
  172. partNumber
  173. ] = `https://bucket.s3.us-east-2.amazonaws.com/test/upload/multitest.dat?partNumber=${partNumber}&uploadId=6aeb1980f3fc7ce0b5454d25b71992&X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=AKIATEST%2F20210729%2Fus-east-2%2Fs3%2Faws4_request&X-Amz-Date=20210729T014044Z&X-Amz-Expires=600&X-Amz-SignedHeaders=host&X-Amz-Signature=test`
  174. })
  175. return { presignedUrls }
  176. })
  177. // This runs first and only once
  178. // eslint-disable-next-line prefer-promise-reject-errors
  179. .mockImplementationOnce(() => Promise.reject({ source: { status: 500 } })),
  180. })
  181. awsS3Multipart = core.getPlugin('AwsS3Multipart')
  182. })
  183. it('retries prepareUploadParts when it fails once', async () => {
  184. const fileSize = 5 * MB + 1 * MB
  185. core.addFile({
  186. source: 'jest',
  187. name: 'multitest.dat',
  188. type: 'application/octet-stream',
  189. data: new File([new Uint8Array(fileSize)], {
  190. type: 'application/octet-stream',
  191. }),
  192. })
  193. await core.upload()
  194. expect(awsS3Multipart.opts.prepareUploadParts.mock.calls.length).toEqual(2)
  195. })
  196. })
  197. describe('dynamic companionHeader', () => {
  198. let core
  199. let awsS3Multipart
  200. const oldToken = 'old token'
  201. const newToken = 'new token'
  202. beforeEach(() => {
  203. core = new Core()
  204. core.use(AwsS3Multipart, {
  205. companionHeaders: {
  206. authorization: oldToken,
  207. },
  208. })
  209. awsS3Multipart = core.getPlugin('AwsS3Multipart')
  210. })
  211. it('companionHeader is updated before uploading file', async () => {
  212. awsS3Multipart.setOptions({
  213. companionHeaders: {
  214. authorization: newToken,
  215. },
  216. })
  217. await core.upload()
  218. const client = awsS3Multipart[Symbol.for('uppy test: getClient')]()
  219. expect(client[Symbol.for('uppy test: getCompanionHeaders')]().authorization).toEqual(newToken)
  220. })
  221. })
  222. })