ConversionController.test.js 4.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158
  1. import sinon from 'sinon'
  2. import { vi, describe, it, beforeEach, expect } from 'vitest'
  3. import Path from 'node:path'
  4. import { PassThrough } from 'node:stream'
  5. const MODULE_PATH = Path.join(
  6. import.meta.dirname,
  7. '../../../app/js/ConversionController'
  8. )
  9. describe('ConversionController', function () {
  10. beforeEach(async function (ctx) {
  11. ctx.conversionDir = '/path/to/conversion/result'
  12. ctx.zipPath = '/path/to/conversion/result/output.zip'
  13. ctx.zipStat = { size: 1234 }
  14. ctx.Settings = {
  15. enablePandocConversions: true,
  16. }
  17. ctx.ConversionManager = {
  18. promises: {
  19. convertDocxToLaTeXWithLock: sinon.stub().resolves(ctx.zipPath),
  20. },
  21. }
  22. ctx.fs = {
  23. stat: sinon.stub().resolves(ctx.zipStat),
  24. unlink: sinon.stub().resolves(),
  25. rm: sinon.stub().resolves(),
  26. }
  27. ctx.readStream = new PassThrough()
  28. ctx.fsSync = {
  29. createReadStream: sinon.stub().returns(ctx.readStream),
  30. }
  31. ctx.pipeline = sinon.stub().resolves()
  32. vi.doMock('node:fs/promises', () => ({
  33. default: ctx.fs,
  34. }))
  35. vi.doMock('node:fs', () => ({
  36. default: ctx.fsSync,
  37. }))
  38. vi.doMock('node:stream/promises', () => ({
  39. pipeline: ctx.pipeline,
  40. }))
  41. vi.doMock('@overleaf/settings', () => ({
  42. default: ctx.Settings,
  43. }))
  44. vi.doMock('../../../app/js/ConversionManager', () => ({
  45. default: ctx.ConversionManager,
  46. }))
  47. ctx.res = new PassThrough()
  48. ctx.res.attachment = sinon.stub()
  49. ctx.res.setHeader = sinon.stub()
  50. ctx.ConversionController = (await import(MODULE_PATH)).default
  51. })
  52. describe('convertDocxToLaTeX', function () {
  53. describe('when conversions are disabled', function () {
  54. beforeEach(async function (ctx) {
  55. ctx.Settings.enablePandocConversions = false
  56. ctx.req = {
  57. file: { path: '/path/to/uploaded/file.docx' },
  58. }
  59. ctx.res.sendStatus = sinon.stub()
  60. await ctx.ConversionController.convertDocxToLaTeX(ctx.req, ctx.res)
  61. })
  62. it('should remove the uploaded file', function (ctx) {
  63. sinon.assert.calledWith(ctx.fs.unlink, ctx.req.file.path)
  64. })
  65. it('should return 404', function (ctx) {
  66. sinon.assert.calledWith(ctx.res.sendStatus, 404)
  67. })
  68. it('should not call the conversion manager', function (ctx) {
  69. sinon.assert.notCalled(
  70. ctx.ConversionManager.promises.convertDocxToLaTeXWithLock
  71. )
  72. })
  73. })
  74. describe('successfully', function () {
  75. beforeEach(async function (ctx) {
  76. ctx.req = {
  77. file: { path: '/path/to/uploaded/file.docx' },
  78. }
  79. await ctx.ConversionController.convertDocxToLaTeX(ctx.req, ctx.res)
  80. })
  81. it('should call the conversion manager with the uploaded file path', function (ctx) {
  82. sinon.assert.calledWith(
  83. ctx.ConversionManager.promises.convertDocxToLaTeXWithLock,
  84. sinon.match(
  85. /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/
  86. ),
  87. ctx.req.file.path
  88. )
  89. })
  90. it('should look up the generated zip file size', function (ctx) {
  91. sinon.assert.calledWith(ctx.fs.stat, ctx.zipPath)
  92. })
  93. it('should set the response headers for a zip file download', function (ctx) {
  94. sinon.assert.calledWith(
  95. ctx.res.setHeader,
  96. 'Content-Length',
  97. ctx.zipStat.size
  98. )
  99. sinon.assert.calledWith(ctx.res.attachment, 'conversion.zip')
  100. sinon.assert.calledWith(
  101. ctx.res.setHeader,
  102. 'X-Content-Type-Options',
  103. 'nosniff'
  104. )
  105. })
  106. it('should stream the generated zip file to the response', function (ctx) {
  107. sinon.assert.calledWith(ctx.fsSync.createReadStream, ctx.zipPath)
  108. sinon.assert.calledWith(ctx.pipeline, ctx.readStream, ctx.res)
  109. })
  110. it('should clean up the generated zip file', function (ctx) {
  111. sinon.assert.calledWith(ctx.fs.rm, ctx.conversionDir)
  112. })
  113. })
  114. describe('unsuccessfully', function () {
  115. describe('on streaming error', function () {
  116. it('should propagate the error and still clean up', async function (ctx) {
  117. ctx.pipeline.rejects(new Error('mock stream error'))
  118. const res = new PassThrough()
  119. res.attachment = sinon.stub()
  120. res.setHeader = sinon.stub()
  121. const req = { file: { path: '/path/to/uploaded/file.docx' } }
  122. await expect(
  123. ctx.ConversionController.convertDocxToLaTeX(req, res)
  124. ).to.be.rejectedWith('mock stream error')
  125. sinon.assert.calledWith(ctx.fs.rm, ctx.conversionDir)
  126. })
  127. })
  128. })
  129. })
  130. })