ClsiCookieManagerTests.js 9.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314
  1. const sinon = require('sinon')
  2. const { expect } = require('chai')
  3. const modulePath = '../../../../app/src/Features/Compile/ClsiCookieManager.js'
  4. const SandboxedModule = require('sandboxed-module')
  5. describe('ClsiCookieManager', function () {
  6. beforeEach(function () {
  7. this.redis = {
  8. auth() {},
  9. get: sinon.stub(),
  10. setex: sinon.stub().resolves(),
  11. }
  12. this.project_id = '123423431321-proj-id'
  13. this.user_id = 'abc-user-id'
  14. this.fetchUtils = {
  15. fetchNothing: sinon.stub().returns(Promise.resolve()),
  16. fetchStringWithResponse: sinon.stub().returns(Promise.resolve()),
  17. }
  18. this.metrics = { inc: sinon.stub() }
  19. this.settings = {
  20. redis: {
  21. web: 'redis.something',
  22. },
  23. apis: {
  24. clsi: {
  25. url: 'http://clsi.example.com',
  26. },
  27. },
  28. clsiCookie: {
  29. ttlInSeconds: Math.random().toString(),
  30. ttlInSecondsRegular: Math.random().toString(),
  31. key: 'coooookie',
  32. },
  33. }
  34. this.requires = {
  35. '../../infrastructure/RedisWrapper': (this.RedisWrapper = {
  36. client: () => this.redis,
  37. }),
  38. '@overleaf/settings': this.settings,
  39. '@overleaf/fetch-utils': this.fetchUtils,
  40. '@overleaf/metrics': this.metrics,
  41. }
  42. this.ClsiCookieManager = SandboxedModule.require(modulePath, {
  43. requires: this.requires,
  44. })()
  45. })
  46. describe('getServerId', function () {
  47. it('should call get for the key', async function () {
  48. this.redis.get.resolves('clsi-7')
  49. const serverId = await this.ClsiCookieManager.promises.getServerId(
  50. this.project_id,
  51. this.user_id,
  52. '',
  53. 'e2'
  54. )
  55. this.redis.get
  56. .calledWith(`clsiserver:${this.project_id}:${this.user_id}`)
  57. .should.equal(true)
  58. serverId.should.equal('clsi-7')
  59. })
  60. it('should _populateServerIdViaRequest if no key is found', async function () {
  61. this.ClsiCookieManager.promises._populateServerIdViaRequest = sinon
  62. .stub()
  63. .resolves()
  64. this.redis.get.resolves(null)
  65. await this.ClsiCookieManager.promises.getServerId(
  66. this.project_id,
  67. this.user_id,
  68. ''
  69. )
  70. this.ClsiCookieManager.promises._populateServerIdViaRequest
  71. .calledWith(this.project_id, this.user_id)
  72. .should.equal(true)
  73. })
  74. it('should _populateServerIdViaRequest if no key is blank', async function () {
  75. this.ClsiCookieManager.promises._populateServerIdViaRequest = sinon
  76. .stub()
  77. .resolves(null)
  78. this.redis.get.resolves('')
  79. await this.ClsiCookieManager.promises.getServerId(
  80. this.project_id,
  81. this.user_id,
  82. '',
  83. 'e2'
  84. )
  85. this.ClsiCookieManager.promises._populateServerIdViaRequest
  86. .calledWith(this.project_id, this.user_id)
  87. .should.equal(true)
  88. })
  89. })
  90. describe('_populateServerIdViaRequest', function () {
  91. beforeEach(function () {
  92. this.clsiServerId = 'server-id'
  93. this.ClsiCookieManager.promises.setServerId = sinon.stub().resolves()
  94. })
  95. describe('with a server id in the response', function () {
  96. beforeEach(function () {
  97. this.response = {
  98. headers: {
  99. 'set-cookie': [
  100. `${this.settings.clsiCookie.key}=${this.clsiServerId}`,
  101. ],
  102. },
  103. }
  104. this.fetchUtils.fetchNothing.returns(this.response)
  105. })
  106. it('should make a request to the clsi', async function () {
  107. await this.ClsiCookieManager.promises._populateServerIdViaRequest(
  108. this.project_id,
  109. this.user_id,
  110. 'standard',
  111. 'e2'
  112. )
  113. const args = this.ClsiCookieManager.promises.setServerId.args[0]
  114. args[0].should.equal(this.project_id)
  115. args[1].should.equal(this.user_id)
  116. args[2].should.equal('standard')
  117. args[3].should.equal('e2')
  118. args[4].should.deep.equal(this.clsiServerId)
  119. })
  120. it('should return the server id', async function () {
  121. const serverId =
  122. await this.ClsiCookieManager.promises._populateServerIdViaRequest(
  123. this.project_id,
  124. this.user_id,
  125. '',
  126. 'e2'
  127. )
  128. serverId.should.equal(this.clsiServerId)
  129. })
  130. })
  131. describe('without a server id in the response', function () {
  132. beforeEach(function () {
  133. this.response = { headers: {} }
  134. this.fetchUtils.fetchNothing.returns(this.response)
  135. })
  136. it('should not set the server id there is no server id in the response', async function () {
  137. this.ClsiCookieManager._parseServerIdFromResponse = sinon
  138. .stub()
  139. .returns(null)
  140. await this.ClsiCookieManager.promises.setServerId(
  141. this.project_id,
  142. this.user_id,
  143. 'standard',
  144. 'e2',
  145. this.clsiServerId,
  146. null
  147. )
  148. this.redis.setex.called.should.equal(false)
  149. })
  150. })
  151. })
  152. describe('setServerId', function () {
  153. beforeEach(function () {
  154. this.clsiServerId = 'server-id'
  155. this.ClsiCookieManager._parseServerIdFromResponse = sinon
  156. .stub()
  157. .returns('clsi-8')
  158. })
  159. it('should set the server id with a ttl', async function () {
  160. await this.ClsiCookieManager.promises.setServerId(
  161. this.project_id,
  162. this.user_id,
  163. 'standard',
  164. 'e2',
  165. this.clsiServerId,
  166. null
  167. )
  168. this.redis.setex.should.have.been.calledWith(
  169. `clsiserver:${this.project_id}:${this.user_id}`,
  170. this.settings.clsiCookie.ttlInSeconds,
  171. this.clsiServerId
  172. )
  173. })
  174. it('should set the server id with the regular ttl for reg instance', async function () {
  175. this.clsiServerId = 'clsi-reg-8'
  176. await this.ClsiCookieManager.promises.setServerId(
  177. this.project_id,
  178. this.user_id,
  179. 'standard',
  180. 'e2',
  181. this.clsiServerId,
  182. null
  183. )
  184. expect(this.redis.setex).to.have.been.calledWith(
  185. `clsiserver:${this.project_id}:${this.user_id}`,
  186. this.settings.clsiCookie.ttlInSecondsRegular,
  187. this.clsiServerId
  188. )
  189. })
  190. it('should not set the server id if clsiCookies are not enabled', async function () {
  191. delete this.settings.clsiCookie.key
  192. this.ClsiCookieManager2 = SandboxedModule.require(modulePath, {
  193. globals: {
  194. console,
  195. },
  196. requires: this.requires,
  197. })()
  198. await this.ClsiCookieManager2.promises.setServerId(
  199. this.project_id,
  200. this.user_id,
  201. 'standard',
  202. 'e2',
  203. this.clsiServerId,
  204. null
  205. )
  206. this.redis.setex.called.should.equal(false)
  207. })
  208. it('should also set in the secondary if secondary redis is enabled', async function () {
  209. this.redis_secondary = { setex: sinon.stub().resolves() }
  210. this.settings.redis.clsi_cookie_secondary = {}
  211. this.RedisWrapper.client = sinon.stub()
  212. this.RedisWrapper.client.withArgs('clsi_cookie').returns(this.redis)
  213. this.RedisWrapper.client
  214. .withArgs('clsi_cookie_secondary')
  215. .returns(this.redis_secondary)
  216. this.ClsiCookieManager2 = SandboxedModule.require(modulePath, {
  217. globals: {
  218. console,
  219. },
  220. requires: this.requires,
  221. })()
  222. this.ClsiCookieManager2._parseServerIdFromResponse = sinon
  223. .stub()
  224. .returns('clsi-8')
  225. await this.ClsiCookieManager2.promises.setServerId(
  226. this.project_id,
  227. this.user_id,
  228. 'standard',
  229. 'e2',
  230. this.clsiServerId,
  231. null
  232. )
  233. this.redis_secondary.setex.should.have.been.calledWith(
  234. `clsiserver:${this.project_id}:${this.user_id}`,
  235. this.settings.clsiCookie.ttlInSeconds,
  236. this.clsiServerId
  237. )
  238. })
  239. describe('checkIsLoadSheddingEvent', function () {
  240. beforeEach(function () {
  241. this.fetchUtils.fetchStringWithResponse.reset()
  242. this.call = async () => {
  243. await this.ClsiCookieManager.promises.setServerId(
  244. this.project_id,
  245. this.user_id,
  246. 'standard',
  247. 'e2',
  248. this.clsiServerId,
  249. 'previous-clsi-server-id'
  250. )
  251. expect(
  252. this.fetchUtils.fetchStringWithResponse
  253. ).to.have.been.calledWith(
  254. `${this.settings.apis.clsi.url}/instance-state?clsiserverid=previous-clsi-server-id&compileGroup=standard&compileBackendClass=e2`,
  255. { method: 'GET', signal: sinon.match.instanceOf(AbortSignal) }
  256. )
  257. }
  258. })
  259. it('should report "load-shedding" when previous is UP', async function () {
  260. this.fetchUtils.fetchStringWithResponse.resolves({
  261. response: { status: 200 },
  262. body: 'previous-clsi-server-id,UP\n',
  263. })
  264. await this.call()
  265. expect(this.metrics.inc).to.have.been.calledWith(
  266. 'clsi-lb-switch-backend',
  267. 1,
  268. { status: 'load-shedding' }
  269. )
  270. })
  271. it('should report "cycle" when other is UP', async function () {
  272. this.fetchUtils.fetchStringWithResponse.resolves({
  273. response: { status: 200 },
  274. body: 'other-clsi-server-id,UP\n',
  275. })
  276. await this.call()
  277. expect(this.metrics.inc).to.have.been.calledWith(
  278. 'clsi-lb-switch-backend',
  279. 1,
  280. { status: 'cycle' }
  281. )
  282. })
  283. it('should report "cycle" when previous is 404', async function () {
  284. this.fetchUtils.fetchStringWithResponse.resolves({
  285. response: { status: 404 },
  286. })
  287. await this.call()
  288. expect(this.metrics.inc).to.have.been.calledWith(
  289. 'clsi-lb-switch-backend',
  290. 1,
  291. { status: 'cycle' }
  292. )
  293. })
  294. })
  295. })
  296. })