SplitTestHandlerTests.js 8.6 KB

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