HttpControllerTests.js 38 KB

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