ProjectEntityUpdateHandlerTests.js 68 KB

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