mirror of
https://github.com/yu-i-i/overleaf-cep.git
synced 2026-05-23 17:19:37 +02:00
[WEB + CLSI] Download as markdown GitOrigin-RevId: 181eddf2513e9c5edacbab37e93f9cac2191ee1a
357 lines
11 KiB
JavaScript
357 lines
11 KiB
JavaScript
import { describe, expect, vi, beforeEach } from 'vitest'
|
|
import sinon from 'sinon'
|
|
import FormData from 'form-data'
|
|
import { FileTooLargeError } from '../../../../app/src/Features/Errors/Errors.js'
|
|
|
|
const MODULE_PATH =
|
|
'../../../../app/src/Features/Uploads/DocumentConversionManager.mjs'
|
|
|
|
describe('DocumentConversionManager', function () {
|
|
beforeEach(async function (ctx) {
|
|
ctx.fs = {
|
|
createReadStream: sinon.stub().returns('mocked-read-stream'),
|
|
createWriteStream: sinon.stub().returns('mocked-write-stream'),
|
|
}
|
|
|
|
ctx.fsPromises = {
|
|
unlink: sinon.stub().resolves(),
|
|
}
|
|
|
|
ctx.fetchUtils = {
|
|
fetchStreamWithResponse: sinon.stub().resolves(),
|
|
}
|
|
|
|
ctx.nodeStream = {
|
|
pipeline: sinon.stub().resolves(),
|
|
}
|
|
|
|
ctx.CompileManager = {
|
|
promises: {
|
|
_getUserCompileLimits: sinon.stub().resolves({
|
|
compileBackendClass: 'test-backend-class',
|
|
compileGroup: 'test-compile-group',
|
|
}),
|
|
},
|
|
}
|
|
|
|
ctx.Settings = {
|
|
maxUploadSize: 100,
|
|
path: {
|
|
dumpFolder: '/path/to/dump/folder',
|
|
},
|
|
apis: {
|
|
clsi: {
|
|
url: 'http://mock-clsi-url',
|
|
},
|
|
},
|
|
}
|
|
|
|
vi.doMock('node:fs', () => ({
|
|
default: ctx.fs,
|
|
}))
|
|
|
|
vi.doMock('node:fs/promises', () => ({
|
|
default: ctx.fsPromises,
|
|
}))
|
|
|
|
vi.doMock('@overleaf/fetch-utils', () => ({
|
|
fetchStreamWithResponse: ctx.fetchUtils.fetchStreamWithResponse,
|
|
}))
|
|
|
|
vi.doMock('node:stream/promises', () => ({
|
|
pipeline: ctx.nodeStream.pipeline,
|
|
}))
|
|
|
|
vi.doMock('@overleaf/settings', () => ({
|
|
default: ctx.Settings,
|
|
}))
|
|
|
|
vi.doMock(
|
|
'../../../../app/src/Features/Compile/CompileManager.mjs',
|
|
() => ({
|
|
default: ctx.CompileManager,
|
|
})
|
|
)
|
|
|
|
ctx.ClsiManager = {
|
|
promises: {
|
|
buildDocumentConversionRequest: sinon
|
|
.stub()
|
|
.resolves({ some: 'clsi-request' }),
|
|
},
|
|
}
|
|
|
|
vi.doMock('../../../../app/src/Features/Compile/ClsiManager.mjs', () => ({
|
|
default: ctx.ClsiManager,
|
|
}))
|
|
|
|
ctx.DocumentConversionManager = (await import(MODULE_PATH)).default
|
|
})
|
|
|
|
describe('convertDocumentToLaTeXZipArchive', function () {
|
|
describe('with conversionType=docx', function () {
|
|
describe('successfully', function () {
|
|
beforeEach(async function (ctx) {
|
|
ctx.path = '/path/to/input.docx'
|
|
ctx.userId = 'test-user-id'
|
|
ctx.response = {
|
|
headers: {
|
|
get: sinon.stub().returns(null),
|
|
},
|
|
}
|
|
ctx.response.headers.get.withArgs('Content-Length').returns('50')
|
|
|
|
ctx.fetchUtils.fetchStreamWithResponse.resolves({
|
|
stream: 'mocked-fetch-stream',
|
|
response: ctx.response,
|
|
})
|
|
|
|
ctx.result =
|
|
await ctx.DocumentConversionManager.promises.convertDocumentToLaTeXZipArchive(
|
|
ctx.path,
|
|
ctx.userId,
|
|
'docx'
|
|
)
|
|
})
|
|
|
|
it('should call fetchStreamWithResponse with the correct URL and form data', function (ctx) {
|
|
const expectedUrl = new URL(ctx.Settings.apis.clsi.url)
|
|
// TODO: revert this to '/convert/document-to-latex' once the deploy is done (PR #32857)
|
|
expectedUrl.pathname = '/convert/docx-to-latex'
|
|
expectedUrl.searchParams.set(
|
|
'compileBackendClass',
|
|
'test-backend-class'
|
|
)
|
|
expectedUrl.searchParams.set('compileGroup', 'test-compile-group')
|
|
expectedUrl.searchParams.set('type', 'docx')
|
|
|
|
sinon.assert.calledWith(
|
|
ctx.fetchUtils.fetchStreamWithResponse,
|
|
sinon.match(url => url.toString() === expectedUrl.toString()),
|
|
{
|
|
method: 'POST',
|
|
body: sinon.match.instanceOf(FormData),
|
|
signal: sinon.match.instanceOf(AbortSignal),
|
|
}
|
|
)
|
|
})
|
|
|
|
it('should pipe result into the output file', function (ctx) {
|
|
sinon.assert.calledWith(
|
|
ctx.nodeStream.pipeline,
|
|
'mocked-fetch-stream',
|
|
'mocked-write-stream'
|
|
)
|
|
})
|
|
|
|
it('should return a path to the output file', function (ctx) {
|
|
expect(ctx.result).to.match(
|
|
/\/path\/to\/dump\/folder\/[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}_document-conversion\.zip/
|
|
)
|
|
})
|
|
})
|
|
})
|
|
|
|
describe('with conversionType=markdown', function () {
|
|
beforeEach(async function (ctx) {
|
|
ctx.path = '/path/to/input.md'
|
|
ctx.userId = 'test-user-id'
|
|
ctx.response = {
|
|
headers: {
|
|
get: sinon.stub().returns(null),
|
|
},
|
|
}
|
|
ctx.response.headers.get.withArgs('Content-Length').returns('50')
|
|
|
|
ctx.fetchUtils.fetchStreamWithResponse.resolves({
|
|
stream: 'mocked-fetch-stream',
|
|
response: ctx.response,
|
|
})
|
|
|
|
ctx.result =
|
|
await ctx.DocumentConversionManager.promises.convertDocumentToLaTeXZipArchive(
|
|
ctx.path,
|
|
ctx.userId,
|
|
'markdown'
|
|
)
|
|
})
|
|
|
|
it('should call fetchStreamWithResponse with the correct URL including markdown type', function (ctx) {
|
|
const expectedUrl = new URL(ctx.Settings.apis.clsi.url)
|
|
expectedUrl.pathname = '/convert/document-to-latex'
|
|
expectedUrl.searchParams.set(
|
|
'compileBackendClass',
|
|
'test-backend-class'
|
|
)
|
|
expectedUrl.searchParams.set('compileGroup', 'test-compile-group')
|
|
expectedUrl.searchParams.set('type', 'markdown')
|
|
|
|
sinon.assert.calledWith(
|
|
ctx.fetchUtils.fetchStreamWithResponse,
|
|
sinon.match(url => url.toString() === expectedUrl.toString()),
|
|
{
|
|
method: 'POST',
|
|
body: sinon.match.instanceOf(FormData),
|
|
signal: sinon.match.instanceOf(AbortSignal),
|
|
}
|
|
)
|
|
})
|
|
|
|
it('should pipe result into the output file', function (ctx) {
|
|
sinon.assert.calledWith(
|
|
ctx.nodeStream.pipeline,
|
|
'mocked-fetch-stream',
|
|
'mocked-write-stream'
|
|
)
|
|
})
|
|
|
|
it('should return a path to the output file', function (ctx) {
|
|
expect(ctx.result).to.match(
|
|
/\/path\/to\/dump\/folder\/[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}_document-conversion\.zip/
|
|
)
|
|
})
|
|
})
|
|
|
|
describe('when an error occurs during conversion', function () {
|
|
beforeEach(async function (ctx) {
|
|
ctx.path = '/path/to/input.docx'
|
|
ctx.userId = 'test-user-id'
|
|
|
|
ctx.fetchUtils.fetchStreamWithResponse.rejects(
|
|
new Error('Conversion failed')
|
|
)
|
|
|
|
await expect(
|
|
ctx.DocumentConversionManager.promises.convertDocumentToLaTeXZipArchive(
|
|
ctx.path,
|
|
ctx.userId,
|
|
'docx'
|
|
)
|
|
).to.be.rejectedWith('document conversion failed')
|
|
})
|
|
|
|
it('should attempt to clean up the output file', function (ctx) {
|
|
sinon.assert.calledWith(
|
|
ctx.fsPromises.unlink,
|
|
sinon.match(
|
|
/\/path\/to\/dump\/folder\/[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}_document-conversion\.zip/
|
|
)
|
|
)
|
|
})
|
|
})
|
|
|
|
describe('when the converted archive is too large', function () {
|
|
beforeEach(async function (ctx) {
|
|
ctx.path = '/path/to/input.docx'
|
|
ctx.userId = 'test-user-id'
|
|
ctx.stream = {
|
|
destroy: sinon.stub(),
|
|
}
|
|
ctx.response = {
|
|
headers: {
|
|
get: sinon.stub(),
|
|
},
|
|
}
|
|
ctx.response.headers.get.withArgs('Content-Length').returns('150')
|
|
|
|
ctx.fetchUtils.fetchStreamWithResponse.resolves({
|
|
stream: ctx.stream,
|
|
response: ctx.response,
|
|
})
|
|
|
|
await expect(
|
|
ctx.DocumentConversionManager.promises.convertDocumentToLaTeXZipArchive(
|
|
ctx.path,
|
|
ctx.userId,
|
|
'docx'
|
|
)
|
|
).to.be.rejectedWith(sinon.match.instanceOf(FileTooLargeError))
|
|
})
|
|
|
|
it('should abort the request', function (ctx) {
|
|
expect(
|
|
ctx.fetchUtils.fetchStreamWithResponse.firstCall.args[1].signal
|
|
.aborted
|
|
).to.equal(true)
|
|
})
|
|
|
|
it('should destroy the response stream', function (ctx) {
|
|
sinon.assert.calledOnce(ctx.stream.destroy)
|
|
})
|
|
|
|
it('should not write the oversized archive to disk', function (ctx) {
|
|
sinon.assert.notCalled(ctx.fs.createWriteStream)
|
|
sinon.assert.notCalled(ctx.nodeStream.pipeline)
|
|
})
|
|
|
|
it('should attempt to clean up the output path', function (ctx) {
|
|
sinon.assert.calledWith(
|
|
ctx.fsPromises.unlink,
|
|
sinon.match(
|
|
/\/path\/to\/dump\/folder\/[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}_document-conversion\.zip/
|
|
)
|
|
)
|
|
})
|
|
})
|
|
})
|
|
|
|
describe('convertProjectToDocument', function () {
|
|
beforeEach(function (ctx) {
|
|
ctx.projectId = 'test-project-id'
|
|
ctx.userId = 'test-user-id'
|
|
ctx.type = 'docx'
|
|
ctx.mockStream = { destroy: sinon.stub() }
|
|
ctx.response = {
|
|
headers: { get: sinon.stub().returns(null) },
|
|
}
|
|
ctx.response.headers.get.withArgs('Content-Length').returns('50')
|
|
ctx.fetchUtils.fetchStreamWithResponse.resolves({
|
|
stream: ctx.mockStream,
|
|
response: ctx.response,
|
|
})
|
|
})
|
|
|
|
describe('successfully converts the document', function () {
|
|
beforeEach(async function (ctx) {
|
|
ctx.result =
|
|
await ctx.DocumentConversionManager.promises.convertProjectToDocument(
|
|
ctx.projectId,
|
|
ctx.userId,
|
|
ctx.type
|
|
)
|
|
})
|
|
|
|
it('should build the CLSI document conversion request', function (ctx) {
|
|
sinon.assert.calledWith(
|
|
ctx.ClsiManager.promises.buildDocumentConversionRequest,
|
|
ctx.projectId
|
|
)
|
|
})
|
|
|
|
it('should call CLSI with the correct URL', function (ctx) {
|
|
const expectedUrl = new URL(ctx.Settings.apis.clsi.url)
|
|
expectedUrl.pathname = `/project/${ctx.projectId}/user/${ctx.userId}/download/project-to-document`
|
|
expectedUrl.searchParams.set('type', ctx.type)
|
|
expectedUrl.searchParams.set(
|
|
'compileBackendClass',
|
|
'test-backend-class'
|
|
)
|
|
expectedUrl.searchParams.set('compileGroup', 'test-compile-group')
|
|
|
|
sinon.assert.calledWith(
|
|
ctx.fetchUtils.fetchStreamWithResponse,
|
|
sinon.match(url => url.toString() === expectedUrl.toString()),
|
|
{ method: 'POST', json: { some: 'clsi-request' } }
|
|
)
|
|
})
|
|
|
|
it('should return the stream and content length', function (ctx) {
|
|
expect(ctx.result).to.deep.equal({
|
|
stream: ctx.mockStream,
|
|
contentLength: 50,
|
|
})
|
|
})
|
|
})
|
|
})
|
|
})
|