FileHandler.test.js 8.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283
  1. import sinon from 'sinon'
  2. import { beforeEach, describe, expect, it, vi } from 'vitest'
  3. import { ObjectId } from 'mongodb'
  4. import OriginalSettings from '@overleaf/settings'
  5. const modulePath = '../../../app/js/FileHandler.js'
  6. describe('FileHandler', () => {
  7. let PersistorManager,
  8. LocalFileWriter,
  9. FileConverter,
  10. KeyBuilder,
  11. ImageOptimiser,
  12. FileHandler,
  13. Settings,
  14. fs
  15. const bucket = 'my_bucket'
  16. const key = `${new ObjectId()}/${new ObjectId()}`
  17. const convertedFolderKey = `${new ObjectId()}/${new ObjectId()}`
  18. const sourceStream = 'sourceStream'
  19. const convertedKey = 'convertedKey'
  20. const redirectUrl = 'https://wombat.potato/giraffe'
  21. const readStream = {
  22. stream: 'readStream',
  23. on: sinon.stub(),
  24. }
  25. beforeEach(async () => {
  26. PersistorManager = {
  27. getObjectStream: sinon.stub().resolves(sourceStream),
  28. getRedirectUrl: sinon.stub().resolves(redirectUrl),
  29. checkIfObjectExists: sinon.stub().resolves(),
  30. deleteObject: sinon.stub().resolves(),
  31. deleteDirectory: sinon.stub().resolves(),
  32. sendStream: sinon.stub().resolves(),
  33. insertFile: sinon.stub().resolves(),
  34. sendFile: sinon.stub().resolves(),
  35. }
  36. LocalFileWriter = {
  37. // the callback style is used for detached cleanup calls
  38. deleteFile: sinon.stub().yields(),
  39. promises: {
  40. writeStream: sinon.stub().resolves(),
  41. deleteFile: sinon.stub().resolves(),
  42. },
  43. }
  44. FileConverter = {
  45. promises: {
  46. convert: sinon.stub().resolves(),
  47. thumbnail: sinon.stub().resolves(),
  48. preview: sinon.stub().resolves(),
  49. },
  50. }
  51. KeyBuilder = {
  52. addCachingToKey: sinon.stub().returns(convertedKey),
  53. getConvertedFolderKey: sinon.stub().returns(convertedFolderKey),
  54. }
  55. ImageOptimiser = {
  56. promises: {
  57. compressPng: sinon.stub().resolves(),
  58. },
  59. }
  60. Settings = {
  61. ...OriginalSettings,
  62. filestore: {
  63. stores: {
  64. ...(OriginalSettings.filestore?.stores ?? {}),
  65. template_files: 'template_files',
  66. },
  67. },
  68. }
  69. fs = {
  70. createReadStream: sinon.stub().returns(readStream),
  71. }
  72. vi.doMock('../../../app/js/PersistorManager', () => ({
  73. default: PersistorManager,
  74. }))
  75. vi.doMock('../../../app/js/LocalFileWriter', () => ({
  76. default: LocalFileWriter,
  77. }))
  78. vi.doMock('../../../app/js/FileConverter', () => ({
  79. default: FileConverter,
  80. }))
  81. vi.doMock('../../../app/js/KeyBuilder', () => ({
  82. default: KeyBuilder,
  83. }))
  84. vi.doMock('../../../app/js/ImageOptimiser', () => ({
  85. default: ImageOptimiser,
  86. }))
  87. vi.doMock('@overleaf/settings', () => {
  88. return {
  89. default: Settings,
  90. }
  91. })
  92. vi.doMock('@overleaf/metrics', () => ({
  93. default: {
  94. gauge: sinon.stub(),
  95. Timer: sinon.stub().returns({ done: sinon.stub() }),
  96. },
  97. }))
  98. vi.doMock('node:fs', () => ({
  99. default: fs,
  100. }))
  101. FileHandler = (await import(modulePath)).default
  102. FileHandler._TESTONLYSwapPersistorManager(PersistorManager)
  103. })
  104. describe('insertFile', () => {
  105. const stream = 'stream'
  106. it('should send file to the filestore', async () => {
  107. await FileHandler.promises.insertFile(bucket, key, stream)
  108. expect(PersistorManager.sendStream).to.have.been.calledWith(
  109. bucket,
  110. key,
  111. stream
  112. )
  113. })
  114. it('should not make a delete request for the convertedKey folder', async () => {
  115. await FileHandler.promises.insertFile(bucket, key, stream)
  116. expect(PersistorManager.deleteDirectory).not.to.have.been.called
  117. })
  118. it('should accept templates-api key format', async () => {
  119. KeyBuilder.getConvertedFolderKey.returns(
  120. '5ecba29f1a294e007d0bccb4/v/0/pdf'
  121. )
  122. await FileHandler.promises.insertFile(bucket, key, stream)
  123. })
  124. it('should throw an error when the key is in the wrong format', async () => {
  125. KeyBuilder.getConvertedFolderKey.returns('wombat')
  126. expect(FileHandler.promises.insertFile(bucket, key, stream)).to.be
  127. .rejected
  128. })
  129. })
  130. describe('getFile', () => {
  131. it('should return the source stream no format or style are defined', async () => {
  132. const stream = await FileHandler.promises.getFile(bucket, key, null)
  133. expect(stream).to.equal(sourceStream)
  134. })
  135. it('should pass options through to PersistorManager', async () => {
  136. const options = { start: 0, end: 8 }
  137. await FileHandler.promises.getFile(bucket, key, options)
  138. expect(PersistorManager.getObjectStream).to.have.been.calledWith(
  139. bucket,
  140. key,
  141. options
  142. )
  143. })
  144. describe('when a format is defined', () => {
  145. let result
  146. describe('when the file is not cached', () => {
  147. beforeEach(async () => {
  148. const stream = await FileHandler.promises.getFile(bucket, key, {
  149. format: 'png',
  150. })
  151. result = { stream }
  152. })
  153. it('should convert the file', () => {
  154. expect(FileConverter.promises.convert).to.have.been.called
  155. })
  156. it('should compress the converted file', () => {
  157. expect(ImageOptimiser.promises.compressPng).to.have.been.called
  158. })
  159. it('should return the the converted stream', () => {
  160. expect(result.stream).to.equal(readStream)
  161. expect(PersistorManager.getObjectStream).to.have.been.calledWith(
  162. bucket,
  163. key
  164. )
  165. })
  166. })
  167. describe('when the file is cached', () => {
  168. beforeEach(async () => {
  169. PersistorManager.checkIfObjectExists = sinon.stub().resolves(true)
  170. const stream = await FileHandler.promises.getFile(bucket, key, {
  171. format: 'png',
  172. })
  173. result = { stream }
  174. })
  175. it('should not convert the file', () => {
  176. expect(FileConverter.promises.convert).not.to.have.been.called
  177. })
  178. it('should not compress the converted file again', () => {
  179. expect(ImageOptimiser.promises.compressPng).not.to.have.been.called
  180. })
  181. it('should return the cached stream', () => {
  182. expect(result.stream).to.equal(sourceStream)
  183. expect(PersistorManager.getObjectStream).to.have.been.calledWith(
  184. bucket,
  185. convertedKey
  186. )
  187. })
  188. })
  189. })
  190. describe('when a style is defined', () => {
  191. it('generates a thumbnail when requested', async () => {
  192. await FileHandler.promises.getFile(bucket, key, { style: 'thumbnail' })
  193. expect(FileConverter.promises.thumbnail).to.have.been.called
  194. expect(FileConverter.promises.preview).not.to.have.been.called
  195. })
  196. it('generates a preview when requested', async () => {
  197. await FileHandler.promises.getFile(bucket, key, { style: 'preview' })
  198. expect(FileConverter.promises.thumbnail).not.to.have.been.called
  199. expect(FileConverter.promises.preview).to.have.been.called
  200. })
  201. })
  202. })
  203. describe('getRedirectUrl', () => {
  204. beforeEach(() => {
  205. Settings.filestore = {
  206. ...OriginalSettings.filestore,
  207. allowRedirects: true,
  208. stores: {
  209. ...OriginalSettings.filestore.stores,
  210. userFiles: bucket,
  211. },
  212. }
  213. })
  214. it('should return a redirect url', async () => {
  215. const url = await FileHandler.promises.getRedirectUrl(bucket, key)
  216. expect(url).to.equal(redirectUrl)
  217. })
  218. it('should call the persistor to get a redirect url', async () => {
  219. await FileHandler.promises.getRedirectUrl(bucket, key)
  220. expect(PersistorManager.getRedirectUrl).to.have.been.calledWith(
  221. bucket,
  222. key
  223. )
  224. })
  225. it('should return null if options are supplied', async () => {
  226. const url = await FileHandler.promises.getRedirectUrl(bucket, key, {
  227. start: 100,
  228. end: 200,
  229. })
  230. expect(url).to.be.null
  231. })
  232. it('should return null if the bucket is not one of the defined ones', async () => {
  233. const url = await FileHandler.promises.getRedirectUrl(
  234. 'a_different_bucket',
  235. key
  236. )
  237. expect(url).to.be.null
  238. })
  239. it('should return null if redirects are not enabled', async () => {
  240. Settings.filestore.allowRedirects = false
  241. const url = await FileHandler.promises.getRedirectUrl(bucket, key)
  242. expect(url).to.be.null
  243. })
  244. })
  245. })