MongoManagerTests.js 14 KB

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