HttpControllerTests.js 34 KB

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