S3PersistorTests.js 25 KB

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