DocManager.test.js 22 KB

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