HttpControllerTests.js 27 KB

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