HistoryControllerTests.js 7.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265
  1. const sinon = require('sinon')
  2. const { expect } = require('chai')
  3. const { RequestFailedError } = require('@overleaf/fetch-utils')
  4. const Errors = require('../../../../app/src/Features/Errors/Errors')
  5. const modulePath = '../../../../app/src/Features/History/HistoryController'
  6. const SandboxedModule = require('sandboxed-module')
  7. const { ObjectId } = require('mongodb-legacy')
  8. describe('HistoryController', function () {
  9. beforeEach(function () {
  10. this.callback = sinon.stub()
  11. this.user_id = 'user-id-123'
  12. this.project_id = 'mock-project-id'
  13. this.stream = sinon.stub()
  14. this.fetchResponse = {
  15. headers: {
  16. get: sinon.stub(),
  17. },
  18. }
  19. this.next = sinon.stub()
  20. this.SessionManager = {
  21. getLoggedInUserId: sinon.stub().returns(this.user_id),
  22. }
  23. this.Stream = {
  24. pipeline: sinon.stub().resolves(),
  25. }
  26. this.HistoryManager = {
  27. promises: {
  28. injectUserDetails: sinon.stub(),
  29. },
  30. }
  31. this.ProjectEntityUpdateHandler = {
  32. promises: {
  33. resyncProjectHistory: sinon.stub().resolves(),
  34. },
  35. }
  36. this.fetchJson = sinon.stub()
  37. this.fetchStream = sinon.stub().resolves(this.stream)
  38. this.fetchStreamWithResponse = sinon
  39. .stub()
  40. .resolves({ stream: this.stream, response: this.fetchResponse })
  41. this.fetchNothing = sinon.stub().resolves()
  42. this.HistoryController = SandboxedModule.require(modulePath, {
  43. requires: {
  44. 'stream/promises': this.Stream,
  45. '@overleaf/settings': (this.settings = {}),
  46. '@overleaf/fetch-utils': {
  47. fetchJson: this.fetchJson,
  48. fetchStream: this.fetchStream,
  49. fetchStreamWithResponse: this.fetchStreamWithResponse,
  50. fetchNothing: this.fetchNothing,
  51. },
  52. '@overleaf/Metrics': {},
  53. '../../infrastructure/mongodb': { ObjectId },
  54. '../Authentication/SessionManager': this.SessionManager,
  55. './HistoryManager': this.HistoryManager,
  56. '../Project/ProjectDetailsHandler': (this.ProjectDetailsHandler = {}),
  57. '../Project/ProjectEntityUpdateHandler':
  58. this.ProjectEntityUpdateHandler,
  59. '../User/UserGetter': (this.UserGetter = {}),
  60. '../Project/ProjectGetter': (this.ProjectGetter = {}),
  61. './RestoreManager': (this.RestoreManager = {}),
  62. '../../infrastructure/Features': (this.Features = sinon
  63. .stub()
  64. .withArgs('saas')
  65. .returns(true)),
  66. },
  67. })
  68. this.settings.apis = {
  69. project_history: {
  70. url: 'http://project_history.example.com',
  71. },
  72. }
  73. })
  74. describe('proxyToHistoryApi', function () {
  75. beforeEach(async function () {
  76. this.req = { url: '/mock/url', method: 'POST', session: sinon.stub() }
  77. this.res = {
  78. set: sinon.stub(),
  79. }
  80. this.contentType = 'application/json'
  81. this.contentLength = 212
  82. this.fetchResponse.headers.get
  83. .withArgs('Content-Type')
  84. .returns(this.contentType)
  85. this.fetchResponse.headers.get
  86. .withArgs('Content-Length')
  87. .returns(this.contentLength)
  88. await this.HistoryController.proxyToHistoryApi(
  89. this.req,
  90. this.res,
  91. this.next
  92. )
  93. })
  94. it('should get the user id', function () {
  95. this.SessionManager.getLoggedInUserId.should.have.been.calledWith(
  96. this.req.session
  97. )
  98. })
  99. it('should call the project history api', function () {
  100. this.fetchStreamWithResponse.should.have.been.calledWith(
  101. `${this.settings.apis.project_history.url}${this.req.url}`,
  102. {
  103. method: this.req.method,
  104. headers: {
  105. 'X-User-Id': this.user_id,
  106. },
  107. }
  108. )
  109. })
  110. it('should pipe the response to the client', function () {
  111. expect(this.Stream.pipeline).to.have.been.calledWith(
  112. this.stream,
  113. this.res
  114. )
  115. })
  116. it('should propagate the appropriate headers', function () {
  117. expect(this.res.set).to.have.been.calledWith(
  118. 'Content-Type',
  119. this.contentType
  120. )
  121. expect(this.res.set).to.have.been.calledWith(
  122. 'Content-Length',
  123. this.contentLength
  124. )
  125. })
  126. })
  127. describe('proxyToHistoryApiAndInjectUserDetails', function () {
  128. beforeEach(async function () {
  129. this.req = { url: '/mock/url', method: 'POST' }
  130. this.res = { json: sinon.stub() }
  131. this.data = 'mock-data'
  132. this.dataWithUsers = 'mock-injected-data'
  133. this.fetchJson.resolves(this.data)
  134. this.HistoryManager.promises.injectUserDetails.resolves(
  135. this.dataWithUsers
  136. )
  137. await this.HistoryController.proxyToHistoryApiAndInjectUserDetails(
  138. this.req,
  139. this.res,
  140. this.next
  141. )
  142. })
  143. it('should get the user id', function () {
  144. this.SessionManager.getLoggedInUserId.should.have.been.calledWith(
  145. this.req.session
  146. )
  147. })
  148. it('should call the project history api', function () {
  149. this.fetchJson.should.have.been.calledWith(
  150. `${this.settings.apis.project_history.url}${this.req.url}`,
  151. {
  152. method: this.req.method,
  153. headers: {
  154. 'X-User-Id': this.user_id,
  155. },
  156. }
  157. )
  158. })
  159. it('should inject the user data', function () {
  160. this.HistoryManager.promises.injectUserDetails.should.have.been.calledWith(
  161. this.data
  162. )
  163. })
  164. it('should return the data with users to the client', function () {
  165. this.res.json.should.have.been.calledWith(this.dataWithUsers)
  166. })
  167. })
  168. describe('proxyToHistoryApiAndInjectUserDetails (with the history API failing)', function () {
  169. beforeEach(async function () {
  170. this.url = '/mock/url'
  171. this.req = { url: this.url, method: 'POST' }
  172. this.res = { json: sinon.stub() }
  173. this.err = new RequestFailedError(this.url, {}, { status: 500 })
  174. this.fetchJson.rejects(this.err)
  175. await this.HistoryController.proxyToHistoryApiAndInjectUserDetails(
  176. this.req,
  177. this.res,
  178. this.next
  179. )
  180. })
  181. it('should not inject the user data', function () {
  182. this.HistoryManager.promises.injectUserDetails.should.not.have.been.called
  183. })
  184. it('should not return the data with users to the client', function () {
  185. this.res.json.should.not.have.been.called
  186. })
  187. it('should throw an error', function () {
  188. this.next.should.have.been.calledWith(this.err)
  189. })
  190. })
  191. describe('resyncProjectHistory', function () {
  192. describe('for a project without project-history enabled', function () {
  193. beforeEach(async function () {
  194. this.req = { params: { Project_id: this.project_id }, body: {} }
  195. this.res = { setTimeout: sinon.stub(), sendStatus: sinon.stub() }
  196. this.error = new Errors.ProjectHistoryDisabledError()
  197. this.ProjectEntityUpdateHandler.promises.resyncProjectHistory.rejects(
  198. this.error
  199. )
  200. await this.HistoryController.resyncProjectHistory(
  201. this.req,
  202. this.res,
  203. this.next
  204. )
  205. })
  206. it('response with a 404', function () {
  207. this.res.sendStatus.should.have.been.calledWith(404)
  208. })
  209. })
  210. describe('for a project with project-history enabled', function () {
  211. beforeEach(async function () {
  212. this.req = { params: { Project_id: this.project_id }, body: {} }
  213. this.res = { setTimeout: sinon.stub(), sendStatus: sinon.stub() }
  214. await this.HistoryController.resyncProjectHistory(
  215. this.req,
  216. this.res,
  217. this.next
  218. )
  219. })
  220. it('sets an extended response timeout', function () {
  221. this.res.setTimeout.should.have.been.calledWith(6 * 60 * 1000)
  222. })
  223. it('resyncs the project', function () {
  224. this.ProjectEntityUpdateHandler.promises.resyncProjectHistory.should.have.been.calledWith(
  225. this.project_id
  226. )
  227. })
  228. it('responds with a 204', function () {
  229. this.res.sendStatus.should.have.been.calledWith(204)
  230. })
  231. })
  232. })
  233. })