ProjectUploadManagerTests.js 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368
  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. createdBlob: true,
  62. },
  63. ]
  64. this.fs = {
  65. promises: {
  66. rm: sinon.stub().resolves(),
  67. },
  68. }
  69. this.ArchiveManager = {
  70. promises: {
  71. extractZipArchive: sinon.stub().resolves(),
  72. findTopLevelDirectory: sinon
  73. .stub()
  74. .withArgs(this.extractedZipPath)
  75. .resolves(this.topLevelDestination),
  76. },
  77. }
  78. this.Doc = sinon.stub().returns(this.doc)
  79. this.DocstoreManager = {
  80. promises: {
  81. updateDoc: sinon.stub().resolves(),
  82. },
  83. }
  84. this.DocumentHelper = {
  85. getTitleFromTexContent: sinon
  86. .stub()
  87. .withArgs(this.mainContent)
  88. .returns(this.projectName),
  89. }
  90. this.DocumentUpdaterHandler = {
  91. promises: {
  92. updateProjectStructure: sinon.stub().resolves(),
  93. },
  94. }
  95. this.FileStoreHandler = {
  96. promises: {
  97. uploadFileFromDiskWithHistoryId: sinon.stub().resolves({
  98. fileRef: this.file,
  99. createdBlob: true,
  100. }),
  101. },
  102. }
  103. this.FileSystemImportManager = {
  104. promises: {
  105. importDir: sinon
  106. .stub()
  107. .withArgs(this.topLevelDestination)
  108. .resolves(this.importEntries),
  109. },
  110. }
  111. this.ProjectCreationHandler = {
  112. promises: {
  113. createBlankProject: sinon.stub().resolves(this.project),
  114. },
  115. }
  116. this.ProjectEntityMongoUpdateHandler = {
  117. promises: {
  118. createNewFolderStructure: sinon.stub().resolves(this.newProjectVersion),
  119. },
  120. }
  121. this.ProjectRootDocManager = {
  122. promises: {
  123. setRootDocAutomatically: sinon.stub().resolves(),
  124. findRootDocFileFromDirectory: sinon
  125. .stub()
  126. .resolves({ path: 'main.tex', content: this.mainContent }),
  127. setRootDocFromName: sinon.stub().resolves(),
  128. },
  129. }
  130. this.ProjectDetailsHandler = {
  131. fixProjectName: sinon
  132. .stub()
  133. .withArgs(this.projectName)
  134. .returns(this.fixedProjectName),
  135. promises: {
  136. generateUniqueName: sinon.stub().resolves(this.uniqueProjectName),
  137. },
  138. }
  139. this.ProjectDeleter = {
  140. promises: {
  141. deleteProject: sinon.stub().resolves(),
  142. },
  143. }
  144. this.TpdsProjectFlusher = {
  145. promises: {
  146. flushProjectToTpds: sinon.stub().resolves(),
  147. },
  148. }
  149. this.ProjectUploadManager = SandboxedModule.require(MODULE_PATH, {
  150. requires: {
  151. fs: this.fs,
  152. './ArchiveManager': this.ArchiveManager,
  153. '../../models/Doc': { Doc: this.Doc },
  154. '../Docstore/DocstoreManager': this.DocstoreManager,
  155. '../Documents/DocumentHelper': this.DocumentHelper,
  156. '../DocumentUpdater/DocumentUpdaterHandler':
  157. this.DocumentUpdaterHandler,
  158. '../FileStore/FileStoreHandler': this.FileStoreHandler,
  159. './FileSystemImportManager': this.FileSystemImportManager,
  160. '../Project/ProjectCreationHandler': this.ProjectCreationHandler,
  161. '../Project/ProjectEntityMongoUpdateHandler':
  162. this.ProjectEntityMongoUpdateHandler,
  163. '../Project/ProjectRootDocManager': this.ProjectRootDocManager,
  164. '../Project/ProjectDetailsHandler': this.ProjectDetailsHandler,
  165. '../Project/ProjectDeleter': this.ProjectDeleter,
  166. '../ThirdPartyDataStore/TpdsProjectFlusher': this.TpdsProjectFlusher,
  167. },
  168. })
  169. })
  170. afterEach(function () {
  171. timekeeper.reset()
  172. })
  173. describe('createProjectFromZipArchive', function () {
  174. describe('when the title can be read from the root document', function () {
  175. beforeEach(async function () {
  176. await this.ProjectUploadManager.promises.createProjectFromZipArchive(
  177. this.ownerId,
  178. this.projectName,
  179. this.zipPath
  180. )
  181. })
  182. it('should extract the archive', function () {
  183. this.ArchiveManager.promises.extractZipArchive.should.have.been.calledWith(
  184. this.zipPath,
  185. this.extractedZipPath
  186. )
  187. })
  188. it('should create a project', function () {
  189. this.ProjectCreationHandler.promises.createBlankProject.should.have.been.calledWith(
  190. this.ownerId,
  191. this.uniqueProjectName
  192. )
  193. })
  194. it('should initialize the file tree', function () {
  195. this.ProjectEntityMongoUpdateHandler.promises.createNewFolderStructure.should.have.been.calledWith(
  196. this.project._id,
  197. this.docEntries,
  198. this.fileEntries
  199. )
  200. })
  201. it('should notify document updater', function () {
  202. this.DocumentUpdaterHandler.promises.updateProjectStructure.should.have.been.calledWith(
  203. this.project._id,
  204. this.project.overleaf.history.id,
  205. this.ownerId,
  206. {
  207. newDocs: this.docEntries,
  208. newFiles: this.fileEntries,
  209. newProject: { version: this.newProjectVersion },
  210. },
  211. null
  212. )
  213. })
  214. it('should flush the project to TPDS', function () {
  215. this.TpdsProjectFlusher.promises.flushProjectToTpds.should.have.been.calledWith(
  216. this.project._id
  217. )
  218. })
  219. it('should set the root document', function () {
  220. this.ProjectRootDocManager.promises.setRootDocFromName.should.have.been.calledWith(
  221. this.project._id,
  222. 'main.tex'
  223. )
  224. })
  225. it('should remove the destination directory afterwards', function () {
  226. this.fs.promises.rm.should.have.been.calledWith(this.extractedZipPath, {
  227. recursive: true,
  228. force: true,
  229. })
  230. })
  231. })
  232. describe("when the root document can't be determined", function () {
  233. beforeEach(async function () {
  234. this.ProjectRootDocManager.promises.findRootDocFileFromDirectory.resolves(
  235. {}
  236. )
  237. await this.ProjectUploadManager.promises.createProjectFromZipArchive(
  238. this.ownerId,
  239. this.projectName,
  240. this.zipPath
  241. )
  242. })
  243. it('should not try to set the root doc', function () {
  244. this.ProjectRootDocManager.promises.setRootDocFromName.should.not.have
  245. .been.called
  246. })
  247. })
  248. })
  249. describe('createProjectFromZipArchiveWithName', function () {
  250. beforeEach(async function () {
  251. await this.ProjectUploadManager.promises.createProjectFromZipArchiveWithName(
  252. this.ownerId,
  253. this.projectName,
  254. this.zipPath
  255. )
  256. })
  257. it('should extract the archive', function () {
  258. this.ArchiveManager.promises.extractZipArchive.should.have.been.calledWith(
  259. this.zipPath,
  260. this.extractedZipPath
  261. )
  262. })
  263. it('should create a project owned by the owner_id', function () {
  264. this.ProjectCreationHandler.promises.createBlankProject.should.have.been.calledWith(
  265. this.ownerId,
  266. this.uniqueProjectName
  267. )
  268. })
  269. it('should automatically set the root doc', function () {
  270. this.ProjectRootDocManager.promises.setRootDocAutomatically.should.have.been.calledWith(
  271. this.project._id
  272. )
  273. })
  274. it('should initialize the file tree', function () {
  275. this.ProjectEntityMongoUpdateHandler.promises.createNewFolderStructure.should.have.been.calledWith(
  276. this.project._id,
  277. this.docEntries,
  278. this.fileEntries
  279. )
  280. })
  281. it('should notify document updater', function () {
  282. this.DocumentUpdaterHandler.promises.updateProjectStructure.should.have.been.calledWith(
  283. this.project._id,
  284. this.project.overleaf.history.id,
  285. this.ownerId,
  286. {
  287. newDocs: this.docEntries,
  288. newFiles: this.fileEntries,
  289. newProject: { version: this.newProjectVersion },
  290. },
  291. null
  292. )
  293. })
  294. it('should flush the project to TPDS', function () {
  295. this.TpdsProjectFlusher.promises.flushProjectToTpds.should.have.been.calledWith(
  296. this.project._id
  297. )
  298. })
  299. it('should remove the destination directory afterwards', function () {
  300. this.fs.promises.rm.should.have.been.calledWith(this.extractedZipPath, {
  301. recursive: true,
  302. force: true,
  303. })
  304. })
  305. describe('when initializing the folder structure fails', function () {
  306. beforeEach(async function () {
  307. this.ProjectEntityMongoUpdateHandler.promises.createNewFolderStructure.rejects()
  308. await expect(
  309. this.ProjectUploadManager.promises.createProjectFromZipArchiveWithName(
  310. this.ownerId,
  311. this.projectName,
  312. this.zipPath
  313. )
  314. ).to.be.rejected
  315. })
  316. it('should cleanup the blank project created', async function () {
  317. this.ProjectDeleter.promises.deleteProject.should.have.been.calledWith(
  318. this.project._id
  319. )
  320. })
  321. })
  322. describe('when setting automatically the root doc fails', function () {
  323. beforeEach(async function () {
  324. this.ProjectRootDocManager.promises.setRootDocAutomatically.rejects()
  325. await expect(
  326. this.ProjectUploadManager.promises.createProjectFromZipArchiveWithName(
  327. this.ownerId,
  328. this.projectName,
  329. this.zipPath
  330. )
  331. ).to.be.rejected
  332. })
  333. it('should cleanup the blank project created', function () {
  334. this.ProjectDeleter.promises.deleteProject.should.have.been.calledWith(
  335. this.project._id
  336. )
  337. })
  338. })
  339. })
  340. })