HttpControllerTests.js 16 KB

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