HttpControllerTests.js 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578
  1. const SandboxedModule = require('sandboxed-module')
  2. const sinon = require('sinon')
  3. const { assert, expect } = require('chai')
  4. const modulePath = require('path').join(
  5. __dirname,
  6. '../../../app/js/HttpController'
  7. )
  8. const { ObjectId } = require('mongodb-legacy')
  9. const Errors = require('../../../app/js/Errors')
  10. describe('HttpController', function () {
  11. beforeEach(function () {
  12. const settings = {
  13. max_doc_length: 2 * 1024 * 1024,
  14. }
  15. this.DocArchiveManager = {
  16. unArchiveAllDocs: sinon.stub().yields(),
  17. }
  18. this.DocManager = {}
  19. this.HttpController = SandboxedModule.require(modulePath, {
  20. requires: {
  21. './DocManager': this.DocManager,
  22. './DocArchiveManager': this.DocArchiveManager,
  23. '@overleaf/settings': settings,
  24. './HealthChecker': {},
  25. './Errors': Errors,
  26. },
  27. })
  28. this.res = {
  29. send: sinon.stub(),
  30. sendStatus: sinon.stub(),
  31. json: sinon.stub(),
  32. setHeader: sinon.stub(),
  33. }
  34. this.res.status = sinon.stub().returns(this.res)
  35. this.req = { query: {} }
  36. this.next = sinon.stub()
  37. this.projectId = 'mock-project-id'
  38. this.docId = 'mock-doc-id'
  39. this.doc = {
  40. _id: this.docId,
  41. lines: ['mock', 'lines', ' here', '', '', ' spaces '],
  42. version: 42,
  43. rev: 5,
  44. }
  45. this.deletedDoc = {
  46. deleted: true,
  47. _id: this.docId,
  48. lines: ['mock', 'lines', ' here', '', '', ' spaces '],
  49. version: 42,
  50. rev: 5,
  51. }
  52. })
  53. describe('getDoc', function () {
  54. describe('without deleted docs', function () {
  55. beforeEach(function () {
  56. this.req.params = {
  57. project_id: this.projectId,
  58. doc_id: this.docId,
  59. }
  60. this.DocManager.getFullDoc = sinon
  61. .stub()
  62. .callsArgWith(2, null, this.doc)
  63. this.HttpController.getDoc(this.req, this.res, this.next)
  64. })
  65. it('should get the document with the version (including deleted)', function () {
  66. this.DocManager.getFullDoc
  67. .calledWith(this.projectId, this.docId)
  68. .should.equal(true)
  69. })
  70. it('should return the doc as JSON', function () {
  71. this.res.json
  72. .calledWith({
  73. _id: this.docId,
  74. lines: this.doc.lines,
  75. rev: this.doc.rev,
  76. version: this.doc.version,
  77. })
  78. .should.equal(true)
  79. })
  80. })
  81. describe('which is deleted', function () {
  82. beforeEach(function () {
  83. this.req.params = {
  84. project_id: this.projectId,
  85. doc_id: this.docId,
  86. }
  87. this.DocManager.getFullDoc = sinon
  88. .stub()
  89. .callsArgWith(2, null, this.deletedDoc)
  90. })
  91. it('should get the doc from the doc manager', function () {
  92. this.HttpController.getDoc(this.req, this.res, this.next)
  93. this.DocManager.getFullDoc
  94. .calledWith(this.projectId, this.docId)
  95. .should.equal(true)
  96. })
  97. it('should return 404 if the query string delete is not set ', function () {
  98. this.HttpController.getDoc(this.req, this.res, this.next)
  99. this.res.sendStatus.calledWith(404).should.equal(true)
  100. })
  101. it('should return the doc as JSON if include_deleted is set to true', function () {
  102. this.req.query.include_deleted = 'true'
  103. this.HttpController.getDoc(this.req, this.res, this.next)
  104. this.res.json
  105. .calledWith({
  106. _id: this.docId,
  107. lines: this.doc.lines,
  108. rev: this.doc.rev,
  109. deleted: true,
  110. version: this.doc.version,
  111. })
  112. .should.equal(true)
  113. })
  114. })
  115. })
  116. describe('getRawDoc', function () {
  117. beforeEach(function () {
  118. this.req.params = {
  119. project_id: this.projectId,
  120. doc_id: this.docId,
  121. }
  122. this.DocManager.getDocLines = sinon.stub().callsArgWith(2, null, this.doc)
  123. this.HttpController.getRawDoc(this.req, this.res, this.next)
  124. })
  125. it('should get the document without the version', function () {
  126. this.DocManager.getDocLines
  127. .calledWith(this.projectId, this.docId)
  128. .should.equal(true)
  129. })
  130. it('should set the content type header', function () {
  131. this.res.setHeader
  132. .calledWith('content-type', 'text/plain')
  133. .should.equal(true)
  134. })
  135. it('should send the raw version of the doc', function () {
  136. assert.deepEqual(
  137. this.res.send.args[0][0],
  138. `${this.doc.lines[0]}\n${this.doc.lines[1]}\n${this.doc.lines[2]}\n${this.doc.lines[3]}\n${this.doc.lines[4]}\n${this.doc.lines[5]}`
  139. )
  140. })
  141. })
  142. describe('getAllDocs', function () {
  143. describe('normally', function () {
  144. beforeEach(function () {
  145. this.req.params = { project_id: this.projectId }
  146. this.docs = [
  147. {
  148. _id: new ObjectId(),
  149. lines: ['mock', 'lines', 'one'],
  150. rev: 2,
  151. },
  152. {
  153. _id: new ObjectId(),
  154. lines: ['mock', 'lines', 'two'],
  155. rev: 4,
  156. },
  157. ]
  158. this.DocManager.getAllNonDeletedDocs = sinon
  159. .stub()
  160. .callsArgWith(2, null, this.docs)
  161. this.HttpController.getAllDocs(this.req, this.res, this.next)
  162. })
  163. it('should get all the (non-deleted) docs', function () {
  164. this.DocManager.getAllNonDeletedDocs
  165. .calledWith(this.projectId, { lines: true, rev: true })
  166. .should.equal(true)
  167. })
  168. it('should return the doc as JSON', function () {
  169. this.res.json
  170. .calledWith([
  171. {
  172. _id: this.docs[0]._id.toString(),
  173. lines: this.docs[0].lines,
  174. rev: this.docs[0].rev,
  175. },
  176. {
  177. _id: this.docs[1]._id.toString(),
  178. lines: this.docs[1].lines,
  179. rev: this.docs[1].rev,
  180. },
  181. ])
  182. .should.equal(true)
  183. })
  184. })
  185. describe('with null lines', function () {
  186. beforeEach(function () {
  187. this.req.params = { project_id: this.projectId }
  188. this.docs = [
  189. {
  190. _id: new ObjectId(),
  191. lines: null,
  192. rev: 2,
  193. },
  194. {
  195. _id: new ObjectId(),
  196. lines: ['mock', 'lines', 'two'],
  197. rev: 4,
  198. },
  199. ]
  200. this.DocManager.getAllNonDeletedDocs = sinon
  201. .stub()
  202. .callsArgWith(2, null, this.docs)
  203. this.HttpController.getAllDocs(this.req, this.res, this.next)
  204. })
  205. it('should return the doc with fallback lines', function () {
  206. this.res.json
  207. .calledWith([
  208. {
  209. _id: this.docs[0]._id.toString(),
  210. lines: [],
  211. rev: this.docs[0].rev,
  212. },
  213. {
  214. _id: this.docs[1]._id.toString(),
  215. lines: this.docs[1].lines,
  216. rev: this.docs[1].rev,
  217. },
  218. ])
  219. .should.equal(true)
  220. })
  221. })
  222. describe('with a null doc', function () {
  223. beforeEach(function () {
  224. this.req.params = { project_id: this.projectId }
  225. this.docs = [
  226. {
  227. _id: new ObjectId(),
  228. lines: ['mock', 'lines', 'one'],
  229. rev: 2,
  230. },
  231. null,
  232. {
  233. _id: new ObjectId(),
  234. lines: ['mock', 'lines', 'two'],
  235. rev: 4,
  236. },
  237. ]
  238. this.DocManager.getAllNonDeletedDocs = sinon
  239. .stub()
  240. .callsArgWith(2, null, this.docs)
  241. this.HttpController.getAllDocs(this.req, this.res, this.next)
  242. })
  243. it('should return the non null docs as JSON', function () {
  244. this.res.json
  245. .calledWith([
  246. {
  247. _id: this.docs[0]._id.toString(),
  248. lines: this.docs[0].lines,
  249. rev: this.docs[0].rev,
  250. },
  251. {
  252. _id: this.docs[2]._id.toString(),
  253. lines: this.docs[2].lines,
  254. rev: this.docs[2].rev,
  255. },
  256. ])
  257. .should.equal(true)
  258. })
  259. it('should log out an error', function () {
  260. this.logger.error
  261. .calledWith(
  262. {
  263. err: sinon.match.has('message', 'null doc'),
  264. projectId: this.projectId,
  265. },
  266. 'encountered null doc'
  267. )
  268. .should.equal(true)
  269. })
  270. })
  271. })
  272. describe('getAllRanges', function () {
  273. describe('normally', function () {
  274. beforeEach(function () {
  275. this.req.params = { project_id: this.projectId }
  276. this.docs = [
  277. {
  278. _id: new ObjectId(),
  279. ranges: { mock_ranges: 'one' },
  280. },
  281. {
  282. _id: new ObjectId(),
  283. ranges: { mock_ranges: 'two' },
  284. },
  285. ]
  286. this.DocManager.getAllNonDeletedDocs = sinon
  287. .stub()
  288. .callsArgWith(2, null, this.docs)
  289. this.HttpController.getAllRanges(this.req, this.res, this.next)
  290. })
  291. it('should get all the (non-deleted) doc ranges', function () {
  292. this.DocManager.getAllNonDeletedDocs
  293. .calledWith(this.projectId, { ranges: true })
  294. .should.equal(true)
  295. })
  296. it('should return the doc as JSON', function () {
  297. this.res.json
  298. .calledWith([
  299. {
  300. _id: this.docs[0]._id.toString(),
  301. ranges: this.docs[0].ranges,
  302. },
  303. {
  304. _id: this.docs[1]._id.toString(),
  305. ranges: this.docs[1].ranges,
  306. },
  307. ])
  308. .should.equal(true)
  309. })
  310. })
  311. })
  312. describe('updateDoc', function () {
  313. beforeEach(function () {
  314. this.req.params = {
  315. project_id: this.projectId,
  316. doc_id: this.docId,
  317. }
  318. })
  319. describe('when the doc lines exist and were updated', function () {
  320. beforeEach(function () {
  321. this.req.body = {
  322. lines: (this.lines = ['hello', 'world']),
  323. version: (this.version = 42),
  324. ranges: (this.ranges = { changes: 'mock' }),
  325. }
  326. this.DocManager.updateDoc = sinon
  327. .stub()
  328. .yields(null, true, (this.rev = 5))
  329. this.HttpController.updateDoc(this.req, this.res, this.next)
  330. })
  331. it('should update the document', function () {
  332. this.DocManager.updateDoc
  333. .calledWith(
  334. this.projectId,
  335. this.docId,
  336. this.lines,
  337. this.version,
  338. this.ranges
  339. )
  340. .should.equal(true)
  341. })
  342. it('should return a modified status', function () {
  343. this.res.json
  344. .calledWith({ modified: true, rev: this.rev })
  345. .should.equal(true)
  346. })
  347. })
  348. describe('when the doc lines exist and were not updated', function () {
  349. beforeEach(function () {
  350. this.req.body = {
  351. lines: (this.lines = ['hello', 'world']),
  352. version: (this.version = 42),
  353. ranges: {},
  354. }
  355. this.DocManager.updateDoc = sinon
  356. .stub()
  357. .yields(null, false, (this.rev = 5))
  358. this.HttpController.updateDoc(this.req, this.res, this.next)
  359. })
  360. it('should return a modified status', function () {
  361. this.res.json
  362. .calledWith({ modified: false, rev: this.rev })
  363. .should.equal(true)
  364. })
  365. })
  366. describe('when the doc lines are not provided', function () {
  367. beforeEach(function () {
  368. this.req.body = { version: 42, ranges: {} }
  369. this.DocManager.updateDoc = sinon.stub().yields(null, false)
  370. this.HttpController.updateDoc(this.req, this.res, this.next)
  371. })
  372. it('should not update the document', function () {
  373. this.DocManager.updateDoc.called.should.equal(false)
  374. })
  375. it('should return a 400 (bad request) response', function () {
  376. this.res.sendStatus.calledWith(400).should.equal(true)
  377. })
  378. })
  379. describe('when the doc version are not provided', function () {
  380. beforeEach(function () {
  381. this.req.body = { version: 42, lines: ['hello world'] }
  382. this.DocManager.updateDoc = sinon.stub().yields(null, false)
  383. this.HttpController.updateDoc(this.req, this.res, this.next)
  384. })
  385. it('should not update the document', function () {
  386. this.DocManager.updateDoc.called.should.equal(false)
  387. })
  388. it('should return a 400 (bad request) response', function () {
  389. this.res.sendStatus.calledWith(400).should.equal(true)
  390. })
  391. })
  392. describe('when the doc ranges is not provided', function () {
  393. beforeEach(function () {
  394. this.req.body = { lines: ['foo'], version: 42 }
  395. this.DocManager.updateDoc = sinon.stub().yields(null, false)
  396. this.HttpController.updateDoc(this.req, this.res, this.next)
  397. })
  398. it('should not update the document', function () {
  399. this.DocManager.updateDoc.called.should.equal(false)
  400. })
  401. it('should return a 400 (bad request) response', function () {
  402. this.res.sendStatus.calledWith(400).should.equal(true)
  403. })
  404. })
  405. describe('when the doc body is too large', function () {
  406. beforeEach(function () {
  407. this.req.body = {
  408. lines: (this.lines = Array(2049).fill('a'.repeat(1024))),
  409. version: (this.version = 42),
  410. ranges: (this.ranges = { changes: 'mock' }),
  411. }
  412. this.HttpController.updateDoc(this.req, this.res, this.next)
  413. })
  414. it('should return a 413 (too large) response', function () {
  415. sinon.assert.calledWith(this.res.status, 413)
  416. })
  417. it('should report that the document body is too large', function () {
  418. sinon.assert.calledWith(this.res.send, 'document body too large')
  419. })
  420. })
  421. })
  422. describe('patchDoc', function () {
  423. beforeEach(function () {
  424. this.req.params = {
  425. project_id: this.projectId,
  426. doc_id: this.docId,
  427. }
  428. this.req.body = { name: 'foo.tex' }
  429. this.DocManager.patchDoc = sinon.stub().yields(null)
  430. this.HttpController.patchDoc(this.req, this.res, this.next)
  431. })
  432. it('should delete the document', function () {
  433. expect(this.DocManager.patchDoc).to.have.been.calledWith(
  434. this.projectId,
  435. this.docId
  436. )
  437. })
  438. it('should return a 204 (No Content)', function () {
  439. expect(this.res.sendStatus).to.have.been.calledWith(204)
  440. })
  441. describe('with an invalid payload', function () {
  442. beforeEach(function () {
  443. this.req.body = { cannot: 'happen' }
  444. this.DocManager.patchDoc = sinon.stub().yields(null)
  445. this.HttpController.patchDoc(this.req, this.res, this.next)
  446. })
  447. it('should log a message', function () {
  448. expect(this.logger.fatal).to.have.been.calledWith(
  449. { field: 'cannot' },
  450. 'joi validation for pathDoc is broken'
  451. )
  452. })
  453. it('should not pass the invalid field along', function () {
  454. expect(this.DocManager.patchDoc).to.have.been.calledWith(
  455. this.projectId,
  456. this.docId,
  457. {}
  458. )
  459. })
  460. })
  461. })
  462. describe('archiveAllDocs', function () {
  463. beforeEach(function () {
  464. this.req.params = { project_id: this.projectId }
  465. this.DocArchiveManager.archiveAllDocs = sinon.stub().callsArg(1)
  466. this.HttpController.archiveAllDocs(this.req, this.res, this.next)
  467. })
  468. it('should archive the project', function () {
  469. this.DocArchiveManager.archiveAllDocs
  470. .calledWith(this.projectId)
  471. .should.equal(true)
  472. })
  473. it('should return a 204 (No Content)', function () {
  474. this.res.sendStatus.calledWith(204).should.equal(true)
  475. })
  476. })
  477. describe('unArchiveAllDocs', function () {
  478. beforeEach(function () {
  479. this.req.params = { project_id: this.projectId }
  480. })
  481. describe('on success', function () {
  482. beforeEach(function (done) {
  483. this.res.sendStatus.callsFake(() => done())
  484. this.HttpController.unArchiveAllDocs(this.req, this.res, this.next)
  485. })
  486. it('returns a 200', function () {
  487. expect(this.res.sendStatus).to.have.been.calledWith(200)
  488. })
  489. })
  490. describe("when the archived rev doesn't match", function () {
  491. beforeEach(function (done) {
  492. this.res.sendStatus.callsFake(() => done())
  493. this.DocArchiveManager.unArchiveAllDocs.yields(
  494. new Errors.DocRevValueError('bad rev')
  495. )
  496. this.HttpController.unArchiveAllDocs(this.req, this.res, this.next)
  497. })
  498. it('returns a 409', function () {
  499. expect(this.res.sendStatus).to.have.been.calledWith(409)
  500. })
  501. })
  502. })
  503. describe('destroyProject', function () {
  504. beforeEach(function () {
  505. this.req.params = { project_id: this.projectId }
  506. this.DocArchiveManager.destroyProject = sinon.stub().callsArg(1)
  507. this.HttpController.destroyProject(this.req, this.res, this.next)
  508. })
  509. it('should destroy the docs', function () {
  510. sinon.assert.calledWith(
  511. this.DocArchiveManager.destroyProject,
  512. this.projectId
  513. )
  514. })
  515. it('should return 204', function () {
  516. sinon.assert.calledWith(this.res.sendStatus, 204)
  517. })
  518. })
  519. })