DocumentManagerTests.js 33 KB

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