HttpControllerTests.js 32 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107
  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. DOC_OPS_TTL: 42,
  19. }),
  20. './Metrics': (this.Metrics = {}),
  21. './Errors': Errors,
  22. '@overleaf/settings': { max_doc_length: 2 * 1024 * 1024 },
  23. },
  24. })
  25. this.Metrics.Timer = class Timer {}
  26. this.Metrics.Timer.prototype.done = sinon.stub()
  27. this.project_id = 'project-id-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. )
  67. this.HttpController.getDoc(this.req, this.res, this.next)
  68. })
  69. it('should get the doc', function () {
  70. this.DocumentManager.getDocAndRecentOpsWithLock
  71. .calledWith(this.project_id, this.doc_id, -1)
  72. .should.equal(true)
  73. })
  74. it('should return the doc as JSON', function () {
  75. this.res.json
  76. .calledWith({
  77. id: this.doc_id,
  78. lines: this.lines,
  79. version: this.version,
  80. ops: [],
  81. ranges: this.ranges,
  82. pathname: this.pathname,
  83. ttlInS: 42,
  84. })
  85. .should.equal(true)
  86. })
  87. it('should log the request', function () {
  88. this.logger.debug
  89. .calledWith(
  90. { docId: this.doc_id, projectId: this.project_id },
  91. 'getting doc via http'
  92. )
  93. .should.equal(true)
  94. })
  95. it('should time the request', function () {
  96. this.Metrics.Timer.prototype.done.called.should.equal(true)
  97. })
  98. })
  99. describe('when recent ops are requested', function () {
  100. beforeEach(function () {
  101. this.DocumentManager.getDocAndRecentOpsWithLock = sinon
  102. .stub()
  103. .callsArgWith(
  104. 3,
  105. null,
  106. this.lines,
  107. this.version,
  108. this.ops,
  109. this.ranges,
  110. this.pathname
  111. )
  112. this.req.query = { fromVersion: `${this.fromVersion}` }
  113. this.HttpController.getDoc(this.req, this.res, this.next)
  114. })
  115. it('should get the doc', function () {
  116. this.DocumentManager.getDocAndRecentOpsWithLock
  117. .calledWith(this.project_id, this.doc_id, this.fromVersion)
  118. .should.equal(true)
  119. })
  120. it('should return the doc as JSON', function () {
  121. this.res.json
  122. .calledWith({
  123. id: this.doc_id,
  124. lines: this.lines,
  125. version: this.version,
  126. ops: this.ops,
  127. ranges: this.ranges,
  128. pathname: this.pathname,
  129. ttlInS: 42,
  130. })
  131. .should.equal(true)
  132. })
  133. it('should log the request', function () {
  134. this.logger.debug
  135. .calledWith(
  136. { docId: this.doc_id, projectId: this.project_id },
  137. 'getting doc via http'
  138. )
  139. .should.equal(true)
  140. })
  141. it('should time the request', function () {
  142. this.Metrics.Timer.prototype.done.called.should.equal(true)
  143. })
  144. })
  145. describe('when the document does not exist', function () {
  146. beforeEach(function () {
  147. this.DocumentManager.getDocAndRecentOpsWithLock = sinon
  148. .stub()
  149. .callsArgWith(3, null, null, null)
  150. this.HttpController.getDoc(this.req, this.res, this.next)
  151. })
  152. it('should call next with NotFoundError', function () {
  153. this.next
  154. .calledWith(sinon.match.instanceOf(Errors.NotFoundError))
  155. .should.equal(true)
  156. })
  157. })
  158. describe('when an errors occurs', function () {
  159. beforeEach(function () {
  160. this.DocumentManager.getDocAndRecentOpsWithLock = sinon
  161. .stub()
  162. .callsArgWith(3, new Error('oops'), null, null)
  163. this.HttpController.getDoc(this.req, this.res, this.next)
  164. })
  165. it('should call next with the error', function () {
  166. this.next.calledWith(sinon.match.instanceOf(Error)).should.equal(true)
  167. })
  168. })
  169. })
  170. describe('setDoc', function () {
  171. beforeEach(function () {
  172. this.lines = ['one', 'two', 'three']
  173. this.source = 'dropbox'
  174. this.user_id = 'user-id-123'
  175. this.req = {
  176. headers: {},
  177. params: {
  178. project_id: this.project_id,
  179. doc_id: this.doc_id,
  180. },
  181. query: {},
  182. body: {
  183. lines: this.lines,
  184. source: this.source,
  185. user_id: this.user_id,
  186. undoing: (this.undoing = true),
  187. },
  188. }
  189. })
  190. describe('successfully', function () {
  191. beforeEach(function () {
  192. this.DocumentManager.setDocWithLock = sinon
  193. .stub()
  194. .callsArgWith(6, null, { rev: '123' })
  195. this.HttpController.setDoc(this.req, this.res, this.next)
  196. })
  197. it('should set the doc', function () {
  198. this.DocumentManager.setDocWithLock
  199. .calledWith(
  200. this.project_id,
  201. this.doc_id,
  202. this.lines,
  203. this.source,
  204. this.user_id,
  205. this.undoing
  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(6, 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. })