HttpControllerTests.js 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576
  1. import sinon from 'sinon'
  2. import { strict as esmock } from 'esmock'
  3. import mongodb from 'mongodb-legacy'
  4. const { ObjectId } = mongodb
  5. const MODULE_PATH = '../../../../app/js/HttpController.js'
  6. describe('HttpController', function () {
  7. beforeEach(async function () {
  8. this.UpdatesProcessor = {
  9. processUpdatesForProject: sinon.stub().yields(),
  10. }
  11. this.SummarizedUpdatesManager = {
  12. getSummarizedProjectUpdates: sinon.stub(),
  13. }
  14. this.DiffManager = {
  15. getDiff: sinon.stub(),
  16. }
  17. this.HistoryStoreManager = {
  18. deleteProject: sinon.stub().yields(),
  19. getMostRecentVersion: sinon.stub(),
  20. getProjectBlobStream: sinon.stub(),
  21. initializeProject: sinon.stub(),
  22. }
  23. this.SnapshotManager = {
  24. getFileSnapshotStream: sinon.stub(),
  25. getProjectSnapshot: sinon.stub(),
  26. }
  27. this.HealthChecker = {}
  28. this.SyncManager = {
  29. clearResyncState: sinon.stub().yields(),
  30. startResync: sinon.stub().yields(),
  31. }
  32. this.WebApiManager = {
  33. getHistoryId: sinon.stub(),
  34. }
  35. this.RedisManager = {
  36. destroyDocUpdatesQueue: sinon.stub().yields(),
  37. clearFirstOpTimestamp: sinon.stub().yields(),
  38. clearCachedHistoryId: sinon.stub().yields(),
  39. }
  40. this.ErrorRecorder = {
  41. record: sinon.stub().yields(),
  42. }
  43. this.LabelsManager = {
  44. createLabel: sinon.stub(),
  45. deleteLabel: sinon.stub().yields(),
  46. deleteLabelForUser: sinon.stub().yields(),
  47. getLabels: sinon.stub(),
  48. }
  49. this.HistoryApiManager = {
  50. shouldUseProjectHistory: sinon.stub(),
  51. }
  52. this.RetryManager = {}
  53. this.FlushManager = {}
  54. this.request = {}
  55. this.pipeline = sinon.stub()
  56. this.HttpController = await esmock(MODULE_PATH, {
  57. request: this.request,
  58. stream: { pipeline: this.pipeline },
  59. '../../../../app/js/UpdatesProcessor.js': this.UpdatesProcessor,
  60. '../../../../app/js/SummarizedUpdatesManager.js':
  61. this.SummarizedUpdatesManager,
  62. '../../../../app/js/DiffManager.js': this.DiffManager,
  63. '../../../../app/js/HistoryStoreManager.js': this.HistoryStoreManager,
  64. '../../../../app/js/SnapshotManager.js': this.SnapshotManager,
  65. '../../../../app/js/HealthChecker.js': this.HealthChecker,
  66. '../../../../app/js/SyncManager.js': this.SyncManager,
  67. '../../../../app/js/WebApiManager.js': this.WebApiManager,
  68. '../../../../app/js/RedisManager.js': this.RedisManager,
  69. '../../../../app/js/ErrorRecorder.js': this.ErrorRecorder,
  70. '../../../../app/js/LabelsManager.js': this.LabelsManager,
  71. '../../../../app/js/HistoryApiManager.js': this.HistoryApiManager,
  72. '../../../../app/js/RetryManager.js': this.RetryManager,
  73. '../../../../app/js/FlushManager.js': this.FlushManager,
  74. })
  75. this.pathname = 'doc-id-123'
  76. this.projectId = new ObjectId().toString()
  77. this.projectOwnerId = new ObjectId().toString()
  78. this.next = sinon.stub()
  79. this.userId = new ObjectId().toString()
  80. this.now = Date.now()
  81. this.res = {
  82. json: sinon.stub(),
  83. send: sinon.stub(),
  84. sendStatus: sinon.stub(),
  85. setHeader: sinon.stub(),
  86. }
  87. })
  88. describe('getProjectBlob', function () {
  89. beforeEach(function () {
  90. this.blobHash = 'abcd'
  91. this.stream = {}
  92. this.HistoryStoreManager.getProjectBlobStream.yields(null, this.stream)
  93. this.HttpController.getProjectBlob(
  94. { params: { project_id: this.projectId, hash: this.blobHash } },
  95. this.res,
  96. this.next
  97. )
  98. })
  99. it('should get a blob stream', function () {
  100. this.HistoryStoreManager.getProjectBlobStream
  101. .calledWith(this.projectId, this.blobHash)
  102. .should.equal(true)
  103. this.pipeline.should.have.been.calledWith(this.stream, this.res)
  104. })
  105. it('should set caching header', function () {
  106. this.res.setHeader.should.have.been.calledWith(
  107. 'Cache-Control',
  108. 'private, max-age=86400'
  109. )
  110. })
  111. })
  112. describe('initializeProject', function () {
  113. beforeEach(function () {
  114. this.historyId = new ObjectId().toString()
  115. this.req = { body: { historyId: this.historyId } }
  116. this.HistoryStoreManager.initializeProject.yields(null, this.historyId)
  117. this.HttpController.initializeProject(this.req, this.res, this.next)
  118. })
  119. it('should initialize the project', function () {
  120. this.HistoryStoreManager.initializeProject.calledWith().should.equal(true)
  121. })
  122. it('should return the new overleaf id', function () {
  123. this.res.json
  124. .calledWith({ project: { id: this.historyId } })
  125. .should.equal(true)
  126. })
  127. })
  128. describe('flushProject', function () {
  129. beforeEach(function () {
  130. this.req = {
  131. params: {
  132. project_id: this.projectId,
  133. },
  134. query: {},
  135. }
  136. this.HttpController.flushProject(this.req, this.res, this.next)
  137. })
  138. it('should process the updates', function () {
  139. this.UpdatesProcessor.processUpdatesForProject
  140. .calledWith(this.projectId)
  141. .should.equal(true)
  142. })
  143. it('should return a success code', function () {
  144. this.res.sendStatus.calledWith(204).should.equal(true)
  145. })
  146. })
  147. describe('getDiff', function () {
  148. beforeEach(function () {
  149. this.from = 42
  150. this.to = 45
  151. this.req = {
  152. params: {
  153. project_id: this.projectId,
  154. },
  155. query: {
  156. pathname: this.pathname,
  157. from: this.from,
  158. to: this.to,
  159. },
  160. }
  161. this.diff = [{ u: 'mock-diff' }]
  162. this.DiffManager.getDiff.yields(null, this.diff)
  163. this.HttpController.getDiff(this.req, this.res, this.next)
  164. })
  165. it('should get the diff', function () {
  166. this.DiffManager.getDiff.should.have.been.calledWith(
  167. this.projectId,
  168. this.pathname,
  169. this.from,
  170. this.to
  171. )
  172. })
  173. it('should return the diff', function () {
  174. this.res.json.calledWith({ diff: this.diff }).should.equal(true)
  175. })
  176. })
  177. describe('getUpdates', function () {
  178. beforeEach(function () {
  179. this.before = Date.now()
  180. this.nextBeforeTimestamp = this.before - 100
  181. this.min_count = 10
  182. this.req = {
  183. params: {
  184. project_id: this.projectId,
  185. },
  186. query: {
  187. before: this.before,
  188. min_count: this.min_count,
  189. },
  190. }
  191. this.updates = [{ i: 'mock-summarized-updates', p: 10 }]
  192. this.SummarizedUpdatesManager.getSummarizedProjectUpdates.yields(
  193. null,
  194. this.updates,
  195. this.nextBeforeTimestamp
  196. )
  197. this.HttpController.getUpdates(this.req, this.res, this.next)
  198. })
  199. it('should get the updates', function () {
  200. this.SummarizedUpdatesManager.getSummarizedProjectUpdates.should.have.been.calledWith(
  201. this.projectId,
  202. {
  203. before: this.before,
  204. min_count: this.min_count,
  205. }
  206. )
  207. })
  208. it('should return the formatted updates', function () {
  209. this.res.json.should.have.been.calledWith({
  210. updates: this.updates,
  211. nextBeforeTimestamp: this.nextBeforeTimestamp,
  212. })
  213. })
  214. })
  215. describe('latestVersion', function () {
  216. beforeEach(function () {
  217. this.historyId = 1234
  218. this.req = {
  219. params: {
  220. project_id: this.projectId,
  221. },
  222. }
  223. this.version = 99
  224. this.lastChange = {
  225. v2Authors: ['1234'],
  226. timestamp: '2016-08-16T10:44:40.227Z',
  227. }
  228. this.versionInfo = {
  229. version: this.version,
  230. v2Authors: ['1234'],
  231. timestamp: '2016-08-16T10:44:40.227Z',
  232. }
  233. this.WebApiManager.getHistoryId.yields(null, this.historyId)
  234. this.HistoryStoreManager.getMostRecentVersion.yields(
  235. null,
  236. this.version,
  237. {},
  238. this.lastChange
  239. )
  240. this.HttpController.latestVersion(this.req, this.res, this.next)
  241. })
  242. it('should process the updates', function () {
  243. this.UpdatesProcessor.processUpdatesForProject
  244. .calledWith(this.projectId)
  245. .should.equal(true)
  246. })
  247. it('should get the ol project id', function () {
  248. this.WebApiManager.getHistoryId
  249. .calledWith(this.projectId)
  250. .should.equal(true)
  251. })
  252. it('should get the latest version', function () {
  253. this.HistoryStoreManager.getMostRecentVersion
  254. .calledWith(this.projectId, this.historyId)
  255. .should.equal(true)
  256. })
  257. it('should return version number', function () {
  258. this.res.json.calledWith(this.versionInfo).should.equal(true)
  259. })
  260. })
  261. describe('resyncProject', function () {
  262. beforeEach(function () {
  263. this.req = {
  264. params: {
  265. project_id: this.projectId,
  266. },
  267. query: {},
  268. body: {},
  269. }
  270. this.HttpController.resyncProject(this.req, this.res, this.next)
  271. })
  272. it('should resync the project', function () {
  273. this.SyncManager.startResync.calledWith(this.projectId).should.equal(true)
  274. })
  275. it('should flush the queue', function () {
  276. this.UpdatesProcessor.processUpdatesForProject
  277. .calledWith(this.projectId)
  278. .should.equal(true)
  279. })
  280. it('should return 204', function () {
  281. this.res.sendStatus.calledWith(204).should.equal(true)
  282. })
  283. })
  284. describe('getFileSnapshot', function () {
  285. beforeEach(function () {
  286. this.version = 42
  287. this.pathname = 'foo.tex'
  288. this.req = {
  289. params: {
  290. project_id: this.projectId,
  291. version: this.version,
  292. pathname: this.pathname,
  293. },
  294. }
  295. this.res = { mock: 'res' }
  296. this.stream = {}
  297. this.SnapshotManager.getFileSnapshotStream.yields(null, this.stream)
  298. this.HttpController.getFileSnapshot(this.req, this.res, this.next)
  299. })
  300. it('should get the snapshot', function () {
  301. this.SnapshotManager.getFileSnapshotStream.should.have.been.calledWith(
  302. this.projectId,
  303. this.version,
  304. this.pathname
  305. )
  306. })
  307. it('should pipe the returned stream into the response', function () {
  308. this.pipeline.should.have.been.calledWith(this.stream, this.res)
  309. })
  310. })
  311. describe('getProjectSnapshot', function () {
  312. beforeEach(function () {
  313. this.version = 42
  314. this.req = {
  315. params: {
  316. project_id: this.projectId,
  317. version: this.version,
  318. },
  319. }
  320. this.res = { json: sinon.stub() }
  321. this.snapshotData = { one: 1 }
  322. this.SnapshotManager.getProjectSnapshot.yields(null, this.snapshotData)
  323. this.HttpController.getProjectSnapshot(this.req, this.res, this.next)
  324. })
  325. it('should get the snapshot', function () {
  326. this.SnapshotManager.getProjectSnapshot.should.have.been.calledWith(
  327. this.projectId,
  328. this.version
  329. )
  330. })
  331. it('should send json response', function () {
  332. this.res.json.calledWith(this.snapshotData).should.equal(true)
  333. })
  334. })
  335. describe('getLabels', function () {
  336. beforeEach(function () {
  337. this.req = {
  338. params: {
  339. project_id: this.projectId,
  340. },
  341. }
  342. this.labels = ['label-1', 'label-2']
  343. this.LabelsManager.getLabels.yields(null, this.labels)
  344. })
  345. describe('project history is enabled', function () {
  346. beforeEach(function () {
  347. this.HistoryApiManager.shouldUseProjectHistory.yields(null, true)
  348. this.HttpController.getLabels(this.req, this.res, this.next)
  349. })
  350. it('should get the labels for a project', function () {
  351. this.LabelsManager.getLabels
  352. .calledWith(this.projectId)
  353. .should.equal(true)
  354. })
  355. it('should return the labels', function () {
  356. this.res.json.calledWith(this.labels).should.equal(true)
  357. })
  358. })
  359. describe('project history is not enabled', function () {
  360. beforeEach(function () {
  361. this.HistoryApiManager.shouldUseProjectHistory.yields(null, false)
  362. this.HttpController.getLabels(this.req, this.res, this.next)
  363. })
  364. it('should return 409', function () {
  365. this.res.sendStatus.calledWith(409).should.equal(true)
  366. })
  367. })
  368. })
  369. describe('createLabel', function () {
  370. beforeEach(function () {
  371. this.req = {
  372. params: {
  373. project_id: this.projectId,
  374. user_id: this.userId,
  375. },
  376. body: {
  377. version: (this.version = 'label-1'),
  378. comment: (this.comment = 'a comment'),
  379. created_at: (this.created_at = Date.now().toString()),
  380. validate_exists: true,
  381. },
  382. }
  383. this.label = { _id: new ObjectId() }
  384. this.LabelsManager.createLabel.yields(null, this.label)
  385. })
  386. describe('project history is enabled', function () {
  387. beforeEach(function () {
  388. this.HistoryApiManager.shouldUseProjectHistory.yields(null, true)
  389. this.HttpController.createLabel(this.req, this.res, this.next)
  390. })
  391. it('should create a label for a project', function () {
  392. this.LabelsManager.createLabel.should.have.been.calledWith(
  393. this.projectId,
  394. this.userId,
  395. this.version,
  396. this.comment,
  397. this.created_at,
  398. true
  399. )
  400. })
  401. it('should return the label', function () {
  402. this.res.json.calledWith(this.label).should.equal(true)
  403. })
  404. })
  405. describe('validate_exists = false is passed', function () {
  406. beforeEach(function () {
  407. this.req.body.validate_exists = false
  408. this.HistoryApiManager.shouldUseProjectHistory.yields(null, true)
  409. this.HttpController.createLabel(this.req, this.res, this.next)
  410. })
  411. it('should create a label for a project', function () {
  412. this.LabelsManager.createLabel
  413. .calledWith(
  414. this.projectId,
  415. this.userId,
  416. this.version,
  417. this.comment,
  418. this.created_at,
  419. false
  420. )
  421. .should.equal(true)
  422. })
  423. it('should return the label', function () {
  424. this.res.json.calledWith(this.label).should.equal(true)
  425. })
  426. })
  427. describe('project history is not enabled', function () {
  428. beforeEach(function () {
  429. this.HistoryApiManager.shouldUseProjectHistory.yields(null, false)
  430. this.HttpController.createLabel(this.req, this.res, this.next)
  431. })
  432. it('should return 409', function () {
  433. this.res.sendStatus.calledWith(409).should.equal(true)
  434. })
  435. })
  436. })
  437. describe('deleteLabelForUser', function () {
  438. beforeEach(function () {
  439. this.req = {
  440. params: {
  441. project_id: this.projectId,
  442. user_id: this.userId,
  443. label_id: (this.label_id = new ObjectId()),
  444. },
  445. }
  446. this.HttpController.deleteLabelForUser(this.req, this.res, this.next)
  447. })
  448. it('should delete a label for a project', function () {
  449. this.LabelsManager.deleteLabelForUser
  450. .calledWith(this.projectId, this.userId, this.label_id)
  451. .should.equal(true)
  452. })
  453. it('should return 204', function () {
  454. this.res.sendStatus.calledWith(204).should.equal(true)
  455. })
  456. })
  457. describe('deleteLabel', function () {
  458. beforeEach(function () {
  459. this.req = {
  460. params: {
  461. project_id: this.projectId,
  462. label_id: (this.label_id = new ObjectId()),
  463. },
  464. }
  465. this.HttpController.deleteLabel(this.req, this.res, this.next)
  466. })
  467. it('should delete a label for a project', function () {
  468. this.LabelsManager.deleteLabel
  469. .calledWith(this.projectId, this.label_id)
  470. .should.equal(true)
  471. })
  472. it('should return 204', function () {
  473. this.res.sendStatus.calledWith(204).should.equal(true)
  474. })
  475. })
  476. describe('deleteProject', function () {
  477. beforeEach(function () {
  478. this.req = {
  479. params: {
  480. project_id: this.projectId,
  481. },
  482. }
  483. this.WebApiManager.getHistoryId
  484. .withArgs(this.projectId)
  485. .yields(null, this.historyId)
  486. this.HttpController.deleteProject(this.req, this.res, this.next)
  487. })
  488. it('should delete the updates queue', function () {
  489. this.RedisManager.destroyDocUpdatesQueue.should.have.been.calledWith(
  490. this.projectId
  491. )
  492. })
  493. it('should clear the first op timestamp', function () {
  494. this.RedisManager.clearFirstOpTimestamp.should.have.been.calledWith(
  495. this.projectId
  496. )
  497. })
  498. it('should clear the cached history id', function () {
  499. this.RedisManager.clearCachedHistoryId.should.have.been.calledWith(
  500. this.projectId
  501. )
  502. })
  503. it('should clear the resync state', function () {
  504. this.SyncManager.clearResyncState.should.have.been.calledWith(
  505. this.projectId
  506. )
  507. })
  508. it('should clear any failure record', function () {
  509. this.ErrorRecorder.record.should.have.been.calledWith(
  510. this.projectId,
  511. 0,
  512. null
  513. )
  514. })
  515. })
  516. })