ProjectEntityUpdateHandlerTests.js 81 KB

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