DocumentManagerTests.js 41 KB

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