ProjectUploadController.test.mjs 24 KB

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