ProjectEntityUpdateHandlerTests.js 65 KB

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