FSPersistorTests.js 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333
  1. const sinon = require('sinon')
  2. const chai = require('chai')
  3. const { expect } = chai
  4. const SandboxedModule = require('sandboxed-module')
  5. const Errors = require('../../src/Errors')
  6. const StreamModule = require('stream')
  7. const modulePath = '../../src/FSPersistor.js'
  8. describe('FSPersistorTests', function () {
  9. const stat = { size: 4, isFile: sinon.stub().returns(true) }
  10. const fd = 1234
  11. const writeStream = 'writeStream'
  12. const remoteStream = 'remoteStream'
  13. const location = '/foo'
  14. const error = new Error('guru meditation error')
  15. const md5 = 'ffffffff'
  16. const files = ['animals/wombat.tex', 'vegetables/potato.tex']
  17. const globs = [`${location}/${files[0]}`, `${location}/${files[1]}`]
  18. const filteredFilenames = ['animals_wombat.tex', 'vegetables_potato.tex']
  19. let fs, stream, FSPersistor, glob, readStream, crypto, Hash, uuid, tempFile
  20. beforeEach(function () {
  21. const randomNumber = Math.random().toString()
  22. readStream = {
  23. name: 'readStream',
  24. on: sinon.stub().yields(),
  25. pipe: sinon.stub()
  26. }
  27. uuid = {
  28. v1: () => randomNumber
  29. }
  30. tempFile = `/tmp/${randomNumber}`
  31. fs = {
  32. createReadStream: sinon.stub().returns(readStream),
  33. createWriteStream: sinon.stub().returns(writeStream),
  34. unlink: sinon.stub().yields(),
  35. open: sinon.stub().yields(null, fd),
  36. stat: sinon.stub().yields(null, stat)
  37. }
  38. glob = sinon.stub().yields(null, globs)
  39. stream = {
  40. pipeline: sinon.stub().yields(),
  41. Transform: StreamModule.Transform
  42. }
  43. Hash = {
  44. end: sinon.stub(),
  45. read: sinon.stub().returns(md5),
  46. digest: sinon.stub().returns(md5),
  47. setEncoding: sinon.stub()
  48. }
  49. crypto = {
  50. createHash: sinon.stub().returns(Hash)
  51. }
  52. FSPersistor = new (SandboxedModule.require(modulePath, {
  53. requires: {
  54. './Errors': Errors,
  55. fs,
  56. glob,
  57. stream,
  58. crypto,
  59. 'node-uuid': uuid,
  60. // imported by PersistorHelper but otherwise unused here
  61. 'logger-sharelatex': {},
  62. 'metrics-sharelatex': {}
  63. },
  64. globals: { console }
  65. }))({ paths: { uploadFolder: '/tmp' } })
  66. })
  67. describe('sendFile', function () {
  68. const localFilesystemPath = '/path/to/local/file'
  69. it('should copy the file', async function () {
  70. await FSPersistor.sendFile(location, files[0], localFilesystemPath)
  71. expect(fs.createReadStream).to.have.been.calledWith(localFilesystemPath)
  72. expect(fs.createWriteStream).to.have.been.calledWith(
  73. `${location}/${filteredFilenames[0]}`
  74. )
  75. expect(stream.pipeline).to.have.been.calledWith(readStream, writeStream)
  76. })
  77. it('should return an error if the file cannot be stored', async function () {
  78. stream.pipeline.yields(error)
  79. await expect(
  80. FSPersistor.sendFile(location, files[0], localFilesystemPath)
  81. ).to.eventually.be.rejected.and.have.property('cause', error)
  82. })
  83. })
  84. describe('sendStream', function () {
  85. it('should write the stream to disk', async function () {
  86. await FSPersistor.sendStream(location, files[0], remoteStream)
  87. expect(stream.pipeline).to.have.been.calledWith(remoteStream, writeStream)
  88. })
  89. it('should delete the temporary file', async function () {
  90. await FSPersistor.sendStream(location, files[0], remoteStream)
  91. expect(fs.unlink).to.have.been.calledWith(tempFile)
  92. })
  93. it('should wrap the error from the filesystem', async function () {
  94. stream.pipeline.yields(error)
  95. await expect(FSPersistor.sendStream(location, files[0], remoteStream))
  96. .to.eventually.be.rejected.and.be.instanceOf(Errors.WriteError)
  97. .and.have.property('cause', error)
  98. })
  99. it('should send the temporary file to the filestore', async function () {
  100. await FSPersistor.sendStream(location, files[0], remoteStream)
  101. expect(fs.createReadStream).to.have.been.calledWith(tempFile)
  102. })
  103. describe('when the md5 hash does not match', function () {
  104. it('should return a write error', async function () {
  105. await expect(
  106. FSPersistor.sendStream(location, files[0], remoteStream, '00000000')
  107. )
  108. .to.eventually.be.rejected.and.be.an.instanceOf(Errors.WriteError)
  109. .and.have.property('message', 'md5 hash mismatch')
  110. })
  111. it('deletes the copied file', async function () {
  112. try {
  113. await FSPersistor.sendStream(
  114. location,
  115. files[0],
  116. remoteStream,
  117. '00000000'
  118. )
  119. } catch (_) {}
  120. expect(fs.unlink).to.have.been.calledWith(
  121. `${location}/${filteredFilenames[0]}`
  122. )
  123. })
  124. })
  125. })
  126. describe('getObjectStream', function () {
  127. it('should use correct file location', async function () {
  128. await FSPersistor.getObjectStream(location, files[0], {})
  129. expect(fs.open).to.have.been.calledWith(
  130. `${location}/${filteredFilenames[0]}`
  131. )
  132. })
  133. it('should pass the options to createReadStream', async function () {
  134. await FSPersistor.getObjectStream(location, files[0], {
  135. start: 0,
  136. end: 8
  137. })
  138. expect(fs.createReadStream).to.have.been.calledWith(null, {
  139. start: 0,
  140. end: 8,
  141. fd
  142. })
  143. })
  144. it('should give a NotFoundError if the file does not exist', async function () {
  145. const err = new Error()
  146. err.code = 'ENOENT'
  147. fs.open.yields(err)
  148. await expect(FSPersistor.getObjectStream(location, files[0], {}))
  149. .to.eventually.be.rejected.and.be.an.instanceOf(Errors.NotFoundError)
  150. .and.have.property('cause', err)
  151. })
  152. it('should wrap any other error', async function () {
  153. fs.open.yields(error)
  154. await expect(FSPersistor.getObjectStream(location, files[0], {}))
  155. .to.eventually.be.rejectedWith('failed to open file for streaming')
  156. .and.be.an.instanceOf(Errors.ReadError)
  157. .and.have.property('cause', error)
  158. })
  159. })
  160. describe('getObjectSize', function () {
  161. const badFilename = 'neenaw.tex'
  162. const size = 65536
  163. const noentError = new Error('not found')
  164. noentError.code = 'ENOENT'
  165. beforeEach(function () {
  166. fs.stat
  167. .yields(error)
  168. .withArgs(`${location}/${filteredFilenames[0]}`)
  169. .yields(null, { size })
  170. .withArgs(`${location}/${badFilename}`)
  171. .yields(noentError)
  172. })
  173. it('should return the file size', async function () {
  174. expect(await FSPersistor.getObjectSize(location, files[0])).to.equal(size)
  175. })
  176. it('should throw a NotFoundError if the file does not exist', async function () {
  177. await expect(
  178. FSPersistor.getObjectSize(location, badFilename)
  179. ).to.eventually.be.rejected.and.be.an.instanceOf(Errors.NotFoundError)
  180. })
  181. it('should wrap any other error', async function () {
  182. await expect(FSPersistor.getObjectSize(location, 'raccoon'))
  183. .to.eventually.be.rejected.and.be.an.instanceOf(Errors.ReadError)
  184. .and.have.property('cause', error)
  185. })
  186. })
  187. describe('copyObject', function () {
  188. it('Should open the source for reading', async function () {
  189. await FSPersistor.copyObject(location, files[0], files[1])
  190. expect(fs.createReadStream).to.have.been.calledWith(
  191. `${location}/${filteredFilenames[0]}`
  192. )
  193. })
  194. it('Should open the target for writing', async function () {
  195. await FSPersistor.copyObject(location, files[0], files[1])
  196. expect(fs.createWriteStream).to.have.been.calledWith(
  197. `${location}/${filteredFilenames[1]}`
  198. )
  199. })
  200. it('Should pipe the source to the target', async function () {
  201. await FSPersistor.copyObject(location, files[0], files[1])
  202. expect(stream.pipeline).to.have.been.calledWith(readStream, writeStream)
  203. })
  204. })
  205. describe('deleteObject', function () {
  206. it('Should call unlink with correct options', async function () {
  207. await FSPersistor.deleteObject(location, files[0])
  208. expect(fs.unlink).to.have.been.calledWith(
  209. `${location}/${filteredFilenames[0]}`
  210. )
  211. })
  212. it('Should propagate the error', async function () {
  213. fs.unlink.yields(error)
  214. await expect(
  215. FSPersistor.deleteObject(location, files[0])
  216. ).to.eventually.be.rejected.and.have.property('cause', error)
  217. })
  218. })
  219. describe('deleteDirectory', function () {
  220. it('Should call glob with correct options', async function () {
  221. await FSPersistor.deleteDirectory(location, files[0])
  222. expect(glob).to.have.been.calledWith(
  223. `${location}/${filteredFilenames[0]}_*`
  224. )
  225. })
  226. it('Should call unlink on the returned files', async function () {
  227. await FSPersistor.deleteDirectory(location, files[0])
  228. for (const filename of globs) {
  229. expect(fs.unlink).to.have.been.calledWith(filename)
  230. }
  231. })
  232. it('Should propagate the error', async function () {
  233. glob.yields(error)
  234. await expect(
  235. FSPersistor.deleteDirectory(location, files[0])
  236. ).to.eventually.be.rejected.and.have.property('cause', error)
  237. })
  238. })
  239. describe('checkIfObjectExists', function () {
  240. const badFilename = 'pototo'
  241. const noentError = new Error('not found')
  242. noentError.code = 'ENOENT'
  243. beforeEach(function () {
  244. fs.stat
  245. .yields(error)
  246. .withArgs(`${location}/${filteredFilenames[0]}`)
  247. .yields(null, {})
  248. .withArgs(`${location}/${badFilename}`)
  249. .yields(noentError)
  250. })
  251. it('Should call stat with correct options', async function () {
  252. await FSPersistor.checkIfObjectExists(location, files[0])
  253. expect(fs.stat).to.have.been.calledWith(
  254. `${location}/${filteredFilenames[0]}`
  255. )
  256. })
  257. it('Should return true for existing files', async function () {
  258. expect(
  259. await FSPersistor.checkIfObjectExists(location, files[0])
  260. ).to.equal(true)
  261. })
  262. it('Should return false for non-existing files', async function () {
  263. expect(
  264. await FSPersistor.checkIfObjectExists(location, badFilename)
  265. ).to.equal(false)
  266. })
  267. it('should wrap the error if there is a problem', async function () {
  268. await expect(FSPersistor.checkIfObjectExists(location, 'llama'))
  269. .to.eventually.be.rejected.and.be.an.instanceOf(Errors.ReadError)
  270. .and.have.property('cause', error)
  271. })
  272. })
  273. describe('directorySize', function () {
  274. it('should wrap the error', async function () {
  275. glob.yields(error)
  276. await expect(FSPersistor.directorySize(location, files[0]))
  277. .to.eventually.be.rejected.and.be.an.instanceOf(Errors.ReadError)
  278. .and.include({ cause: error })
  279. .and.have.property('info')
  280. .which.includes({ location, name: files[0] })
  281. })
  282. it('should filter the directory name', async function () {
  283. await FSPersistor.directorySize(location, files[0])
  284. expect(glob).to.have.been.calledWith(
  285. `${location}/${filteredFilenames[0]}_*`
  286. )
  287. })
  288. it('should sum directory files size', async function () {
  289. expect(await FSPersistor.directorySize(location, files[0])).to.equal(
  290. stat.size * files.length
  291. )
  292. })
  293. })
  294. })