HttpControllerTests.js 15 KB

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