HttpControllerTests.js 16 KB

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