MongoManagerTests.js 11 KB

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