DocManagerTests.js 24 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794
  1. const SandboxedModule = require('sandboxed-module')
  2. const sinon = require('sinon')
  3. const { expect } = require('chai')
  4. const modulePath = require('path').join(__dirname, '../../../app/js/DocManager')
  5. const { ObjectId } = require('mongodb')
  6. const Errors = require('../../../app/js/Errors')
  7. describe('DocManager', function () {
  8. beforeEach(function () {
  9. this.doc_id = new ObjectId().toString()
  10. this.project_id = new ObjectId().toString()
  11. this.another_project_id = new ObjectId().toString()
  12. this.stubbedError = new Error('blew up')
  13. this.version = 42
  14. this.MongoManager = {
  15. promises: {
  16. findDoc: sinon.stub(),
  17. getDocVersion: sinon.stub().resolves(this.version),
  18. getProjectsDocs: sinon.stub(),
  19. patchDoc: sinon.stub().resolves(),
  20. upsertIntoDocCollection: sinon.stub().resolves(),
  21. },
  22. }
  23. this.DocArchiveManager = {
  24. promises: {
  25. unarchiveDoc: sinon.stub(),
  26. unArchiveAllDocs: sinon.stub(),
  27. archiveDoc: sinon.stub().resolves(),
  28. },
  29. }
  30. this.RangeManager = {
  31. jsonRangesToMongo(r) {
  32. return r
  33. },
  34. shouldUpdateRanges: sinon.stub().returns(false),
  35. }
  36. this.settings = { docstore: {} }
  37. this.DocManager = SandboxedModule.require(modulePath, {
  38. requires: {
  39. './MongoManager': this.MongoManager,
  40. './DocArchiveManager': this.DocArchiveManager,
  41. './RangeManager': this.RangeManager,
  42. '@overleaf/settings': this.settings,
  43. './Errors': Errors,
  44. },
  45. })
  46. })
  47. describe('getFullDoc', function () {
  48. beforeEach(function () {
  49. this.DocManager.promises._getDoc = sinon.stub()
  50. this.doc = {
  51. _id: this.doc_id,
  52. lines: ['2134'],
  53. }
  54. })
  55. it('should call get doc with a quick filter', async function () {
  56. this.DocManager.promises._getDoc.resolves(this.doc)
  57. const doc = await this.DocManager.promises.getFullDoc(
  58. this.project_id,
  59. this.doc_id
  60. )
  61. doc.should.equal(this.doc)
  62. this.DocManager.promises._getDoc
  63. .calledWith(this.project_id, this.doc_id, {
  64. lines: true,
  65. rev: true,
  66. deleted: true,
  67. version: true,
  68. ranges: true,
  69. inS3: true,
  70. })
  71. .should.equal(true)
  72. })
  73. it('should return error when get doc errors', async function () {
  74. this.DocManager.promises._getDoc.rejects(this.stubbedError)
  75. await expect(
  76. this.DocManager.promises.getFullDoc(this.project_id, this.doc_id)
  77. ).to.be.rejectedWith(this.stubbedError)
  78. })
  79. })
  80. describe('getRawDoc', function () {
  81. beforeEach(function () {
  82. this.DocManager.promises._getDoc = sinon.stub()
  83. this.doc = { lines: ['2134'] }
  84. })
  85. it('should call get doc with a quick filter', async function () {
  86. this.DocManager.promises._getDoc.resolves(this.doc)
  87. const doc = await this.DocManager.promises.getDocLines(
  88. this.project_id,
  89. this.doc_id
  90. )
  91. doc.should.equal(this.doc)
  92. this.DocManager.promises._getDoc
  93. .calledWith(this.project_id, this.doc_id, {
  94. lines: true,
  95. inS3: true,
  96. })
  97. .should.equal(true)
  98. })
  99. it('should return error when get doc errors', async function () {
  100. this.DocManager.promises._getDoc.rejects(this.stubbedError)
  101. await expect(
  102. this.DocManager.promises.getDocLines(this.project_id, this.doc_id)
  103. ).to.be.rejectedWith(this.stubbedError)
  104. })
  105. })
  106. describe('getDoc', function () {
  107. beforeEach(function () {
  108. this.project = { name: 'mock-project' }
  109. this.doc = {
  110. _id: this.doc_id,
  111. project_id: this.project_id,
  112. lines: ['mock-lines'],
  113. }
  114. })
  115. describe('when using a filter', function () {
  116. beforeEach(function () {
  117. this.MongoManager.promises.findDoc.resolves(this.doc)
  118. })
  119. it('should error if inS3 is not set to true', async function () {
  120. await expect(
  121. this.DocManager.promises._getDoc(this.project_id, this.doc_id, {
  122. inS3: false,
  123. })
  124. ).to.be.rejected
  125. })
  126. it('should always get inS3 even when no filter is passed', async function () {
  127. await expect(
  128. this.DocManager.promises._getDoc(this.project_id, this.doc_id)
  129. ).to.be.rejected
  130. this.MongoManager.promises.findDoc.called.should.equal(false)
  131. })
  132. it('should not error if inS3 is set to true', async function () {
  133. await this.DocManager.promises._getDoc(this.project_id, this.doc_id, {
  134. inS3: true,
  135. })
  136. })
  137. })
  138. describe('when the doc is in the doc collection', function () {
  139. beforeEach(async function () {
  140. this.MongoManager.promises.findDoc.resolves(this.doc)
  141. this.result = await this.DocManager.promises._getDoc(
  142. this.project_id,
  143. this.doc_id,
  144. { version: true, inS3: true }
  145. )
  146. })
  147. it('should get the doc from the doc collection', function () {
  148. this.MongoManager.promises.findDoc
  149. .calledWith(this.project_id, this.doc_id)
  150. .should.equal(true)
  151. })
  152. it('should get the doc version from the docOps collection', function () {
  153. this.MongoManager.promises.getDocVersion
  154. .calledWith(this.doc_id)
  155. .should.equal(true)
  156. })
  157. it('should return the doc with the version', function () {
  158. this.result.lines.should.equal(this.doc.lines)
  159. this.result.version.should.equal(this.version)
  160. })
  161. })
  162. describe('without the version filter', function () {
  163. beforeEach(async function () {
  164. this.MongoManager.promises.findDoc.resolves(this.doc)
  165. await this.DocManager.promises._getDoc(this.project_id, this.doc_id, {
  166. version: false,
  167. inS3: true,
  168. })
  169. })
  170. it('should not get the doc version from the docOps collection', function () {
  171. this.MongoManager.promises.getDocVersion.called.should.equal(false)
  172. })
  173. })
  174. describe('when the doc record already has a version', function () {
  175. beforeEach(async function () {
  176. this.docWithVersion = { ...this.doc, version: 109 }
  177. this.MongoManager.promises.findDoc.resolves(this.docWithVersion)
  178. this.result = await this.DocManager.promises._getDoc(
  179. this.project_id,
  180. this.doc_id,
  181. {
  182. version: true,
  183. inS3: true,
  184. }
  185. )
  186. })
  187. it('should return that version', function () {
  188. expect(this.result).to.deep.equal(this.docWithVersion)
  189. })
  190. it('should not fetch the doc version from the docOps collection', function () {
  191. expect(this.MongoManager.promises.getDocVersion).not.to.have.been.called
  192. })
  193. })
  194. describe('when MongoManager.findDoc errors', function () {
  195. it('should return the error', async function () {
  196. this.MongoManager.promises.findDoc.rejects(this.stubbedError)
  197. await expect(
  198. this.DocManager.promises._getDoc(this.project_id, this.doc_id, {
  199. version: true,
  200. inS3: true,
  201. })
  202. ).to.be.rejectedWith(this.stubbedError)
  203. })
  204. })
  205. describe('when the doc is archived', function () {
  206. beforeEach(async function () {
  207. this.doc = {
  208. _id: this.doc_id,
  209. project_id: this.project_id,
  210. inS3: true,
  211. }
  212. this.unarchivedDoc = {
  213. _id: this.doc_id,
  214. project_id: this.project_id,
  215. lines: ['mock-lines'],
  216. inS3: false,
  217. }
  218. this.MongoManager.promises.findDoc.resolves(this.doc)
  219. this.DocArchiveManager.promises.unarchiveDoc.callsFake(
  220. async (projectId, docId) => {
  221. this.MongoManager.promises.findDoc.resolves({
  222. ...this.unarchivedDoc,
  223. })
  224. }
  225. )
  226. this.result = await this.DocManager.promises._getDoc(
  227. this.project_id,
  228. this.doc_id,
  229. {
  230. version: true,
  231. inS3: true,
  232. }
  233. )
  234. })
  235. it('should call the DocArchive to unarchive the doc', function () {
  236. this.DocArchiveManager.promises.unarchiveDoc
  237. .calledWith(this.project_id, this.doc_id)
  238. .should.equal(true)
  239. })
  240. it('should look up the doc twice', function () {
  241. this.MongoManager.promises.findDoc.calledTwice.should.equal(true)
  242. })
  243. it('should return the doc', function () {
  244. expect(this.result).to.deep.equal({
  245. ...this.unarchivedDoc,
  246. version: this.version,
  247. })
  248. })
  249. })
  250. describe('when the doc does not exist in the docs collection', function () {
  251. it('should return a NotFoundError', async function () {
  252. this.MongoManager.promises.findDoc.resolves(null)
  253. await expect(
  254. this.DocManager.promises._getDoc(this.project_id, this.doc_id, {
  255. version: true,
  256. inS3: true,
  257. })
  258. ).to.be.rejectedWith(
  259. `No such doc: ${this.doc_id} in project ${this.project_id}`
  260. )
  261. })
  262. })
  263. })
  264. describe('getAllNonDeletedDocs', function () {
  265. describe('when the project exists', function () {
  266. beforeEach(async function () {
  267. this.docs = [
  268. {
  269. _id: this.doc_id,
  270. project_id: this.project_id,
  271. lines: ['mock-lines'],
  272. },
  273. ]
  274. this.MongoManager.promises.getProjectsDocs.resolves(this.docs)
  275. this.DocArchiveManager.promises.unArchiveAllDocs.resolves(this.docs)
  276. this.filter = { lines: true }
  277. this.result = await this.DocManager.promises.getAllNonDeletedDocs(
  278. this.project_id,
  279. this.filter
  280. )
  281. })
  282. it('should get the project from the database', function () {
  283. this.MongoManager.promises.getProjectsDocs.should.have.been.calledWith(
  284. this.project_id,
  285. { include_deleted: false },
  286. this.filter
  287. )
  288. })
  289. it('should return the docs', function () {
  290. expect(this.result).to.deep.equal(this.docs)
  291. })
  292. })
  293. describe('when there are no docs for the project', function () {
  294. it('should return a NotFoundError', async function () {
  295. this.MongoManager.promises.getProjectsDocs.resolves(null)
  296. this.DocArchiveManager.promises.unArchiveAllDocs.resolves(null)
  297. await expect(
  298. this.DocManager.promises.getAllNonDeletedDocs(
  299. this.project_id,
  300. this.filter
  301. )
  302. ).to.be.rejectedWith(`No docs for project ${this.project_id}`)
  303. })
  304. })
  305. })
  306. describe('patchDoc', function () {
  307. describe('when the doc exists', function () {
  308. beforeEach(function () {
  309. this.lines = ['mock', 'doc', 'lines']
  310. this.rev = 77
  311. this.MongoManager.promises.findDoc.resolves({
  312. _id: new ObjectId(this.doc_id),
  313. })
  314. this.meta = {}
  315. })
  316. describe('standard path', function () {
  317. beforeEach(async function () {
  318. await this.DocManager.promises.patchDoc(
  319. this.project_id,
  320. this.doc_id,
  321. this.meta
  322. )
  323. })
  324. it('should get the doc', function () {
  325. expect(this.MongoManager.promises.findDoc).to.have.been.calledWith(
  326. this.project_id,
  327. this.doc_id
  328. )
  329. })
  330. it('should persist the meta', function () {
  331. expect(this.MongoManager.promises.patchDoc).to.have.been.calledWith(
  332. this.project_id,
  333. this.doc_id,
  334. this.meta
  335. )
  336. })
  337. })
  338. describe('background flush disabled and deleting a doc', function () {
  339. beforeEach(async function () {
  340. this.settings.docstore.archiveOnSoftDelete = false
  341. this.meta.deleted = true
  342. await this.DocManager.promises.patchDoc(
  343. this.project_id,
  344. this.doc_id,
  345. this.meta
  346. )
  347. })
  348. it('should not flush the doc out of mongo', function () {
  349. expect(this.DocArchiveManager.promises.archiveDoc).to.not.have.been
  350. .called
  351. })
  352. })
  353. describe('background flush enabled and not deleting a doc', function () {
  354. beforeEach(async function () {
  355. this.settings.docstore.archiveOnSoftDelete = false
  356. this.meta.deleted = false
  357. await this.DocManager.promises.patchDoc(
  358. this.project_id,
  359. this.doc_id,
  360. this.meta
  361. )
  362. })
  363. it('should not flush the doc out of mongo', function () {
  364. expect(this.DocArchiveManager.promises.archiveDoc).to.not.have.been
  365. .called
  366. })
  367. })
  368. describe('background flush enabled and deleting a doc', function () {
  369. beforeEach(function () {
  370. this.settings.docstore.archiveOnSoftDelete = true
  371. this.meta.deleted = true
  372. })
  373. describe('when the background flush succeeds', function () {
  374. beforeEach(async function () {
  375. await this.DocManager.promises.patchDoc(
  376. this.project_id,
  377. this.doc_id,
  378. this.meta
  379. )
  380. })
  381. it('should not log a warning', function () {
  382. expect(this.logger.warn).to.not.have.been.called
  383. })
  384. it('should flush the doc out of mongo', function () {
  385. expect(
  386. this.DocArchiveManager.promises.archiveDoc
  387. ).to.have.been.calledWith(this.project_id, this.doc_id)
  388. })
  389. })
  390. describe('when the background flush fails', function () {
  391. beforeEach(async function () {
  392. this.err = new Error('foo')
  393. this.DocArchiveManager.promises.archiveDoc.rejects(this.err)
  394. await this.DocManager.promises.patchDoc(
  395. this.project_id,
  396. this.doc_id,
  397. this.meta
  398. )
  399. })
  400. it('should log a warning', function () {
  401. expect(this.logger.warn).to.have.been.calledWith(
  402. sinon.match({
  403. projectId: this.project_id,
  404. docId: this.doc_id,
  405. err: this.err,
  406. }),
  407. 'archiving a single doc in the background failed'
  408. )
  409. })
  410. })
  411. })
  412. })
  413. describe('when the doc does not exist', function () {
  414. it('should return a NotFoundError', async function () {
  415. this.MongoManager.promises.findDoc.resolves(null)
  416. await expect(
  417. this.DocManager.promises.patchDoc(this.project_id, this.doc_id, {})
  418. ).to.be.rejectedWith(
  419. `No such project/doc to delete: ${this.project_id}/${this.doc_id}`
  420. )
  421. })
  422. })
  423. })
  424. describe('updateDoc', function () {
  425. beforeEach(function () {
  426. this.oldDocLines = ['old', 'doc', 'lines']
  427. this.newDocLines = ['new', 'doc', 'lines']
  428. this.originalRanges = {
  429. changes: [
  430. {
  431. id: new ObjectId().toString(),
  432. op: { i: 'foo', p: 3 },
  433. meta: {
  434. user_id: new ObjectId().toString(),
  435. ts: new Date().toString(),
  436. },
  437. },
  438. ],
  439. }
  440. this.newRanges = {
  441. changes: [
  442. {
  443. id: new ObjectId().toString(),
  444. op: { i: 'bar', p: 6 },
  445. meta: {
  446. user_id: new ObjectId().toString(),
  447. ts: new Date().toString(),
  448. },
  449. },
  450. ],
  451. }
  452. this.version = 42
  453. this.doc = {
  454. _id: this.doc_id,
  455. project_id: this.project_id,
  456. lines: this.oldDocLines,
  457. rev: (this.rev = 5),
  458. version: this.version,
  459. ranges: this.originalRanges,
  460. }
  461. this.DocManager.promises._getDoc = sinon.stub()
  462. })
  463. describe('when only the doc lines have changed', function () {
  464. beforeEach(async function () {
  465. this.DocManager.promises._getDoc = sinon.stub().resolves(this.doc)
  466. this.result = await this.DocManager.promises.updateDoc(
  467. this.project_id,
  468. this.doc_id,
  469. this.newDocLines,
  470. this.version,
  471. this.originalRanges
  472. )
  473. })
  474. it('should get the existing doc', function () {
  475. this.DocManager.promises._getDoc
  476. .calledWith(this.project_id, this.doc_id, {
  477. version: true,
  478. rev: true,
  479. lines: true,
  480. ranges: true,
  481. inS3: true,
  482. })
  483. .should.equal(true)
  484. })
  485. it('should upsert the document to the doc collection', function () {
  486. this.MongoManager.promises.upsertIntoDocCollection
  487. .calledWith(this.project_id, this.doc_id, this.rev, {
  488. lines: this.newDocLines,
  489. })
  490. .should.equal(true)
  491. })
  492. it('should return the new rev', function () {
  493. expect(this.result).to.deep.equal({ modified: true, rev: this.rev + 1 })
  494. })
  495. })
  496. describe('when the doc ranges have changed', function () {
  497. beforeEach(async function () {
  498. this.DocManager.promises._getDoc = sinon.stub().resolves(this.doc)
  499. this.RangeManager.shouldUpdateRanges.returns(true)
  500. this.result = await this.DocManager.promises.updateDoc(
  501. this.project_id,
  502. this.doc_id,
  503. this.oldDocLines,
  504. this.version,
  505. this.newRanges
  506. )
  507. })
  508. it('should upsert the ranges', function () {
  509. this.MongoManager.promises.upsertIntoDocCollection
  510. .calledWith(this.project_id, this.doc_id, this.rev, {
  511. ranges: this.newRanges,
  512. })
  513. .should.equal(true)
  514. })
  515. it('should return the new rev', function () {
  516. expect(this.result).to.deep.equal({ modified: true, rev: this.rev + 1 })
  517. })
  518. })
  519. describe('when only the version has changed', function () {
  520. beforeEach(async function () {
  521. this.DocManager.promises._getDoc = sinon.stub().resolves(this.doc)
  522. this.result = await this.DocManager.promises.updateDoc(
  523. this.project_id,
  524. this.doc_id,
  525. this.oldDocLines,
  526. this.version + 1,
  527. this.originalRanges
  528. )
  529. })
  530. it('should update the version', function () {
  531. this.MongoManager.promises.upsertIntoDocCollection.should.have.been.calledWith(
  532. this.project_id,
  533. this.doc_id,
  534. this.rev,
  535. { version: this.version + 1 }
  536. )
  537. })
  538. it('should return the old rev', function () {
  539. expect(this.result).to.deep.equal({ modified: true, rev: this.rev })
  540. })
  541. })
  542. describe('when the doc has not changed at all', function () {
  543. beforeEach(async function () {
  544. this.DocManager.promises._getDoc = sinon.stub().resolves(this.doc)
  545. this.result = await this.DocManager.promises.updateDoc(
  546. this.project_id,
  547. this.doc_id,
  548. this.oldDocLines,
  549. this.version,
  550. this.originalRanges
  551. )
  552. })
  553. it('should not update the ranges or lines or version', function () {
  554. this.MongoManager.promises.upsertIntoDocCollection.called.should.equal(
  555. false
  556. )
  557. })
  558. it('should return the old rev and modified == false', function () {
  559. expect(this.result).to.deep.equal({ modified: false, rev: this.rev })
  560. })
  561. })
  562. describe('when the version is null', function () {
  563. it('should return an error', async function () {
  564. await expect(
  565. this.DocManager.promises.updateDoc(
  566. this.project_id,
  567. this.doc_id,
  568. this.newDocLines,
  569. null,
  570. this.originalRanges
  571. )
  572. ).to.be.rejectedWith('no lines, version or ranges provided')
  573. })
  574. })
  575. describe('when the lines are null', function () {
  576. it('should return an error', async function () {
  577. await expect(
  578. this.DocManager.promises.updateDoc(
  579. this.project_id,
  580. this.doc_id,
  581. null,
  582. this.version,
  583. this.originalRanges
  584. )
  585. ).to.be.rejectedWith('no lines, version or ranges provided')
  586. })
  587. })
  588. describe('when the ranges are null', function () {
  589. it('should return an error', async function () {
  590. await expect(
  591. this.DocManager.promises.updateDoc(
  592. this.project_id,
  593. this.doc_id,
  594. this.newDocLines,
  595. this.version,
  596. null
  597. )
  598. ).to.be.rejectedWith('no lines, version or ranges provided')
  599. })
  600. })
  601. describe('when there is a generic error getting the doc', function () {
  602. beforeEach(async function () {
  603. this.error = new Error('doc could not be found')
  604. this.DocManager.promises._getDoc = sinon.stub().rejects(this.error)
  605. await expect(
  606. this.DocManager.promises.updateDoc(
  607. this.project_id,
  608. this.doc_id,
  609. this.newDocLines,
  610. this.version,
  611. this.originalRanges
  612. )
  613. ).to.be.rejectedWith(this.error)
  614. })
  615. it('should not upsert the document to the doc collection', function () {
  616. this.MongoManager.promises.upsertIntoDocCollection.should.not.have.been
  617. .called
  618. })
  619. })
  620. describe('when the version was decremented', function () {
  621. it('should return an error', async function () {
  622. this.DocManager.promises._getDoc = sinon.stub().resolves(this.doc)
  623. await expect(
  624. this.DocManager.promises.updateDoc(
  625. this.project_id,
  626. this.doc_id,
  627. this.newDocLines,
  628. this.version - 1,
  629. this.originalRanges
  630. )
  631. ).to.be.rejectedWith(Errors.DocVersionDecrementedError)
  632. })
  633. })
  634. describe('when the doc lines have not changed', function () {
  635. beforeEach(async function () {
  636. this.DocManager.promises._getDoc = sinon.stub().resolves(this.doc)
  637. this.result = await this.DocManager.promises.updateDoc(
  638. this.project_id,
  639. this.doc_id,
  640. this.oldDocLines.slice(),
  641. this.version,
  642. this.originalRanges
  643. )
  644. })
  645. it('should not update the doc', function () {
  646. this.MongoManager.promises.upsertIntoDocCollection.called.should.equal(
  647. false
  648. )
  649. })
  650. it('should return the existing rev', function () {
  651. expect(this.result).to.deep.equal({ modified: false, rev: this.rev })
  652. })
  653. })
  654. describe('when the doc does not exist', function () {
  655. beforeEach(async function () {
  656. this.DocManager.promises._getDoc = sinon.stub().resolves(null)
  657. this.result = await this.DocManager.promises.updateDoc(
  658. this.project_id,
  659. this.doc_id,
  660. this.newDocLines,
  661. this.version,
  662. this.originalRanges
  663. )
  664. })
  665. it('should upsert the document to the doc collection', function () {
  666. this.MongoManager.promises.upsertIntoDocCollection.should.have.been.calledWith(
  667. this.project_id,
  668. this.doc_id,
  669. undefined,
  670. {
  671. lines: this.newDocLines,
  672. ranges: this.originalRanges,
  673. version: this.version,
  674. }
  675. )
  676. })
  677. it('should return the new rev', function () {
  678. expect(this.result).to.deep.equal({ modified: true, rev: 1 })
  679. })
  680. })
  681. describe('when another update is racing', function () {
  682. beforeEach(async function () {
  683. this.DocManager.promises._getDoc = sinon.stub().resolves(this.doc)
  684. this.MongoManager.promises.upsertIntoDocCollection
  685. .onFirstCall()
  686. .rejects(new Errors.DocRevValueError())
  687. this.RangeManager.shouldUpdateRanges.returns(true)
  688. this.result = await this.DocManager.promises.updateDoc(
  689. this.project_id,
  690. this.doc_id,
  691. this.newDocLines,
  692. this.version + 1,
  693. this.newRanges
  694. )
  695. })
  696. it('should upsert the doc twice', function () {
  697. this.MongoManager.promises.upsertIntoDocCollection.should.have.been.calledWith(
  698. this.project_id,
  699. this.doc_id,
  700. this.rev,
  701. {
  702. ranges: this.newRanges,
  703. lines: this.newDocLines,
  704. version: this.version + 1,
  705. }
  706. )
  707. this.MongoManager.promises.upsertIntoDocCollection.should.have.been
  708. .calledTwice
  709. })
  710. it('should return the new rev', function () {
  711. expect(this.result).to.deep.equal({ modified: true, rev: this.rev + 1 })
  712. })
  713. })
  714. })
  715. })