HttpControllerTests.js 16 KB

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