DocumentManagerTests.js 32 KB

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