DocumentManagerTests.js 31 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092
  1. const sinon = require('sinon')
  2. const modulePath = '../../../../app/js/DocumentManager.js'
  3. const SandboxedModule = require('sandboxed-module')
  4. const Errors = require('../../../../app/js/Errors')
  5. const tk = require('timekeeper')
  6. describe('DocumentManager', function () {
  7. beforeEach(function () {
  8. tk.freeze(new Date())
  9. this.Metrics = {
  10. Timer: class Timer {},
  11. inc: sinon.stub(),
  12. }
  13. this.Metrics.Timer.prototype.done = sinon.stub()
  14. this.DocumentManager = SandboxedModule.require(modulePath, {
  15. requires: {
  16. './RedisManager': (this.RedisManager = {}),
  17. './ProjectHistoryRedisManager': (this.ProjectHistoryRedisManager = {}),
  18. './PersistenceManager': (this.PersistenceManager = {}),
  19. './HistoryManager': (this.HistoryManager = {
  20. flushDocChangesAsync: sinon.stub(),
  21. flushProjectChangesAsync: sinon.stub(),
  22. }),
  23. './Metrics': this.Metrics,
  24. './RealTimeRedisManager': (this.RealTimeRedisManager = {}),
  25. './DiffCodec': (this.DiffCodec = {}),
  26. './UpdateManager': (this.UpdateManager = {}),
  27. './RangesManager': (this.RangesManager = {}),
  28. './Errors': Errors,
  29. },
  30. })
  31. this.project_id = 'project-id-123'
  32. this.projectHistoryId = 'history-id-123'
  33. this.projectHistoryType = 'project-history'
  34. this.doc_id = 'doc-id-123'
  35. this.user_id = 1234
  36. this.callback = sinon.stub()
  37. this.lines = ['one', 'two', 'three']
  38. this.version = 42
  39. this.ranges = { comments: 'mock', entries: 'mock' }
  40. this.pathname = '/a/b/c.tex'
  41. this.unflushedTime = Date.now()
  42. this.lastUpdatedAt = Date.now()
  43. this.lastUpdatedBy = 'last-author-id'
  44. this.source = 'external-source'
  45. })
  46. afterEach(function () {
  47. tk.reset()
  48. })
  49. describe('flushAndDeleteDoc', function () {
  50. describe('successfully', function () {
  51. beforeEach(function () {
  52. this.RedisManager.removeDocFromMemory = sinon.stub().callsArg(2)
  53. this.DocumentManager.flushDocIfLoaded = sinon.stub().callsArgWith(2)
  54. this.DocumentManager.flushAndDeleteDoc(
  55. this.project_id,
  56. this.doc_id,
  57. {},
  58. this.callback
  59. )
  60. })
  61. it('should flush the doc', function () {
  62. this.DocumentManager.flushDocIfLoaded
  63. .calledWith(this.project_id, this.doc_id)
  64. .should.equal(true)
  65. })
  66. it('should remove the doc from redis', function () {
  67. this.RedisManager.removeDocFromMemory
  68. .calledWith(this.project_id, this.doc_id)
  69. .should.equal(true)
  70. })
  71. it('should call the callback without error', function () {
  72. this.callback.calledWith(null).should.equal(true)
  73. })
  74. it('should time the execution', function () {
  75. this.Metrics.Timer.prototype.done.called.should.equal(true)
  76. })
  77. it('should flush to the history api', function () {
  78. this.HistoryManager.flushDocChangesAsync
  79. .calledWithExactly(this.project_id, this.doc_id)
  80. .should.equal(true)
  81. })
  82. })
  83. describe('when a flush error occurs', function () {
  84. beforeEach(function () {
  85. this.DocumentManager.flushDocIfLoaded = sinon
  86. .stub()
  87. .callsArgWith(2, new Error('boom!'))
  88. this.RedisManager.removeDocFromMemory = sinon.stub().callsArg(2)
  89. })
  90. it('should not remove the doc from redis', function (done) {
  91. this.DocumentManager.flushAndDeleteDoc(
  92. this.project_id,
  93. this.doc_id,
  94. {},
  95. error => {
  96. error.should.exist
  97. this.RedisManager.removeDocFromMemory.called.should.equal(false)
  98. done()
  99. }
  100. )
  101. })
  102. describe('when ignoring flush errors', function () {
  103. it('should remove the doc from redis', function (done) {
  104. this.DocumentManager.flushAndDeleteDoc(
  105. this.project_id,
  106. this.doc_id,
  107. { ignoreFlushErrors: true },
  108. error => {
  109. if (error) {
  110. return done(error)
  111. }
  112. this.RedisManager.removeDocFromMemory.called.should.equal(true)
  113. done()
  114. }
  115. )
  116. })
  117. })
  118. })
  119. })
  120. describe('flushDocIfLoaded', function () {
  121. describe('when the doc is in Redis', function () {
  122. beforeEach(function () {
  123. this.RedisManager.getDoc = sinon
  124. .stub()
  125. .callsArgWith(
  126. 2,
  127. null,
  128. this.lines,
  129. this.version,
  130. this.ranges,
  131. this.pathname,
  132. this.projectHistoryId,
  133. this.unflushedTime,
  134. this.lastUpdatedAt,
  135. this.lastUpdatedBy
  136. )
  137. this.RedisManager.clearUnflushedTime = sinon
  138. .stub()
  139. .callsArgWith(1, null)
  140. this.PersistenceManager.setDoc = sinon.stub().yields()
  141. this.DocumentManager.flushDocIfLoaded(
  142. this.project_id,
  143. this.doc_id,
  144. this.callback
  145. )
  146. })
  147. it('should get the doc from redis', function () {
  148. this.RedisManager.getDoc
  149. .calledWith(this.project_id, this.doc_id)
  150. .should.equal(true)
  151. })
  152. it('should write the doc lines to the persistence layer', function () {
  153. this.PersistenceManager.setDoc
  154. .calledWith(
  155. this.project_id,
  156. this.doc_id,
  157. this.lines,
  158. this.version,
  159. this.ranges,
  160. this.lastUpdatedAt,
  161. this.lastUpdatedBy
  162. )
  163. .should.equal(true)
  164. })
  165. it('should call the callback without error', function () {
  166. this.callback.calledWith(null).should.equal(true)
  167. })
  168. it('should time the execution', function () {
  169. this.Metrics.Timer.prototype.done.called.should.equal(true)
  170. })
  171. })
  172. describe('when the document is not in Redis', function () {
  173. beforeEach(function () {
  174. this.RedisManager.getDoc = sinon
  175. .stub()
  176. .callsArgWith(2, null, null, null, null)
  177. this.PersistenceManager.setDoc = sinon.stub().yields()
  178. this.DocumentManager.flushDocIfLoaded(
  179. this.project_id,
  180. this.doc_id,
  181. this.callback
  182. )
  183. })
  184. it('should get the doc from redis', function () {
  185. this.RedisManager.getDoc
  186. .calledWith(this.project_id, this.doc_id)
  187. .should.equal(true)
  188. })
  189. it('should not write anything to the persistence layer', function () {
  190. this.PersistenceManager.setDoc.called.should.equal(false)
  191. })
  192. it('should call the callback without error', function () {
  193. this.callback.calledWith(null).should.equal(true)
  194. })
  195. it('should time the execution', function () {
  196. this.Metrics.Timer.prototype.done.called.should.equal(true)
  197. })
  198. })
  199. })
  200. describe('getDocAndRecentOps', function () {
  201. describe('with a previous version specified', function () {
  202. beforeEach(function () {
  203. this.DocumentManager.getDoc = sinon
  204. .stub()
  205. .callsArgWith(
  206. 2,
  207. null,
  208. this.lines,
  209. this.version,
  210. this.ranges,
  211. this.pathname,
  212. this.projectHistoryId
  213. )
  214. this.RedisManager.getPreviousDocOps = sinon
  215. .stub()
  216. .callsArgWith(3, null, this.ops)
  217. this.DocumentManager.getDocAndRecentOps(
  218. this.project_id,
  219. this.doc_id,
  220. this.fromVersion,
  221. this.callback
  222. )
  223. })
  224. it('should get the doc', function () {
  225. this.DocumentManager.getDoc
  226. .calledWith(this.project_id, this.doc_id)
  227. .should.equal(true)
  228. })
  229. it('should get the doc ops', function () {
  230. this.RedisManager.getPreviousDocOps
  231. .calledWith(this.doc_id, this.fromVersion, this.version)
  232. .should.equal(true)
  233. })
  234. it('should call the callback with the doc info', function () {
  235. this.callback
  236. .calledWith(
  237. null,
  238. this.lines,
  239. this.version,
  240. this.ops,
  241. this.ranges,
  242. this.pathname,
  243. this.projectHistoryId
  244. )
  245. .should.equal(true)
  246. })
  247. it('should time the execution', function () {
  248. this.Metrics.Timer.prototype.done.called.should.equal(true)
  249. })
  250. })
  251. describe('with no previous version specified', function () {
  252. beforeEach(function () {
  253. this.DocumentManager.getDoc = sinon
  254. .stub()
  255. .callsArgWith(
  256. 2,
  257. null,
  258. this.lines,
  259. this.version,
  260. this.ranges,
  261. this.pathname,
  262. this.projectHistoryId
  263. )
  264. this.RedisManager.getPreviousDocOps = sinon
  265. .stub()
  266. .callsArgWith(3, null, this.ops)
  267. this.DocumentManager.getDocAndRecentOps(
  268. this.project_id,
  269. this.doc_id,
  270. -1,
  271. this.callback
  272. )
  273. })
  274. it('should get the doc', function () {
  275. this.DocumentManager.getDoc
  276. .calledWith(this.project_id, this.doc_id)
  277. .should.equal(true)
  278. })
  279. it('should not need to get the doc ops', function () {
  280. this.RedisManager.getPreviousDocOps.called.should.equal(false)
  281. })
  282. it('should call the callback with the doc info', function () {
  283. this.callback
  284. .calledWith(
  285. null,
  286. this.lines,
  287. this.version,
  288. [],
  289. this.ranges,
  290. this.pathname,
  291. this.projectHistoryId
  292. )
  293. .should.equal(true)
  294. })
  295. it('should time the execution', function () {
  296. this.Metrics.Timer.prototype.done.called.should.equal(true)
  297. })
  298. })
  299. })
  300. describe('getDoc', function () {
  301. describe('when the doc exists in Redis', function () {
  302. beforeEach(function () {
  303. this.RedisManager.getDoc = sinon
  304. .stub()
  305. .callsArgWith(
  306. 2,
  307. null,
  308. this.lines,
  309. this.version,
  310. this.ranges,
  311. this.pathname,
  312. this.projectHistoryId,
  313. this.unflushedTime
  314. )
  315. this.DocumentManager.getDoc(this.project_id, this.doc_id, this.callback)
  316. })
  317. it('should get the doc from Redis', function () {
  318. this.RedisManager.getDoc
  319. .calledWith(this.project_id, this.doc_id)
  320. .should.equal(true)
  321. })
  322. it('should call the callback with the doc info', function () {
  323. this.callback
  324. .calledWith(
  325. null,
  326. this.lines,
  327. this.version,
  328. this.ranges,
  329. this.pathname,
  330. this.projectHistoryId,
  331. this.unflushedTime,
  332. true
  333. )
  334. .should.equal(true)
  335. })
  336. it('should time the execution', function () {
  337. this.Metrics.Timer.prototype.done.called.should.equal(true)
  338. })
  339. })
  340. describe('when the doc does not exist in Redis', function () {
  341. beforeEach(function () {
  342. this.RedisManager.getDoc = sinon
  343. .stub()
  344. .callsArgWith(2, null, null, null, null, null, null)
  345. this.PersistenceManager.getDoc = sinon
  346. .stub()
  347. .callsArgWith(
  348. 2,
  349. null,
  350. this.lines,
  351. this.version,
  352. this.ranges,
  353. this.pathname,
  354. this.projectHistoryId,
  355. this.projectHistoryType
  356. )
  357. this.RedisManager.putDocInMemory = sinon.stub().yields()
  358. this.RedisManager.setHistoryType = sinon.stub().yields()
  359. this.DocumentManager.getDoc(this.project_id, this.doc_id, this.callback)
  360. })
  361. it('should try to get the doc from Redis', function () {
  362. this.RedisManager.getDoc
  363. .calledWith(this.project_id, this.doc_id)
  364. .should.equal(true)
  365. })
  366. it('should get the doc from the PersistenceManager', function () {
  367. this.PersistenceManager.getDoc
  368. .calledWith(this.project_id, this.doc_id)
  369. .should.equal(true)
  370. })
  371. it('should set the doc in Redis', function () {
  372. this.RedisManager.putDocInMemory
  373. .calledWith(
  374. this.project_id,
  375. this.doc_id,
  376. this.lines,
  377. this.version,
  378. this.ranges,
  379. this.pathname,
  380. this.projectHistoryId
  381. )
  382. .should.equal(true)
  383. })
  384. it('should set the history type in Redis', function () {
  385. this.RedisManager.setHistoryType
  386. .calledWith(this.doc_id, this.projectHistoryType)
  387. .should.equal(true)
  388. })
  389. it('should call the callback with the doc info', function () {
  390. this.callback
  391. .calledWith(
  392. null,
  393. this.lines,
  394. this.version,
  395. this.ranges,
  396. this.pathname,
  397. this.projectHistoryId,
  398. null,
  399. false
  400. )
  401. .should.equal(true)
  402. })
  403. it('should time the execution', function () {
  404. this.Metrics.Timer.prototype.done.called.should.equal(true)
  405. })
  406. })
  407. })
  408. describe('setDoc', function () {
  409. describe('with plain tex lines', function () {
  410. beforeEach(function () {
  411. this.beforeLines = ['before', 'lines']
  412. this.afterLines = ['after', 'lines']
  413. this.ops = [
  414. { i: 'foo', p: 4 },
  415. { d: 'bar', p: 42 },
  416. ]
  417. this.DocumentManager.getDoc = sinon
  418. .stub()
  419. .callsArgWith(
  420. 2,
  421. null,
  422. this.beforeLines,
  423. this.version,
  424. this.ranges,
  425. this.pathname,
  426. this.projectHistoryId,
  427. this.unflushedTime,
  428. true
  429. )
  430. this.DiffCodec.diffAsShareJsOp = sinon
  431. .stub()
  432. .callsArgWith(2, null, this.ops)
  433. this.UpdateManager.applyUpdate = sinon.stub().callsArgWith(3, null)
  434. this.DocumentManager.flushDocIfLoaded = sinon.stub().callsArg(2)
  435. this.DocumentManager.flushAndDeleteDoc = sinon.stub().callsArg(3)
  436. })
  437. describe('when already loaded', function () {
  438. beforeEach(function () {
  439. this.DocumentManager.setDoc(
  440. this.project_id,
  441. this.doc_id,
  442. this.afterLines,
  443. this.source,
  444. this.user_id,
  445. false,
  446. this.callback
  447. )
  448. })
  449. it('should get the current doc lines', function () {
  450. this.DocumentManager.getDoc
  451. .calledWith(this.project_id, this.doc_id)
  452. .should.equal(true)
  453. })
  454. it('should return a diff of the old and new lines', function () {
  455. this.DiffCodec.diffAsShareJsOp
  456. .calledWith(this.beforeLines, this.afterLines)
  457. .should.equal(true)
  458. })
  459. it('should apply the diff as a ShareJS op', function () {
  460. this.UpdateManager.applyUpdate
  461. .calledWith(this.project_id, this.doc_id, {
  462. doc: this.doc_id,
  463. v: this.version,
  464. op: this.ops,
  465. meta: {
  466. type: 'external',
  467. source: this.source,
  468. user_id: this.user_id,
  469. },
  470. })
  471. .should.equal(true)
  472. })
  473. it('should increment the external update metric', function () {
  474. this.Metrics.inc
  475. .calledWith('external-update', 1, {
  476. status: 'diff',
  477. method: 'flush',
  478. path: this.source,
  479. })
  480. .should.equal(true)
  481. })
  482. it('should flush the doc to Mongo', function () {
  483. this.DocumentManager.flushDocIfLoaded
  484. .calledWith(this.project_id, this.doc_id)
  485. .should.equal(true)
  486. })
  487. it('should not flush the project history', function () {
  488. this.HistoryManager.flushProjectChangesAsync.called.should.equal(
  489. false
  490. )
  491. })
  492. it('should call the callback', function () {
  493. this.callback.calledWith(null).should.equal(true)
  494. })
  495. it('should time the execution', function () {
  496. this.Metrics.Timer.prototype.done.called.should.equal(true)
  497. })
  498. })
  499. describe('when not already loaded', function () {
  500. beforeEach(function () {
  501. this.DocumentManager.getDoc = sinon
  502. .stub()
  503. .callsArgWith(
  504. 2,
  505. null,
  506. this.beforeLines,
  507. this.version,
  508. this.pathname,
  509. null,
  510. false
  511. )
  512. this.DocumentManager.setDoc(
  513. this.project_id,
  514. this.doc_id,
  515. this.afterLines,
  516. this.source,
  517. this.user_id,
  518. false,
  519. this.callback
  520. )
  521. })
  522. it('should flush and delete the doc from the doc updater', function () {
  523. this.DocumentManager.flushAndDeleteDoc
  524. .calledWith(this.project_id, this.doc_id, {})
  525. .should.equal(true)
  526. })
  527. it('should increment the external update metric', function () {
  528. this.Metrics.inc
  529. .calledWith('external-update', 1, {
  530. status: 'diff',
  531. method: 'evict',
  532. path: this.source,
  533. })
  534. .should.equal(true)
  535. })
  536. it('should not flush the project history', function () {
  537. this.HistoryManager.flushProjectChangesAsync
  538. .calledWithExactly(this.project_id)
  539. .should.equal(true)
  540. })
  541. })
  542. describe('without new lines', function () {
  543. beforeEach(function () {
  544. this.DocumentManager.setDoc(
  545. this.project_id,
  546. this.doc_id,
  547. null,
  548. this.source,
  549. this.user_id,
  550. false,
  551. this.callback
  552. )
  553. })
  554. it('should return the callback with an error', function () {
  555. this.callback.calledWith(new Error('No lines were passed to setDoc'))
  556. })
  557. it('should not try to get the doc lines', function () {
  558. this.DocumentManager.getDoc.called.should.equal(false)
  559. })
  560. })
  561. describe('with the undoing flag', function () {
  562. beforeEach(function () {
  563. // Copy ops so we don't interfere with other tests
  564. this.ops = [
  565. { i: 'foo', p: 4 },
  566. { d: 'bar', p: 42 },
  567. ]
  568. this.DiffCodec.diffAsShareJsOp = sinon
  569. .stub()
  570. .callsArgWith(2, null, this.ops)
  571. this.DocumentManager.setDoc(
  572. this.project_id,
  573. this.doc_id,
  574. this.afterLines,
  575. this.source,
  576. this.user_id,
  577. true,
  578. this.callback
  579. )
  580. })
  581. it('should set the undo flag on each op', function () {
  582. this.ops.map(op => op.u.should.equal(true))
  583. })
  584. })
  585. })
  586. })
  587. describe('acceptChanges', function () {
  588. beforeEach(function () {
  589. this.change_id = 'mock-change-id'
  590. this.change_ids = [
  591. 'mock-change-id-1',
  592. 'mock-change-id-2',
  593. 'mock-change-id-3',
  594. 'mock-change-id-4',
  595. ]
  596. this.version = 34
  597. this.lines = ['original', 'lines']
  598. this.ranges = { entries: 'mock', comments: 'mock' }
  599. this.updated_ranges = { entries: 'updated', comments: 'updated' }
  600. this.DocumentManager.getDoc = sinon
  601. .stub()
  602. .yields(null, this.lines, this.version, this.ranges)
  603. this.RangesManager.acceptChanges = sinon
  604. .stub()
  605. .yields(null, this.updated_ranges)
  606. this.RedisManager.updateDocument = sinon.stub().yields()
  607. })
  608. describe('successfully with a single change', function () {
  609. beforeEach(function () {
  610. this.DocumentManager.acceptChanges(
  611. this.project_id,
  612. this.doc_id,
  613. [this.change_id],
  614. this.callback
  615. )
  616. })
  617. it("should get the document's current ranges", function () {
  618. this.DocumentManager.getDoc
  619. .calledWith(this.project_id, this.doc_id)
  620. .should.equal(true)
  621. })
  622. it('should apply the accept change to the ranges', function () {
  623. this.RangesManager.acceptChanges
  624. .calledWith([this.change_id], this.ranges)
  625. .should.equal(true)
  626. })
  627. it('should save the updated ranges', function () {
  628. this.RedisManager.updateDocument
  629. .calledWith(
  630. this.project_id,
  631. this.doc_id,
  632. this.lines,
  633. this.version,
  634. [],
  635. this.updated_ranges,
  636. {}
  637. )
  638. .should.equal(true)
  639. })
  640. it('should call the callback', function () {
  641. this.callback.called.should.equal(true)
  642. })
  643. })
  644. describe('successfully with multiple changes', function () {
  645. beforeEach(function () {
  646. this.DocumentManager.acceptChanges(
  647. this.project_id,
  648. this.doc_id,
  649. this.change_ids,
  650. this.callback
  651. )
  652. })
  653. it('should apply the accept change to the ranges', function () {
  654. this.RangesManager.acceptChanges
  655. .calledWith(this.change_ids, this.ranges)
  656. .should.equal(true)
  657. })
  658. })
  659. describe('when the doc is not found', function () {
  660. beforeEach(function () {
  661. this.DocumentManager.getDoc = sinon
  662. .stub()
  663. .yields(null, null, null, null)
  664. this.DocumentManager.acceptChanges(
  665. this.project_id,
  666. this.doc_id,
  667. [this.change_id],
  668. this.callback
  669. )
  670. })
  671. it('should not save anything', function () {
  672. this.RedisManager.updateDocument.called.should.equal(false)
  673. })
  674. it('should call the callback with a not found error', function () {
  675. this.callback
  676. .calledWith(sinon.match.instanceOf(Errors.NotFoundError))
  677. .should.equal(true)
  678. })
  679. })
  680. })
  681. describe('deleteComment', function () {
  682. beforeEach(function () {
  683. this.comment_id = 'mock-comment-id'
  684. this.version = 34
  685. this.lines = ['original', 'lines']
  686. this.ranges = { comments: ['one', 'two', 'three'] }
  687. this.updated_ranges = { comments: ['one', 'three'] }
  688. this.DocumentManager.getDoc = sinon
  689. .stub()
  690. .yields(null, this.lines, this.version, this.ranges)
  691. this.RangesManager.deleteComment = sinon
  692. .stub()
  693. .yields(null, this.updated_ranges)
  694. this.RedisManager.updateDocument = sinon.stub().yields()
  695. })
  696. describe('successfully', function () {
  697. beforeEach(function () {
  698. this.DocumentManager.deleteComment(
  699. this.project_id,
  700. this.doc_id,
  701. this.comment_id,
  702. this.callback
  703. )
  704. })
  705. it("should get the document's current ranges", function () {
  706. this.DocumentManager.getDoc
  707. .calledWith(this.project_id, this.doc_id)
  708. .should.equal(true)
  709. })
  710. it('should delete the comment from the ranges', function () {
  711. this.RangesManager.deleteComment
  712. .calledWith(this.comment_id, this.ranges)
  713. .should.equal(true)
  714. })
  715. it('should save the updated ranges', function () {
  716. this.RedisManager.updateDocument
  717. .calledWith(
  718. this.project_id,
  719. this.doc_id,
  720. this.lines,
  721. this.version,
  722. [],
  723. this.updated_ranges,
  724. {}
  725. )
  726. .should.equal(true)
  727. })
  728. it('should call the callback', function () {
  729. this.callback.called.should.equal(true)
  730. })
  731. })
  732. describe('when the doc is not found', function () {
  733. beforeEach(function () {
  734. this.DocumentManager.getDoc = sinon
  735. .stub()
  736. .yields(null, null, null, null)
  737. this.DocumentManager.acceptChanges(
  738. this.project_id,
  739. this.doc_id,
  740. [this.comment_id],
  741. this.callback
  742. )
  743. })
  744. it('should not save anything', function () {
  745. this.RedisManager.updateDocument.called.should.equal(false)
  746. })
  747. it('should call the callback with a not found error', function () {
  748. this.callback
  749. .calledWith(sinon.match.instanceOf(Errors.NotFoundError))
  750. .should.equal(true)
  751. })
  752. })
  753. })
  754. describe('getDocAndFlushIfOld', function () {
  755. beforeEach(function () {
  756. this.DocumentManager.flushDocIfLoaded = sinon.stub().callsArg(2)
  757. })
  758. describe('when the doc is in Redis', function () {
  759. describe('and has changes to be flushed', function () {
  760. beforeEach(function () {
  761. this.DocumentManager.getDoc = sinon
  762. .stub()
  763. .callsArgWith(
  764. 2,
  765. null,
  766. this.lines,
  767. this.version,
  768. this.ranges,
  769. this.projectHistoryId,
  770. this.pathname,
  771. Date.now() - 1e9,
  772. true
  773. )
  774. this.DocumentManager.getDocAndFlushIfOld(
  775. this.project_id,
  776. this.doc_id,
  777. this.callback
  778. )
  779. })
  780. it('should get the doc', function () {
  781. this.DocumentManager.getDoc
  782. .calledWith(this.project_id, this.doc_id)
  783. .should.equal(true)
  784. })
  785. it('should flush the doc', function () {
  786. this.DocumentManager.flushDocIfLoaded
  787. .calledWith(this.project_id, this.doc_id)
  788. .should.equal(true)
  789. })
  790. it('should call the callback with the lines and versions', function () {
  791. this.callback
  792. .calledWith(null, this.lines, this.version)
  793. .should.equal(true)
  794. })
  795. })
  796. describe("and has only changes that don't need to be flushed", function () {
  797. beforeEach(function () {
  798. this.DocumentManager.getDoc = sinon
  799. .stub()
  800. .callsArgWith(
  801. 2,
  802. null,
  803. this.lines,
  804. this.version,
  805. this.ranges,
  806. this.pathname,
  807. Date.now() - 100,
  808. true
  809. )
  810. this.DocumentManager.getDocAndFlushIfOld(
  811. this.project_id,
  812. this.doc_id,
  813. this.callback
  814. )
  815. })
  816. it('should get the doc', function () {
  817. this.DocumentManager.getDoc
  818. .calledWith(this.project_id, this.doc_id)
  819. .should.equal(true)
  820. })
  821. it('should not flush the doc', function () {
  822. this.DocumentManager.flushDocIfLoaded.called.should.equal(false)
  823. })
  824. it('should call the callback with the lines and versions', function () {
  825. this.callback
  826. .calledWith(null, this.lines, this.version)
  827. .should.equal(true)
  828. })
  829. })
  830. })
  831. describe('when the doc is not in Redis', function () {
  832. beforeEach(function () {
  833. this.DocumentManager.getDoc = sinon
  834. .stub()
  835. .callsArgWith(
  836. 2,
  837. null,
  838. this.lines,
  839. this.version,
  840. this.ranges,
  841. null,
  842. false
  843. )
  844. this.DocumentManager.getDocAndFlushIfOld(
  845. this.project_id,
  846. this.doc_id,
  847. this.callback
  848. )
  849. })
  850. it('should get the doc', function () {
  851. this.DocumentManager.getDoc
  852. .calledWith(this.project_id, this.doc_id)
  853. .should.equal(true)
  854. })
  855. it('should not flush the doc', function () {
  856. this.DocumentManager.flushDocIfLoaded.called.should.equal(false)
  857. })
  858. it('should call the callback with the lines and versions', function () {
  859. this.callback
  860. .calledWith(null, this.lines, this.version)
  861. .should.equal(true)
  862. })
  863. })
  864. })
  865. describe('renameDoc', function () {
  866. beforeEach(function () {
  867. this.update = 'some-update'
  868. this.RedisManager.renameDoc = sinon.stub().yields()
  869. })
  870. describe('successfully', function () {
  871. beforeEach(function () {
  872. this.DocumentManager.renameDoc(
  873. this.project_id,
  874. this.doc_id,
  875. this.user_id,
  876. this.update,
  877. this.projectHistoryId,
  878. this.callback
  879. )
  880. })
  881. it('should rename the document', function () {
  882. this.RedisManager.renameDoc
  883. .calledWith(
  884. this.project_id,
  885. this.doc_id,
  886. this.user_id,
  887. this.update,
  888. this.projectHistoryId
  889. )
  890. .should.equal(true)
  891. })
  892. it('should call the callback', function () {
  893. this.callback.called.should.equal(true)
  894. })
  895. })
  896. })
  897. describe('resyncDocContents', function () {
  898. describe('when doc is loaded in redis', function () {
  899. beforeEach(function () {
  900. this.pathnameFromProjectStructureUpdate = '/foo/bar.tex'
  901. this.RedisManager.getDoc = sinon
  902. .stub()
  903. .callsArgWith(
  904. 2,
  905. null,
  906. this.lines,
  907. this.version,
  908. this.ranges,
  909. this.pathname,
  910. this.projectHistoryId
  911. )
  912. this.ProjectHistoryRedisManager.queueResyncDocContent = sinon.stub()
  913. this.DocumentManager.resyncDocContents(
  914. this.project_id,
  915. this.doc_id,
  916. this.pathnameFromProjectStructureUpdate,
  917. this.callback
  918. )
  919. })
  920. it('gets the doc contents from redis', function () {
  921. this.RedisManager.getDoc
  922. .calledWith(this.project_id, this.doc_id)
  923. .should.equal(true)
  924. })
  925. it('queues a resync doc content update', function () {
  926. this.ProjectHistoryRedisManager.queueResyncDocContent
  927. .calledWith(
  928. this.project_id,
  929. this.projectHistoryId,
  930. this.doc_id,
  931. this.lines,
  932. this.version,
  933. this.pathnameFromProjectStructureUpdate,
  934. this.callback
  935. )
  936. .should.equal(true)
  937. })
  938. })
  939. describe('when doc is not loaded in redis', function () {
  940. beforeEach(function () {
  941. this.pathnameFromProjectStructureUpdate = '/foo/bar.tex'
  942. this.RedisManager.getDoc = sinon.stub().callsArgWith(2, null)
  943. this.PersistenceManager.getDoc = sinon
  944. .stub()
  945. .yields(
  946. null,
  947. this.lines,
  948. this.version,
  949. this.ranges,
  950. this.pathname,
  951. this.projectHistoryId
  952. )
  953. this.ProjectHistoryRedisManager.queueResyncDocContent = sinon.stub()
  954. this.DocumentManager.resyncDocContents(
  955. this.project_id,
  956. this.doc_id,
  957. this.pathnameFromProjectStructureUpdate,
  958. this.callback
  959. )
  960. })
  961. it('tries to get the doc contents from redis', function () {
  962. this.RedisManager.getDoc
  963. .calledWith(this.project_id, this.doc_id)
  964. .should.equal(true)
  965. })
  966. it('gets the doc contents from web', function () {
  967. this.PersistenceManager.getDoc
  968. .calledWith(this.project_id, this.doc_id, { peek: true })
  969. .should.equal(true)
  970. })
  971. it('queues a resync doc content update', function () {
  972. this.ProjectHistoryRedisManager.queueResyncDocContent
  973. .calledWith(
  974. this.project_id,
  975. this.projectHistoryId,
  976. this.doc_id,
  977. this.lines,
  978. this.version,
  979. this.pathnameFromProjectStructureUpdate,
  980. this.callback
  981. )
  982. .should.equal(true)
  983. })
  984. })
  985. })
  986. })