ProjectUploadController.test.mjs 24 KB

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