ProjectEntityUpdateHandlerTests.js 60 KB

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