ProjectEntityUpdateHandlerTests.js 68 KB

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