MongoManagerTests.js 13 KB

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