FileController.test.js 6.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257
  1. import sinon from 'sinon'
  2. import { beforeEach, describe, expect, it, vi } from 'vitest'
  3. import Errors from '../../../app/js/Errors.js'
  4. const modulePath = '../../../app/js/FileController.js'
  5. describe('FileController', () => {
  6. let FileHandler, LocalFileWriter, FileController, req, res, next, stream
  7. const settings = {
  8. s3: {
  9. buckets: {
  10. template_files: 'template_files',
  11. },
  12. },
  13. }
  14. const fileSize = 1234
  15. const fileStream = {
  16. destroy() {},
  17. }
  18. const projectId = 'projectId'
  19. const fileId = 'file_id'
  20. const bucket = 'template_files'
  21. const key = `${projectId}/${fileId}`
  22. const error = new Error('incorrect utensil')
  23. beforeEach(async () => {
  24. FileHandler = {
  25. getFile: sinon.stub().yields(null, fileStream),
  26. getFileSize: sinon.stub().yields(null, fileSize),
  27. insertFile: sinon.stub().yields(),
  28. getRedirectUrl: sinon.stub().yields(null, null),
  29. }
  30. LocalFileWriter = {}
  31. stream = {
  32. pipeline: sinon.stub(),
  33. }
  34. vi.doMock('../../../app/js/LocalFileWriter', () => ({
  35. default: LocalFileWriter,
  36. }))
  37. vi.doMock('../../../app/js/FileHandler', () => ({
  38. default: FileHandler,
  39. }))
  40. vi.doMock('../../../app/js/Errors', () => ({
  41. default: Errors,
  42. }))
  43. vi.doMock('stream', () => stream)
  44. vi.doMock('@overleaf/settings', () => ({
  45. default: settings,
  46. }))
  47. vi.doMock('@overleaf/metrics', () => ({
  48. default: {
  49. inc() {},
  50. },
  51. }))
  52. FileController = (await import(modulePath)).default
  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', () => {
  76. it('should try and get a redirect url first', () => {
  77. FileController.getFile(req, res, next)
  78. expect(FileHandler.getRedirectUrl).to.have.been.calledWith(bucket, key)
  79. })
  80. it('should pipe the stream', () => {
  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', async () => {
  85. req.query.cacheWarm = true
  86. await new Promise(resolve => {
  87. res.sendStatus = statusCode => {
  88. expect(statusCode).to.equal(200)
  89. resolve()
  90. }
  91. FileController.getFile(req, res, next)
  92. })
  93. })
  94. it('should send an error if there is a problem', () => {
  95. FileHandler.getFile.yields(error)
  96. FileController.getFile(req, res, next)
  97. expect(next).to.have.been.calledWith(error)
  98. })
  99. describe('with a redirect url', () => {
  100. const redirectUrl = 'https://wombat.potato/giraffe'
  101. beforeEach(() => {
  102. FileHandler.getRedirectUrl.yields(null, redirectUrl)
  103. res.redirect = sinon.stub()
  104. })
  105. it('should redirect', () => {
  106. FileController.getFile(req, res, next)
  107. expect(res.redirect).to.have.been.calledWith(redirectUrl)
  108. })
  109. it('should not get a file stream', () => {
  110. FileController.getFile(req, res, next)
  111. expect(FileHandler.getFile).not.to.have.been.called
  112. })
  113. describe('when there is an error getting the redirect url', () => {
  114. beforeEach(() => {
  115. FileHandler.getRedirectUrl.yields(new Error('wombat herding error'))
  116. })
  117. it('should not redirect', () => {
  118. FileController.getFile(req, res, next)
  119. expect(res.redirect).not.to.have.been.called
  120. })
  121. it('should not return an error', () => {
  122. FileController.getFile(req, res, next)
  123. expect(next).not.to.have.been.called
  124. })
  125. it('should proxy the file', () => {
  126. FileController.getFile(req, res, next)
  127. expect(FileHandler.getFile).to.have.been.calledWith(bucket, key)
  128. })
  129. })
  130. })
  131. describe('with a range header', () => {
  132. let expectedOptions
  133. beforeEach(() => {
  134. expectedOptions = {
  135. bucket,
  136. key,
  137. format: undefined,
  138. style: undefined,
  139. }
  140. })
  141. it('should pass range options to FileHandler', () => {
  142. req.headers.range = 'bytes=0-8'
  143. expectedOptions.start = 0
  144. expectedOptions.end = 8
  145. FileController.getFile(req, res, next)
  146. expect(FileHandler.getFile).to.have.been.calledWith(
  147. bucket,
  148. key,
  149. expectedOptions
  150. )
  151. })
  152. it('should ignore an invalid range header', () => {
  153. req.headers.range = 'potato'
  154. FileController.getFile(req, res, next)
  155. expect(FileHandler.getFile).to.have.been.calledWith(
  156. bucket,
  157. key,
  158. expectedOptions
  159. )
  160. })
  161. it("should ignore any type other than 'bytes'", () => {
  162. req.headers.range = 'wombats=0-8'
  163. FileController.getFile(req, res, next)
  164. expect(FileHandler.getFile).to.have.been.calledWith(
  165. bucket,
  166. key,
  167. expectedOptions
  168. )
  169. })
  170. })
  171. })
  172. describe('getFileHead', () => {
  173. it('should return the file size in a Content-Length header', async () => {
  174. await new Promise(resolve => {
  175. res.end = () => {
  176. expect(res.status).to.have.been.calledWith(200)
  177. expect(res.set).to.have.been.calledWith('Content-Length', fileSize)
  178. resolve()
  179. }
  180. FileController.getFileHead(req, res, next)
  181. })
  182. })
  183. it('should return a 404 is the file is not found', async () => {
  184. await new Promise(resolve => {
  185. FileHandler.getFileSize.yields(
  186. new Errors.NotFoundError({ message: 'not found', info: {} })
  187. )
  188. res.sendStatus = code => {
  189. expect(code).to.equal(404)
  190. resolve()
  191. }
  192. FileController.getFileHead(req, res, next)
  193. })
  194. })
  195. it('should send an error on internal errors', () => {
  196. FileHandler.getFileSize.yields(error)
  197. FileController.getFileHead(req, res, next)
  198. expect(next).to.have.been.calledWith(error)
  199. })
  200. })
  201. describe('insertFile', () => {
  202. it('should send bucket name key and res to FileHandler', async () => {
  203. await new Promise(resolve => {
  204. res.sendStatus = code => {
  205. expect(FileHandler.insertFile).to.have.been.calledWith(
  206. bucket,
  207. key,
  208. req
  209. )
  210. expect(code).to.equal(200)
  211. resolve()
  212. }
  213. FileController.insertFile(req, res, next)
  214. })
  215. })
  216. })
  217. })