HttpControllerTests.js 27 KB

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