S3PersistorTests.js 26 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952
  1. const sinon = require('sinon')
  2. const chai = require('chai')
  3. const { expect } = chai
  4. const modulePath = '../../src/S3Persistor.js'
  5. const SandboxedModule = require('sandboxed-module')
  6. const Errors = require('../../src/Errors')
  7. describe('S3PersistorTests', function () {
  8. const defaultS3Key = 'frog'
  9. const defaultS3Secret = 'prince'
  10. const defaultS3Credentials = {
  11. credentials: {
  12. accessKeyId: defaultS3Key,
  13. secretAccessKey: defaultS3Secret
  14. }
  15. }
  16. const filename = '/wombat/potato.tex'
  17. const bucket = 'womBucket'
  18. const key = 'monKey'
  19. const destKey = 'donKey'
  20. const objectSize = 5555
  21. const genericError = new Error('guru meditation error')
  22. const files = [
  23. { Key: 'llama', Size: 11 },
  24. { Key: 'hippo', Size: 22 }
  25. ]
  26. const filesSize = 33
  27. const md5 = 'ffffffff00000000ffffffff00000000'
  28. const redirectUrl = 'https://wombat.potato/giraffe'
  29. let Logger,
  30. Transform,
  31. S3,
  32. Fs,
  33. ReadStream,
  34. Stream,
  35. S3Persistor,
  36. S3Client,
  37. S3ReadStream,
  38. S3NotFoundError,
  39. S3AccessDeniedError,
  40. FileNotFoundError,
  41. EmptyPromise,
  42. settings,
  43. Hash,
  44. crypto
  45. beforeEach(function () {
  46. settings = {
  47. secret: defaultS3Secret,
  48. key: defaultS3Key,
  49. partSize: 100 * 1024 * 1024
  50. }
  51. Transform = class {
  52. on(event, callback) {
  53. if (event === 'readable') {
  54. callback()
  55. }
  56. }
  57. once() {}
  58. removeListener() {}
  59. }
  60. Stream = {
  61. pipeline: sinon.stub().yields(),
  62. Transform: Transform
  63. }
  64. EmptyPromise = {
  65. promise: sinon.stub().resolves()
  66. }
  67. ReadStream = {
  68. pipe: sinon.stub().returns('readStream'),
  69. on: sinon.stub(),
  70. removeListener: sinon.stub()
  71. }
  72. ReadStream.on.withArgs('end').yields()
  73. ReadStream.on.withArgs('pipe').yields({
  74. unpipe: sinon.stub(),
  75. resume: sinon.stub()
  76. })
  77. FileNotFoundError = new Error('File not found')
  78. FileNotFoundError.code = 'ENOENT'
  79. Fs = {
  80. createReadStream: sinon.stub().returns(ReadStream)
  81. }
  82. S3NotFoundError = new Error('not found')
  83. S3NotFoundError.code = 'NoSuchKey'
  84. S3AccessDeniedError = new Error('access denied')
  85. S3AccessDeniedError.code = 'AccessDenied'
  86. S3ReadStream = {
  87. on: sinon.stub(),
  88. pipe: sinon.stub(),
  89. removeListener: sinon.stub()
  90. }
  91. S3ReadStream.on.withArgs('end').yields()
  92. S3ReadStream.on.withArgs('pipe').yields({
  93. unpipe: sinon.stub(),
  94. resume: sinon.stub()
  95. })
  96. S3Client = {
  97. getObject: sinon.stub().returns({
  98. createReadStream: sinon.stub().returns(S3ReadStream)
  99. }),
  100. headObject: sinon.stub().returns({
  101. promise: sinon.stub().resolves({
  102. ContentLength: objectSize,
  103. ETag: md5
  104. })
  105. }),
  106. listObjectsV2: sinon.stub().returns({
  107. promise: sinon.stub().resolves({
  108. Contents: files
  109. })
  110. }),
  111. upload: sinon
  112. .stub()
  113. .returns({ promise: sinon.stub().resolves({ ETag: `"${md5}"` }) }),
  114. copyObject: sinon.stub().returns(EmptyPromise),
  115. deleteObject: sinon.stub().returns(EmptyPromise),
  116. deleteObjects: sinon.stub().returns(EmptyPromise),
  117. getSignedUrlPromise: sinon.stub().resolves(redirectUrl)
  118. }
  119. S3 = sinon.stub().returns(S3Client)
  120. Hash = {
  121. end: sinon.stub(),
  122. read: sinon.stub().returns(md5),
  123. setEncoding: sinon.stub()
  124. }
  125. crypto = {
  126. createHash: sinon.stub().returns(Hash)
  127. }
  128. Logger = {
  129. warn: sinon.stub()
  130. }
  131. S3Persistor = new (SandboxedModule.require(modulePath, {
  132. requires: {
  133. 'aws-sdk/clients/s3': S3,
  134. 'logger-sharelatex': Logger,
  135. './Errors': Errors,
  136. fs: Fs,
  137. stream: Stream,
  138. crypto
  139. },
  140. globals: { console, Buffer }
  141. }))(settings)
  142. })
  143. describe('getObjectStream', function () {
  144. describe('when called with valid parameters', function () {
  145. let stream
  146. beforeEach(async function () {
  147. stream = await S3Persistor.getObjectStream(bucket, key)
  148. })
  149. it('returns a metered stream', function () {
  150. expect(stream).to.be.instanceOf(Transform)
  151. })
  152. it('sets the AWS client up with credentials from settings', function () {
  153. expect(S3).to.have.been.calledWith(defaultS3Credentials)
  154. })
  155. it('fetches the right key from the right bucket', function () {
  156. expect(S3Client.getObject).to.have.been.calledWith({
  157. Bucket: bucket,
  158. Key: key
  159. })
  160. })
  161. it('pipes the stream through the meter', async function () {
  162. expect(S3ReadStream.pipe).to.have.been.calledWith(
  163. sinon.match.instanceOf(Transform)
  164. )
  165. })
  166. })
  167. describe('when called with a byte range', function () {
  168. let stream
  169. beforeEach(async function () {
  170. stream = await S3Persistor.getObjectStream(bucket, key, {
  171. start: 5,
  172. end: 10
  173. })
  174. })
  175. it('returns a metered stream', function () {
  176. expect(stream).to.be.instanceOf(Stream.Transform)
  177. })
  178. it('passes the byte range on to S3', function () {
  179. expect(S3Client.getObject).to.have.been.calledWith({
  180. Bucket: bucket,
  181. Key: key,
  182. Range: 'bytes=5-10'
  183. })
  184. })
  185. })
  186. describe('when there are alternative credentials', function () {
  187. let stream
  188. const alternativeSecret = 'giraffe'
  189. const alternativeKey = 'hippo'
  190. const alternativeS3Credentials = {
  191. credentials: {
  192. accessKeyId: alternativeKey,
  193. secretAccessKey: alternativeSecret
  194. }
  195. }
  196. beforeEach(async function () {
  197. settings.bucketCreds = {}
  198. settings.bucketCreds[bucket] = {
  199. auth_key: alternativeKey,
  200. auth_secret: alternativeSecret
  201. }
  202. stream = await S3Persistor.getObjectStream(bucket, key)
  203. })
  204. it('returns a metered stream', function () {
  205. expect(stream).to.be.instanceOf(Stream.Transform)
  206. })
  207. it('sets the AWS client up with the alternative credentials', function () {
  208. expect(S3).to.have.been.calledWith(alternativeS3Credentials)
  209. })
  210. it('fetches the right key from the right bucket', function () {
  211. expect(S3Client.getObject).to.have.been.calledWith({
  212. Bucket: bucket,
  213. Key: key
  214. })
  215. })
  216. it('uses the default credentials for an unknown bucket', async function () {
  217. stream = await S3Persistor.getObjectStream('anotherBucket', key)
  218. expect(S3).to.have.been.calledTwice
  219. expect(S3.firstCall).to.have.been.calledWith(alternativeS3Credentials)
  220. expect(S3.secondCall).to.have.been.calledWith(defaultS3Credentials)
  221. })
  222. it('throws an error if there are no credentials for the bucket', async function () {
  223. delete settings.key
  224. delete settings.secret
  225. await expect(
  226. S3Persistor.getObjectStream('anotherBucket', key)
  227. ).to.eventually.be.rejected.and.be.an.instanceOf(Errors.SettingsError)
  228. })
  229. })
  230. describe("when the file doesn't exist", function () {
  231. let error, stream
  232. beforeEach(async function () {
  233. Transform.prototype.on = sinon.stub()
  234. S3ReadStream.on.withArgs('error').yields(S3NotFoundError)
  235. try {
  236. stream = await S3Persistor.getObjectStream(bucket, key)
  237. } catch (err) {
  238. error = err
  239. }
  240. })
  241. it('does not return a stream', function () {
  242. expect(stream).not.to.exist
  243. })
  244. it('throws a NotFoundError', function () {
  245. expect(error).to.be.an.instanceOf(Errors.NotFoundError)
  246. })
  247. it('wraps the error', function () {
  248. expect(error.cause).to.exist
  249. })
  250. it('stores the bucket and key in the error', function () {
  251. expect(error.info).to.include({ bucketName: bucket, key: key })
  252. })
  253. })
  254. describe('when access to the file is denied', function () {
  255. let error, stream
  256. beforeEach(async function () {
  257. Transform.prototype.on = sinon.stub()
  258. S3ReadStream.on.withArgs('error').yields(S3AccessDeniedError)
  259. try {
  260. stream = await S3Persistor.getObjectStream(bucket, key)
  261. } catch (err) {
  262. error = err
  263. }
  264. })
  265. it('does not return a stream', function () {
  266. expect(stream).not.to.exist
  267. })
  268. it('throws a NotFoundError', function () {
  269. expect(error).to.be.an.instanceOf(Errors.NotFoundError)
  270. })
  271. it('wraps the error', function () {
  272. expect(error.cause).to.exist
  273. })
  274. it('stores the bucket and key in the error', function () {
  275. expect(error.info).to.include({ bucketName: bucket, key: key })
  276. })
  277. })
  278. describe('when S3 encounters an unkown error', function () {
  279. let error, stream
  280. beforeEach(async function () {
  281. Transform.prototype.on = sinon.stub()
  282. S3ReadStream.on.withArgs('error').yields(genericError)
  283. try {
  284. stream = await S3Persistor.getObjectStream(bucket, key)
  285. } catch (err) {
  286. error = err
  287. }
  288. })
  289. it('does not return a stream', function () {
  290. expect(stream).not.to.exist
  291. })
  292. it('throws a ReadError', function () {
  293. expect(error).to.be.an.instanceOf(Errors.ReadError)
  294. })
  295. it('wraps the error', function () {
  296. expect(error.cause).to.exist
  297. })
  298. it('stores the bucket and key in the error', function () {
  299. expect(error.info).to.include({ bucketName: bucket, key: key })
  300. })
  301. })
  302. })
  303. describe('getRedirectUrl', function () {
  304. let signedUrl
  305. beforeEach(async function () {
  306. signedUrl = await S3Persistor.getRedirectUrl(bucket, key)
  307. })
  308. it('should request a signed URL', function () {
  309. expect(S3Client.getSignedUrlPromise).to.have.been.called
  310. })
  311. it('should return the url', function () {
  312. expect(signedUrl).to.equal(redirectUrl)
  313. })
  314. })
  315. describe('getObjectSize', function () {
  316. describe('when called with valid parameters', function () {
  317. let size
  318. beforeEach(async function () {
  319. size = await S3Persistor.getObjectSize(bucket, key)
  320. })
  321. it('should return the object size', function () {
  322. expect(size).to.equal(objectSize)
  323. })
  324. it('should pass the bucket and key to S3', function () {
  325. expect(S3Client.headObject).to.have.been.calledWith({
  326. Bucket: bucket,
  327. Key: key
  328. })
  329. })
  330. })
  331. describe('when the object is not found', function () {
  332. let error
  333. beforeEach(async function () {
  334. S3Client.headObject = sinon.stub().returns({
  335. promise: sinon.stub().rejects(S3NotFoundError)
  336. })
  337. try {
  338. await S3Persistor.getObjectSize(bucket, key)
  339. } catch (err) {
  340. error = err
  341. }
  342. })
  343. it('should return a NotFoundError', function () {
  344. expect(error).to.be.an.instanceOf(Errors.NotFoundError)
  345. })
  346. it('should wrap the error', function () {
  347. expect(error.cause).to.equal(S3NotFoundError)
  348. })
  349. })
  350. describe('when S3 returns an error', function () {
  351. let error
  352. beforeEach(async function () {
  353. S3Client.headObject = sinon.stub().returns({
  354. promise: sinon.stub().rejects(genericError)
  355. })
  356. try {
  357. await S3Persistor.getObjectSize(bucket, key)
  358. } catch (err) {
  359. error = err
  360. }
  361. })
  362. it('should return a ReadError', function () {
  363. expect(error).to.be.an.instanceOf(Errors.ReadError)
  364. })
  365. it('should wrap the error', function () {
  366. expect(error.cause).to.equal(genericError)
  367. })
  368. })
  369. })
  370. describe('sendStream', function () {
  371. describe('with valid parameters', function () {
  372. beforeEach(async function () {
  373. return S3Persistor.sendStream(bucket, key, ReadStream)
  374. })
  375. it('should upload the stream', function () {
  376. expect(S3Client.upload).to.have.been.calledWith({
  377. Bucket: bucket,
  378. Key: key,
  379. Body: sinon.match.instanceOf(Stream.Transform)
  380. })
  381. })
  382. it('should upload files in a single part', function () {
  383. expect(S3Client.upload).to.have.been.calledWith(sinon.match.any, {
  384. partSize: 100 * 1024 * 1024
  385. })
  386. })
  387. it('should meter the stream', function () {
  388. expect(ReadStream.pipe).to.have.been.calledWith(
  389. sinon.match.instanceOf(Stream.Transform)
  390. )
  391. })
  392. })
  393. describe('when a hash is supplied', function () {
  394. beforeEach(async function () {
  395. return S3Persistor.sendStream(bucket, key, ReadStream, {
  396. sourceMd5: 'aaaaaaaabbbbbbbbaaaaaaaabbbbbbbb'
  397. })
  398. })
  399. it('sends the hash in base64', function () {
  400. expect(S3Client.upload).to.have.been.calledWith({
  401. Bucket: bucket,
  402. Key: key,
  403. Body: sinon.match.instanceOf(Transform),
  404. ContentMD5: 'qqqqqru7u7uqqqqqu7u7uw=='
  405. })
  406. })
  407. })
  408. describe('when metadata is supplied', function () {
  409. const contentType = 'text/csv'
  410. const contentEncoding = 'gzip'
  411. beforeEach(async function () {
  412. return S3Persistor.sendStream(bucket, key, ReadStream, {
  413. contentType,
  414. contentEncoding
  415. })
  416. })
  417. it('sends the metadata to S3', function () {
  418. expect(S3Client.upload).to.have.been.calledWith({
  419. Bucket: bucket,
  420. Key: key,
  421. Body: sinon.match.instanceOf(Transform),
  422. ContentType: contentType,
  423. ContentEncoding: contentEncoding
  424. })
  425. })
  426. })
  427. describe('when the upload fails', function () {
  428. let error
  429. beforeEach(async function () {
  430. S3Client.upload = sinon.stub().returns({
  431. promise: sinon.stub().rejects(genericError)
  432. })
  433. try {
  434. await S3Persistor.sendStream(bucket, key, ReadStream)
  435. } catch (err) {
  436. error = err
  437. }
  438. })
  439. it('throws a WriteError', function () {
  440. expect(error).to.be.an.instanceOf(Errors.WriteError)
  441. })
  442. })
  443. })
  444. describe('sendFile', function () {
  445. describe('with valid parameters', function () {
  446. beforeEach(async function () {
  447. return S3Persistor.sendFile(bucket, key, filename)
  448. })
  449. it('should create a read stream for the file', function () {
  450. expect(Fs.createReadStream).to.have.been.calledWith(filename)
  451. })
  452. it('should upload the stream', function () {
  453. expect(S3Client.upload).to.have.been.calledWith({
  454. Bucket: bucket,
  455. Key: key,
  456. Body: sinon.match.instanceOf(Transform)
  457. })
  458. })
  459. })
  460. })
  461. describe('getObjectMd5Hash', function () {
  462. describe('when the etag is a valid md5 hash', function () {
  463. let hash
  464. beforeEach(async function () {
  465. hash = await S3Persistor.getObjectMd5Hash(bucket, key)
  466. })
  467. it('should return the object hash', function () {
  468. expect(hash).to.equal(md5)
  469. })
  470. it('should get the hash from the object metadata', function () {
  471. expect(S3Client.headObject).to.have.been.calledWith({
  472. Bucket: bucket,
  473. Key: key
  474. })
  475. })
  476. it('should not download the object', function () {
  477. expect(S3Client.getObject).not.to.have.been.called
  478. })
  479. })
  480. describe("when the etag isn't a valid md5 hash", function () {
  481. let hash
  482. beforeEach(async function () {
  483. S3Client.headObject = sinon.stub().returns({
  484. promise: sinon.stub().resolves({
  485. ETag: 'somethingthatisntanmd5',
  486. Bucket: bucket,
  487. Key: key
  488. })
  489. })
  490. hash = await S3Persistor.getObjectMd5Hash(bucket, key)
  491. })
  492. it('should re-fetch the file to verify it', function () {
  493. expect(S3Client.getObject).to.have.been.calledWith({
  494. Bucket: bucket,
  495. Key: key
  496. })
  497. })
  498. it('should meter the download', function () {
  499. expect(S3ReadStream.pipe).to.have.been.calledWith(
  500. sinon.match.instanceOf(Stream.Transform)
  501. )
  502. })
  503. it('should calculate the md5 hash from the file', function () {
  504. expect(Hash.read).to.have.been.called
  505. })
  506. it('should return the md5 hash', function () {
  507. expect(hash).to.equal(md5)
  508. })
  509. })
  510. })
  511. describe('copyObject', function () {
  512. describe('with valid parameters', function () {
  513. beforeEach(async function () {
  514. return S3Persistor.copyObject(bucket, key, destKey)
  515. })
  516. it('should copy the object', function () {
  517. expect(S3Client.copyObject).to.have.been.calledWith({
  518. Bucket: bucket,
  519. Key: destKey,
  520. CopySource: `${bucket}/${key}`
  521. })
  522. })
  523. })
  524. describe('when the file does not exist', function () {
  525. let error
  526. beforeEach(async function () {
  527. S3Client.copyObject = sinon.stub().returns({
  528. promise: sinon.stub().rejects(S3NotFoundError)
  529. })
  530. try {
  531. await S3Persistor.copyObject(bucket, key, destKey)
  532. } catch (err) {
  533. error = err
  534. }
  535. })
  536. it('should throw a NotFoundError', function () {
  537. expect(error).to.be.an.instanceOf(Errors.NotFoundError)
  538. })
  539. })
  540. })
  541. describe('deleteObject', function () {
  542. describe('with valid parameters', function () {
  543. beforeEach(async function () {
  544. return S3Persistor.deleteObject(bucket, key)
  545. })
  546. it('should delete the object', function () {
  547. expect(S3Client.deleteObject).to.have.been.calledWith({
  548. Bucket: bucket,
  549. Key: key
  550. })
  551. })
  552. })
  553. })
  554. describe('deleteDirectory', function () {
  555. describe('with valid parameters', function () {
  556. beforeEach(async function () {
  557. return S3Persistor.deleteDirectory(bucket, key)
  558. })
  559. it('should list the objects in the directory', function () {
  560. expect(S3Client.listObjectsV2).to.have.been.calledWith({
  561. Bucket: bucket,
  562. Prefix: key
  563. })
  564. })
  565. it('should delete the objects using their keys', function () {
  566. expect(S3Client.deleteObjects).to.have.been.calledWith({
  567. Bucket: bucket,
  568. Delete: {
  569. Objects: [{ Key: 'llama' }, { Key: 'hippo' }],
  570. Quiet: true
  571. }
  572. })
  573. })
  574. })
  575. describe('when there are no files', function () {
  576. beforeEach(async function () {
  577. S3Client.listObjectsV2 = sinon
  578. .stub()
  579. .returns({ promise: sinon.stub().resolves({ Contents: [] }) })
  580. return S3Persistor.deleteDirectory(bucket, key)
  581. })
  582. it('should list the objects in the directory', function () {
  583. expect(S3Client.listObjectsV2).to.have.been.calledWith({
  584. Bucket: bucket,
  585. Prefix: key
  586. })
  587. })
  588. it('should not try to delete any objects', function () {
  589. expect(S3Client.deleteObjects).not.to.have.been.called
  590. })
  591. })
  592. describe('when there are more files available', function () {
  593. const continuationToken = 'wombat'
  594. beforeEach(async function () {
  595. S3Client.listObjectsV2.onCall(0).returns({
  596. promise: sinon.stub().resolves({
  597. Contents: files,
  598. IsTruncated: true,
  599. NextContinuationToken: continuationToken
  600. })
  601. })
  602. return S3Persistor.deleteDirectory(bucket, key)
  603. })
  604. it('should list the objects a second time, with a continuation token', function () {
  605. expect(S3Client.listObjectsV2).to.be.calledTwice
  606. expect(S3Client.listObjectsV2).to.be.calledWith({
  607. Bucket: bucket,
  608. Prefix: key
  609. })
  610. expect(S3Client.listObjectsV2).to.be.calledWith({
  611. Bucket: bucket,
  612. Prefix: key,
  613. ContinuationToken: continuationToken
  614. })
  615. })
  616. it('should delete both sets of files', function () {
  617. expect(S3Client.deleteObjects).to.have.been.calledTwice
  618. })
  619. })
  620. describe('when there is an error listing the objects', function () {
  621. let error
  622. beforeEach(async function () {
  623. S3Client.listObjectsV2 = sinon
  624. .stub()
  625. .returns({ promise: sinon.stub().rejects(genericError) })
  626. try {
  627. await S3Persistor.deleteDirectory(bucket, key)
  628. } catch (err) {
  629. error = err
  630. }
  631. })
  632. it('should generate a ReadError', function () {
  633. expect(error).to.be.an.instanceOf(Errors.ReadError)
  634. })
  635. it('should wrap the error', function () {
  636. expect(error.cause).to.equal(genericError)
  637. })
  638. it('should not try to delete any objects', function () {
  639. expect(S3Client.deleteObjects).not.to.have.been.called
  640. })
  641. })
  642. describe('when there is an error deleting the objects', function () {
  643. let error
  644. beforeEach(async function () {
  645. S3Client.deleteObjects = sinon
  646. .stub()
  647. .returns({ promise: sinon.stub().rejects(genericError) })
  648. try {
  649. await S3Persistor.deleteDirectory(bucket, key)
  650. } catch (err) {
  651. error = err
  652. }
  653. })
  654. it('should generate a WriteError', function () {
  655. expect(error).to.be.an.instanceOf(Errors.WriteError)
  656. })
  657. it('should wrap the error', function () {
  658. expect(error.cause).to.equal(genericError)
  659. })
  660. })
  661. })
  662. describe('directorySize', function () {
  663. describe('with valid parameters', function () {
  664. let size
  665. beforeEach(async function () {
  666. size = await S3Persistor.directorySize(bucket, key)
  667. })
  668. it('should list the objects in the directory', function () {
  669. expect(S3Client.listObjectsV2).to.have.been.calledWith({
  670. Bucket: bucket,
  671. Prefix: key
  672. })
  673. })
  674. it('should return the directory size', function () {
  675. expect(size).to.equal(filesSize)
  676. })
  677. })
  678. describe('when there are no files', function () {
  679. let size
  680. beforeEach(async function () {
  681. S3Client.listObjectsV2 = sinon
  682. .stub()
  683. .returns({ promise: sinon.stub().resolves({ Contents: [] }) })
  684. size = await S3Persistor.directorySize(bucket, key)
  685. })
  686. it('should list the objects in the directory', function () {
  687. expect(S3Client.listObjectsV2).to.have.been.calledWith({
  688. Bucket: bucket,
  689. Prefix: key
  690. })
  691. })
  692. it('should return zero', function () {
  693. expect(size).to.equal(0)
  694. })
  695. })
  696. describe('when there are more files available', function () {
  697. const continuationToken = 'wombat'
  698. let size
  699. beforeEach(async function () {
  700. S3Client.listObjectsV2.onCall(0).returns({
  701. promise: sinon.stub().resolves({
  702. Contents: files,
  703. IsTruncated: true,
  704. NextContinuationToken: continuationToken
  705. })
  706. })
  707. size = await S3Persistor.directorySize(bucket, key)
  708. })
  709. it('should list the objects a second time, with a continuation token', function () {
  710. expect(S3Client.listObjectsV2).to.be.calledTwice
  711. expect(S3Client.listObjectsV2).to.be.calledWith({
  712. Bucket: bucket,
  713. Prefix: key
  714. })
  715. expect(S3Client.listObjectsV2).to.be.calledWith({
  716. Bucket: bucket,
  717. Prefix: key,
  718. ContinuationToken: continuationToken
  719. })
  720. })
  721. it('should return the size of both sets of files', function () {
  722. expect(size).to.equal(filesSize * 2)
  723. })
  724. })
  725. describe('when there is an error listing the objects', function () {
  726. let error
  727. beforeEach(async function () {
  728. S3Client.listObjectsV2 = sinon
  729. .stub()
  730. .returns({ promise: sinon.stub().rejects(genericError) })
  731. try {
  732. await S3Persistor.directorySize(bucket, key)
  733. } catch (err) {
  734. error = err
  735. }
  736. })
  737. it('should generate a ReadError', function () {
  738. expect(error).to.be.an.instanceOf(Errors.ReadError)
  739. })
  740. it('should wrap the error', function () {
  741. expect(error.cause).to.equal(genericError)
  742. })
  743. })
  744. })
  745. describe('checkIfObjectExists', function () {
  746. describe('when the file exists', function () {
  747. let exists
  748. beforeEach(async function () {
  749. exists = await S3Persistor.checkIfObjectExists(bucket, key)
  750. })
  751. it('should get the object header', function () {
  752. expect(S3Client.headObject).to.have.been.calledWith({
  753. Bucket: bucket,
  754. Key: key
  755. })
  756. })
  757. it('should return that the file exists', function () {
  758. expect(exists).to.equal(true)
  759. })
  760. })
  761. describe('when the file does not exist', function () {
  762. let exists
  763. beforeEach(async function () {
  764. S3Client.headObject = sinon
  765. .stub()
  766. .returns({ promise: sinon.stub().rejects(S3NotFoundError) })
  767. exists = await S3Persistor.checkIfObjectExists(bucket, key)
  768. })
  769. it('should get the object header', function () {
  770. expect(S3Client.headObject).to.have.been.calledWith({
  771. Bucket: bucket,
  772. Key: key
  773. })
  774. })
  775. it('should return that the file does not exist', function () {
  776. expect(exists).to.equal(false)
  777. })
  778. })
  779. describe('when there is an error', function () {
  780. let error
  781. beforeEach(async function () {
  782. S3Client.headObject = sinon
  783. .stub()
  784. .returns({ promise: sinon.stub().rejects(genericError) })
  785. try {
  786. await S3Persistor.checkIfObjectExists(bucket, key)
  787. } catch (err) {
  788. error = err
  789. }
  790. })
  791. it('should generate a ReadError', function () {
  792. expect(error).to.be.an.instanceOf(Errors.ReadError)
  793. })
  794. it('should wrap the upstream ReadError', function () {
  795. expect(error.cause).to.be.an.instanceOf(Errors.ReadError)
  796. })
  797. it('should eventually wrap the error', function () {
  798. expect(error.cause.cause).to.equal(genericError)
  799. })
  800. })
  801. })
  802. })