ConversionTests.js 4.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142
  1. import Client from './helpers/Client.js'
  2. import ClsiApp from './helpers/ClsiApp.js'
  3. import Path from 'node:path'
  4. import fs from 'node:fs'
  5. import { pipeline } from 'node:stream/promises'
  6. import { buffer } from 'node:stream/consumers'
  7. import yauzl from 'yauzl'
  8. import { expect } from 'chai'
  9. import { fetchStreamWithResponse } from '@overleaf/fetch-utils'
  10. import Settings from '@overleaf/settings'
  11. describe('Conversions', function () {
  12. describe('docx conversion', function () {
  13. before(async function () {
  14. await ClsiApp.ensureRunning()
  15. try {
  16. this.body = await Client.compile(this.project_id, this.request)
  17. } catch (error) {
  18. this.error = error
  19. }
  20. })
  21. it('should convert file to docx', async function () {
  22. const sourcePath = Path.join(
  23. import.meta.dirname,
  24. '../fixtures/conversion-source.docx'
  25. )
  26. const outputStream = fs.createWriteStream(
  27. '/tmp/clsi_acceptance_tests_' + crypto.randomUUID() + '.zip'
  28. )
  29. const { stream } = await Client.convertDocument(sourcePath, 'docx')
  30. await pipeline(stream, outputStream)
  31. await new Promise((resolve, reject) => {
  32. yauzl.open(outputStream.path, { lazyEntries: true }, (err, zipfile) => {
  33. if (err) {
  34. return reject(err)
  35. }
  36. zipfile.on('error', reject)
  37. zipfile.on('end', resolve)
  38. zipfile.readEntry()
  39. zipfile.on('entry', entry => {
  40. if (entry.fileName === 'main.tex') {
  41. zipfile.openReadStream(entry, (err, readStream) => {
  42. if (err) {
  43. return reject(err)
  44. }
  45. let data = ''
  46. readStream.on('data', chunk => {
  47. data += chunk.toString()
  48. })
  49. readStream.on('end', () => {
  50. try {
  51. expect(data).to.include('\\begin{document}')
  52. expect(data).to.include(
  53. '\\[x = \\frac{- b \\pm \\sqrt{b^{2} - 4ac}}{2a}\\]'
  54. )
  55. zipfile.readEntry()
  56. } catch (err) {
  57. reject(err)
  58. }
  59. })
  60. })
  61. } else if (entry.fileName === 'media/') {
  62. // Skip the media directory entry
  63. zipfile.readEntry()
  64. } else if (entry.fileName.startsWith('media/')) {
  65. expect(entry.fileName).to.equal('media/image1.png')
  66. zipfile.readEntry()
  67. } else {
  68. reject(new Error('Unexpected file in zip: ' + entry.fileName))
  69. }
  70. })
  71. })
  72. })
  73. })
  74. it('should fail with 422 and surface pandoc stderr when file is not a docx', async function () {
  75. const sourcePath = Path.join(
  76. import.meta.dirname,
  77. '../fixtures/minimal.pdf'
  78. )
  79. const { status, body } = await Client.convertDocument(sourcePath, 'docx')
  80. expect(status).to.equal(422)
  81. expect(body.error).to.include("couldn't unpack docx container")
  82. })
  83. })
  84. describe('project-to-document conversion (responseFormat=json)', function () {
  85. before(async function () {
  86. await ClsiApp.ensureRunning()
  87. })
  88. it('returns ids and serves the docx output via nginx', async function () {
  89. const projectId = Client.randomId()
  90. const userId = '0123456789abcdef01234567'
  91. const request = {
  92. rootResourcePath: 'main.tex',
  93. resources: [
  94. {
  95. path: 'main.tex',
  96. content: `\
  97. \\documentclass{article}
  98. \\begin{document}
  99. Hello world
  100. \\end{document}\
  101. `,
  102. },
  103. ],
  104. }
  105. const { conversionId, buildId, file } =
  106. await Client.convertProjectToDocument(
  107. projectId,
  108. userId,
  109. 'docx',
  110. request,
  111. 'json'
  112. )
  113. expect(conversionId).to.match(
  114. /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/
  115. )
  116. expect(buildId).to.match(/^[0-9a-f]+-[0-9a-f]+$/)
  117. const downloadUrl = new URL(Settings.apis.clsi.downloadHost)
  118. downloadUrl.pathname = `/project/${conversionId}/build/${buildId}/output/${file}`
  119. const { stream, response } = await fetchStreamWithResponse(
  120. downloadUrl.href
  121. )
  122. expect(response.status).to.equal(200)
  123. const body = await buffer(stream)
  124. expect(body.length).to.be.greaterThan(0)
  125. // .docx is a zip archive — verify the PK\x03\x04 magic bytes
  126. expect(body[0]).to.equal(0x50)
  127. expect(body[1]).to.equal(0x4b)
  128. expect(body[2]).to.equal(0x03)
  129. expect(body[3]).to.equal(0x04)
  130. })
  131. })
  132. })