RangesManagerTests.js 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518
  1. const sinon = require('sinon')
  2. const { expect } = require('chai')
  3. const SandboxedModule = require('sandboxed-module')
  4. const MODULE_PATH = '../../../../app/js/RangesManager.js'
  5. describe('RangesManager', function () {
  6. beforeEach(function () {
  7. this.RangesManager = SandboxedModule.require(MODULE_PATH, {
  8. requires: {
  9. '@overleaf/metrics': (this.Metrics = { histogram: sinon.stub() }),
  10. },
  11. })
  12. this.doc_id = 'doc-id-123'
  13. this.project_id = 'project-id-123'
  14. this.user_id = 'user-id-123'
  15. })
  16. describe('applyUpdate', function () {
  17. beforeEach(function () {
  18. this.updates = [
  19. {
  20. meta: {
  21. user_id: this.user_id,
  22. },
  23. op: [
  24. {
  25. i: 'two ',
  26. p: 4,
  27. },
  28. ],
  29. },
  30. ]
  31. this.entries = {
  32. comments: [
  33. {
  34. op: {
  35. c: 'three ',
  36. p: 4,
  37. },
  38. metadata: {
  39. user_id: this.user_id,
  40. },
  41. },
  42. ],
  43. changes: [
  44. {
  45. op: {
  46. i: 'five',
  47. p: 15,
  48. },
  49. metadata: {
  50. user_id: this.user_id,
  51. },
  52. },
  53. ],
  54. }
  55. this.newDocLines = ['one two three four five']
  56. }) // old is "one three four five"
  57. describe('successfully', function () {
  58. beforeEach(function () {
  59. this.result = this.RangesManager.applyUpdate(
  60. this.project_id,
  61. this.doc_id,
  62. this.entries,
  63. this.updates,
  64. this.newDocLines
  65. )
  66. })
  67. it('should return the modified the comments and changes', function () {
  68. expect(this.result.rangesWereCollapsed).to.equal(false)
  69. this.result.newRanges.comments[0].op.should.deep.equal({
  70. c: 'three ',
  71. p: 8,
  72. })
  73. this.result.newRanges.changes[0].op.should.deep.equal({
  74. i: 'five',
  75. p: 19,
  76. })
  77. })
  78. })
  79. describe('with empty comments', function () {
  80. beforeEach(function () {
  81. this.entries.comments = []
  82. this.result = this.RangesManager.applyUpdate(
  83. this.project_id,
  84. this.doc_id,
  85. this.entries,
  86. this.updates,
  87. this.newDocLines
  88. )
  89. })
  90. it('should return an object with no comments', function () {
  91. // Save space in redis and don't store just {}
  92. expect(this.result.newRanges.comments).to.be.undefined
  93. })
  94. })
  95. describe('with empty changes', function () {
  96. beforeEach(function () {
  97. this.entries.changes = []
  98. this.result = this.RangesManager.applyUpdate(
  99. this.project_id,
  100. this.doc_id,
  101. this.entries,
  102. this.updates,
  103. this.newDocLines
  104. )
  105. })
  106. it('should return an object with no changes', function () {
  107. // Save space in redis and don't store just {}
  108. expect(this.result.newRanges.changes).to.be.undefined
  109. })
  110. })
  111. describe('with too many comments', function () {
  112. beforeEach(function () {
  113. this.RangesManager.MAX_COMMENTS = 2
  114. this.updates = [
  115. {
  116. meta: {
  117. user_id: this.user_id,
  118. },
  119. op: [
  120. {
  121. c: 'one',
  122. p: 0,
  123. t: 'thread-id-1',
  124. },
  125. ],
  126. },
  127. ]
  128. this.entries = {
  129. comments: [
  130. {
  131. op: {
  132. c: 'three ',
  133. p: 4,
  134. t: 'thread-id-2',
  135. },
  136. metadata: {
  137. user_id: this.user_id,
  138. },
  139. },
  140. {
  141. op: {
  142. c: 'four ',
  143. p: 10,
  144. t: 'thread-id-3',
  145. },
  146. metadata: {
  147. user_id: this.user_id,
  148. },
  149. },
  150. ],
  151. changes: [],
  152. }
  153. })
  154. it('should throw an error', function () {
  155. expect(() => {
  156. this.RangesManager.applyUpdate(
  157. this.project_id,
  158. this.doc_id,
  159. this.entries,
  160. this.updates,
  161. this.newDocLines
  162. )
  163. }).to.throw('too many comments or tracked changes')
  164. })
  165. })
  166. describe('with too many changes', function () {
  167. beforeEach(function () {
  168. this.RangesManager.MAX_CHANGES = 2
  169. this.updates = [
  170. {
  171. meta: {
  172. user_id: this.user_id,
  173. tc: 'track-changes-id-yes',
  174. },
  175. op: [
  176. {
  177. i: 'one ',
  178. p: 0,
  179. },
  180. ],
  181. },
  182. ]
  183. this.entries = {
  184. changes: [
  185. {
  186. op: {
  187. i: 'three',
  188. p: 4,
  189. },
  190. metadata: {
  191. user_id: this.user_id,
  192. },
  193. },
  194. {
  195. op: {
  196. i: 'four',
  197. p: 10,
  198. },
  199. metadata: {
  200. user_id: this.user_id,
  201. },
  202. },
  203. ],
  204. comments: [],
  205. }
  206. this.newDocLines = ['one two three four']
  207. })
  208. it('should throw an error', function () {
  209. expect(() => {
  210. this.RangesManager.applyUpdate(
  211. this.project_id,
  212. this.doc_id,
  213. this.entries,
  214. this.updates,
  215. this.newDocLines
  216. )
  217. }).to.throw('too many comments or tracked changes')
  218. })
  219. })
  220. describe('inconsistent changes', function () {
  221. beforeEach(function () {
  222. this.updates = [
  223. {
  224. meta: {
  225. user_id: this.user_id,
  226. },
  227. op: [
  228. {
  229. c: "doesn't match",
  230. p: 0,
  231. },
  232. ],
  233. },
  234. ]
  235. })
  236. it('should throw an error', function () {
  237. expect(() => {
  238. this.RangesManager.applyUpdate(
  239. this.project_id,
  240. this.doc_id,
  241. this.entries,
  242. this.updates,
  243. this.newDocLines
  244. )
  245. }).to.throw(
  246. 'Change ({"op":{"i":"five","p":15},"metadata":{"user_id":"user-id-123"}}) doesn\'t match text ("our ")'
  247. )
  248. })
  249. })
  250. describe('with an update that collapses a range', function () {
  251. beforeEach(function () {
  252. this.updates = [
  253. {
  254. meta: {
  255. user_id: this.user_id,
  256. },
  257. op: [
  258. {
  259. d: 'one',
  260. p: 0,
  261. t: 'thread-id-1',
  262. },
  263. ],
  264. },
  265. ]
  266. this.entries = {
  267. comments: [
  268. {
  269. op: {
  270. c: 'n',
  271. p: 1,
  272. t: 'thread-id-2',
  273. },
  274. metadata: {
  275. user_id: this.user_id,
  276. },
  277. },
  278. ],
  279. changes: [],
  280. }
  281. this.result = this.RangesManager.applyUpdate(
  282. this.project_id,
  283. this.doc_id,
  284. this.entries,
  285. this.updates,
  286. this.newDocLines
  287. )
  288. })
  289. it('should return ranges_were_collapsed == true', function () {
  290. expect(this.result.rangesWereCollapsed).to.equal(true)
  291. })
  292. })
  293. describe('with an update that deletes ranges', function () {
  294. beforeEach(function () {
  295. this.updates = [
  296. {
  297. meta: {
  298. user_id: this.user_id,
  299. },
  300. op: [
  301. {
  302. d: 'one two three four five',
  303. p: 0,
  304. },
  305. ],
  306. },
  307. ]
  308. this.entries = {
  309. comments: [
  310. {
  311. op: {
  312. c: 'n',
  313. p: 1,
  314. t: 'thread-id-2',
  315. },
  316. metadata: {
  317. user_id: this.user_id,
  318. },
  319. },
  320. ],
  321. changes: [
  322. {
  323. op: {
  324. i: 'hello',
  325. p: 1,
  326. t: 'thread-id-2',
  327. },
  328. metadata: {
  329. user_id: this.user_id,
  330. },
  331. },
  332. ],
  333. }
  334. this.result = this.RangesManager.applyUpdate(
  335. this.project_id,
  336. this.doc_id,
  337. this.entries,
  338. this.updates,
  339. this.newDocLines
  340. )
  341. })
  342. it('should increment the range-delta histogram', function () {
  343. this.Metrics.histogram.called.should.equal(true)
  344. })
  345. it('should return ranges_were_collapsed == true', function () {
  346. expect(this.result.rangesWereCollapsed).to.equal(true)
  347. })
  348. })
  349. })
  350. describe('acceptChanges', function () {
  351. beforeEach(function () {
  352. this.RangesManager = SandboxedModule.require(MODULE_PATH, {
  353. requires: {
  354. '@overleaf/ranges-tracker': (this.RangesTracker =
  355. SandboxedModule.require('@overleaf/ranges-tracker')),
  356. '@overleaf/metrics': {},
  357. },
  358. })
  359. this.ranges = {
  360. comments: [],
  361. changes: [
  362. {
  363. id: 'a1',
  364. op: {
  365. i: 'lorem',
  366. p: 0,
  367. },
  368. },
  369. {
  370. id: 'a2',
  371. op: {
  372. i: 'ipsum',
  373. p: 10,
  374. },
  375. },
  376. {
  377. id: 'a3',
  378. op: {
  379. i: 'dolor',
  380. p: 20,
  381. },
  382. },
  383. {
  384. id: 'a4',
  385. op: {
  386. i: 'sit',
  387. p: 30,
  388. },
  389. },
  390. {
  391. id: 'a5',
  392. op: {
  393. i: 'amet',
  394. p: 40,
  395. },
  396. },
  397. ],
  398. }
  399. this.removeChangeIdsSpy = sinon.spy(
  400. this.RangesTracker.prototype,
  401. 'removeChangeIds'
  402. )
  403. })
  404. describe('successfully with a single change', function () {
  405. beforeEach(function () {
  406. this.change_ids = [this.ranges.changes[1].id]
  407. this.result = this.RangesManager.acceptChanges(
  408. this.change_ids,
  409. this.ranges
  410. )
  411. })
  412. it('should log the call with the correct number of changes', function () {
  413. this.logger.debug
  414. .calledWith('accepting 1 changes in ranges')
  415. .should.equal(true)
  416. })
  417. it('should delegate the change removal to the ranges tracker', function () {
  418. this.removeChangeIdsSpy.calledWith(this.change_ids).should.equal(true)
  419. })
  420. it('should remove the change', function () {
  421. expect(
  422. this.result.changes.find(
  423. change => change.id === this.ranges.changes[1].id
  424. )
  425. ).to.be.undefined
  426. })
  427. it('should return the original number of changes minus 1', function () {
  428. this.result.changes.length.should.equal(this.ranges.changes.length - 1)
  429. })
  430. it('should not touch other changes', function () {
  431. for (const i of [0, 2, 3, 4]) {
  432. expect(
  433. this.result.changes.find(
  434. change => change.id === this.ranges.changes[i].id
  435. )
  436. ).to.deep.equal(this.ranges.changes[i])
  437. }
  438. })
  439. })
  440. describe('successfully with multiple changes', function () {
  441. beforeEach(function () {
  442. this.change_ids = [
  443. this.ranges.changes[1].id,
  444. this.ranges.changes[3].id,
  445. this.ranges.changes[4].id,
  446. ]
  447. this.result = this.RangesManager.acceptChanges(
  448. this.change_ids,
  449. this.ranges
  450. )
  451. })
  452. it('should log the call with the correct number of changes', function () {
  453. this.logger.debug
  454. .calledWith(`accepting ${this.change_ids.length} changes in ranges`)
  455. .should.equal(true)
  456. })
  457. it('should delegate the change removal to the ranges tracker', function () {
  458. this.removeChangeIdsSpy.calledWith(this.change_ids).should.equal(true)
  459. })
  460. it('should remove the changes', function () {
  461. for (const i of [1, 3, 4]) {
  462. expect(
  463. this.result.changes.find(
  464. change => change.id === this.ranges.changes[i].id
  465. )
  466. ).to.be.undefined
  467. }
  468. })
  469. it('should return the original number of changes minus the number of accepted changes', function () {
  470. this.result.changes.length.should.equal(this.ranges.changes.length - 3)
  471. })
  472. it('should not touch other changes', function () {
  473. for (const i of [0, 2]) {
  474. expect(
  475. this.result.changes.find(
  476. change => change.id === this.ranges.changes[i].id
  477. )
  478. ).to.deep.equal(this.ranges.changes[i])
  479. }
  480. })
  481. })
  482. })
  483. })