DocumentUpdaterManagerTests.js 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424
  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 SandboxedModule = require('sandboxed-module')
  15. const path = require('path')
  16. const modulePath = '../../../app/js/DocumentUpdaterManager'
  17. const _ = require('underscore')
  18. describe('DocumentUpdaterManager', function () {
  19. beforeEach(function () {
  20. let Timer
  21. this.project_id = 'project-id-923'
  22. this.doc_id = 'doc-id-394'
  23. this.lines = ['one', 'two', 'three']
  24. this.version = 42
  25. this.settings = {
  26. apis: { documentupdater: { url: 'http://doc-updater.example.com' } },
  27. redis: {
  28. documentupdater: {
  29. key_schema: {
  30. pendingUpdates({ doc_id }) {
  31. return `PendingUpdates:${doc_id}`
  32. },
  33. },
  34. },
  35. },
  36. maxUpdateSize: 7 * 1024 * 1024,
  37. pendingUpdateListShardCount: 10,
  38. }
  39. this.rclient = { auth() {} }
  40. return (this.DocumentUpdaterManager = SandboxedModule.require(modulePath, {
  41. requires: {
  42. '@overleaf/settings': this.settings,
  43. request: (this.request = {}),
  44. '@overleaf/redis-wrapper': { createClient: () => this.rclient },
  45. '@overleaf/metrics': (this.Metrics = {
  46. summary: sinon.stub(),
  47. Timer: (Timer = class Timer {
  48. done() {}
  49. }),
  50. }),
  51. },
  52. }))
  53. }) // avoid modifying JSON object directly
  54. describe('getDocument', function () {
  55. beforeEach(function () {
  56. return (this.callback = sinon.stub())
  57. })
  58. describe('successfully', function () {
  59. beforeEach(function () {
  60. this.body = JSON.stringify({
  61. lines: this.lines,
  62. version: this.version,
  63. ops: (this.ops = ['mock-op-1', 'mock-op-2']),
  64. ranges: (this.ranges = { mock: 'ranges' }),
  65. })
  66. this.fromVersion = 2
  67. this.request.get = sinon
  68. .stub()
  69. .callsArgWith(1, null, { statusCode: 200 }, this.body)
  70. return this.DocumentUpdaterManager.getDocument(
  71. this.project_id,
  72. this.doc_id,
  73. this.fromVersion,
  74. this.callback
  75. )
  76. })
  77. it('should get the document from the document updater', function () {
  78. const url = `${this.settings.apis.documentupdater.url}/project/${this.project_id}/doc/${this.doc_id}?fromVersion=${this.fromVersion}`
  79. return this.request.get.calledWith(url).should.equal(true)
  80. })
  81. return it('should call the callback with the lines, version, ranges and ops', function () {
  82. return this.callback
  83. .calledWith(null, this.lines, this.version, this.ranges, this.ops)
  84. .should.equal(true)
  85. })
  86. })
  87. describe('when the document updater API returns an error', function () {
  88. beforeEach(function () {
  89. this.request.get = sinon
  90. .stub()
  91. .callsArgWith(
  92. 1,
  93. (this.error = new Error('something went wrong')),
  94. null,
  95. null
  96. )
  97. return this.DocumentUpdaterManager.getDocument(
  98. this.project_id,
  99. this.doc_id,
  100. this.fromVersion,
  101. this.callback
  102. )
  103. })
  104. return it('should return an error to the callback', function () {
  105. return this.callback.calledWith(this.error).should.equal(true)
  106. })
  107. })
  108. ;[404, 422].forEach(statusCode =>
  109. describe(`when the document updater returns a ${statusCode} status code`, function () {
  110. beforeEach(function () {
  111. this.request.get = sinon
  112. .stub()
  113. .callsArgWith(1, null, { statusCode }, '')
  114. return this.DocumentUpdaterManager.getDocument(
  115. this.project_id,
  116. this.doc_id,
  117. this.fromVersion,
  118. this.callback
  119. )
  120. })
  121. return it('should return the callback with an error', function () {
  122. this.callback.called.should.equal(true)
  123. this.callback
  124. .calledWith(
  125. sinon.match({
  126. message: 'doc updater could not load requested ops',
  127. info: { statusCode },
  128. })
  129. )
  130. .should.equal(true)
  131. this.logger.error.called.should.equal(false)
  132. this.logger.warn.called.should.equal(false)
  133. })
  134. })
  135. )
  136. return describe('when the document updater returns a failure error code', function () {
  137. beforeEach(function () {
  138. this.request.get = sinon
  139. .stub()
  140. .callsArgWith(1, null, { statusCode: 500 }, '')
  141. return this.DocumentUpdaterManager.getDocument(
  142. this.project_id,
  143. this.doc_id,
  144. this.fromVersion,
  145. this.callback
  146. )
  147. })
  148. return it('should return the callback with an error', function () {
  149. this.callback.called.should.equal(true)
  150. this.callback
  151. .calledWith(
  152. sinon.match({
  153. message: 'doc updater returned a non-success status code',
  154. info: {
  155. action: 'getDocument',
  156. statusCode: 500,
  157. },
  158. })
  159. )
  160. .should.equal(true)
  161. this.logger.error.called.should.equal(false)
  162. })
  163. })
  164. })
  165. describe('flushProjectToMongoAndDelete', function () {
  166. beforeEach(function () {
  167. return (this.callback = sinon.stub())
  168. })
  169. describe('successfully', function () {
  170. beforeEach(function () {
  171. this.request.del = sinon
  172. .stub()
  173. .callsArgWith(1, null, { statusCode: 204 }, '')
  174. return this.DocumentUpdaterManager.flushProjectToMongoAndDelete(
  175. this.project_id,
  176. this.callback
  177. )
  178. })
  179. it('should delete the project from the document updater', function () {
  180. const url = `${this.settings.apis.documentupdater.url}/project/${this.project_id}?background=true`
  181. return this.request.del.calledWith(url).should.equal(true)
  182. })
  183. return it('should call the callback with no error', function () {
  184. return this.callback.calledWith(null).should.equal(true)
  185. })
  186. })
  187. describe('when the document updater API returns an error', function () {
  188. beforeEach(function () {
  189. this.request.del = sinon
  190. .stub()
  191. .callsArgWith(
  192. 1,
  193. (this.error = new Error('something went wrong')),
  194. null,
  195. null
  196. )
  197. return this.DocumentUpdaterManager.flushProjectToMongoAndDelete(
  198. this.project_id,
  199. this.callback
  200. )
  201. })
  202. return it('should return an error to the callback', function () {
  203. return this.callback.calledWith(this.error).should.equal(true)
  204. })
  205. })
  206. return describe('when the document updater returns a failure error code', function () {
  207. beforeEach(function () {
  208. this.request.del = sinon
  209. .stub()
  210. .callsArgWith(1, null, { statusCode: 500 }, '')
  211. return this.DocumentUpdaterManager.flushProjectToMongoAndDelete(
  212. this.project_id,
  213. this.callback
  214. )
  215. })
  216. return it('should return the callback with an error', function () {
  217. this.callback.called.should.equal(true)
  218. this.callback
  219. .calledWith(
  220. sinon.match({
  221. message: 'doc updater returned a non-success status code',
  222. info: {
  223. action: 'flushProjectToMongoAndDelete',
  224. statusCode: 500,
  225. },
  226. })
  227. )
  228. .should.equal(true)
  229. })
  230. })
  231. })
  232. describe('queueChange', function () {
  233. beforeEach(function () {
  234. this.change = {
  235. doc: '1234567890',
  236. op: [{ d: 'test', p: 345 }],
  237. v: 789,
  238. }
  239. this.rclient.rpush = sinon.stub().yields()
  240. return (this.callback = sinon.stub())
  241. })
  242. describe('successfully', function () {
  243. beforeEach(function () {
  244. this.pendingUpdateListKey = `pending-updates-list-key-${Math.random()}`
  245. this.DocumentUpdaterManager._getPendingUpdateListKey = sinon
  246. .stub()
  247. .returns(this.pendingUpdateListKey)
  248. this.DocumentUpdaterManager.queueChange(
  249. this.project_id,
  250. this.doc_id,
  251. this.change,
  252. this.callback
  253. )
  254. })
  255. it('should push the change', function () {
  256. this.rclient.rpush
  257. .calledWith(
  258. `PendingUpdates:${this.doc_id}`,
  259. JSON.stringify(this.change)
  260. )
  261. .should.equal(true)
  262. })
  263. it('should notify the doc updater of the change via the pending-updates-list queue', function () {
  264. this.rclient.rpush
  265. .calledWith(
  266. this.pendingUpdateListKey,
  267. `${this.project_id}:${this.doc_id}`
  268. )
  269. .should.equal(true)
  270. })
  271. })
  272. describe('with error talking to redis during rpush', function () {
  273. beforeEach(function () {
  274. this.rclient.rpush = sinon
  275. .stub()
  276. .yields(new Error('something went wrong'))
  277. return this.DocumentUpdaterManager.queueChange(
  278. this.project_id,
  279. this.doc_id,
  280. this.change,
  281. this.callback
  282. )
  283. })
  284. return it('should return an error', function () {
  285. return this.callback
  286. .calledWithExactly(sinon.match(Error))
  287. .should.equal(true)
  288. })
  289. })
  290. describe('with null byte corruption', function () {
  291. beforeEach(function () {
  292. this.stringifyStub = sinon
  293. .stub(JSON, 'stringify')
  294. .callsFake(() => '["bad bytes! \u0000 <- here"]')
  295. return this.DocumentUpdaterManager.queueChange(
  296. this.project_id,
  297. this.doc_id,
  298. this.change,
  299. this.callback
  300. )
  301. })
  302. afterEach(function () {
  303. this.stringifyStub.restore()
  304. })
  305. it('should return an error', function () {
  306. return this.callback
  307. .calledWithExactly(sinon.match(Error))
  308. .should.equal(true)
  309. })
  310. return it('should not push the change onto the pending-updates-list queue', function () {
  311. return this.rclient.rpush.called.should.equal(false)
  312. })
  313. })
  314. describe('when the update is too large', function () {
  315. beforeEach(function () {
  316. this.change = {
  317. op: { p: 12, t: 'update is too large'.repeat(1024 * 400) },
  318. }
  319. return this.DocumentUpdaterManager.queueChange(
  320. this.project_id,
  321. this.doc_id,
  322. this.change,
  323. this.callback
  324. )
  325. })
  326. it('should return an error', function () {
  327. return this.callback
  328. .calledWithExactly(sinon.match(Error))
  329. .should.equal(true)
  330. })
  331. it('should add the size to the error', function () {
  332. return this.callback.args[0][0].info.updateSize.should.equal(7782422)
  333. })
  334. return it('should not push the change onto the pending-updates-list queue', function () {
  335. return this.rclient.rpush.called.should.equal(false)
  336. })
  337. })
  338. describe('with invalid keys', function () {
  339. beforeEach(function () {
  340. this.change = {
  341. op: [{ d: 'test', p: 345 }],
  342. version: 789, // not a valid key
  343. }
  344. return this.DocumentUpdaterManager.queueChange(
  345. this.project_id,
  346. this.doc_id,
  347. this.change,
  348. this.callback
  349. )
  350. })
  351. it('should remove the invalid keys from the change', function () {
  352. return this.rclient.rpush
  353. .calledWith(
  354. `PendingUpdates:${this.doc_id}`,
  355. JSON.stringify({ op: this.change.op })
  356. )
  357. .should.equal(true)
  358. })
  359. })
  360. })
  361. describe('_getPendingUpdateListKey', function () {
  362. beforeEach(function () {
  363. const keys = _.times(
  364. 10000,
  365. this.DocumentUpdaterManager._getPendingUpdateListKey
  366. )
  367. this.keys = _.unique(keys)
  368. })
  369. it('should return normal pending updates key', function () {
  370. _.contains(this.keys, 'pending-updates-list').should.equal(true)
  371. })
  372. it('should return pending-updates-list-n keys', function () {
  373. _.contains(this.keys, 'pending-updates-list-1').should.equal(true)
  374. _.contains(this.keys, 'pending-updates-list-3').should.equal(true)
  375. _.contains(this.keys, 'pending-updates-list-9').should.equal(true)
  376. })
  377. it('should not include pending-updates-list-0 key', function () {
  378. _.contains(this.keys, 'pending-updates-list-0').should.equal(false)
  379. })
  380. it('should not include maximum as pendingUpdateListShardCount value', function () {
  381. _.contains(this.keys, 'pending-updates-list-10').should.equal(false)
  382. })
  383. })
  384. })