HttpControllerTests.js 38 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348
  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.project_id = 'project-id-123'
  8. this.projectHistoryId = '123'
  9. this.doc_id = 'doc-id-123'
  10. this.source = 'editor'
  11. this.next = sinon.stub()
  12. this.res = {
  13. send: sinon.stub(),
  14. sendStatus: sinon.stub(),
  15. json: sinon.stub(),
  16. }
  17. this.DocumentManager = {
  18. promises: {
  19. getDocAndRecentOpsWithLock: sinon.stub(),
  20. getCommentWithLock: sinon.stub(),
  21. setDocWithLock: sinon.stub(),
  22. flushDocIfLoadedWithLock: sinon.stub().resolves(),
  23. flushAndDeleteDocWithLock: sinon.stub().resolves(),
  24. acceptChangesWithLock: sinon.stub().resolves(),
  25. updateCommentStateWithLock: sinon.stub().resolves(),
  26. deleteCommentWithLock: sinon.stub().resolves(),
  27. appendToDocWithLock: sinon.stub(),
  28. },
  29. }
  30. this.HistoryManager = {
  31. flushProjectChangesAsync: sinon.stub(),
  32. promises: {
  33. resyncProjectHistory: sinon.stub().resolves(),
  34. },
  35. }
  36. this.ProjectHistoryRedisManager = {
  37. promises: {
  38. queueOps: sinon.stub().resolves(),
  39. },
  40. }
  41. this.ProjectManager = {
  42. promises: {
  43. flushProjectWithLocks: sinon.stub().resolves(),
  44. flushAndDeleteProjectWithLocks: sinon.stub().resolves(),
  45. queueFlushAndDeleteProject: sinon.stub().resolves(),
  46. getProjectDocsAndFlushIfOld: sinon.stub(),
  47. updateProjectWithLocks: sinon.stub().resolves(),
  48. },
  49. }
  50. this.DeleteQueueManager = {}
  51. this.RedisManager = {
  52. DOC_OPS_TTL: 42,
  53. }
  54. this.Metrics = {
  55. Timer: class Timer {},
  56. }
  57. this.Metrics.Timer.prototype.done = sinon.stub()
  58. this.Utils = {
  59. addTrackedDeletesToContent: sinon.stub().returnsArg(0),
  60. }
  61. this.HistoryConversions = {
  62. toHistoryRanges: sinon.stub().returnsArg(0),
  63. }
  64. this.HttpController = SandboxedModule.require(modulePath, {
  65. requires: {
  66. './DocumentManager': this.DocumentManager,
  67. './HistoryManager': this.HistoryManager,
  68. './ProjectHistoryRedisManager': this.ProjectHistoryRedisManager,
  69. './ProjectManager': this.ProjectManager,
  70. './DeleteQueueManager': this.DeleteQueueManager,
  71. './RedisManager': this.RedisManager,
  72. './Metrics': this.Metrics,
  73. './Errors': Errors,
  74. './Utils': this.Utils,
  75. './HistoryConversions': this.HistoryConversions,
  76. '@overleaf/settings': { max_doc_length: 2 * 1024 * 1024 },
  77. },
  78. })
  79. })
  80. describe('getDoc', function () {
  81. beforeEach(function () {
  82. this.lines = ['one', 'two', 'three']
  83. this.ops = ['mock-op-1', 'mock-op-2']
  84. this.version = 42
  85. this.fromVersion = 42
  86. this.ranges = { changes: 'mock', comments: 'mock' }
  87. this.pathname = '/a/b/c'
  88. this.req = {
  89. params: {
  90. project_id: this.project_id,
  91. doc_id: this.doc_id,
  92. },
  93. query: {},
  94. body: {},
  95. }
  96. })
  97. describe('when the document exists and no recent ops are requested', function () {
  98. beforeEach(async function () {
  99. this.DocumentManager.promises.getDocAndRecentOpsWithLock.resolves({
  100. lines: this.lines,
  101. version: this.version,
  102. ops: [],
  103. ranges: this.ranges,
  104. pathname: this.pathname,
  105. projectHistoryId: this.projectHistoryId,
  106. type: 'sharejs-text-ot',
  107. })
  108. await this.HttpController.getDoc(this.req, this.res, this.next)
  109. })
  110. it('should get the doc', function () {
  111. this.DocumentManager.promises.getDocAndRecentOpsWithLock.should.have.been.calledWith(
  112. this.project_id,
  113. this.doc_id,
  114. -1
  115. )
  116. })
  117. it('should return the doc as JSON', function () {
  118. this.res.json.should.have.been.calledWith({
  119. id: this.doc_id,
  120. lines: this.lines,
  121. version: this.version,
  122. ops: [],
  123. ranges: this.ranges,
  124. pathname: this.pathname,
  125. ttlInS: 42,
  126. type: 'sharejs-text-ot',
  127. })
  128. })
  129. it('should log the request', function () {
  130. this.logger.debug
  131. .calledWith(
  132. {
  133. docId: this.doc_id,
  134. projectId: this.project_id,
  135. historyRanges: false,
  136. },
  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 recent ops are requested', function () {
  146. beforeEach(async function () {
  147. this.DocumentManager.promises.getDocAndRecentOpsWithLock.resolves({
  148. lines: this.lines,
  149. version: this.version,
  150. ops: this.ops,
  151. ranges: this.ranges,
  152. pathname: this.pathname,
  153. projectHistoryId: this.projectHistoryId,
  154. type: 'sharejs-text-ot',
  155. })
  156. this.req.query = { fromVersion: `${this.fromVersion}` }
  157. await this.HttpController.getDoc(this.req, this.res, this.next)
  158. })
  159. it('should get the doc', function () {
  160. this.DocumentManager.promises.getDocAndRecentOpsWithLock.should.have.been.calledWith(
  161. this.project_id,
  162. this.doc_id,
  163. this.fromVersion
  164. )
  165. })
  166. it('should return the doc as JSON', function () {
  167. this.res.json.should.have.been.calledWith({
  168. id: this.doc_id,
  169. lines: this.lines,
  170. version: this.version,
  171. ops: this.ops,
  172. ranges: this.ranges,
  173. pathname: this.pathname,
  174. ttlInS: 42,
  175. type: 'sharejs-text-ot',
  176. })
  177. })
  178. it('should log the request', function () {
  179. this.logger.debug
  180. .calledWith(
  181. {
  182. docId: this.doc_id,
  183. projectId: this.project_id,
  184. historyRanges: false,
  185. },
  186. 'getting doc via http'
  187. )
  188. .should.equal(true)
  189. })
  190. it('should time the request', function () {
  191. this.Metrics.Timer.prototype.done.called.should.equal(true)
  192. })
  193. })
  194. describe('when historyRanges query param is true', function () {
  195. beforeEach(async function () {
  196. this.DocumentManager.promises.getDocAndRecentOpsWithLock.resolves({
  197. lines: this.lines,
  198. version: this.version,
  199. ops: [],
  200. ranges: this.ranges,
  201. pathname: this.pathname,
  202. projectHistoryId: this.projectHistoryId,
  203. type: 'sharejs-text-ot',
  204. })
  205. this.req.query = { historyRanges: 'true' }
  206. await this.HttpController.getDoc(this.req, this.res, this.next)
  207. })
  208. it('should get the doc', function () {
  209. this.DocumentManager.promises.getDocAndRecentOpsWithLock.should.have.been.calledWith(
  210. this.project_id,
  211. this.doc_id,
  212. -1
  213. )
  214. })
  215. it('should return the doc as JSON with history ranges processing', function () {
  216. this.res.json.should.have.been.calledWith({
  217. id: this.doc_id,
  218. lines: this.lines,
  219. version: this.version,
  220. ops: [],
  221. ranges: this.ranges,
  222. pathname: this.pathname,
  223. ttlInS: 42,
  224. type: 'sharejs-text-ot',
  225. })
  226. })
  227. it('should call addTrackedDeletesToContent for history ranges processing', function () {
  228. this.Utils.addTrackedDeletesToContent.called.should.equal(true)
  229. })
  230. it('should call toHistoryRanges for range conversion', function () {
  231. this.HistoryConversions.toHistoryRanges.called.should.equal(true)
  232. })
  233. it('should log the request with historyRanges: true', function () {
  234. this.logger.debug
  235. .calledWith(
  236. {
  237. docId: this.doc_id,
  238. projectId: this.project_id,
  239. historyRanges: true,
  240. },
  241. 'getting doc via http'
  242. )
  243. .should.equal(true)
  244. })
  245. it('should time the request', function () {
  246. this.Metrics.Timer.prototype.done.called.should.equal(true)
  247. })
  248. })
  249. describe('when the document does not exist', function () {
  250. beforeEach(async function () {
  251. this.DocumentManager.promises.getDocAndRecentOpsWithLock.resolves({
  252. lines: null,
  253. version: null,
  254. })
  255. await this.HttpController.getDoc(this.req, this.res, this.next)
  256. })
  257. it('should call next with NotFoundError', function () {
  258. this.next
  259. .calledWith(sinon.match.instanceOf(Errors.NotFoundError))
  260. .should.equal(true)
  261. })
  262. })
  263. describe('when an errors occurs', function () {
  264. beforeEach(async function () {
  265. this.DocumentManager.promises.getDocAndRecentOpsWithLock.rejects(
  266. new Error('oops')
  267. )
  268. await this.HttpController.getDoc(this.req, this.res, this.next)
  269. })
  270. it('should call next with the error', function () {
  271. this.next.calledWith(sinon.match.instanceOf(Error)).should.equal(true)
  272. })
  273. })
  274. })
  275. describe('getComment', function () {
  276. beforeEach(function () {
  277. this.ranges = {
  278. changes: 'mock',
  279. comments: [
  280. {
  281. id: 'comment-id-1',
  282. },
  283. {
  284. id: 'comment-id-2',
  285. },
  286. ],
  287. }
  288. this.req = {
  289. params: {
  290. project_id: this.project_id,
  291. doc_id: this.doc_id,
  292. comment_id: this.comment_id,
  293. },
  294. query: {},
  295. body: {},
  296. }
  297. })
  298. beforeEach(async function () {
  299. this.DocumentManager.promises.getCommentWithLock.resolves(
  300. this.ranges.comments[0]
  301. )
  302. await this.HttpController.getComment(this.req, this.res, this.next)
  303. })
  304. it('should get the comment', function () {
  305. this.DocumentManager.promises.getCommentWithLock.should.have.been.calledWith(
  306. this.project_id,
  307. this.doc_id,
  308. this.comment_id
  309. )
  310. })
  311. it('should return the comment as JSON', function () {
  312. this.res.json
  313. .calledWith({
  314. id: 'comment-id-1',
  315. })
  316. .should.equal(true)
  317. })
  318. it('should log the request', function () {
  319. this.logger.debug
  320. .calledWith(
  321. {
  322. projectId: this.project_id,
  323. docId: this.doc_id,
  324. commentId: this.comment_id,
  325. },
  326. 'getting comment via http'
  327. )
  328. .should.equal(true)
  329. })
  330. })
  331. describe('setDoc', function () {
  332. beforeEach(function () {
  333. this.lines = ['one', 'two', 'three']
  334. this.source = 'dropbox'
  335. this.user_id = 'user-id-123'
  336. this.req = {
  337. headers: {},
  338. params: {
  339. project_id: this.project_id,
  340. doc_id: this.doc_id,
  341. },
  342. query: {},
  343. body: {
  344. lines: this.lines,
  345. source: this.source,
  346. user_id: this.user_id,
  347. undoing: (this.undoing = true),
  348. },
  349. }
  350. })
  351. describe('successfully', function () {
  352. beforeEach(async function () {
  353. this.DocumentManager.promises.setDocWithLock.resolves({ rev: '123' })
  354. await this.HttpController.setDoc(this.req, this.res, this.next)
  355. })
  356. it('should set the doc', function () {
  357. this.DocumentManager.promises.setDocWithLock.should.have.been.calledWith(
  358. this.project_id,
  359. this.doc_id,
  360. this.lines,
  361. this.source,
  362. this.user_id,
  363. this.undoing,
  364. true
  365. )
  366. })
  367. it('should return a json response with the document rev from web', function () {
  368. this.res.json.calledWithMatch({ rev: '123' }).should.equal(true)
  369. })
  370. it('should log the request', function () {
  371. this.logger.debug
  372. .calledWith(
  373. {
  374. docId: this.doc_id,
  375. projectId: this.project_id,
  376. lines: this.lines,
  377. source: this.source,
  378. userId: this.user_id,
  379. undoing: this.undoing,
  380. },
  381. 'setting doc via http'
  382. )
  383. .should.equal(true)
  384. })
  385. it('should time the request', function () {
  386. this.Metrics.Timer.prototype.done.called.should.equal(true)
  387. })
  388. })
  389. describe('when an errors occurs', function () {
  390. beforeEach(async function () {
  391. this.DocumentManager.promises.setDocWithLock.rejects(new Error('oops'))
  392. await this.HttpController.setDoc(this.req, this.res, this.next)
  393. })
  394. it('should call next with the error', function () {
  395. this.next.calledWith(sinon.match.instanceOf(Error)).should.equal(true)
  396. })
  397. })
  398. describe('when the payload is too large', function () {
  399. beforeEach(async function () {
  400. const lines = []
  401. for (let _ = 0; _ <= 200000; _++) {
  402. lines.push('test test test')
  403. }
  404. this.req.body.lines = lines
  405. this.DocumentManager.promises.setDocWithLock.resolves()
  406. await this.HttpController.setDoc(this.req, this.res, this.next)
  407. })
  408. it('should send back a 406 response', function () {
  409. this.res.sendStatus.calledWith(406).should.equal(true)
  410. })
  411. it('should not call setDocWithLock', function () {
  412. this.DocumentManager.promises.setDocWithLock.should.not.have.been.called
  413. })
  414. })
  415. })
  416. describe('flushProject', function () {
  417. beforeEach(function () {
  418. this.req = {
  419. params: {
  420. project_id: this.project_id,
  421. },
  422. query: {},
  423. body: {},
  424. }
  425. })
  426. describe('successfully', function () {
  427. beforeEach(async function () {
  428. await this.HttpController.flushProject(this.req, this.res, this.next)
  429. })
  430. it('should flush the project', function () {
  431. this.ProjectManager.promises.flushProjectWithLocks.should.have.been.calledWith(
  432. this.project_id
  433. )
  434. })
  435. it('should return a successful No Content response', function () {
  436. this.res.sendStatus.calledWith(204).should.equal(true)
  437. })
  438. it('should log the request', function () {
  439. this.logger.debug
  440. .calledWith(
  441. { projectId: this.project_id },
  442. 'flushing project via http'
  443. )
  444. .should.equal(true)
  445. })
  446. it('should time the request', function () {
  447. this.Metrics.Timer.prototype.done.called.should.equal(true)
  448. })
  449. })
  450. describe('when an errors occurs', function () {
  451. beforeEach(async function () {
  452. this.ProjectManager.promises.flushProjectWithLocks.rejects(
  453. new Error('oops')
  454. )
  455. await this.HttpController.flushProject(this.req, this.res, this.next)
  456. })
  457. it('should call next with the error', function () {
  458. this.next.calledWith(sinon.match.instanceOf(Error)).should.equal(true)
  459. })
  460. })
  461. })
  462. describe('flushDocIfLoaded', function () {
  463. beforeEach(function () {
  464. this.lines = ['one', 'two', 'three']
  465. this.version = 42
  466. this.req = {
  467. params: {
  468. project_id: this.project_id,
  469. doc_id: this.doc_id,
  470. },
  471. query: {},
  472. body: {},
  473. }
  474. })
  475. describe('successfully', function () {
  476. beforeEach(async function () {
  477. await this.HttpController.flushDocIfLoaded(
  478. this.req,
  479. this.res,
  480. this.next
  481. )
  482. })
  483. it('should flush the doc', function () {
  484. this.DocumentManager.promises.flushDocIfLoadedWithLock.should.have.been.calledWith(
  485. this.project_id,
  486. this.doc_id
  487. )
  488. })
  489. it('should return a successful No Content response', function () {
  490. this.res.sendStatus.calledWith(204).should.equal(true)
  491. })
  492. it('should log the request', function () {
  493. this.logger.debug
  494. .calledWith(
  495. { docId: this.doc_id, projectId: this.project_id },
  496. 'flushing doc via http'
  497. )
  498. .should.equal(true)
  499. })
  500. it('should time the request', function () {
  501. this.Metrics.Timer.prototype.done.called.should.equal(true)
  502. })
  503. })
  504. describe('when an errors occurs', function () {
  505. beforeEach(async function () {
  506. this.DocumentManager.promises.flushDocIfLoadedWithLock.rejects(
  507. new Error('oops')
  508. )
  509. await this.HttpController.flushDocIfLoaded(
  510. this.req,
  511. this.res,
  512. this.next
  513. )
  514. })
  515. it('should call next with the error', function () {
  516. this.next.calledWith(sinon.match.instanceOf(Error)).should.equal(true)
  517. })
  518. })
  519. })
  520. describe('deleteDoc', function () {
  521. beforeEach(function () {
  522. this.req = {
  523. params: {
  524. project_id: this.project_id,
  525. doc_id: this.doc_id,
  526. },
  527. query: {},
  528. body: {},
  529. }
  530. })
  531. describe('successfully', function () {
  532. beforeEach(async function () {
  533. await this.HttpController.deleteDoc(this.req, this.res, this.next)
  534. })
  535. it('should flush and delete the doc', function () {
  536. this.DocumentManager.promises.flushAndDeleteDocWithLock.should.have.been.calledWith(
  537. this.project_id,
  538. this.doc_id,
  539. { ignoreFlushErrors: false }
  540. )
  541. })
  542. it('should flush project history', function () {
  543. this.HistoryManager.flushProjectChangesAsync
  544. .calledWithExactly(this.project_id)
  545. .should.equal(true)
  546. })
  547. it('should return a successful No Content response', function () {
  548. this.res.sendStatus.calledWith(204).should.equal(true)
  549. })
  550. it('should log the request', function () {
  551. this.logger.debug
  552. .calledWith(
  553. { docId: this.doc_id, projectId: this.project_id },
  554. 'deleting doc via http'
  555. )
  556. .should.equal(true)
  557. })
  558. it('should time the request', function () {
  559. this.Metrics.Timer.prototype.done.called.should.equal(true)
  560. })
  561. })
  562. describe('ignoring errors', function () {
  563. beforeEach(async function () {
  564. this.req.query.ignore_flush_errors = 'true'
  565. await this.HttpController.deleteDoc(this.req, this.res, this.next)
  566. })
  567. it('should delete the doc', function () {
  568. this.DocumentManager.promises.flushAndDeleteDocWithLock.should.have.been.calledWith(
  569. this.project_id,
  570. this.doc_id,
  571. { ignoreFlushErrors: true }
  572. )
  573. })
  574. it('should return a successful No Content response', function () {
  575. this.res.sendStatus.calledWith(204).should.equal(true)
  576. })
  577. })
  578. describe('when an errors occurs', function () {
  579. beforeEach(async function () {
  580. this.DocumentManager.promises.flushAndDeleteDocWithLock.rejects(
  581. new Error('oops')
  582. )
  583. await this.HttpController.deleteDoc(this.req, this.res, this.next)
  584. })
  585. it('should flush project history', function () {
  586. this.HistoryManager.flushProjectChangesAsync
  587. .calledWithExactly(this.project_id)
  588. .should.equal(true)
  589. })
  590. it('should call next with the error', function () {
  591. this.next.calledWith(sinon.match.instanceOf(Error)).should.equal(true)
  592. })
  593. })
  594. })
  595. describe('deleteProject', function () {
  596. beforeEach(function () {
  597. this.req = {
  598. params: {
  599. project_id: this.project_id,
  600. },
  601. query: {},
  602. body: {},
  603. }
  604. })
  605. describe('successfully', function () {
  606. beforeEach(async function () {
  607. await this.HttpController.deleteProject(this.req, this.res, this.next)
  608. })
  609. it('should delete the project', function () {
  610. this.ProjectManager.promises.flushAndDeleteProjectWithLocks.should.have.been.calledWith(
  611. this.project_id
  612. )
  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. { projectId: this.project_id },
  621. 'deleting project via http'
  622. )
  623. .should.equal(true)
  624. })
  625. it('should time the request', function () {
  626. this.Metrics.Timer.prototype.done.called.should.equal(true)
  627. })
  628. })
  629. describe('with the background=true option from realtime', function () {
  630. beforeEach(async function () {
  631. this.req.query = { background: true, shutdown: true }
  632. await this.HttpController.deleteProject(this.req, this.res, this.next)
  633. })
  634. it('should queue the flush and delete', function () {
  635. this.ProjectManager.promises.queueFlushAndDeleteProject.should.have.been.calledWith(
  636. this.project_id
  637. )
  638. })
  639. })
  640. describe('when an errors occurs', function () {
  641. beforeEach(async function () {
  642. this.ProjectManager.promises.flushAndDeleteProjectWithLocks.rejects(
  643. new Error('oops')
  644. )
  645. await this.HttpController.deleteProject(this.req, this.res, this.next)
  646. })
  647. it('should call next with the error', function () {
  648. this.next.calledWith(sinon.match.instanceOf(Error)).should.equal(true)
  649. })
  650. })
  651. })
  652. describe('acceptChanges', function () {
  653. beforeEach(function () {
  654. this.req = {
  655. params: {
  656. project_id: this.project_id,
  657. doc_id: this.doc_id,
  658. change_id: (this.change_id = 'mock-change-od-1'),
  659. },
  660. query: {},
  661. body: {},
  662. }
  663. })
  664. describe('successfully with a single change', function () {
  665. beforeEach(async function () {
  666. await this.HttpController.acceptChanges(this.req, this.res, this.next)
  667. })
  668. it('should accept the change', function () {
  669. this.DocumentManager.promises.acceptChangesWithLock.should.have.been.calledWith(
  670. this.project_id,
  671. this.doc_id,
  672. [this.change_id]
  673. )
  674. })
  675. it('should return a successful No Content response', function () {
  676. this.res.sendStatus.calledWith(204).should.equal(true)
  677. })
  678. it('should log the request', function () {
  679. this.logger.debug
  680. .calledWith(
  681. { projectId: this.project_id, docId: this.doc_id },
  682. 'accepting 1 changes via http'
  683. )
  684. .should.equal(true)
  685. })
  686. it('should time the request', function () {
  687. this.Metrics.Timer.prototype.done.called.should.equal(true)
  688. })
  689. })
  690. describe('succesfully with with multiple changes', function () {
  691. beforeEach(async function () {
  692. this.change_ids = [
  693. 'mock-change-od-1',
  694. 'mock-change-od-2',
  695. 'mock-change-od-3',
  696. 'mock-change-od-4',
  697. ]
  698. this.req.body = { change_ids: this.change_ids }
  699. await this.HttpController.acceptChanges(this.req, this.res, this.next)
  700. })
  701. it('should accept the changes in the body payload', function () {
  702. this.DocumentManager.promises.acceptChangesWithLock.should.have.been.calledWith(
  703. this.project_id,
  704. this.doc_id,
  705. this.change_ids
  706. )
  707. })
  708. it('should log the request with the correct number of changes', function () {
  709. this.logger.debug
  710. .calledWith(
  711. { projectId: this.project_id, docId: this.doc_id },
  712. `accepting ${this.change_ids.length} changes via http`
  713. )
  714. .should.equal(true)
  715. })
  716. })
  717. describe('when an errors occurs', function () {
  718. beforeEach(async function () {
  719. this.DocumentManager.promises.acceptChangesWithLock.rejects(
  720. new Error('oops')
  721. )
  722. await this.HttpController.acceptChanges(this.req, this.res, this.next)
  723. })
  724. it('should call next with the error', function () {
  725. this.next.calledWith(sinon.match.instanceOf(Error)).should.equal(true)
  726. })
  727. })
  728. })
  729. describe('resolveComment', function () {
  730. beforeEach(function () {
  731. this.user_id = 'user-id-123'
  732. this.req = {
  733. params: {
  734. project_id: this.project_id,
  735. doc_id: this.doc_id,
  736. comment_id: (this.comment_id = 'mock-comment-id'),
  737. },
  738. query: {},
  739. body: {
  740. user_id: this.user_id,
  741. },
  742. }
  743. this.resolved = true
  744. })
  745. describe('successfully', function () {
  746. beforeEach(async function () {
  747. await this.HttpController.resolveComment(this.req, this.res, this.next)
  748. })
  749. it('should accept the change', function () {
  750. this.DocumentManager.promises.updateCommentStateWithLock.should.have.been.calledWith(
  751. this.project_id,
  752. this.doc_id,
  753. this.comment_id,
  754. this.user_id,
  755. this.resolved
  756. )
  757. })
  758. it('should return a successful No Content response', function () {
  759. this.res.sendStatus.calledWith(204).should.equal(true)
  760. })
  761. it('should log the request', function () {
  762. this.logger.debug
  763. .calledWith(
  764. {
  765. projectId: this.project_id,
  766. docId: this.doc_id,
  767. commentId: this.comment_id,
  768. },
  769. 'resolving comment via http'
  770. )
  771. .should.equal(true)
  772. })
  773. })
  774. describe('when an errors occurs', function () {
  775. beforeEach(async function () {
  776. this.DocumentManager.promises.updateCommentStateWithLock.rejects(
  777. new Error('oops')
  778. )
  779. await this.HttpController.resolveComment(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('reopenComment', function () {
  787. beforeEach(function () {
  788. this.user_id = 'user-id-123'
  789. this.req = {
  790. params: {
  791. project_id: this.project_id,
  792. doc_id: this.doc_id,
  793. comment_id: (this.comment_id = 'mock-comment-id'),
  794. },
  795. query: {},
  796. body: {
  797. user_id: this.user_id,
  798. },
  799. }
  800. this.resolved = false
  801. })
  802. describe('successfully', function () {
  803. beforeEach(async function () {
  804. await this.HttpController.reopenComment(this.req, this.res, this.next)
  805. })
  806. it('should accept the change', function () {
  807. this.DocumentManager.promises.updateCommentStateWithLock.should.have.been.calledWith(
  808. this.project_id,
  809. this.doc_id,
  810. this.comment_id,
  811. this.user_id,
  812. this.resolved
  813. )
  814. })
  815. it('should return a successful No Content response', function () {
  816. this.res.sendStatus.calledWith(204).should.equal(true)
  817. })
  818. it('should log the request', function () {
  819. this.logger.debug
  820. .calledWith(
  821. {
  822. projectId: this.project_id,
  823. docId: this.doc_id,
  824. commentId: this.comment_id,
  825. },
  826. 'reopening comment via http'
  827. )
  828. .should.equal(true)
  829. })
  830. })
  831. describe('when an errors occurs', function () {
  832. beforeEach(async function () {
  833. this.DocumentManager.promises.updateCommentStateWithLock.rejects(
  834. new Error('oops')
  835. )
  836. await this.HttpController.reopenComment(this.req, this.res, this.next)
  837. })
  838. it('should call next with the error', function () {
  839. this.next.calledWith(sinon.match.instanceOf(Error)).should.equal(true)
  840. })
  841. })
  842. })
  843. describe('deleteComment', function () {
  844. beforeEach(function () {
  845. this.user_id = 'user-id-123'
  846. this.req = {
  847. params: {
  848. project_id: this.project_id,
  849. doc_id: this.doc_id,
  850. comment_id: (this.comment_id = 'mock-comment-id'),
  851. },
  852. query: {},
  853. body: {
  854. user_id: this.user_id,
  855. },
  856. }
  857. })
  858. describe('successfully', function () {
  859. beforeEach(async function () {
  860. await this.HttpController.deleteComment(this.req, this.res, this.next)
  861. })
  862. it('should accept the change', function () {
  863. this.DocumentManager.promises.deleteCommentWithLock.should.have.been.calledWith(
  864. this.project_id,
  865. this.doc_id,
  866. this.comment_id,
  867. this.user_id
  868. )
  869. })
  870. it('should return a successful No Content response', function () {
  871. this.res.sendStatus.calledWith(204).should.equal(true)
  872. })
  873. it('should log the request', function () {
  874. this.logger.debug
  875. .calledWith(
  876. {
  877. projectId: this.project_id,
  878. docId: this.doc_id,
  879. commentId: this.comment_id,
  880. },
  881. 'deleting comment via http'
  882. )
  883. .should.equal(true)
  884. })
  885. it('should time the request', function () {
  886. this.Metrics.Timer.prototype.done.called.should.equal(true)
  887. })
  888. })
  889. describe('when an errors occurs', function () {
  890. beforeEach(async function () {
  891. this.DocumentManager.promises.deleteCommentWithLock.rejects(
  892. new Error('oops')
  893. )
  894. await this.HttpController.deleteComment(this.req, this.res, this.next)
  895. })
  896. it('should call next with the error', function () {
  897. this.next.calledWith(sinon.match.instanceOf(Error)).should.equal(true)
  898. })
  899. })
  900. })
  901. describe('getProjectDocsAndFlushIfOld', function () {
  902. beforeEach(function () {
  903. this.state = '01234567890abcdef'
  904. this.docs = [
  905. { _id: '1234', lines: 'hello', v: 23 },
  906. { _id: '4567', lines: 'world', v: 45 },
  907. ]
  908. this.req = {
  909. params: {
  910. project_id: this.project_id,
  911. },
  912. query: {
  913. state: this.state,
  914. },
  915. body: {},
  916. }
  917. })
  918. describe('successfully', function () {
  919. beforeEach(async function () {
  920. this.ProjectManager.promises.getProjectDocsAndFlushIfOld.resolves(
  921. this.docs
  922. )
  923. await this.HttpController.getProjectDocsAndFlushIfOld(
  924. this.req,
  925. this.res,
  926. this.next
  927. )
  928. })
  929. it('should get docs from the project manager', function () {
  930. this.ProjectManager.promises.getProjectDocsAndFlushIfOld.should.have.been.calledWith(
  931. this.project_id,
  932. this.state,
  933. {}
  934. )
  935. })
  936. it('should return a successful response', function () {
  937. this.res.send.calledWith(this.docs).should.equal(true)
  938. })
  939. it('should log the request', function () {
  940. this.logger.debug
  941. .calledWith(
  942. { projectId: this.project_id, exclude: [] },
  943. 'getting docs via http'
  944. )
  945. .should.equal(true)
  946. })
  947. it('should log the response', function () {
  948. this.logger.debug
  949. .calledWith(
  950. { projectId: this.project_id, result: ['1234:23', '4567:45'] },
  951. 'got docs via http'
  952. )
  953. .should.equal(true)
  954. })
  955. it('should time the request', function () {
  956. this.Metrics.Timer.prototype.done.called.should.equal(true)
  957. })
  958. })
  959. describe('when there is a conflict', function () {
  960. beforeEach(async function () {
  961. this.ProjectManager.promises.getProjectDocsAndFlushIfOld.rejects(
  962. new Errors.ProjectStateChangedError('project state changed')
  963. )
  964. await this.HttpController.getProjectDocsAndFlushIfOld(
  965. this.req,
  966. this.res,
  967. this.next
  968. )
  969. })
  970. it('should return an HTTP 409 Conflict response', function () {
  971. this.res.sendStatus.calledWith(409).should.equal(true)
  972. })
  973. })
  974. describe('when an error occurs', function () {
  975. beforeEach(async function () {
  976. this.ProjectManager.promises.getProjectDocsAndFlushIfOld.rejects(
  977. new Error('oops')
  978. )
  979. await this.HttpController.getProjectDocsAndFlushIfOld(
  980. this.req,
  981. this.res,
  982. this.next
  983. )
  984. })
  985. it('should call next with the error', function () {
  986. this.next.calledWith(sinon.match.instanceOf(Error)).should.equal(true)
  987. })
  988. })
  989. })
  990. describe('updateProject', function () {
  991. beforeEach(function () {
  992. this.projectHistoryId = 'history-id-123'
  993. this.userId = 'user-id-123'
  994. this.updates = [
  995. {
  996. type: 'rename-doc',
  997. id: 1,
  998. pathname: 'thesis.tex',
  999. newPathname: 'book.tex',
  1000. },
  1001. { type: 'add-doc', id: 2, pathname: 'article.tex', docLines: 'hello' },
  1002. {
  1003. type: 'rename-file',
  1004. id: 3,
  1005. pathname: 'apple.png',
  1006. newPathname: 'banana.png',
  1007. },
  1008. { type: 'add-file', id: 4, url: 'filestore.example.com/4' },
  1009. ]
  1010. this.version = 1234567
  1011. this.req = {
  1012. query: {},
  1013. body: {
  1014. projectHistoryId: this.projectHistoryId,
  1015. userId: this.userId,
  1016. updates: this.updates,
  1017. version: this.version,
  1018. source: this.source,
  1019. },
  1020. params: {
  1021. project_id: this.project_id,
  1022. },
  1023. }
  1024. })
  1025. describe('successfully', function () {
  1026. beforeEach(async function () {
  1027. await this.HttpController.updateProject(this.req, this.res, this.next)
  1028. })
  1029. it('should accept the change', function () {
  1030. this.ProjectManager.promises.updateProjectWithLocks.should.have.been.calledWith(
  1031. this.project_id,
  1032. this.projectHistoryId,
  1033. this.userId,
  1034. this.updates,
  1035. this.version,
  1036. this.source
  1037. )
  1038. })
  1039. it('should return a successful No Content response', function () {
  1040. this.res.sendStatus.calledWith(204).should.equal(true)
  1041. })
  1042. it('should time the request', function () {
  1043. this.Metrics.Timer.prototype.done.called.should.equal(true)
  1044. })
  1045. })
  1046. describe('when an errors occurs', function () {
  1047. beforeEach(async function () {
  1048. this.ProjectManager.promises.updateProjectWithLocks.rejects(
  1049. new Error('oops')
  1050. )
  1051. await this.HttpController.updateProject(this.req, this.res, this.next)
  1052. })
  1053. it('should call next with the error', function () {
  1054. this.next.calledWith(sinon.match.instanceOf(Error)).should.equal(true)
  1055. })
  1056. })
  1057. })
  1058. describe('resyncProjectHistory', function () {
  1059. beforeEach(function () {
  1060. this.projectHistoryId = 'history-id-123'
  1061. this.docs = sinon.stub()
  1062. this.files = sinon.stub()
  1063. this.fileUpdates = sinon.stub()
  1064. this.req = {
  1065. query: {},
  1066. body: {
  1067. projectHistoryId: this.projectHistoryId,
  1068. docs: this.docs,
  1069. files: this.files,
  1070. },
  1071. params: {
  1072. project_id: this.project_id,
  1073. },
  1074. }
  1075. })
  1076. describe('successfully', function () {
  1077. beforeEach(async function () {
  1078. await this.HttpController.resyncProjectHistory(
  1079. this.req,
  1080. this.res,
  1081. this.next
  1082. )
  1083. })
  1084. it('should accept the change', function () {
  1085. this.HistoryManager.promises.resyncProjectHistory.should.have.been.calledWith(
  1086. this.project_id,
  1087. this.projectHistoryId,
  1088. this.docs,
  1089. this.files,
  1090. {}
  1091. )
  1092. })
  1093. it('should return a successful No Content response', function () {
  1094. this.res.sendStatus.should.have.been.calledWith(204)
  1095. })
  1096. })
  1097. describe('when an errors occurs', function () {
  1098. beforeEach(async function () {
  1099. this.HistoryManager.promises.resyncProjectHistory.rejects(
  1100. new Error('oops')
  1101. )
  1102. await this.HttpController.resyncProjectHistory(
  1103. this.req,
  1104. this.res,
  1105. this.next
  1106. )
  1107. })
  1108. it('should call next with the error', function () {
  1109. this.next.calledWith(sinon.match.instanceOf(Error)).should.equal(true)
  1110. })
  1111. })
  1112. })
  1113. describe('appendToDoc', function () {
  1114. beforeEach(function () {
  1115. this.lines = ['one', 'two', 'three']
  1116. this.source = 'dropbox'
  1117. this.user_id = 'user-id-123'
  1118. this.req = {
  1119. headers: {},
  1120. params: {
  1121. project_id: this.project_id,
  1122. doc_id: this.doc_id,
  1123. },
  1124. query: {},
  1125. body: {
  1126. lines: this.lines,
  1127. source: this.source,
  1128. user_id: this.user_id,
  1129. undoing: (this.undoing = true),
  1130. },
  1131. }
  1132. })
  1133. describe('successfully', function () {
  1134. beforeEach(async function () {
  1135. this.DocumentManager.promises.appendToDocWithLock.resolves({
  1136. rev: '123',
  1137. })
  1138. await this.HttpController.appendToDoc(this.req, this.res, this.next)
  1139. })
  1140. it('should append to the doc', function () {
  1141. this.DocumentManager.promises.appendToDocWithLock.should.have.been.calledWith(
  1142. this.project_id,
  1143. this.doc_id,
  1144. this.lines,
  1145. this.source,
  1146. this.user_id
  1147. )
  1148. })
  1149. it('should return a json response with the document rev from web', function () {
  1150. this.res.json.calledWithMatch({ rev: '123' }).should.equal(true)
  1151. })
  1152. it('should log the request', function () {
  1153. this.logger.debug
  1154. .calledWith(
  1155. {
  1156. docId: this.doc_id,
  1157. projectId: this.project_id,
  1158. lines: this.lines,
  1159. source: this.source,
  1160. userId: this.user_id,
  1161. },
  1162. 'appending to doc via http'
  1163. )
  1164. .should.equal(true)
  1165. })
  1166. it('should time the request', function () {
  1167. this.Metrics.Timer.prototype.done.called.should.equal(true)
  1168. })
  1169. })
  1170. describe('when an errors occurs', function () {
  1171. beforeEach(async function () {
  1172. this.DocumentManager.promises.appendToDocWithLock.rejects(
  1173. new Error('oops')
  1174. )
  1175. await this.HttpController.appendToDoc(this.req, this.res, this.next)
  1176. })
  1177. it('should call next with the error', function () {
  1178. this.next.calledWith(sinon.match.instanceOf(Error)).should.equal(true)
  1179. })
  1180. })
  1181. describe('when the payload is too large', function () {
  1182. beforeEach(async function () {
  1183. this.DocumentManager.promises.appendToDocWithLock.rejects(
  1184. new Errors.FileTooLargeError()
  1185. )
  1186. await this.HttpController.appendToDoc(this.req, this.res, this.next)
  1187. })
  1188. it('should send back a 422 response', function () {
  1189. this.res.sendStatus.calledWith(422).should.equal(true)
  1190. })
  1191. })
  1192. })
  1193. })