MongoManagerTests.js 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386
  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 } = require('chai')
  9. const Errors = require('../../../app/js/Errors')
  10. describe('MongoManager', function () {
  11. beforeEach(function () {
  12. this.MongoManager = SandboxedModule.require(modulePath, {
  13. requires: {
  14. './mongodb': {
  15. db: (this.db = { docs: {}, docOps: {} }),
  16. ObjectId,
  17. },
  18. '@overleaf/metrics': { timeAsyncMethod: sinon.stub() },
  19. '@overleaf/settings': { max_deleted_docs: 42 },
  20. './Errors': Errors,
  21. },
  22. })
  23. this.project_id = ObjectId().toString()
  24. this.doc_id = ObjectId().toString()
  25. this.callback = sinon.stub()
  26. this.stubbedErr = new Error('hello world')
  27. })
  28. describe('findDoc', function () {
  29. beforeEach(function () {
  30. this.doc = { name: 'mock-doc' }
  31. this.db.docs.findOne = sinon.stub().callsArgWith(2, null, this.doc)
  32. this.filter = { lines: true }
  33. this.MongoManager.findDoc(
  34. this.project_id,
  35. this.doc_id,
  36. this.filter,
  37. this.callback
  38. )
  39. })
  40. it('should find the doc', function () {
  41. this.db.docs.findOne
  42. .calledWith(
  43. {
  44. _id: ObjectId(this.doc_id),
  45. project_id: ObjectId(this.project_id),
  46. },
  47. {
  48. projection: this.filter,
  49. }
  50. )
  51. .should.equal(true)
  52. })
  53. it('should call the callback with the doc', function () {
  54. this.callback.calledWith(null, this.doc).should.equal(true)
  55. })
  56. })
  57. describe('patchDoc', function () {
  58. beforeEach(function (done) {
  59. this.db.docs.updateOne = sinon.stub().yields(null)
  60. this.meta = { name: 'foo.tex' }
  61. this.callback.callsFake(done)
  62. this.MongoManager.patchDoc(
  63. this.project_id,
  64. this.doc_id,
  65. this.meta,
  66. this.callback
  67. )
  68. })
  69. it('should pass the parameter along', function () {
  70. this.db.docs.updateOne.should.have.been.calledWith(
  71. {
  72. _id: ObjectId(this.doc_id),
  73. project_id: ObjectId(this.project_id),
  74. },
  75. {
  76. $set: this.meta,
  77. },
  78. this.callback
  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
  91. .stub()
  92. .callsArgWith(0, null, [this.doc, this.doc3, this.doc4]),
  93. })
  94. })
  95. describe('with included_deleted = false', function () {
  96. beforeEach(function () {
  97. this.MongoManager.getProjectsDocs(
  98. this.project_id,
  99. { include_deleted: false },
  100. this.filter,
  101. this.callback
  102. )
  103. })
  104. it('should find the non-deleted docs via the project_id', function () {
  105. this.db.docs.find
  106. .calledWith(
  107. {
  108. project_id: ObjectId(this.project_id),
  109. deleted: { $ne: true },
  110. },
  111. {
  112. projection: this.filter,
  113. }
  114. )
  115. .should.equal(true)
  116. })
  117. it('should call the callback with the docs', function () {
  118. this.callback
  119. .calledWith(null, [this.doc, this.doc3, this.doc4])
  120. .should.equal(true)
  121. })
  122. })
  123. describe('with included_deleted = true', function () {
  124. beforeEach(function () {
  125. this.MongoManager.getProjectsDocs(
  126. this.project_id,
  127. { include_deleted: true },
  128. this.filter,
  129. this.callback
  130. )
  131. })
  132. it('should find all via the project_id', function () {
  133. this.db.docs.find
  134. .calledWith(
  135. {
  136. project_id: ObjectId(this.project_id),
  137. },
  138. {
  139. projection: this.filter,
  140. }
  141. )
  142. .should.equal(true)
  143. })
  144. it('should call the callback with the docs', function () {
  145. this.callback
  146. .calledWith(null, [this.doc, this.doc3, this.doc4])
  147. .should.equal(true)
  148. })
  149. })
  150. })
  151. describe('getProjectsDeletedDocs', function () {
  152. beforeEach(function (done) {
  153. this.filter = { name: true }
  154. this.doc1 = { _id: '1', name: 'mock-doc1.tex' }
  155. this.doc2 = { _id: '2', name: 'mock-doc2.tex' }
  156. this.doc3 = { _id: '3', name: 'mock-doc3.tex' }
  157. this.db.docs.find = sinon.stub().returns({
  158. toArray: sinon.stub().yields(null, [this.doc1, this.doc2, this.doc3]),
  159. })
  160. this.callback.callsFake(done)
  161. this.MongoManager.getProjectsDeletedDocs(
  162. this.project_id,
  163. this.filter,
  164. this.callback
  165. )
  166. })
  167. it('should find the deleted docs via the project_id', function () {
  168. this.db.docs.find
  169. .calledWith({
  170. project_id: ObjectId(this.project_id),
  171. deleted: true,
  172. })
  173. .should.equal(true)
  174. })
  175. it('should filter, sort by deletedAt and limit', function () {
  176. this.db.docs.find
  177. .calledWith(sinon.match.any, {
  178. projection: this.filter,
  179. sort: { deletedAt: -1 },
  180. limit: 42,
  181. })
  182. .should.equal(true)
  183. })
  184. it('should call the callback with the docs', function () {
  185. this.callback
  186. .calledWith(null, [this.doc1, this.doc2, this.doc3])
  187. .should.equal(true)
  188. })
  189. })
  190. describe('upsertIntoDocCollection', function () {
  191. beforeEach(function () {
  192. this.db.docs.updateOne = sinon.stub().callsArgWith(3, this.stubbedErr)
  193. this.oldRev = 77
  194. })
  195. it('should upsert the document', function (done) {
  196. this.MongoManager.upsertIntoDocCollection(
  197. this.project_id,
  198. this.doc_id,
  199. { lines: this.lines },
  200. err => {
  201. assert.equal(err, this.stubbedErr)
  202. const args = this.db.docs.updateOne.args[0]
  203. assert.deepEqual(args[0], { _id: ObjectId(this.doc_id) })
  204. assert.equal(args[1].$set.lines, this.lines)
  205. assert.equal(args[1].$inc.rev, 1)
  206. assert.deepEqual(args[1].$set.project_id, ObjectId(this.project_id))
  207. done()
  208. }
  209. )
  210. })
  211. it('should return the error', function (done) {
  212. this.MongoManager.upsertIntoDocCollection(
  213. this.project_id,
  214. this.doc_id,
  215. { lines: this.lines },
  216. err => {
  217. err.should.equal(this.stubbedErr)
  218. done()
  219. }
  220. )
  221. })
  222. })
  223. describe('destroyDoc', function () {
  224. beforeEach(function (done) {
  225. this.db.docs.deleteOne = sinon.stub().yields()
  226. this.db.docOps.deleteOne = sinon.stub().yields()
  227. this.MongoManager.destroyDoc('123456789012', done)
  228. })
  229. it('should destroy the doc', function () {
  230. sinon.assert.calledWith(this.db.docs.deleteOne, {
  231. _id: ObjectId('123456789012'),
  232. })
  233. })
  234. it('should destroy the docOps', function () {
  235. sinon.assert.calledWith(this.db.docOps.deleteOne, {
  236. doc_id: ObjectId('123456789012'),
  237. })
  238. })
  239. })
  240. describe('getDocVersion', function () {
  241. describe('when the doc exists', function () {
  242. beforeEach(function () {
  243. this.doc = { version: (this.version = 42) }
  244. this.db.docOps.findOne = sinon.stub().callsArgWith(2, null, this.doc)
  245. this.MongoManager.getDocVersion(this.doc_id, this.callback)
  246. })
  247. it('should look for the doc in the database', function () {
  248. this.db.docOps.findOne
  249. .calledWith(
  250. { doc_id: ObjectId(this.doc_id) },
  251. {
  252. projection: { version: 1 },
  253. }
  254. )
  255. .should.equal(true)
  256. })
  257. it('should call the callback with the version', function () {
  258. this.callback.calledWith(null, this.version).should.equal(true)
  259. })
  260. })
  261. describe("when the doc doesn't exist", function () {
  262. beforeEach(function () {
  263. this.db.docOps.findOne = sinon.stub().callsArgWith(2, null, null)
  264. this.MongoManager.getDocVersion(this.doc_id, this.callback)
  265. })
  266. it('should call the callback with 0', function () {
  267. this.callback.calledWith(null, 0).should.equal(true)
  268. })
  269. })
  270. })
  271. describe('setDocVersion', function () {
  272. beforeEach(function () {
  273. this.version = 42
  274. this.db.docOps.updateOne = sinon.stub().callsArg(3)
  275. this.MongoManager.setDocVersion(this.doc_id, this.version, this.callback)
  276. })
  277. it('should update the doc version', function () {
  278. this.db.docOps.updateOne
  279. .calledWith(
  280. {
  281. doc_id: ObjectId(this.doc_id),
  282. },
  283. {
  284. $set: {
  285. version: this.version,
  286. },
  287. },
  288. {
  289. upsert: true,
  290. }
  291. )
  292. .should.equal(true)
  293. })
  294. it('should call the callback', function () {
  295. this.callback.called.should.equal(true)
  296. })
  297. })
  298. describe('withRevCheck', function () {
  299. this.beforeEach(function () {
  300. this.doc = { _id: ObjectId(), name: 'mock-doc', rev: 1 }
  301. this.testFunction = sinon.stub().yields(null, 'foo')
  302. })
  303. it('should call the callback when the rev has not changed', function (done) {
  304. this.db.docs.findOne = sinon.stub().callsArgWith(2, null, { rev: 1 })
  305. this.MongoManager.withRevCheck(
  306. this.doc,
  307. this.testFunction,
  308. (err, result) => {
  309. result.should.equal('foo')
  310. assert.isNull(err)
  311. done()
  312. }
  313. )
  314. })
  315. it('should return an error when the rev has changed', function (done) {
  316. this.db.docs.findOne = sinon.stub().callsArgWith(2, null, { rev: 2 })
  317. this.MongoManager.withRevCheck(
  318. this.doc,
  319. this.testFunction,
  320. (err, result) => {
  321. err.should.be.instanceof(Errors.DocModifiedError)
  322. done()
  323. }
  324. )
  325. })
  326. it('should return a value error if incoming rev is NaN', function (done) {
  327. this.db.docs.findOne = sinon.stub().callsArgWith(2, null, { rev: 2 })
  328. this.doc = { _id: ObjectId(), name: 'mock-doc', rev: NaN }
  329. this.MongoManager.withRevCheck(
  330. this.doc,
  331. this.testFunction,
  332. (err, result) => {
  333. err.should.be.instanceof(Errors.DocRevValueError)
  334. done()
  335. }
  336. )
  337. })
  338. it('should return a value error if checked doc rev is NaN', function (done) {
  339. this.db.docs.findOne = sinon.stub().callsArgWith(2, null, { rev: NaN })
  340. this.MongoManager.withRevCheck(
  341. this.doc,
  342. this.testFunction,
  343. (err, result) => {
  344. err.should.be.instanceof(Errors.DocRevValueError)
  345. done()
  346. }
  347. )
  348. })
  349. })
  350. })