HttpControllerTests.js 16 KB

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