HttpControllerTests.js 32 KB

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