| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436 |
- import Path from 'node:path'
- import sinon from 'sinon'
- import { vi, describe, beforeEach, afterEach, it, expect } from 'vitest'
- const MODULE_PATH = Path.join(
- import.meta.dirname,
- '../../../app/js/ConversionManager'
- )
- describe('ConversionManager', function () {
- beforeEach(async function (ctx) {
- ctx.CommandRunner = {
- promises: {
- run: sinon.stub().resolves({ stdout: '', stderr: '', exitCode: 0 }),
- },
- }
- ctx.lock = {
- release: sinon.stub(),
- }
- ctx.LockManager = {
- acquire: sinon.stub().returns(ctx.lock),
- }
- ctx.Settings = {
- pandocImage: 'mock-pandoc-image',
- conversionTimeoutSeconds: 60,
- path: { compilesDir: '/compiles' },
- }
- ctx.fs = {
- mkdir: sinon.stub().resolves(),
- copyFile: sinon.stub().resolves(),
- rm: sinon.stub().resolves(),
- unlink: sinon.stub().resolves(),
- }
- ctx.conversionId = 'test-conversion-id'
- ctx.conversionDir = '/compiles/test-conversion-id'
- ctx.outputPath = '/compiles/test-conversion-id/output-uuid.zip'
- ctx.uuidStub = sinon
- .stub(globalThis.crypto, 'randomUUID')
- .returns('output-uuid')
- vi.doMock('../../../app/js/LockManager', () => ({
- default: ctx.LockManager,
- }))
- vi.doMock('@overleaf/settings', () => ({
- default: ctx.Settings,
- }))
- vi.doMock('../../../app/js/CommandRunner', () => ({
- default: ctx.CommandRunner,
- }))
- vi.doMock('node:fs/promises', () => ({ default: ctx.fs }))
- ctx.ConversionManager = (await import(MODULE_PATH)).default
- })
- afterEach(function (ctx) {
- ctx.uuidStub.restore()
- })
- describe('convertToLaTeXWithLock', function () {
- describe('with conversionType=docx', function () {
- beforeEach(function (ctx) {
- ctx.inputPath = '/path/to/input.docx'
- })
- describe('file setup and pandoc args', function () {
- beforeEach(async function (ctx) {
- ctx.result =
- await ctx.ConversionManager.promises.convertToLaTeXWithLock(
- ctx.conversionId,
- ctx.inputPath,
- 'docx'
- )
- })
- it('should acquire a lock', async function (ctx) {
- sinon.assert.calledWith(ctx.LockManager.acquire, ctx.conversionDir)
- })
- it('should copy the input file to the conversion directory with docx filename', async function (ctx) {
- sinon.assert.calledWith(ctx.fs.mkdir, ctx.conversionDir, {
- recursive: true,
- })
- sinon.assert.calledWith(
- ctx.fs.copyFile,
- ctx.inputPath,
- Path.join(ctx.conversionDir, 'input.docx')
- )
- })
- it('should convert conversion timeout to milliseconds', async function (ctx) {
- expect(ctx.CommandRunner.promises.run.firstCall.args[4]).toBe(60_000)
- expect(ctx.CommandRunner.promises.run.secondCall.args[4]).toBe(60_000)
- })
- it('should run pandoc with docx args followed by zip', function (ctx) {
- expect(ctx.CommandRunner.promises.run.callCount).toBe(2)
- expect(ctx.CommandRunner.promises.run.firstCall.args).toEqual([
- ctx.conversionId,
- [
- 'pandoc',
- 'input.docx',
- '--output',
- 'main.tex',
- '--to',
- 'latex',
- '--standalone',
- '--extract-media=.',
- '--from',
- 'docx+citations',
- '--citeproc',
- ],
- ctx.conversionDir,
- ctx.Settings.pandocImage,
- 60_000,
- {},
- 'conversions',
- ])
- expect(ctx.CommandRunner.promises.run.secondCall.args).toEqual([
- ctx.conversionId,
- ['zip', '-r', 'output-uuid.zip', '.'],
- ctx.conversionDir,
- ctx.Settings.pandocImage,
- 60_000,
- {},
- 'conversions',
- ])
- })
- })
- describe('successful conversion', function () {
- beforeEach(async function (ctx) {
- ctx.CommandRunner.promises.run.resolves({
- stdout: 'mock-stdout',
- stderr: 'mock-stderr',
- exitCode: 0,
- })
- ctx.result =
- await ctx.ConversionManager.promises.convertToLaTeXWithLock(
- ctx.conversionId,
- ctx.inputPath,
- 'docx'
- )
- })
- it('should remove the source document after conversion', async function (ctx) {
- sinon.assert.calledWith(
- ctx.fs.unlink,
- Path.join(ctx.conversionDir, 'input.docx')
- )
- })
- it('should return the output zip path', function (ctx) {
- expect(ctx.result).toBe(ctx.outputPath)
- })
- it('should release the lock', function (ctx) {
- sinon.assert.called(ctx.lock.release)
- })
- })
- describe('unsuccessful conversion (exitcode)', function () {
- beforeEach(async function (ctx) {
- ctx.CommandRunner.promises.run.resolves({
- stdout: 'mock-stdout',
- stderr: 'mock-stderr',
- exitCode: 63,
- })
- await expect(
- ctx.ConversionManager.promises.convertToLaTeXWithLock(
- ctx.conversionId,
- ctx.inputPath,
- 'docx'
- )
- ).to.be.rejectedWith('pandoc conversion failed')
- })
- it('should remove the entire conversion directory', async function (ctx) {
- sinon.assert.calledWith(ctx.fs.rm, ctx.conversionDir, {
- force: true,
- recursive: true,
- })
- })
- it('should release the lock', function (ctx) {
- sinon.assert.called(ctx.lock.release)
- })
- })
- describe('unsuccessful compression (exitcode)', function () {
- beforeEach(async function (ctx) {
- ctx.CommandRunner.promises.run
- .onFirstCall()
- .resolves({
- stdout: 'mock-pandoc-stdout',
- stderr: 'mock-pandoc-stderr',
- exitCode: 0,
- })
- .onSecondCall()
- .resolves({
- stdout: 'mock-zip-stdout',
- stderr: 'mock-zip-stderr',
- exitCode: 12,
- })
- await expect(
- ctx.ConversionManager.promises.convertToLaTeXWithLock(
- ctx.conversionId,
- ctx.inputPath,
- 'docx'
- )
- ).to.be.rejectedWith('pandoc conversion failed')
- })
- it('should remove the entire conversion directory', async function (ctx) {
- sinon.assert.calledWith(ctx.fs.rm, ctx.conversionDir, {
- force: true,
- recursive: true,
- })
- })
- it('should release the lock', function (ctx) {
- sinon.assert.called(ctx.lock.release)
- })
- })
- describe('unsuccessful conversion (throws)', function () {
- beforeEach(async function (ctx) {
- ctx.CommandRunner.promises.run.rejects(
- new Error('mock conversion error')
- )
- await expect(
- ctx.ConversionManager.promises.convertToLaTeXWithLock(
- ctx.conversionId,
- ctx.inputPath,
- 'docx'
- )
- ).to.be.rejectedWith('pandoc conversion failed')
- })
- it('should remove the entire conversion directory', async function (ctx) {
- sinon.assert.calledWith(ctx.fs.rm, ctx.conversionDir, {
- force: true,
- recursive: true,
- })
- })
- it('should release the lock', function (ctx) {
- sinon.assert.called(ctx.lock.release)
- })
- })
- })
- describe('with conversionType=markdown', function () {
- beforeEach(function (ctx) {
- ctx.inputPath = '/path/to/input.md'
- })
- describe('file setup and pandoc args', function () {
- beforeEach(async function (ctx) {
- ctx.result =
- await ctx.ConversionManager.promises.convertToLaTeXWithLock(
- ctx.conversionId,
- ctx.inputPath,
- 'markdown'
- )
- })
- it('should copy the input file to the conversion directory with md filename', async function (ctx) {
- sinon.assert.calledWith(ctx.fs.mkdir, ctx.conversionDir, {
- recursive: true,
- })
- sinon.assert.calledWith(
- ctx.fs.copyFile,
- ctx.inputPath,
- Path.join(ctx.conversionDir, 'input.md')
- )
- })
- it('should run pandoc with markdown args followed by zip', function (ctx) {
- expect(ctx.CommandRunner.promises.run.callCount).toBe(2)
- expect(ctx.CommandRunner.promises.run.firstCall.args).toEqual([
- ctx.conversionId,
- [
- 'pandoc',
- 'input.md',
- '--output',
- 'main.tex',
- '--to',
- 'latex',
- '--standalone',
- '--from',
- 'markdown',
- ],
- ctx.conversionDir,
- ctx.Settings.pandocImage,
- 60_000,
- {},
- 'conversions',
- ])
- expect(ctx.CommandRunner.promises.run.secondCall.args).toEqual([
- ctx.conversionId,
- ['zip', '-r', 'output-uuid.zip', '.'],
- ctx.conversionDir,
- ctx.Settings.pandocImage,
- 60_000,
- {},
- 'conversions',
- ])
- })
- })
- describe('successful conversion', function () {
- beforeEach(async function (ctx) {
- ctx.CommandRunner.promises.run.resolves({
- stdout: 'mock-stdout',
- stderr: 'mock-stderr',
- exitCode: 0,
- })
- ctx.result =
- await ctx.ConversionManager.promises.convertToLaTeXWithLock(
- ctx.conversionId,
- ctx.inputPath,
- 'markdown'
- )
- })
- it('should remove the source document after conversion', async function (ctx) {
- sinon.assert.calledWith(
- ctx.fs.unlink,
- Path.join(ctx.conversionDir, 'input.md')
- )
- })
- it('should return the output zip path', function (ctx) {
- expect(ctx.result).toBe(ctx.outputPath)
- })
- })
- })
- })
- describe('convertLaTeXToDocumentInDirWithLock', function () {
- describe('successfully', function () {
- beforeEach(async function (ctx) {
- ctx.compileDir = '/compiles/test-compile-dir'
- ctx.rootDocPath = 'main.tex'
- ctx.type = 'docx'
- ctx.extension = 'docx'
- ctx.result =
- await ctx.ConversionManager.promises.convertLaTeXToDocumentInDirWithLock(
- ctx.conversionId,
- ctx.compileDir,
- ctx.rootDocPath,
- ctx.type,
- ctx.extension
- )
- })
- it('should acquire a lock on the compile dir', function (ctx) {
- sinon.assert.calledWith(ctx.LockManager.acquire, ctx.compileDir)
- })
- it('should release the lock', function (ctx) {
- sinon.assert.called(ctx.lock.release)
- })
- it('should run pandoc with correct arguments', function (ctx) {
- expect(ctx.CommandRunner.promises.run.callCount).toBe(1)
- expect(ctx.CommandRunner.promises.run.firstCall.args).toEqual([
- ctx.conversionId,
- [
- 'pandoc',
- ctx.rootDocPath,
- '--output',
- `output-uuid.${ctx.extension}`,
- '--from',
- 'latex',
- '--to',
- ctx.type,
- '--resource-path=.',
- ],
- ctx.compileDir,
- ctx.Settings.pandocImage,
- 60_000,
- {},
- 'conversions',
- ])
- })
- it('should convert conversion timeout to milliseconds', function (ctx) {
- expect(ctx.CommandRunner.promises.run.firstCall.args[4]).toBe(60_000)
- })
- it('should return path to the output document', function (ctx) {
- expect(ctx.result).toBe(
- Path.join(ctx.compileDir, `output-uuid.${ctx.extension}`)
- )
- })
- })
- describe('when pandoc fails (non-zero exit code)', function () {
- it('should reject with an error and release the lock', async function (ctx) {
- ctx.compileDir = '/compiles/test-compile-dir'
- ctx.CommandRunner.promises.run.resolves({
- stdout: 'mock-stdout',
- stderr: 'mock-stderr',
- exitCode: 1,
- })
- await expect(
- ctx.ConversionManager.promises.convertLaTeXToDocumentInDirWithLock(
- ctx.conversionId,
- ctx.compileDir,
- 'main.tex',
- 'docx',
- 'docx'
- )
- ).to.be.rejectedWith('pandoc latex-to-document conversion failed')
- sinon.assert.called(ctx.lock.release)
- })
- })
- })
- })
|