MongoManagerTests.js 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463
  1. const SandboxedModule = require('sandboxed-module')
  2. const sinon = require('sinon')
  3. const modulePath = require('path').join(
  4. __dirname,
  5. '../../../app/js/MongoManager'
  6. )
  7. const { ObjectId } = require('mongodb')
  8. const { assert, expect } = require('chai')
  9. const Errors = require('../../../app/js/Errors')
  10. describe('MongoManager', function () {
  11. beforeEach(function () {
  12. this.db = {
  13. docs: {
  14. updateOne: sinon.stub().resolves({ matchedCount: 1 }),
  15. insertOne: sinon.stub().resolves(),
  16. },
  17. docOps: {},
  18. }
  19. this.MongoManager = SandboxedModule.require(modulePath, {
  20. requires: {
  21. './mongodb': {
  22. db: this.db,
  23. ObjectId,
  24. },
  25. '@overleaf/metrics': { timeAsyncMethod: sinon.stub() },
  26. '@overleaf/settings': {
  27. max_deleted_docs: 42,
  28. docstore: { archivingLockDurationMs: 5000 },
  29. },
  30. './Errors': Errors,
  31. },
  32. })
  33. this.projectId = new ObjectId().toString()
  34. this.docId = new ObjectId().toString()
  35. this.rev = 42
  36. this.stubbedErr = new Error('hello world')
  37. this.lines = ['Three French hens', 'Two turtle doves']
  38. })
  39. describe('findDoc', function () {
  40. beforeEach(async function () {
  41. this.doc = { name: 'mock-doc' }
  42. this.db.docs.findOne = sinon.stub().resolves(this.doc)
  43. this.filter = { lines: true }
  44. this.result = await this.MongoManager.promises.findDoc(
  45. this.projectId,
  46. this.docId,
  47. this.filter
  48. )
  49. })
  50. it('should find the doc', function () {
  51. this.db.docs.findOne
  52. .calledWith(
  53. {
  54. _id: new ObjectId(this.docId),
  55. project_id: new ObjectId(this.projectId),
  56. },
  57. {
  58. projection: this.filter,
  59. }
  60. )
  61. .should.equal(true)
  62. })
  63. it('should return the doc', function () {
  64. expect(this.doc).to.deep.equal(this.doc)
  65. })
  66. })
  67. describe('patchDoc', function () {
  68. beforeEach(async function () {
  69. this.meta = { name: 'foo.tex' }
  70. await this.MongoManager.promises.patchDoc(
  71. this.projectId,
  72. this.docId,
  73. this.meta
  74. )
  75. })
  76. it('should pass the parameter along', function () {
  77. this.db.docs.updateOne.should.have.been.calledWith(
  78. {
  79. _id: new ObjectId(this.docId),
  80. project_id: new ObjectId(this.projectId),
  81. },
  82. {
  83. $set: this.meta,
  84. }
  85. )
  86. })
  87. })
  88. describe('getProjectsDocs', function () {
  89. beforeEach(function () {
  90. this.filter = { lines: true }
  91. this.doc1 = { name: 'mock-doc1' }
  92. this.doc2 = { name: 'mock-doc2' }
  93. this.doc3 = { name: 'mock-doc3' }
  94. this.doc4 = { name: 'mock-doc4' }
  95. this.db.docs.find = sinon.stub().returns({
  96. toArray: sinon.stub().resolves([this.doc, this.doc3, this.doc4]),
  97. })
  98. })
  99. describe('with included_deleted = false', function () {
  100. beforeEach(async function () {
  101. this.result = await this.MongoManager.promises.getProjectsDocs(
  102. this.projectId,
  103. { include_deleted: false },
  104. this.filter
  105. )
  106. })
  107. it('should find the non-deleted docs via the project_id', function () {
  108. this.db.docs.find
  109. .calledWith(
  110. {
  111. project_id: new ObjectId(this.projectId),
  112. deleted: { $ne: true },
  113. },
  114. {
  115. projection: this.filter,
  116. }
  117. )
  118. .should.equal(true)
  119. })
  120. it('should call return the docs', function () {
  121. expect(this.result).to.deep.equal([this.doc, this.doc3, this.doc4])
  122. })
  123. })
  124. describe('with included_deleted = true', function () {
  125. beforeEach(async function () {
  126. this.result = await this.MongoManager.promises.getProjectsDocs(
  127. this.projectId,
  128. { include_deleted: true },
  129. this.filter
  130. )
  131. })
  132. it('should find all via the project_id', function () {
  133. this.db.docs.find
  134. .calledWith(
  135. {
  136. project_id: new ObjectId(this.projectId),
  137. },
  138. {
  139. projection: this.filter,
  140. }
  141. )
  142. .should.equal(true)
  143. })
  144. it('should return the docs', function () {
  145. expect(this.result).to.deep.equal([this.doc, this.doc3, this.doc4])
  146. })
  147. })
  148. })
  149. describe('getProjectsDeletedDocs', function () {
  150. beforeEach(async function () {
  151. this.filter = { name: true }
  152. this.doc1 = { _id: '1', name: 'mock-doc1.tex' }
  153. this.doc2 = { _id: '2', name: 'mock-doc2.tex' }
  154. this.doc3 = { _id: '3', name: 'mock-doc3.tex' }
  155. this.db.docs.find = sinon.stub().returns({
  156. toArray: sinon.stub().resolves([this.doc1, this.doc2, this.doc3]),
  157. })
  158. this.result = await this.MongoManager.promises.getProjectsDeletedDocs(
  159. this.projectId,
  160. this.filter
  161. )
  162. })
  163. it('should find the deleted docs via the project_id', function () {
  164. this.db.docs.find
  165. .calledWith({
  166. project_id: new ObjectId(this.projectId),
  167. deleted: true,
  168. })
  169. .should.equal(true)
  170. })
  171. it('should filter, sort by deletedAt and limit', function () {
  172. this.db.docs.find
  173. .calledWith(sinon.match.any, {
  174. projection: this.filter,
  175. sort: { deletedAt: -1 },
  176. limit: 42,
  177. })
  178. .should.equal(true)
  179. })
  180. it('should return the docs', function () {
  181. expect(this.result).to.deep.equal([this.doc1, this.doc2, this.doc3])
  182. })
  183. })
  184. describe('upsertIntoDocCollection', function () {
  185. beforeEach(function () {
  186. this.oldRev = 77
  187. })
  188. it('should upsert the document', async function () {
  189. await this.MongoManager.promises.upsertIntoDocCollection(
  190. this.projectId,
  191. this.docId,
  192. this.oldRev,
  193. { lines: this.lines }
  194. )
  195. const args = this.db.docs.updateOne.args[0]
  196. assert.deepEqual(args[0], {
  197. _id: new ObjectId(this.docId),
  198. project_id: new ObjectId(this.projectId),
  199. rev: this.oldRev,
  200. })
  201. assert.equal(args[1].$set.lines, this.lines)
  202. assert.equal(args[1].$inc.rev, 1)
  203. })
  204. it('should handle update error', async function () {
  205. this.db.docs.updateOne.rejects(this.stubbedErr)
  206. await expect(
  207. this.MongoManager.promises.upsertIntoDocCollection(
  208. this.projectId,
  209. this.docId,
  210. this.rev,
  211. {
  212. lines: this.lines,
  213. }
  214. )
  215. ).to.be.rejectedWith(this.stubbedErr)
  216. })
  217. it('should insert without a previous rev', async function () {
  218. await this.MongoManager.promises.upsertIntoDocCollection(
  219. this.projectId,
  220. this.docId,
  221. null,
  222. { lines: this.lines, ranges: this.ranges }
  223. )
  224. expect(this.db.docs.insertOne).to.have.been.calledWith({
  225. _id: new ObjectId(this.docId),
  226. project_id: new ObjectId(this.projectId),
  227. rev: 1,
  228. lines: this.lines,
  229. ranges: this.ranges,
  230. })
  231. })
  232. it('should handle generic insert error', async function () {
  233. this.db.docs.insertOne.rejects(this.stubbedErr)
  234. await expect(
  235. this.MongoManager.promises.upsertIntoDocCollection(
  236. this.projectId,
  237. this.docId,
  238. null,
  239. { lines: this.lines, ranges: this.ranges }
  240. )
  241. ).to.be.rejectedWith(this.stubbedErr)
  242. })
  243. it('should handle duplicate insert error', async function () {
  244. this.db.docs.insertOne.rejects({ code: 11000 })
  245. await expect(
  246. this.MongoManager.promises.upsertIntoDocCollection(
  247. this.projectId,
  248. this.docId,
  249. null,
  250. { lines: this.lines, ranges: this.ranges }
  251. )
  252. ).to.be.rejectedWith(Errors.DocRevValueError)
  253. })
  254. })
  255. describe('destroyProject', function () {
  256. beforeEach(async function () {
  257. this.projectId = new ObjectId()
  258. this.docIds = [new ObjectId(), new ObjectId()]
  259. this.db.docs.deleteMany = sinon.stub().resolves()
  260. this.db.docOps.deleteMany = sinon.stub().resolves()
  261. this.db.docs.find = sinon
  262. .stub()
  263. .withArgs({ project_id: this.projectId })
  264. .returns({
  265. toArray: sinon.stub().resolves(
  266. this.docIds.map(id => ({
  267. _id: id,
  268. }))
  269. ),
  270. })
  271. await this.MongoManager.promises.destroyProject(this.projectId)
  272. })
  273. it('should destroy all docs', function () {
  274. sinon.assert.calledWith(this.db.docs.deleteMany, {
  275. project_id: this.projectId,
  276. })
  277. })
  278. it('should destroy the docOps', function () {
  279. sinon.assert.calledWith(this.db.docOps.deleteMany, {
  280. doc_id: { $in: this.docIds },
  281. })
  282. })
  283. })
  284. describe('getDocVersion', function () {
  285. describe('when the doc exists', function () {
  286. beforeEach(async function () {
  287. this.doc = { version: (this.version = 42) }
  288. this.db.docOps.findOne = sinon.stub().resolves(this.doc)
  289. this.result = await this.MongoManager.promises.getDocVersion(this.docId)
  290. })
  291. it('should look for the doc in the database', function () {
  292. this.db.docOps.findOne
  293. .calledWith(
  294. { doc_id: new ObjectId(this.docId) },
  295. {
  296. projection: { version: 1 },
  297. }
  298. )
  299. .should.equal(true)
  300. })
  301. it('should return the version', function () {
  302. expect(this.result).to.equal(this.version)
  303. })
  304. })
  305. describe("when the doc doesn't exist", function () {
  306. beforeEach(async function () {
  307. this.db.docOps.findOne = sinon.stub().resolves(null)
  308. this.result = await this.MongoManager.promises.getDocVersion(this.docId)
  309. })
  310. it('should return 0', function () {
  311. expect(this.result).to.equal(0)
  312. })
  313. })
  314. })
  315. describe('checkRevUnchanged', function () {
  316. this.beforeEach(function () {
  317. this.doc = { _id: new ObjectId(), name: 'mock-doc', rev: 1 }
  318. })
  319. it('should not error when the rev has not changed', async function () {
  320. this.db.docs.findOne = sinon.stub().resolves({ rev: 1 })
  321. await this.MongoManager.promises.checkRevUnchanged(this.doc)
  322. })
  323. it('should return an error when the rev has changed', async function () {
  324. this.db.docs.findOne = sinon.stub().resolves({ rev: 2 })
  325. await expect(
  326. this.MongoManager.promises.checkRevUnchanged(this.doc)
  327. ).to.be.rejectedWith(Errors.DocModifiedError)
  328. })
  329. it('should return a value error if incoming rev is NaN', async function () {
  330. this.db.docs.findOne = sinon.stub().resolves({ rev: 2 })
  331. this.doc = { _id: new ObjectId(), name: 'mock-doc', rev: NaN }
  332. await expect(
  333. this.MongoManager.promises.checkRevUnchanged(this.doc)
  334. ).to.be.rejectedWith(Errors.DocRevValueError)
  335. })
  336. it('should return a value error if checked doc rev is NaN', async function () {
  337. this.db.docs.findOne = sinon.stub().resolves({ rev: NaN })
  338. await expect(
  339. this.MongoManager.promises.checkRevUnchanged(this.doc)
  340. ).to.be.rejectedWith(Errors.DocRevValueError)
  341. })
  342. })
  343. describe('restoreArchivedDoc', function () {
  344. beforeEach(function () {
  345. this.archivedDoc = {
  346. lines: ['a', 'b', 'c'],
  347. ranges: { some: 'ranges' },
  348. rev: 2,
  349. }
  350. })
  351. describe('complete doc', function () {
  352. beforeEach(async function () {
  353. await this.MongoManager.promises.restoreArchivedDoc(
  354. this.projectId,
  355. this.docId,
  356. this.archivedDoc
  357. )
  358. })
  359. it('updates Mongo', function () {
  360. expect(this.db.docs.updateOne).to.have.been.calledWith(
  361. {
  362. _id: new ObjectId(this.docId),
  363. project_id: new ObjectId(this.projectId),
  364. rev: this.archivedDoc.rev,
  365. },
  366. {
  367. $set: {
  368. lines: this.archivedDoc.lines,
  369. ranges: this.archivedDoc.ranges,
  370. },
  371. $unset: {
  372. inS3: true,
  373. },
  374. }
  375. )
  376. })
  377. })
  378. describe('without ranges', function () {
  379. beforeEach(async function () {
  380. delete this.archivedDoc.ranges
  381. await this.MongoManager.promises.restoreArchivedDoc(
  382. this.projectId,
  383. this.docId,
  384. this.archivedDoc
  385. )
  386. })
  387. it('sets ranges to an empty object', function () {
  388. expect(this.db.docs.updateOne).to.have.been.calledWith(
  389. {
  390. _id: new ObjectId(this.docId),
  391. project_id: new ObjectId(this.projectId),
  392. rev: this.archivedDoc.rev,
  393. },
  394. {
  395. $set: {
  396. lines: this.archivedDoc.lines,
  397. ranges: {},
  398. },
  399. $unset: {
  400. inS3: true,
  401. },
  402. }
  403. )
  404. })
  405. })
  406. describe("when the update doesn't succeed", function () {
  407. it('throws a DocRevValueError', async function () {
  408. this.db.docs.updateOne.resolves({ matchedCount: 0 })
  409. await expect(
  410. this.MongoManager.promises.restoreArchivedDoc(
  411. this.projectId,
  412. this.docId,
  413. this.archivedDoc
  414. )
  415. ).to.be.rejectedWith(Errors.DocRevValueError)
  416. })
  417. })
  418. })
  419. })