S3PersistorTests.js 25 KB

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