MongoManagerTests.js 13 KB

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