DocumentUpdaterManager.test.js 12 KB

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