FileControllerTests.js 9.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347
  1. const sinon = require('sinon')
  2. const chai = require('chai')
  3. const { expect } = chai
  4. const SandboxedModule = require('sandboxed-module')
  5. const Errors = require('../../../app/js/Errors')
  6. const modulePath = '../../../app/js/FileController.js'
  7. describe('FileController', function() {
  8. let PersistorManager,
  9. FileHandler,
  10. LocalFileWriter,
  11. FileController,
  12. req,
  13. res,
  14. next,
  15. stream
  16. const settings = {
  17. s3: {
  18. buckets: {
  19. user_files: 'user_files'
  20. }
  21. }
  22. }
  23. const fileSize = 1234
  24. const fileStream = 'fileStream'
  25. const projectId = 'projectId'
  26. const fileId = 'file_id'
  27. const bucket = 'user_files'
  28. const key = `${projectId}/${fileId}`
  29. const error = new Error('incorrect utensil')
  30. beforeEach(function() {
  31. PersistorManager = {
  32. sendStream: sinon.stub().yields(),
  33. copyObject: sinon.stub().resolves(),
  34. deleteObject: sinon.stub().yields()
  35. }
  36. FileHandler = {
  37. getFile: sinon.stub().yields(null, fileStream),
  38. getFileSize: sinon.stub().yields(null, fileSize),
  39. deleteFile: sinon.stub().yields(),
  40. deleteProject: sinon.stub().yields(),
  41. insertFile: sinon.stub().yields(),
  42. getDirectorySize: sinon.stub().yields(null, fileSize),
  43. getRedirectUrl: sinon.stub().yields(null, null)
  44. }
  45. LocalFileWriter = {}
  46. stream = {
  47. pipeline: sinon.stub()
  48. }
  49. FileController = SandboxedModule.require(modulePath, {
  50. requires: {
  51. './LocalFileWriter': LocalFileWriter,
  52. './FileHandler': FileHandler,
  53. './PersistorManager': PersistorManager,
  54. './Errors': Errors,
  55. stream: stream,
  56. 'settings-sharelatex': settings,
  57. 'metrics-sharelatex': {
  58. inc() {}
  59. }
  60. },
  61. globals: { console }
  62. })
  63. req = {
  64. key: key,
  65. bucket: bucket,
  66. project_id: projectId,
  67. query: {},
  68. params: {
  69. project_id: projectId,
  70. file_id: fileId
  71. },
  72. headers: {},
  73. requestLogger: {
  74. setMessage: sinon.stub(),
  75. addFields: sinon.stub()
  76. }
  77. }
  78. res = {
  79. set: sinon.stub().returnsThis(),
  80. sendStatus: sinon.stub().returnsThis(),
  81. status: sinon.stub().returnsThis()
  82. }
  83. next = sinon.stub()
  84. })
  85. describe('getFile', function() {
  86. it('should try and get a redirect url first', function() {
  87. FileController.getFile(req, res, next)
  88. expect(FileHandler.getRedirectUrl).to.have.been.calledWith(bucket, key)
  89. })
  90. it('should pipe the stream', function() {
  91. FileController.getFile(req, res, next)
  92. expect(stream.pipeline).to.have.been.calledWith(fileStream, res)
  93. })
  94. it('should send a 200 if the cacheWarm param is true', function(done) {
  95. req.query.cacheWarm = true
  96. res.sendStatus = statusCode => {
  97. statusCode.should.equal(200)
  98. done()
  99. }
  100. FileController.getFile(req, res, next)
  101. })
  102. it('should send an error if there is a problem', function() {
  103. FileHandler.getFile.yields(error)
  104. FileController.getFile(req, res, next)
  105. expect(next).to.have.been.calledWith(error)
  106. })
  107. describe('with a redirect url', function() {
  108. const redirectUrl = 'https://wombat.potato/giraffe'
  109. beforeEach(function() {
  110. FileHandler.getRedirectUrl.yields(null, redirectUrl)
  111. res.redirect = sinon.stub()
  112. })
  113. it('should redirect', function() {
  114. FileController.getFile(req, res, next)
  115. expect(res.redirect).to.have.been.calledWith(redirectUrl)
  116. })
  117. it('should not get a file stream', function() {
  118. FileController.getFile(req, res, next)
  119. expect(FileHandler.getFile).not.to.have.been.called
  120. })
  121. describe('when there is an error getting the redirect url', function() {
  122. beforeEach(function() {
  123. FileHandler.getRedirectUrl.yields(new Error('wombat herding error'))
  124. })
  125. it('should not redirect', function() {
  126. FileController.getFile(req, res, next)
  127. expect(res.redirect).not.to.have.been.called
  128. })
  129. it('should not return an error', function() {
  130. FileController.getFile(req, res, next)
  131. expect(next).not.to.have.been.called
  132. })
  133. it('should proxy the file', function() {
  134. FileController.getFile(req, res, next)
  135. expect(FileHandler.getFile).to.have.been.calledWith(bucket, key)
  136. })
  137. })
  138. })
  139. describe('with a range header', function() {
  140. let expectedOptions
  141. beforeEach(function() {
  142. expectedOptions = {
  143. bucket,
  144. key,
  145. format: undefined,
  146. style: undefined
  147. }
  148. })
  149. it('should pass range options to FileHandler', function() {
  150. req.headers.range = 'bytes=0-8'
  151. expectedOptions.start = 0
  152. expectedOptions.end = 8
  153. FileController.getFile(req, res, next)
  154. expect(FileHandler.getFile).to.have.been.calledWith(
  155. bucket,
  156. key,
  157. expectedOptions
  158. )
  159. })
  160. it('should ignore an invalid range header', function() {
  161. req.headers.range = 'potato'
  162. FileController.getFile(req, res, next)
  163. expect(FileHandler.getFile).to.have.been.calledWith(
  164. bucket,
  165. key,
  166. expectedOptions
  167. )
  168. })
  169. it("should ignore any type other than 'bytes'", function() {
  170. req.headers.range = 'wombats=0-8'
  171. FileController.getFile(req, res, next)
  172. expect(FileHandler.getFile).to.have.been.calledWith(
  173. bucket,
  174. key,
  175. expectedOptions
  176. )
  177. })
  178. })
  179. })
  180. describe('getFileHead', function() {
  181. it('should return the file size in a Content-Length header', function(done) {
  182. res.end = () => {
  183. expect(res.status).to.have.been.calledWith(200)
  184. expect(res.set).to.have.been.calledWith('Content-Length', fileSize)
  185. done()
  186. }
  187. FileController.getFileHead(req, res, next)
  188. })
  189. it('should return a 404 is the file is not found', function(done) {
  190. FileHandler.getFileSize.yields(
  191. new Errors.NotFoundError({ message: 'not found', info: {} })
  192. )
  193. res.sendStatus = code => {
  194. expect(code).to.equal(404)
  195. done()
  196. }
  197. FileController.getFileHead(req, res, next)
  198. })
  199. it('should send an error on internal errors', function() {
  200. FileHandler.getFileSize.yields(error)
  201. FileController.getFileHead(req, res, next)
  202. expect(next).to.have.been.calledWith(error)
  203. })
  204. })
  205. describe('insertFile', function() {
  206. it('should send bucket name key and res to PersistorManager', function(done) {
  207. res.sendStatus = code => {
  208. expect(FileHandler.insertFile).to.have.been.calledWith(bucket, key, req)
  209. expect(code).to.equal(200)
  210. done()
  211. }
  212. FileController.insertFile(req, res, next)
  213. })
  214. })
  215. describe('copyFile', function() {
  216. const oldFileId = 'oldFileId'
  217. const oldProjectId = 'oldProjectid'
  218. const oldKey = `${oldProjectId}/${oldFileId}`
  219. beforeEach(function() {
  220. req.body = {
  221. source: {
  222. project_id: oldProjectId,
  223. file_id: oldFileId
  224. }
  225. }
  226. })
  227. it('should send bucket name and both keys to PersistorManager', function(done) {
  228. res.sendStatus = code => {
  229. code.should.equal(200)
  230. expect(PersistorManager.copyObject).to.have.been.calledWith(
  231. bucket,
  232. oldKey,
  233. key
  234. )
  235. done()
  236. }
  237. FileController.copyFile(req, res, next)
  238. })
  239. it('should send a 404 if the original file was not found', function(done) {
  240. PersistorManager.copyObject.rejects(
  241. new Errors.NotFoundError({ message: 'not found', info: {} })
  242. )
  243. res.sendStatus = code => {
  244. code.should.equal(404)
  245. done()
  246. }
  247. FileController.copyFile(req, res, next)
  248. })
  249. it('should send an error if there was an error', function(done) {
  250. PersistorManager.copyObject.rejects(error)
  251. FileController.copyFile(req, res, err => {
  252. expect(err).to.equal(error)
  253. done()
  254. })
  255. })
  256. })
  257. describe('delete file', function() {
  258. it('should tell the file handler', function(done) {
  259. res.sendStatus = code => {
  260. code.should.equal(204)
  261. expect(FileHandler.deleteFile).to.have.been.calledWith(bucket, key)
  262. done()
  263. }
  264. FileController.deleteFile(req, res, next)
  265. })
  266. it('should send a 500 if there was an error', function() {
  267. FileHandler.deleteFile.yields(error)
  268. FileController.deleteFile(req, res, next)
  269. expect(next).to.have.been.calledWith(error)
  270. })
  271. })
  272. describe('delete project', function() {
  273. it('should tell the file handler', function(done) {
  274. res.sendStatus = code => {
  275. code.should.equal(204)
  276. expect(FileHandler.deleteProject).to.have.been.calledWith(bucket, key)
  277. done()
  278. }
  279. FileController.deleteProject(req, res, next)
  280. })
  281. it('should send a 500 if there was an error', function() {
  282. FileHandler.deleteProject.yields(error)
  283. FileController.deleteProject(req, res, next)
  284. expect(next).to.have.been.calledWith(error)
  285. })
  286. })
  287. describe('directorySize', function() {
  288. it('should return total directory size bytes', function(done) {
  289. FileController.directorySize(req, {
  290. json: result => {
  291. expect(result['total bytes']).to.equal(fileSize)
  292. done()
  293. }
  294. })
  295. })
  296. it('should send a 500 if there was an error', function() {
  297. FileHandler.getDirectorySize.yields(error)
  298. FileController.directorySize(req, res, next)
  299. expect(next).to.have.been.calledWith(error)
  300. })
  301. })
  302. })