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