HttpControllerTests.js 36 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258
  1. const sinon = require('sinon')
  2. const modulePath = '../../../../app/js/HttpController.js'
  3. const SandboxedModule = require('sandboxed-module')
  4. const Errors = require('../../../../app/js/Errors.js')
  5. describe('HttpController', function () {
  6. beforeEach(function () {
  7. this.HttpController = SandboxedModule.require(modulePath, {
  8. requires: {
  9. './DocumentManager': (this.DocumentManager = {}),
  10. './HistoryManager': (this.HistoryManager = {
  11. flushProjectChangesAsync: sinon.stub(),
  12. }),
  13. './ProjectHistoryRedisManager': (this.ProjectHistoryRedisManager = {}),
  14. './ProjectManager': (this.ProjectManager = {}),
  15. './DeleteQueueManager': (this.DeleteQueueManager = {}),
  16. './RedisManager': (this.RedisManager = {
  17. DOC_OPS_TTL: 42,
  18. }),
  19. './Metrics': (this.Metrics = {}),
  20. './Errors': Errors,
  21. '@overleaf/settings': { max_doc_length: 2 * 1024 * 1024 },
  22. },
  23. })
  24. this.Metrics.Timer = class Timer {}
  25. this.Metrics.Timer.prototype.done = sinon.stub()
  26. this.project_id = 'project-id-123'
  27. this.doc_id = 'doc-id-123'
  28. this.source = 'editor'
  29. this.next = sinon.stub()
  30. this.res = {
  31. send: sinon.stub(),
  32. sendStatus: sinon.stub(),
  33. json: sinon.stub(),
  34. }
  35. })
  36. describe('getDoc', function () {
  37. beforeEach(function () {
  38. this.lines = ['one', 'two', 'three']
  39. this.ops = ['mock-op-1', 'mock-op-2']
  40. this.version = 42
  41. this.fromVersion = 42
  42. this.ranges = { changes: 'mock', comments: 'mock' }
  43. this.pathname = '/a/b/c'
  44. this.req = {
  45. params: {
  46. project_id: this.project_id,
  47. doc_id: this.doc_id,
  48. },
  49. query: {},
  50. body: {},
  51. }
  52. })
  53. describe('when the document exists and no recent ops are requested', function () {
  54. beforeEach(function () {
  55. this.DocumentManager.getDocAndRecentOpsWithLock = sinon
  56. .stub()
  57. .callsArgWith(
  58. 3,
  59. null,
  60. this.lines,
  61. this.version,
  62. [],
  63. this.ranges,
  64. this.pathname
  65. )
  66. this.HttpController.getDoc(this.req, this.res, this.next)
  67. })
  68. it('should get the doc', function () {
  69. this.DocumentManager.getDocAndRecentOpsWithLock
  70. .calledWith(this.project_id, this.doc_id, -1)
  71. .should.equal(true)
  72. })
  73. it('should return the doc as JSON', function () {
  74. this.res.json
  75. .calledWith({
  76. id: this.doc_id,
  77. lines: this.lines,
  78. version: this.version,
  79. ops: [],
  80. ranges: this.ranges,
  81. pathname: this.pathname,
  82. ttlInS: 42,
  83. })
  84. .should.equal(true)
  85. })
  86. it('should log the request', function () {
  87. this.logger.debug
  88. .calledWith(
  89. { docId: this.doc_id, projectId: this.project_id },
  90. 'getting doc via http'
  91. )
  92. .should.equal(true)
  93. })
  94. it('should time the request', function () {
  95. this.Metrics.Timer.prototype.done.called.should.equal(true)
  96. })
  97. })
  98. describe('when recent ops are requested', function () {
  99. beforeEach(function () {
  100. this.DocumentManager.getDocAndRecentOpsWithLock = sinon
  101. .stub()
  102. .callsArgWith(
  103. 3,
  104. null,
  105. this.lines,
  106. this.version,
  107. this.ops,
  108. this.ranges,
  109. this.pathname
  110. )
  111. this.req.query = { fromVersion: `${this.fromVersion}` }
  112. this.HttpController.getDoc(this.req, this.res, this.next)
  113. })
  114. it('should get the doc', function () {
  115. this.DocumentManager.getDocAndRecentOpsWithLock
  116. .calledWith(this.project_id, this.doc_id, this.fromVersion)
  117. .should.equal(true)
  118. })
  119. it('should return the doc as JSON', function () {
  120. this.res.json
  121. .calledWith({
  122. id: this.doc_id,
  123. lines: this.lines,
  124. version: this.version,
  125. ops: this.ops,
  126. ranges: this.ranges,
  127. pathname: this.pathname,
  128. ttlInS: 42,
  129. })
  130. .should.equal(true)
  131. })
  132. it('should log the request', function () {
  133. this.logger.debug
  134. .calledWith(
  135. { docId: this.doc_id, projectId: this.project_id },
  136. 'getting doc via http'
  137. )
  138. .should.equal(true)
  139. })
  140. it('should time the request', function () {
  141. this.Metrics.Timer.prototype.done.called.should.equal(true)
  142. })
  143. })
  144. describe('when the document does not exist', function () {
  145. beforeEach(function () {
  146. this.DocumentManager.getDocAndRecentOpsWithLock = sinon
  147. .stub()
  148. .callsArgWith(3, null, null, null)
  149. this.HttpController.getDoc(this.req, this.res, this.next)
  150. })
  151. it('should call next with NotFoundError', function () {
  152. this.next
  153. .calledWith(sinon.match.instanceOf(Errors.NotFoundError))
  154. .should.equal(true)
  155. })
  156. })
  157. describe('when an errors occurs', function () {
  158. beforeEach(function () {
  159. this.DocumentManager.getDocAndRecentOpsWithLock = sinon
  160. .stub()
  161. .callsArgWith(3, new Error('oops'), null, null)
  162. this.HttpController.getDoc(this.req, this.res, this.next)
  163. })
  164. it('should call next with the error', function () {
  165. this.next.calledWith(sinon.match.instanceOf(Error)).should.equal(true)
  166. })
  167. })
  168. })
  169. describe('getComment', function () {
  170. beforeEach(function () {
  171. this.ranges = {
  172. changes: 'mock',
  173. comments: [
  174. {
  175. id: 'comment-id-1',
  176. },
  177. {
  178. id: 'comment-id-2',
  179. },
  180. ],
  181. }
  182. this.req = {
  183. params: {
  184. project_id: this.project_id,
  185. doc_id: this.doc_id,
  186. comment_id: this.comment_id,
  187. },
  188. query: {},
  189. body: {},
  190. }
  191. })
  192. beforeEach(function () {
  193. this.DocumentManager.getCommentWithLock = sinon
  194. .stub()
  195. .callsArgWith(3, null, this.ranges.comments[0])
  196. this.HttpController.getComment(this.req, this.res, this.next)
  197. })
  198. it('should get the comment', function () {
  199. this.DocumentManager.getCommentWithLock
  200. .calledWith(this.project_id, this.doc_id, this.comment_id)
  201. .should.equal(true)
  202. })
  203. it('should return the comment as JSON', function () {
  204. this.res.json
  205. .calledWith({
  206. id: 'comment-id-1',
  207. })
  208. .should.equal(true)
  209. })
  210. it('should log the request', function () {
  211. this.logger.debug
  212. .calledWith(
  213. {
  214. projectId: this.project_id,
  215. docId: this.doc_id,
  216. commentId: this.comment_id,
  217. },
  218. 'getting comment via http'
  219. )
  220. .should.equal(true)
  221. })
  222. })
  223. describe('setDoc', function () {
  224. beforeEach(function () {
  225. this.lines = ['one', 'two', 'three']
  226. this.source = 'dropbox'
  227. this.user_id = 'user-id-123'
  228. this.req = {
  229. headers: {},
  230. params: {
  231. project_id: this.project_id,
  232. doc_id: this.doc_id,
  233. },
  234. query: {},
  235. body: {
  236. lines: this.lines,
  237. source: this.source,
  238. user_id: this.user_id,
  239. undoing: (this.undoing = true),
  240. },
  241. }
  242. })
  243. describe('successfully', function () {
  244. beforeEach(function () {
  245. this.DocumentManager.setDocWithLock = sinon
  246. .stub()
  247. .callsArgWith(7, null, { rev: '123' })
  248. this.HttpController.setDoc(this.req, this.res, this.next)
  249. })
  250. it('should set the doc', function () {
  251. this.DocumentManager.setDocWithLock
  252. .calledWith(
  253. this.project_id,
  254. this.doc_id,
  255. this.lines,
  256. this.source,
  257. this.user_id,
  258. this.undoing,
  259. true
  260. )
  261. .should.equal(true)
  262. })
  263. it('should return a json response with the document rev from web', function () {
  264. this.res.json.calledWithMatch({ rev: '123' }).should.equal(true)
  265. })
  266. it('should log the request', function () {
  267. this.logger.debug
  268. .calledWith(
  269. {
  270. docId: this.doc_id,
  271. projectId: this.project_id,
  272. lines: this.lines,
  273. source: this.source,
  274. userId: this.user_id,
  275. undoing: this.undoing,
  276. },
  277. 'setting doc via http'
  278. )
  279. .should.equal(true)
  280. })
  281. it('should time the request', function () {
  282. this.Metrics.Timer.prototype.done.called.should.equal(true)
  283. })
  284. })
  285. describe('when an errors occurs', function () {
  286. beforeEach(function () {
  287. this.DocumentManager.setDocWithLock = sinon
  288. .stub()
  289. .callsArgWith(7, new Error('oops'))
  290. this.HttpController.setDoc(this.req, this.res, this.next)
  291. })
  292. it('should call next with the error', function () {
  293. this.next.calledWith(sinon.match.instanceOf(Error)).should.equal(true)
  294. })
  295. })
  296. describe('when the payload is too large', function () {
  297. beforeEach(function () {
  298. const lines = []
  299. for (let _ = 0; _ <= 200000; _++) {
  300. lines.push('test test test')
  301. }
  302. this.req.body.lines = lines
  303. this.DocumentManager.setDocWithLock = sinon.stub().callsArgWith(6)
  304. this.HttpController.setDoc(this.req, this.res, this.next)
  305. })
  306. it('should send back a 406 response', function () {
  307. this.res.sendStatus.calledWith(406).should.equal(true)
  308. })
  309. it('should not call setDocWithLock', function () {
  310. this.DocumentManager.setDocWithLock.callCount.should.equal(0)
  311. })
  312. })
  313. })
  314. describe('flushProject', function () {
  315. beforeEach(function () {
  316. this.req = {
  317. params: {
  318. project_id: this.project_id,
  319. },
  320. query: {},
  321. body: {},
  322. }
  323. })
  324. describe('successfully', function () {
  325. beforeEach(function () {
  326. this.ProjectManager.flushProjectWithLocks = sinon.stub().callsArgWith(1)
  327. this.HttpController.flushProject(this.req, this.res, this.next)
  328. })
  329. it('should flush the project', function () {
  330. this.ProjectManager.flushProjectWithLocks
  331. .calledWith(this.project_id)
  332. .should.equal(true)
  333. })
  334. it('should return a successful No Content response', function () {
  335. this.res.sendStatus.calledWith(204).should.equal(true)
  336. })
  337. it('should log the request', function () {
  338. this.logger.debug
  339. .calledWith(
  340. { projectId: this.project_id },
  341. 'flushing project via http'
  342. )
  343. .should.equal(true)
  344. })
  345. it('should time the request', function () {
  346. this.Metrics.Timer.prototype.done.called.should.equal(true)
  347. })
  348. })
  349. describe('when an errors occurs', function () {
  350. beforeEach(function () {
  351. this.ProjectManager.flushProjectWithLocks = sinon
  352. .stub()
  353. .callsArgWith(1, new Error('oops'))
  354. this.HttpController.flushProject(this.req, this.res, this.next)
  355. })
  356. it('should call next with the error', function () {
  357. this.next.calledWith(sinon.match.instanceOf(Error)).should.equal(true)
  358. })
  359. })
  360. })
  361. describe('flushDocIfLoaded', function () {
  362. beforeEach(function () {
  363. this.lines = ['one', 'two', 'three']
  364. this.version = 42
  365. this.req = {
  366. params: {
  367. project_id: this.project_id,
  368. doc_id: this.doc_id,
  369. },
  370. query: {},
  371. body: {},
  372. }
  373. })
  374. describe('successfully', function () {
  375. beforeEach(function () {
  376. this.DocumentManager.flushDocIfLoadedWithLock = sinon
  377. .stub()
  378. .callsArgWith(2)
  379. this.HttpController.flushDocIfLoaded(this.req, this.res, this.next)
  380. })
  381. it('should flush the doc', function () {
  382. this.DocumentManager.flushDocIfLoadedWithLock
  383. .calledWith(this.project_id, this.doc_id)
  384. .should.equal(true)
  385. })
  386. it('should return a successful No Content response', function () {
  387. this.res.sendStatus.calledWith(204).should.equal(true)
  388. })
  389. it('should log the request', function () {
  390. this.logger.debug
  391. .calledWith(
  392. { docId: this.doc_id, projectId: this.project_id },
  393. 'flushing doc via http'
  394. )
  395. .should.equal(true)
  396. })
  397. it('should time the request', function () {
  398. this.Metrics.Timer.prototype.done.called.should.equal(true)
  399. })
  400. })
  401. describe('when an errors occurs', function () {
  402. beforeEach(function () {
  403. this.DocumentManager.flushDocIfLoadedWithLock = sinon
  404. .stub()
  405. .callsArgWith(2, new Error('oops'))
  406. this.HttpController.flushDocIfLoaded(this.req, this.res, this.next)
  407. })
  408. it('should call next with the error', function () {
  409. this.next.calledWith(sinon.match.instanceOf(Error)).should.equal(true)
  410. })
  411. })
  412. })
  413. describe('deleteDoc', function () {
  414. beforeEach(function () {
  415. this.req = {
  416. params: {
  417. project_id: this.project_id,
  418. doc_id: this.doc_id,
  419. },
  420. query: {},
  421. body: {},
  422. }
  423. })
  424. describe('successfully', function () {
  425. beforeEach(function () {
  426. this.DocumentManager.flushAndDeleteDocWithLock = sinon
  427. .stub()
  428. .callsArgWith(3)
  429. this.HttpController.deleteDoc(this.req, this.res, this.next)
  430. })
  431. it('should flush and delete the doc', function () {
  432. this.DocumentManager.flushAndDeleteDocWithLock
  433. .calledWith(this.project_id, this.doc_id, {
  434. ignoreFlushErrors: false,
  435. })
  436. .should.equal(true)
  437. })
  438. it('should flush project history', function () {
  439. this.HistoryManager.flushProjectChangesAsync
  440. .calledWithExactly(this.project_id)
  441. .should.equal(true)
  442. })
  443. it('should return a successful No Content response', function () {
  444. this.res.sendStatus.calledWith(204).should.equal(true)
  445. })
  446. it('should log the request', function () {
  447. this.logger.debug
  448. .calledWith(
  449. { docId: this.doc_id, projectId: this.project_id },
  450. 'deleting doc via http'
  451. )
  452. .should.equal(true)
  453. })
  454. it('should time the request', function () {
  455. this.Metrics.Timer.prototype.done.called.should.equal(true)
  456. })
  457. })
  458. describe('ignoring errors', function () {
  459. beforeEach(function () {
  460. this.req.query.ignore_flush_errors = 'true'
  461. this.DocumentManager.flushAndDeleteDocWithLock = sinon.stub().yields()
  462. this.HttpController.deleteDoc(this.req, this.res, this.next)
  463. })
  464. it('should delete the doc', function () {
  465. this.DocumentManager.flushAndDeleteDocWithLock
  466. .calledWith(this.project_id, this.doc_id, { ignoreFlushErrors: true })
  467. .should.equal(true)
  468. })
  469. it('should return a successful No Content response', function () {
  470. this.res.sendStatus.calledWith(204).should.equal(true)
  471. })
  472. })
  473. describe('when an errors occurs', function () {
  474. beforeEach(function () {
  475. this.DocumentManager.flushAndDeleteDocWithLock = sinon
  476. .stub()
  477. .callsArgWith(3, new Error('oops'))
  478. this.HttpController.deleteDoc(this.req, this.res, this.next)
  479. })
  480. it('should flush project history', function () {
  481. this.HistoryManager.flushProjectChangesAsync
  482. .calledWithExactly(this.project_id)
  483. .should.equal(true)
  484. })
  485. it('should call next with the error', function () {
  486. this.next.calledWith(sinon.match.instanceOf(Error)).should.equal(true)
  487. })
  488. })
  489. })
  490. describe('deleteProject', function () {
  491. beforeEach(function () {
  492. this.req = {
  493. params: {
  494. project_id: this.project_id,
  495. },
  496. query: {},
  497. body: {},
  498. }
  499. })
  500. describe('successfully', function () {
  501. beforeEach(function () {
  502. this.ProjectManager.flushAndDeleteProjectWithLocks = sinon
  503. .stub()
  504. .callsArgWith(2)
  505. this.HttpController.deleteProject(this.req, this.res, this.next)
  506. })
  507. it('should delete the project', function () {
  508. this.ProjectManager.flushAndDeleteProjectWithLocks
  509. .calledWith(this.project_id)
  510. .should.equal(true)
  511. })
  512. it('should return a successful No Content response', function () {
  513. this.res.sendStatus.calledWith(204).should.equal(true)
  514. })
  515. it('should log the request', function () {
  516. this.logger.debug
  517. .calledWith(
  518. { projectId: this.project_id },
  519. 'deleting project via http'
  520. )
  521. .should.equal(true)
  522. })
  523. it('should time the request', function () {
  524. this.Metrics.Timer.prototype.done.called.should.equal(true)
  525. })
  526. })
  527. describe('with the background=true option from realtime', function () {
  528. beforeEach(function () {
  529. this.ProjectManager.queueFlushAndDeleteProject = sinon
  530. .stub()
  531. .callsArgWith(1)
  532. this.req.query = { background: true, shutdown: true }
  533. this.HttpController.deleteProject(this.req, this.res, this.next)
  534. })
  535. it('should queue the flush and delete', function () {
  536. this.ProjectManager.queueFlushAndDeleteProject
  537. .calledWith(this.project_id)
  538. .should.equal(true)
  539. })
  540. })
  541. describe('when an errors occurs', function () {
  542. beforeEach(function () {
  543. this.ProjectManager.flushAndDeleteProjectWithLocks = sinon
  544. .stub()
  545. .callsArgWith(2, new Error('oops'))
  546. this.HttpController.deleteProject(this.req, this.res, this.next)
  547. })
  548. it('should call next with the error', function () {
  549. this.next.calledWith(sinon.match.instanceOf(Error)).should.equal(true)
  550. })
  551. })
  552. })
  553. describe('acceptChanges', function () {
  554. beforeEach(function () {
  555. this.req = {
  556. params: {
  557. project_id: this.project_id,
  558. doc_id: this.doc_id,
  559. change_id: (this.change_id = 'mock-change-od-1'),
  560. },
  561. query: {},
  562. body: {},
  563. }
  564. })
  565. describe('successfully with a single change', function () {
  566. beforeEach(function () {
  567. this.DocumentManager.acceptChangesWithLock = sinon
  568. .stub()
  569. .callsArgWith(3)
  570. this.HttpController.acceptChanges(this.req, this.res, this.next)
  571. })
  572. it('should accept the change', function () {
  573. this.DocumentManager.acceptChangesWithLock
  574. .calledWith(this.project_id, this.doc_id, [this.change_id])
  575. .should.equal(true)
  576. })
  577. it('should return a successful No Content response', function () {
  578. this.res.sendStatus.calledWith(204).should.equal(true)
  579. })
  580. it('should log the request', function () {
  581. this.logger.debug
  582. .calledWith(
  583. { projectId: this.project_id, docId: this.doc_id },
  584. 'accepting 1 changes via http'
  585. )
  586. .should.equal(true)
  587. })
  588. it('should time the request', function () {
  589. this.Metrics.Timer.prototype.done.called.should.equal(true)
  590. })
  591. })
  592. describe('succesfully with with multiple changes', function () {
  593. beforeEach(function () {
  594. this.change_ids = [
  595. 'mock-change-od-1',
  596. 'mock-change-od-2',
  597. 'mock-change-od-3',
  598. 'mock-change-od-4',
  599. ]
  600. this.req.body = { change_ids: this.change_ids }
  601. this.DocumentManager.acceptChangesWithLock = sinon
  602. .stub()
  603. .callsArgWith(3)
  604. this.HttpController.acceptChanges(this.req, this.res, this.next)
  605. })
  606. it('should accept the changes in the body payload', function () {
  607. this.DocumentManager.acceptChangesWithLock
  608. .calledWith(this.project_id, this.doc_id, this.change_ids)
  609. .should.equal(true)
  610. })
  611. it('should log the request with the correct number of changes', function () {
  612. this.logger.debug
  613. .calledWith(
  614. { projectId: this.project_id, docId: this.doc_id },
  615. `accepting ${this.change_ids.length} changes via http`
  616. )
  617. .should.equal(true)
  618. })
  619. })
  620. describe('when an errors occurs', function () {
  621. beforeEach(function () {
  622. this.DocumentManager.acceptChangesWithLock = sinon
  623. .stub()
  624. .callsArgWith(3, new Error('oops'))
  625. this.HttpController.acceptChanges(this.req, this.res, this.next)
  626. })
  627. it('should call next with the error', function () {
  628. this.next.calledWith(sinon.match.instanceOf(Error)).should.equal(true)
  629. })
  630. })
  631. })
  632. describe('resolveComment', function () {
  633. beforeEach(function () {
  634. this.user_id = 'user-id-123'
  635. this.req = {
  636. params: {
  637. project_id: this.project_id,
  638. doc_id: this.doc_id,
  639. comment_id: (this.comment_id = 'mock-comment-id'),
  640. },
  641. query: {},
  642. body: {
  643. user_id: this.user_id,
  644. },
  645. }
  646. this.resolved = true
  647. })
  648. describe('successfully', function () {
  649. beforeEach(function (done) {
  650. this.DocumentManager.updateCommentStateWithLock = sinon
  651. .stub()
  652. .callsArgWith(5)
  653. this.ProjectHistoryRedisManager.queueOps = sinon.stub()
  654. this.res.sendStatus.callsFake(() => done())
  655. this.HttpController.resolveComment(this.req, this.res, this.next)
  656. })
  657. it('should accept the change', function () {
  658. this.DocumentManager.updateCommentStateWithLock
  659. .calledWith(
  660. this.project_id,
  661. this.doc_id,
  662. this.comment_id,
  663. this.user_id,
  664. this.resolved
  665. )
  666. .should.equal(true)
  667. })
  668. it('should return a successful No Content response', function () {
  669. this.res.sendStatus.calledWith(204).should.equal(true)
  670. })
  671. it('should log the request', function () {
  672. this.logger.debug
  673. .calledWith(
  674. {
  675. projectId: this.project_id,
  676. docId: this.doc_id,
  677. commentId: this.comment_id,
  678. },
  679. 'resolving comment via http'
  680. )
  681. .should.equal(true)
  682. })
  683. })
  684. describe('when an errors occurs', function () {
  685. beforeEach(function () {
  686. this.DocumentManager.updateCommentStateWithLock = sinon
  687. .stub()
  688. .callsArgWith(5, new Error('oops'))
  689. this.HttpController.resolveComment(this.req, this.res, this.next)
  690. })
  691. it('should call next with the error', function () {
  692. this.next.calledWith(sinon.match.instanceOf(Error)).should.equal(true)
  693. })
  694. })
  695. })
  696. describe('reopenComment', function () {
  697. beforeEach(function () {
  698. this.user_id = 'user-id-123'
  699. this.req = {
  700. params: {
  701. project_id: this.project_id,
  702. doc_id: this.doc_id,
  703. comment_id: (this.comment_id = 'mock-comment-id'),
  704. },
  705. query: {},
  706. body: {
  707. user_id: this.user_id,
  708. },
  709. }
  710. this.resolved = false
  711. })
  712. describe('successfully', function () {
  713. beforeEach(function () {
  714. this.DocumentManager.updateCommentStateWithLock = sinon
  715. .stub()
  716. .callsArgWith(5)
  717. this.ProjectHistoryRedisManager.queueOps = sinon.stub()
  718. this.HttpController.reopenComment(this.req, this.res, this.next)
  719. })
  720. it('should accept the change', function () {
  721. this.DocumentManager.updateCommentStateWithLock
  722. .calledWith(
  723. this.project_id,
  724. this.doc_id,
  725. this.comment_id,
  726. this.user_id,
  727. this.resolved
  728. )
  729. .should.equal(true)
  730. })
  731. it('should return a successful No Content response', function () {
  732. this.res.sendStatus.calledWith(204).should.equal(true)
  733. })
  734. it('should log the request', function () {
  735. this.logger.debug
  736. .calledWith(
  737. {
  738. projectId: this.project_id,
  739. docId: this.doc_id,
  740. commentId: this.comment_id,
  741. },
  742. 'reopening comment via http'
  743. )
  744. .should.equal(true)
  745. })
  746. })
  747. describe('when an errors occurs', function () {
  748. beforeEach(function () {
  749. this.DocumentManager.updateCommentStateWithLock = sinon
  750. .stub()
  751. .callsArgWith(5, new Error('oops'))
  752. this.HttpController.reopenComment(this.req, this.res, this.next)
  753. })
  754. it('should call next with the error', function () {
  755. this.next.calledWith(sinon.match.instanceOf(Error)).should.equal(true)
  756. })
  757. })
  758. })
  759. describe('deleteComment', function () {
  760. beforeEach(function () {
  761. this.user_id = 'user-id-123'
  762. this.req = {
  763. params: {
  764. project_id: this.project_id,
  765. doc_id: this.doc_id,
  766. comment_id: (this.comment_id = 'mock-comment-id'),
  767. },
  768. query: {},
  769. body: {
  770. user_id: this.user_id,
  771. },
  772. }
  773. })
  774. describe('successfully', function () {
  775. beforeEach(function () {
  776. this.DocumentManager.deleteCommentWithLock = sinon
  777. .stub()
  778. .callsArgWith(4)
  779. this.ProjectHistoryRedisManager.queueOps = sinon.stub()
  780. this.HttpController.deleteComment(this.req, this.res, this.next)
  781. })
  782. it('should accept the change', function () {
  783. this.DocumentManager.deleteCommentWithLock
  784. .calledWith(
  785. this.project_id,
  786. this.doc_id,
  787. this.comment_id,
  788. this.user_id
  789. )
  790. .should.equal(true)
  791. })
  792. it('should return a successful No Content response', function () {
  793. this.res.sendStatus.calledWith(204).should.equal(true)
  794. })
  795. it('should log the request', function () {
  796. this.logger.debug
  797. .calledWith(
  798. {
  799. projectId: this.project_id,
  800. docId: this.doc_id,
  801. commentId: this.comment_id,
  802. },
  803. 'deleting comment via http'
  804. )
  805. .should.equal(true)
  806. })
  807. it('should time the request', function () {
  808. this.Metrics.Timer.prototype.done.called.should.equal(true)
  809. })
  810. })
  811. describe('when an errors occurs', function () {
  812. beforeEach(function () {
  813. this.DocumentManager.deleteCommentWithLock = sinon
  814. .stub()
  815. .callsArgWith(4, new Error('oops'))
  816. this.HttpController.deleteComment(this.req, this.res, this.next)
  817. })
  818. it('should call next with the error', function () {
  819. this.next.calledWith(sinon.match.instanceOf(Error)).should.equal(true)
  820. })
  821. })
  822. })
  823. describe('getProjectDocsAndFlushIfOld', function () {
  824. beforeEach(function () {
  825. this.state = '01234567890abcdef'
  826. this.docs = [
  827. { _id: '1234', lines: 'hello', v: 23 },
  828. { _id: '4567', lines: 'world', v: 45 },
  829. ]
  830. this.req = {
  831. params: {
  832. project_id: this.project_id,
  833. },
  834. query: {
  835. state: this.state,
  836. },
  837. body: {},
  838. }
  839. })
  840. describe('successfully', function () {
  841. beforeEach(function () {
  842. this.ProjectManager.getProjectDocsAndFlushIfOld = sinon
  843. .stub()
  844. .callsArgWith(3, null, this.docs)
  845. this.HttpController.getProjectDocsAndFlushIfOld(
  846. this.req,
  847. this.res,
  848. this.next
  849. )
  850. })
  851. it('should get docs from the project manager', function () {
  852. this.ProjectManager.getProjectDocsAndFlushIfOld
  853. .calledWith(this.project_id, this.state, {})
  854. .should.equal(true)
  855. })
  856. it('should return a successful response', function () {
  857. this.res.send.calledWith(this.docs).should.equal(true)
  858. })
  859. it('should log the request', function () {
  860. this.logger.debug
  861. .calledWith(
  862. { projectId: this.project_id, exclude: [] },
  863. 'getting docs via http'
  864. )
  865. .should.equal(true)
  866. })
  867. it('should log the response', function () {
  868. this.logger.debug
  869. .calledWith(
  870. { projectId: this.project_id, result: ['1234:23', '4567:45'] },
  871. 'got docs via http'
  872. )
  873. .should.equal(true)
  874. })
  875. it('should time the request', function () {
  876. this.Metrics.Timer.prototype.done.called.should.equal(true)
  877. })
  878. })
  879. describe('when there is a conflict', function () {
  880. beforeEach(function () {
  881. this.ProjectManager.getProjectDocsAndFlushIfOld = sinon
  882. .stub()
  883. .callsArgWith(
  884. 3,
  885. new Errors.ProjectStateChangedError('project state changed')
  886. )
  887. this.HttpController.getProjectDocsAndFlushIfOld(
  888. this.req,
  889. this.res,
  890. this.next
  891. )
  892. })
  893. it('should return an HTTP 409 Conflict response', function () {
  894. this.res.sendStatus.calledWith(409).should.equal(true)
  895. })
  896. })
  897. describe('when an error occurs', function () {
  898. beforeEach(function () {
  899. this.ProjectManager.getProjectDocsAndFlushIfOld = sinon
  900. .stub()
  901. .callsArgWith(3, new Error('oops'))
  902. this.HttpController.getProjectDocsAndFlushIfOld(
  903. this.req,
  904. this.res,
  905. this.next
  906. )
  907. })
  908. it('should call next with the error', function () {
  909. this.next.calledWith(sinon.match.instanceOf(Error)).should.equal(true)
  910. })
  911. })
  912. })
  913. describe('updateProject', function () {
  914. beforeEach(function () {
  915. this.projectHistoryId = 'history-id-123'
  916. this.userId = 'user-id-123'
  917. this.updates = [
  918. {
  919. type: 'rename-doc',
  920. id: 1,
  921. pathname: 'thesis.tex',
  922. newPathname: 'book.tex',
  923. },
  924. { type: 'add-doc', id: 2, pathname: 'article.tex', docLines: 'hello' },
  925. {
  926. type: 'rename-file',
  927. id: 3,
  928. pathname: 'apple.png',
  929. newPathname: 'banana.png',
  930. },
  931. { type: 'add-file', id: 4, url: 'filestore.example.com/4' },
  932. ]
  933. this.version = 1234567
  934. this.req = {
  935. query: {},
  936. body: {
  937. projectHistoryId: this.projectHistoryId,
  938. userId: this.userId,
  939. updates: this.updates,
  940. version: this.version,
  941. source: this.source,
  942. },
  943. params: {
  944. project_id: this.project_id,
  945. },
  946. }
  947. })
  948. describe('successfully', function () {
  949. beforeEach(function () {
  950. this.ProjectManager.updateProjectWithLocks = sinon.stub().yields()
  951. this.HttpController.updateProject(this.req, this.res, this.next)
  952. })
  953. it('should accept the change', function () {
  954. this.ProjectManager.updateProjectWithLocks
  955. .calledWith(
  956. this.project_id,
  957. this.projectHistoryId,
  958. this.userId,
  959. this.updates,
  960. this.version,
  961. this.source
  962. )
  963. .should.equal(true)
  964. })
  965. it('should return a successful No Content response', function () {
  966. this.res.sendStatus.calledWith(204).should.equal(true)
  967. })
  968. it('should time the request', function () {
  969. this.Metrics.Timer.prototype.done.called.should.equal(true)
  970. })
  971. })
  972. describe('when an errors occurs', function () {
  973. beforeEach(function () {
  974. this.ProjectManager.updateProjectWithLocks = sinon
  975. .stub()
  976. .yields(new Error('oops'))
  977. this.HttpController.updateProject(this.req, this.res, this.next)
  978. })
  979. it('should call next with the error', function () {
  980. this.next.calledWith(sinon.match.instanceOf(Error)).should.equal(true)
  981. })
  982. })
  983. })
  984. describe('resyncProjectHistory', function () {
  985. beforeEach(function () {
  986. this.projectHistoryId = 'history-id-123'
  987. this.docs = sinon.stub()
  988. this.files = sinon.stub()
  989. this.fileUpdates = sinon.stub()
  990. this.req = {
  991. query: {},
  992. body: {
  993. projectHistoryId: this.projectHistoryId,
  994. docs: this.docs,
  995. files: this.files,
  996. },
  997. params: {
  998. project_id: this.project_id,
  999. },
  1000. }
  1001. })
  1002. describe('successfully', function () {
  1003. beforeEach(function () {
  1004. this.HistoryManager.resyncProjectHistory = sinon.stub().callsArgWith(5)
  1005. this.HttpController.resyncProjectHistory(this.req, this.res, this.next)
  1006. })
  1007. it('should accept the change', function () {
  1008. this.HistoryManager.resyncProjectHistory
  1009. .calledWith(
  1010. this.project_id,
  1011. this.projectHistoryId,
  1012. this.docs,
  1013. this.files,
  1014. {}
  1015. )
  1016. .should.equal(true)
  1017. })
  1018. it('should return a successful No Content response', function () {
  1019. this.res.sendStatus.should.have.been.calledWith(204)
  1020. })
  1021. })
  1022. describe('when an errors occurs', function () {
  1023. beforeEach(function () {
  1024. this.HistoryManager.resyncProjectHistory = sinon
  1025. .stub()
  1026. .callsArgWith(5, new Error('oops'))
  1027. this.HttpController.resyncProjectHistory(this.req, this.res, this.next)
  1028. })
  1029. it('should call next with the error', function () {
  1030. this.next.calledWith(sinon.match.instanceOf(Error)).should.equal(true)
  1031. })
  1032. })
  1033. })
  1034. describe('appendToDoc', function () {
  1035. beforeEach(function () {
  1036. this.lines = ['one', 'two', 'three']
  1037. this.source = 'dropbox'
  1038. this.user_id = 'user-id-123'
  1039. this.req = {
  1040. headers: {},
  1041. params: {
  1042. project_id: this.project_id,
  1043. doc_id: this.doc_id,
  1044. },
  1045. query: {},
  1046. body: {
  1047. lines: this.lines,
  1048. source: this.source,
  1049. user_id: this.user_id,
  1050. undoing: (this.undoing = true),
  1051. },
  1052. }
  1053. })
  1054. describe('successfully', function () {
  1055. beforeEach(function () {
  1056. this.DocumentManager.appendToDocWithLock = sinon
  1057. .stub()
  1058. .callsArgWith(5, null, { rev: '123' })
  1059. this.HttpController.appendToDoc(this.req, this.res, this.next)
  1060. })
  1061. it('should append to the doc', function () {
  1062. this.DocumentManager.appendToDocWithLock
  1063. .calledWith(
  1064. this.project_id,
  1065. this.doc_id,
  1066. this.lines,
  1067. this.source,
  1068. this.user_id
  1069. )
  1070. .should.equal(true)
  1071. })
  1072. it('should return a json response with the document rev from web', function () {
  1073. this.res.json.calledWithMatch({ rev: '123' }).should.equal(true)
  1074. })
  1075. it('should log the request', function () {
  1076. this.logger.debug
  1077. .calledWith(
  1078. {
  1079. docId: this.doc_id,
  1080. projectId: this.project_id,
  1081. lines: this.lines,
  1082. source: this.source,
  1083. userId: this.user_id,
  1084. },
  1085. 'appending to doc via http'
  1086. )
  1087. .should.equal(true)
  1088. })
  1089. it('should time the request', function () {
  1090. this.Metrics.Timer.prototype.done.called.should.equal(true)
  1091. })
  1092. })
  1093. describe('when an errors occurs', function () {
  1094. beforeEach(function () {
  1095. this.DocumentManager.appendToDocWithLock = sinon
  1096. .stub()
  1097. .callsArgWith(5, new Error('oops'))
  1098. this.HttpController.appendToDoc(this.req, this.res, this.next)
  1099. })
  1100. it('should call next with the error', function () {
  1101. this.next.calledWith(sinon.match.instanceOf(Error)).should.equal(true)
  1102. })
  1103. })
  1104. describe('when the payload is too large', function () {
  1105. beforeEach(function () {
  1106. this.DocumentManager.appendToDocWithLock = sinon
  1107. .stub()
  1108. .callsArgWith(5, new Errors.FileTooLargeError())
  1109. this.HttpController.appendToDoc(this.req, this.res, this.next)
  1110. })
  1111. it('should send back a 422 response', function () {
  1112. this.res.sendStatus.calledWith(422).should.equal(true)
  1113. })
  1114. })
  1115. })
  1116. })