UNPKG

optimizely-server-sdk

Version:

Node SDK for Optimizely X Full Stack

462 lines (407 loc) 20.2 kB
/**************************************************************************** * Copyright 2017, Optimizely, Inc. and contributors * * * * Licensed under the Apache License, Version 2.0 (the "License"); * * you may not use this file except in compliance with the License. * * You may obtain a copy of the License at * * * * http://www.apache.org/licenses/LICENSE-2.0 * * * * Unless required by applicable law or agreed to in writing, software * * distributed under the License is distributed on an "AS IS" BASIS, * * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * * See the License for the specific language governing permissions and * * limitations under the License. * ***************************************************************************/ var Optimizely = require('../../optimizely'); var eventBuilder = require('../../core/event_builder/index.js'); var eventDispatcher = require('../../plugins/event_dispatcher'); var errorHandler = require('../../plugins/error_handler'); var bucketer = require('../bucketer'); var DecisionService = require('./'); var enums = require('../../utils/enums'); var logger = require('../../plugins/logger'); var projectConfig = require('../project_config'); var sprintf = require('sprintf'); var testData = require('../../../tests/test_data').getTestProjectConfig(); var jsonSchemaValidator = require('../../utils/json_schema_validator'); var chai = require('chai'); var sinon = require('sinon'); var assert = chai.assert; var LOG_LEVEL = enums.LOG_LEVEL; var LOG_MESSAGES = enums.LOG_MESSAGES; describe('lib/core/decision_service', function() { describe('APIs', function() { var configObj = projectConfig.createProjectConfig(testData); var decisionServiceInstance; var mockLogger = logger.createLogger({logLevel: LOG_LEVEL.INFO}); var bucketerStub; beforeEach(function () { bucketerStub = sinon.stub(bucketer, 'bucket'); sinon.stub(mockLogger, 'log'); decisionServiceInstance = DecisionService.createDecisionService({ configObj: configObj, logger: mockLogger, }); }); afterEach(function () { bucketer.bucket.restore(); mockLogger.log.restore(); }); describe('#getVariation', function () { it('should return the correct variation for the given experiment key and user ID for a running experiment', function () { bucketerStub.returns('111128'); // ID of the 'control' variation from `test_data` assert.strictEqual('control', decisionServiceInstance.getVariation('testExperiment', 'decision_service_user')); sinon.assert.calledOnce(bucketerStub); }); it('should return the whitelisted variation if the user is whitelisted', function () { assert.strictEqual('variationWithAudience', decisionServiceInstance.getVariation('testExperimentWithAudiences', 'user2')); sinon.assert.notCalled(bucketerStub); assert.strictEqual(2, mockLogger.log.callCount); assert.strictEqual(mockLogger.log.args[0][1], 'PROJECT_CONFIG: User user2 is not in the forced variation map.'); assert.strictEqual(mockLogger.log.args[1][1], 'DECISION_SERVICE: User user2 is forced in variation variationWithAudience.'); }); it('should return null if the user does not meet audience conditions', function () { assert.isNull(decisionServiceInstance.getVariation('testExperimentWithAudiences', 'user3', {foo: 'bar'})); assert.strictEqual(2, mockLogger.log.callCount); assert.strictEqual(mockLogger.log.args[0][1], 'PROJECT_CONFIG: User user3 is not in the forced variation map.'); assert.strictEqual(mockLogger.log.args[1][1], 'DECISION_SERVICE: User user3 does not meet conditions to be in experiment testExperimentWithAudiences.'); }); it('should return null if the experiment is not running', function () { assert.isNull(decisionServiceInstance.getVariation('testExperimentNotRunning', 'user1')); sinon.assert.notCalled(bucketerStub); assert.strictEqual(1, mockLogger.log.callCount); assert.strictEqual(mockLogger.log.args[0][1], 'DECISION_SERVICE: Experiment testExperimentNotRunning is not running.'); }); describe('when a user profile service is provided', function () { var userProfileServiceInstance = null; var userProfileLookupStub; var userProfileSaveStub; beforeEach(function () { userProfileServiceInstance = { lookup: function () { }, save: function () { }, }; decisionServiceInstance = DecisionService.createDecisionService({ configObj: configObj, logger: mockLogger, userProfileService: userProfileServiceInstance, }); userProfileLookupStub = sinon.stub(userProfileServiceInstance, 'lookup'); userProfileSaveStub = sinon.stub(userProfileServiceInstance, 'save'); sinon.stub(decisionServiceInstance, '__getWhitelistedVariation').returns(null); }); afterEach(function () { userProfileServiceInstance.lookup.restore(); userProfileServiceInstance.save.restore(); decisionServiceInstance.__getWhitelistedVariation.restore(); }); it('should return the previously bucketed variation', function () { userProfileLookupStub.returns({ user_id: 'decision_service_user', experiment_bucket_map: { '111127': { 'variation_id': '111128' // ID of the 'control' variation }, }, }); assert.strictEqual('control', decisionServiceInstance.getVariation('testExperiment', 'decision_service_user')); sinon.assert.calledWith(userProfileLookupStub, 'decision_service_user'); sinon.assert.notCalled(bucketerStub); assert.strictEqual(mockLogger.log.args[0][1], 'PROJECT_CONFIG: User decision_service_user is not in the forced variation map.'); assert.strictEqual(mockLogger.log.args[1][1], 'DECISION_SERVICE: Returning previously activated variation \"control\" of experiment \"testExperiment\" for user \"decision_service_user\" from user profile.'); }); it('should bucket if there was no prevously bucketed variation', function () { bucketerStub.returns('111128'); // ID of the 'control' variation userProfileLookupStub.returns({ user_id: 'decision_service_user', experiment_bucket_map: {}, }); assert.strictEqual('control', decisionServiceInstance.getVariation('testExperiment', 'decision_service_user')); sinon.assert.calledWith(userProfileLookupStub, 'decision_service_user'); sinon.assert.calledOnce(bucketerStub); // make sure we save the decision sinon.assert.calledWith(userProfileSaveStub, { user_id: 'decision_service_user', experiment_bucket_map: { '111127': { 'variation_id': '111128', } }, }); }); it('should bucket if the user profile service returns null', function () { bucketerStub.returns('111128'); // ID of the 'control' variation userProfileLookupStub.returns(null); assert.strictEqual('control', decisionServiceInstance.getVariation('testExperiment', 'decision_service_user')); sinon.assert.calledWith(userProfileLookupStub, 'decision_service_user'); sinon.assert.calledOnce(bucketerStub); // make sure we save the decision sinon.assert.calledWith(userProfileSaveStub, { user_id: 'decision_service_user', experiment_bucket_map: { '111127': { 'variation_id': '111128', } }, }); }); it('should re-bucket if the stored variation is no longer valid', function () { bucketerStub.returns('111128'); // ID of the 'control' variation userProfileLookupStub.returns({ user_id: 'decision_service_user', experiment_bucket_map: { '111127': { 'variation_id': 'not valid variation', }, }, }); assert.strictEqual('control', decisionServiceInstance.getVariation('testExperiment', 'decision_service_user')); sinon.assert.calledWith(userProfileLookupStub, 'decision_service_user'); sinon.assert.calledOnce(bucketerStub); assert.strictEqual(mockLogger.log.args[0][1], 'PROJECT_CONFIG: User decision_service_user is not in the forced variation map.'); assert.strictEqual(mockLogger.log.args[1][1], 'DECISION_SERVICE: User decision_service_user was previously bucketed into variation with ID not valid variation for experiment testExperiment, but no matching variation was found.'); // make sure we save the decision sinon.assert.calledWith(userProfileSaveStub, { user_id: 'decision_service_user', experiment_bucket_map: { '111127': { 'variation_id': '111128', } }, }); }); it('should store the bucketed variation for the user', function () { bucketerStub.returns('111128'); // ID of the 'control' variation userProfileLookupStub.returns({ user_id: 'decision_service_user', experiment_bucket_map: {}, // no decisions for user }); assert.strictEqual('control', decisionServiceInstance.getVariation('testExperiment', 'decision_service_user')); sinon.assert.calledWith(userProfileLookupStub, 'decision_service_user'); sinon.assert.calledOnce(bucketerStub); sinon.assert.calledWith(userProfileServiceInstance.save, { user_id: 'decision_service_user', experiment_bucket_map: { '111127': { variation_id: '111128', }, }, }); assert.strictEqual(mockLogger.log.args[0][1], 'PROJECT_CONFIG: User decision_service_user is not in the forced variation map.'); assert.strictEqual(mockLogger.log.args[1][1], 'DECISION_SERVICE: Saved variation "control" of experiment "testExperiment" for user "decision_service_user".'); }); it('should log an error message if "lookup" throws an error', function () { bucketerStub.returns('111128'); // ID of the 'control' variation userProfileLookupStub.throws(new Error('I am an error')); assert.strictEqual('control', decisionServiceInstance.getVariation('testExperiment', 'decision_service_user')); sinon.assert.calledWith(userProfileLookupStub, 'decision_service_user'); sinon.assert.calledOnce(bucketerStub); // should still go through with bucketing assert.strictEqual(mockLogger.log.args[0][1], 'PROJECT_CONFIG: User decision_service_user is not in the forced variation map.'); assert.strictEqual(mockLogger.log.args[1][1], 'DECISION_SERVICE: Error while looking up user profile for user ID "decision_service_user": I am an error.'); }); it('should log an error message if "save" throws an error', function () { bucketerStub.returns('111128'); // ID of the 'control' variation userProfileLookupStub.returns(null); userProfileSaveStub.throws(new Error('I am an error')); assert.strictEqual('control', decisionServiceInstance.getVariation('testExperiment', 'decision_service_user')); sinon.assert.calledWith(userProfileLookupStub, 'decision_service_user'); sinon.assert.calledOnce(bucketerStub); // should still go through with bucketing assert.strictEqual(mockLogger.log.args[0][1], 'PROJECT_CONFIG: User decision_service_user is not in the forced variation map.'); assert.strictEqual(mockLogger.log.args[1][1], 'DECISION_SERVICE: Error while saving user profile for user ID "decision_service_user": I am an error.'); // make sure that we save the decision sinon.assert.calledWith(userProfileSaveStub, { user_id: 'decision_service_user', experiment_bucket_map: { '111127': { 'variation_id': '111128', } }, }); }); }); }); describe('__buildBucketerParams', function () { it('should return params object with correct properties', function () { var bucketerParams = decisionServiceInstance.__buildBucketerParams('testExperiment', 'testUser', 'testUser'); var expectedParams = { bucketingId: 'testUser', experimentKey: 'testExperiment', userId: 'testUser', experimentId: '111127', trafficAllocationConfig: [ { entityId: '111128', endOfRange: 4000, }, { entityId: '111129', endOfRange: 9000, }, ], variationIdMap: configObj.variationIdMap, logger: mockLogger, experimentKeyMap: configObj.experimentKeyMap, groupIdMap: configObj.groupIdMap, }; assert.deepEqual(bucketerParams, expectedParams); sinon.assert.notCalled(mockLogger.log); }); }); describe('__checkIfExperimentIsActive', function () { it('should return true if experiment is running', function () { assert.isTrue(decisionServiceInstance.__checkIfExperimentIsActive('testExperiment', 'testUser')); sinon.assert.notCalled(mockLogger.log); }); it('should return false when experiment is not running', function () { assert.isFalse(decisionServiceInstance.__checkIfExperimentIsActive('testExperimentNotRunning', 'testUser')); sinon.assert.calledOnce(mockLogger.log); var logMessage = mockLogger.log.args[0][1]; assert.strictEqual(logMessage, sprintf(LOG_MESSAGES.EXPERIMENT_NOT_RUNNING, 'DECISION_SERVICE', 'testExperimentNotRunning')); }); }); describe('__checkIfUserIsInAudience', function () { it('should return true when audience conditions are met', function () { assert.isTrue(decisionServiceInstance.__checkIfUserIsInAudience('testExperimentWithAudiences', 'testUser', {browser_type: 'firefox'})); sinon.assert.notCalled(mockLogger.log); }); it('should return true when experiment has no audience', function () { assert.isTrue(decisionServiceInstance.__checkIfUserIsInAudience('testExperiment', 'testUser')); sinon.assert.notCalled(mockLogger.log); }); it('should return false when audience conditions are not met', function () { assert.isFalse(decisionServiceInstance.__checkIfUserIsInAudience('testExperimentWithAudiences', 'testUser', {browser_type: 'chrome'})); sinon.assert.calledOnce(mockLogger.log); }); }); describe('__getWhitelistedVariation', function () { it('should return forced variation ID if forced variation is provided for the user ID', function () { var testExperiment = configObj.experimentKeyMap['testExperiment']; var expectedVariation = configObj.variationIdMap['111128']; assert.strictEqual(decisionServiceInstance.__getWhitelistedVariation(testExperiment, 'user1'), expectedVariation); }); it('should return null if forced variation is not provided for the user ID', function () { var testExperiment = configObj.experimentKeyMap['testExperiment']; assert.isNull(decisionServiceInstance.__getWhitelistedVariation(testExperiment, 'notInForcedVariations')); }); }); }); describe('when a bucketingID is provided', function() { var configObj = projectConfig.createProjectConfig(testData); var createdLogger = logger.createLogger({logLevel: LOG_LEVEL.DEBUG}); var optlyInstance; beforeEach(function () { optlyInstance = new Optimizely({ clientEngine: 'node-sdk', datafile: testData, jsonSchemaValidator: jsonSchemaValidator, isValidInstance: true, logger: createdLogger, eventBuilder: eventBuilder, eventDispatcher: eventDispatcher, errorHandler: errorHandler, }); sinon.stub(eventDispatcher, 'dispatchEvent'); sinon.stub(errorHandler, 'handleError'); sinon.stub(createdLogger, 'log'); }); afterEach(function () { eventDispatcher.dispatchEvent.restore(); errorHandler.handleError.restore(); createdLogger.log.restore(); }); var testUserAttributes = { 'browser_type': 'firefox', } var userAttributesWithBucketingId = { 'browser_type': 'firefox', '$opt_bucketing_id': '123456789' }; var invalidUserAttributesWithBucketingId = { 'browser_type': 'safari', '$opt_bucketing_id': 'testBucketingIdControl!' }; it('confirm normal bucketing occurs before setting bucketingId', function () { assert.strictEqual('variation', optlyInstance.getVariation( 'testExperiment', 'test_user', testUserAttributes)); }); it('confirm valid bucketing with bucketing ID set in attributes', function () { assert.strictEqual('variationWithAudience', optlyInstance.getVariation( 'testExperimentWithAudiences', 'test_user', userAttributesWithBucketingId )); }); it('check invalid audience with bucketingId', function () { assert.strictEqual(null, optlyInstance.getVariation( 'testExperimentWithAudiences', 'test_user', invalidUserAttributesWithBucketingId )); }); it('test that an experiment that is not running returns a null variation', function () { assert.strictEqual(null, optlyInstance.getVariation( 'testExperimentNotRunning', 'test_user', userAttributesWithBucketingId )); }); it('test that an invalid experiment key gets a null variation', function () { assert.strictEqual(null, optlyInstance.getVariation( 'invalidExperiment', 'test_user', userAttributesWithBucketingId )); }); it('check forced variation', function () { assert.isTrue(optlyInstance.setForcedVariation( 'testExperiment', 'test_user', 'control'), sprintf('Set variation to "%s" failed', 'control') ); assert.strictEqual('control', optlyInstance.getVariation( 'testExperiment', 'test_user', userAttributesWithBucketingId )); }); it('check whitelisted variation', function () { assert.strictEqual('control', optlyInstance.getVariation( 'testExperiment', 'user1', userAttributesWithBucketingId )); }); it('check user profile', function () { var userProfileLookupStub; var userProfileServiceInstance = { lookup: function () { }, } userProfileLookupStub = sinon.stub(userProfileServiceInstance, 'lookup'); userProfileLookupStub.returns({ user_id: 'test_user', experiment_bucket_map: { '111127': { 'variation_id': '111128' // ID of the 'control' variation }, }, }); var decisionServiceInstance = DecisionService.createDecisionService({ configObj: configObj, logger: createdLogger, userProfileService: userProfileServiceInstance, }); assert.strictEqual('control', decisionServiceInstance.getVariation( 'testExperiment', 'test_user', userAttributesWithBucketingId )); }); }); });