ProjectEntityUpdateHandlerTests.js 87 KB

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