SplitTestHandlerTests.js 9.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343
  1. const Path = require('path')
  2. const SandboxedModule = require('sandboxed-module')
  3. const sinon = require('sinon')
  4. const { ObjectId } = require('mongodb-legacy')
  5. const { assert, expect } = require('chai')
  6. const MockRequest = require('../helpers/MockRequest')
  7. const MockResponse = require('../helpers/MockResponse')
  8. const MODULE_PATH = Path.join(
  9. __dirname,
  10. '../../../../app/src/Features/SplitTests/SplitTestHandler'
  11. )
  12. describe('SplitTestHandler', function () {
  13. beforeEach(function () {
  14. this.splitTests = [
  15. makeSplitTest('active-test'),
  16. makeSplitTest('not-active-test', { active: false }),
  17. makeSplitTest('legacy-test'),
  18. makeSplitTest('no-analytics-test-1', { analyticsEnabled: false }),
  19. makeSplitTest('no-analytics-test-2', {
  20. analyticsEnabled: false,
  21. versionNumber: 2,
  22. }),
  23. ]
  24. this.cachedSplitTests = new Map()
  25. for (const splitTest of this.splitTests) {
  26. this.cachedSplitTests.set(splitTest.name, splitTest)
  27. }
  28. this.SplitTest = {
  29. find: sinon.stub().returns({
  30. exec: sinon.stub().resolves(this.splitTests),
  31. }),
  32. }
  33. this.SplitTestCache = {
  34. get: sinon.stub().resolves({}),
  35. }
  36. this.SplitTestCache.get.resolves(this.cachedSplitTests)
  37. this.Settings = {
  38. moduleImportSequence: [],
  39. overleaf: {},
  40. devToolbar: {
  41. enabled: false,
  42. },
  43. }
  44. this.AnalyticsManager = {
  45. getIdsFromSession: sinon.stub(),
  46. setUserPropertyForAnalyticsId: sinon.stub().resolves(),
  47. }
  48. this.LocalsHelper = {
  49. setSplitTestVariant: sinon.stub(),
  50. setSplitTestInfo: sinon.stub(),
  51. }
  52. this.SplitTestSessionHandler = {
  53. collectSessionStats: sinon.stub(),
  54. }
  55. this.SplitTestUserGetter = {
  56. promises: {
  57. getUser: sinon.stub().resolves(null),
  58. },
  59. }
  60. this.SplitTestHandler = SandboxedModule.require(MODULE_PATH, {
  61. requires: {
  62. '../User/UserGetter': this.UserGetter,
  63. './SplitTestCache': this.SplitTestCache,
  64. '../../models/SplitTest': { SplitTest: this.SplitTest },
  65. '../User/UserUpdater': {},
  66. '../Analytics/AnalyticsManager': this.AnalyticsManager,
  67. './LocalsHelper': this.LocalsHelper,
  68. './SplitTestSessionHandler': this.SplitTestSessionHandler,
  69. './SplitTestUserGetter': this.SplitTestUserGetter,
  70. '@overleaf/settings': this.Settings,
  71. },
  72. })
  73. this.req = new MockRequest()
  74. this.res = new MockResponse()
  75. })
  76. describe('with an existing user', function () {
  77. beforeEach(async function () {
  78. this.user = {
  79. _id: new ObjectId(),
  80. splitTests: {
  81. 'active-test': [
  82. {
  83. variantName: 'default',
  84. versionNumber: 1,
  85. assignedAt: 'active-test-assigned-at',
  86. },
  87. ],
  88. 'legacy-test': 'legacy-variant',
  89. 'inactive-test': [{ variantName: 'trythis' }],
  90. 'unknown-test': [{ variantName: 'trythis' }],
  91. 'no-analytics-test-2': [
  92. {
  93. variantName: 'some-variant',
  94. versionNumber: 1,
  95. assignedAt: 'no-analytics-assigned-at',
  96. },
  97. ],
  98. },
  99. }
  100. this.SplitTestUserGetter.promises.getUser.resolves(this.user)
  101. this.assignments =
  102. await this.SplitTestHandler.promises.getActiveAssignmentsForUser(
  103. this.user._id
  104. )
  105. })
  106. it('handles the legacy assignment format', function () {
  107. expect(this.assignments['legacy-test']).to.deep.equal({
  108. variantName: 'variant-1',
  109. phase: 'release',
  110. versionNumber: 1,
  111. })
  112. })
  113. it('returns the current assignment for each active test', function () {
  114. expect(this.assignments['active-test']).to.deep.equal({
  115. variantName: 'variant-1',
  116. phase: 'release',
  117. versionNumber: 1,
  118. assignedAt: 'active-test-assigned-at',
  119. })
  120. })
  121. it('returns the current assignment for tests with analytics disabled', function () {
  122. expect(this.assignments['no-analytics-test-1']).to.deep.equal({
  123. variantName: 'variant-1',
  124. phase: 'release',
  125. versionNumber: 1,
  126. })
  127. })
  128. it('returns the current assignment for tests with analytics disabled that had previous assignments', function () {
  129. expect(this.assignments['no-analytics-test-2']).to.deep.equal({
  130. variantName: 'variant-1',
  131. phase: 'release',
  132. versionNumber: 2,
  133. })
  134. })
  135. it('does not return assignments for unknown tests', function () {
  136. expect(this.assignments).not.to.have.property('unknown-test')
  137. })
  138. })
  139. describe('with an non-existent user', function () {
  140. beforeEach(async function () {
  141. const unknownUserId = new ObjectId()
  142. this.assignments =
  143. await this.SplitTestHandler.promises.getActiveAssignmentsForUser(
  144. unknownUserId
  145. )
  146. })
  147. it('returns empty assignments', function () {
  148. expect(this.assignments).to.deep.equal({})
  149. })
  150. })
  151. describe('with a user without assignments', function () {
  152. beforeEach(async function () {
  153. this.user = { _id: new ObjectId() }
  154. this.SplitTestUserGetter.promises.getUser.resolves(this.user)
  155. this.assignments =
  156. await this.SplitTestHandler.promises.getActiveAssignmentsForUser(
  157. this.user._id
  158. )
  159. })
  160. it('returns current assignments', function () {
  161. expect(this.assignments).to.deep.equal({
  162. 'active-test': {
  163. phase: 'release',
  164. variantName: 'variant-1',
  165. versionNumber: 1,
  166. },
  167. 'legacy-test': {
  168. phase: 'release',
  169. variantName: 'variant-1',
  170. versionNumber: 1,
  171. },
  172. 'no-analytics-test-1': {
  173. phase: 'release',
  174. variantName: 'variant-1',
  175. versionNumber: 1,
  176. },
  177. 'no-analytics-test-2': {
  178. phase: 'release',
  179. variantName: 'variant-1',
  180. versionNumber: 2,
  181. },
  182. 'not-active-test': {
  183. phase: 'release',
  184. variantName: 'variant-1',
  185. versionNumber: 1,
  186. },
  187. })
  188. })
  189. })
  190. describe('with settings overrides', function () {
  191. beforeEach(function () {
  192. this.Settings.splitTestOverrides = {
  193. 'my-test-name': 'foo-1',
  194. }
  195. })
  196. it('should not use the override when in SaaS mode', async function () {
  197. this.AnalyticsManager.getIdsFromSession.returns({
  198. userId: 'abc123abc123',
  199. })
  200. this.SplitTestCache.get.resolves(
  201. new Map([
  202. [
  203. 'my-test-name',
  204. {
  205. name: 'my-test-name',
  206. versions: [
  207. {
  208. versionNumber: 0,
  209. active: true,
  210. variants: [
  211. {
  212. name: '100-percent-variant',
  213. rolloutPercent: 100,
  214. rolloutStripes: [{ start: 0, end: 100 }],
  215. },
  216. ],
  217. },
  218. ],
  219. },
  220. ],
  221. ])
  222. )
  223. const assignment = await this.SplitTestHandler.promises.getAssignment(
  224. this.req,
  225. this.res,
  226. 'my-test-name'
  227. )
  228. assert.equal('100-percent-variant', assignment.variant)
  229. })
  230. it('should use the override when not in SaaS mode', async function () {
  231. this.Settings.splitTestOverrides = {
  232. 'my-test-name': 'foo-1',
  233. }
  234. this.Settings.overleaf = undefined
  235. const assignment = await this.SplitTestHandler.promises.getAssignment(
  236. this.req,
  237. this.res,
  238. 'my-test-name'
  239. )
  240. assert.equal('foo-1', assignment.variant)
  241. })
  242. it('should use default when not in SaaS mode and no override is provided', async function () {
  243. this.Settings.splitTestOverrides = {}
  244. this.Settings.overleaf = undefined
  245. const assignment = await this.SplitTestHandler.promises.getAssignment(
  246. this.req,
  247. this.res,
  248. 'my-test-name'
  249. )
  250. assert.equal('default', assignment.variant)
  251. })
  252. })
  253. describe('save assignments to res.locals', function () {
  254. beforeEach(function () {
  255. this.AnalyticsManager.getIdsFromSession.returns({
  256. userId: 'abc123abc123',
  257. })
  258. })
  259. it('when in SaaS mode it should set the variant', async function () {
  260. await this.SplitTestHandler.promises.getAssignment(
  261. this.req,
  262. this.res,
  263. 'active-test'
  264. )
  265. expect(this.LocalsHelper.setSplitTestVariant).to.have.been.calledWith(
  266. this.res.locals,
  267. 'active-test',
  268. 'variant-1'
  269. )
  270. })
  271. it('when not in SaaS mode it should set the default variant', async function () {
  272. this.Settings.overleaf = undefined
  273. await this.SplitTestHandler.promises.getAssignment(
  274. this.req,
  275. this.res,
  276. 'active-test'
  277. )
  278. expect(this.LocalsHelper.setSplitTestVariant).to.have.been.calledWith(
  279. this.res.locals,
  280. 'active-test',
  281. 'default'
  282. )
  283. })
  284. })
  285. })
  286. function makeSplitTest(
  287. name,
  288. {
  289. active = true,
  290. analyticsEnabled = active,
  291. phase = 'release',
  292. versionNumber = 1,
  293. } = {}
  294. ) {
  295. return {
  296. name,
  297. versions: [
  298. {
  299. active,
  300. analyticsEnabled,
  301. phase,
  302. versionNumber,
  303. variants: [
  304. {
  305. name: 'variant-1',
  306. rolloutPercent: 100,
  307. rolloutStripes: [{ start: 0, end: 100 }],
  308. },
  309. ],
  310. },
  311. ],
  312. }
  313. }