SettingADocumentTests.js 29 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004
  1. const sinon = require('sinon')
  2. const { expect } = require('chai')
  3. const { setTimeout } = require('node:timers/promises')
  4. const Settings = require('@overleaf/settings')
  5. const docUpdaterRedis = require('@overleaf/redis-wrapper').createClient(
  6. Settings.redis.documentupdater
  7. )
  8. const Keys = Settings.redis.documentupdater.key_schema
  9. const MockProjectHistoryApi = require('./helpers/MockProjectHistoryApi')
  10. const MockWebApi = require('./helpers/MockWebApi')
  11. const DocUpdaterClient = require('./helpers/DocUpdaterClient')
  12. const DocUpdaterApp = require('./helpers/DocUpdaterApp')
  13. const { RequestFailedError } = require('@overleaf/fetch-utils')
  14. describe('Setting a document', function () {
  15. let numberOfReceivedUpdates = 0
  16. before(async function () {
  17. DocUpdaterClient.subscribeToAppliedOps(() => {
  18. numberOfReceivedUpdates++
  19. })
  20. this.lines = ['one', 'two', 'three']
  21. this.version = 42
  22. this.update = {
  23. doc: this.doc_id,
  24. op: [
  25. {
  26. i: 'one and a half\n',
  27. p: 4,
  28. },
  29. ],
  30. v: this.version,
  31. }
  32. this.result = ['one', 'one and a half', 'two', 'three']
  33. this.newLines = ['these', 'are', 'the', 'new', 'lines']
  34. this.source = 'dropbox'
  35. this.user_id = 'user-id-123'
  36. sinon.spy(MockProjectHistoryApi, 'flushProject')
  37. sinon.spy(MockWebApi, 'setDocument')
  38. await DocUpdaterApp.ensureRunning()
  39. })
  40. after(function () {
  41. MockProjectHistoryApi.flushProject.restore()
  42. MockWebApi.setDocument.restore()
  43. })
  44. describe('when the updated doc exists in the doc updater', function () {
  45. before(async function () {
  46. numberOfReceivedUpdates = 0
  47. this.project_id = DocUpdaterClient.randomId()
  48. this.doc_id = DocUpdaterClient.randomId()
  49. MockWebApi.insertDoc(this.project_id, this.doc_id, {
  50. lines: this.lines,
  51. version: this.version,
  52. })
  53. await DocUpdaterClient.preloadDoc(this.project_id, this.doc_id)
  54. await DocUpdaterClient.sendUpdate(
  55. this.project_id,
  56. this.doc_id,
  57. this.update
  58. )
  59. await setTimeout(200)
  60. this.body = await DocUpdaterClient.setDocLines(
  61. this.project_id,
  62. this.doc_id,
  63. this.newLines,
  64. this.source,
  65. this.user_id,
  66. false
  67. )
  68. })
  69. after(function () {
  70. MockProjectHistoryApi.flushProject.resetHistory()
  71. MockWebApi.setDocument.resetHistory()
  72. })
  73. it('should emit two updates (from sendUpdate and setDocLines)', function () {
  74. expect(numberOfReceivedUpdates).to.equal(2)
  75. })
  76. it('should send the updated doc lines and version to the web api', function () {
  77. MockWebApi.setDocument
  78. .calledWith(this.project_id, this.doc_id, this.newLines)
  79. .should.equal(true)
  80. })
  81. it('should update the lines in the doc updater', async function () {
  82. const doc = await DocUpdaterClient.getDoc(this.project_id, this.doc_id)
  83. doc.lines.should.deep.equal(this.newLines)
  84. })
  85. it('should bump the version in the doc updater', async function () {
  86. const doc = await DocUpdaterClient.getDoc(this.project_id, this.doc_id)
  87. doc.version.should.equal(this.version + 2)
  88. })
  89. it('should leave the document in redis', function (done) {
  90. docUpdaterRedis.get(
  91. Keys.docLines({ doc_id: this.doc_id }),
  92. (error, lines) => {
  93. if (error) {
  94. throw error
  95. }
  96. expect(JSON.parse(lines)).to.deep.equal(this.newLines)
  97. done()
  98. }
  99. )
  100. })
  101. it('should return the mongo rev in the json response', function () {
  102. this.body.should.deep.equal({ rev: '123' })
  103. })
  104. describe('when doc has the same contents', function () {
  105. beforeEach(async function () {
  106. numberOfReceivedUpdates = 0
  107. await DocUpdaterClient.setDocLines(
  108. this.project_id,
  109. this.doc_id,
  110. this.newLines,
  111. this.source,
  112. this.user_id,
  113. false
  114. )
  115. })
  116. it('should not bump the version in doc updater', async function () {
  117. const doc = await DocUpdaterClient.getDoc(this.project_id, this.doc_id)
  118. doc.version.should.equal(this.version + 2)
  119. })
  120. it('should not emit any updates', async function () {
  121. // delay by 100ms: make sure we do not check too early!
  122. await setTimeout(100)
  123. expect(numberOfReceivedUpdates).to.equal(0)
  124. })
  125. })
  126. })
  127. describe('when the updated doc exists in the doc updater (history-ot)', function () {
  128. before(async function () {
  129. numberOfReceivedUpdates = 0
  130. this.project_id = DocUpdaterClient.randomId()
  131. this.doc_id = DocUpdaterClient.randomId()
  132. this.historyOTUpdate = {
  133. doc: this.doc_id,
  134. op: [{ textOperation: [4, 'one and a half\n', 9] }],
  135. v: this.version,
  136. meta: { source: 'random-publicId' },
  137. }
  138. MockWebApi.insertDoc(this.project_id, this.doc_id, {
  139. lines: this.lines,
  140. version: this.version,
  141. otMigrationStage: 1,
  142. })
  143. await DocUpdaterClient.preloadDoc(this.project_id, this.doc_id)
  144. await DocUpdaterClient.sendUpdate(
  145. this.project_id,
  146. this.doc_id,
  147. this.historyOTUpdate
  148. )
  149. await setTimeout(200)
  150. this.body = await DocUpdaterClient.setDocLines(
  151. this.project_id,
  152. this.doc_id,
  153. this.newLines,
  154. this.source,
  155. this.user_id,
  156. false
  157. )
  158. })
  159. after(function () {
  160. MockProjectHistoryApi.flushProject.resetHistory()
  161. MockWebApi.setDocument.resetHistory()
  162. })
  163. it('should emit two updates (from sendUpdate and setDocLines)', function () {
  164. expect(numberOfReceivedUpdates).to.equal(2)
  165. })
  166. it('should send the updated doc lines and version to the web api', function () {
  167. MockWebApi.setDocument
  168. .calledWith(this.project_id, this.doc_id, this.newLines)
  169. .should.equal(true)
  170. })
  171. it('should update the lines in the doc updater', async function () {
  172. const doc = await DocUpdaterClient.getDoc(this.project_id, this.doc_id)
  173. doc.lines.should.deep.equal(this.newLines)
  174. })
  175. it('should bump the version in the doc updater', async function () {
  176. const doc = await DocUpdaterClient.getDoc(this.project_id, this.doc_id)
  177. doc.version.should.equal(this.version + 2)
  178. })
  179. it('should leave the document in redis', function (done) {
  180. docUpdaterRedis.get(
  181. Keys.docLines({ doc_id: this.doc_id }),
  182. (error, lines) => {
  183. if (error) {
  184. throw error
  185. }
  186. expect(JSON.parse(lines)).to.deep.equal({
  187. content: this.newLines.join('\n'),
  188. })
  189. done()
  190. }
  191. )
  192. })
  193. it('should return the mongo rev in the json response', function () {
  194. this.body.should.deep.equal({ rev: '123' })
  195. })
  196. describe('when doc has the same contents', function () {
  197. beforeEach(async function () {
  198. numberOfReceivedUpdates = 0
  199. this.body = await DocUpdaterClient.setDocLines(
  200. this.project_id,
  201. this.doc_id,
  202. this.newLines,
  203. this.source,
  204. this.user_id,
  205. false
  206. )
  207. })
  208. it('should not bump the version in doc updater', async function () {
  209. const doc = await DocUpdaterClient.getDoc(this.project_id, this.doc_id)
  210. doc.version.should.equal(this.version + 2)
  211. })
  212. it('should not emit any updates', async function () {
  213. // delay by 100ms: make sure we do not check too early!
  214. await setTimeout(100)
  215. expect(numberOfReceivedUpdates).to.equal(0)
  216. })
  217. })
  218. })
  219. describe('when the updated doc does not exist in the doc updater', function () {
  220. before(async function () {
  221. this.project_id = DocUpdaterClient.randomId()
  222. this.doc_id = DocUpdaterClient.randomId()
  223. numberOfReceivedUpdates = 0
  224. MockWebApi.insertDoc(this.project_id, this.doc_id, {
  225. lines: this.lines,
  226. version: this.version,
  227. })
  228. this.body = await DocUpdaterClient.setDocLines(
  229. this.project_id,
  230. this.doc_id,
  231. this.newLines,
  232. this.source,
  233. this.user_id,
  234. false
  235. )
  236. await setTimeout(200)
  237. })
  238. after(function () {
  239. MockProjectHistoryApi.flushProject.resetHistory()
  240. MockWebApi.setDocument.resetHistory()
  241. })
  242. it('should emit an update', function () {
  243. expect(numberOfReceivedUpdates).to.equal(1)
  244. })
  245. it('should send the updated doc lines to the web api', function () {
  246. MockWebApi.setDocument
  247. .calledWith(this.project_id, this.doc_id, this.newLines)
  248. .should.equal(true)
  249. })
  250. it('should flush project history', function () {
  251. MockProjectHistoryApi.flushProject
  252. .calledWith(this.project_id)
  253. .should.equal(true)
  254. })
  255. it('should remove the document from redis', function (done) {
  256. docUpdaterRedis.get(
  257. Keys.docLines({ doc_id: this.doc_id }),
  258. (error, lines) => {
  259. if (error) {
  260. throw error
  261. }
  262. expect(lines).to.not.exist
  263. done()
  264. }
  265. )
  266. })
  267. it('should return the mongo rev in the json response', function () {
  268. this.body.should.deep.equal({ rev: '123' })
  269. })
  270. })
  271. const DOC_TOO_LARGE_TEST_CASES = [
  272. {
  273. desc: 'when the updated doc is too large for the body parser',
  274. size: Settings.maxJsonRequestSize,
  275. expectedStatusCode: 413,
  276. },
  277. {
  278. desc: 'when the updated doc is larger than the HTTP controller limit',
  279. size: Settings.max_doc_length,
  280. expectedStatusCode: 406,
  281. },
  282. ]
  283. DOC_TOO_LARGE_TEST_CASES.forEach(testCase => {
  284. describe(testCase.desc, function () {
  285. before(async function () {
  286. this.project_id = DocUpdaterClient.randomId()
  287. this.doc_id = DocUpdaterClient.randomId()
  288. MockWebApi.insertDoc(this.project_id, this.doc_id, {
  289. lines: this.lines,
  290. version: this.version,
  291. })
  292. this.newLines = []
  293. while (JSON.stringify(this.newLines).length <= testCase.size) {
  294. this.newLines.push('(a long line of text)'.repeat(10000))
  295. }
  296. try {
  297. await DocUpdaterClient.setDocLines(
  298. this.project_id,
  299. this.doc_id,
  300. this.newLines,
  301. this.source,
  302. this.user_id,
  303. false
  304. )
  305. this.statusCode = 200
  306. } catch (err) {
  307. if (err instanceof RequestFailedError) {
  308. this.statusCode = err.response.status
  309. } else {
  310. throw err
  311. }
  312. }
  313. await setTimeout(200)
  314. })
  315. after(function () {
  316. MockProjectHistoryApi.flushProject.resetHistory()
  317. MockWebApi.setDocument.resetHistory()
  318. })
  319. it(`should return a ${testCase.expectedStatusCode} status code`, function () {
  320. this.statusCode.should.equal(testCase.expectedStatusCode)
  321. })
  322. it('should not send the updated doc lines to the web api', function () {
  323. MockWebApi.setDocument.called.should.equal(false)
  324. })
  325. it('should not flush project history', function () {
  326. MockProjectHistoryApi.flushProject.called.should.equal(false)
  327. })
  328. })
  329. })
  330. describe('when the updated doc is large but under the bodyParser and HTTPController size limit', function () {
  331. before(async function () {
  332. this.project_id = DocUpdaterClient.randomId()
  333. this.doc_id = DocUpdaterClient.randomId()
  334. MockWebApi.insertDoc(this.project_id, this.doc_id, {
  335. lines: this.lines,
  336. version: this.version,
  337. })
  338. this.newLines = []
  339. while (JSON.stringify(this.newLines).length < 2 * 1024 * 1024) {
  340. // limit in HTTPController
  341. this.newLines.push('(a long line of text)'.repeat(10000))
  342. }
  343. this.newLines.pop() // remove the line which took it over the limit
  344. this.body = await DocUpdaterClient.setDocLines(
  345. this.project_id,
  346. this.doc_id,
  347. this.newLines,
  348. this.source,
  349. this.user_id,
  350. false
  351. )
  352. await setTimeout(200)
  353. })
  354. after(function () {
  355. MockProjectHistoryApi.flushProject.resetHistory()
  356. MockWebApi.setDocument.resetHistory()
  357. })
  358. it('should send the updated doc lines to the web api', function () {
  359. MockWebApi.setDocument
  360. .calledWith(this.project_id, this.doc_id, this.newLines)
  361. .should.equal(true)
  362. })
  363. it('should return the mongo rev in the json response', function () {
  364. this.body.should.deep.equal({ rev: '123' })
  365. })
  366. })
  367. describe('with track changes', function () {
  368. before(function () {
  369. this.lines = ['one', 'one and a half', 'two', 'three']
  370. this.id_seed = '587357bd35e64f6157'
  371. this.update = {
  372. doc: this.doc_id,
  373. op: [
  374. {
  375. d: 'one and a half\n',
  376. p: 4,
  377. },
  378. ],
  379. meta: {
  380. tc: this.id_seed,
  381. user_id: this.user_id,
  382. },
  383. v: this.version,
  384. }
  385. })
  386. describe('with the undo flag', function () {
  387. before(async function () {
  388. this.project_id = DocUpdaterClient.randomId()
  389. this.doc_id = DocUpdaterClient.randomId()
  390. MockWebApi.insertDoc(this.project_id, this.doc_id, {
  391. lines: this.lines,
  392. version: this.version,
  393. })
  394. await DocUpdaterClient.preloadDoc(this.project_id, this.doc_id)
  395. await DocUpdaterClient.sendUpdate(
  396. this.project_id,
  397. this.doc_id,
  398. this.update
  399. )
  400. // Go back to old lines, with undo flag
  401. await DocUpdaterClient.setDocLines(
  402. this.project_id,
  403. this.doc_id,
  404. this.lines,
  405. this.source,
  406. this.user_id,
  407. true
  408. )
  409. await setTimeout(200)
  410. })
  411. after(function () {
  412. MockProjectHistoryApi.flushProject.resetHistory()
  413. MockWebApi.setDocument.resetHistory()
  414. })
  415. it('should undo the tracked changes', async function () {
  416. const doc = await DocUpdaterClient.getDoc(this.project_id, this.doc_id)
  417. expect(doc.ranges.changes).to.be.undefined
  418. })
  419. })
  420. describe('without the undo flag', function () {
  421. before(async function () {
  422. this.project_id = DocUpdaterClient.randomId()
  423. this.doc_id = DocUpdaterClient.randomId()
  424. MockWebApi.insertDoc(this.project_id, this.doc_id, {
  425. lines: this.lines,
  426. version: this.version,
  427. })
  428. await DocUpdaterClient.preloadDoc(this.project_id, this.doc_id)
  429. await DocUpdaterClient.sendUpdate(
  430. this.project_id,
  431. this.doc_id,
  432. this.update
  433. )
  434. // Go back to old lines, without undo flag
  435. await DocUpdaterClient.setDocLines(
  436. this.project_id,
  437. this.doc_id,
  438. this.lines,
  439. this.source,
  440. this.user_id,
  441. false
  442. )
  443. await setTimeout(200)
  444. })
  445. after(function () {
  446. MockProjectHistoryApi.flushProject.resetHistory()
  447. MockWebApi.setDocument.resetHistory()
  448. })
  449. it('should not undo the tracked changes', async function () {
  450. const doc = await DocUpdaterClient.getDoc(this.project_id, this.doc_id)
  451. expect(doc.ranges.changes.length).to.equal(1)
  452. })
  453. })
  454. })
  455. describe('with track changes (history-ot)', function () {
  456. const lines = ['one', 'one and a half', 'two', 'three']
  457. const userId = DocUpdaterClient.randomId()
  458. const ts = new Date().toISOString()
  459. beforeEach(async function () {
  460. numberOfReceivedUpdates = 0
  461. this.newLines = ['one', 'two', 'three']
  462. this.project_id = DocUpdaterClient.randomId()
  463. this.doc_id = DocUpdaterClient.randomId()
  464. this.historyOTUpdate = {
  465. doc: this.doc_id,
  466. op: [
  467. {
  468. textOperation: [
  469. 4,
  470. {
  471. r: 'one and a half\n'.length,
  472. tracking: {
  473. type: 'delete',
  474. userId,
  475. ts,
  476. },
  477. },
  478. 9,
  479. ],
  480. },
  481. ],
  482. v: this.version,
  483. meta: { source: 'random-publicId' },
  484. }
  485. MockWebApi.insertDoc(this.project_id, this.doc_id, {
  486. lines,
  487. version: this.version,
  488. otMigrationStage: 1,
  489. })
  490. await DocUpdaterClient.preloadDoc(this.project_id, this.doc_id)
  491. await DocUpdaterClient.sendUpdate(
  492. this.project_id,
  493. this.doc_id,
  494. this.historyOTUpdate
  495. )
  496. await DocUpdaterClient.waitForPendingUpdates(this.doc_id)
  497. })
  498. afterEach(function () {
  499. MockProjectHistoryApi.flushProject.resetHistory()
  500. MockWebApi.setDocument.resetHistory()
  501. })
  502. it('should record tracked changes', function (done) {
  503. docUpdaterRedis.get(
  504. Keys.docLines({ doc_id: this.doc_id }),
  505. (error, data) => {
  506. if (error) {
  507. throw error
  508. }
  509. expect(JSON.parse(data)).to.deep.equal({
  510. content: lines.join('\n'),
  511. trackedChanges: [
  512. {
  513. range: {
  514. pos: 4,
  515. length: 15,
  516. },
  517. tracking: {
  518. ts,
  519. type: 'delete',
  520. userId,
  521. },
  522. },
  523. ],
  524. })
  525. done()
  526. }
  527. )
  528. })
  529. it('should apply the change', async function () {
  530. const doc = await DocUpdaterClient.getDoc(this.project_id, this.doc_id)
  531. expect(doc.lines).to.deep.equal(this.newLines)
  532. })
  533. const cases = [
  534. {
  535. name: 'when resetting the content',
  536. lines,
  537. want: {
  538. content: 'one\none and a half\none and a half\ntwo\nthree',
  539. trackedChanges: [
  540. {
  541. range: {
  542. pos: 'one and a half\n'.length + 4,
  543. length: 15,
  544. },
  545. tracking: {
  546. ts,
  547. type: 'delete',
  548. userId,
  549. },
  550. },
  551. ],
  552. },
  553. },
  554. {
  555. name: 'when adding content before a tracked delete',
  556. lines: ['one', 'INSERT', 'two', 'three'],
  557. want: {
  558. content: 'one\nINSERT\none and a half\ntwo\nthree',
  559. trackedChanges: [
  560. {
  561. range: {
  562. pos: 'INSERT\n'.length + 4,
  563. length: 15,
  564. },
  565. tracking: {
  566. ts,
  567. type: 'delete',
  568. userId,
  569. },
  570. },
  571. ],
  572. },
  573. },
  574. {
  575. name: 'when adding content after a tracked delete',
  576. lines: ['one', 'two', 'INSERT', 'three'],
  577. want: {
  578. content: 'one\none and a half\ntwo\nINSERT\nthree',
  579. trackedChanges: [
  580. {
  581. range: {
  582. pos: 4,
  583. length: 15,
  584. },
  585. tracking: {
  586. ts,
  587. type: 'delete',
  588. userId,
  589. },
  590. },
  591. ],
  592. },
  593. },
  594. {
  595. name: 'when deleting content before a tracked delete',
  596. lines: ['two', 'three'],
  597. want: {
  598. content: 'one and a half\ntwo\nthree',
  599. trackedChanges: [
  600. {
  601. range: {
  602. pos: 0,
  603. length: 15,
  604. },
  605. tracking: {
  606. ts,
  607. type: 'delete',
  608. userId,
  609. },
  610. },
  611. ],
  612. },
  613. },
  614. {
  615. name: 'when deleting content after a tracked delete',
  616. lines: ['one', 'two'],
  617. want: {
  618. content: 'one\none and a half\ntwo',
  619. trackedChanges: [
  620. {
  621. range: {
  622. pos: 4,
  623. length: 15,
  624. },
  625. tracking: {
  626. ts,
  627. type: 'delete',
  628. userId,
  629. },
  630. },
  631. ],
  632. },
  633. },
  634. {
  635. name: 'when deleting content immediately after a tracked delete',
  636. lines: ['one', 'three'],
  637. want: {
  638. content: 'one\none and a half\nthree',
  639. trackedChanges: [
  640. {
  641. range: {
  642. pos: 4,
  643. length: 15,
  644. },
  645. tracking: {
  646. ts,
  647. type: 'delete',
  648. userId,
  649. },
  650. },
  651. ],
  652. },
  653. },
  654. {
  655. name: 'when deleting content across a tracked delete',
  656. lines: ['onethree'],
  657. want: {
  658. content: 'oneone and a half\nthree',
  659. trackedChanges: [
  660. {
  661. range: {
  662. pos: 3,
  663. length: 15,
  664. },
  665. tracking: {
  666. ts,
  667. type: 'delete',
  668. userId,
  669. },
  670. },
  671. ],
  672. },
  673. },
  674. ]
  675. for (const { name, lines, want } of cases) {
  676. describe(name, function () {
  677. beforeEach(async function () {
  678. this.body = await DocUpdaterClient.setDocLines(
  679. this.project_id,
  680. this.doc_id,
  681. lines,
  682. this.source,
  683. userId,
  684. false
  685. )
  686. })
  687. it('should update accordingly', function (done) {
  688. docUpdaterRedis.get(
  689. Keys.docLines({ doc_id: this.doc_id }),
  690. (error, data) => {
  691. if (error) {
  692. throw error
  693. }
  694. expect(JSON.parse(data)).to.deep.equal(want)
  695. done()
  696. }
  697. )
  698. })
  699. })
  700. }
  701. })
  702. describe('when the first request returns a connection error', function () {
  703. beforeEach(function () {
  704. const origSetDocumentController =
  705. MockWebApi.setDocumentController.bind(MockWebApi)
  706. const setDocumentStub = sinon
  707. .stub(MockWebApi, 'setDocumentController')
  708. .onCall(0)
  709. .callsFake((req, res, next) => {
  710. res.destroy() // simulate a network error
  711. })
  712. setDocumentStub.onCall(1).callsFake(origSetDocumentController)
  713. })
  714. afterEach(function () {
  715. MockWebApi.setDocumentController.restore()
  716. })
  717. it('should retry on connection error and set the document', async function () {
  718. this.project_id = DocUpdaterClient.randomId()
  719. this.doc_id = DocUpdaterClient.randomId()
  720. MockWebApi.insertDoc(this.project_id, this.doc_id, {
  721. lines: this.lines,
  722. version: this.version,
  723. projectHistoryId: this.project_id,
  724. })
  725. await DocUpdaterClient.preloadDoc(this.project_id, this.doc_id)
  726. await expect(
  727. DocUpdaterClient.setDocLines(
  728. this.project_id,
  729. this.doc_id,
  730. this.newLines,
  731. this.source,
  732. this.user_id,
  733. false
  734. )
  735. ).to.eventually.deep.include({ rev: '123' })
  736. expect(MockWebApi.setDocumentController).to.be.calledTwice
  737. })
  738. })
  739. describe('when the document does not exist', function () {
  740. before(function () {
  741. sinon.spy(MockWebApi, 'setDocumentController')
  742. })
  743. after(function () {
  744. MockWebApi.setDocumentController.restore()
  745. })
  746. it('should return 404', async function () {
  747. this.project_id = DocUpdaterClient.randomId()
  748. this.doc_id = DocUpdaterClient.randomId()
  749. MockWebApi.insertDoc(this.project_id, this.doc_id, {
  750. lines: this.lines,
  751. version: this.version,
  752. projectHistoryId: this.project_id,
  753. })
  754. await DocUpdaterClient.preloadDoc(this.project_id, this.doc_id)
  755. MockWebApi.clearDocs()
  756. await expect(
  757. DocUpdaterClient.setDocLines(
  758. this.project_id,
  759. this.doc_id,
  760. this.newLines,
  761. this.source,
  762. this.user_id,
  763. false
  764. )
  765. )
  766. .to.be.rejectedWith(RequestFailedError)
  767. .and.eventually.have.nested.property('response.status', 404)
  768. expect(MockWebApi.setDocumentController).to.be.calledOnce
  769. })
  770. })
  771. describe('when the document is too large', function () {
  772. beforeEach(function () {
  773. sinon
  774. .stub(MockWebApi, 'setDocumentController')
  775. .callsFake((req, res, next) => {
  776. res.sendStatus(413) // simulate a large file error
  777. })
  778. })
  779. afterEach(function () {
  780. MockWebApi.setDocumentController.restore()
  781. })
  782. it('should return 413', async function () {
  783. this.project_id = DocUpdaterClient.randomId()
  784. this.doc_id = DocUpdaterClient.randomId()
  785. MockWebApi.insertDoc(this.project_id, this.doc_id, {
  786. lines: this.lines,
  787. version: this.version,
  788. projectHistoryId: this.project_id,
  789. })
  790. await DocUpdaterClient.preloadDoc(this.project_id, this.doc_id)
  791. MockWebApi.clearDocs()
  792. await expect(
  793. DocUpdaterClient.setDocLines(
  794. this.project_id,
  795. this.doc_id,
  796. this.newLines,
  797. this.source,
  798. this.user_id,
  799. false
  800. )
  801. )
  802. .to.be.rejectedWith(RequestFailedError)
  803. .and.eventually.have.nested.property('response.status', 413)
  804. expect(MockWebApi.setDocumentController).to.be.calledOnce
  805. })
  806. })
  807. describe('when the first request returns a 500 error', function () {
  808. beforeEach(function () {
  809. const origSetDocumentController =
  810. MockWebApi.setDocumentController.bind(MockWebApi)
  811. const setDocumentStub = sinon
  812. .stub(MockWebApi, 'setDocumentController')
  813. .onCall(0)
  814. .callsFake((req, res, next) => {
  815. res.sendStatus(500)
  816. })
  817. setDocumentStub.onCall(1).callsFake(origSetDocumentController)
  818. })
  819. afterEach(function () {
  820. MockWebApi.setDocumentController.restore()
  821. })
  822. it('should retry on a 500 error and set the document', async function () {
  823. this.project_id = DocUpdaterClient.randomId()
  824. this.doc_id = DocUpdaterClient.randomId()
  825. MockWebApi.insertDoc(this.project_id, this.doc_id, {
  826. lines: this.lines,
  827. version: this.version,
  828. projectHistoryId: this.project_id,
  829. })
  830. await DocUpdaterClient.preloadDoc(this.project_id, this.doc_id)
  831. await expect(
  832. DocUpdaterClient.setDocLines(
  833. this.project_id,
  834. this.doc_id,
  835. this.newLines,
  836. this.source,
  837. this.user_id,
  838. false
  839. )
  840. ).to.eventually.deep.include({ rev: '123' })
  841. expect(MockWebApi.setDocumentController).to.be.calledTwice
  842. })
  843. })
  844. describe('when the web api http request times out on the first request', function () {
  845. beforeEach(async function () {
  846. this.project_id = DocUpdaterClient.randomId()
  847. this.doc_id = DocUpdaterClient.randomId()
  848. MockWebApi.insertDoc(this.project_id, this.doc_id, {
  849. lines: this.lines,
  850. version: this.version,
  851. projectHistoryId: this.project_id,
  852. })
  853. await DocUpdaterClient.preloadDoc(this.project_id, this.doc_id)
  854. const origSetDocumentController =
  855. MockWebApi.setDocumentController.bind(MockWebApi)
  856. const setDocumentStub = sinon
  857. .stub(MockWebApi, 'setDocumentController')
  858. .onFirstCall()
  859. .callsFake(async (req, res, next) => {
  860. await setTimeout(30_000)
  861. })
  862. setDocumentStub.onCall(1).callsFake(origSetDocumentController)
  863. })
  864. afterEach(function () {
  865. MockWebApi.setDocumentController.restore()
  866. })
  867. it('should retry the request and return the document', async function () {
  868. this.timeout(10000)
  869. const returnedDoc = await DocUpdaterClient.setDocLines(
  870. this.project_id,
  871. this.doc_id,
  872. this.newLines,
  873. this.source,
  874. this.user_id,
  875. false
  876. )
  877. expect(returnedDoc).to.deep.include({ rev: '123' })
  878. expect(MockWebApi.setDocumentController).to.be.calledTwice
  879. })
  880. })
  881. describe('when the web api http request times out repeatedly', function () {
  882. beforeEach(async function () {
  883. this.project_id = DocUpdaterClient.randomId()
  884. this.doc_id = DocUpdaterClient.randomId()
  885. MockWebApi.insertDoc(this.project_id, this.doc_id, {
  886. lines: this.lines,
  887. version: this.version,
  888. projectHistoryId: this.project_id,
  889. })
  890. await DocUpdaterClient.preloadDoc(this.project_id, this.doc_id)
  891. sinon
  892. .stub(MockWebApi, 'setDocumentController')
  893. .callsFake(async (req, res, next) => {
  894. await setTimeout(30_000)
  895. })
  896. })
  897. afterEach(function () {
  898. MockWebApi.setDocumentController.restore()
  899. })
  900. it('should return an error after two attempts', async function () {
  901. this.timeout(15000)
  902. const start = Date.now()
  903. await expect(
  904. DocUpdaterClient.setDocLines(
  905. this.project_id,
  906. this.doc_id,
  907. this.newLines,
  908. this.source,
  909. this.user_id,
  910. false
  911. )
  912. ).to.be.rejectedWith('request failed')
  913. const delta = Date.now() - start
  914. expect(delta).to.be.above(10_000) // 2 * 5000ms timeout
  915. expect(delta).to.be.below(20_000)
  916. expect(MockWebApi.setDocumentController).to.be.calledTwice
  917. })
  918. })
  919. })