HttpControllerTests.js 36 KB

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