DocumentManagerTests.js 33 KB

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