ProjectEntityUpdateHandlerTests.js 94 KB

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