GcsPersistorTests.js 23 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849
  1. const { EventEmitter } = require('node:events')
  2. const sinon = require('sinon')
  3. const chai = require('chai')
  4. const { expect } = chai
  5. const modulePath = '../../src/GcsPersistor.js'
  6. const SandboxedModule = require('sandboxed-module')
  7. const { ObjectId } = require('mongodb')
  8. const asyncPool = require('tiny-async-pool')
  9. const Errors = require('../../src/Errors')
  10. describe('GcsPersistorTests', function () {
  11. const filename = '/wombat/potato.tex'
  12. const bucket = 'womBucket'
  13. const key = 'monKey'
  14. const destKey = 'donKey'
  15. const genericError = new Error('guru meditation error')
  16. const filesSize = 33
  17. const md5 = 'ffffffff00000000ffffffff00000000'
  18. const WriteStream = 'writeStream'
  19. const redirectUrl = 'https://wombat.potato/giraffe'
  20. let Logger,
  21. Transform,
  22. PassThrough,
  23. Storage,
  24. Fs,
  25. GcsNotFoundError,
  26. ReadStream,
  27. Stream,
  28. StreamPromises,
  29. GcsBucket,
  30. GcsFile,
  31. GcsPersistor,
  32. FileNotFoundError,
  33. Hash,
  34. Settings,
  35. crypto,
  36. files
  37. beforeEach(function () {
  38. Settings = {
  39. directoryKeyRegex: /^[0-9a-fA-F]{24}\/[0-9a-fA-F]{24}/,
  40. }
  41. files = [
  42. {
  43. metadata: { size: '11', md5Hash: '/////wAAAAD/////AAAAAA==' },
  44. delete: sinon.stub(),
  45. },
  46. {
  47. metadata: { size: '22', md5Hash: '/////wAAAAD/////AAAAAA==' },
  48. delete: sinon.stub(),
  49. },
  50. ]
  51. class FakeGCSResponse extends EventEmitter {
  52. constructor() {
  53. super()
  54. this.statusCode = 200
  55. this.err = null
  56. }
  57. read() {
  58. if (this.err) return this.emit('error', this.err)
  59. this.emit('response', { statusCode: this.statusCode, headers: {} })
  60. }
  61. }
  62. ReadStream = new FakeGCSResponse()
  63. PassThrough = class {}
  64. Transform = class {
  65. once() {}
  66. }
  67. Stream = {
  68. PassThrough,
  69. Transform,
  70. }
  71. StreamPromises = {
  72. pipeline: sinon.stub().resolves(),
  73. }
  74. GcsFile = {
  75. delete: sinon.stub().resolves(),
  76. createReadStream: sinon.stub().returns(ReadStream),
  77. getMetadata: sinon.stub().resolves([files[0].metadata]),
  78. createWriteStream: sinon.stub().returns(WriteStream),
  79. copy: sinon.stub().resolves(),
  80. exists: sinon.stub().resolves([true]),
  81. getSignedUrl: sinon.stub().resolves([redirectUrl]),
  82. }
  83. GcsBucket = {
  84. file: sinon.stub().returns(GcsFile),
  85. getFiles: sinon.stub().resolves([files]),
  86. }
  87. Storage = class {
  88. constructor() {
  89. this.interceptors = []
  90. }
  91. }
  92. Storage.prototype.bucket = sinon.stub().returns(GcsBucket)
  93. GcsNotFoundError = new Error('File not found')
  94. GcsNotFoundError.code = 404
  95. Fs = {
  96. createReadStream: sinon.stub().returns(ReadStream),
  97. }
  98. FileNotFoundError = new Error('File not found')
  99. FileNotFoundError.code = 'ENOENT'
  100. Hash = {
  101. end: sinon.stub(),
  102. read: sinon.stub().returns(md5),
  103. digest: sinon.stub().returns(md5),
  104. setEncoding: sinon.stub(),
  105. }
  106. crypto = {
  107. createHash: sinon.stub().returns(Hash),
  108. }
  109. Logger = {
  110. warn: sinon.stub(),
  111. }
  112. GcsPersistor = new (SandboxedModule.require(modulePath, {
  113. requires: {
  114. '@google-cloud/storage': { Storage },
  115. '@overleaf/logger': Logger,
  116. 'tiny-async-pool': asyncPool,
  117. './Errors': Errors,
  118. fs: Fs,
  119. stream: Stream,
  120. 'stream/promises': StreamPromises,
  121. crypto,
  122. },
  123. globals: { console, Buffer },
  124. }))(Settings)
  125. })
  126. describe('getObjectStream', function () {
  127. describe('when called with valid parameters', function () {
  128. let stream
  129. beforeEach(async function () {
  130. stream = await GcsPersistor.getObjectStream(bucket, key)
  131. })
  132. it('returns a PassThrough stream', function () {
  133. expect(stream).to.be.instanceOf(PassThrough)
  134. })
  135. it('fetches the right key from the right bucket', function () {
  136. expect(Storage.prototype.bucket).to.have.been.calledWith(bucket)
  137. expect(GcsBucket.file).to.have.been.calledWith(key)
  138. expect(GcsFile.createReadStream).to.have.been.called
  139. })
  140. it('disables automatic decompression', function () {
  141. expect(GcsFile.createReadStream).to.have.been.calledWith({
  142. decompress: false,
  143. })
  144. })
  145. it('pipes the stream through the meter', function () {
  146. expect(StreamPromises.pipeline).to.have.been.calledWith(
  147. ReadStream,
  148. sinon.match.instanceOf(Transform),
  149. sinon.match.instanceOf(PassThrough)
  150. )
  151. })
  152. })
  153. describe('when called with a byte range', function () {
  154. let stream
  155. beforeEach(async function () {
  156. stream = await GcsPersistor.getObjectStream(bucket, key, {
  157. start: 5,
  158. end: 10,
  159. })
  160. })
  161. it('returns a PassThrough stream', function () {
  162. expect(stream).to.be.instanceOf(PassThrough)
  163. })
  164. it('passes the byte range on to GCS', function () {
  165. expect(GcsFile.createReadStream).to.have.been.calledWith({
  166. decompress: false,
  167. start: 5,
  168. end: 10,
  169. })
  170. })
  171. })
  172. describe("when the file doesn't exist", function () {
  173. let error, stream
  174. beforeEach(async function () {
  175. ReadStream.statusCode = 404
  176. try {
  177. stream = await GcsPersistor.getObjectStream(bucket, key)
  178. } catch (e) {
  179. error = e
  180. }
  181. })
  182. it('does not return a stream', function () {
  183. expect(stream).not.to.exist
  184. })
  185. it('throws a NotFoundError', function () {
  186. expect(error).to.be.an.instanceOf(Errors.NotFoundError)
  187. })
  188. it('wraps the error', function () {
  189. expect(error.cause).to.exist
  190. })
  191. it('stores the bucket and key in the error', function () {
  192. expect(error.info).to.include({ bucketName: bucket, key })
  193. })
  194. })
  195. describe('when Gcs encounters an unknown error', function () {
  196. let error, stream
  197. beforeEach(async function () {
  198. ReadStream.err = genericError
  199. try {
  200. stream = await GcsPersistor.getObjectStream(bucket, key)
  201. } catch (err) {
  202. error = err
  203. }
  204. })
  205. it('does not return a stream', function () {
  206. expect(stream).not.to.exist
  207. })
  208. it('throws a ReadError', function () {
  209. expect(error).to.be.an.instanceOf(Errors.ReadError)
  210. })
  211. it('wraps the error', function () {
  212. expect(error.cause).to.exist
  213. })
  214. it('stores the bucket and key in the error', function () {
  215. expect(error.info).to.include({ bucketName: bucket, key })
  216. })
  217. })
  218. })
  219. describe('getRedirectUrl', function () {
  220. let signedUrl
  221. describe('with signed URLs', function () {
  222. beforeEach(async function () {
  223. signedUrl = await GcsPersistor.getRedirectUrl(bucket, key)
  224. })
  225. it('should request a signed URL', function () {
  226. expect(GcsFile.getSignedUrl).to.have.been.called
  227. })
  228. it('should return the url', function () {
  229. expect(signedUrl).to.equal(redirectUrl)
  230. })
  231. })
  232. describe('with unsigned URLs', function () {
  233. beforeEach(async function () {
  234. GcsPersistor.settings.unsignedUrls = true
  235. GcsPersistor.settings.endpoint = {
  236. apiEndpoint: 'http://custom.endpoint',
  237. }
  238. signedUrl = await GcsPersistor.getRedirectUrl(bucket, key)
  239. })
  240. it('should return a plain URL', function () {
  241. expect(signedUrl).to.equal(
  242. `http://custom.endpoint/download/storage/v1/b/${bucket}/o/${key}?alt=media`
  243. )
  244. })
  245. })
  246. })
  247. describe('getObjectSize', function () {
  248. describe('when called with valid parameters', function () {
  249. let size
  250. beforeEach(async function () {
  251. size = await GcsPersistor.getObjectSize(bucket, key)
  252. })
  253. it('should return the object size', function () {
  254. expect(size).to.equal(11)
  255. })
  256. it('should pass the bucket and key to GCS', function () {
  257. expect(Storage.prototype.bucket).to.have.been.calledWith(bucket)
  258. expect(GcsBucket.file).to.have.been.calledWith(key)
  259. expect(GcsFile.getMetadata).to.have.been.called
  260. })
  261. })
  262. describe('when the object is not found', function () {
  263. let error
  264. beforeEach(async function () {
  265. GcsFile.getMetadata = sinon.stub().rejects(GcsNotFoundError)
  266. try {
  267. await GcsPersistor.getObjectSize(bucket, key)
  268. } catch (err) {
  269. error = err
  270. }
  271. })
  272. it('should return a NotFoundError', function () {
  273. expect(error).to.be.an.instanceOf(Errors.NotFoundError)
  274. })
  275. it('should wrap the error', function () {
  276. expect(error.cause).to.equal(GcsNotFoundError)
  277. })
  278. })
  279. describe('when GCS returns an error', function () {
  280. let error
  281. beforeEach(async function () {
  282. GcsFile.getMetadata = sinon.stub().rejects(genericError)
  283. try {
  284. await GcsPersistor.getObjectSize(bucket, key)
  285. } catch (err) {
  286. error = err
  287. }
  288. })
  289. it('should return a ReadError', function () {
  290. expect(error).to.be.an.instanceOf(Errors.ReadError)
  291. })
  292. it('should wrap the error', function () {
  293. expect(error.cause).to.equal(genericError)
  294. })
  295. })
  296. })
  297. describe('sendStream', function () {
  298. describe('with valid parameters', function () {
  299. beforeEach(async function () {
  300. return GcsPersistor.sendStream(bucket, key, ReadStream)
  301. })
  302. it('should upload the stream', function () {
  303. expect(Storage.prototype.bucket).to.have.been.calledWith(bucket)
  304. expect(GcsBucket.file).to.have.been.calledWith(key)
  305. expect(GcsFile.createWriteStream).to.have.been.called
  306. })
  307. it('should not try to create a resumable upload', function () {
  308. expect(GcsFile.createWriteStream).to.have.been.calledWith({
  309. resumable: false,
  310. })
  311. })
  312. it('should meter the stream and pass it to GCS', function () {
  313. expect(StreamPromises.pipeline).to.have.been.calledWith(
  314. ReadStream,
  315. sinon.match.instanceOf(Transform),
  316. WriteStream
  317. )
  318. })
  319. it('calculates the md5 hash of the file', function () {
  320. expect(Hash.digest).to.have.been.called
  321. })
  322. })
  323. describe('when a hash is supplied', function () {
  324. beforeEach(async function () {
  325. return GcsPersistor.sendStream(bucket, key, ReadStream, {
  326. sourceMd5: 'aaaaaaaabbbbbbbbaaaaaaaabbbbbbbb',
  327. })
  328. })
  329. it('should not calculate the md5 hash of the file', function () {
  330. expect(Hash.digest).not.to.have.been.called
  331. })
  332. it('sends the hash in base64', function () {
  333. expect(GcsFile.createWriteStream).to.have.been.calledWith({
  334. validation: 'md5',
  335. metadata: {
  336. md5Hash: 'qqqqqru7u7uqqqqqu7u7uw==',
  337. },
  338. resumable: false,
  339. })
  340. })
  341. it('does not fetch the md5 hash of the uploaded file', function () {
  342. expect(GcsFile.getMetadata).not.to.have.been.called
  343. })
  344. })
  345. describe('when metadata is supplied', function () {
  346. const contentType = 'text/csv'
  347. const contentEncoding = 'gzip'
  348. beforeEach(async function () {
  349. return GcsPersistor.sendStream(bucket, key, ReadStream, {
  350. contentType,
  351. contentEncoding,
  352. })
  353. })
  354. it('should send the metadata to GCS', function () {
  355. expect(GcsFile.createWriteStream).to.have.been.calledWith({
  356. metadata: { contentType, contentEncoding },
  357. resumable: false,
  358. })
  359. })
  360. })
  361. describe('when the upload fails', function () {
  362. let error
  363. beforeEach(async function () {
  364. StreamPromises.pipeline
  365. .withArgs(ReadStream, sinon.match.instanceOf(Transform), WriteStream)
  366. .rejects(genericError)
  367. try {
  368. await GcsPersistor.sendStream(bucket, key, ReadStream)
  369. } catch (err) {
  370. error = err
  371. }
  372. })
  373. it('throws a WriteError', function () {
  374. expect(error).to.be.an.instanceOf(Errors.WriteError)
  375. })
  376. it('wraps the error', function () {
  377. expect(error.cause).to.equal(genericError)
  378. })
  379. })
  380. })
  381. describe('sendFile', function () {
  382. describe('with valid parameters', function () {
  383. beforeEach(async function () {
  384. return GcsPersistor.sendFile(bucket, key, filename)
  385. })
  386. it('should create a read stream for the file', function () {
  387. expect(Fs.createReadStream).to.have.been.calledWith(filename)
  388. })
  389. it('should create a write stream', function () {
  390. expect(Storage.prototype.bucket).to.have.been.calledWith(bucket)
  391. expect(GcsBucket.file).to.have.been.calledWith(key)
  392. expect(GcsFile.createWriteStream).to.have.been.called
  393. })
  394. it('should upload the stream via the meter', function () {
  395. expect(StreamPromises.pipeline).to.have.been.calledWith(
  396. ReadStream,
  397. sinon.match.instanceOf(Transform),
  398. WriteStream
  399. )
  400. })
  401. })
  402. })
  403. describe('copyObject', function () {
  404. const destinationFile = 'destFile'
  405. beforeEach(function () {
  406. GcsBucket.file.withArgs(destKey).returns(destinationFile)
  407. })
  408. describe('with valid parameters', function () {
  409. beforeEach(async function () {
  410. return GcsPersistor.copyObject(bucket, key, destKey)
  411. })
  412. it('should copy the object', function () {
  413. expect(Storage.prototype.bucket).to.have.been.calledWith(bucket)
  414. expect(GcsBucket.file).to.have.been.calledWith(key)
  415. expect(GcsFile.copy).to.have.been.calledWith(destinationFile)
  416. })
  417. })
  418. describe('when the file does not exist', function () {
  419. let error
  420. beforeEach(async function () {
  421. GcsFile.copy = sinon.stub().rejects(GcsNotFoundError)
  422. try {
  423. await GcsPersistor.copyObject(bucket, key, destKey)
  424. } catch (err) {
  425. error = err
  426. }
  427. })
  428. it('should throw a NotFoundError', function () {
  429. expect(error).to.be.an.instanceOf(Errors.NotFoundError)
  430. })
  431. })
  432. })
  433. describe('deleteObject', function () {
  434. describe('with valid parameters', function () {
  435. beforeEach(async function () {
  436. return GcsPersistor.deleteObject(bucket, key)
  437. })
  438. it('should delete the object', function () {
  439. expect(Storage.prototype.bucket).to.have.been.calledWith(bucket)
  440. expect(GcsBucket.file).to.have.been.calledWith(key)
  441. expect(GcsFile.delete).to.have.been.called
  442. })
  443. })
  444. describe('when the file does not exist', function () {
  445. let error
  446. beforeEach(async function () {
  447. GcsFile.delete = sinon.stub().rejects(GcsNotFoundError)
  448. try {
  449. await GcsPersistor.deleteObject(bucket, key)
  450. } catch (err) {
  451. error = err
  452. }
  453. })
  454. it('should not throw an error', function () {
  455. expect(error).not.to.exist
  456. })
  457. })
  458. })
  459. describe('deleteDirectory', function () {
  460. const directoryName = `${new ObjectId()}/${new ObjectId()}`
  461. const directoryPrefix = `${directoryName}/`
  462. describe('with valid parameters', function () {
  463. beforeEach(async function () {
  464. GcsBucket.getFiles = sinon.stub()
  465. // set up multiple paginated calls to getFiles
  466. GcsBucket.getFiles
  467. .withArgs({ prefix: directoryPrefix, autoPaginate: false })
  468. .resolves([['aaa', 'bbb'], 'call-1'])
  469. GcsBucket.getFiles
  470. .withArgs('call-1')
  471. .resolves([['ccc', 'ddd', 'eee'], 'call-2'])
  472. GcsBucket.getFiles.withArgs('call-2').resolves([['fff', 'ggg']])
  473. return GcsPersistor.deleteDirectory(bucket, directoryName)
  474. })
  475. it('should list the objects in the directory', function () {
  476. expect(Storage.prototype.bucket).to.have.been.calledWith(bucket)
  477. expect(GcsBucket.getFiles).to.have.been.calledWith({
  478. prefix: directoryPrefix,
  479. autoPaginate: false,
  480. })
  481. expect(GcsBucket.getFiles).to.have.been.calledWith('call-1')
  482. expect(GcsBucket.getFiles).to.have.been.calledWith('call-2')
  483. })
  484. it('should delete the files', function () {
  485. expect(GcsFile.delete.callCount).to.equal(7)
  486. })
  487. })
  488. describe('when there is an error listing the objects', function () {
  489. let error
  490. beforeEach(async function () {
  491. GcsBucket.getFiles = sinon.stub().rejects(genericError)
  492. try {
  493. await GcsPersistor.deleteDirectory(bucket, directoryName)
  494. } catch (err) {
  495. error = err
  496. }
  497. })
  498. it('should generate a WriteError', function () {
  499. expect(error).to.be.an.instanceOf(Errors.WriteError)
  500. })
  501. it('should wrap the error', function () {
  502. expect(error.cause).to.equal(genericError)
  503. })
  504. })
  505. })
  506. describe('directorySize', function () {
  507. describe('with valid parameters', function () {
  508. let size
  509. beforeEach(async function () {
  510. size = await GcsPersistor.directorySize(bucket, key)
  511. })
  512. it('should list the objects in the directory', function () {
  513. expect(Storage.prototype.bucket).to.have.been.calledWith(bucket)
  514. expect(GcsBucket.getFiles).to.have.been.calledWith({
  515. prefix: `${key}/`,
  516. })
  517. })
  518. it('should return the directory size', function () {
  519. expect(size).to.equal(filesSize)
  520. })
  521. })
  522. describe('when there are no files', function () {
  523. let size
  524. beforeEach(async function () {
  525. GcsBucket.getFiles.resolves([[]])
  526. size = await GcsPersistor.directorySize(bucket, key)
  527. })
  528. it('should list the objects in the directory', function () {
  529. expect(Storage.prototype.bucket).to.have.been.calledWith(bucket)
  530. expect(GcsBucket.getFiles).to.have.been.calledWith({
  531. prefix: `${key}/`,
  532. })
  533. })
  534. it('should return zero', function () {
  535. expect(size).to.equal(0)
  536. })
  537. })
  538. describe('when there is an error listing the objects', function () {
  539. let error
  540. beforeEach(async function () {
  541. GcsBucket.getFiles.rejects(genericError)
  542. try {
  543. await GcsPersistor.directorySize(bucket, key)
  544. } catch (err) {
  545. error = err
  546. }
  547. })
  548. it('should generate a ReadError', function () {
  549. expect(error).to.be.an.instanceOf(Errors.ReadError)
  550. })
  551. it('should wrap the error', function () {
  552. expect(error.cause).to.equal(genericError)
  553. })
  554. })
  555. })
  556. describe('checkIfObjectExists', function () {
  557. describe('when the file exists', function () {
  558. let exists
  559. beforeEach(async function () {
  560. exists = await GcsPersistor.checkIfObjectExists(bucket, key)
  561. })
  562. it('should ask the file if it exists', function () {
  563. expect(Storage.prototype.bucket).to.have.been.calledWith(bucket)
  564. expect(GcsBucket.file).to.have.been.calledWith(key)
  565. expect(GcsFile.exists).to.have.been.called
  566. })
  567. it('should return that the file exists', function () {
  568. expect(exists).to.equal(true)
  569. })
  570. })
  571. describe('when the file does not exist', function () {
  572. let exists
  573. beforeEach(async function () {
  574. GcsFile.exists = sinon.stub().resolves([false])
  575. exists = await GcsPersistor.checkIfObjectExists(bucket, key)
  576. })
  577. it('should get the object header', function () {
  578. expect(Storage.prototype.bucket).to.have.been.calledWith(bucket)
  579. expect(GcsBucket.file).to.have.been.calledWith(key)
  580. expect(GcsFile.exists).to.have.been.called
  581. })
  582. it('should return that the file does not exist', function () {
  583. expect(exists).to.equal(false)
  584. })
  585. })
  586. describe('when there is an error', function () {
  587. let error
  588. beforeEach(async function () {
  589. GcsFile.exists = sinon.stub().rejects(genericError)
  590. try {
  591. await GcsPersistor.checkIfObjectExists(bucket, key)
  592. } catch (err) {
  593. error = err
  594. }
  595. })
  596. it('should generate a ReadError', function () {
  597. expect(error).to.be.an.instanceOf(Errors.ReadError)
  598. })
  599. it('should wrap the error', function () {
  600. expect(error.cause).to.equal(genericError)
  601. })
  602. })
  603. })
  604. describe('listDirectoryKeys', function () {
  605. describe('with valid parameters', function () {
  606. let keys
  607. beforeEach(async function () {
  608. const filesWithNames = files.map((file, i) => ({
  609. ...file,
  610. name: i === 0 ? 'llama' : 'hippo',
  611. }))
  612. GcsBucket.getFiles.resolves([filesWithNames])
  613. keys = await GcsPersistor.listDirectoryKeys(bucket, key)
  614. })
  615. it('should list the objects in the directory', function () {
  616. expect(Storage.prototype.bucket).to.have.been.calledWith(bucket)
  617. expect(GcsBucket.getFiles).to.have.been.calledWith({
  618. prefix: `${key}/`,
  619. })
  620. })
  621. it('should return the keys', function () {
  622. expect(keys).to.deep.equal(['llama', 'hippo'])
  623. })
  624. })
  625. describe('when there are no files', function () {
  626. let keys
  627. beforeEach(async function () {
  628. GcsBucket.getFiles.resolves([[]])
  629. keys = await GcsPersistor.listDirectoryKeys(bucket, key)
  630. })
  631. it('should return an empty array', function () {
  632. expect(keys).to.deep.equal([])
  633. })
  634. })
  635. describe('when there is an error listing the objects', function () {
  636. let error
  637. beforeEach(async function () {
  638. GcsBucket.getFiles.rejects(genericError)
  639. try {
  640. await GcsPersistor.listDirectoryKeys(bucket, key)
  641. } catch (err) {
  642. error = err
  643. }
  644. })
  645. it('should generate a ReadError', function () {
  646. expect(error).to.be.an.instanceOf(Errors.ReadError)
  647. })
  648. it('should wrap the error', function () {
  649. expect(error.cause).to.equal(genericError)
  650. })
  651. })
  652. })
  653. describe('listDirectoryStats', function () {
  654. describe('with valid parameters', function () {
  655. let stats
  656. beforeEach(async function () {
  657. const filesWithNames = files.map((file, i) => ({
  658. ...file,
  659. name: i === 0 ? 'llama' : 'hippo',
  660. }))
  661. GcsBucket.getFiles.resolves([filesWithNames])
  662. stats = await GcsPersistor.listDirectoryStats(bucket, key)
  663. })
  664. it('should list the objects in the directory', function () {
  665. expect(Storage.prototype.bucket).to.have.been.calledWith(bucket)
  666. expect(GcsBucket.getFiles).to.have.been.calledWith({
  667. prefix: `${key}/`,
  668. })
  669. })
  670. it('should return the stats', function () {
  671. expect(stats).to.deep.equal([
  672. { key: 'llama', size: 11 },
  673. { key: 'hippo', size: 22 },
  674. ])
  675. })
  676. })
  677. describe('when there are no files', function () {
  678. let stats
  679. beforeEach(async function () {
  680. GcsBucket.getFiles.resolves([[]])
  681. stats = await GcsPersistor.listDirectoryStats(bucket, key)
  682. })
  683. it('should return an empty array', function () {
  684. expect(stats).to.deep.equal([])
  685. })
  686. })
  687. describe('when there is an error listing the objects', function () {
  688. let error
  689. beforeEach(async function () {
  690. GcsBucket.getFiles.rejects(genericError)
  691. try {
  692. await GcsPersistor.listDirectoryStats(bucket, key)
  693. } catch (err) {
  694. error = err
  695. }
  696. })
  697. it('should generate a ReadError', function () {
  698. expect(error).to.be.an.instanceOf(Errors.ReadError)
  699. })
  700. it('should wrap the error', function () {
  701. expect(error.cause).to.equal(genericError)
  702. })
  703. })
  704. })
  705. })