1 | const chai = require('chai');
|
2 | const spies = require('chai-spies');
|
3 | const { expect, assert } = chai;
|
4 | chai.use(spies);
|
5 | const mocks = require('../mocks/index');
|
6 | const abby = require('../lib/client');
|
7 | const expressionEvaluator = require('expression-evaluator');
|
8 |
|
9 | describe('client', () => {
|
10 | let abbyClient,
|
11 | synchroniser = mocks.synchroniser;
|
12 | beforeEach(() => {
|
13 | abbyClient = abby({
|
14 | tags: 'test, production',
|
15 | apiEndpoint: 'https://abby.io/api/v1/'
|
16 | }, synchroniser);
|
17 | });
|
18 |
|
19 |
|
20 | describe('when there is already a session cookie', () => {
|
21 | let request,
|
22 | response,
|
23 | props;
|
24 | function initSpec(experiments = [], forced = [], rebalance = []) {
|
25 |
|
26 | request = {
|
27 | headers: {
|
28 | cookie: 'ABBY_SESSION=123-123;ABBY_EXPERIMENTS=' + experiments.join(',')
|
29 | },
|
30 | url: '?abby_experiment=' + forced.join(',')
|
31 | };
|
32 |
|
33 | response = {
|
34 | headers: {},
|
35 | setHeader() {
|
36 | },
|
37 | getHeader() { }
|
38 | };
|
39 | props = {
|
40 | device: 'MOBILE'
|
41 | };
|
42 |
|
43 | abbyClient(request, response, props, { rebalance });
|
44 | }
|
45 | it('should not set features to request', () => {
|
46 | initSpec();
|
47 | expect(request.experiments).to.be.empty;
|
48 | });
|
49 | it('should preserve current features', () => {
|
50 | initSpec(['feature_abc|control|1']);
|
51 | expect(request.experiments.feature_abc).to.not.undefined;
|
52 | });
|
53 | it('should add a forced experiment', () => {
|
54 | initSpec(['feature_abc|control|1'], ['feature_def|control|1']);
|
55 | expect(request.experiments.feature_def).to.not.undefined;
|
56 | });
|
57 | it('should rebalance a experiment', () => {
|
58 | initSpec(['feature_rebalance|variant|1'], [], ['feature_rebalance']);
|
59 | expect(request.experiments.feature_rebalance.variantCode).to.equal('control');
|
60 | });
|
61 | it('should rebalance a experiment when toggle set to true', () => {
|
62 | initSpec(['feature_rebalance_toggle|variant|1']);
|
63 | expect(request.experiments.feature_rebalance_toggle.variantCode).to.equal('control');
|
64 | });
|
65 | });
|
66 |
|
67 | describe('when there is no Abby cookie', () => {
|
68 | let request,
|
69 | response;
|
70 | beforeEach(() => {
|
71 |
|
72 | request = {
|
73 | headers: {
|
74 | cookie: null
|
75 | },
|
76 | url: 'anything'
|
77 | };
|
78 |
|
79 | response = {
|
80 | headers: {},
|
81 | setHeader() {
|
82 |
|
83 | response.headers.cookie = 'uhhh cookies!';
|
84 | },
|
85 | getHeader() { }
|
86 | };
|
87 |
|
88 | abbyClient(request, response);
|
89 | });
|
90 | it('should set all features to request', () => {
|
91 | expect(request.experiments.feature_abc).to.not.undefined;
|
92 | expect(request.experiments.feature_def).to.not.undefined;
|
93 | });
|
94 | it('should store the evaluation result in Cookies', () => {
|
95 |
|
96 | expect(response.headers.cookie).to.equal('uhhh cookies!');
|
97 | });
|
98 | it('should store variant and experiment by experimentCode', () => {
|
99 | expect(request.experiments.feature_abc.experiment).to.not.undefined;
|
100 | expect(request.experiments.feature_abc.variant).to.not.undefined;
|
101 | });
|
102 | });
|
103 |
|
104 | describe('when there is a forced query experiment in request', () => {
|
105 | let request,
|
106 | response;
|
107 | function initSpec({ isAbbyQuery = true, experiment = 'feature_ghi', variant = 'variant_1_ghi' } = {}) {
|
108 |
|
109 | request = {
|
110 | headers: {
|
111 | cookie: null
|
112 | },
|
113 | url: isAbbyQuery ? `?abby_experiment=${experiment}|${variant}` : `anything`
|
114 | };
|
115 |
|
116 | response = {
|
117 | headers: {},
|
118 | setHeader() {
|
119 |
|
120 | response.headers.cookie = 'uhhh cookies!';
|
121 | },
|
122 | getHeader() { }
|
123 | };
|
124 |
|
125 | abbyClient(request, response);
|
126 | }
|
127 | it('should set feature_ghi to variant_1_ghi', () => {
|
128 | initSpec();
|
129 | expect(request.experiments.feature_ghi.variant.code).to.equal('variant_1_ghi');
|
130 | });
|
131 | });
|
132 |
|
133 | describe('distribution of original/variant', () => {
|
134 | let deviation;
|
135 |
|
136 | function initSpec() {
|
137 | let originalCount = 0,
|
138 | variantCount = 0,
|
139 | distributionRate = 10000,
|
140 | originalPercent,
|
141 | variantPercent,
|
142 | request,
|
143 | response;
|
144 |
|
145 | request = {
|
146 | headers: {
|
147 | cookie: null
|
148 | },
|
149 | url: 'anything'
|
150 | };
|
151 |
|
152 | response = {
|
153 | headers: {},
|
154 | setHeader() {
|
155 |
|
156 | response.headers.cookie = 'uhhh cookies!';
|
157 | },
|
158 | getHeader() { }
|
159 | };
|
160 | for (let i = 0; i < distributionRate; i++) {
|
161 | abbyClient(request, response);
|
162 | request.experiments.feature_ghi.variant.code === 'variant_1_ghi' ? variantCount++ : originalCount++;
|
163 | }
|
164 | originalPercent = (originalCount * 100) / distributionRate;
|
165 | variantPercent = (variantCount * 100) / distributionRate;
|
166 | deviation = Math.abs(originalPercent - variantPercent);
|
167 | }
|
168 |
|
169 | it('should have approximately same range', () => {
|
170 | initSpec();
|
171 | assert.isAtMost(deviation, 5);
|
172 | }).timeout(10000);
|
173 | });
|
174 |
|
175 | describe('getTargetedExperiments', () => {
|
176 | let targeting = { 'type': 'and' };
|
177 | let userExperiments;
|
178 | beforeEach(() => {
|
179 | userExperiments = [{
|
180 | experiment: {
|
181 | tags: ['tag'],
|
182 | targeting
|
183 | },
|
184 | variant: { original: false }
|
185 | }];
|
186 | });
|
187 | afterEach(() => chai.spy.restore(expressionEvaluator));
|
188 | describe('filtering', () => {
|
189 | beforeEach(() => chai.spy.on(expressionEvaluator, 'evaluate', () => true));
|
190 |
|
191 | it('should return empty array when tags do not match', () => {
|
192 | let result = abbyClient.getTargetedExperiments(userExperiments, {}, { tags: ['whatever'] });
|
193 | expect(result.length).to.equal(0);
|
194 | });
|
195 | it('should return 1 item when tag matches', () => {
|
196 | let result = abbyClient.getTargetedExperiments(userExperiments, {}, { tags: ['tag'] });
|
197 | expect(result.length).to.equal(1);
|
198 | });
|
199 | });
|
200 |
|
201 | describe('targeting', () => {
|
202 | let props = { 'a': '1' };
|
203 |
|
204 | it('should return empty array when target does not match', () => {
|
205 | let spy = chai.spy.on(expressionEvaluator, 'evaluate', () => false);
|
206 | let result = abbyClient.getTargetedExperiments(userExperiments, props);
|
207 | expect(spy).to.have.been.called.with(targeting, props);
|
208 | expect(result.length).to.equal(0);
|
209 | });
|
210 | it('should return 1 item when target does match', () => {
|
211 | let spy = chai.spy.on(expressionEvaluator, 'evaluate', () => true);
|
212 | let result = abbyClient.getTargetedExperiments(userExperiments, props);
|
213 | expect(spy).to.have.been.called.with(targeting, props);
|
214 | expect(result.length).to.equal(1);
|
215 | });
|
216 | });
|
217 |
|
218 | it('should return empty when variant is original', () => {
|
219 | userExperiments[0].variant.original = true;
|
220 | let result = abbyClient.getTargetedExperiments(userExperiments, {});
|
221 | expect(result.length).to.equal(0);
|
222 | });
|
223 | });
|
224 |
|
225 | describe('when there is a paused experiment', () => {
|
226 | let request,
|
227 | response,
|
228 | addedToCookie;
|
229 |
|
230 | function initSpec() {
|
231 | addedToCookie = false;
|
232 |
|
233 | request = {
|
234 | headers: {
|
235 | cookie: ''
|
236 | },
|
237 | url: '/'
|
238 | };
|
239 |
|
240 | response = {
|
241 | headers: {},
|
242 | setHeader(key, value) {
|
243 | if (Array.isArray(value) && value.join('').indexOf('feature_paused') > -1) {
|
244 | addedToCookie = true;
|
245 | }
|
246 | },
|
247 | getHeader() { }
|
248 | };
|
249 |
|
250 | abbyClient(request, response);
|
251 | }
|
252 | it('should not add the experiment to the request', () => {
|
253 | initSpec();
|
254 | expect(request.experiments.feature_paused).to.be.undefined;
|
255 | });
|
256 | it('should add the experiment to the cookie', () => {
|
257 | initSpec();
|
258 | expect(addedToCookie).to.be.true;
|
259 | });
|
260 | });
|
261 | });
|