FileControllerTests.js 9.3 KB

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