ProjectUploadManagerTests.js 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370
  1. const sinon = require('sinon')
  2. const { expect } = require('chai')
  3. const timekeeper = require('timekeeper')
  4. const SandboxedModule = require('sandboxed-module')
  5. const { ObjectId } = require('mongodb-legacy')
  6. const MODULE_PATH =
  7. '../../../../app/src/Features/Uploads/ProjectUploadManager.js'
  8. describe('ProjectUploadManager', function () {
  9. beforeEach(function () {
  10. this.now = Date.now()
  11. timekeeper.freeze(this.now)
  12. this.rootFolderId = new ObjectId()
  13. this.ownerId = new ObjectId()
  14. this.zipPath = '/path/to/zip/file-name.zip'
  15. this.extractedZipPath = `/path/to/zip/file-name-${this.now}`
  16. this.mainContent = 'Contents of main.tex'
  17. this.projectName = 'My project*'
  18. this.fixedProjectName = 'My project'
  19. this.uniqueProjectName = 'My project (1)'
  20. this.project = {
  21. _id: new ObjectId(),
  22. rootFolder: [{ _id: this.rootFolderId }],
  23. overleaf: { history: { id: 12345 } },
  24. }
  25. this.doc = {
  26. _id: new ObjectId(),
  27. name: 'main.tex',
  28. }
  29. this.docFsPath = '/path/to/doc'
  30. this.docLines = ['My thesis', 'by A. U. Thor']
  31. this.file = {
  32. _id: new ObjectId(),
  33. name: 'image.png',
  34. }
  35. this.fileFsPath = '/path/to/file'
  36. this.topLevelDestination = '/path/to/zip/file-extracted/nested'
  37. this.newProjectVersion = 123
  38. this.importEntries = [
  39. {
  40. type: 'doc',
  41. projectPath: '/main.tex',
  42. lines: this.docLines,
  43. },
  44. {
  45. type: 'file',
  46. projectPath: `/${this.file.name}`,
  47. fsPath: this.fileFsPath,
  48. },
  49. ]
  50. this.docEntries = [
  51. {
  52. doc: this.doc,
  53. path: `/${this.doc.name}`,
  54. docLines: this.docLines.join('\n'),
  55. },
  56. ]
  57. this.fileEntries = [
  58. {
  59. file: this.file,
  60. path: `/${this.file.name}`,
  61. url: this.fileStoreUrl,
  62. createdBlob: true,
  63. },
  64. ]
  65. this.fs = {
  66. promises: {
  67. rm: sinon.stub().resolves(),
  68. },
  69. }
  70. this.ArchiveManager = {
  71. promises: {
  72. extractZipArchive: sinon.stub().resolves(),
  73. findTopLevelDirectory: sinon
  74. .stub()
  75. .withArgs(this.extractedZipPath)
  76. .resolves(this.topLevelDestination),
  77. },
  78. }
  79. this.Doc = sinon.stub().returns(this.doc)
  80. this.DocstoreManager = {
  81. promises: {
  82. updateDoc: sinon.stub().resolves(),
  83. },
  84. }
  85. this.DocumentHelper = {
  86. getTitleFromTexContent: sinon
  87. .stub()
  88. .withArgs(this.mainContent)
  89. .returns(this.projectName),
  90. }
  91. this.DocumentUpdaterHandler = {
  92. promises: {
  93. updateProjectStructure: sinon.stub().resolves(),
  94. },
  95. }
  96. this.FileStoreHandler = {
  97. promises: {
  98. uploadFileFromDiskWithHistoryId: sinon.stub().resolves({
  99. fileRef: this.file,
  100. url: this.fileStoreUrl,
  101. createdBlob: true,
  102. }),
  103. },
  104. }
  105. this.FileSystemImportManager = {
  106. promises: {
  107. importDir: sinon
  108. .stub()
  109. .withArgs(this.topLevelDestination)
  110. .resolves(this.importEntries),
  111. },
  112. }
  113. this.ProjectCreationHandler = {
  114. promises: {
  115. createBlankProject: sinon.stub().resolves(this.project),
  116. },
  117. }
  118. this.ProjectEntityMongoUpdateHandler = {
  119. promises: {
  120. createNewFolderStructure: sinon.stub().resolves(this.newProjectVersion),
  121. },
  122. }
  123. this.ProjectRootDocManager = {
  124. promises: {
  125. setRootDocAutomatically: sinon.stub().resolves(),
  126. findRootDocFileFromDirectory: sinon
  127. .stub()
  128. .resolves({ path: 'main.tex', content: this.mainContent }),
  129. setRootDocFromName: sinon.stub().resolves(),
  130. },
  131. }
  132. this.ProjectDetailsHandler = {
  133. fixProjectName: sinon
  134. .stub()
  135. .withArgs(this.projectName)
  136. .returns(this.fixedProjectName),
  137. promises: {
  138. generateUniqueName: sinon.stub().resolves(this.uniqueProjectName),
  139. },
  140. }
  141. this.ProjectDeleter = {
  142. promises: {
  143. deleteProject: sinon.stub().resolves(),
  144. },
  145. }
  146. this.TpdsProjectFlusher = {
  147. promises: {
  148. flushProjectToTpds: sinon.stub().resolves(),
  149. },
  150. }
  151. this.ProjectUploadManager = SandboxedModule.require(MODULE_PATH, {
  152. requires: {
  153. fs: this.fs,
  154. './ArchiveManager': this.ArchiveManager,
  155. '../../models/Doc': { Doc: this.Doc },
  156. '../Docstore/DocstoreManager': this.DocstoreManager,
  157. '../Documents/DocumentHelper': this.DocumentHelper,
  158. '../DocumentUpdater/DocumentUpdaterHandler':
  159. this.DocumentUpdaterHandler,
  160. '../FileStore/FileStoreHandler': this.FileStoreHandler,
  161. './FileSystemImportManager': this.FileSystemImportManager,
  162. '../Project/ProjectCreationHandler': this.ProjectCreationHandler,
  163. '../Project/ProjectEntityMongoUpdateHandler':
  164. this.ProjectEntityMongoUpdateHandler,
  165. '../Project/ProjectRootDocManager': this.ProjectRootDocManager,
  166. '../Project/ProjectDetailsHandler': this.ProjectDetailsHandler,
  167. '../Project/ProjectDeleter': this.ProjectDeleter,
  168. '../ThirdPartyDataStore/TpdsProjectFlusher': this.TpdsProjectFlusher,
  169. },
  170. })
  171. })
  172. afterEach(function () {
  173. timekeeper.reset()
  174. })
  175. describe('createProjectFromZipArchive', function () {
  176. describe('when the title can be read from the root document', function () {
  177. beforeEach(async function () {
  178. await this.ProjectUploadManager.promises.createProjectFromZipArchive(
  179. this.ownerId,
  180. this.projectName,
  181. this.zipPath
  182. )
  183. })
  184. it('should extract the archive', function () {
  185. this.ArchiveManager.promises.extractZipArchive.should.have.been.calledWith(
  186. this.zipPath,
  187. this.extractedZipPath
  188. )
  189. })
  190. it('should create a project', function () {
  191. this.ProjectCreationHandler.promises.createBlankProject.should.have.been.calledWith(
  192. this.ownerId,
  193. this.uniqueProjectName
  194. )
  195. })
  196. it('should initialize the file tree', function () {
  197. this.ProjectEntityMongoUpdateHandler.promises.createNewFolderStructure.should.have.been.calledWith(
  198. this.project._id,
  199. this.docEntries,
  200. this.fileEntries
  201. )
  202. })
  203. it('should notify document updater', function () {
  204. this.DocumentUpdaterHandler.promises.updateProjectStructure.should.have.been.calledWith(
  205. this.project._id,
  206. this.project.overleaf.history.id,
  207. this.ownerId,
  208. {
  209. newDocs: this.docEntries,
  210. newFiles: this.fileEntries,
  211. newProject: { version: this.newProjectVersion },
  212. },
  213. null
  214. )
  215. })
  216. it('should flush the project to TPDS', function () {
  217. this.TpdsProjectFlusher.promises.flushProjectToTpds.should.have.been.calledWith(
  218. this.project._id
  219. )
  220. })
  221. it('should set the root document', function () {
  222. this.ProjectRootDocManager.promises.setRootDocFromName.should.have.been.calledWith(
  223. this.project._id,
  224. 'main.tex'
  225. )
  226. })
  227. it('should remove the destination directory afterwards', function () {
  228. this.fs.promises.rm.should.have.been.calledWith(this.extractedZipPath, {
  229. recursive: true,
  230. force: true,
  231. })
  232. })
  233. })
  234. describe("when the root document can't be determined", function () {
  235. beforeEach(async function () {
  236. this.ProjectRootDocManager.promises.findRootDocFileFromDirectory.resolves(
  237. {}
  238. )
  239. await this.ProjectUploadManager.promises.createProjectFromZipArchive(
  240. this.ownerId,
  241. this.projectName,
  242. this.zipPath
  243. )
  244. })
  245. it('should not try to set the root doc', function () {
  246. this.ProjectRootDocManager.promises.setRootDocFromName.should.not.have
  247. .been.called
  248. })
  249. })
  250. })
  251. describe('createProjectFromZipArchiveWithName', function () {
  252. beforeEach(async function () {
  253. await this.ProjectUploadManager.promises.createProjectFromZipArchiveWithName(
  254. this.ownerId,
  255. this.projectName,
  256. this.zipPath
  257. )
  258. })
  259. it('should extract the archive', function () {
  260. this.ArchiveManager.promises.extractZipArchive.should.have.been.calledWith(
  261. this.zipPath,
  262. this.extractedZipPath
  263. )
  264. })
  265. it('should create a project owned by the owner_id', function () {
  266. this.ProjectCreationHandler.promises.createBlankProject.should.have.been.calledWith(
  267. this.ownerId,
  268. this.uniqueProjectName
  269. )
  270. })
  271. it('should automatically set the root doc', function () {
  272. this.ProjectRootDocManager.promises.setRootDocAutomatically.should.have.been.calledWith(
  273. this.project._id
  274. )
  275. })
  276. it('should initialize the file tree', function () {
  277. this.ProjectEntityMongoUpdateHandler.promises.createNewFolderStructure.should.have.been.calledWith(
  278. this.project._id,
  279. this.docEntries,
  280. this.fileEntries
  281. )
  282. })
  283. it('should notify document updater', function () {
  284. this.DocumentUpdaterHandler.promises.updateProjectStructure.should.have.been.calledWith(
  285. this.project._id,
  286. this.project.overleaf.history.id,
  287. this.ownerId,
  288. {
  289. newDocs: this.docEntries,
  290. newFiles: this.fileEntries,
  291. newProject: { version: this.newProjectVersion },
  292. },
  293. null
  294. )
  295. })
  296. it('should flush the project to TPDS', function () {
  297. this.TpdsProjectFlusher.promises.flushProjectToTpds.should.have.been.calledWith(
  298. this.project._id
  299. )
  300. })
  301. it('should remove the destination directory afterwards', function () {
  302. this.fs.promises.rm.should.have.been.calledWith(this.extractedZipPath, {
  303. recursive: true,
  304. force: true,
  305. })
  306. })
  307. describe('when initializing the folder structure fails', function () {
  308. beforeEach(async function () {
  309. this.ProjectEntityMongoUpdateHandler.promises.createNewFolderStructure.rejects()
  310. await expect(
  311. this.ProjectUploadManager.promises.createProjectFromZipArchiveWithName(
  312. this.ownerId,
  313. this.projectName,
  314. this.zipPath
  315. )
  316. ).to.be.rejected
  317. })
  318. it('should cleanup the blank project created', async function () {
  319. this.ProjectDeleter.promises.deleteProject.should.have.been.calledWith(
  320. this.project._id
  321. )
  322. })
  323. })
  324. describe('when setting automatically the root doc fails', function () {
  325. beforeEach(async function () {
  326. this.ProjectRootDocManager.promises.setRootDocAutomatically.rejects()
  327. await expect(
  328. this.ProjectUploadManager.promises.createProjectFromZipArchiveWithName(
  329. this.ownerId,
  330. this.projectName,
  331. this.zipPath
  332. )
  333. ).to.be.rejected
  334. })
  335. it('should cleanup the blank project created', function () {
  336. this.ProjectDeleter.promises.deleteProject.should.have.been.calledWith(
  337. this.project._id
  338. )
  339. })
  340. })
  341. })
  342. })