MongoManagerTests.js 11 KB

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