ProjectUploadController.test.mjs 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418
  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. const modulePath =
  14. '../../../../app/src/Features/Uploads/ProjectUploadController.mjs'
  15. describe('ProjectUploadController', function () {
  16. beforeEach(async function (ctx) {
  17. let Timer
  18. ctx.req = new MockRequest(vi)
  19. ctx.res = new MockResponse(vi)
  20. ctx.user_id = 'user-id-123'
  21. ctx.metrics = {
  22. Timer: (Timer = (function () {
  23. Timer = class Timer {
  24. static initClass() {
  25. this.prototype.done = sinon.stub()
  26. }
  27. }
  28. Timer.initClass()
  29. return Timer
  30. })()),
  31. }
  32. ctx.SessionManager = {
  33. getLoggedInUserId: sinon.stub().returns(ctx.user_id),
  34. }
  35. ctx.ProjectLocator = {
  36. promises: {},
  37. }
  38. ctx.EditorController = {
  39. promises: {},
  40. }
  41. vi.doMock('multer', () => ({
  42. default: sinon.stub(),
  43. }))
  44. vi.doMock('@overleaf/settings', () => ({
  45. default: { path: {} },
  46. }))
  47. vi.doMock(
  48. '../../../../app/src/Features/Uploads/ProjectUploadManager',
  49. () => ({
  50. default: (ctx.ProjectUploadManager = {}),
  51. })
  52. )
  53. vi.doMock(
  54. '../../../../app/src/Features/Uploads/FileSystemImportManager',
  55. () => ({
  56. default: (ctx.FileSystemImportManager = {}),
  57. })
  58. )
  59. vi.doMock('@overleaf/metrics', () => ({
  60. default: ctx.metrics,
  61. }))
  62. vi.doMock(
  63. '../../../../app/src/Features/Authentication/SessionManager',
  64. () => ({
  65. default: ctx.SessionManager,
  66. })
  67. )
  68. vi.doMock(
  69. '../../../../app/src/Features/Uploads/ArchiveErrors',
  70. () => ArchiveErrors
  71. )
  72. vi.doMock('../../../../app/src/Features/Project/ProjectLocator', () => ({
  73. default: ctx.ProjectLocator,
  74. }))
  75. vi.doMock('../../../../app/src/Features/Editor/EditorController', () => ({
  76. default: ctx.EditorController,
  77. }))
  78. vi.doMock('fs', () => ({
  79. default: (ctx.fs = {}),
  80. }))
  81. ctx.ProjectUploadController = (await import(modulePath)).default
  82. })
  83. describe('uploadProject', function () {
  84. beforeEach(function (ctx) {
  85. ctx.path = '/path/to/file/on/disk.zip'
  86. ctx.fileName = 'filename.zip'
  87. ctx.req.file = {
  88. path: ctx.path,
  89. }
  90. ctx.req.body = {
  91. name: ctx.fileName,
  92. }
  93. ctx.req.session = {
  94. user: {
  95. _id: ctx.user_id,
  96. },
  97. }
  98. ctx.project = { _id: (ctx.project_id = 'project-id-123') }
  99. ctx.fs.unlink = sinon.stub()
  100. })
  101. describe('successfully', function () {
  102. beforeEach(function (ctx) {
  103. ctx.ProjectUploadManager.createProjectFromZipArchive = sinon
  104. .stub()
  105. .callsArgWith(3, null, ctx.project)
  106. ctx.ProjectUploadController.uploadProject(ctx.req, ctx.res)
  107. })
  108. it('should create a project owned by the logged in user', function (ctx) {
  109. ctx.ProjectUploadManager.createProjectFromZipArchive
  110. .calledWith(ctx.user_id)
  111. .should.equal(true)
  112. })
  113. it('should create a project with the same name as the zip archive', function (ctx) {
  114. ctx.ProjectUploadManager.createProjectFromZipArchive
  115. .calledWith(sinon.match.any, 'filename', sinon.match.any)
  116. .should.equal(true)
  117. })
  118. it('should create a project from the zip archive', function (ctx) {
  119. ctx.ProjectUploadManager.createProjectFromZipArchive
  120. .calledWith(sinon.match.any, sinon.match.any, ctx.path)
  121. .should.equal(true)
  122. })
  123. it('should return a successful response to the FileUploader client', function (ctx) {
  124. expect(ctx.res.body).to.deep.equal(
  125. JSON.stringify({
  126. success: true,
  127. project_id: ctx.project_id,
  128. })
  129. )
  130. })
  131. it('should record the time taken to do the upload', function (ctx) {
  132. ctx.metrics.Timer.prototype.done.called.should.equal(true)
  133. })
  134. it('should remove the uploaded file', function (ctx) {
  135. ctx.fs.unlink.calledWith(ctx.path).should.equal(true)
  136. })
  137. })
  138. describe('when ProjectUploadManager.createProjectFromZipArchive fails', function () {
  139. beforeEach(function (ctx) {
  140. ctx.ProjectUploadManager.createProjectFromZipArchive = sinon
  141. .stub()
  142. .callsArgWith(3, new Error('Something went wrong'), ctx.project)
  143. ctx.ProjectUploadController.uploadProject(ctx.req, ctx.res)
  144. })
  145. it('should return a failed response to the FileUploader client', function (ctx) {
  146. expect(ctx.res.body).to.deep.equal(
  147. JSON.stringify({ success: false, error: 'upload_failed' })
  148. )
  149. })
  150. })
  151. describe('when ProjectUploadManager.createProjectFromZipArchive reports the file as invalid', function () {
  152. beforeEach(function (ctx) {
  153. ctx.ProjectUploadManager.createProjectFromZipArchive = sinon
  154. .stub()
  155. .callsArgWith(
  156. 3,
  157. new ArchiveErrors.ZipContentsTooLargeError(),
  158. ctx.project
  159. )
  160. ctx.ProjectUploadController.uploadProject(ctx.req, ctx.res)
  161. })
  162. it('should return the reported error to the FileUploader client', function (ctx) {
  163. expect(JSON.parse(ctx.res.body)).to.deep.equal({
  164. success: false,
  165. error: 'zip_contents_too_large',
  166. })
  167. })
  168. it("should return an 'unprocessable entity' status code", function (ctx) {
  169. expect(ctx.res.statusCode).to.equal(422)
  170. })
  171. })
  172. })
  173. describe('uploadFile', function () {
  174. beforeEach(function (ctx) {
  175. ctx.project_id = 'project-id-123'
  176. ctx.folder_id = 'folder-id-123'
  177. ctx.path = '/path/to/file/on/disk.png'
  178. ctx.fileName = 'filename.png'
  179. ctx.req.file = {
  180. path: ctx.path,
  181. }
  182. ctx.req.body = {
  183. name: ctx.fileName,
  184. }
  185. ctx.req.session = {
  186. user: {
  187. _id: ctx.user_id,
  188. },
  189. }
  190. ctx.req.params = { Project_id: ctx.project_id }
  191. ctx.req.query = { folder_id: ctx.folder_id }
  192. ctx.fs.unlink = sinon.stub()
  193. })
  194. describe('successfully', function () {
  195. beforeEach(function (ctx) {
  196. ctx.entity = {
  197. _id: '1234',
  198. type: 'file',
  199. }
  200. ctx.FileSystemImportManager.addEntity = sinon
  201. .stub()
  202. .callsArgWith(6, null, ctx.entity)
  203. ctx.ProjectUploadController.uploadFile(ctx.req, ctx.res)
  204. })
  205. it('should insert the file', function (ctx) {
  206. return ctx.FileSystemImportManager.addEntity
  207. .calledWith(
  208. ctx.user_id,
  209. ctx.project_id,
  210. ctx.folder_id,
  211. ctx.fileName,
  212. ctx.path
  213. )
  214. .should.equal(true)
  215. })
  216. it('should return a successful response to the FileUploader client', function (ctx) {
  217. expect(ctx.res.body).to.deep.equal(
  218. JSON.stringify({
  219. success: true,
  220. entity_id: ctx.entity._id,
  221. entity_type: 'file',
  222. })
  223. )
  224. })
  225. it('should time the request', function (ctx) {
  226. ctx.metrics.Timer.prototype.done.called.should.equal(true)
  227. })
  228. it('should remove the uploaded file', function (ctx) {
  229. ctx.fs.unlink.calledWith(ctx.path).should.equal(true)
  230. })
  231. })
  232. describe('with folder structure', function () {
  233. beforeEach(async function (ctx) {
  234. await new Promise(resolve => {
  235. ctx.entity = {
  236. _id: '1234',
  237. type: 'file',
  238. }
  239. ctx.FileSystemImportManager.addEntity = sinon
  240. .stub()
  241. .callsArgWith(6, null, ctx.entity)
  242. ctx.ProjectLocator.promises.findElement = sinon.stub().resolves({
  243. path: { fileSystem: '/test' },
  244. })
  245. ctx.EditorController.promises.mkdirp = sinon.stub().resolves({
  246. lastFolder: { _id: 'folder-id' },
  247. })
  248. ctx.req.body.relativePath = 'foo/bar/' + ctx.fileName
  249. ctx.res.json = data => {
  250. expect(data.success).to.be.true
  251. resolve()
  252. }
  253. ctx.ProjectUploadController.uploadFile(ctx.req, ctx.res)
  254. })
  255. })
  256. it('should insert the file', function (ctx) {
  257. ctx.ProjectLocator.promises.findElement.should.be.calledOnceWithExactly(
  258. {
  259. project_id: ctx.project_id,
  260. element_id: ctx.folder_id,
  261. type: 'folder',
  262. }
  263. )
  264. ctx.EditorController.promises.mkdirp.should.be.calledWith(
  265. ctx.project_id,
  266. '/test/foo/bar',
  267. ctx.user_id
  268. )
  269. ctx.FileSystemImportManager.addEntity.should.be.calledOnceWith(
  270. ctx.user_id,
  271. ctx.project_id,
  272. 'folder-id',
  273. ctx.fileName,
  274. ctx.path
  275. )
  276. })
  277. })
  278. describe('when looking up the folder structure fails', function () {
  279. beforeEach(async function (ctx) {
  280. await new Promise(resolve => {
  281. ctx.error = new Error('woops')
  282. ctx.ProjectLocator.promises.findElement = sinon
  283. .stub()
  284. .rejects(ctx.error)
  285. ctx.req.body.relativePath = 'foo/bar/' + ctx.fileName
  286. ctx.next = error => {
  287. ctx.nextError = error
  288. resolve()
  289. }
  290. ctx.ProjectUploadController.uploadFile(ctx.req, ctx.res, ctx.next)
  291. })
  292. })
  293. it('should unlink the file', function (ctx) {
  294. ctx.fs.unlink.should.have.been.calledWith(ctx.path)
  295. })
  296. it('should call next with the error', function (ctx) {
  297. expect(ctx.nextError).to.equal(ctx.error)
  298. })
  299. })
  300. describe('when FileSystemImportManager.addEntity returns a generic error', function () {
  301. beforeEach(function (ctx) {
  302. ctx.FileSystemImportManager.addEntity = sinon
  303. .stub()
  304. .callsArgWith(6, new Error('Sorry something went wrong'))
  305. ctx.ProjectUploadController.uploadFile(ctx.req, ctx.res)
  306. })
  307. it('should return an unsuccessful response to the FileUploader client', function (ctx) {
  308. expect(ctx.res.body).to.deep.equal(
  309. JSON.stringify({
  310. success: false,
  311. })
  312. )
  313. })
  314. })
  315. describe('when FileSystemImportManager.addEntity returns a too many files error', function () {
  316. beforeEach(function (ctx) {
  317. ctx.FileSystemImportManager.addEntity = sinon
  318. .stub()
  319. .callsArgWith(6, new Error('project_has_too_many_files'))
  320. ctx.ProjectUploadController.uploadFile(ctx.req, ctx.res)
  321. })
  322. it('should return an unsuccessful response to the FileUploader client', function (ctx) {
  323. expect(ctx.res.body).to.deep.equal(
  324. JSON.stringify({
  325. success: false,
  326. error: 'project_has_too_many_files',
  327. })
  328. )
  329. })
  330. })
  331. describe('with an invalid filename', function () {
  332. beforeEach(function (ctx) {
  333. ctx.req.body.name = ''
  334. ctx.ProjectUploadController.uploadFile(ctx.req, ctx.res)
  335. })
  336. it('should return a non success response', function (ctx) {
  337. expect(ctx.res.body).to.deep.equal(
  338. JSON.stringify({
  339. success: false,
  340. error: 'invalid_filename',
  341. })
  342. )
  343. })
  344. it('should remove the uploaded file', function (ctx) {
  345. ctx.fs.unlink.calledWith(ctx.path).should.equal(true)
  346. })
  347. })
  348. describe('with a filename that is too long', function () {
  349. beforeEach(function (ctx) {
  350. ctx.req.body.name = 'a'.repeat(151)
  351. ctx.ProjectUploadController.uploadFile(ctx.req, ctx.res)
  352. })
  353. it('should return a non success response', function (ctx) {
  354. expect(ctx.res.body).to.deep.equal(
  355. JSON.stringify({
  356. success: false,
  357. error: 'invalid_filename',
  358. })
  359. )
  360. })
  361. it('should remove the uploaded file', function (ctx) {
  362. ctx.fs.unlink.calledWith(ctx.path).should.equal(true)
  363. })
  364. })
  365. })
  366. })