DocManagerTests.js 31 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067
  1. /* eslint-disable
  2. camelcase,
  3. handle-callback-err,
  4. no-dupe-keys,
  5. no-return-assign,
  6. no-unused-vars,
  7. */
  8. // TODO: This file was created by bulk-decaffeinate.
  9. // Fix any style issues and re-enable lint.
  10. /*
  11. * decaffeinate suggestions:
  12. * DS102: Remove unnecessary code created because of implicit returns
  13. * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
  14. */
  15. const SandboxedModule = require('sandboxed-module')
  16. const sinon = require('sinon')
  17. const chai = require('chai')
  18. chai.use(require('sinon-chai'))
  19. const { assert } = require('chai')
  20. chai.should()
  21. const { expect } = chai
  22. const modulePath = require('path').join(__dirname, '../../../app/js/DocManager')
  23. const { ObjectId } = require('mongodb')
  24. const Errors = require('../../../app/js/Errors')
  25. describe('DocManager', function () {
  26. beforeEach(function () {
  27. this.DocManager = SandboxedModule.require(modulePath, {
  28. requires: {
  29. './MongoManager': (this.MongoManager = {}),
  30. './DocArchiveManager': (this.DocArchiveManager = {}),
  31. './RangeManager': (this.RangeManager = {
  32. jsonRangesToMongo(r) {
  33. return r
  34. },
  35. shouldUpdateRanges: sinon.stub().returns(false)
  36. }),
  37. 'settings-sharelatex': (this.settings = { docstore: {} }),
  38. 'logger-sharelatex': (this.logger = {
  39. log: sinon.stub(),
  40. warn() {},
  41. err() {}
  42. }),
  43. './Errors': Errors
  44. }
  45. })
  46. this.doc_id = ObjectId().toString()
  47. this.project_id = ObjectId().toString()
  48. this.another_project_id = ObjectId().toString()
  49. this.callback = sinon.stub()
  50. return (this.stubbedError = new Error('blew up'))
  51. })
  52. describe('checkDocExists', function () {
  53. beforeEach(function () {
  54. return (this.DocManager._getDoc = sinon.stub())
  55. })
  56. it('should call get doc with a quick filter', function (done) {
  57. this.DocManager._getDoc.callsArgWith(3, null, { _id: this.doc_id })
  58. return this.DocManager.checkDocExists(
  59. this.project_id,
  60. this.doc_id,
  61. (err, exist) => {
  62. exist.should.equal(true)
  63. this.DocManager._getDoc
  64. .calledWith(this.project_id, this.doc_id, { _id: 1, inS3: true })
  65. .should.equal(true)
  66. return done()
  67. }
  68. )
  69. })
  70. it('should return false when doc is not there', function (done) {
  71. this.DocManager._getDoc.callsArgWith(3, null)
  72. return this.DocManager.checkDocExists(
  73. this.project_id,
  74. this.doc_id,
  75. (err, exist) => {
  76. exist.should.equal(false)
  77. return done()
  78. }
  79. )
  80. })
  81. return it('should return error when get doc errors', function (done) {
  82. this.DocManager._getDoc.callsArgWith(3, 'error')
  83. return this.DocManager.checkDocExists(
  84. this.project_id,
  85. this.doc_id,
  86. (err, exist) => {
  87. err.should.equal('error')
  88. return done()
  89. }
  90. )
  91. })
  92. })
  93. describe('getFullDoc', function () {
  94. beforeEach(function () {
  95. this.DocManager._getDoc = sinon.stub()
  96. return (this.doc = {
  97. _id: this.doc_id,
  98. lines: ['2134']
  99. })
  100. })
  101. it('should call get doc with a quick filter', function (done) {
  102. this.DocManager._getDoc.callsArgWith(3, null, this.doc)
  103. return this.DocManager.getFullDoc(
  104. this.project_id,
  105. this.doc_id,
  106. (err, doc) => {
  107. doc.should.equal(this.doc)
  108. this.DocManager._getDoc
  109. .calledWith(this.project_id, this.doc_id, {
  110. lines: true,
  111. rev: true,
  112. deleted: true,
  113. version: true,
  114. ranges: true,
  115. inS3: true
  116. })
  117. .should.equal(true)
  118. return done()
  119. }
  120. )
  121. })
  122. return it('should return error when get doc errors', function (done) {
  123. this.DocManager._getDoc.callsArgWith(3, 'error')
  124. return this.DocManager.getFullDoc(
  125. this.project_id,
  126. this.doc_id,
  127. (err, exist) => {
  128. err.should.equal('error')
  129. return done()
  130. }
  131. )
  132. })
  133. })
  134. describe('getRawDoc', function () {
  135. beforeEach(function () {
  136. this.DocManager._getDoc = sinon.stub()
  137. return (this.doc = { lines: ['2134'] })
  138. })
  139. it('should call get doc with a quick filter', function (done) {
  140. this.DocManager._getDoc.callsArgWith(3, null, this.doc)
  141. return this.DocManager.getDocLines(
  142. this.project_id,
  143. this.doc_id,
  144. (err, doc) => {
  145. doc.should.equal(this.doc)
  146. this.DocManager._getDoc
  147. .calledWith(this.project_id, this.doc_id, {
  148. lines: true,
  149. inS3: true
  150. })
  151. .should.equal(true)
  152. return done()
  153. }
  154. )
  155. })
  156. return it('should return error when get doc errors', function (done) {
  157. this.DocManager._getDoc.callsArgWith(3, 'error')
  158. return this.DocManager.getDocLines(
  159. this.project_id,
  160. this.doc_id,
  161. (err, exist) => {
  162. err.should.equal('error')
  163. return done()
  164. }
  165. )
  166. })
  167. })
  168. describe('getDoc', function () {
  169. beforeEach(function () {
  170. this.project = { name: 'mock-project' }
  171. this.doc = {
  172. _id: this.doc_id,
  173. project_id: this.project_id,
  174. lines: ['mock-lines']
  175. }
  176. this.version = 42
  177. this.MongoManager.findDoc = sinon.stub()
  178. return (this.MongoManager.getDocVersion = sinon
  179. .stub()
  180. .yields(null, this.version))
  181. })
  182. describe('when using a filter', function () {
  183. beforeEach(function () {
  184. return this.MongoManager.findDoc.yields(null, this.doc)
  185. })
  186. it('should error if inS3 is not set to true', function (done) {
  187. return this.DocManager._getDoc(
  188. this.project_id,
  189. this.doc_id,
  190. { inS3: false },
  191. (err) => {
  192. expect(err).to.exist
  193. return done()
  194. }
  195. )
  196. })
  197. it('should always get inS3 even when no filter is passed', function (done) {
  198. return this.DocManager._getDoc(
  199. this.project_id,
  200. this.doc_id,
  201. undefined,
  202. (err) => {
  203. this.MongoManager.findDoc.called.should.equal(false)
  204. expect(err).to.exist
  205. return done()
  206. }
  207. )
  208. })
  209. return it('should not error if inS3 is set to true', function (done) {
  210. return this.DocManager._getDoc(
  211. this.project_id,
  212. this.doc_id,
  213. { inS3: true },
  214. (err) => {
  215. expect(err).to.not.exist
  216. return done()
  217. }
  218. )
  219. })
  220. })
  221. describe('when the doc is in the doc collection', function () {
  222. beforeEach(function () {
  223. this.MongoManager.findDoc.yields(null, this.doc)
  224. return this.DocManager._getDoc(
  225. this.project_id,
  226. this.doc_id,
  227. { version: true, inS3: true },
  228. this.callback
  229. )
  230. })
  231. it('should get the doc from the doc collection', function () {
  232. return this.MongoManager.findDoc
  233. .calledWith(this.project_id, this.doc_id)
  234. .should.equal(true)
  235. })
  236. it('should get the doc version from the docOps collection', function () {
  237. return this.MongoManager.getDocVersion
  238. .calledWith(this.doc_id)
  239. .should.equal(true)
  240. })
  241. return it('should return the callback with the doc with the version', function () {
  242. this.callback.called.should.equal(true)
  243. const doc = this.callback.args[0][1]
  244. doc.lines.should.equal(this.doc.lines)
  245. return doc.version.should.equal(this.version)
  246. })
  247. })
  248. describe('without the version filter', function () {
  249. beforeEach(function () {
  250. this.MongoManager.findDoc.yields(null, this.doc)
  251. return this.DocManager._getDoc(
  252. this.project_id,
  253. this.doc_id,
  254. { version: false, inS3: true },
  255. this.callback
  256. )
  257. })
  258. return it('should not get the doc version from the docOps collection', function () {
  259. return this.MongoManager.getDocVersion.called.should.equal(false)
  260. })
  261. })
  262. describe('when MongoManager.findDoc errors', function () {
  263. beforeEach(function () {
  264. this.MongoManager.findDoc.yields(this.stubbedError)
  265. return this.DocManager._getDoc(
  266. this.project_id,
  267. this.doc_id,
  268. { version: true, inS3: true },
  269. this.callback
  270. )
  271. })
  272. return it('should return the error', function () {
  273. return this.callback.calledWith(this.stubbedError).should.equal(true)
  274. })
  275. })
  276. describe('when the doc is archived', function () {
  277. beforeEach(function () {
  278. this.doc = {
  279. _id: this.doc_id,
  280. project_id: this.project_id,
  281. lines: ['mock-lines'],
  282. inS3: true
  283. }
  284. this.MongoManager.findDoc.yields(null, this.doc)
  285. this.DocArchiveManager.unarchiveDoc = (
  286. project_id,
  287. doc_id,
  288. callback
  289. ) => {
  290. this.doc.inS3 = false
  291. return callback()
  292. }
  293. sinon.spy(this.DocArchiveManager, 'unarchiveDoc')
  294. return this.DocManager._getDoc(
  295. this.project_id,
  296. this.doc_id,
  297. { version: true, inS3: true },
  298. this.callback
  299. )
  300. })
  301. it('should call the DocArchive to unarchive the doc', function () {
  302. return this.DocArchiveManager.unarchiveDoc
  303. .calledWith(this.project_id, this.doc_id)
  304. .should.equal(true)
  305. })
  306. it('should look up the doc twice', function () {
  307. return this.MongoManager.findDoc.calledTwice.should.equal(true)
  308. })
  309. return it('should return the doc', function () {
  310. return this.callback.calledWith(null, this.doc).should.equal(true)
  311. })
  312. })
  313. return describe('when the doc does not exist in the docs collection', function () {
  314. beforeEach(function () {
  315. this.MongoManager.findDoc = sinon.stub().yields(null, null)
  316. return this.DocManager._getDoc(
  317. this.project_id,
  318. this.doc_id,
  319. { version: true, inS3: true },
  320. this.callback
  321. )
  322. })
  323. return it('should return a NotFoundError', function () {
  324. return this.callback
  325. .calledWith(
  326. sinon.match.has(
  327. 'message',
  328. `No such doc: ${this.doc_id} in project ${this.project_id}`
  329. )
  330. )
  331. .should.equal(true)
  332. })
  333. })
  334. })
  335. describe('getAllNonDeletedDocs', function () {
  336. describe('when the project exists', function () {
  337. beforeEach(function () {
  338. this.docs = [
  339. {
  340. _id: this.doc_id,
  341. project_id: this.project_id,
  342. lines: ['mock-lines']
  343. }
  344. ]
  345. this.MongoManager.getProjectsDocs = sinon
  346. .stub()
  347. .callsArgWith(3, null, this.docs)
  348. this.DocArchiveManager.unArchiveAllDocs = sinon
  349. .stub()
  350. .callsArgWith(1, null, this.docs)
  351. this.filter = { lines: true }
  352. return this.DocManager.getAllNonDeletedDocs(
  353. this.project_id,
  354. this.filter,
  355. this.callback
  356. )
  357. })
  358. it('should get the project from the database', function () {
  359. return this.MongoManager.getProjectsDocs
  360. .calledWith(this.project_id, { include_deleted: false }, this.filter)
  361. .should.equal(true)
  362. })
  363. return it('should return the docs', function () {
  364. return this.callback.calledWith(null, this.docs).should.equal(true)
  365. })
  366. })
  367. return describe('when there are no docs for the project', function () {
  368. beforeEach(function () {
  369. this.MongoManager.getProjectsDocs = sinon
  370. .stub()
  371. .callsArgWith(3, null, null)
  372. this.DocArchiveManager.unArchiveAllDocs = sinon
  373. .stub()
  374. .callsArgWith(1, null)
  375. return this.DocManager.getAllNonDeletedDocs(
  376. this.project_id,
  377. this.filter,
  378. this.callback
  379. )
  380. })
  381. return it('should return a NotFoundError', function () {
  382. return this.callback
  383. .calledWith(
  384. sinon.match.has('message', `No docs for project ${this.project_id}`)
  385. )
  386. .should.equal(true)
  387. })
  388. })
  389. })
  390. describe('deleteDoc', function () {
  391. describe('when the doc exists', function () {
  392. beforeEach(function (done) {
  393. this.callback = sinon.stub().callsFake(done)
  394. this.lines = ['mock', 'doc', 'lines']
  395. this.rev = 77
  396. this.DocManager.checkDocExists = sinon
  397. .stub()
  398. .callsArgWith(2, null, true)
  399. this.MongoManager.markDocAsDeleted = sinon.stub().yields(null)
  400. this.DocArchiveManager.archiveDocById = sinon.stub().yields(null)
  401. return this.DocManager.deleteDoc(
  402. this.project_id,
  403. this.doc_id,
  404. this.callback
  405. )
  406. })
  407. it('should get the doc', function () {
  408. return this.DocManager.checkDocExists
  409. .calledWith(this.project_id, this.doc_id)
  410. .should.equal(true)
  411. })
  412. it('should mark doc as deleted', function () {
  413. return this.MongoManager.markDocAsDeleted
  414. .calledWith(this.project_id, this.doc_id)
  415. .should.equal(true)
  416. })
  417. it('should return the callback', function () {
  418. return this.callback.called.should.equal(true)
  419. })
  420. describe('background flush disabled', function () {
  421. beforeEach(function () {
  422. this.settings.docstore.archiveOnSoftDelete = false
  423. })
  424. it('should not flush the doc out of mongo', function () {
  425. this.DocArchiveManager.archiveDocById.should.not.have.been.called
  426. })
  427. })
  428. describe('background flush enabled', function () {
  429. beforeEach(function (done) {
  430. this.settings.docstore.archiveOnSoftDelete = true
  431. this.callback = sinon.stub().callsFake(done)
  432. this.DocManager.deleteDoc(this.project_id, this.doc_id, this.callback)
  433. })
  434. it('should not fail the delete process', function () {
  435. this.callback.should.have.been.calledWith(null)
  436. })
  437. it('should flush the doc out of mongo', function () {
  438. this.DocArchiveManager.archiveDocById.should.have.been.calledWith(
  439. this.project_id,
  440. this.doc_id
  441. )
  442. })
  443. describe('when the background flush fails', function () {
  444. beforeEach(function (done) {
  445. this.err = new Error('foo')
  446. this.DocManager.checkDocExists = sinon.stub().yields(null, true)
  447. this.MongoManager.markDocAsDeleted = sinon.stub().yields(null)
  448. this.DocArchiveManager.archiveDocById = sinon
  449. .stub()
  450. .yields(this.err)
  451. this.logger.warn = sinon.stub()
  452. this.callback = sinon.stub().callsFake(done)
  453. this.DocManager.deleteDoc(
  454. this.project_id,
  455. this.doc_id,
  456. this.callback
  457. )
  458. })
  459. it('should log a warning', function () {
  460. this.logger.warn.should.have.been.calledWith(
  461. sinon.match({
  462. project_id: this.project_id,
  463. doc_id: this.doc_id,
  464. err: this.err
  465. }),
  466. 'archiving a single doc in the background failed'
  467. )
  468. })
  469. it('should not fail the delete process', function () {
  470. this.callback.should.have.been.calledWith(null)
  471. })
  472. })
  473. })
  474. })
  475. return describe('when the doc does not exist', function () {
  476. beforeEach(function () {
  477. this.DocManager.checkDocExists = sinon
  478. .stub()
  479. .callsArgWith(2, null, false)
  480. return this.DocManager.deleteDoc(
  481. this.project_id,
  482. this.doc_id,
  483. this.callback
  484. )
  485. })
  486. return it('should return a NotFoundError', function () {
  487. return this.callback
  488. .calledWith(
  489. sinon.match.has(
  490. 'message',
  491. `No such project/doc to delete: ${this.project_id}/${this.doc_id}`
  492. )
  493. )
  494. .should.equal(true)
  495. })
  496. })
  497. })
  498. describe('patchDoc', function () {
  499. describe('when the doc exists', function () {
  500. beforeEach(function () {
  501. this.lines = ['mock', 'doc', 'lines']
  502. this.rev = 77
  503. this.MongoManager.findDoc = sinon
  504. .stub()
  505. .yields(null, { _id: ObjectId(this.doc_id) })
  506. this.MongoManager.patchDoc = sinon.stub().yields(null)
  507. this.DocArchiveManager.archiveDocById = sinon.stub().yields(null)
  508. this.meta = {}
  509. })
  510. describe('standard path', function () {
  511. beforeEach(function (done) {
  512. this.callback = sinon.stub().callsFake(done)
  513. this.DocManager.patchDoc(
  514. this.project_id,
  515. this.doc_id,
  516. this.meta,
  517. this.callback
  518. )
  519. })
  520. it('should get the doc', function () {
  521. expect(this.MongoManager.findDoc).to.have.been.calledWith(
  522. this.project_id,
  523. this.doc_id
  524. )
  525. })
  526. it('should persist the meta', function () {
  527. expect(this.MongoManager.patchDoc).to.have.been.calledWith(
  528. this.project_id,
  529. this.doc_id,
  530. this.meta
  531. )
  532. })
  533. it('should return the callback', function () {
  534. expect(this.callback).to.have.been.calledWith(null)
  535. })
  536. })
  537. describe('background flush disabled and deleting a doc', function () {
  538. beforeEach(function (done) {
  539. this.settings.docstore.archiveOnSoftDelete = false
  540. this.meta.deleted = true
  541. this.callback = sinon.stub().callsFake(done)
  542. this.DocManager.patchDoc(
  543. this.project_id,
  544. this.doc_id,
  545. this.meta,
  546. this.callback
  547. )
  548. })
  549. it('should not flush the doc out of mongo', function () {
  550. expect(this.DocArchiveManager.archiveDocById).to.not.have.been.called
  551. })
  552. })
  553. describe('background flush enabled and not deleting a doc', function () {
  554. beforeEach(function (done) {
  555. this.settings.docstore.archiveOnSoftDelete = false
  556. this.meta.deleted = false
  557. this.callback = sinon.stub().callsFake(done)
  558. this.DocManager.patchDoc(
  559. this.project_id,
  560. this.doc_id,
  561. this.meta,
  562. this.callback
  563. )
  564. })
  565. it('should not flush the doc out of mongo', function () {
  566. expect(this.DocArchiveManager.archiveDocById).to.not.have.been.called
  567. })
  568. })
  569. describe('background flush enabled and deleting a doc', function () {
  570. beforeEach(function () {
  571. this.settings.docstore.archiveOnSoftDelete = true
  572. this.meta.deleted = true
  573. this.logger.warn = sinon.stub()
  574. })
  575. describe('when the background flush succeeds', function () {
  576. beforeEach(function (done) {
  577. this.DocArchiveManager.archiveDocById = sinon.stub().yields(null)
  578. this.callback = sinon.stub().callsFake(done)
  579. this.DocManager.patchDoc(
  580. this.project_id,
  581. this.doc_id,
  582. this.meta,
  583. this.callback
  584. )
  585. })
  586. it('should not log a warning', function () {
  587. expect(this.logger.warn).to.not.have.been.called
  588. })
  589. it('should flush the doc out of mongo', function () {
  590. expect(
  591. this.DocArchiveManager.archiveDocById
  592. ).to.have.been.calledWith(this.project_id, this.doc_id)
  593. })
  594. })
  595. describe('when the background flush fails', function () {
  596. beforeEach(function (done) {
  597. this.err = new Error('foo')
  598. this.DocArchiveManager.archiveDocById = sinon
  599. .stub()
  600. .yields(this.err)
  601. this.callback = sinon.stub().callsFake(done)
  602. this.DocManager.patchDoc(
  603. this.project_id,
  604. this.doc_id,
  605. this.meta,
  606. this.callback
  607. )
  608. })
  609. it('should log a warning', function () {
  610. expect(this.logger.warn).to.have.been.calledWith(
  611. sinon.match({
  612. project_id: this.project_id,
  613. doc_id: this.doc_id,
  614. err: this.err
  615. }),
  616. 'archiving a single doc in the background failed'
  617. )
  618. })
  619. it('should not fail the delete process', function () {
  620. expect(this.callback).to.have.been.calledWith(null)
  621. })
  622. })
  623. })
  624. })
  625. describe('when the doc is already deleted', function () {
  626. beforeEach(function (done) {
  627. this.MongoManager.findDoc = sinon
  628. .stub()
  629. .yields(null, { _id: ObjectId(this.doc_id), deleted: true })
  630. this.MongoManager.patchDoc = sinon.stub()
  631. this.callback = sinon.stub().callsFake(() => done())
  632. this.DocManager.patchDoc(
  633. this.project_id,
  634. this.doc_id,
  635. 'tomato.tex',
  636. this.callback
  637. )
  638. })
  639. it('should reject the operation', function () {
  640. expect(this.callback).to.have.been.calledWith(
  641. sinon.match.has('message', 'Cannot PATCH after doc deletion')
  642. )
  643. })
  644. it('should not persist the change to mongo', function () {
  645. expect(this.MongoManager.patchDoc).to.not.have.been.called
  646. })
  647. })
  648. describe('when the doc does not exist', function () {
  649. beforeEach(function () {
  650. this.MongoManager.findDoc = sinon.stub().yields(null)
  651. this.DocManager.patchDoc(
  652. this.project_id,
  653. this.doc_id,
  654. {},
  655. this.callback
  656. )
  657. })
  658. it('should return a NotFoundError', function () {
  659. expect(this.callback).to.have.been.calledWith(
  660. sinon.match.has(
  661. 'message',
  662. `No such project/doc to delete: ${this.project_id}/${this.doc_id}`
  663. )
  664. )
  665. })
  666. })
  667. })
  668. return describe('updateDoc', function () {
  669. beforeEach(function () {
  670. this.oldDocLines = ['old', 'doc', 'lines']
  671. this.newDocLines = ['new', 'doc', 'lines']
  672. this.originalRanges = {
  673. changes: [
  674. {
  675. id: ObjectId().toString(),
  676. op: { i: 'foo', p: 3 },
  677. meta: {
  678. user_id: ObjectId().toString(),
  679. ts: new Date().toString()
  680. }
  681. }
  682. ]
  683. }
  684. this.newRanges = {
  685. changes: [
  686. {
  687. id: ObjectId().toString(),
  688. op: { i: 'bar', p: 6 },
  689. meta: {
  690. user_id: ObjectId().toString(),
  691. ts: new Date().toString()
  692. }
  693. }
  694. ]
  695. }
  696. this.version = 42
  697. this.doc = {
  698. _id: this.doc_id,
  699. project_id: this.project_id,
  700. lines: this.oldDocLines,
  701. rev: (this.rev = 5),
  702. version: this.version,
  703. ranges: this.originalRanges
  704. }
  705. this.MongoManager.upsertIntoDocCollection = sinon.stub().callsArg(3)
  706. this.MongoManager.setDocVersion = sinon.stub().yields()
  707. return (this.DocManager._getDoc = sinon.stub())
  708. })
  709. describe('when only the doc lines have changed', function () {
  710. beforeEach(function () {
  711. this.DocManager._getDoc = sinon.stub().callsArgWith(3, null, this.doc)
  712. return this.DocManager.updateDoc(
  713. this.project_id,
  714. this.doc_id,
  715. this.newDocLines,
  716. this.version,
  717. this.originalRanges,
  718. this.callback
  719. )
  720. })
  721. it('should get the existing doc', function () {
  722. return this.DocManager._getDoc
  723. .calledWith(this.project_id, this.doc_id, {
  724. version: true,
  725. rev: true,
  726. lines: true,
  727. version: true,
  728. ranges: true,
  729. inS3: true
  730. })
  731. .should.equal(true)
  732. })
  733. it('should upsert the document to the doc collection', function () {
  734. return this.MongoManager.upsertIntoDocCollection
  735. .calledWith(this.project_id, this.doc_id, { lines: this.newDocLines })
  736. .should.equal(true)
  737. })
  738. it('should not update the version', function () {
  739. return this.MongoManager.setDocVersion.called.should.equal(false)
  740. })
  741. return it('should return the callback with the new rev', function () {
  742. return this.callback
  743. .calledWith(null, true, this.rev + 1)
  744. .should.equal(true)
  745. })
  746. })
  747. describe('when the doc ranges have changed', function () {
  748. beforeEach(function () {
  749. this.DocManager._getDoc = sinon.stub().callsArgWith(3, null, this.doc)
  750. this.RangeManager.shouldUpdateRanges.returns(true)
  751. return this.DocManager.updateDoc(
  752. this.project_id,
  753. this.doc_id,
  754. this.oldDocLines,
  755. this.version,
  756. this.newRanges,
  757. this.callback
  758. )
  759. })
  760. it('should upsert the ranges', function () {
  761. return this.MongoManager.upsertIntoDocCollection
  762. .calledWith(this.project_id, this.doc_id, { ranges: this.newRanges })
  763. .should.equal(true)
  764. })
  765. it('should not update the version', function () {
  766. return this.MongoManager.setDocVersion.called.should.equal(false)
  767. })
  768. return it('should return the callback with the new rev', function () {
  769. return this.callback
  770. .calledWith(null, true, this.rev + 1)
  771. .should.equal(true)
  772. })
  773. })
  774. describe('when only the version has changed', function () {
  775. beforeEach(function () {
  776. this.DocManager._getDoc = sinon.stub().callsArgWith(3, null, this.doc)
  777. return this.DocManager.updateDoc(
  778. this.project_id,
  779. this.doc_id,
  780. this.oldDocLines,
  781. this.version + 1,
  782. this.originalRanges,
  783. this.callback
  784. )
  785. })
  786. it('should not change the lines or ranges', function () {
  787. return this.MongoManager.upsertIntoDocCollection.called.should.equal(
  788. false
  789. )
  790. })
  791. it('should update the version', function () {
  792. return this.MongoManager.setDocVersion
  793. .calledWith(this.doc_id, this.version + 1)
  794. .should.equal(true)
  795. })
  796. return it('should return the callback with the old rev', function () {
  797. return this.callback.calledWith(null, true, this.rev).should.equal(true)
  798. })
  799. })
  800. describe('when the doc has not changed at all', function () {
  801. beforeEach(function () {
  802. this.DocManager._getDoc = sinon.stub().callsArgWith(3, null, this.doc)
  803. return this.DocManager.updateDoc(
  804. this.project_id,
  805. this.doc_id,
  806. this.oldDocLines,
  807. this.version,
  808. this.originalRanges,
  809. this.callback
  810. )
  811. })
  812. it('should not update the ranges or lines', function () {
  813. return this.MongoManager.upsertIntoDocCollection.called.should.equal(
  814. false
  815. )
  816. })
  817. it('should not update the version', function () {
  818. return this.MongoManager.setDocVersion.called.should.equal(false)
  819. })
  820. return it('should return the callback with the old rev and modified == false', function () {
  821. return this.callback
  822. .calledWith(null, false, this.rev)
  823. .should.equal(true)
  824. })
  825. })
  826. describe('when the version is null', function () {
  827. beforeEach(function () {
  828. return this.DocManager.updateDoc(
  829. this.project_id,
  830. this.doc_id,
  831. this.newDocLines,
  832. null,
  833. this.originalRanges,
  834. this.callback
  835. )
  836. })
  837. return it('should return an error', function () {
  838. return this.callback
  839. .calledWith(
  840. sinon.match.has('message', 'no lines, version or ranges provided')
  841. )
  842. .should.equal(true)
  843. })
  844. })
  845. describe('when the lines are null', function () {
  846. beforeEach(function () {
  847. return this.DocManager.updateDoc(
  848. this.project_id,
  849. this.doc_id,
  850. null,
  851. this.version,
  852. this.originalRanges,
  853. this.callback
  854. )
  855. })
  856. return it('should return an error', function () {
  857. return this.callback
  858. .calledWith(
  859. sinon.match.has('message', 'no lines, version or ranges provided')
  860. )
  861. .should.equal(true)
  862. })
  863. })
  864. describe('when the ranges are null', function () {
  865. beforeEach(function () {
  866. return this.DocManager.updateDoc(
  867. this.project_id,
  868. this.doc_id,
  869. this.newDocLines,
  870. this.version,
  871. null,
  872. this.callback
  873. )
  874. })
  875. return it('should return an error', function () {
  876. return this.callback
  877. .calledWith(
  878. sinon.match.has('message', 'no lines, version or ranges provided')
  879. )
  880. .should.equal(true)
  881. })
  882. })
  883. describe('when there is a generic error getting the doc', function () {
  884. beforeEach(function () {
  885. this.error = new Error('doc could not be found')
  886. this.DocManager._getDoc = sinon
  887. .stub()
  888. .callsArgWith(3, this.error, null, null)
  889. return this.DocManager.updateDoc(
  890. this.project_id,
  891. this.doc_id,
  892. this.newDocLines,
  893. this.version,
  894. this.originalRanges,
  895. this.callback
  896. )
  897. })
  898. it('should not upsert the document to the doc collection', function () {
  899. return this.MongoManager.upsertIntoDocCollection.called.should.equal(
  900. false
  901. )
  902. })
  903. return it('should return the callback with the error', function () {
  904. return this.callback.calledWith(this.error).should.equal(true)
  905. })
  906. })
  907. describe('when the doc lines have not changed', function () {
  908. beforeEach(function () {
  909. this.DocManager._getDoc = sinon.stub().callsArgWith(3, null, this.doc)
  910. return this.DocManager.updateDoc(
  911. this.project_id,
  912. this.doc_id,
  913. this.oldDocLines.slice(),
  914. this.version,
  915. this.originalRanges,
  916. this.callback
  917. )
  918. })
  919. it('should not update the doc', function () {
  920. return this.MongoManager.upsertIntoDocCollection.called.should.equal(
  921. false
  922. )
  923. })
  924. return it('should return the callback with the existing rev', function () {
  925. return this.callback
  926. .calledWith(null, false, this.rev)
  927. .should.equal(true)
  928. })
  929. })
  930. return describe('when the doc does not exist', function () {
  931. beforeEach(function () {
  932. this.DocManager._getDoc = sinon.stub().callsArgWith(3, null, null, null)
  933. return this.DocManager.updateDoc(
  934. this.project_id,
  935. this.doc_id,
  936. this.newDocLines,
  937. this.version,
  938. this.originalRanges,
  939. this.callback
  940. )
  941. })
  942. it('should upsert the document to the doc collection', function () {
  943. return this.MongoManager.upsertIntoDocCollection
  944. .calledWith(this.project_id, this.doc_id, {
  945. lines: this.newDocLines,
  946. ranges: this.originalRanges
  947. })
  948. .should.equal(true)
  949. })
  950. it('should set the version', function () {
  951. return this.MongoManager.setDocVersion
  952. .calledWith(this.doc_id, this.version)
  953. .should.equal(true)
  954. })
  955. return it('should return the callback with the new rev', function () {
  956. return this.callback.calledWith(null, true, 1).should.equal(true)
  957. })
  958. })
  959. })
  960. })