DiffManagerTests.js 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451
  1. /* eslint-disable
  2. camelcase,
  3. no-return-assign,
  4. no-unused-vars,
  5. */
  6. // TODO: This file was created by bulk-decaffeinate.
  7. // Fix any style issues and re-enable lint.
  8. /*
  9. * decaffeinate suggestions:
  10. * DS102: Remove unnecessary code created because of implicit returns
  11. * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
  12. */
  13. const sinon = require('sinon')
  14. const chai = require('chai')
  15. const should = chai.should()
  16. const { expect } = chai
  17. const modulePath = '../../../../app/js/DiffManager.js'
  18. const SandboxedModule = require('sandboxed-module')
  19. describe('DiffManager', function () {
  20. beforeEach(function () {
  21. this.DiffManager = SandboxedModule.require(modulePath, {
  22. requires: {
  23. 'logger-sharelatex': (this.logger = {
  24. log: sinon.stub(),
  25. error: sinon.stub(),
  26. warn: sinon.stub()
  27. }),
  28. './UpdatesManager': (this.UpdatesManager = {}),
  29. './DocumentUpdaterManager': (this.DocumentUpdaterManager = {}),
  30. './DiffGenerator': (this.DiffGenerator = {})
  31. }
  32. })
  33. this.callback = sinon.stub()
  34. this.from = new Date()
  35. this.to = new Date(Date.now() + 10000)
  36. this.project_id = 'mock-project-id'
  37. return (this.doc_id = 'mock-doc-id')
  38. })
  39. describe('getLatestDocAndUpdates', function () {
  40. beforeEach(function () {
  41. this.content = 'hello world'
  42. this.version = 42
  43. this.updates = ['mock-update-1', 'mock-update-2']
  44. this.DocumentUpdaterManager.getDocument = sinon
  45. .stub()
  46. .callsArgWith(2, null, this.content, this.version)
  47. return (this.UpdatesManager.getDocUpdatesWithUserInfo = sinon
  48. .stub()
  49. .callsArgWith(3, null, this.updates))
  50. })
  51. describe('with a fromVersion', function () {
  52. beforeEach(function () {
  53. return this.DiffManager.getLatestDocAndUpdates(
  54. this.project_id,
  55. this.doc_id,
  56. this.from,
  57. this.callback
  58. )
  59. })
  60. it('should get the latest version of the doc', function () {
  61. return this.DocumentUpdaterManager.getDocument
  62. .calledWith(this.project_id, this.doc_id)
  63. .should.equal(true)
  64. })
  65. it('should get the latest updates', function () {
  66. return this.UpdatesManager.getDocUpdatesWithUserInfo
  67. .calledWith(this.project_id, this.doc_id, { from: this.from })
  68. .should.equal(true)
  69. })
  70. return it('should call the callback with the content, version and updates', function () {
  71. return this.callback
  72. .calledWith(null, this.content, this.version, this.updates)
  73. .should.equal(true)
  74. })
  75. })
  76. return describe('with no fromVersion', function () {
  77. beforeEach(function () {
  78. return this.DiffManager.getLatestDocAndUpdates(
  79. this.project_id,
  80. this.doc_id,
  81. null,
  82. this.callback
  83. )
  84. })
  85. it('should get the latest version of the doc', function () {
  86. return this.DocumentUpdaterManager.getDocument
  87. .calledWith(this.project_id, this.doc_id)
  88. .should.equal(true)
  89. })
  90. it('should not get the latest updates', function () {
  91. return this.UpdatesManager.getDocUpdatesWithUserInfo.called.should.equal(
  92. false
  93. )
  94. })
  95. return it('should call the callback with the content, version and blank updates', function () {
  96. return this.callback
  97. .calledWith(null, this.content, this.version, [])
  98. .should.equal(true)
  99. })
  100. })
  101. })
  102. describe('getDiff', function () {
  103. beforeEach(function () {
  104. this.content = 'hello world'
  105. // Op versions are the version they were applied to, so doc is always one version
  106. // ahead.s
  107. this.version = 43
  108. this.updates = [
  109. {
  110. op: 'mock-4',
  111. v: 42,
  112. meta: { start_ts: new Date(this.to.getTime() + 20) }
  113. },
  114. {
  115. op: 'mock-3',
  116. v: 41,
  117. meta: { start_ts: new Date(this.to.getTime() + 10) }
  118. },
  119. {
  120. op: 'mock-2',
  121. v: 40,
  122. meta: { start_ts: new Date(this.to.getTime() - 10) }
  123. },
  124. {
  125. op: 'mock-1',
  126. v: 39,
  127. meta: { start_ts: new Date(this.to.getTime() - 20) }
  128. }
  129. ]
  130. this.fromVersion = 39
  131. this.toVersion = 40
  132. this.diffed_updates = this.updates.slice(2)
  133. this.rewound_content = 'rewound-content'
  134. return (this.diff = [{ u: 'mock-diff' }])
  135. })
  136. describe('with matching versions', function () {
  137. beforeEach(function () {
  138. this.DiffManager.getDocumentBeforeVersion = sinon
  139. .stub()
  140. .callsArgWith(3, null, this.rewound_content, this.updates)
  141. this.DiffGenerator.buildDiff = sinon.stub().returns(this.diff)
  142. return this.DiffManager.getDiff(
  143. this.project_id,
  144. this.doc_id,
  145. this.fromVersion,
  146. this.toVersion,
  147. this.callback
  148. )
  149. })
  150. it('should get the latest doc and version with all recent updates', function () {
  151. return this.DiffManager.getDocumentBeforeVersion
  152. .calledWith(this.project_id, this.doc_id, this.fromVersion)
  153. .should.equal(true)
  154. })
  155. it('should generate the diff', function () {
  156. return this.DiffGenerator.buildDiff
  157. .calledWith(
  158. this.rewound_content,
  159. this.diffed_updates.slice().reverse()
  160. )
  161. .should.equal(true)
  162. })
  163. return it('should call the callback with the diff', function () {
  164. return this.callback.calledWith(null, this.diff).should.equal(true)
  165. })
  166. })
  167. describe('when the updates are inconsistent', function () {
  168. beforeEach(function () {
  169. this.DiffManager.getLatestDocAndUpdates = sinon
  170. .stub()
  171. .callsArgWith(3, null, this.content, this.version, this.updates)
  172. this.DiffGenerator.buildDiff = sinon
  173. .stub()
  174. .throws((this.error = new Error('inconsistent!')))
  175. this.DiffGenerator.rewindUpdates = sinon.stub()
  176. this.DiffManager.getDiff(
  177. this.project_id,
  178. this.doc_id,
  179. this.fromVersion,
  180. this.toVersion,
  181. this.callback
  182. )
  183. })
  184. it('should call the callback with an error', function () {
  185. this.callback.calledWith(sinon.match(Error)).should.equal(true)
  186. const errorObj = this.callback.args[0][0]
  187. expect(errorObj.message).to.include('inconsistent!')
  188. })
  189. })
  190. })
  191. describe('getDocumentBeforeVersion', function () {
  192. beforeEach(function () {
  193. this.DiffManager._tryGetDocumentBeforeVersion = sinon.stub()
  194. this.document = 'mock-documents'
  195. return (this.rewound_updates = 'mock-rewound-updates')
  196. })
  197. describe('succesfully', function () {
  198. beforeEach(function () {
  199. this.DiffManager._tryGetDocumentBeforeVersion.yields(
  200. null,
  201. this.document,
  202. this.rewound_updates
  203. )
  204. return this.DiffManager.getDocumentBeforeVersion(
  205. this.project_id,
  206. this.doc_id,
  207. this.version,
  208. this.callback
  209. )
  210. })
  211. it('should call _tryGetDocumentBeforeVersion', function () {
  212. return this.DiffManager._tryGetDocumentBeforeVersion
  213. .calledWith(this.project_id, this.doc_id, this.version)
  214. .should.equal(true)
  215. })
  216. return it('should call the callback with the response', function () {
  217. return this.callback
  218. .calledWith(null, this.document, this.rewound_updates)
  219. .should.equal(true)
  220. })
  221. })
  222. describe('with a retry needed', function () {
  223. beforeEach(function () {
  224. let retried = false
  225. this.DiffManager._tryGetDocumentBeforeVersion = (
  226. project_id,
  227. doc_id,
  228. version,
  229. callback
  230. ) => {
  231. if (!retried) {
  232. retried = true
  233. const error = new Error()
  234. error.retry = true
  235. return callback(error)
  236. } else {
  237. return callback(null, this.document, this.rewound_updates)
  238. }
  239. }
  240. sinon.spy(this.DiffManager, '_tryGetDocumentBeforeVersion')
  241. return this.DiffManager.getDocumentBeforeVersion(
  242. this.project_id,
  243. this.doc_id,
  244. this.version,
  245. this.callback
  246. )
  247. })
  248. it('should call _tryGetDocumentBeforeVersion twice', function () {
  249. return this.DiffManager._tryGetDocumentBeforeVersion.calledTwice.should.equal(
  250. true
  251. )
  252. })
  253. return it('should call the callback with the response', function () {
  254. return this.callback
  255. .calledWith(null, this.document, this.rewound_updates)
  256. .should.equal(true)
  257. })
  258. })
  259. describe('with a non-retriable error', function () {
  260. beforeEach(function () {
  261. this.error = new Error('oops')
  262. this.DiffManager._tryGetDocumentBeforeVersion.yields(this.error)
  263. return this.DiffManager.getDocumentBeforeVersion(
  264. this.project_id,
  265. this.doc_id,
  266. this.version,
  267. this.callback
  268. )
  269. })
  270. it('should call _tryGetDocumentBeforeVersion once', function () {
  271. return this.DiffManager._tryGetDocumentBeforeVersion.calledOnce.should.equal(
  272. true
  273. )
  274. })
  275. return it('should call the callback with the error', function () {
  276. return this.callback.calledWith(this.error).should.equal(true)
  277. })
  278. })
  279. return describe('when retry limit is matched', function () {
  280. beforeEach(function () {
  281. this.error = new Error('oops')
  282. this.error.retry = true
  283. this.DiffManager._tryGetDocumentBeforeVersion.yields(this.error)
  284. return this.DiffManager.getDocumentBeforeVersion(
  285. this.project_id,
  286. this.doc_id,
  287. this.version,
  288. this.callback
  289. )
  290. })
  291. it('should call _tryGetDocumentBeforeVersion three times (max retries)', function () {
  292. return this.DiffManager._tryGetDocumentBeforeVersion.calledThrice.should.equal(
  293. true
  294. )
  295. })
  296. return it('should call the callback with the error', function () {
  297. return this.callback.calledWith(this.error).should.equal(true)
  298. })
  299. })
  300. })
  301. return describe('_tryGetDocumentBeforeVersion', function () {
  302. beforeEach(function () {
  303. this.content = 'hello world'
  304. // Op versions are the version they were applied to, so doc is always one version
  305. // ahead.s
  306. this.version = 43
  307. this.updates = [
  308. {
  309. op: 'mock-4',
  310. v: 42,
  311. meta: { start_ts: new Date(this.to.getTime() + 20) }
  312. },
  313. {
  314. op: 'mock-3',
  315. v: 41,
  316. meta: { start_ts: new Date(this.to.getTime() + 10) }
  317. },
  318. {
  319. op: 'mock-2',
  320. v: 40,
  321. meta: { start_ts: new Date(this.to.getTime() - 10) }
  322. },
  323. {
  324. op: 'mock-1',
  325. v: 39,
  326. meta: { start_ts: new Date(this.to.getTime() - 20) }
  327. }
  328. ]
  329. this.fromVersion = 39
  330. this.rewound_content = 'rewound-content'
  331. return (this.diff = [{ u: 'mock-diff' }])
  332. })
  333. describe('with matching versions', function () {
  334. beforeEach(function () {
  335. this.DiffManager.getLatestDocAndUpdates = sinon
  336. .stub()
  337. .callsArgWith(3, null, this.content, this.version, this.updates)
  338. this.DiffGenerator.rewindUpdates = sinon.spy((content, updates) => {
  339. // the rewindUpdates method reverses the 'updates' array
  340. updates.reverse()
  341. return this.rewound_content
  342. })
  343. this.rewindUpdatesWithArgs = this.DiffGenerator.rewindUpdates.withArgs(
  344. this.content,
  345. this.updates.slice().reverse()
  346. )
  347. return this.DiffManager._tryGetDocumentBeforeVersion(
  348. this.project_id,
  349. this.doc_id,
  350. this.fromVersion,
  351. this.callback
  352. )
  353. })
  354. it('should get the latest doc and version with all recent updates', function () {
  355. return this.DiffManager.getLatestDocAndUpdates
  356. .calledWith(this.project_id, this.doc_id, this.fromVersion)
  357. .should.equal(true)
  358. })
  359. it('should rewind the diff', function () {
  360. return sinon.assert.calledOnce(this.rewindUpdatesWithArgs)
  361. })
  362. return it('should call the callback with the rewound document and updates', function () {
  363. return this.callback
  364. .calledWith(null, this.rewound_content, this.updates)
  365. .should.equal(true)
  366. })
  367. })
  368. describe('with mismatching versions', function () {
  369. beforeEach(function () {
  370. this.version = 50
  371. this.updates = [
  372. { op: 'mock-1', v: 40 },
  373. { op: 'mock-1', v: 39 }
  374. ]
  375. this.DiffManager.getLatestDocAndUpdates = sinon
  376. .stub()
  377. .callsArgWith(3, null, this.content, this.version, this.updates)
  378. return this.DiffManager._tryGetDocumentBeforeVersion(
  379. this.project_id,
  380. this.doc_id,
  381. this.fromVersion,
  382. this.callback
  383. )
  384. })
  385. return it('should call the callback with an error with retry = true set', function () {
  386. this.callback.calledOnce.should.equal(true)
  387. const error = this.callback.args[0][0]
  388. return expect(error.retry).to.equal(true)
  389. })
  390. })
  391. return describe('when the updates are inconsistent', function () {
  392. beforeEach(function () {
  393. this.DiffManager.getLatestDocAndUpdates = sinon
  394. .stub()
  395. .callsArgWith(3, null, this.content, this.version, this.updates)
  396. this.DiffGenerator.rewindUpdates = sinon
  397. .stub()
  398. .throws((this.error = new Error('inconsistent!')))
  399. return this.DiffManager.getDocumentBeforeVersion(
  400. this.project_id,
  401. this.doc_id,
  402. this.fromVersion,
  403. this.callback
  404. )
  405. })
  406. return it('should call the callback with an error', function () {
  407. return this.callback.calledWith(this.error).should.equal(true)
  408. })
  409. })
  410. })
  411. })