DocumentUpdaterManagerTests.js 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381
  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. require('chai').should()
  14. const sinon = require('sinon')
  15. const SandboxedModule = require('sandboxed-module')
  16. const path = require('path')
  17. const modulePath = '../../../app/js/DocumentUpdaterManager'
  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. }
  38. this.rclient = { auth() {} }
  39. return (this.DocumentUpdaterManager = SandboxedModule.require(modulePath, {
  40. requires: {
  41. 'settings-sharelatex': this.settings,
  42. 'logger-sharelatex': (this.logger = {
  43. log: sinon.stub(),
  44. error: sinon.stub(),
  45. warn: sinon.stub()
  46. }),
  47. request: (this.request = {}),
  48. 'redis-sharelatex': { createClient: () => this.rclient },
  49. 'metrics-sharelatex': (this.Metrics = {
  50. summary: sinon.stub(),
  51. Timer: (Timer = class Timer {
  52. done() {}
  53. })
  54. })
  55. },
  56. globals: {
  57. JSON: (this.JSON = Object.create(JSON))
  58. }
  59. }))
  60. }) // avoid modifying JSON object directly
  61. describe('getDocument', function () {
  62. beforeEach(function () {
  63. return (this.callback = sinon.stub())
  64. })
  65. describe('successfully', function () {
  66. beforeEach(function () {
  67. this.body = JSON.stringify({
  68. lines: this.lines,
  69. version: this.version,
  70. ops: (this.ops = ['mock-op-1', 'mock-op-2']),
  71. ranges: (this.ranges = { mock: 'ranges' })
  72. })
  73. this.fromVersion = 2
  74. this.request.get = sinon
  75. .stub()
  76. .callsArgWith(1, null, { statusCode: 200 }, this.body)
  77. return this.DocumentUpdaterManager.getDocument(
  78. this.project_id,
  79. this.doc_id,
  80. this.fromVersion,
  81. this.callback
  82. )
  83. })
  84. it('should get the document from the document updater', function () {
  85. const url = `${this.settings.apis.documentupdater.url}/project/${this.project_id}/doc/${this.doc_id}?fromVersion=${this.fromVersion}`
  86. return this.request.get.calledWith(url).should.equal(true)
  87. })
  88. return it('should call the callback with the lines, version, ranges and ops', function () {
  89. return this.callback
  90. .calledWith(null, this.lines, this.version, this.ranges, this.ops)
  91. .should.equal(true)
  92. })
  93. })
  94. describe('when the document updater API returns an error', function () {
  95. beforeEach(function () {
  96. this.request.get = sinon
  97. .stub()
  98. .callsArgWith(
  99. 1,
  100. (this.error = new Error('something went wrong')),
  101. null,
  102. null
  103. )
  104. return this.DocumentUpdaterManager.getDocument(
  105. this.project_id,
  106. this.doc_id,
  107. this.fromVersion,
  108. this.callback
  109. )
  110. })
  111. return it('should return an error to the callback', function () {
  112. return this.callback.calledWith(this.error).should.equal(true)
  113. })
  114. })
  115. ;[404, 422].forEach((statusCode) =>
  116. describe(`when the document updater returns a ${statusCode} status code`, function () {
  117. beforeEach(function () {
  118. this.request.get = sinon
  119. .stub()
  120. .callsArgWith(1, null, { statusCode }, '')
  121. return this.DocumentUpdaterManager.getDocument(
  122. this.project_id,
  123. this.doc_id,
  124. this.fromVersion,
  125. this.callback
  126. )
  127. })
  128. return it('should return the callback with an error', function () {
  129. this.callback.called.should.equal(true)
  130. const err = this.callback.getCall(0).args[0]
  131. err.should.have.property('statusCode', statusCode)
  132. err.should.have.property(
  133. 'message',
  134. 'doc updater could not load requested ops'
  135. )
  136. this.logger.error.called.should.equal(false)
  137. return this.logger.warn.called.should.equal(true)
  138. })
  139. })
  140. )
  141. return describe('when the document updater returns a failure error code', function () {
  142. beforeEach(function () {
  143. this.request.get = sinon
  144. .stub()
  145. .callsArgWith(1, null, { statusCode: 500 }, '')
  146. return this.DocumentUpdaterManager.getDocument(
  147. this.project_id,
  148. this.doc_id,
  149. this.fromVersion,
  150. this.callback
  151. )
  152. })
  153. return it('should return the callback with an error', function () {
  154. this.callback.called.should.equal(true)
  155. const err = this.callback.getCall(0).args[0]
  156. err.should.have.property('statusCode', 500)
  157. err.should.have.property(
  158. 'message',
  159. 'doc updater returned a non-success status code: 500'
  160. )
  161. return this.logger.error.called.should.equal(true)
  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. const err = this.callback.getCall(0).args[0]
  219. err.should.have.property('statusCode', 500)
  220. return err.should.have.property(
  221. 'message',
  222. 'document updater returned a failure status code: 500'
  223. )
  224. })
  225. })
  226. })
  227. return describe('queueChange', function () {
  228. beforeEach(function () {
  229. this.change = {
  230. doc: '1234567890',
  231. op: [{ d: 'test', p: 345 }],
  232. v: 789
  233. }
  234. this.rclient.rpush = sinon.stub().yields()
  235. return (this.callback = sinon.stub())
  236. })
  237. describe('successfully', function () {
  238. beforeEach(function () {
  239. return this.DocumentUpdaterManager.queueChange(
  240. this.project_id,
  241. this.doc_id,
  242. this.change,
  243. this.callback
  244. )
  245. })
  246. it('should push the change', function () {
  247. return this.rclient.rpush
  248. .calledWith(
  249. `PendingUpdates:${this.doc_id}`,
  250. JSON.stringify(this.change)
  251. )
  252. .should.equal(true)
  253. })
  254. return it('should notify the doc updater of the change via the pending-updates-list queue', function () {
  255. return this.rclient.rpush
  256. .calledWith(
  257. 'pending-updates-list',
  258. `${this.project_id}:${this.doc_id}`
  259. )
  260. .should.equal(true)
  261. })
  262. })
  263. describe('with error talking to redis during rpush', function () {
  264. beforeEach(function () {
  265. this.rclient.rpush = sinon
  266. .stub()
  267. .yields(new Error('something went wrong'))
  268. return this.DocumentUpdaterManager.queueChange(
  269. this.project_id,
  270. this.doc_id,
  271. this.change,
  272. this.callback
  273. )
  274. })
  275. return it('should return an error', function () {
  276. return this.callback
  277. .calledWithExactly(sinon.match(Error))
  278. .should.equal(true)
  279. })
  280. })
  281. describe('with null byte corruption', function () {
  282. beforeEach(function () {
  283. this.JSON.stringify = () => '["bad bytes! \u0000 <- here"]'
  284. return this.DocumentUpdaterManager.queueChange(
  285. this.project_id,
  286. this.doc_id,
  287. this.change,
  288. this.callback
  289. )
  290. })
  291. it('should return an error', function () {
  292. return this.callback
  293. .calledWithExactly(sinon.match(Error))
  294. .should.equal(true)
  295. })
  296. return it('should not push the change onto the pending-updates-list queue', function () {
  297. return this.rclient.rpush.called.should.equal(false)
  298. })
  299. })
  300. describe('when the update is too large', function () {
  301. beforeEach(function () {
  302. this.change = {
  303. op: { p: 12, t: 'update is too large'.repeat(1024 * 400) }
  304. }
  305. return this.DocumentUpdaterManager.queueChange(
  306. this.project_id,
  307. this.doc_id,
  308. this.change,
  309. this.callback
  310. )
  311. })
  312. it('should return an error', function () {
  313. return this.callback
  314. .calledWithExactly(sinon.match(Error))
  315. .should.equal(true)
  316. })
  317. it('should add the size to the error', function () {
  318. return this.callback.args[0][0].updateSize.should.equal(7782422)
  319. })
  320. return it('should not push the change onto the pending-updates-list queue', function () {
  321. return this.rclient.rpush.called.should.equal(false)
  322. })
  323. })
  324. return describe('with invalid keys', function () {
  325. beforeEach(function () {
  326. this.change = {
  327. op: [{ d: 'test', p: 345 }],
  328. version: 789 // not a valid key
  329. }
  330. return this.DocumentUpdaterManager.queueChange(
  331. this.project_id,
  332. this.doc_id,
  333. this.change,
  334. this.callback
  335. )
  336. })
  337. return it('should remove the invalid keys from the change', function () {
  338. return this.rclient.rpush
  339. .calledWith(
  340. `PendingUpdates:${this.doc_id}`,
  341. JSON.stringify({ op: this.change.op })
  342. )
  343. .should.equal(true)
  344. })
  345. })
  346. })
  347. })