DocumentManagerTests.js 34 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196
  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.historyRangesSupport = true
  757. this.DocumentManager.getDoc = sinon
  758. .stub()
  759. .yields(
  760. null,
  761. this.lines,
  762. this.version,
  763. this.ranges,
  764. this.pathname,
  765. this.projectHistoryId,
  766. Date.now() - 1e9,
  767. true,
  768. this.historyRangesSupport
  769. )
  770. this.RangesManager.deleteComment = sinon
  771. .stub()
  772. .returns(this.updated_ranges)
  773. this.RedisManager.updateDocument = sinon.stub().yields()
  774. this.ProjectHistoryRedisManager.queueOps = sinon.stub().yields()
  775. })
  776. describe('successfully', function () {
  777. beforeEach(function () {
  778. this.DocumentManager.deleteComment(
  779. this.project_id,
  780. this.doc_id,
  781. this.comment_id,
  782. this.user_id,
  783. this.callback
  784. )
  785. })
  786. it("should get the document's current ranges", function () {
  787. this.DocumentManager.getDoc
  788. .calledWith(this.project_id, this.doc_id)
  789. .should.equal(true)
  790. })
  791. it('should delete the comment from the ranges', function () {
  792. this.RangesManager.deleteComment
  793. .calledWith(this.comment_id, this.ranges)
  794. .should.equal(true)
  795. })
  796. it('should save the updated ranges', function () {
  797. this.RedisManager.updateDocument
  798. .calledWith(
  799. this.project_id,
  800. this.doc_id,
  801. this.lines,
  802. this.version,
  803. [],
  804. this.updated_ranges,
  805. {}
  806. )
  807. .should.equal(true)
  808. })
  809. it('should queue the delete comment operation', function () {
  810. this.ProjectHistoryRedisManager.queueOps
  811. .calledWith(
  812. this.project_id,
  813. JSON.stringify({
  814. pathname: this.pathname,
  815. deleteComment: this.comment_id,
  816. meta: {
  817. ts: new Date(),
  818. user_id: this.user_id,
  819. },
  820. }),
  821. sinon.match.func
  822. )
  823. .should.equal(true)
  824. })
  825. it('should call the callback', function () {
  826. this.callback.called.should.equal(true)
  827. })
  828. })
  829. describe('when the doc is not found', function () {
  830. beforeEach(function () {
  831. this.DocumentManager.getDoc = sinon
  832. .stub()
  833. .yields(null, null, null, null)
  834. this.DocumentManager.acceptChanges(
  835. this.project_id,
  836. this.doc_id,
  837. [this.comment_id],
  838. this.callback
  839. )
  840. })
  841. it('should not save anything', function () {
  842. this.RedisManager.updateDocument.called.should.equal(false)
  843. })
  844. it('should call the callback with a not found error', function () {
  845. this.callback
  846. .calledWith(sinon.match.instanceOf(Errors.NotFoundError))
  847. .should.equal(true)
  848. })
  849. })
  850. })
  851. describe('getDocAndFlushIfOld', function () {
  852. beforeEach(function () {
  853. this.DocumentManager.flushDocIfLoaded = sinon.stub().callsArg(2)
  854. })
  855. describe('when the doc is in Redis', function () {
  856. describe('and has changes to be flushed', function () {
  857. beforeEach(function () {
  858. this.DocumentManager.getDoc = sinon
  859. .stub()
  860. .callsArgWith(
  861. 2,
  862. null,
  863. this.lines,
  864. this.version,
  865. this.ranges,
  866. this.projectHistoryId,
  867. this.pathname,
  868. Date.now() - 1e9,
  869. true
  870. )
  871. this.DocumentManager.getDocAndFlushIfOld(
  872. this.project_id,
  873. this.doc_id,
  874. this.callback
  875. )
  876. })
  877. it('should get the doc', function () {
  878. this.DocumentManager.getDoc
  879. .calledWith(this.project_id, this.doc_id)
  880. .should.equal(true)
  881. })
  882. it('should flush the doc', function () {
  883. this.DocumentManager.flushDocIfLoaded
  884. .calledWith(this.project_id, this.doc_id)
  885. .should.equal(true)
  886. })
  887. it('should call the callback with the lines and versions', function () {
  888. this.callback
  889. .calledWith(null, this.lines, this.version)
  890. .should.equal(true)
  891. })
  892. })
  893. describe("and has only changes that don't need to be flushed", function () {
  894. beforeEach(function () {
  895. this.DocumentManager.getDoc = sinon
  896. .stub()
  897. .callsArgWith(
  898. 2,
  899. null,
  900. this.lines,
  901. this.version,
  902. this.ranges,
  903. this.pathname,
  904. Date.now() - 100,
  905. true
  906. )
  907. this.DocumentManager.getDocAndFlushIfOld(
  908. this.project_id,
  909. this.doc_id,
  910. this.callback
  911. )
  912. })
  913. it('should get the doc', function () {
  914. this.DocumentManager.getDoc
  915. .calledWith(this.project_id, this.doc_id)
  916. .should.equal(true)
  917. })
  918. it('should not flush the doc', function () {
  919. this.DocumentManager.flushDocIfLoaded.called.should.equal(false)
  920. })
  921. it('should call the callback with the lines and versions', function () {
  922. this.callback
  923. .calledWith(null, this.lines, this.version)
  924. .should.equal(true)
  925. })
  926. })
  927. })
  928. describe('when the doc is not in Redis', function () {
  929. beforeEach(function () {
  930. this.DocumentManager.getDoc = sinon
  931. .stub()
  932. .callsArgWith(
  933. 2,
  934. null,
  935. this.lines,
  936. this.version,
  937. this.ranges,
  938. null,
  939. false
  940. )
  941. this.DocumentManager.getDocAndFlushIfOld(
  942. this.project_id,
  943. this.doc_id,
  944. this.callback
  945. )
  946. })
  947. it('should get the doc', function () {
  948. this.DocumentManager.getDoc
  949. .calledWith(this.project_id, this.doc_id)
  950. .should.equal(true)
  951. })
  952. it('should not flush the doc', function () {
  953. this.DocumentManager.flushDocIfLoaded.called.should.equal(false)
  954. })
  955. it('should call the callback with the lines and versions', function () {
  956. this.callback
  957. .calledWith(null, this.lines, this.version)
  958. .should.equal(true)
  959. })
  960. })
  961. })
  962. describe('renameDoc', function () {
  963. beforeEach(function () {
  964. this.update = 'some-update'
  965. this.RedisManager.renameDoc = sinon.stub().yields()
  966. })
  967. describe('successfully', function () {
  968. beforeEach(function () {
  969. this.DocumentManager.renameDoc(
  970. this.project_id,
  971. this.doc_id,
  972. this.user_id,
  973. this.update,
  974. this.projectHistoryId,
  975. this.callback
  976. )
  977. })
  978. it('should rename the document', function () {
  979. this.RedisManager.renameDoc
  980. .calledWith(
  981. this.project_id,
  982. this.doc_id,
  983. this.user_id,
  984. this.update,
  985. this.projectHistoryId
  986. )
  987. .should.equal(true)
  988. })
  989. it('should call the callback', function () {
  990. this.callback.called.should.equal(true)
  991. })
  992. })
  993. })
  994. describe('resyncDocContents', function () {
  995. describe('when doc is loaded in redis', function () {
  996. beforeEach(function () {
  997. this.pathnameFromProjectStructureUpdate = '/foo/bar.tex'
  998. this.RedisManager.getDoc = sinon
  999. .stub()
  1000. .callsArgWith(
  1001. 2,
  1002. null,
  1003. this.lines,
  1004. this.version,
  1005. this.ranges,
  1006. this.pathname,
  1007. this.projectHistoryId
  1008. )
  1009. this.ProjectHistoryRedisManager.queueResyncDocContent = sinon.stub()
  1010. this.DocumentManager.resyncDocContents(
  1011. this.project_id,
  1012. this.doc_id,
  1013. this.pathnameFromProjectStructureUpdate,
  1014. this.callback
  1015. )
  1016. })
  1017. it('gets the doc contents from redis', function () {
  1018. this.RedisManager.getDoc
  1019. .calledWith(this.project_id, this.doc_id)
  1020. .should.equal(true)
  1021. })
  1022. it('queues a resync doc content update', function () {
  1023. this.ProjectHistoryRedisManager.queueResyncDocContent
  1024. .calledWith(
  1025. this.project_id,
  1026. this.projectHistoryId,
  1027. this.doc_id,
  1028. this.lines,
  1029. this.version,
  1030. this.pathnameFromProjectStructureUpdate,
  1031. this.callback
  1032. )
  1033. .should.equal(true)
  1034. })
  1035. })
  1036. describe('when doc is not loaded in redis', function () {
  1037. beforeEach(function () {
  1038. this.pathnameFromProjectStructureUpdate = '/foo/bar.tex'
  1039. this.RedisManager.getDoc = sinon.stub().callsArgWith(2, null)
  1040. this.PersistenceManager.getDoc = sinon
  1041. .stub()
  1042. .yields(
  1043. null,
  1044. this.lines,
  1045. this.version,
  1046. this.ranges,
  1047. this.pathname,
  1048. this.projectHistoryId
  1049. )
  1050. this.ProjectHistoryRedisManager.queueResyncDocContent = sinon.stub()
  1051. this.DocumentManager.resyncDocContents(
  1052. this.project_id,
  1053. this.doc_id,
  1054. this.pathnameFromProjectStructureUpdate,
  1055. this.callback
  1056. )
  1057. })
  1058. it('tries to get the doc contents from redis', function () {
  1059. this.RedisManager.getDoc
  1060. .calledWith(this.project_id, this.doc_id)
  1061. .should.equal(true)
  1062. })
  1063. it('gets the doc contents from web', function () {
  1064. this.PersistenceManager.getDoc
  1065. .calledWith(this.project_id, this.doc_id, { peek: true })
  1066. .should.equal(true)
  1067. })
  1068. it('queues a resync doc content update', function () {
  1069. this.ProjectHistoryRedisManager.queueResyncDocContent
  1070. .calledWith(
  1071. this.project_id,
  1072. this.projectHistoryId,
  1073. this.doc_id,
  1074. this.lines,
  1075. this.version,
  1076. this.pathnameFromProjectStructureUpdate,
  1077. this.callback
  1078. )
  1079. .should.equal(true)
  1080. })
  1081. })
  1082. })
  1083. })