FileStoreControllerTests.js 7.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243
  1. const { expect } = require('chai')
  2. const sinon = require('sinon')
  3. const SandboxedModule = require('sandboxed-module')
  4. const Errors = require('../../../../app/src/Features/Errors/Errors')
  5. const MODULE_PATH =
  6. '../../../../app/src/Features/FileStore/FileStoreController.js'
  7. describe('FileStoreController', function() {
  8. beforeEach(function() {
  9. this.FileStoreHandler = {
  10. getFileStream: sinon.stub(),
  11. getFileSize: sinon.stub()
  12. }
  13. this.ProjectLocator = { findElement: sinon.stub() }
  14. this.controller = SandboxedModule.require(MODULE_PATH, {
  15. requires: {
  16. 'settings-sharelatex': this.settings,
  17. '../Project/ProjectLocator': this.ProjectLocator,
  18. './FileStoreHandler': this.FileStoreHandler
  19. }
  20. })
  21. this.stream = {}
  22. this.projectId = '2k3j1lk3j21lk3j'
  23. this.fileId = '12321kklj1lk3jk12'
  24. this.req = {
  25. params: {
  26. Project_id: this.projectId,
  27. File_id: this.fileId
  28. },
  29. query: 'query string here',
  30. get(key) {
  31. return undefined
  32. }
  33. }
  34. this.res = {
  35. set: sinon.stub().returnsThis(),
  36. setHeader: sinon.stub(),
  37. setContentDisposition: sinon.stub(),
  38. status: sinon.stub().returnsThis()
  39. }
  40. this.file = { name: 'myfile.png' }
  41. })
  42. describe('getFile', function() {
  43. beforeEach(function() {
  44. this.FileStoreHandler.getFileStream.callsArgWith(3, null, this.stream)
  45. this.ProjectLocator.findElement.callsArgWith(1, null, this.file)
  46. })
  47. it('should call the file store handler with the project_id file_id and any query string', function(done) {
  48. this.stream.pipe = des => {
  49. this.FileStoreHandler.getFileStream
  50. .calledWith(
  51. this.req.params.Project_id,
  52. this.req.params.File_id,
  53. this.req.query
  54. )
  55. .should.equal(true)
  56. done()
  57. }
  58. this.controller.getFile(this.req, this.res)
  59. })
  60. it('should pipe to res', function(done) {
  61. this.stream.pipe = des => {
  62. des.should.equal(this.res)
  63. done()
  64. }
  65. this.controller.getFile(this.req, this.res)
  66. })
  67. it('should get the file from the db', function(done) {
  68. this.stream.pipe = des => {
  69. const opts = {
  70. project_id: this.projectId,
  71. element_id: this.fileId,
  72. type: 'file'
  73. }
  74. this.ProjectLocator.findElement.calledWith(opts).should.equal(true)
  75. done()
  76. }
  77. this.controller.getFile(this.req, this.res)
  78. })
  79. it('should set the Content-Disposition header', function(done) {
  80. this.stream.pipe = des => {
  81. this.res.setContentDisposition
  82. .calledWith('attachment', { filename: this.file.name })
  83. .should.equal(true)
  84. done()
  85. }
  86. this.controller.getFile(this.req, this.res)
  87. })
  88. // Test behaviour around handling html files
  89. ;['.html', '.htm', '.xhtml'].forEach(extension => {
  90. describe(`with a '${extension}' file extension`, function() {
  91. beforeEach(function() {
  92. this.file.name = `bad${extension}`
  93. this.req.get = key => {
  94. if (key === 'User-Agent') {
  95. return 'A generic browser'
  96. }
  97. }
  98. })
  99. describe('from a non-ios browser', function() {
  100. it('should not set Content-Type', function(done) {
  101. this.stream.pipe = des => {
  102. this.res.setHeader
  103. .calledWith('Content-Type', 'text/plain')
  104. .should.equal(false)
  105. done()
  106. }
  107. this.controller.getFile(this.req, this.res)
  108. })
  109. })
  110. describe('from an iPhone', function() {
  111. beforeEach(function() {
  112. this.req.get = key => {
  113. if (key === 'User-Agent') {
  114. return 'An iPhone browser'
  115. }
  116. }
  117. })
  118. it("should set Content-Type to 'text/plain'", function(done) {
  119. this.stream.pipe = des => {
  120. this.res.setHeader
  121. .calledWith('Content-Type', 'text/plain')
  122. .should.equal(true)
  123. done()
  124. }
  125. this.controller.getFile(this.req, this.res)
  126. })
  127. })
  128. describe('from an iPad', function() {
  129. beforeEach(function() {
  130. this.req.get = key => {
  131. if (key === 'User-Agent') {
  132. return 'An iPad browser'
  133. }
  134. }
  135. })
  136. it("should set Content-Type to 'text/plain'", function(done) {
  137. this.stream.pipe = des => {
  138. this.res.setHeader
  139. .calledWith('Content-Type', 'text/plain')
  140. .should.equal(true)
  141. done()
  142. }
  143. this.controller.getFile(this.req, this.res)
  144. })
  145. })
  146. })
  147. })
  148. ;[
  149. // None of these should trigger the iOS/html logic
  150. ('x.html-is-rad',
  151. 'html.pdf',
  152. '.html-is-good-for-hidden-files',
  153. 'somefile')
  154. ].forEach(filename => {
  155. describe(`with filename as '${filename}'`, function() {
  156. beforeEach(function() {
  157. this.user_agent = 'A generic browser'
  158. this.file.name = filename
  159. this.req.get = key => {
  160. if (key === 'User-Agent') {
  161. return this.user_agent
  162. }
  163. }
  164. })
  165. ;[('iPhone', 'iPad', 'Firefox', 'Chrome')].forEach(browser => {
  166. describe(`downloaded from ${browser}`, function() {
  167. beforeEach(function() {
  168. this.user_agent = `Some ${browser} thing`
  169. })
  170. it('Should not set the Content-type', function(done) {
  171. this.stream.pipe = des => {
  172. this.res.setHeader
  173. .calledWith('Content-Type', 'text/plain')
  174. .should.equal(false)
  175. done()
  176. }
  177. this.controller.getFile(this.req, this.res)
  178. })
  179. })
  180. })
  181. })
  182. })
  183. })
  184. describe('getFileHead', function() {
  185. it('reports the file size', function(done) {
  186. const expectedFileSize = 99393
  187. this.FileStoreHandler.getFileSize.yields(
  188. new Error('getFileSize: unexpected arguments')
  189. )
  190. this.FileStoreHandler.getFileSize
  191. .withArgs(this.projectId, this.fileId)
  192. .yields(null, expectedFileSize)
  193. this.res.end = () => {
  194. expect(this.res.status.lastCall.args).to.deep.equal([200])
  195. expect(this.res.set.lastCall.args).to.deep.equal([
  196. 'Content-Length',
  197. expectedFileSize
  198. ])
  199. done()
  200. }
  201. this.controller.getFileHead(this.req, this.res)
  202. })
  203. it('returns 404 on NotFoundError', function(done) {
  204. this.FileStoreHandler.getFileSize.yields(new Errors.NotFoundError())
  205. this.res.end = () => {
  206. expect(this.res.status.lastCall.args).to.deep.equal([404])
  207. done()
  208. }
  209. this.controller.getFileHead(this.req, this.res)
  210. })
  211. it('returns 500 on error', function(done) {
  212. this.FileStoreHandler.getFileSize.yields(new Error('boom!'))
  213. this.res.end = () => {
  214. expect(this.res.status.lastCall.args).to.deep.equal([500])
  215. done()
  216. }
  217. this.controller.getFileHead(this.req, this.res)
  218. })
  219. })
  220. })