ProjectUploadController.test.mjs 22 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722
  1. // TODO: This file was created by bulk-decaffeinate.
  2. // Fix any style issues and re-enable lint.
  3. /*
  4. * decaffeinate suggestions:
  5. * DS206: Consider reworking classes to avoid initClass
  6. * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
  7. */
  8. import { expect, vi } from 'vitest'
  9. import sinon from 'sinon'
  10. import MockRequest from '../helpers/MockRequest.mjs'
  11. import MockResponse from '../helpers/MockResponse.mjs'
  12. import ArchiveErrors from '../../../../app/src/Features/Uploads/ArchiveErrors.mjs'
  13. import { FileTooLargeError } from '../../../../app/src/Features/Errors/Errors.js'
  14. const modulePath =
  15. '../../../../app/src/Features/Uploads/ProjectUploadController.mjs'
  16. describe('ProjectUploadController', function () {
  17. beforeEach(async function (ctx) {
  18. let Timer
  19. ctx.req = new MockRequest(vi)
  20. ctx.res = new MockResponse(vi)
  21. ctx.user_id = 'user-id-123'
  22. ctx.metrics = {
  23. Timer: (Timer = (function () {
  24. Timer = class Timer {
  25. static initClass() {
  26. this.prototype.done = sinon.stub()
  27. }
  28. }
  29. Timer.initClass()
  30. return Timer
  31. })()),
  32. }
  33. ctx.SessionManager = {
  34. getLoggedInUserId: sinon.stub().returns(ctx.user_id),
  35. }
  36. ctx.ProjectLocator = {
  37. promises: {},
  38. }
  39. ctx.EditorController = {
  40. promises: {},
  41. }
  42. ctx.ProjectOptionsHandler = {
  43. promises: {
  44. setCompiler: sinon.stub().resolves(),
  45. },
  46. }
  47. ctx.DocumentConversionManager = {
  48. promises: {
  49. convertDocumentToLaTeXZipArchive: sinon.stub(),
  50. },
  51. }
  52. vi.doMock('multer', () => ({
  53. default: sinon.stub(),
  54. }))
  55. vi.doMock('@overleaf/settings', () => ({
  56. default: { path: {} },
  57. }))
  58. vi.doMock(
  59. '../../../../app/src/Features/Uploads/ProjectUploadManager',
  60. () => ({
  61. default: (ctx.ProjectUploadManager = { promises: {} }),
  62. })
  63. )
  64. vi.doMock(
  65. '../../../../app/src/Features/Uploads/FileSystemImportManager',
  66. () => ({
  67. default: (ctx.FileSystemImportManager = {}),
  68. })
  69. )
  70. vi.doMock('@overleaf/metrics', () => ({
  71. default: ctx.metrics,
  72. }))
  73. vi.doMock(
  74. '../../../../app/src/Features/Authentication/SessionManager',
  75. () => ({
  76. default: ctx.SessionManager,
  77. })
  78. )
  79. vi.doMock(
  80. '../../../../app/src/Features/Uploads/ArchiveErrors',
  81. () => ArchiveErrors
  82. )
  83. vi.doMock('../../../../app/src/Features/Project/ProjectLocator', () => ({
  84. default: ctx.ProjectLocator,
  85. }))
  86. vi.doMock('../../../../app/src/Features/Editor/EditorController', () => ({
  87. default: ctx.EditorController,
  88. }))
  89. vi.doMock(
  90. '../../../../app/src/Features/Project/ProjectOptionsHandler',
  91. () => ({
  92. default: ctx.ProjectOptionsHandler,
  93. })
  94. )
  95. vi.doMock(
  96. '../../../../app/src/Features/Uploads/DocumentConversionManager.mjs',
  97. () => ({
  98. default: ctx.DocumentConversionManager,
  99. })
  100. )
  101. ctx.AnalyticsManager = {
  102. recordEventForUserInBackground: sinon.stub(),
  103. }
  104. vi.doMock(
  105. '../../../../app/src/Features/Analytics/AnalyticsManager.mjs',
  106. () => ({
  107. default: ctx.AnalyticsManager,
  108. })
  109. )
  110. vi.doMock('node:fs', () => ({
  111. default: (ctx.fs = {}),
  112. }))
  113. vi.doMock('node:fs/promises', () => ({
  114. default: (ctx.fsPromises = {}),
  115. }))
  116. ctx.ProjectUploadController = (await import(modulePath)).default
  117. })
  118. describe('uploadProject', function () {
  119. beforeEach(function (ctx) {
  120. ctx.path = '/path/to/file/on/disk.zip'
  121. ctx.fileName = 'filename.zip'
  122. ctx.req.file = {
  123. path: ctx.path,
  124. }
  125. ctx.req.body = {
  126. name: ctx.fileName,
  127. }
  128. ctx.req.session = {
  129. user: {
  130. _id: ctx.user_id,
  131. },
  132. }
  133. ctx.project = { _id: (ctx.project_id = 'project-id-123') }
  134. ctx.fs.unlink = sinon.stub()
  135. ctx.fsPromises.unlink = sinon.stub().resolves()
  136. })
  137. describe('successfully', function () {
  138. beforeEach(function (ctx) {
  139. ctx.ProjectUploadManager.createProjectFromZipArchive = sinon
  140. .stub()
  141. .callsArgWith(3, null, ctx.project)
  142. ctx.ProjectUploadController.uploadProject(ctx.req, ctx.res)
  143. })
  144. it('should create a project owned by the logged in user', function (ctx) {
  145. ctx.ProjectUploadManager.createProjectFromZipArchive
  146. .calledWith(ctx.user_id)
  147. .should.equal(true)
  148. })
  149. it('should create a project with the same name as the zip archive', function (ctx) {
  150. ctx.ProjectUploadManager.createProjectFromZipArchive
  151. .calledWith(sinon.match.any, 'filename', sinon.match.any)
  152. .should.equal(true)
  153. })
  154. it('should create a project from the zip archive', function (ctx) {
  155. ctx.ProjectUploadManager.createProjectFromZipArchive
  156. .calledWith(sinon.match.any, sinon.match.any, ctx.path)
  157. .should.equal(true)
  158. })
  159. it('should return a successful response to the FileUploader client', function (ctx) {
  160. expect(ctx.res.body).to.deep.equal(
  161. JSON.stringify({
  162. success: true,
  163. project_id: ctx.project_id,
  164. })
  165. )
  166. })
  167. it('should record the time taken to do the upload', function (ctx) {
  168. ctx.metrics.Timer.prototype.done.called.should.equal(true)
  169. })
  170. it('should remove the uploaded file', function (ctx) {
  171. ctx.fs.unlink.calledWith(ctx.path).should.equal(true)
  172. })
  173. })
  174. describe('when ProjectUploadManager.createProjectFromZipArchive fails', function () {
  175. beforeEach(function (ctx) {
  176. ctx.ProjectUploadManager.createProjectFromZipArchive = sinon
  177. .stub()
  178. .callsArgWith(3, new Error('Something went wrong'), ctx.project)
  179. ctx.ProjectUploadController.uploadProject(ctx.req, ctx.res)
  180. })
  181. it('should return a failed response to the FileUploader client', function (ctx) {
  182. expect(ctx.res.body).to.deep.equal(
  183. JSON.stringify({ success: false, error: 'upload_failed' })
  184. )
  185. })
  186. it('should remove the uploaded file', function (ctx) {
  187. ctx.fs.unlink.calledWith(ctx.path).should.equal(true)
  188. })
  189. })
  190. describe('when ProjectUploadManager.createProjectFromZipArchive reports the file as invalid', function () {
  191. beforeEach(function (ctx) {
  192. ctx.ProjectUploadManager.createProjectFromZipArchive = sinon
  193. .stub()
  194. .callsArgWith(
  195. 3,
  196. new ArchiveErrors.ZipContentsTooLargeError(),
  197. ctx.project
  198. )
  199. ctx.ProjectUploadController.uploadProject(ctx.req, ctx.res)
  200. })
  201. it('should return the reported error to the FileUploader client', function (ctx) {
  202. expect(JSON.parse(ctx.res.body)).to.deep.equal({
  203. success: false,
  204. error: 'zip_contents_too_large',
  205. })
  206. })
  207. it("should return an 'unprocessable entity' status code", function (ctx) {
  208. expect(ctx.res.statusCode).to.equal(422)
  209. })
  210. it('should remove the uploaded file', function (ctx) {
  211. ctx.fs.unlink.calledWith(ctx.path).should.equal(true)
  212. })
  213. })
  214. })
  215. describe('uploadFile', function () {
  216. beforeEach(function (ctx) {
  217. ctx.project_id = 'project-id-123'
  218. ctx.folder_id = 'folder-id-123'
  219. ctx.path = '/path/to/file/on/disk.png'
  220. ctx.fileName = 'filename.png'
  221. ctx.req.file = {
  222. path: ctx.path,
  223. }
  224. ctx.req.body = {
  225. name: ctx.fileName,
  226. }
  227. ctx.req.session = {
  228. user: {
  229. _id: ctx.user_id,
  230. },
  231. }
  232. ctx.req.params = { Project_id: ctx.project_id }
  233. ctx.req.query = { folder_id: ctx.folder_id }
  234. ctx.fs.unlink = sinon.stub()
  235. ctx.fsPromises.unlink = sinon.stub().resolves()
  236. })
  237. describe('successfully', function () {
  238. beforeEach(function (ctx) {
  239. ctx.entity = {
  240. _id: '1234',
  241. type: 'file',
  242. }
  243. ctx.FileSystemImportManager.addEntity = sinon
  244. .stub()
  245. .callsArgWith(6, null, ctx.entity)
  246. ctx.ProjectUploadController.uploadFile(ctx.req, ctx.res)
  247. })
  248. it('should insert the file', function (ctx) {
  249. return ctx.FileSystemImportManager.addEntity
  250. .calledWith(
  251. ctx.user_id,
  252. ctx.project_id,
  253. ctx.folder_id,
  254. ctx.fileName,
  255. ctx.path
  256. )
  257. .should.equal(true)
  258. })
  259. it('should return a successful response to the FileUploader client', function (ctx) {
  260. expect(ctx.res.body).to.deep.equal(
  261. JSON.stringify({
  262. success: true,
  263. entity_id: ctx.entity._id,
  264. entity_type: 'file',
  265. })
  266. )
  267. })
  268. it('should time the request', function (ctx) {
  269. ctx.metrics.Timer.prototype.done.called.should.equal(true)
  270. })
  271. it('should remove the uploaded file', function (ctx) {
  272. ctx.fs.unlink.calledWith(ctx.path).should.equal(true)
  273. })
  274. })
  275. describe('with folder structure', function () {
  276. beforeEach(async function (ctx) {
  277. await new Promise(resolve => {
  278. ctx.entity = {
  279. _id: '1234',
  280. type: 'file',
  281. }
  282. ctx.FileSystemImportManager.addEntity = sinon
  283. .stub()
  284. .callsArgWith(6, null, ctx.entity)
  285. ctx.ProjectLocator.promises.findElement = sinon.stub().resolves({
  286. path: { fileSystem: '/test' },
  287. })
  288. ctx.EditorController.promises.mkdirp = sinon.stub().resolves({
  289. lastFolder: { _id: 'folder-id' },
  290. })
  291. ctx.req.body.relativePath = 'foo/bar/' + ctx.fileName
  292. ctx.res.json = data => {
  293. expect(data.success).to.be.true
  294. resolve()
  295. }
  296. ctx.ProjectUploadController.uploadFile(ctx.req, ctx.res)
  297. })
  298. })
  299. it('should insert the file', function (ctx) {
  300. ctx.ProjectLocator.promises.findElement.should.be.calledOnceWithExactly(
  301. {
  302. project_id: ctx.project_id,
  303. element_id: ctx.folder_id,
  304. type: 'folder',
  305. }
  306. )
  307. ctx.EditorController.promises.mkdirp.should.be.calledWith(
  308. ctx.project_id,
  309. '/test/foo/bar',
  310. ctx.user_id
  311. )
  312. ctx.FileSystemImportManager.addEntity.should.be.calledOnceWith(
  313. ctx.user_id,
  314. ctx.project_id,
  315. 'folder-id',
  316. ctx.fileName,
  317. ctx.path
  318. )
  319. })
  320. })
  321. describe('when looking up the folder structure fails', function () {
  322. beforeEach(async function (ctx) {
  323. await new Promise(resolve => {
  324. ctx.error = new Error('woops')
  325. ctx.ProjectLocator.promises.findElement = sinon
  326. .stub()
  327. .rejects(ctx.error)
  328. ctx.req.body.relativePath = 'foo/bar/' + ctx.fileName
  329. ctx.next = error => {
  330. ctx.nextError = error
  331. resolve()
  332. }
  333. ctx.ProjectUploadController.uploadFile(ctx.req, ctx.res, ctx.next)
  334. })
  335. })
  336. it('should unlink the file', function (ctx) {
  337. ctx.fsPromises.unlink.should.have.been.calledWith(ctx.path)
  338. })
  339. it('should call next with the error', function (ctx) {
  340. expect(ctx.nextError).to.equal(ctx.error)
  341. })
  342. })
  343. describe('when FileSystemImportManager.addEntity returns a generic error', function () {
  344. beforeEach(function (ctx) {
  345. ctx.FileSystemImportManager.addEntity = sinon
  346. .stub()
  347. .callsArgWith(6, new Error('Sorry something went wrong'))
  348. ctx.ProjectUploadController.uploadFile(ctx.req, ctx.res)
  349. })
  350. it('should return an unsuccessful response to the FileUploader client', function (ctx) {
  351. expect(ctx.res.body).to.deep.equal(
  352. JSON.stringify({
  353. success: false,
  354. })
  355. )
  356. })
  357. it('should remove the uploaded file', function (ctx) {
  358. ctx.fs.unlink.calledWith(ctx.path).should.equal(true)
  359. })
  360. })
  361. describe('when FileSystemImportManager.addEntity returns a too many files error', function () {
  362. beforeEach(function (ctx) {
  363. ctx.FileSystemImportManager.addEntity = sinon
  364. .stub()
  365. .callsArgWith(6, new Error('project_has_too_many_files'))
  366. ctx.ProjectUploadController.uploadFile(ctx.req, ctx.res)
  367. })
  368. it('should return an unsuccessful response to the FileUploader client', function (ctx) {
  369. expect(ctx.res.body).to.deep.equal(
  370. JSON.stringify({
  371. success: false,
  372. error: 'project_has_too_many_files',
  373. })
  374. )
  375. })
  376. it('should remove the uploaded file', function (ctx) {
  377. ctx.fs.unlink.calledWith(ctx.path).should.equal(true)
  378. })
  379. })
  380. describe('with an invalid filename', function () {
  381. beforeEach(function (ctx) {
  382. ctx.req.body.name = ''
  383. ctx.ProjectUploadController.uploadFile(ctx.req, ctx.res)
  384. })
  385. it('should return a non success response', function (ctx) {
  386. expect(ctx.res.body).to.deep.equal(
  387. JSON.stringify({
  388. success: false,
  389. error: 'invalid_filename',
  390. })
  391. )
  392. })
  393. it('should remove the uploaded file', function (ctx) {
  394. ctx.fsPromises.unlink.calledWith(ctx.path).should.equal(true)
  395. })
  396. })
  397. describe('with a filename that is too long', function () {
  398. beforeEach(function (ctx) {
  399. ctx.req.body.name = 'a'.repeat(151)
  400. ctx.ProjectUploadController.uploadFile(ctx.req, ctx.res)
  401. })
  402. it('should return a non success response', function (ctx) {
  403. expect(ctx.res.body).to.deep.equal(
  404. JSON.stringify({
  405. success: false,
  406. error: 'invalid_filename',
  407. })
  408. )
  409. })
  410. it('should remove the uploaded file', function (ctx) {
  411. ctx.fsPromises.unlink.calledWith(ctx.path).should.equal(true)
  412. })
  413. })
  414. })
  415. describe('importDocument', function () {
  416. beforeEach(async function (ctx) {
  417. ctx.req.file = {
  418. path: '/path/to/uploaded/file.docx',
  419. }
  420. ctx.req.body = {
  421. name: 'file.docx',
  422. }
  423. ctx.req.query = { type: 'docx' }
  424. ctx.archivePath = '/path/to/archive.zip'
  425. ctx.fsPromises.unlink = sinon.stub().resolves()
  426. })
  427. describe('with conversionType=docx', async function () {
  428. describe('successfully', async function () {
  429. beforeEach(async function (ctx) {
  430. ctx.DocumentConversionManager.promises.convertDocumentToLaTeXZipArchive =
  431. sinon.stub().resolves(ctx.archivePath)
  432. ctx.ProjectUploadManager.promises.createProjectFromZipArchive = sinon
  433. .stub()
  434. .resolves({
  435. _id: 'new-project-id',
  436. })
  437. await new Promise(resolve => {
  438. ctx.res.json = data => {
  439. expect(data.success).to.be.true
  440. expect(data.project_id).to.equal('new-project-id')
  441. resolve()
  442. }
  443. ctx.ProjectUploadController.importDocument(ctx.req, ctx.res)
  444. })
  445. })
  446. it('should call the DocumentConversionManager with file path and type', function (ctx) {
  447. expect(
  448. ctx.DocumentConversionManager.promises
  449. .convertDocumentToLaTeXZipArchive
  450. ).to.have.been.calledWith(ctx.req.file.path, ctx.user_id, 'docx')
  451. })
  452. it('should use the resulting archive to create a new project', function (ctx) {
  453. expect(
  454. ctx.ProjectUploadManager.promises.createProjectFromZipArchive
  455. ).to.have.been.calledWith(ctx.user_id, 'file', ctx.archivePath)
  456. })
  457. it('should set the compiler to lualatex', function (ctx) {
  458. expect(
  459. ctx.ProjectOptionsHandler.promises.setCompiler
  460. ).to.have.been.calledWith('new-project-id', 'lualatex')
  461. })
  462. it('should unlink the archive after creating the project', function (ctx) {
  463. expect(ctx.fsPromises.unlink).to.have.been.calledWith(ctx.archivePath)
  464. })
  465. it('should unlink the uploaded file', function (ctx) {
  466. expect(ctx.fsPromises.unlink).to.have.been.calledWith(
  467. ctx.req.file.path
  468. )
  469. })
  470. it('should record a successful convert-format analytics event', function (ctx) {
  471. sinon.assert.calledWith(
  472. ctx.AnalyticsManager.recordEventForUserInBackground,
  473. ctx.user_id,
  474. 'convert-format',
  475. {
  476. sourceFormat: 'docx',
  477. targetFormat: 'latex',
  478. status: 'success',
  479. operation: 'import',
  480. }
  481. )
  482. })
  483. })
  484. })
  485. describe('with conversionType=markdown', async function () {
  486. beforeEach(async function (ctx) {
  487. ctx.req.file = {
  488. path: '/path/to/uploaded/file.md',
  489. }
  490. ctx.req.body = {
  491. name: 'file.md',
  492. }
  493. ctx.req.query = { type: 'markdown' }
  494. ctx.DocumentConversionManager.promises.convertDocumentToLaTeXZipArchive =
  495. sinon.stub().resolves(ctx.archivePath)
  496. ctx.ProjectUploadManager.promises.createProjectFromZipArchive = sinon
  497. .stub()
  498. .resolves({
  499. _id: 'new-project-id',
  500. })
  501. await new Promise(resolve => {
  502. ctx.res.json = data => {
  503. expect(data.success).to.be.true
  504. expect(data.project_id).to.equal('new-project-id')
  505. resolve()
  506. }
  507. ctx.ProjectUploadController.importDocument(ctx.req, ctx.res)
  508. })
  509. })
  510. it('should call the DocumentConversionManager with file path and markdown type', function (ctx) {
  511. expect(
  512. ctx.DocumentConversionManager.promises
  513. .convertDocumentToLaTeXZipArchive
  514. ).to.have.been.calledWith(ctx.req.file.path, ctx.user_id, 'markdown')
  515. })
  516. it('should use the resulting archive to create a new project', function (ctx) {
  517. expect(
  518. ctx.ProjectUploadManager.promises.createProjectFromZipArchive
  519. ).to.have.been.calledWith(ctx.user_id, 'file', ctx.archivePath)
  520. })
  521. it('should set the compiler to lualatex', function (ctx) {
  522. expect(
  523. ctx.ProjectOptionsHandler.promises.setCompiler
  524. ).to.have.been.calledWith('new-project-id', 'lualatex')
  525. })
  526. it('should unlink the archive after creating the project', function (ctx) {
  527. expect(ctx.fsPromises.unlink).to.have.been.calledWith(ctx.archivePath)
  528. })
  529. it('should unlink the uploaded file', function (ctx) {
  530. expect(ctx.fsPromises.unlink).to.have.been.calledWith(ctx.req.file.path)
  531. })
  532. it('should record a successful convert-format analytics event', function (ctx) {
  533. sinon.assert.calledWith(
  534. ctx.AnalyticsManager.recordEventForUserInBackground,
  535. ctx.user_id,
  536. 'convert-format',
  537. {
  538. sourceFormat: 'markdown',
  539. targetFormat: 'latex',
  540. status: 'success',
  541. operation: 'import',
  542. }
  543. )
  544. })
  545. })
  546. describe('with an invalid conversionType', async function () {
  547. beforeEach(async function (ctx) {
  548. ctx.req.query = { type: 'invalid' }
  549. await new Promise(resolve => {
  550. ctx.res.json = data => {
  551. expect(data).to.deep.equal({
  552. success: false,
  553. error: 'invalid_type',
  554. })
  555. resolve()
  556. }
  557. ctx.ProjectUploadController.importDocument(ctx.req, ctx.res)
  558. })
  559. })
  560. it('should return http 400', function (ctx) {
  561. expect(ctx.res.statusCode).to.equal(400)
  562. })
  563. it('should not call DocumentConversionManager', function (ctx) {
  564. expect(
  565. ctx.DocumentConversionManager.promises
  566. .convertDocumentToLaTeXZipArchive
  567. ).not.to.have.been.called
  568. })
  569. })
  570. describe('unsuccessfully', async function () {
  571. beforeEach(async function (ctx) {
  572. ctx.DocumentConversionManager.promises.convertDocumentToLaTeXZipArchive =
  573. sinon.stub().rejects(new Error('Conversion failed'))
  574. await new Promise(resolve => {
  575. ctx.res.json = data => {
  576. expect(data.success).to.be.false
  577. resolve()
  578. }
  579. ctx.ProjectUploadController.importDocument(ctx.req, ctx.res)
  580. })
  581. })
  582. it('should call the DocumentConversionManager to convert the file', function (ctx) {
  583. expect(
  584. ctx.DocumentConversionManager.promises
  585. .convertDocumentToLaTeXZipArchive
  586. ).to.have.been.calledWith(ctx.req.file.path, ctx.user_id, 'docx')
  587. })
  588. it('should unlink the uploaded file', function (ctx) {
  589. expect(ctx.fsPromises.unlink).to.have.been.calledWith(ctx.req.file.path)
  590. })
  591. it('should return http 500', function (ctx) {
  592. expect(ctx.res.statusCode).to.equal(500)
  593. })
  594. it('should record a failed convert-format analytics event', function (ctx) {
  595. sinon.assert.calledWith(
  596. ctx.AnalyticsManager.recordEventForUserInBackground,
  597. ctx.user_id,
  598. 'convert-format',
  599. {
  600. sourceFormat: 'docx',
  601. targetFormat: 'latex',
  602. status: 'failure',
  603. operation: 'import',
  604. }
  605. )
  606. })
  607. })
  608. describe('when the converted archive is too large', async function () {
  609. beforeEach(async function (ctx) {
  610. ctx.DocumentConversionManager.promises.convertDocumentToLaTeXZipArchive =
  611. sinon.stub().rejects(new FileTooLargeError('file too large'))
  612. await new Promise(resolve => {
  613. ctx.res.json = data => {
  614. expect(data).to.deep.equal({
  615. success: false,
  616. error: 'file_too_large',
  617. })
  618. resolve()
  619. }
  620. ctx.ProjectUploadController.importDocument(ctx.req, ctx.res)
  621. })
  622. })
  623. it('should return http 422', function (ctx) {
  624. expect(ctx.res.statusCode).to.equal(422)
  625. })
  626. it('should unlink the uploaded file', function (ctx) {
  627. expect(ctx.fsPromises.unlink).to.have.been.calledWith(ctx.req.file.path)
  628. })
  629. })
  630. })
  631. })