SplitTestHandler.test.mjs 24 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858
  1. import { vi, assert, expect } from 'vitest'
  2. import Path from 'node:path'
  3. import sinon from 'sinon'
  4. import mongodb from 'mongodb-legacy'
  5. import MockRequest from '../helpers/MockRequest.mjs'
  6. import MockResponse from '../helpers/MockResponse.mjs'
  7. const { ObjectId } = mongodb
  8. const MODULE_PATH = Path.join(
  9. import.meta.dirname,
  10. '../../../../app/src/Features/SplitTests/SplitTestHandler'
  11. )
  12. describe('SplitTestHandler', function () {
  13. let Features
  14. beforeEach(async function (ctx) {
  15. ctx.splitTests = [
  16. makeSplitTest('active-test', { versionNumber: 2 }),
  17. makeSplitTest('not-active-test', { active: false }),
  18. makeSplitTest('legacy-test'),
  19. makeSplitTest('no-analytics-test-1', { analyticsEnabled: false }),
  20. makeSplitTest('no-analytics-test-2', {
  21. analyticsEnabled: false,
  22. versionNumber: 2,
  23. }),
  24. ]
  25. ctx.cachedSplitTests = new Map()
  26. for (const splitTest of ctx.splitTests) {
  27. ctx.cachedSplitTests.set(splitTest.name, splitTest)
  28. }
  29. ctx.SplitTest = {
  30. find: sinon.stub().returns({
  31. exec: sinon.stub().resolves(ctx.splitTests),
  32. }),
  33. }
  34. ctx.SplitTestCache = {
  35. get: sinon.stub().resolves({}),
  36. }
  37. ctx.SplitTestCache.get.resolves(ctx.cachedSplitTests)
  38. ctx.Settings = {
  39. moduleImportSequence: [],
  40. overleaf: {},
  41. devToolbar: {
  42. enabled: false,
  43. },
  44. }
  45. ctx.AnalyticsManager = {
  46. getIdsFromSession: sinon.stub(),
  47. setUserPropertyForAnalyticsId: sinon.stub().resolves(),
  48. }
  49. ctx.LocalsHelper = {
  50. setSplitTestVariant: sinon.stub(),
  51. setSplitTestInfo: sinon.stub(),
  52. }
  53. ctx.SplitTestSessionHandler = {
  54. collectSessionStats: sinon.stub(),
  55. getCachedVariant: sinon.stub(),
  56. setVariantInCache: sinon.stub(),
  57. }
  58. ctx.SplitTestUserGetter = {
  59. promises: {
  60. getUser: sinon.stub().resolves(null),
  61. },
  62. }
  63. ctx.SessionManager = {
  64. isUserLoggedIn: sinon.stub().returns(false),
  65. }
  66. Features = {
  67. hasFeature: vi.fn().mockReturnValue(true),
  68. }
  69. vi.doMock('../../../../app/src/infrastructure/Features', () => ({
  70. default: Features,
  71. }))
  72. vi.doMock('../../../../app/src/Features/User/UserGetter', () => ({
  73. default: ctx.UserGetter,
  74. }))
  75. vi.doMock('../../../../app/src/Features/SplitTests/SplitTestCache', () => ({
  76. default: ctx.SplitTestCache,
  77. }))
  78. vi.doMock('../../../../app/src/models/SplitTest', () => ({
  79. SplitTest: ctx.SplitTest,
  80. }))
  81. vi.doMock('../../../../app/src/Features/User/UserUpdater', () => ({
  82. default: {},
  83. }))
  84. vi.doMock(
  85. '../../../../app/src/Features/Analytics/AnalyticsManager',
  86. () => ({
  87. default: ctx.AnalyticsManager,
  88. })
  89. )
  90. vi.doMock('../../../../app/src/Features/SplitTests/LocalsHelper', () => ({
  91. default: ctx.LocalsHelper,
  92. }))
  93. vi.doMock(
  94. '../../../../app/src/Features/SplitTests/SplitTestSessionHandler',
  95. () => ({
  96. default: ctx.SplitTestSessionHandler,
  97. })
  98. )
  99. vi.doMock(
  100. '../../../../app/src/Features/SplitTests/SplitTestUserGetter',
  101. () => ({
  102. default: ctx.SplitTestUserGetter,
  103. })
  104. )
  105. vi.doMock(
  106. '../../../../app/src/Features/Authentication/SessionManager',
  107. () => ({
  108. default: ctx.SessionManager,
  109. })
  110. )
  111. vi.doMock('@overleaf/settings', () => ({
  112. default: ctx.Settings,
  113. }))
  114. ctx.SplitTestHandler = (await import(MODULE_PATH)).default
  115. ctx.req = new MockRequest(vi)
  116. ctx.res = new MockResponse(vi)
  117. })
  118. describe('with an existing user', function () {
  119. beforeEach(async function (ctx) {
  120. ctx.user = {
  121. _id: new ObjectId(),
  122. splitTests: {
  123. 'active-test': [
  124. {
  125. variantName: 'default',
  126. versionNumber: 1,
  127. assignedAt: 'active-test-assigned-at',
  128. },
  129. ],
  130. 'legacy-test': 'legacy-variant',
  131. 'inactive-test': [{ variantName: 'trythis' }],
  132. 'unknown-test': [{ variantName: 'trythis' }],
  133. 'no-analytics-test-2': [
  134. {
  135. variantName: 'some-variant',
  136. versionNumber: 1,
  137. assignedAt: 'no-analytics-assigned-at',
  138. },
  139. ],
  140. },
  141. }
  142. ctx.SplitTestUserGetter.promises.getUser.resolves(ctx.user)
  143. ctx.SessionManager.isUserLoggedIn.returns(true)
  144. ctx.assignments =
  145. await ctx.SplitTestHandler.promises.getActiveAssignmentsForUser(
  146. ctx.user._id
  147. )
  148. ctx.explicitAssignments =
  149. await ctx.SplitTestHandler.promises.getActiveAssignmentsForUser(
  150. ctx.user._id,
  151. false,
  152. true
  153. )
  154. ctx.assignedToActiveTest =
  155. await ctx.SplitTestHandler.promises.hasUserBeenAssignedToVariant(
  156. ctx.req,
  157. ctx.user._id,
  158. 'active-test',
  159. 'variant-1'
  160. )
  161. ctx.assignedToActiveTestAnyVersion =
  162. await ctx.SplitTestHandler.promises.hasUserBeenAssignedToVariant(
  163. ctx.req,
  164. ctx.user._id,
  165. 'active-test',
  166. 'variant-1',
  167. true
  168. )
  169. })
  170. it('handles the legacy assignment format', function (ctx) {
  171. expect(ctx.assignments['legacy-test']).to.deep.equal({
  172. variantName: 'variant-1',
  173. phase: 'release',
  174. versionNumber: 1,
  175. })
  176. })
  177. it('returns the current assignment for each active test', function (ctx) {
  178. expect(ctx.assignments['active-test']).to.deep.equal({
  179. variantName: 'variant-1',
  180. phase: 'release',
  181. versionNumber: 2,
  182. })
  183. })
  184. it('returns the explicit assignment for each active test', function (ctx) {
  185. expect(ctx.explicitAssignments['active-test']).to.deep.equal({
  186. variantName: 'variant-1',
  187. phase: 'release',
  188. versionNumber: 2,
  189. assignedAt: 'active-test-assigned-at',
  190. })
  191. })
  192. it('returns the current assignment for tests with analytics disabled', function (ctx) {
  193. expect(ctx.assignments['no-analytics-test-1']).to.deep.equal({
  194. variantName: 'variant-1',
  195. phase: 'release',
  196. versionNumber: 1,
  197. })
  198. })
  199. it('returns the current assignment for tests with analytics disabled that had previous assignments', function (ctx) {
  200. expect(ctx.assignments['no-analytics-test-2']).to.deep.equal({
  201. variantName: 'variant-1',
  202. phase: 'release',
  203. versionNumber: 2,
  204. })
  205. })
  206. it('shows user has been assigned to previous version of variant', function (ctx) {
  207. expect(ctx.assignedToActiveTestAnyVersion).to.be.true
  208. })
  209. it('shows user has not been explicitly assigned to current version of variant', function (ctx) {
  210. expect(ctx.assignedToActiveTest).to.be.false
  211. })
  212. it('does not return assignments for unknown tests', function (ctx) {
  213. expect(ctx.assignments).not.to.have.property('unknown-test')
  214. })
  215. })
  216. describe('with an non-existent user', function () {
  217. beforeEach(async function (ctx) {
  218. const unknownUserId = new ObjectId()
  219. ctx.assignments =
  220. await ctx.SplitTestHandler.promises.getActiveAssignmentsForUser(
  221. unknownUserId
  222. )
  223. })
  224. it('returns empty assignments', function (ctx) {
  225. expect(ctx.assignments).to.deep.equal({})
  226. })
  227. })
  228. describe('with a user without assignments', function () {
  229. beforeEach(async function (ctx) {
  230. ctx.user = { _id: new ObjectId() }
  231. ctx.SplitTestUserGetter.promises.getUser.resolves(ctx.user)
  232. ctx.assignments =
  233. await ctx.SplitTestHandler.promises.getActiveAssignmentsForUser(
  234. ctx.user._id
  235. )
  236. ctx.explicitAssignments =
  237. await ctx.SplitTestHandler.promises.getActiveAssignmentsForUser(
  238. ctx.user._id,
  239. false,
  240. true
  241. )
  242. ctx.assignedToActiveTest =
  243. await ctx.SplitTestHandler.promises.hasUserBeenAssignedToVariant(
  244. ctx.req,
  245. ctx.user._id,
  246. 'active-test',
  247. 'variant-1'
  248. )
  249. })
  250. it('returns current assignments', function (ctx) {
  251. expect(ctx.assignments).to.deep.equal({
  252. 'active-test': {
  253. phase: 'release',
  254. variantName: 'variant-1',
  255. versionNumber: 2,
  256. },
  257. 'legacy-test': {
  258. phase: 'release',
  259. variantName: 'variant-1',
  260. versionNumber: 1,
  261. },
  262. 'no-analytics-test-1': {
  263. phase: 'release',
  264. variantName: 'variant-1',
  265. versionNumber: 1,
  266. },
  267. 'no-analytics-test-2': {
  268. phase: 'release',
  269. variantName: 'variant-1',
  270. versionNumber: 2,
  271. },
  272. })
  273. })
  274. it('shows user not assigned to variant', function (ctx) {
  275. expect(ctx.assignedToActiveTest).to.be.false
  276. })
  277. })
  278. describe('with settings overrides', function () {
  279. beforeEach(function (ctx) {
  280. ctx.Settings.splitTestOverrides = {
  281. 'my-test-name': 'foo-1',
  282. }
  283. })
  284. it('should not use the override when in SaaS mode', async function (ctx) {
  285. ctx.AnalyticsManager.getIdsFromSession.returns({
  286. userId: 'abc123abc123',
  287. })
  288. ctx.SplitTestCache.get.resolves(
  289. new Map([
  290. [
  291. 'my-test-name',
  292. {
  293. name: 'my-test-name',
  294. versions: [
  295. {
  296. versionNumber: 0,
  297. active: true,
  298. variants: [
  299. {
  300. name: '100-percent-variant',
  301. rolloutPercent: 100,
  302. rolloutStripes: [{ start: 0, end: 100 }],
  303. },
  304. ],
  305. },
  306. ],
  307. },
  308. ],
  309. ])
  310. )
  311. const assignment = await ctx.SplitTestHandler.promises.getAssignment(
  312. ctx.req,
  313. ctx.res,
  314. 'my-test-name'
  315. )
  316. assert.equal('100-percent-variant', assignment.variant)
  317. })
  318. it('should use the override when not in SaaS mode', async function (ctx) {
  319. ctx.Settings.splitTestOverrides = {
  320. 'my-test-name': 'foo-1',
  321. }
  322. ctx.Settings.overleaf = undefined
  323. Features.hasFeature.mockImplementation(function (feature) {
  324. return feature !== 'saas'
  325. })
  326. const assignment = await ctx.SplitTestHandler.promises.getAssignment(
  327. ctx.req,
  328. ctx.res,
  329. 'my-test-name'
  330. )
  331. assert.equal('foo-1', assignment.variant)
  332. })
  333. it('should use default when not in SaaS mode and no override is provided', async function (ctx) {
  334. ctx.Settings.splitTestOverrides = {}
  335. ctx.Settings.overleaf = undefined
  336. const assignment = await ctx.SplitTestHandler.promises.getAssignment(
  337. ctx.req,
  338. ctx.res,
  339. 'my-test-name'
  340. )
  341. assert.equal('default', assignment.variant)
  342. })
  343. })
  344. describe('save assignments to res.locals', function () {
  345. beforeEach(function (ctx) {
  346. ctx.AnalyticsManager.getIdsFromSession.returns({
  347. userId: 'abc123abc123',
  348. })
  349. })
  350. it('when in SaaS mode it should set the variant', async function (ctx) {
  351. await ctx.SplitTestHandler.promises.getAssignment(
  352. ctx.req,
  353. ctx.res,
  354. 'active-test'
  355. )
  356. expect(ctx.LocalsHelper.setSplitTestVariant).to.have.been.calledWith(
  357. ctx.res.locals,
  358. 'active-test',
  359. 'variant-1'
  360. )
  361. })
  362. it('when not in SaaS mode it should set the default variant', async function (ctx) {
  363. Features.hasFeature.mockImplementation(function (feature) {
  364. return feature !== 'saas'
  365. })
  366. await ctx.SplitTestHandler.promises.getAssignment(
  367. ctx.req,
  368. ctx.res,
  369. 'active-test'
  370. )
  371. expect(ctx.LocalsHelper.setSplitTestVariant).to.have.been.calledWith(
  372. ctx.res.locals,
  373. 'active-test',
  374. 'default'
  375. )
  376. })
  377. })
  378. describe('variant user limits', function () {
  379. beforeEach(function (ctx) {
  380. ctx.AnalyticsManager.getIdsFromSession.returns({
  381. userId: 'abc123abc123',
  382. })
  383. ctx.SplitTestUserGetter.promises.getUser.resolves({
  384. _id: new ObjectId('abc123abc123abc123abc123'),
  385. splitTests: {},
  386. })
  387. })
  388. it('should assign to variant when under limit', async function (ctx) {
  389. ctx.cachedSplitTests.set(
  390. 'active-test',
  391. makeSplitTest('active-test', { userLimit: 100, userCount: 50 })
  392. )
  393. const assignment = await ctx.SplitTestHandler.promises.getAssignment(
  394. ctx.req,
  395. ctx.res,
  396. 'active-test'
  397. )
  398. expect(assignment.variant).to.equal('variant-1')
  399. })
  400. it('should assign to default when limit reached', async function (ctx) {
  401. ctx.cachedSplitTests.set(
  402. 'active-test',
  403. makeSplitTest('active-test', { userLimit: 100, userCount: 100 })
  404. )
  405. const assignment = await ctx.SplitTestHandler.promises.getAssignment(
  406. ctx.req,
  407. ctx.res,
  408. 'active-test'
  409. )
  410. expect(assignment.variant).to.equal('default')
  411. })
  412. it('should not apply limits when no limit configured', async function (ctx) {
  413. const assignment = await ctx.SplitTestHandler.promises.getAssignment(
  414. ctx.req,
  415. ctx.res,
  416. 'active-test'
  417. )
  418. expect(assignment.variant).to.equal('variant-1')
  419. })
  420. it('should allow already assigned users even when limit reached', async function (ctx) {
  421. ctx.cachedSplitTests.set(
  422. 'active-test',
  423. makeSplitTest('active-test', { userLimit: 100, userCount: 100 })
  424. )
  425. ctx.SplitTestUserGetter.promises.getUser.resolves({
  426. _id: new ObjectId('abc123abc123abc123abc123'),
  427. splitTests: {
  428. 'active-test': [
  429. {
  430. variantName: 'variant-1',
  431. versionNumber: 1,
  432. assignedAt: new Date(),
  433. phase: 'release',
  434. },
  435. ],
  436. },
  437. })
  438. const assignment = await ctx.SplitTestHandler.promises.getAssignment(
  439. ctx.req,
  440. ctx.res,
  441. 'active-test'
  442. )
  443. expect(assignment.variant).to.equal('variant-1')
  444. })
  445. })
  446. describe('labs phase assignment (split test)', function () {
  447. beforeEach(function (ctx) {
  448. ctx.AnalyticsManager.getIdsFromSession.returns({
  449. userId: 'abc123abc123',
  450. })
  451. ctx.cachedSplitTests.set(
  452. 'labs-experiment',
  453. makeSplitTest('labs-experiment', { phase: 'labs' })
  454. )
  455. })
  456. it('should assign to variant when user is in labs program and has opted into the experiment', async function (ctx) {
  457. ctx.SplitTestUserGetter.promises.getUser.resolves({
  458. _id: new ObjectId('abc123abc123abc123abc123'),
  459. splitTests: {},
  460. labsProgram: true,
  461. labsExperiments: ['labs-experiment'],
  462. })
  463. const assignment = await ctx.SplitTestHandler.promises.getAssignment(
  464. ctx.req,
  465. ctx.res,
  466. 'labs-experiment'
  467. )
  468. expect(assignment.variant).to.equal('variant-1')
  469. })
  470. it('should assign to default when user is in labs program but has not opted into the experiment', async function (ctx) {
  471. ctx.SplitTestUserGetter.promises.getUser.resolves({
  472. _id: new ObjectId('abc123abc123abc123abc123'),
  473. splitTests: {},
  474. labsProgram: true,
  475. labsExperiments: ['some-other-experiment'],
  476. })
  477. const assignment = await ctx.SplitTestHandler.promises.getAssignment(
  478. ctx.req,
  479. ctx.res,
  480. 'labs-experiment'
  481. )
  482. expect(assignment.variant).to.equal('default')
  483. })
  484. it('should assign to default when user is not in labs program', async function (ctx) {
  485. ctx.SplitTestUserGetter.promises.getUser.resolves({
  486. _id: new ObjectId('abc123abc123abc123abc123'),
  487. splitTests: {},
  488. labsProgram: false,
  489. labsExperiments: [],
  490. })
  491. const assignment = await ctx.SplitTestHandler.promises.getAssignment(
  492. ctx.req,
  493. ctx.res,
  494. 'labs-experiment'
  495. )
  496. expect(assignment.variant).to.equal('default')
  497. })
  498. it('should assign to default when user has no labsExperiments field', async function (ctx) {
  499. ctx.SplitTestUserGetter.promises.getUser.resolves({
  500. _id: new ObjectId('abc123abc123abc123abc123'),
  501. splitTests: {},
  502. labsProgram: true,
  503. })
  504. const assignment = await ctx.SplitTestHandler.promises.getAssignment(
  505. ctx.req,
  506. ctx.res,
  507. 'labs-experiment'
  508. )
  509. expect(assignment.variant).to.equal('default')
  510. })
  511. it('should assign to variant when under user limit', async function (ctx) {
  512. ctx.cachedSplitTests.set(
  513. 'labs-experiment',
  514. makeSplitTest('labs-experiment', {
  515. phase: 'labs',
  516. userLimit: 10,
  517. userCount: 5,
  518. })
  519. )
  520. ctx.SplitTestUserGetter.promises.getUser.resolves({
  521. _id: new ObjectId('abc123abc123abc123abc123'),
  522. splitTests: {},
  523. labsProgram: true,
  524. labsExperiments: ['labs-experiment'],
  525. })
  526. const assignment = await ctx.SplitTestHandler.promises.getAssignment(
  527. ctx.req,
  528. ctx.res,
  529. 'labs-experiment'
  530. )
  531. expect(assignment.variant).to.equal('variant-1')
  532. })
  533. it('should assign enrolled user to variant even when user limit is reached', async function (ctx) {
  534. ctx.cachedSplitTests.set(
  535. 'labs-experiment',
  536. makeSplitTest('labs-experiment', {
  537. phase: 'labs',
  538. userLimit: 10,
  539. userCount: 10,
  540. })
  541. )
  542. ctx.SplitTestUserGetter.promises.getUser.resolves({
  543. _id: new ObjectId('abc123abc123abc123abc123'),
  544. splitTests: {},
  545. labsProgram: true,
  546. labsExperiments: ['labs-experiment'],
  547. })
  548. const assignment = await ctx.SplitTestHandler.promises.getAssignment(
  549. ctx.req,
  550. ctx.res,
  551. 'labs-experiment'
  552. )
  553. expect(assignment.variant).to.equal('variant-1')
  554. })
  555. it('should allow already-assigned user even when limit is reached', async function (ctx) {
  556. ctx.cachedSplitTests.set(
  557. 'labs-experiment',
  558. makeSplitTest('labs-experiment', {
  559. phase: 'labs',
  560. userLimit: 10,
  561. userCount: 10,
  562. })
  563. )
  564. ctx.SplitTestUserGetter.promises.getUser.resolves({
  565. _id: new ObjectId('abc123abc123abc123abc123'),
  566. splitTests: {
  567. 'labs-experiment': [
  568. {
  569. variantName: 'variant-1',
  570. versionNumber: 1,
  571. assignedAt: new Date(),
  572. phase: 'labs',
  573. },
  574. ],
  575. },
  576. labsProgram: true,
  577. labsExperiments: ['labs-experiment'],
  578. })
  579. const assignment = await ctx.SplitTestHandler.promises.getAssignment(
  580. ctx.req,
  581. ctx.res,
  582. 'labs-experiment'
  583. )
  584. expect(assignment.variant).to.equal('variant-1')
  585. })
  586. })
  587. describe('labs phase assignment (gradual rollout)', function () {
  588. beforeEach(function (ctx) {
  589. ctx.AnalyticsManager.getIdsFromSession.returns({
  590. userId: 'abc123abc123',
  591. })
  592. ctx.cachedSplitTests.set(
  593. 'labs-experiment-gr',
  594. makeSplitTest('labs-experiment-gr', {
  595. phase: 'labs',
  596. analyticsEnabled: false,
  597. })
  598. )
  599. })
  600. it('should assign to variant when user is in labs program and has opted in', async function (ctx) {
  601. ctx.SplitTestUserGetter.promises.getUser.resolves({
  602. _id: new ObjectId('abc123abc123abc123abc123'),
  603. splitTests: {},
  604. labsProgram: true,
  605. labsExperiments: ['labs-experiment-gr'],
  606. })
  607. const assignment = await ctx.SplitTestHandler.promises.getAssignment(
  608. ctx.req,
  609. ctx.res,
  610. 'labs-experiment-gr'
  611. )
  612. expect(assignment.variant).to.equal('variant-1')
  613. })
  614. it('should assign to default when user has not opted in', async function (ctx) {
  615. ctx.SplitTestUserGetter.promises.getUser.resolves({
  616. _id: new ObjectId('abc123abc123abc123abc123'),
  617. splitTests: {},
  618. labsProgram: true,
  619. labsExperiments: [],
  620. })
  621. const assignment = await ctx.SplitTestHandler.promises.getAssignment(
  622. ctx.req,
  623. ctx.res,
  624. 'labs-experiment-gr'
  625. )
  626. expect(assignment.variant).to.equal('default')
  627. })
  628. it('should assign to default when user is not in labs program', async function (ctx) {
  629. ctx.SplitTestUserGetter.promises.getUser.resolves({
  630. _id: new ObjectId('abc123abc123abc123abc123'),
  631. splitTests: {},
  632. labsProgram: false,
  633. labsExperiments: [],
  634. })
  635. const assignment = await ctx.SplitTestHandler.promises.getAssignment(
  636. ctx.req,
  637. ctx.res,
  638. 'labs-experiment-gr'
  639. )
  640. expect(assignment.variant).to.equal('default')
  641. })
  642. it('should assign to variant when under user limit', async function (ctx) {
  643. ctx.cachedSplitTests.set(
  644. 'labs-experiment-gr',
  645. makeSplitTest('labs-experiment-gr', {
  646. phase: 'labs',
  647. analyticsEnabled: false,
  648. userLimit: 10,
  649. userCount: 5,
  650. })
  651. )
  652. ctx.SplitTestUserGetter.promises.getUser.resolves({
  653. _id: new ObjectId('abc123abc123abc123abc123'),
  654. splitTests: {},
  655. labsProgram: true,
  656. labsExperiments: ['labs-experiment-gr'],
  657. })
  658. const assignment = await ctx.SplitTestHandler.promises.getAssignment(
  659. ctx.req,
  660. ctx.res,
  661. 'labs-experiment-gr'
  662. )
  663. expect(assignment.variant).to.equal('variant-1')
  664. })
  665. it('should assign enrolled user to variant even when user limit is reached', async function (ctx) {
  666. ctx.cachedSplitTests.set(
  667. 'labs-experiment-gr',
  668. makeSplitTest('labs-experiment-gr', {
  669. phase: 'labs',
  670. analyticsEnabled: false,
  671. userLimit: 10,
  672. userCount: 10,
  673. })
  674. )
  675. ctx.SplitTestUserGetter.promises.getUser.resolves({
  676. _id: new ObjectId('abc123abc123abc123abc123'),
  677. splitTests: {},
  678. labsProgram: true,
  679. labsExperiments: ['labs-experiment-gr'],
  680. })
  681. const assignment = await ctx.SplitTestHandler.promises.getAssignment(
  682. ctx.req,
  683. ctx.res,
  684. 'labs-experiment-gr'
  685. )
  686. expect(assignment.variant).to.equal('variant-1')
  687. })
  688. it('should allow already-assigned user even when limit is reached', async function (ctx) {
  689. ctx.cachedSplitTests.set(
  690. 'labs-experiment-gr',
  691. makeSplitTest('labs-experiment-gr', {
  692. phase: 'labs',
  693. analyticsEnabled: false,
  694. userLimit: 10,
  695. userCount: 10,
  696. })
  697. )
  698. ctx.SplitTestUserGetter.promises.getUser.resolves({
  699. _id: new ObjectId('abc123abc123abc123abc123'),
  700. splitTests: {
  701. 'labs-experiment-gr': [
  702. {
  703. variantName: 'variant-1',
  704. versionNumber: 1,
  705. assignedAt: new Date(),
  706. phase: 'labs',
  707. },
  708. ],
  709. },
  710. labsProgram: true,
  711. labsExperiments: ['labs-experiment-gr'],
  712. })
  713. const assignment = await ctx.SplitTestHandler.promises.getAssignment(
  714. ctx.req,
  715. ctx.res,
  716. 'labs-experiment-gr'
  717. )
  718. expect(assignment.variant).to.equal('variant-1')
  719. })
  720. })
  721. })
  722. function makeSplitTest(
  723. name,
  724. {
  725. active = true,
  726. analyticsEnabled = active,
  727. phase = 'release',
  728. versionNumber = 1,
  729. userLimit = undefined,
  730. userCount = undefined,
  731. } = {}
  732. ) {
  733. const variant = {
  734. name: 'variant-1',
  735. rolloutPercent: 100,
  736. rolloutStripes: [{ start: 0, end: 100 }],
  737. }
  738. if (userLimit !== undefined) {
  739. variant.userLimit = userLimit
  740. }
  741. if (userCount !== undefined) {
  742. variant.userCount = userCount
  743. }
  744. return {
  745. name,
  746. versions: [
  747. {
  748. active,
  749. analyticsEnabled,
  750. phase,
  751. versionNumber,
  752. variants: [variant],
  753. },
  754. ],
  755. }
  756. }