ClsiCookieManagerTests.js 11 KB

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