SplitTestHandlerTests.js 9.1 KB

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