FSPersistorTests.js 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331
  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. },
  63. globals: { console }
  64. }))({ paths: { uploadFolder: '/tmp' } })
  65. })
  66. describe('sendFile', function () {
  67. const localFilesystemPath = '/path/to/local/file'
  68. it('should copy the file', async function () {
  69. await FSPersistor.sendFile(location, files[0], localFilesystemPath)
  70. expect(fs.createReadStream).to.have.been.calledWith(localFilesystemPath)
  71. expect(fs.createWriteStream).to.have.been.calledWith(
  72. `${location}/${filteredFilenames[0]}`
  73. )
  74. expect(stream.pipeline).to.have.been.calledWith(readStream, writeStream)
  75. })
  76. it('should return an error if the file cannot be stored', async function () {
  77. stream.pipeline.yields(error)
  78. await expect(
  79. FSPersistor.sendFile(location, files[0], localFilesystemPath)
  80. ).to.eventually.be.rejected.and.have.property('cause', error)
  81. })
  82. })
  83. describe('sendStream', function () {
  84. it('should write the stream to disk', async function () {
  85. await FSPersistor.sendStream(location, files[0], remoteStream)
  86. expect(stream.pipeline).to.have.been.calledWith(remoteStream, writeStream)
  87. })
  88. it('should delete the temporary file', async function () {
  89. await FSPersistor.sendStream(location, files[0], remoteStream)
  90. expect(fs.unlink).to.have.been.calledWith(tempFile)
  91. })
  92. it('should wrap the error from the filesystem', async function () {
  93. stream.pipeline.yields(error)
  94. await expect(FSPersistor.sendStream(location, files[0], remoteStream))
  95. .to.eventually.be.rejected.and.be.instanceOf(Errors.WriteError)
  96. .and.have.property('cause', error)
  97. })
  98. it('should send the temporary file to the filestore', async function () {
  99. await FSPersistor.sendStream(location, files[0], remoteStream)
  100. expect(fs.createReadStream).to.have.been.calledWith(tempFile)
  101. })
  102. describe('when the md5 hash does not match', function () {
  103. it('should return a write error', async function () {
  104. await expect(
  105. FSPersistor.sendStream(location, files[0], remoteStream, {
  106. sourceMd5: '00000000'
  107. })
  108. )
  109. .to.eventually.be.rejected.and.be.an.instanceOf(Errors.WriteError)
  110. .and.have.property('message', 'md5 hash mismatch')
  111. })
  112. it('deletes the copied file', async function () {
  113. try {
  114. await FSPersistor.sendStream(location, files[0], remoteStream, {
  115. sourceMd5: '00000000'
  116. })
  117. } catch (_) {}
  118. expect(fs.unlink).to.have.been.calledWith(
  119. `${location}/${filteredFilenames[0]}`
  120. )
  121. })
  122. })
  123. })
  124. describe('getObjectStream', function () {
  125. it('should use correct file location', async function () {
  126. await FSPersistor.getObjectStream(location, files[0], {})
  127. expect(fs.open).to.have.been.calledWith(
  128. `${location}/${filteredFilenames[0]}`
  129. )
  130. })
  131. it('should pass the options to createReadStream', async function () {
  132. await FSPersistor.getObjectStream(location, files[0], {
  133. start: 0,
  134. end: 8
  135. })
  136. expect(fs.createReadStream).to.have.been.calledWith(null, {
  137. start: 0,
  138. end: 8,
  139. fd
  140. })
  141. })
  142. it('should give a NotFoundError if the file does not exist', async function () {
  143. const err = new Error()
  144. err.code = 'ENOENT'
  145. fs.open.yields(err)
  146. await expect(FSPersistor.getObjectStream(location, files[0], {}))
  147. .to.eventually.be.rejected.and.be.an.instanceOf(Errors.NotFoundError)
  148. .and.have.property('cause', err)
  149. })
  150. it('should wrap any other error', async function () {
  151. fs.open.yields(error)
  152. await expect(FSPersistor.getObjectStream(location, files[0], {}))
  153. .to.eventually.be.rejectedWith('failed to open file for streaming')
  154. .and.be.an.instanceOf(Errors.ReadError)
  155. .and.have.property('cause', error)
  156. })
  157. })
  158. describe('getObjectSize', function () {
  159. const badFilename = 'neenaw.tex'
  160. const size = 65536
  161. const noentError = new Error('not found')
  162. noentError.code = 'ENOENT'
  163. beforeEach(function () {
  164. fs.stat
  165. .yields(error)
  166. .withArgs(`${location}/${filteredFilenames[0]}`)
  167. .yields(null, { size })
  168. .withArgs(`${location}/${badFilename}`)
  169. .yields(noentError)
  170. })
  171. it('should return the file size', async function () {
  172. expect(await FSPersistor.getObjectSize(location, files[0])).to.equal(size)
  173. })
  174. it('should throw a NotFoundError if the file does not exist', async function () {
  175. await expect(
  176. FSPersistor.getObjectSize(location, badFilename)
  177. ).to.eventually.be.rejected.and.be.an.instanceOf(Errors.NotFoundError)
  178. })
  179. it('should wrap any other error', async function () {
  180. await expect(FSPersistor.getObjectSize(location, 'raccoon'))
  181. .to.eventually.be.rejected.and.be.an.instanceOf(Errors.ReadError)
  182. .and.have.property('cause', error)
  183. })
  184. })
  185. describe('copyObject', function () {
  186. it('Should open the source for reading', async function () {
  187. await FSPersistor.copyObject(location, files[0], files[1])
  188. expect(fs.createReadStream).to.have.been.calledWith(
  189. `${location}/${filteredFilenames[0]}`
  190. )
  191. })
  192. it('Should open the target for writing', async function () {
  193. await FSPersistor.copyObject(location, files[0], files[1])
  194. expect(fs.createWriteStream).to.have.been.calledWith(
  195. `${location}/${filteredFilenames[1]}`
  196. )
  197. })
  198. it('Should pipe the source to the target', async function () {
  199. await FSPersistor.copyObject(location, files[0], files[1])
  200. expect(stream.pipeline).to.have.been.calledWith(readStream, writeStream)
  201. })
  202. })
  203. describe('deleteObject', function () {
  204. it('Should call unlink with correct options', async function () {
  205. await FSPersistor.deleteObject(location, files[0])
  206. expect(fs.unlink).to.have.been.calledWith(
  207. `${location}/${filteredFilenames[0]}`
  208. )
  209. })
  210. it('Should propagate the error', async function () {
  211. fs.unlink.yields(error)
  212. await expect(
  213. FSPersistor.deleteObject(location, files[0])
  214. ).to.eventually.be.rejected.and.have.property('cause', error)
  215. })
  216. })
  217. describe('deleteDirectory', function () {
  218. it('Should call glob with correct options', async function () {
  219. await FSPersistor.deleteDirectory(location, files[0])
  220. expect(glob).to.have.been.calledWith(
  221. `${location}/${filteredFilenames[0]}_*`
  222. )
  223. })
  224. it('Should call unlink on the returned files', async function () {
  225. await FSPersistor.deleteDirectory(location, files[0])
  226. for (const filename of globs) {
  227. expect(fs.unlink).to.have.been.calledWith(filename)
  228. }
  229. })
  230. it('Should propagate the error', async function () {
  231. glob.yields(error)
  232. await expect(
  233. FSPersistor.deleteDirectory(location, files[0])
  234. ).to.eventually.be.rejected.and.have.property('cause', error)
  235. })
  236. })
  237. describe('checkIfObjectExists', function () {
  238. const badFilename = 'pototo'
  239. const noentError = new Error('not found')
  240. noentError.code = 'ENOENT'
  241. beforeEach(function () {
  242. fs.stat
  243. .yields(error)
  244. .withArgs(`${location}/${filteredFilenames[0]}`)
  245. .yields(null, {})
  246. .withArgs(`${location}/${badFilename}`)
  247. .yields(noentError)
  248. })
  249. it('Should call stat with correct options', async function () {
  250. await FSPersistor.checkIfObjectExists(location, files[0])
  251. expect(fs.stat).to.have.been.calledWith(
  252. `${location}/${filteredFilenames[0]}`
  253. )
  254. })
  255. it('Should return true for existing files', async function () {
  256. expect(
  257. await FSPersistor.checkIfObjectExists(location, files[0])
  258. ).to.equal(true)
  259. })
  260. it('Should return false for non-existing files', async function () {
  261. expect(
  262. await FSPersistor.checkIfObjectExists(location, badFilename)
  263. ).to.equal(false)
  264. })
  265. it('should wrap the error if there is a problem', async function () {
  266. await expect(FSPersistor.checkIfObjectExists(location, 'llama'))
  267. .to.eventually.be.rejected.and.be.an.instanceOf(Errors.ReadError)
  268. .and.have.property('cause', error)
  269. })
  270. })
  271. describe('directorySize', function () {
  272. it('should wrap the error', async function () {
  273. glob.yields(error)
  274. await expect(FSPersistor.directorySize(location, files[0]))
  275. .to.eventually.be.rejected.and.be.an.instanceOf(Errors.ReadError)
  276. .and.include({ cause: error })
  277. .and.have.property('info')
  278. .which.includes({ location, name: files[0] })
  279. })
  280. it('should filter the directory name', async function () {
  281. await FSPersistor.directorySize(location, files[0])
  282. expect(glob).to.have.been.calledWith(
  283. `${location}/${filteredFilenames[0]}_*`
  284. )
  285. })
  286. it('should sum directory files size', async function () {
  287. expect(await FSPersistor.directorySize(location, files[0])).to.equal(
  288. stat.size * files.length
  289. )
  290. })
  291. })
  292. })