SettingADocumentTests.js 26 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919
  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. })