HttpControllerTests.js 27 KB

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