HttpControllerTests.js 17 KB

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