ProjectEntityUpdateHandlerTests.js 77 KB

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