ProjectEntityUpdateHandlerTests.js 68 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600160116021603160416051606160716081609161016111612161316141615161616171618161916201621162216231624162516261627162816291630163116321633163416351636163716381639164016411642164316441645164616471648164916501651165216531654165516561657165816591660166116621663166416651666166716681669167016711672167316741675167616771678167916801681168216831684168516861687168816891690169116921693169416951696169716981699170017011702170317041705170617071708170917101711171217131714171517161717171817191720172117221723172417251726172717281729173017311732173317341735173617371738173917401741174217431744174517461747174817491750175117521753175417551756175717581759176017611762176317641765176617671768176917701771177217731774177517761777177817791780178117821783178417851786178717881789179017911792179317941795179617971798179918001801180218031804180518061807180818091810181118121813181418151816181718181819182018211822182318241825182618271828182918301831183218331834183518361837183818391840184118421843184418451846184718481849185018511852185318541855185618571858185918601861186218631864186518661867186818691870187118721873187418751876187718781879188018811882188318841885188618871888188918901891189218931894189518961897189818991900190119021903190419051906190719081909191019111912191319141915191619171918191919201921192219231924192519261927192819291930193119321933193419351936193719381939194019411942194319441945194619471948194919501951195219531954195519561957195819591960196119621963196419651966196719681969197019711972197319741975197619771978197919801981198219831984198519861987198819891990199119921993199419951996199719981999200020012002200320042005200620072008200920102011201220132014201520162017201820192020202120222023202420252026202720282029203020312032203320342035203620372038203920402041204220432044204520462047204820492050205120522053205420552056205720582059206020612062206320642065206620672068206920702071207220732074207520762077207820792080208120822083208420852086208720882089209020912092209320942095209620972098209921002101210221032104210521062107210821092110211121122113211421152116211721182119212021212122212321242125212621272128212921302131213221332134213521362137213821392140214121422143214421452146214721482149215021512152215321542155215621572158215921602161216221632164216521662167216821692170217121722173217421752176217721782179218021812182218321842185218621872188218921902191219221932194219521962197219821992200220122022203220422052206220722082209221022112212221322142215221622172218221922202221222222232224222522262227222822292230223122322233223422352236223722382239224022412242224322442245224622472248224922502251225222532254225522562257225822592260226122622263226422652266226722682269227022712272227322742275227622772278227922802281228222832284228522862287228822892290229122922293229422952296229722982299230023012302230323042305230623072308230923102311231223132314231523162317231823192320232123222323232423252326232723282329233023312332233323342335233623372338233923402341234223432344234523462347234823492350235123522353235423552356
  1. const { expect } = require('chai')
  2. const sinon = require('sinon')
  3. const Errors = require('../../../../app/src/Features/Errors/Errors')
  4. const SandboxedModule = require('sandboxed-module')
  5. const { ObjectId } = require('mongodb')
  6. const MODULE_PATH =
  7. '../../../../app/src/Features/Project/ProjectEntityUpdateHandler'
  8. describe('ProjectEntityUpdateHandler', function () {
  9. const projectId = '4eecb1c1bffa66588e0000a1'
  10. const projectHistoryId = '123456'
  11. const docId = '4eecb1c1bffa66588e0000a2'
  12. const fileId = '4eecaffcbffa66588e000009'
  13. const folderId = '4eecaffcbffa66588e000008'
  14. const newFileId = '4eecaffcbffa66588e000099'
  15. const userId = 1234
  16. beforeEach(function () {
  17. this.project = {
  18. _id: projectId,
  19. name: 'project name',
  20. overleaf: {
  21. history: {
  22. id: projectHistoryId,
  23. },
  24. },
  25. }
  26. this.fileUrl = 'filestore.example.com/file'
  27. this.user = { _id: new ObjectId() }
  28. this.DocModel = class Doc {
  29. constructor(options) {
  30. this.name = options.name
  31. this.lines = options.lines
  32. this._id = docId
  33. this.rev = 0
  34. }
  35. }
  36. this.FileModel = class File {
  37. constructor(options) {
  38. this.name = options.name
  39. // use a new id for replacement files
  40. if (this.name === 'dummy-upload-filename') {
  41. this._id = newFileId
  42. } else {
  43. this._id = fileId
  44. }
  45. this.rev = 0
  46. if (options.linkedFileData != null) {
  47. this.linkedFileData = options.linkedFileData
  48. }
  49. if (options.hash != null) {
  50. this.hash = options.hash
  51. }
  52. }
  53. }
  54. this.docName = 'doc-name'
  55. this.docLines = ['1234', 'abc']
  56. this.doc = { _id: new ObjectId(), name: this.docName }
  57. this.fileName = 'something.jpg'
  58. this.fileSystemPath = 'somehintg'
  59. this.file = { _id: new ObjectId(), name: this.fileName, rev: 2 }
  60. this.linkedFileData = { provider: 'url' }
  61. this.source = 'editor'
  62. this.callback = sinon.stub()
  63. this.DocstoreManager = {
  64. getDoc: sinon.stub(),
  65. isDocDeleted: sinon.stub(),
  66. updateDoc: sinon.stub(),
  67. deleteDoc: sinon.stub(),
  68. }
  69. this.DocumentUpdaterHandler = {
  70. flushDocToMongo: sinon.stub().yields(),
  71. updateProjectStructure: sinon.stub().yields(),
  72. setDocument: sinon.stub(),
  73. resyncProjectHistory: sinon.stub(),
  74. deleteDoc: sinon.stub().yields(),
  75. }
  76. this.fs = {
  77. unlink: sinon.stub().yields(),
  78. }
  79. this.LockManager = {
  80. runWithLock: sinon.spy((namespace, id, runner, callback) =>
  81. runner(callback)
  82. ),
  83. }
  84. this.ProjectModel = {
  85. updateOne: sinon.stub(),
  86. }
  87. this.ProjectGetter = {
  88. getProject: sinon.stub(),
  89. getProjectWithoutDocLines: sinon.stub(),
  90. }
  91. this.ProjectLocator = {
  92. findElement: sinon.stub(),
  93. findElementByPath: sinon.stub(),
  94. }
  95. this.ProjectUpdater = {
  96. markAsUpdated: sinon.stub(),
  97. }
  98. this.ProjectEntityHandler = {
  99. getDoc: sinon.stub(),
  100. getDocPathByProjectIdAndDocId: sinon.stub(),
  101. getAllEntitiesFromProject: sinon.stub(),
  102. }
  103. this.ProjectEntityMongoUpdateHandler = {
  104. addDoc: sinon.stub(),
  105. addFile: sinon.stub(),
  106. addFolder: sinon.stub(),
  107. _confirmFolder: sinon.stub(),
  108. _putElement: sinon.stub(),
  109. _insertDeletedFileReference: sinon.stub(),
  110. replaceFileWithNew: sinon.stub(),
  111. mkdirp: sinon.stub(),
  112. moveEntity: sinon.stub(),
  113. renameEntity: sinon.stub(),
  114. deleteEntity: sinon.stub(),
  115. replaceDocWithFile: sinon.stub(),
  116. replaceFileWithDoc: sinon.stub(),
  117. }
  118. this.TpdsUpdateSender = {
  119. addFile: sinon.stub().yields(),
  120. addDoc: sinon.stub(),
  121. deleteEntity: sinon.stub().yields(),
  122. moveEntity: sinon.stub(),
  123. promises: {
  124. moveEntity: sinon.stub().resolves(),
  125. },
  126. }
  127. this.FileStoreHandler = {
  128. copyFile: sinon.stub(),
  129. uploadFileFromDisk: sinon.stub(),
  130. deleteFile: sinon.stub(),
  131. }
  132. this.FileWriter = {
  133. writeLinesToDisk: sinon.stub(),
  134. }
  135. this.EditorRealTimeController = {
  136. emitToRoom: sinon.stub(),
  137. }
  138. this.ProjectEntityUpdateHandler = SandboxedModule.require(MODULE_PATH, {
  139. requires: {
  140. '@overleaf/settings': { validRootDocExtensions: ['tex'] },
  141. fs: this.fs,
  142. '../../models/Doc': { Doc: this.DocModel },
  143. '../Docstore/DocstoreManager': this.DocstoreManager,
  144. '../../Features/DocumentUpdater/DocumentUpdaterHandler': this
  145. .DocumentUpdaterHandler,
  146. '../../models/File': { File: this.FileModel },
  147. '../FileStore/FileStoreHandler': this.FileStoreHandler,
  148. '../../infrastructure/LockManager': this.LockManager,
  149. '../../models/Project': { Project: this.ProjectModel },
  150. './ProjectGetter': this.ProjectGetter,
  151. './ProjectLocator': this.ProjectLocator,
  152. './ProjectUpdateHandler': this.ProjectUpdater,
  153. './ProjectEntityHandler': this.ProjectEntityHandler,
  154. './ProjectEntityMongoUpdateHandler': this
  155. .ProjectEntityMongoUpdateHandler,
  156. '../ThirdPartyDataStore/TpdsUpdateSender': this.TpdsUpdateSender,
  157. '../Editor/EditorRealTimeController': this.EditorRealTimeController,
  158. '../../infrastructure/FileWriter': this.FileWriter,
  159. },
  160. })
  161. })
  162. describe('updateDocLines', function () {
  163. beforeEach(function () {
  164. this.path = '/somewhere/something.tex'
  165. this.doc = {
  166. _id: docId,
  167. }
  168. this.version = 42
  169. this.ranges = { mock: 'ranges' }
  170. this.lastUpdatedAt = new Date().getTime()
  171. this.lastUpdatedBy = 'fake-last-updater-id'
  172. this.DocstoreManager.isDocDeleted.yields(null, false)
  173. this.ProjectGetter.getProject.yields(null, this.project)
  174. this.ProjectLocator.findElement.yields(null, this.doc, {
  175. fileSystem: this.path,
  176. })
  177. this.TpdsUpdateSender.addDoc.yields()
  178. })
  179. describe('when the doc has been modified', function () {
  180. beforeEach(function () {
  181. this.DocstoreManager.updateDoc.yields(null, true, (this.rev = 5))
  182. this.ProjectEntityUpdateHandler.updateDocLines(
  183. projectId,
  184. docId,
  185. this.docLines,
  186. this.version,
  187. this.ranges,
  188. this.lastUpdatedAt,
  189. this.lastUpdatedBy,
  190. this.callback
  191. )
  192. })
  193. it('should get the project with very few fields', function () {
  194. this.ProjectGetter.getProject
  195. .calledWith(projectId, {
  196. name: true,
  197. rootFolder: true,
  198. })
  199. .should.equal(true)
  200. })
  201. it('should find the doc', function () {
  202. this.ProjectLocator.findElement
  203. .calledWith({
  204. project: this.project,
  205. type: 'docs',
  206. element_id: docId,
  207. })
  208. .should.equal(true)
  209. })
  210. it('should update the doc in the docstore', function () {
  211. this.DocstoreManager.updateDoc
  212. .calledWith(
  213. projectId,
  214. docId,
  215. this.docLines,
  216. this.version,
  217. this.ranges
  218. )
  219. .should.equal(true)
  220. })
  221. it('should mark the project as updated', function () {
  222. sinon.assert.calledWith(
  223. this.ProjectUpdater.markAsUpdated,
  224. projectId,
  225. this.lastUpdatedAt,
  226. this.lastUpdatedBy
  227. )
  228. })
  229. it('should send the doc the to the TPDS', function () {
  230. this.TpdsUpdateSender.addDoc
  231. .calledWith({
  232. project_id: projectId,
  233. project_name: this.project.name,
  234. doc_id: docId,
  235. rev: this.rev,
  236. path: this.path,
  237. })
  238. .should.equal(true)
  239. })
  240. it('should call the callback', function () {
  241. this.callback.called.should.equal(true)
  242. })
  243. })
  244. describe('when the doc has not been modified', function () {
  245. beforeEach(function () {
  246. this.DocstoreManager.updateDoc.yields(null, false, (this.rev = 5))
  247. this.ProjectEntityUpdateHandler.updateDocLines(
  248. projectId,
  249. docId,
  250. this.docLines,
  251. this.version,
  252. this.ranges,
  253. this.lastUpdatedAt,
  254. this.lastUpdatedBy,
  255. this.callback
  256. )
  257. })
  258. it('should not mark the project as updated', function () {
  259. this.ProjectUpdater.markAsUpdated.called.should.equal(false)
  260. })
  261. it('should not send the doc the to the TPDS', function () {
  262. this.TpdsUpdateSender.addDoc.called.should.equal(false)
  263. })
  264. it('should call the callback', function () {
  265. this.callback.called.should.equal(true)
  266. })
  267. })
  268. describe('when the doc has been deleted', function () {
  269. beforeEach(function () {
  270. this.ProjectGetter.getProject.yields(null, this.project)
  271. this.ProjectLocator.findElement.yields(new Errors.NotFoundError())
  272. this.DocstoreManager.isDocDeleted.yields(null, true)
  273. this.DocstoreManager.updateDoc.yields()
  274. this.ProjectEntityUpdateHandler.updateDocLines(
  275. projectId,
  276. docId,
  277. this.docLines,
  278. this.version,
  279. this.ranges,
  280. this.lastUpdatedAt,
  281. this.lastUpdatedBy,
  282. this.callback
  283. )
  284. })
  285. it('should update the doc in the docstore', function () {
  286. this.DocstoreManager.updateDoc
  287. .calledWith(
  288. projectId,
  289. docId,
  290. this.docLines,
  291. this.version,
  292. this.ranges
  293. )
  294. .should.equal(true)
  295. })
  296. it('should not mark the project as updated', function () {
  297. this.ProjectUpdater.markAsUpdated.called.should.equal(false)
  298. })
  299. it('should not send the doc the to the TPDS', function () {
  300. this.TpdsUpdateSender.addDoc.called.should.equal(false)
  301. })
  302. it('should call the callback', function () {
  303. this.callback.called.should.equal(true)
  304. })
  305. })
  306. describe('when projects and docs collection are de-synced', function () {
  307. beforeEach(function () {
  308. this.ProjectGetter.getProject.yields(null, this.project)
  309. // The doc is not in the file-tree, but also not marked as deleted.
  310. // This should not happen, but web should handle it.
  311. this.ProjectLocator.findElement.yields(new Errors.NotFoundError())
  312. this.DocstoreManager.isDocDeleted.yields(null, false)
  313. this.DocstoreManager.updateDoc.yields()
  314. this.ProjectEntityUpdateHandler.updateDocLines(
  315. projectId,
  316. docId,
  317. this.docLines,
  318. this.version,
  319. this.ranges,
  320. this.lastUpdatedAt,
  321. this.lastUpdatedBy,
  322. this.callback
  323. )
  324. })
  325. it('should update the doc in the docstore', function () {
  326. this.DocstoreManager.updateDoc
  327. .calledWith(
  328. projectId,
  329. docId,
  330. this.docLines,
  331. this.version,
  332. this.ranges
  333. )
  334. .should.equal(true)
  335. })
  336. it('should not mark the project as updated', function () {
  337. this.ProjectUpdater.markAsUpdated.called.should.equal(false)
  338. })
  339. it('should not send the doc the to the TPDS', function () {
  340. this.TpdsUpdateSender.addDoc.called.should.equal(false)
  341. })
  342. it('should call the callback', function () {
  343. this.callback.called.should.equal(true)
  344. })
  345. })
  346. describe('when the doc is not related to the project', function () {
  347. beforeEach(function () {
  348. this.ProjectGetter.getProject.yields(null, this.project)
  349. this.ProjectLocator.findElement.yields(new Errors.NotFoundError())
  350. this.DocstoreManager.isDocDeleted.yields(new Errors.NotFoundError())
  351. this.ProjectEntityUpdateHandler.updateDocLines(
  352. projectId,
  353. docId,
  354. this.docLines,
  355. this.version,
  356. this.ranges,
  357. this.lastUpdatedAt,
  358. this.lastUpdatedBy,
  359. this.callback
  360. )
  361. })
  362. it('should return a not found error', function () {
  363. this.callback
  364. .calledWith(sinon.match.instanceOf(Errors.NotFoundError))
  365. .should.equal(true)
  366. })
  367. it('should not update the doc', function () {
  368. this.DocstoreManager.updateDoc.called.should.equal(false)
  369. })
  370. it('should not send the doc the to the TPDS', function () {
  371. this.TpdsUpdateSender.addDoc.called.should.equal(false)
  372. })
  373. })
  374. describe('when the project is not found', function () {
  375. beforeEach(function () {
  376. this.ProjectGetter.getProject.yields(new Errors.NotFoundError())
  377. this.ProjectEntityUpdateHandler.updateDocLines(
  378. projectId,
  379. docId,
  380. this.docLines,
  381. this.version,
  382. this.ranges,
  383. this.lastUpdatedAt,
  384. this.lastUpdatedBy,
  385. this.callback
  386. )
  387. })
  388. it('should return a not found error', function () {
  389. this.callback
  390. .calledWith(sinon.match.instanceOf(Errors.NotFoundError))
  391. .should.equal(true)
  392. })
  393. it('should not update the doc', function () {
  394. this.DocstoreManager.updateDoc.called.should.equal(false)
  395. })
  396. it('should not send the doc the to the TPDS', function () {
  397. this.TpdsUpdateSender.addDoc.called.should.equal(false)
  398. })
  399. })
  400. })
  401. describe('setRootDoc', function () {
  402. beforeEach(function () {
  403. this.rootDocId = 'root-doc-id-123123'
  404. })
  405. it('should call Project.updateOne when the doc exists and has a valid extension', function () {
  406. this.ProjectEntityHandler.getDocPathByProjectIdAndDocId.yields(
  407. null,
  408. `/main.tex`
  409. )
  410. this.ProjectEntityUpdateHandler.setRootDoc(
  411. projectId,
  412. this.rootDocId,
  413. () => {}
  414. )
  415. this.ProjectModel.updateOne
  416. .calledWith({ _id: projectId }, { rootDoc_id: this.rootDocId })
  417. .should.equal(true)
  418. })
  419. it("should not call Project.updateOne when the doc doesn't exist", function () {
  420. this.ProjectEntityHandler.getDocPathByProjectIdAndDocId.yields(
  421. Errors.NotFoundError
  422. )
  423. this.ProjectEntityUpdateHandler.setRootDoc(
  424. projectId,
  425. this.rootDocId,
  426. () => {}
  427. )
  428. this.ProjectModel.updateOne
  429. .calledWith({ _id: projectId }, { rootDoc_id: this.rootDocId })
  430. .should.equal(false)
  431. })
  432. it('should call the callback with an UnsupportedFileTypeError when the doc has an unaccepted file extension', function () {
  433. this.ProjectEntityHandler.getDocPathByProjectIdAndDocId.yields(
  434. null,
  435. `/foo/bar.baz`
  436. )
  437. this.ProjectEntityUpdateHandler.setRootDoc(
  438. projectId,
  439. this.rootDocId,
  440. this.callback
  441. )
  442. expect(this.callback.firstCall.args[0]).to.be.an.instanceof(
  443. Errors.UnsupportedFileTypeError
  444. )
  445. })
  446. })
  447. describe('unsetRootDoc', function () {
  448. it('should call Project.updateOne', function () {
  449. this.ProjectEntityUpdateHandler.unsetRootDoc(projectId)
  450. this.ProjectModel.updateOne
  451. .calledWith({ _id: projectId }, { $unset: { rootDoc_id: true } })
  452. .should.equal(true)
  453. })
  454. })
  455. describe('addDoc', function () {
  456. describe('adding a doc', function () {
  457. beforeEach(function () {
  458. this.path = '/path/to/doc'
  459. this.newDoc = new this.DocModel({
  460. name: this.docName,
  461. lines: undefined,
  462. _id: docId,
  463. rev: 0,
  464. })
  465. this.DocstoreManager.updateDoc.yields(null, false, (this.rev = 5))
  466. this.TpdsUpdateSender.addDoc.yields()
  467. this.ProjectEntityMongoUpdateHandler.addDoc.yields(
  468. null,
  469. { path: { fileSystem: this.path } },
  470. this.project
  471. )
  472. this.ProjectEntityUpdateHandler.addDoc(
  473. projectId,
  474. docId,
  475. this.docName,
  476. this.docLines,
  477. userId,
  478. this.callback
  479. )
  480. })
  481. it('creates the doc without history', function () {
  482. this.DocstoreManager.updateDoc
  483. .calledWith(projectId, docId, this.docLines, 0, {})
  484. .should.equal(true)
  485. })
  486. it('sends the change in project structure to the doc updater', function () {
  487. const newDocs = [
  488. {
  489. doc: this.newDoc,
  490. path: this.path,
  491. docLines: this.docLines.join('\n'),
  492. },
  493. ]
  494. this.DocumentUpdaterHandler.updateProjectStructure
  495. .calledWith(projectId, projectHistoryId, userId, {
  496. newDocs,
  497. newProject: this.project,
  498. })
  499. .should.equal(true)
  500. })
  501. })
  502. describe('adding a doc with an invalid name', function () {
  503. beforeEach(function () {
  504. this.path = '/path/to/doc'
  505. this.newDoc = { _id: docId }
  506. this.ProjectEntityUpdateHandler.addDoc(
  507. projectId,
  508. folderId,
  509. `*${this.docName}`,
  510. this.docLines,
  511. userId,
  512. this.callback
  513. )
  514. })
  515. it('returns an error', function () {
  516. const errorMatcher = sinon.match.instanceOf(Errors.InvalidNameError)
  517. this.callback.calledWithMatch(errorMatcher).should.equal(true)
  518. })
  519. })
  520. })
  521. describe('addFile', function () {
  522. describe('adding a file', function () {
  523. beforeEach(function () {
  524. this.path = '/path/to/file'
  525. this.newFile = {
  526. _id: fileId,
  527. rev: 0,
  528. name: this.fileName,
  529. linkedFileData: this.linkedFileData,
  530. }
  531. this.FileStoreHandler.uploadFileFromDisk.yields(
  532. null,
  533. this.fileUrl,
  534. this.newFile
  535. )
  536. this.TpdsUpdateSender.addFile.yields()
  537. this.ProjectEntityMongoUpdateHandler.addFile.yields(
  538. null,
  539. { path: { fileSystem: this.path } },
  540. this.project
  541. )
  542. this.ProjectEntityUpdateHandler.addFile(
  543. projectId,
  544. folderId,
  545. this.fileName,
  546. this.fileSystemPath,
  547. this.linkedFileData,
  548. userId,
  549. this.callback
  550. )
  551. })
  552. it('updates the file in the filestore', function () {
  553. this.FileStoreHandler.uploadFileFromDisk
  554. .calledWith(
  555. projectId,
  556. { name: this.fileName, linkedFileData: this.linkedFileData },
  557. this.fileSystemPath
  558. )
  559. .should.equal(true)
  560. })
  561. it('updates the file in mongo', function () {
  562. const fileMatcher = sinon.match(file => {
  563. return file.name === this.fileName
  564. })
  565. this.ProjectEntityMongoUpdateHandler.addFile
  566. .calledWithMatch(projectId, folderId, fileMatcher)
  567. .should.equal(true)
  568. })
  569. it('notifies the tpds', function () {
  570. this.TpdsUpdateSender.addFile
  571. .calledWith({
  572. project_id: projectId,
  573. project_name: this.project.name,
  574. file_id: fileId,
  575. rev: 0,
  576. path: this.path,
  577. })
  578. .should.equal(true)
  579. })
  580. it('should mark the project as updated', function () {
  581. const args = this.ProjectUpdater.markAsUpdated.args[0]
  582. args[0].should.equal(projectId)
  583. args[1].should.exist
  584. args[2].should.equal(userId)
  585. })
  586. it('sends the change in project structure to the doc updater', function () {
  587. const newFiles = [
  588. {
  589. file: this.newFile,
  590. path: this.path,
  591. url: this.fileUrl,
  592. },
  593. ]
  594. this.DocumentUpdaterHandler.updateProjectStructure
  595. .calledWith(projectId, projectHistoryId, userId, {
  596. newFiles,
  597. newProject: this.project,
  598. })
  599. .should.equal(true)
  600. })
  601. })
  602. describe('adding a file with an invalid name', function () {
  603. beforeEach(function () {
  604. this.path = '/path/to/file'
  605. this.newFile = {
  606. _id: fileId,
  607. rev: 0,
  608. name: this.fileName,
  609. linkedFileData: this.linkedFileData,
  610. }
  611. this.TpdsUpdateSender.addFile.yields()
  612. this.ProjectEntityMongoUpdateHandler.addFile.yields(
  613. null,
  614. { path: { fileSystem: this.path } },
  615. this.project
  616. )
  617. this.ProjectEntityUpdateHandler.addFile(
  618. projectId,
  619. folderId,
  620. `*${this.fileName}`,
  621. this.fileSystemPath,
  622. this.linkedFileData,
  623. userId,
  624. this.callback
  625. )
  626. })
  627. it('returns an error', function () {
  628. const errorMatcher = sinon.match.instanceOf(Errors.InvalidNameError)
  629. this.callback.calledWithMatch(errorMatcher).should.equal(true)
  630. })
  631. })
  632. })
  633. describe('replaceFile', function () {
  634. beforeEach(function () {
  635. // replacement file now creates a new file object
  636. this.newFileUrl = 'new-file-url'
  637. this.newFile = {
  638. _id: newFileId,
  639. name: 'dummy-upload-filename',
  640. rev: 0,
  641. linkedFileData: this.linkedFileData,
  642. }
  643. this.oldFile = { _id: fileId, rev: 3 }
  644. this.path = '/path/to/file'
  645. this.newProject = 'new project'
  646. this.FileStoreHandler.uploadFileFromDisk.yields(
  647. null,
  648. this.newFileUrl,
  649. this.newFile
  650. )
  651. this.ProjectEntityMongoUpdateHandler._insertDeletedFileReference.yields()
  652. this.ProjectEntityMongoUpdateHandler.replaceFileWithNew.yields(
  653. null,
  654. this.oldFile,
  655. this.project,
  656. { fileSystem: this.path },
  657. this.newProject
  658. )
  659. this.ProjectEntityUpdateHandler.replaceFile(
  660. projectId,
  661. fileId,
  662. this.fileSystemPath,
  663. this.linkedFileData,
  664. userId,
  665. this.callback
  666. )
  667. })
  668. it('uploads a new version of the file', function () {
  669. this.FileStoreHandler.uploadFileFromDisk
  670. .calledWith(
  671. projectId,
  672. {
  673. name: 'dummy-upload-filename',
  674. linkedFileData: this.linkedFileData,
  675. },
  676. this.fileSystemPath
  677. )
  678. .should.equal(true)
  679. })
  680. it('replaces the file in mongo', function () {
  681. this.ProjectEntityMongoUpdateHandler.replaceFileWithNew
  682. .calledWith(projectId, fileId, this.newFile)
  683. .should.equal(true)
  684. })
  685. it('notifies the tpds', function () {
  686. this.TpdsUpdateSender.addFile
  687. .calledWith({
  688. project_id: projectId,
  689. project_name: this.project.name,
  690. file_id: newFileId,
  691. rev: this.oldFile.rev + 1,
  692. path: this.path,
  693. })
  694. .should.equal(true)
  695. })
  696. it('should mark the project as updated', function () {
  697. const args = this.ProjectUpdater.markAsUpdated.args[0]
  698. args[0].should.equal(projectId)
  699. args[1].should.exist
  700. args[2].should.equal(userId)
  701. })
  702. it('updates the project structure in the doc updater', function () {
  703. const oldFiles = [
  704. {
  705. file: this.oldFile,
  706. path: this.path,
  707. },
  708. ]
  709. const newFiles = [
  710. {
  711. file: this.newFile,
  712. path: this.path,
  713. url: this.newFileUrl,
  714. },
  715. ]
  716. this.DocumentUpdaterHandler.updateProjectStructure
  717. .calledWith(projectId, projectHistoryId, userId, {
  718. oldFiles,
  719. newFiles,
  720. newProject: this.newProject,
  721. })
  722. .should.equal(true)
  723. })
  724. })
  725. describe('upsertDoc', function () {
  726. describe('upserting into an invalid folder', function () {
  727. beforeEach(function () {
  728. this.ProjectLocator.findElement.yields()
  729. this.ProjectEntityUpdateHandler.upsertDoc(
  730. projectId,
  731. folderId,
  732. this.docName,
  733. this.docLines,
  734. this.source,
  735. userId,
  736. this.callback
  737. )
  738. })
  739. it('returns an error', function () {
  740. const errorMatcher = sinon.match.instanceOf(Error)
  741. this.callback.calledWithMatch(errorMatcher).should.equal(true)
  742. })
  743. })
  744. describe('updating an existing doc', function () {
  745. beforeEach(function () {
  746. this.existingDoc = { _id: docId, name: this.docName }
  747. this.existingFile = { _id: fileId, name: this.fileName }
  748. this.folder = {
  749. _id: folderId,
  750. docs: [this.existingDoc],
  751. fileRefs: [this.existingFile],
  752. }
  753. this.ProjectLocator.findElement.yields(null, this.folder)
  754. this.DocumentUpdaterHandler.setDocument.yields()
  755. this.ProjectEntityUpdateHandler.upsertDoc(
  756. projectId,
  757. folderId,
  758. this.docName,
  759. this.docLines,
  760. this.source,
  761. userId,
  762. this.callback
  763. )
  764. })
  765. it('tries to find the folder', function () {
  766. this.ProjectLocator.findElement
  767. .calledWith({
  768. project_id: projectId,
  769. element_id: folderId,
  770. type: 'folder',
  771. })
  772. .should.equal(true)
  773. })
  774. it('updates the doc contents', function () {
  775. this.DocumentUpdaterHandler.setDocument
  776. .calledWith(
  777. projectId,
  778. this.existingDoc._id,
  779. userId,
  780. this.docLines,
  781. this.source
  782. )
  783. .should.equal(true)
  784. })
  785. it('flushes the doc contents', function () {
  786. this.DocumentUpdaterHandler.flushDocToMongo
  787. .calledWith(projectId, this.existingDoc._id)
  788. .should.equal(true)
  789. })
  790. it('returns the doc', function () {
  791. this.callback.calledWith(null, this.existingDoc, false)
  792. })
  793. })
  794. describe('creating a new doc', function () {
  795. beforeEach(function () {
  796. this.folder = { _id: folderId, docs: [], fileRefs: [] }
  797. this.newDoc = { _id: docId }
  798. this.ProjectLocator.findElement.yields(null, this.folder)
  799. this.ProjectEntityUpdateHandler.addDocWithRanges = {
  800. withoutLock: sinon.stub().yields(null, this.newDoc),
  801. }
  802. this.ProjectEntityUpdateHandler.upsertDoc(
  803. projectId,
  804. folderId,
  805. this.docName,
  806. this.docLines,
  807. this.source,
  808. userId,
  809. this.callback
  810. )
  811. })
  812. it('tries to find the folder', function () {
  813. this.ProjectLocator.findElement
  814. .calledWith({
  815. project_id: projectId,
  816. element_id: folderId,
  817. type: 'folder',
  818. })
  819. .should.equal(true)
  820. })
  821. it('adds the doc', function () {
  822. this.ProjectEntityUpdateHandler.addDocWithRanges.withoutLock
  823. .calledWith(
  824. projectId,
  825. folderId,
  826. this.docName,
  827. this.docLines,
  828. {},
  829. userId
  830. )
  831. .should.equal(true)
  832. })
  833. it('returns the doc', function () {
  834. this.callback.calledWith(null, this.newDoc, true)
  835. })
  836. })
  837. describe('upserting a new doc with an invalid name', function () {
  838. beforeEach(function () {
  839. this.folder = { _id: folderId, docs: [], fileRefs: [] }
  840. this.newDoc = { _id: docId }
  841. this.ProjectLocator.findElement.yields(null, this.folder)
  842. this.ProjectEntityUpdateHandler.addDocWithRanges = {
  843. withoutLock: sinon.stub().yields(null, this.newDoc),
  844. }
  845. this.ProjectEntityUpdateHandler.upsertDoc(
  846. projectId,
  847. folderId,
  848. `*${this.docName}`,
  849. this.docLines,
  850. this.source,
  851. userId,
  852. this.callback
  853. )
  854. })
  855. it('returns an error', function () {
  856. const errorMatcher = sinon.match.instanceOf(Errors.InvalidNameError)
  857. this.callback.calledWithMatch(errorMatcher).should.equal(true)
  858. })
  859. })
  860. describe('upserting a doc on top of a file', function () {
  861. beforeEach(function () {
  862. this.newProject = {
  863. name: 'new project',
  864. overleaf: { history: { id: projectHistoryId } },
  865. }
  866. this.existingFile = { _id: fileId, name: 'foo.tex', rev: 12 }
  867. this.folder = { _id: folderId, docs: [], fileRefs: [this.existingFile] }
  868. this.newDoc = { _id: docId }
  869. this.docLines = ['line one', 'line two']
  870. this.folderPath = '/path/to/folder'
  871. this.filePath = '/path/to/folder/foo.tex'
  872. this.ProjectLocator.findElement
  873. .withArgs({
  874. project_id: projectId,
  875. element_id: this.folder._id,
  876. type: 'folder',
  877. })
  878. .yields(null, this.folder, {
  879. fileSystem: this.folderPath,
  880. })
  881. this.DocstoreManager.updateDoc.yields()
  882. this.ProjectEntityMongoUpdateHandler.replaceFileWithDoc.yields(
  883. null,
  884. this.newProject
  885. )
  886. this.TpdsUpdateSender.addDoc.yields()
  887. this.ProjectEntityUpdateHandler.upsertDoc(
  888. projectId,
  889. folderId,
  890. 'foo.tex',
  891. this.docLines,
  892. this.source,
  893. userId,
  894. this.callback
  895. )
  896. })
  897. it('notifies docstore of the new doc', function () {
  898. expect(this.DocstoreManager.updateDoc).to.have.been.calledWith(
  899. projectId,
  900. this.newDoc._id,
  901. this.docLines
  902. )
  903. })
  904. it('adds the new doc and removes the file in one go', function () {
  905. expect(
  906. this.ProjectEntityMongoUpdateHandler.replaceFileWithDoc
  907. ).to.have.been.calledWithMatch(
  908. projectId,
  909. this.existingFile._id,
  910. this.newDoc
  911. )
  912. })
  913. it('sends the doc to TPDS', function () {
  914. expect(this.TpdsUpdateSender.addDoc).to.have.been.calledWith({
  915. project_id: projectId,
  916. doc_id: this.newDoc._id,
  917. path: this.filePath,
  918. project_name: this.newProject.name,
  919. rev: this.existingFile.rev + 1,
  920. })
  921. })
  922. it('sends the updates to the doc updater', function () {
  923. const oldFiles = [
  924. {
  925. file: this.existingFile,
  926. path: this.filePath,
  927. },
  928. ]
  929. const newDocs = [
  930. {
  931. doc: sinon.match(this.newDoc),
  932. path: this.filePath,
  933. docLines: this.docLines.join('\n'),
  934. },
  935. ]
  936. expect(
  937. this.DocumentUpdaterHandler.updateProjectStructure
  938. ).to.have.been.calledWith(projectId, projectHistoryId, userId, {
  939. oldFiles,
  940. newDocs,
  941. newProject: this.newProject,
  942. })
  943. })
  944. it('should notify everyone of the file deletion', function () {
  945. expect(
  946. this.EditorRealTimeController.emitToRoom
  947. ).to.have.been.calledWith(
  948. projectId,
  949. 'removeEntity',
  950. this.existingFile._id,
  951. 'convertFileToDoc'
  952. )
  953. })
  954. })
  955. })
  956. describe('upsertFile', function () {
  957. beforeEach(function () {
  958. this.FileStoreHandler.uploadFileFromDisk.yields(
  959. null,
  960. this.fileUrl,
  961. this.newFile
  962. )
  963. })
  964. describe('upserting into an invalid folder', function () {
  965. beforeEach(function () {
  966. this.ProjectLocator.findElement.yields()
  967. this.ProjectEntityUpdateHandler.upsertFile(
  968. projectId,
  969. folderId,
  970. this.fileName,
  971. this.fileSystemPath,
  972. this.linkedFileData,
  973. userId,
  974. this.callback
  975. )
  976. })
  977. it('returns an error', function () {
  978. const errorMatcher = sinon.match.instanceOf(Error)
  979. this.callback.calledWithMatch(errorMatcher).should.equal(true)
  980. })
  981. })
  982. describe('updating an existing file', function () {
  983. beforeEach(function () {
  984. this.existingFile = { _id: fileId, name: this.fileName }
  985. this.folder = { _id: folderId, fileRefs: [this.existingFile], docs: [] }
  986. this.ProjectLocator.findElement.yields(null, this.folder)
  987. this.ProjectEntityUpdateHandler.replaceFile = {
  988. mainTask: sinon.stub().yields(null, this.newFile),
  989. }
  990. this.ProjectEntityUpdateHandler.upsertFile(
  991. projectId,
  992. folderId,
  993. this.fileName,
  994. this.fileSystemPath,
  995. this.linkedFileData,
  996. userId,
  997. this.callback
  998. )
  999. })
  1000. it('replaces the file', function () {
  1001. expect(
  1002. this.ProjectEntityUpdateHandler.replaceFile.mainTask
  1003. ).to.be.calledWith(
  1004. projectId,
  1005. fileId,
  1006. this.fileSystemPath,
  1007. this.linkedFileData,
  1008. userId
  1009. )
  1010. })
  1011. it('returns the file', function () {
  1012. this.callback.calledWith(null, this.existingFile, false)
  1013. })
  1014. })
  1015. describe('creating a new file', function () {
  1016. beforeEach(function () {
  1017. this.folder = { _id: folderId, fileRefs: [], docs: [] }
  1018. this.newFile = { _id: fileId }
  1019. this.ProjectLocator.findElement.yields(null, this.folder)
  1020. this.ProjectEntityUpdateHandler.addFile = {
  1021. mainTask: sinon.stub().yields(null, this.newFile),
  1022. }
  1023. this.ProjectEntityUpdateHandler.upsertFile(
  1024. projectId,
  1025. folderId,
  1026. this.fileName,
  1027. this.fileSystemPath,
  1028. this.linkedFileData,
  1029. userId,
  1030. this.callback
  1031. )
  1032. })
  1033. it('tries to find the folder', function () {
  1034. this.ProjectLocator.findElement
  1035. .calledWith({
  1036. project_id: projectId,
  1037. element_id: folderId,
  1038. type: 'folder',
  1039. })
  1040. .should.equal(true)
  1041. })
  1042. it('adds the file', function () {
  1043. this.ProjectEntityUpdateHandler.addFile.mainTask
  1044. .calledWith(
  1045. projectId,
  1046. folderId,
  1047. this.fileName,
  1048. this.fileSystemPath,
  1049. this.linkedFileData,
  1050. userId
  1051. )
  1052. .should.equal(true)
  1053. })
  1054. it('returns the file', function () {
  1055. this.callback.calledWith(null, this.newFile, true)
  1056. })
  1057. })
  1058. describe('upserting a new file with an invalid name', function () {
  1059. beforeEach(function () {
  1060. this.folder = { _id: folderId, fileRefs: [] }
  1061. this.newFile = { _id: fileId }
  1062. this.ProjectLocator.findElement.yields(null, this.folder)
  1063. this.ProjectEntityUpdateHandler.addFile = {
  1064. mainTask: sinon.stub().yields(null, this.newFile),
  1065. }
  1066. this.ProjectEntityUpdateHandler.upsertFile(
  1067. projectId,
  1068. folderId,
  1069. `*${this.fileName}`,
  1070. this.fileSystemPath,
  1071. this.linkedFileData,
  1072. userId,
  1073. this.callback
  1074. )
  1075. })
  1076. it('returns an error', function () {
  1077. const errorMatcher = sinon.match.instanceOf(Errors.InvalidNameError)
  1078. this.callback.calledWithMatch(errorMatcher).should.equal(true)
  1079. })
  1080. })
  1081. describe('upserting file on top of a doc', function () {
  1082. beforeEach(function (done) {
  1083. this.path = '/path/to/doc'
  1084. this.existingDoc = { _id: new ObjectId(), name: this.fileName }
  1085. this.folder = {
  1086. _id: folderId,
  1087. fileRefs: [],
  1088. docs: [this.existingDoc],
  1089. }
  1090. this.ProjectLocator.findElement
  1091. .withArgs({
  1092. project_id: this.project._id.toString(),
  1093. element_id: folderId,
  1094. type: 'folder',
  1095. })
  1096. .yields(null, this.folder)
  1097. this.ProjectLocator.findElement
  1098. .withArgs({
  1099. project_id: this.project._id.toString(),
  1100. element_id: this.existingDoc._id,
  1101. type: 'doc',
  1102. })
  1103. .yields(null, this.existingDoc, { fileSystem: this.path })
  1104. this.newFileUrl = 'new-file-url'
  1105. this.newFile = {
  1106. _id: newFileId,
  1107. name: 'dummy-upload-filename',
  1108. rev: 0,
  1109. linkedFileData: this.linkedFileData,
  1110. }
  1111. this.newProject = {
  1112. name: 'new project',
  1113. overleaf: { history: { id: projectHistoryId } },
  1114. }
  1115. this.FileStoreHandler.uploadFileFromDisk.yields(
  1116. null,
  1117. this.newFileUrl,
  1118. this.newFile
  1119. )
  1120. this.ProjectEntityMongoUpdateHandler.replaceDocWithFile.yields(
  1121. null,
  1122. this.newProject
  1123. )
  1124. this.ProjectEntityUpdateHandler.upsertFile(
  1125. projectId,
  1126. folderId,
  1127. this.fileName,
  1128. this.fileSystemPath,
  1129. this.linkedFileData,
  1130. userId,
  1131. done
  1132. )
  1133. })
  1134. it('replaces the existing doc with a file', function () {
  1135. expect(
  1136. this.ProjectEntityMongoUpdateHandler.replaceDocWithFile
  1137. ).to.have.been.calledWith(projectId, this.existingDoc._id, this.newFile)
  1138. })
  1139. it('updates the doc structure', function () {
  1140. const oldDocs = [
  1141. {
  1142. doc: this.existingDoc,
  1143. path: this.path,
  1144. },
  1145. ]
  1146. const newFiles = [
  1147. {
  1148. file: this.newFile,
  1149. path: this.path,
  1150. url: this.newFileUrl,
  1151. },
  1152. ]
  1153. const updates = {
  1154. oldDocs,
  1155. newFiles,
  1156. newProject: this.newProject,
  1157. }
  1158. expect(
  1159. this.DocumentUpdaterHandler.updateProjectStructure
  1160. ).to.have.been.calledWith(projectId, projectHistoryId, userId, updates)
  1161. })
  1162. it('tells everyone in the room the doc is removed', function () {
  1163. expect(
  1164. this.EditorRealTimeController.emitToRoom
  1165. ).to.have.been.calledWith(
  1166. projectId,
  1167. 'removeEntity',
  1168. this.existingDoc._id,
  1169. 'convertDocToFile'
  1170. )
  1171. })
  1172. })
  1173. })
  1174. describe('upsertDocWithPath', function () {
  1175. describe('upserting a doc', function () {
  1176. beforeEach(function () {
  1177. this.path = '/folder/doc.tex'
  1178. this.newFolders = ['mock-a', 'mock-b']
  1179. this.folder = { _id: folderId }
  1180. this.doc = { _id: docId }
  1181. this.isNewDoc = true
  1182. this.ProjectEntityUpdateHandler.mkdirp = {
  1183. withoutLock: sinon.stub().yields(null, this.newFolders, this.folder),
  1184. }
  1185. this.ProjectEntityUpdateHandler.upsertDoc = {
  1186. withoutLock: sinon.stub().yields(null, this.doc, this.isNewDoc),
  1187. }
  1188. this.ProjectEntityUpdateHandler.upsertDocWithPath(
  1189. projectId,
  1190. this.path,
  1191. this.docLines,
  1192. this.source,
  1193. userId,
  1194. this.callback
  1195. )
  1196. })
  1197. it('creates any necessary folders', function () {
  1198. this.ProjectEntityUpdateHandler.mkdirp.withoutLock
  1199. .calledWith(projectId, '/folder')
  1200. .should.equal(true)
  1201. })
  1202. it('upserts the doc', function () {
  1203. this.ProjectEntityUpdateHandler.upsertDoc.withoutLock
  1204. .calledWith(
  1205. projectId,
  1206. this.folder._id,
  1207. 'doc.tex',
  1208. this.docLines,
  1209. this.source,
  1210. userId
  1211. )
  1212. .should.equal(true)
  1213. })
  1214. it('calls the callback', function () {
  1215. this.callback
  1216. .calledWith(
  1217. null,
  1218. this.doc,
  1219. this.isNewDoc,
  1220. this.newFolders,
  1221. this.folder
  1222. )
  1223. .should.equal(true)
  1224. })
  1225. })
  1226. describe('upserting a doc with an invalid path', function () {
  1227. beforeEach(function () {
  1228. this.path = '/*folder/doc.tex'
  1229. this.newFolders = ['mock-a', 'mock-b']
  1230. this.folder = { _id: folderId }
  1231. this.doc = { _id: docId }
  1232. this.isNewDoc = true
  1233. this.ProjectEntityUpdateHandler.mkdirp = {
  1234. withoutLock: sinon.stub().yields(null, this.newFolders, this.folder),
  1235. }
  1236. this.ProjectEntityUpdateHandler.upsertDoc = {
  1237. withoutLock: sinon.stub().yields(null, this.doc, this.isNewDoc),
  1238. }
  1239. this.ProjectEntityUpdateHandler.upsertDocWithPath(
  1240. projectId,
  1241. this.path,
  1242. this.docLines,
  1243. this.source,
  1244. userId,
  1245. this.callback
  1246. )
  1247. })
  1248. it('returns an error', function () {
  1249. const errorMatcher = sinon.match.instanceOf(Errors.InvalidNameError)
  1250. this.callback.calledWithMatch(errorMatcher).should.equal(true)
  1251. })
  1252. })
  1253. describe('upserting a doc with an invalid name', function () {
  1254. beforeEach(function () {
  1255. this.path = '/folder/*doc.tex'
  1256. this.newFolders = ['mock-a', 'mock-b']
  1257. this.folder = { _id: folderId }
  1258. this.doc = { _id: docId }
  1259. this.isNewDoc = true
  1260. this.ProjectEntityUpdateHandler.mkdirp = {
  1261. withoutLock: sinon.stub().yields(null, this.newFolders, this.folder),
  1262. }
  1263. this.ProjectEntityUpdateHandler.upsertDoc = {
  1264. withoutLock: sinon.stub().yields(null, this.doc, this.isNewDoc),
  1265. }
  1266. this.ProjectEntityUpdateHandler.upsertDocWithPath(
  1267. projectId,
  1268. this.path,
  1269. this.docLines,
  1270. this.source,
  1271. userId,
  1272. this.callback
  1273. )
  1274. })
  1275. it('returns an error', function () {
  1276. const errorMatcher = sinon.match.instanceOf(Errors.InvalidNameError)
  1277. this.callback.calledWithMatch(errorMatcher).should.equal(true)
  1278. })
  1279. })
  1280. })
  1281. describe('upsertFileWithPath', function () {
  1282. describe('upserting a file', function () {
  1283. beforeEach(function () {
  1284. this.path = '/folder/file.png'
  1285. this.newFolders = ['mock-a', 'mock-b']
  1286. this.folder = { _id: folderId }
  1287. this.file = { _id: fileId }
  1288. this.isNewFile = true
  1289. this.FileStoreHandler.uploadFileFromDisk.yields(
  1290. null,
  1291. this.fileUrl,
  1292. this.newFile
  1293. )
  1294. this.ProjectEntityUpdateHandler.mkdirp = {
  1295. withoutLock: sinon.stub().yields(null, this.newFolders, this.folder),
  1296. }
  1297. this.ProjectEntityUpdateHandler.upsertFile = {
  1298. mainTask: sinon.stub().yields(null, this.file, this.isNewFile),
  1299. }
  1300. this.ProjectEntityUpdateHandler.upsertFileWithPath(
  1301. projectId,
  1302. this.path,
  1303. this.fileSystemPath,
  1304. this.linkedFileData,
  1305. userId,
  1306. this.callback
  1307. )
  1308. })
  1309. it('creates any necessary folders', function () {
  1310. this.ProjectEntityUpdateHandler.mkdirp.withoutLock
  1311. .calledWith(projectId, '/folder')
  1312. .should.equal(true)
  1313. })
  1314. it('upserts the file', function () {
  1315. this.ProjectEntityUpdateHandler.upsertFile.mainTask
  1316. .calledWith(
  1317. projectId,
  1318. this.folder._id,
  1319. 'file.png',
  1320. this.fileSystemPath,
  1321. this.linkedFileData,
  1322. userId
  1323. )
  1324. .should.equal(true)
  1325. })
  1326. it('calls the callback', function () {
  1327. this.callback
  1328. .calledWith(
  1329. null,
  1330. this.file,
  1331. this.isNewFile,
  1332. undefined,
  1333. this.newFolders,
  1334. this.folder
  1335. )
  1336. .should.equal(true)
  1337. })
  1338. })
  1339. describe('upserting a file with an invalid path', function () {
  1340. beforeEach(function () {
  1341. this.path = '/*folder/file.png'
  1342. this.newFolders = ['mock-a', 'mock-b']
  1343. this.folder = { _id: folderId }
  1344. this.file = { _id: fileId }
  1345. this.isNewFile = true
  1346. this.ProjectEntityUpdateHandler.mkdirp = {
  1347. withoutLock: sinon.stub().yields(null, this.newFolders, this.folder),
  1348. }
  1349. this.ProjectEntityUpdateHandler.upsertFile = {
  1350. mainTask: sinon.stub().yields(null, this.file, this.isNewFile),
  1351. }
  1352. this.ProjectEntityUpdateHandler.upsertFileWithPath(
  1353. projectId,
  1354. this.path,
  1355. this.fileSystemPath,
  1356. this.linkedFileData,
  1357. userId,
  1358. this.callback
  1359. )
  1360. })
  1361. it('returns an error', function () {
  1362. const errorMatcher = sinon.match.instanceOf(Errors.InvalidNameError)
  1363. this.callback.calledWithMatch(errorMatcher).should.equal(true)
  1364. })
  1365. })
  1366. describe('upserting a file with an invalid name', function () {
  1367. beforeEach(function () {
  1368. this.path = '/folder/*file.png'
  1369. this.newFolders = ['mock-a', 'mock-b']
  1370. this.folder = { _id: folderId }
  1371. this.file = { _id: fileId }
  1372. this.isNewFile = true
  1373. this.ProjectEntityUpdateHandler.mkdirp = {
  1374. withoutLock: sinon.stub().yields(null, this.newFolders, this.folder),
  1375. }
  1376. this.ProjectEntityUpdateHandler.upsertFile = {
  1377. mainTask: sinon.stub().yields(null, this.file, this.isNewFile),
  1378. }
  1379. this.ProjectEntityUpdateHandler.upsertFileWithPath(
  1380. projectId,
  1381. this.path,
  1382. this.fileSystemPath,
  1383. this.linkedFileData,
  1384. userId,
  1385. this.callback
  1386. )
  1387. })
  1388. it('returns an error', function () {
  1389. const errorMatcher = sinon.match.instanceOf(Errors.InvalidNameError)
  1390. this.callback.calledWithMatch(errorMatcher).should.equal(true)
  1391. })
  1392. })
  1393. })
  1394. describe('deleteEntity', function () {
  1395. beforeEach(function () {
  1396. this.path = '/path/to/doc.tex'
  1397. this.doc = { _id: docId }
  1398. this.projectBeforeDeletion = { _id: projectId, name: 'project' }
  1399. this.newProject = 'new-project'
  1400. this.ProjectEntityMongoUpdateHandler.deleteEntity.yields(
  1401. null,
  1402. this.doc,
  1403. { fileSystem: this.path },
  1404. this.projectBeforeDeletion,
  1405. this.newProject
  1406. )
  1407. this.ProjectEntityUpdateHandler._cleanUpEntity = sinon.stub().yields()
  1408. this.ProjectEntityUpdateHandler.deleteEntity(
  1409. projectId,
  1410. docId,
  1411. 'doc',
  1412. userId,
  1413. this.callback
  1414. )
  1415. })
  1416. it('deletes the entity in mongo', function () {
  1417. this.ProjectEntityMongoUpdateHandler.deleteEntity
  1418. .calledWith(projectId, docId, 'doc')
  1419. .should.equal(true)
  1420. })
  1421. it('cleans up the doc in the docstore', function () {
  1422. this.ProjectEntityUpdateHandler._cleanUpEntity
  1423. .calledWith(
  1424. this.projectBeforeDeletion,
  1425. this.newProject,
  1426. this.doc,
  1427. 'doc',
  1428. this.path,
  1429. userId
  1430. )
  1431. .should.equal(true)
  1432. })
  1433. it('it notifies the tpds', function () {
  1434. this.TpdsUpdateSender.deleteEntity
  1435. .calledWith({
  1436. project_id: projectId,
  1437. path: this.path,
  1438. project_name: this.projectBeforeDeletion.name,
  1439. })
  1440. .should.equal(true)
  1441. })
  1442. it('retuns the entity_id', function () {
  1443. this.callback.calledWith(null, docId).should.equal(true)
  1444. })
  1445. })
  1446. describe('deleteEntityWithPath', function () {
  1447. describe('when the entity exists', function () {
  1448. beforeEach(function () {
  1449. this.doc = { _id: docId }
  1450. this.ProjectLocator.findElementByPath.yields(null, this.doc, 'doc')
  1451. this.ProjectEntityUpdateHandler.deleteEntity = {
  1452. withoutLock: sinon.stub().yields(),
  1453. }
  1454. this.path = '/path/to/doc.tex'
  1455. this.ProjectEntityUpdateHandler.deleteEntityWithPath(
  1456. projectId,
  1457. this.path,
  1458. userId,
  1459. this.callback
  1460. )
  1461. })
  1462. it('finds the entity', function () {
  1463. this.ProjectLocator.findElementByPath
  1464. .calledWith({ project_id: projectId, path: this.path })
  1465. .should.equal(true)
  1466. })
  1467. it('deletes the entity', function () {
  1468. this.ProjectEntityUpdateHandler.deleteEntity.withoutLock
  1469. .calledWith(projectId, this.doc._id, 'doc', userId, this.callback)
  1470. .should.equal(true)
  1471. })
  1472. })
  1473. describe('when the entity does not exist', function () {
  1474. beforeEach(function () {
  1475. this.ProjectLocator.findElementByPath.yields()
  1476. this.path = '/doc.tex'
  1477. this.ProjectEntityUpdateHandler.deleteEntityWithPath(
  1478. projectId,
  1479. this.path,
  1480. userId,
  1481. this.callback
  1482. )
  1483. })
  1484. it('returns an error', function () {
  1485. this.callback
  1486. .calledWith(sinon.match.instanceOf(Errors.NotFoundError))
  1487. .should.equal(true)
  1488. })
  1489. })
  1490. })
  1491. describe('mkdirp', function () {
  1492. beforeEach(function () {
  1493. this.docPath = '/folder/doc.tex'
  1494. this.ProjectEntityMongoUpdateHandler.mkdirp.yields()
  1495. this.ProjectEntityUpdateHandler.mkdirp(
  1496. projectId,
  1497. this.docPath,
  1498. this.callback
  1499. )
  1500. })
  1501. it('calls ProjectEntityMongoUpdateHandler', function () {
  1502. this.ProjectEntityMongoUpdateHandler.mkdirp
  1503. .calledWith(projectId, this.docPath)
  1504. .should.equal(true)
  1505. })
  1506. })
  1507. describe('mkdirpWithExactCase', function () {
  1508. beforeEach(function () {
  1509. this.docPath = '/folder/doc.tex'
  1510. this.ProjectEntityMongoUpdateHandler.mkdirp.yields()
  1511. this.ProjectEntityUpdateHandler.mkdirpWithExactCase(
  1512. projectId,
  1513. this.docPath,
  1514. this.callback
  1515. )
  1516. })
  1517. it('calls ProjectEntityMongoUpdateHandler', function () {
  1518. this.ProjectEntityMongoUpdateHandler.mkdirp
  1519. .calledWith(projectId, this.docPath, { exactCaseMatch: true })
  1520. .should.equal(true)
  1521. })
  1522. })
  1523. describe('addFolder', function () {
  1524. describe('adding a folder', function () {
  1525. beforeEach(function () {
  1526. this.parentFolderId = '123asdf'
  1527. this.folderName = 'new-folder'
  1528. this.ProjectEntityMongoUpdateHandler.addFolder.yields()
  1529. this.ProjectEntityUpdateHandler.addFolder(
  1530. projectId,
  1531. this.parentFolderId,
  1532. this.folderName,
  1533. this.callback
  1534. )
  1535. })
  1536. it('calls ProjectEntityMongoUpdateHandler', function () {
  1537. this.ProjectEntityMongoUpdateHandler.addFolder
  1538. .calledWith(projectId, this.parentFolderId, this.folderName)
  1539. .should.equal(true)
  1540. })
  1541. })
  1542. describe('adding a folder with an invalid name', function () {
  1543. beforeEach(function () {
  1544. this.parentFolderId = '123asdf'
  1545. this.folderName = '*new-folder'
  1546. this.ProjectEntityMongoUpdateHandler.addFolder.yields()
  1547. this.ProjectEntityUpdateHandler.addFolder(
  1548. projectId,
  1549. this.parentFolderId,
  1550. this.folderName,
  1551. this.callback
  1552. )
  1553. })
  1554. it('returns an error', function () {
  1555. const errorMatcher = sinon.match.instanceOf(Errors.InvalidNameError)
  1556. this.callback.calledWithMatch(errorMatcher).should.equal(true)
  1557. })
  1558. })
  1559. })
  1560. describe('moveEntity', function () {
  1561. beforeEach(function () {
  1562. this.project_name = 'project name'
  1563. this.startPath = '/a.tex'
  1564. this.endPath = '/folder/b.tex'
  1565. this.rev = 2
  1566. this.changes = { newDocs: ['old-doc'], newFiles: ['old-file'] }
  1567. this.ProjectEntityMongoUpdateHandler.moveEntity.yields(
  1568. null,
  1569. this.project,
  1570. this.startPath,
  1571. this.endPath,
  1572. this.rev,
  1573. this.changes
  1574. )
  1575. this.ProjectEntityUpdateHandler.moveEntity(
  1576. projectId,
  1577. docId,
  1578. folderId,
  1579. 'doc',
  1580. userId,
  1581. this.callback
  1582. )
  1583. })
  1584. it('moves the entity in mongo', function () {
  1585. this.ProjectEntityMongoUpdateHandler.moveEntity
  1586. .calledWith(projectId, docId, folderId, 'doc')
  1587. .should.equal(true)
  1588. })
  1589. it('notifies tpds', function () {
  1590. this.TpdsUpdateSender.promises.moveEntity
  1591. .calledWith({
  1592. project_id: projectId,
  1593. project_name: this.project_name,
  1594. startPath: this.startPath,
  1595. endPath: this.endPath,
  1596. rev: this.rev,
  1597. })
  1598. .should.equal(true)
  1599. })
  1600. it('sends the changes in project structure to the doc updater', function () {
  1601. this.DocumentUpdaterHandler.updateProjectStructure
  1602. .calledWith(
  1603. projectId,
  1604. projectHistoryId,
  1605. userId,
  1606. this.changes,
  1607. this.callback
  1608. )
  1609. .should.equal(true)
  1610. })
  1611. })
  1612. describe('renameEntity', function () {
  1613. describe('renaming an entity', function () {
  1614. beforeEach(function () {
  1615. this.project_name = 'project name'
  1616. this.startPath = '/folder/a.tex'
  1617. this.endPath = '/folder/b.tex'
  1618. this.rev = 2
  1619. this.changes = { newDocs: ['old-doc'], newFiles: ['old-file'] }
  1620. this.newDocName = 'b.tex'
  1621. this.ProjectEntityMongoUpdateHandler.renameEntity.yields(
  1622. null,
  1623. this.project,
  1624. this.startPath,
  1625. this.endPath,
  1626. this.rev,
  1627. this.changes
  1628. )
  1629. this.ProjectEntityUpdateHandler.renameEntity(
  1630. projectId,
  1631. docId,
  1632. 'doc',
  1633. this.newDocName,
  1634. userId,
  1635. this.callback
  1636. )
  1637. })
  1638. it('moves the entity in mongo', function () {
  1639. this.ProjectEntityMongoUpdateHandler.renameEntity
  1640. .calledWith(projectId, docId, 'doc', this.newDocName)
  1641. .should.equal(true)
  1642. })
  1643. it('notifies tpds', function () {
  1644. this.TpdsUpdateSender.promises.moveEntity
  1645. .calledWith({
  1646. project_id: projectId,
  1647. project_name: this.project_name,
  1648. startPath: this.startPath,
  1649. endPath: this.endPath,
  1650. rev: this.rev,
  1651. })
  1652. .should.equal(true)
  1653. })
  1654. it('sends the changes in project structure to the doc updater', function () {
  1655. this.DocumentUpdaterHandler.updateProjectStructure
  1656. .calledWith(
  1657. projectId,
  1658. projectHistoryId,
  1659. userId,
  1660. this.changes,
  1661. this.callback
  1662. )
  1663. .should.equal(true)
  1664. })
  1665. })
  1666. describe('renaming an entity to an invalid name', function () {
  1667. beforeEach(function () {
  1668. this.project_name = 'project name'
  1669. this.startPath = '/folder/a.tex'
  1670. this.endPath = '/folder/b.tex'
  1671. this.rev = 2
  1672. this.changes = { newDocs: ['old-doc'], newFiles: ['old-file'] }
  1673. this.newDocName = '*b.tex'
  1674. this.ProjectEntityMongoUpdateHandler.renameEntity.yields(
  1675. null,
  1676. this.project,
  1677. this.startPath,
  1678. this.endPath,
  1679. this.rev,
  1680. this.changes
  1681. )
  1682. this.ProjectEntityUpdateHandler.renameEntity(
  1683. projectId,
  1684. docId,
  1685. 'doc',
  1686. this.newDocName,
  1687. userId,
  1688. this.callback
  1689. )
  1690. })
  1691. it('returns an error', function () {
  1692. const errorMatcher = sinon.match.instanceOf(Errors.InvalidNameError)
  1693. this.callback.calledWithMatch(errorMatcher).should.equal(true)
  1694. })
  1695. })
  1696. })
  1697. describe('resyncProjectHistory', function () {
  1698. describe('a deleted project', function () {
  1699. beforeEach(function () {
  1700. this.ProjectGetter.getProject.yields()
  1701. this.ProjectEntityUpdateHandler.resyncProjectHistory(
  1702. projectId,
  1703. this.callback
  1704. )
  1705. })
  1706. it('should return an error', function () {
  1707. expect(this.callback).to.have.been.calledWith(
  1708. sinon.match
  1709. .instanceOf(Errors.ProjectHistoryDisabledError)
  1710. .and(
  1711. sinon.match.has(
  1712. 'message',
  1713. `project history not enabled for ${projectId}`
  1714. )
  1715. )
  1716. )
  1717. })
  1718. })
  1719. describe('a project without project-history enabled', function () {
  1720. beforeEach(function () {
  1721. this.project.overleaf = {}
  1722. this.ProjectGetter.getProject.yields(null, this.project)
  1723. this.ProjectEntityUpdateHandler.resyncProjectHistory(
  1724. projectId,
  1725. this.callback
  1726. )
  1727. })
  1728. it('should return an error', function () {
  1729. expect(this.callback).to.have.been.calledWith(
  1730. sinon.match
  1731. .instanceOf(Errors.ProjectHistoryDisabledError)
  1732. .and(
  1733. sinon.match.has(
  1734. 'message',
  1735. `project history not enabled for ${projectId}`
  1736. )
  1737. )
  1738. )
  1739. })
  1740. })
  1741. describe('a project with project-history enabled', function () {
  1742. beforeEach(function () {
  1743. this.ProjectGetter.getProject.yields(null, this.project)
  1744. const docs = [
  1745. {
  1746. doc: {
  1747. _id: docId,
  1748. },
  1749. path: 'main.tex',
  1750. },
  1751. ]
  1752. const files = [
  1753. {
  1754. file: {
  1755. _id: fileId,
  1756. hash: '123456',
  1757. },
  1758. path: 'universe.png',
  1759. },
  1760. ]
  1761. this.ProjectEntityHandler.getAllEntitiesFromProject.yields(
  1762. null,
  1763. docs,
  1764. files
  1765. )
  1766. this.FileStoreHandler._buildUrl = (projectId, fileId) =>
  1767. `www.filestore.test/${projectId}/${fileId}`
  1768. this.DocumentUpdaterHandler.resyncProjectHistory.yields()
  1769. this.ProjectEntityUpdateHandler.resyncProjectHistory(
  1770. projectId,
  1771. this.callback
  1772. )
  1773. })
  1774. it('gets the project', function () {
  1775. this.ProjectGetter.getProject.calledWith(projectId).should.equal(true)
  1776. })
  1777. it('gets the entities for the project', function () {
  1778. this.ProjectEntityHandler.getAllEntitiesFromProject
  1779. .calledWith(this.project)
  1780. .should.equal(true)
  1781. })
  1782. it('tells the doc updater to sync the project', function () {
  1783. const docs = [
  1784. {
  1785. doc: docId,
  1786. path: 'main.tex',
  1787. },
  1788. ]
  1789. const files = [
  1790. {
  1791. file: fileId,
  1792. path: 'universe.png',
  1793. url: `www.filestore.test/${projectId}/${fileId}`,
  1794. _hash: '123456',
  1795. },
  1796. ]
  1797. this.DocumentUpdaterHandler.resyncProjectHistory
  1798. .calledWith(projectId, projectHistoryId, docs, files)
  1799. .should.equal(true)
  1800. })
  1801. it('calls the callback', function () {
  1802. this.callback.called.should.equal(true)
  1803. })
  1804. })
  1805. })
  1806. describe('_cleanUpEntity', function () {
  1807. beforeEach(function () {
  1808. this.entityId = '4eecaffcbffa66588e000009'
  1809. this.FileStoreHandler.deleteFile.yields()
  1810. this.ProjectEntityUpdateHandler.unsetRootDoc = sinon.stub().yields()
  1811. this.ProjectEntityMongoUpdateHandler._insertDeletedFileReference.yields()
  1812. })
  1813. describe('a file', function () {
  1814. beforeEach(function (done) {
  1815. this.path = '/file/system/path.png'
  1816. this.entity = { _id: this.entityId }
  1817. this.newProject = 'new-project'
  1818. this.ProjectEntityUpdateHandler._cleanUpEntity(
  1819. this.project,
  1820. this.newProject,
  1821. this.entity,
  1822. 'file',
  1823. this.path,
  1824. userId,
  1825. done
  1826. )
  1827. })
  1828. it('should insert the file into the deletedFiles collection', function () {
  1829. this.ProjectEntityMongoUpdateHandler._insertDeletedFileReference
  1830. .calledWith(this.project._id, this.entity)
  1831. .should.equal(true)
  1832. })
  1833. it('should not delete the file from FileStoreHandler', function () {
  1834. this.FileStoreHandler.deleteFile
  1835. .calledWith(projectId, this.entityId)
  1836. .should.equal(false)
  1837. })
  1838. it('should not attempt to delete from the document updater', function () {
  1839. this.DocumentUpdaterHandler.deleteDoc.called.should.equal(false)
  1840. })
  1841. it('should should send the update to the doc updater', function () {
  1842. const oldFiles = [{ file: this.entity, path: this.path }]
  1843. this.DocumentUpdaterHandler.updateProjectStructure
  1844. .calledWith(projectId, projectHistoryId, userId, {
  1845. oldFiles,
  1846. newProject: this.newProject,
  1847. })
  1848. .should.equal(true)
  1849. })
  1850. })
  1851. describe('a doc', function () {
  1852. beforeEach(function (done) {
  1853. this.path = '/file/system/path.tex'
  1854. this.ProjectEntityUpdateHandler._cleanUpDoc = sinon.stub().yields()
  1855. this.entity = { _id: this.entityId }
  1856. this.newProject = 'new-project'
  1857. this.ProjectEntityUpdateHandler._cleanUpEntity(
  1858. this.project,
  1859. this.newProject,
  1860. this.entity,
  1861. 'doc',
  1862. this.path,
  1863. userId,
  1864. done
  1865. )
  1866. })
  1867. it('should clean up the doc', function () {
  1868. this.ProjectEntityUpdateHandler._cleanUpDoc
  1869. .calledWith(this.project, this.entity, this.path, userId)
  1870. .should.equal(true)
  1871. })
  1872. it('should should send the update to the doc updater', function () {
  1873. const oldDocs = [{ doc: this.entity, path: this.path }]
  1874. this.DocumentUpdaterHandler.updateProjectStructure
  1875. .calledWith(projectId, projectHistoryId, userId, {
  1876. oldDocs,
  1877. newProject: this.newProject,
  1878. })
  1879. .should.equal(true)
  1880. })
  1881. })
  1882. describe('a folder', function () {
  1883. beforeEach(function (done) {
  1884. this.folder = {
  1885. folders: [
  1886. {
  1887. name: 'subfolder',
  1888. fileRefs: [
  1889. (this.file1 = { _id: 'file-id-1', name: 'file-name-1' }),
  1890. ],
  1891. docs: [(this.doc1 = { _id: 'doc-id-1', name: 'doc-name-1' })],
  1892. folders: [],
  1893. },
  1894. ],
  1895. fileRefs: [(this.file2 = { _id: 'file-id-2', name: 'file-name-2' })],
  1896. docs: [(this.doc2 = { _id: 'doc-id-2', name: 'doc-name-2' })],
  1897. }
  1898. this.ProjectEntityUpdateHandler._cleanUpDoc = sinon.stub().yields()
  1899. this.ProjectEntityUpdateHandler._cleanUpFile = sinon.stub().yields()
  1900. const path = '/folder'
  1901. this.newProject = 'new-project'
  1902. this.ProjectEntityUpdateHandler._cleanUpEntity(
  1903. this.project,
  1904. this.newProject,
  1905. this.folder,
  1906. 'folder',
  1907. path,
  1908. userId,
  1909. done
  1910. )
  1911. })
  1912. it('should clean up all sub files', function () {
  1913. this.ProjectEntityUpdateHandler._cleanUpFile
  1914. .calledWith(
  1915. this.project,
  1916. this.file1,
  1917. '/folder/subfolder/file-name-1',
  1918. userId
  1919. )
  1920. .should.equal(true)
  1921. this.ProjectEntityUpdateHandler._cleanUpFile
  1922. .calledWith(this.project, this.file2, '/folder/file-name-2', userId)
  1923. .should.equal(true)
  1924. })
  1925. it('should clean up all sub docs', function () {
  1926. this.ProjectEntityUpdateHandler._cleanUpDoc
  1927. .calledWith(
  1928. this.project,
  1929. this.doc1,
  1930. '/folder/subfolder/doc-name-1',
  1931. userId
  1932. )
  1933. .should.equal(true)
  1934. this.ProjectEntityUpdateHandler._cleanUpDoc
  1935. .calledWith(this.project, this.doc2, '/folder/doc-name-2', userId)
  1936. .should.equal(true)
  1937. })
  1938. it('should should send one update to the doc updater for all docs and files', function () {
  1939. const oldFiles = [
  1940. { file: this.file2, path: '/folder/file-name-2' },
  1941. { file: this.file1, path: '/folder/subfolder/file-name-1' },
  1942. ]
  1943. const oldDocs = [
  1944. { doc: this.doc2, path: '/folder/doc-name-2' },
  1945. { doc: this.doc1, path: '/folder/subfolder/doc-name-1' },
  1946. ]
  1947. this.DocumentUpdaterHandler.updateProjectStructure
  1948. .calledWith(projectId, projectHistoryId, userId, {
  1949. oldFiles,
  1950. oldDocs,
  1951. newProject: this.newProject,
  1952. })
  1953. .should.equal(true)
  1954. })
  1955. })
  1956. })
  1957. describe('_cleanUpDoc', function () {
  1958. beforeEach(function () {
  1959. this.doc = {
  1960. _id: ObjectId(),
  1961. name: 'test.tex',
  1962. }
  1963. this.path = '/path/to/doc'
  1964. this.ProjectEntityUpdateHandler.unsetRootDoc = sinon.stub().yields()
  1965. this.DocstoreManager.deleteDoc.yields()
  1966. })
  1967. describe('when the doc is the root doc', function () {
  1968. beforeEach(function () {
  1969. this.project.rootDoc_id = this.doc._id
  1970. this.ProjectEntityUpdateHandler._cleanUpDoc(
  1971. this.project,
  1972. this.doc,
  1973. this.path,
  1974. userId,
  1975. this.callback
  1976. )
  1977. })
  1978. it('should unset the root doc', function () {
  1979. this.ProjectEntityUpdateHandler.unsetRootDoc
  1980. .calledWith(projectId)
  1981. .should.equal(true)
  1982. })
  1983. it('should delete the doc in the doc updater', function () {
  1984. this.DocumentUpdaterHandler.deleteDoc
  1985. .calledWith(projectId, this.doc._id.toString())
  1986. .should.equal(true)
  1987. })
  1988. it('should delete the doc in the doc store', function () {
  1989. this.DocstoreManager.deleteDoc
  1990. .calledWith(projectId, this.doc._id.toString(), 'test.tex')
  1991. .should.equal(true)
  1992. })
  1993. it('should call the callback', function () {
  1994. this.callback.called.should.equal(true)
  1995. })
  1996. })
  1997. describe('when the doc is not the root doc', function () {
  1998. beforeEach(function () {
  1999. this.project.rootDoc_id = ObjectId()
  2000. this.ProjectEntityUpdateHandler._cleanUpDoc(
  2001. this.project,
  2002. this.doc,
  2003. this.path,
  2004. userId,
  2005. this.callback
  2006. )
  2007. })
  2008. it('should not unset the root doc', function () {
  2009. this.ProjectEntityUpdateHandler.unsetRootDoc.called.should.equal(false)
  2010. })
  2011. it('should call the callback', function () {
  2012. this.callback.called.should.equal(true)
  2013. })
  2014. })
  2015. })
  2016. describe('convertDocToFile', function () {
  2017. beforeEach(function () {
  2018. this.docPath = '/folder/doc.tex'
  2019. this.docLines = ['line one', 'line two']
  2020. this.tmpFilePath = '/tmp/file'
  2021. this.fileStoreUrl = 'http://filestore/file'
  2022. this.folder = { _id: new ObjectId() }
  2023. this.rev = 3
  2024. this.ProjectLocator.findElement
  2025. .withArgs({
  2026. project_id: this.project._id,
  2027. element_id: this.doc._id,
  2028. type: 'doc',
  2029. })
  2030. .yields(null, this.doc, { fileSystem: this.path })
  2031. this.ProjectLocator.findElement
  2032. .withArgs({
  2033. project_id: this.project._id.toString(),
  2034. element_id: this.file._id,
  2035. type: 'file',
  2036. })
  2037. .yields(null, this.file, this.docPath, this.folder)
  2038. this.DocstoreManager.getDoc
  2039. .withArgs(this.project._id, this.doc._id)
  2040. .yields(null, this.docLines, this.rev)
  2041. this.FileWriter.writeLinesToDisk.yields(null, this.tmpFilePath)
  2042. this.FileStoreHandler.uploadFileFromDisk.yields(
  2043. null,
  2044. this.fileStoreUrl,
  2045. this.file
  2046. )
  2047. this.ProjectEntityMongoUpdateHandler.replaceDocWithFile.yields(
  2048. null,
  2049. this.project
  2050. )
  2051. })
  2052. describe('successfully', function () {
  2053. beforeEach(function (done) {
  2054. this.ProjectEntityUpdateHandler.convertDocToFile(
  2055. this.project._id,
  2056. this.doc._id,
  2057. this.user._id,
  2058. done
  2059. )
  2060. })
  2061. it('deletes the document in doc updater', function () {
  2062. expect(this.DocumentUpdaterHandler.deleteDoc).to.have.been.calledWith(
  2063. this.project._id,
  2064. this.doc._id
  2065. )
  2066. })
  2067. it('uploads the file to filestore', function () {
  2068. expect(
  2069. this.FileStoreHandler.uploadFileFromDisk
  2070. ).to.have.been.calledWith(
  2071. this.project._id,
  2072. { name: this.doc.name, rev: this.rev + 1 },
  2073. this.tmpFilePath
  2074. )
  2075. })
  2076. it('cleans up the temporary file', function () {
  2077. expect(this.fs.unlink).to.have.been.calledWith(this.tmpFilePath)
  2078. })
  2079. it('replaces the doc with the file', function () {
  2080. expect(
  2081. this.ProjectEntityMongoUpdateHandler.replaceDocWithFile
  2082. ).to.have.been.calledWith(this.project._id, this.doc._id, this.file)
  2083. })
  2084. it('notifies document updater of changes', function () {
  2085. expect(
  2086. this.DocumentUpdaterHandler.updateProjectStructure
  2087. ).to.have.been.calledWith(
  2088. this.project._id,
  2089. this.project.overleaf.history.id,
  2090. this.user._id,
  2091. {
  2092. oldDocs: [{ doc: this.doc, path: this.path }],
  2093. newFiles: [
  2094. { file: this.file, path: this.path, url: this.fileStoreUrl },
  2095. ],
  2096. newProject: this.project,
  2097. }
  2098. )
  2099. })
  2100. it('should notify real-time of the doc deletion', function () {
  2101. expect(
  2102. this.EditorRealTimeController.emitToRoom
  2103. ).to.have.been.calledWith(
  2104. this.project._id,
  2105. 'removeEntity',
  2106. this.doc._id,
  2107. 'convertDocToFile'
  2108. )
  2109. })
  2110. it('should notify real-time of the file creation', function () {
  2111. expect(
  2112. this.EditorRealTimeController.emitToRoom
  2113. ).to.have.been.calledWith(
  2114. this.project._id,
  2115. 'reciveNewFile',
  2116. this.folder._id,
  2117. this.file,
  2118. 'convertDocToFile',
  2119. null
  2120. )
  2121. })
  2122. })
  2123. describe('when the doc has ranges', function () {
  2124. it('should throw a DocHasRangesError', function (done) {
  2125. this.ranges = { comments: [{ id: 123 }] }
  2126. this.DocstoreManager.getDoc
  2127. .withArgs(this.project._id, this.doc._id)
  2128. .yields(null, this.docLines, 'rev', 'version', this.ranges)
  2129. this.ProjectEntityUpdateHandler.convertDocToFile(
  2130. this.project._id,
  2131. this.doc._id,
  2132. this.user._id,
  2133. err => {
  2134. expect(err).to.be.instanceof(Errors.DocHasRangesError)
  2135. done()
  2136. }
  2137. )
  2138. })
  2139. })
  2140. })
  2141. })