DocumentUpdaterManagerTests.js 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391
  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. this.callback
  156. .calledWith(
  157. sinon.match({
  158. message: 'doc updater returned a non-success status code',
  159. info: {
  160. action: 'getDocument',
  161. statusCode: 500
  162. }
  163. })
  164. )
  165. .should.equal(true)
  166. this.logger.error.called.should.equal(false)
  167. })
  168. })
  169. })
  170. describe('flushProjectToMongoAndDelete', function () {
  171. beforeEach(function () {
  172. return (this.callback = sinon.stub())
  173. })
  174. describe('successfully', function () {
  175. beforeEach(function () {
  176. this.request.del = sinon
  177. .stub()
  178. .callsArgWith(1, null, { statusCode: 204 }, '')
  179. return this.DocumentUpdaterManager.flushProjectToMongoAndDelete(
  180. this.project_id,
  181. this.callback
  182. )
  183. })
  184. it('should delete the project from the document updater', function () {
  185. const url = `${this.settings.apis.documentupdater.url}/project/${this.project_id}?background=true`
  186. return this.request.del.calledWith(url).should.equal(true)
  187. })
  188. return it('should call the callback with no error', function () {
  189. return this.callback.calledWith(null).should.equal(true)
  190. })
  191. })
  192. describe('when the document updater API returns an error', function () {
  193. beforeEach(function () {
  194. this.request.del = sinon
  195. .stub()
  196. .callsArgWith(
  197. 1,
  198. (this.error = new Error('something went wrong')),
  199. null,
  200. null
  201. )
  202. return this.DocumentUpdaterManager.flushProjectToMongoAndDelete(
  203. this.project_id,
  204. this.callback
  205. )
  206. })
  207. return it('should return an error to the callback', function () {
  208. return this.callback.calledWith(this.error).should.equal(true)
  209. })
  210. })
  211. return describe('when the document updater returns a failure error code', function () {
  212. beforeEach(function () {
  213. this.request.del = sinon
  214. .stub()
  215. .callsArgWith(1, null, { statusCode: 500 }, '')
  216. return this.DocumentUpdaterManager.flushProjectToMongoAndDelete(
  217. this.project_id,
  218. this.callback
  219. )
  220. })
  221. return it('should return the callback with an error', function () {
  222. this.callback.called.should.equal(true)
  223. this.callback
  224. .calledWith(
  225. sinon.match({
  226. message: 'doc updater returned a non-success status code',
  227. info: {
  228. action: 'flushProjectToMongoAndDelete',
  229. statusCode: 500
  230. }
  231. })
  232. )
  233. .should.equal(true)
  234. })
  235. })
  236. })
  237. return describe('queueChange', function () {
  238. beforeEach(function () {
  239. this.change = {
  240. doc: '1234567890',
  241. op: [{ d: 'test', p: 345 }],
  242. v: 789
  243. }
  244. this.rclient.rpush = sinon.stub().yields()
  245. return (this.callback = sinon.stub())
  246. })
  247. describe('successfully', function () {
  248. beforeEach(function () {
  249. return this.DocumentUpdaterManager.queueChange(
  250. this.project_id,
  251. this.doc_id,
  252. this.change,
  253. this.callback
  254. )
  255. })
  256. it('should push the change', function () {
  257. return this.rclient.rpush
  258. .calledWith(
  259. `PendingUpdates:${this.doc_id}`,
  260. JSON.stringify(this.change)
  261. )
  262. .should.equal(true)
  263. })
  264. return it('should notify the doc updater of the change via the pending-updates-list queue', function () {
  265. return this.rclient.rpush
  266. .calledWith(
  267. 'pending-updates-list',
  268. `${this.project_id}:${this.doc_id}`
  269. )
  270. .should.equal(true)
  271. })
  272. })
  273. describe('with error talking to redis during rpush', function () {
  274. beforeEach(function () {
  275. this.rclient.rpush = sinon
  276. .stub()
  277. .yields(new Error('something went wrong'))
  278. return this.DocumentUpdaterManager.queueChange(
  279. this.project_id,
  280. this.doc_id,
  281. this.change,
  282. this.callback
  283. )
  284. })
  285. return it('should return an error', function () {
  286. return this.callback
  287. .calledWithExactly(sinon.match(Error))
  288. .should.equal(true)
  289. })
  290. })
  291. describe('with null byte corruption', function () {
  292. beforeEach(function () {
  293. this.JSON.stringify = () => '["bad bytes! \u0000 <- here"]'
  294. return this.DocumentUpdaterManager.queueChange(
  295. this.project_id,
  296. this.doc_id,
  297. this.change,
  298. this.callback
  299. )
  300. })
  301. it('should return an error', function () {
  302. return this.callback
  303. .calledWithExactly(sinon.match(Error))
  304. .should.equal(true)
  305. })
  306. return it('should not push the change onto the pending-updates-list queue', function () {
  307. return this.rclient.rpush.called.should.equal(false)
  308. })
  309. })
  310. describe('when the update is too large', function () {
  311. beforeEach(function () {
  312. this.change = {
  313. op: { p: 12, t: 'update is too large'.repeat(1024 * 400) }
  314. }
  315. return this.DocumentUpdaterManager.queueChange(
  316. this.project_id,
  317. this.doc_id,
  318. this.change,
  319. this.callback
  320. )
  321. })
  322. it('should return an error', function () {
  323. return this.callback
  324. .calledWithExactly(sinon.match(Error))
  325. .should.equal(true)
  326. })
  327. it('should add the size to the error', function () {
  328. return this.callback.args[0][0].info.updateSize.should.equal(7782422)
  329. })
  330. return it('should not push the change onto the pending-updates-list queue', function () {
  331. return this.rclient.rpush.called.should.equal(false)
  332. })
  333. })
  334. return describe('with invalid keys', function () {
  335. beforeEach(function () {
  336. this.change = {
  337. op: [{ d: 'test', p: 345 }],
  338. version: 789 // not a valid key
  339. }
  340. return this.DocumentUpdaterManager.queueChange(
  341. this.project_id,
  342. this.doc_id,
  343. this.change,
  344. this.callback
  345. )
  346. })
  347. return it('should remove the invalid keys from the change', function () {
  348. return this.rclient.rpush
  349. .calledWith(
  350. `PendingUpdates:${this.doc_id}`,
  351. JSON.stringify({ op: this.change.op })
  352. )
  353. .should.equal(true)
  354. })
  355. })
  356. })
  357. })