From a89b32fd551a225a17370eb4bd17913a5699978a Mon Sep 17 00:00:00 2001 From: Md Junaed Hossain <169046794+junaed-optimizely@users.noreply.github.com> Date: Mon, 20 Jan 2025 22:06:29 +0600 Subject: [PATCH 1/9] [FSSDK-11034] project config test conversion starts --- lib/project_config/project_config.spec.ts | 448 ++++++++++++++++++++++ 1 file changed, 448 insertions(+) create mode 100644 lib/project_config/project_config.spec.ts diff --git a/lib/project_config/project_config.spec.ts b/lib/project_config/project_config.spec.ts new file mode 100644 index 000000000..0d798e8ff --- /dev/null +++ b/lib/project_config/project_config.spec.ts @@ -0,0 +1,448 @@ +/** + * Copyright 2024, Optimizely + * + * 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 + * + * https://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. + */ +import { describe, it, expect, beforeEach, vi } from 'vitest'; +import { forEach, cloneDeep } from 'lodash'; +import { sprintf } from '../utils/fns'; +import fns from '../utils/fns'; +import projectConfig, { ProjectConfig } from './project_config'; +import { FEATURE_VARIABLE_TYPES, LOG_LEVEL } from '../utils/enums'; +import testDatafile from '../tests/test_data'; +import configValidator from '../utils/config_validator'; +import { + INVALID_EXPERIMENT_ID, + INVALID_EXPERIMENT_KEY, + UNEXPECTED_RESERVED_ATTRIBUTE_PREFIX, + UNRECOGNIZED_ATTRIBUTE, + VARIABLE_KEY_NOT_IN_DATAFILE, + FEATURE_NOT_IN_DATAFILE, + UNABLE_TO_CAST_VALUE, +} from '../error_messages'; +import exp from 'constants'; + +const createLogger = (...args: any) => ({ + debug: () => {}, + info: () => {}, + warn: () => {}, + error: () => {}, + child: () => createLogger(), +}); + +const buildLogMessageFromArgs = (args: any[]) => sprintf(args[1], ...args.splice(2)); +const logger = createLogger(); + +describe('createProjectConfig', () => { + let configObj: ProjectConfig; + + it('should set properties correctly when createProjectConfig is called', () => { + const testData: Record = testDatafile.getTestProjectConfig(); + configObj = projectConfig.createProjectConfig(testData as JSON); + + forEach(testData.audiences, audience => { + audience.conditions = JSON.parse(audience.conditions); + }); + + expect(configObj.accountId).toBe(testData.accountId); + expect(configObj.projectId).toBe(testData.projectId); + expect(configObj.revision).toBe(testData.revision); + expect(configObj.events).toEqual(testData.events); + expect(configObj.audiences).toEqual(testData.audiences); + + testData.groups.forEach((group: any) => { + group.experiments.forEach((experiment: any) => { + experiment.groupId = group.id; + experiment.variationKeyMap = fns.keyBy(experiment.variations, 'key'); + }); + }); + + expect(configObj.groups).toEqual(testData.groups); + + const expectedGroupIdMap = { + 666: testData.groups[0], + 667: testData.groups[1], + }; + + expect(configObj.groupIdMap).toEqual(expectedGroupIdMap); + + const expectedExperiments = testData.experiments.slice(); + forEach(configObj.groupIdMap, (group, groupId) => { + forEach(group.experiments, experiment => { + experiment.groupId = groupId; + expectedExperiments.push(experiment); + }); + }); + + forEach(expectedExperiments, experiment => { + experiment.variationKeyMap = fns.keyBy(experiment.variations, 'key'); + }); + + expect(configObj.experiments).toEqual(expectedExperiments); + + const expectedAttributeKeyMap = { + browser_type: testData.attributes[0], + boolean_key: testData.attributes[1], + integer_key: testData.attributes[2], + double_key: testData.attributes[3], + valid_positive_number: testData.attributes[4], + valid_negative_number: testData.attributes[5], + invalid_number: testData.attributes[6], + array: testData.attributes[7], + }; + + expect(configObj.attributeKeyMap).toEqual(expectedAttributeKeyMap); + + const expectedExperimentKeyMap = { + testExperiment: configObj.experiments[0], + testExperimentWithAudiences: configObj.experiments[1], + testExperimentNotRunning: configObj.experiments[2], + testExperimentLaunched: configObj.experiments[3], + groupExperiment1: configObj.experiments[4], + groupExperiment2: configObj.experiments[5], + overlappingGroupExperiment1: configObj.experiments[6], + }; + + expect(configObj.experimentKeyMap).toEqual(expectedExperimentKeyMap); + + const expectedEventKeyMap = { + testEvent: testData.events[0], + 'Total Revenue': testData.events[1], + testEventWithAudiences: testData.events[2], + testEventWithoutExperiments: testData.events[3], + testEventWithExperimentNotRunning: testData.events[4], + testEventWithMultipleExperiments: testData.events[5], + testEventLaunched: testData.events[6], + }; + + expect(configObj.eventKeyMap).toEqual(expectedEventKeyMap); + + const expectedExperimentIdMap = { + '111127': configObj.experiments[0], + '122227': configObj.experiments[1], + '133337': configObj.experiments[2], + '144447': configObj.experiments[3], + '442': configObj.experiments[4], + '443': configObj.experiments[5], + '444': configObj.experiments[6], + }; + + expect(configObj.experimentIdMap).toEqual(expectedExperimentIdMap); + }); + + it('should not mutate the datafile', () => { + const datafile = testDatafile.getTypedAudiencesConfig(); + const datafileClone = cloneDeep(datafile); + projectConfig.createProjectConfig(datafile as any); + + expect(datafile).toEqual(datafileClone); + }); +}); + +describe('createProjectConfig - feature management', () => { + let configObj: ProjectConfig; + + beforeEach(() => { + configObj = projectConfig.createProjectConfig(testDatafile.getTestProjectConfigWithFeatures()); + }); + + it('should create a rolloutIdMap from rollouts in the datafile', () => { + expect(configObj.rolloutIdMap).toEqual(testDatafile.datafileWithFeaturesExpectedData.rolloutIdMap); + }); + + it('creates a variationVariableUsageMap from rollouts and experiments with features in the datafile', () => { + expect(configObj.variationVariableUsageMap).toEqual( + testDatafile.datafileWithFeaturesExpectedData.variationVariableUsageMap + ); + }); + + it('creates a featureKeyMap from features in the datafile', () => { + expect(configObj.featureKeyMap).toEqual(testDatafile.datafileWithFeaturesExpectedData.featureKeyMap); + }); + + it('adds variations from rollout experiements to the variationKeyMap', () => { + expect(configObj.variationIdMap['594032']).toEqual({ + variables: [ + { value: 'true', id: '4919852825313280' }, + { value: '395', id: '5482802778734592' }, + { value: '4.99', id: '6045752732155904' }, + { value: 'Hello audience', id: '6327227708866560' }, + { value: '{ "count": 2, "message": "Hello audience" }', id: '8765345281230956' }, + ], + featureEnabled: true, + key: '594032', + id: '594032', + }); + + expect(configObj.variationIdMap['594038']).toEqual({ + variables: [ + { value: 'false', id: '4919852825313280' }, + { value: '400', id: '5482802778734592' }, + { value: '14.99', id: '6045752732155904' }, + { value: 'Hello', id: '6327227708866560' }, + { value: '{ "count": 1, "message": "Hello" }', id: '8765345281230956' }, + ], + featureEnabled: false, + key: '594038', + id: '594038', + }); + + expect(configObj.variationIdMap['594061']).toEqual({ + variables: [ + { value: '27.34', id: '5060590313668608' }, + { value: 'Winter is NOT coming', id: '5342065290379264' }, + { value: '10003', id: '6186490220511232' }, + { value: 'false', id: '6467965197221888' }, + ], + featureEnabled: true, + key: '594061', + id: '594061', + }); + + expect(configObj.variationIdMap['594067']).toEqual({ + variables: [ + { value: '30.34', id: '5060590313668608' }, + { value: 'Winter is coming definitely', id: '5342065290379264' }, + { value: '500', id: '6186490220511232' }, + { value: 'true', id: '6467965197221888' }, + ], + featureEnabled: true, + key: '594067', + id: '594067', + }); + }); +}); + +describe('createProjectConfig - flag variations', () => { + let configObj: ProjectConfig; + + beforeEach(() => { + configObj = projectConfig.createProjectConfig(testDatafile.getTestDecideProjectConfig()); + }); + + it('it should populate flagVariationsMap correctly', function() { + const allVariationsForFlag = configObj.flagVariationsMap; + const feature1Variations = allVariationsForFlag.feature_1; + const feature2Variations = allVariationsForFlag.feature_2; + const feature3Variations = allVariationsForFlag.feature_3; + const feature1VariationsKeys = feature1Variations.map(variation => { + return variation.key; + }, {}); + const feature2VariationsKeys = feature2Variations.map(variation => { + return variation.key; + }, {}); + const feature3VariationsKeys = feature3Variations.map(variation => { + return variation.key; + }, {}); + + expect(feature1VariationsKeys).toEqual(['a', 'b', '3324490633', '3324490562', '18257766532']); + expect(feature2VariationsKeys).toEqual(['variation_with_traffic', 'variation_no_traffic']); + expect(feature3VariationsKeys).toEqual([]); + }); +}); + +describe('getExperimentId', () => { + let testData: Record; + let configObj: ProjectConfig; + let createdLogger: any; + + beforeEach(function() { + testData = cloneDeep(testDatafile.getTestProjectConfig()); + configObj = projectConfig.createProjectConfig(cloneDeep(testData) as JSON); + createdLogger = createLogger({ logLevel: LOG_LEVEL.INFO }); + vi.spyOn(createdLogger, 'warn'); + }); + + it('should retrieve experiment ID for valid experiment key in getExperimentId', function() { + expect(projectConfig.getExperimentId(configObj, testData.experiments[0].key)).toBe(testData.experiments[0].id); + }); + + it('should throw error for invalid experiment key in getExperimentId', function() { + expect(() => projectConfig.getExperimentId(configObj, 'invalidExperimentKey')).toThrowError( + sprintf(INVALID_EXPERIMENT_KEY, 'PROJECT_CONFIG', 'invalidExperimentKey') + ); + }); +}); + +describe('getLayerId', () => { + let testData: Record; + let configObj: ProjectConfig; + + beforeEach(function() { + testData = cloneDeep(testDatafile.getTestProjectConfig()); + configObj = projectConfig.createProjectConfig(cloneDeep(testData) as JSON); + }); + + it('should retrieve layer ID for valid experiment key in getLayerId', function() { + expect(projectConfig.getLayerId(configObj, '111127')).toBe('4'); + }); + + it('should throw error for invalid experiment key in getLayerId', function() { + expect(() => projectConfig.getLayerId(configObj, 'invalidExperimentKey')).toThrowError( + sprintf(INVALID_EXPERIMENT_ID, 'PROJECT_CONFIG', 'invalidExperimentKey') + ); + }); +}); + +describe('getAttributeId', () => { + let testData: Record; + let configObj: ProjectConfig; + let createdLogger: any; + + beforeEach(function() { + testData = cloneDeep(testDatafile.getTestProjectConfig()); + configObj = projectConfig.createProjectConfig(cloneDeep(testData) as JSON); + createdLogger = createLogger({ logLevel: LOG_LEVEL.INFO }); + vi.spyOn(createdLogger, 'warn'); + }); + + it('should retrieve attribute ID for valid attribute key in getAttributeId', function() { + expect(projectConfig.getAttributeId(configObj, 'browser_type')).toBe('111094'); + }); + + it('should retrieve attribute ID for reserved attribute key in getAttributeId', function() { + expect(projectConfig.getAttributeId(configObj, '$opt_user_agent')).toBe('$opt_user_agent'); + }); + + it('should return null for invalid attribute key in getAttributeId', function() { + expect(projectConfig.getAttributeId(configObj, 'invalidAttributeKey', createdLogger)).toBe(null); + expect(createdLogger.warn).toHaveBeenCalledWith(UNRECOGNIZED_ATTRIBUTE, 'invalidAttributeKey'); + }); + + it('should return null for invalid attribute key in getAttributeId', () => { + // Adding attribute in key map with reserved prefix + configObj.attributeKeyMap['$opt_some_reserved_attribute'] = { + id: '42', + }; + + expect(projectConfig.getAttributeId(configObj, '$opt_some_reserved_attribute', createdLogger)).toBe('42'); + expect(createdLogger.warn).toHaveBeenCalledWith( + UNEXPECTED_RESERVED_ATTRIBUTE_PREFIX, + '$opt_some_reserved_attribute', + '$opt_' + ); + }); +}); + +describe('getEventId', () => { + let testData: Record; + let configObj: ProjectConfig; + + beforeEach(function() { + testData = cloneDeep(testDatafile.getTestProjectConfig()); + configObj = projectConfig.createProjectConfig(cloneDeep(testData) as JSON); + }); + + it('should retrieve event ID for valid event key in getEventId', function() { + expect(projectConfig.getEventId(configObj, 'testEvent')).toBe('111095'); + }); + + it('should return null for invalid event key in getEventId', function() { + expect(projectConfig.getEventId(configObj, 'invalidEventKey')).toBe(null); + }); +}); + +describe('getExperimentStatus', () => { + let testData: Record; + let configObj: ProjectConfig; + + beforeEach(function() { + testData = cloneDeep(testDatafile.getTestProjectConfig()); + configObj = projectConfig.createProjectConfig(cloneDeep(testData) as JSON); + }); + + it('should retrieve experiment status for valid experiment key in getExperimentStatus', function() { + expect(projectConfig.getExperimentStatus(configObj, testData.experiments[0].key)).toBe( + testData.experiments[0].status + ); + }); + + it('should throw error for invalid experiment key in getExperimentStatus', function() { + expect(() => projectConfig.getExperimentStatus(configObj, 'invalidExperimentKey')).toThrowError( + sprintf(INVALID_EXPERIMENT_KEY, 'PROJECT_CONFIG', 'invalidExperimentKey') + ); + }); +}); + +describe('isActive', () => { + let testData: Record; + let configObj: ProjectConfig; + + beforeEach(function() { + testData = cloneDeep(testDatafile.getTestProjectConfig()); + configObj = projectConfig.createProjectConfig(cloneDeep(testData) as JSON); + }); + + it('should return true if experiment status is set to Running in isActive', function() { + expect(projectConfig.isActive(configObj, 'testExperiment')).toBe(true); + }); + + it('should return false if experiment status is not set to Running in isActive', function() { + expect(projectConfig.isActive(configObj, 'testExperimentNotRunning')).toBe(false); + }); +}); + +describe('isRunning', () => { + let testData: Record; + let configObj: ProjectConfig; + + beforeEach(() => { + testData = cloneDeep(testDatafile.getTestProjectConfig()); + configObj = projectConfig.createProjectConfig(cloneDeep(testData) as JSON); + }); + + it('should return true if experiment status is set to Running in isRunning', function() { + expect(projectConfig.isRunning(configObj, 'testExperiment')).toBe(true); + }); + + it('should return false if experiment status is not set to Running in isRunning', function() { + expect(projectConfig.isRunning(configObj, 'testExperimentLaunched')).toBe(false); + }); +}); + +describe('getVariationKeyFromId', () => { + let testData: Record; + let configObj: ProjectConfig; + + beforeEach(function() { + testData = cloneDeep(testDatafile.getTestProjectConfig()); + configObj = projectConfig.createProjectConfig(cloneDeep(testData) as JSON); + }); + it('should retrieve variation key for valid experiment key and variation ID in getVariationKeyFromId', function() { + expect(projectConfig.getVariationKeyFromId(configObj, testData.experiments[0].variations[0].id)).toBe( + testData.experiments[0].variations[0].key + ); + }); +}); + +describe('getTrafficAllocation', () => { + let testData: Record; + let configObj: ProjectConfig; + + beforeEach(function() { + testData = cloneDeep(testDatafile.getTestProjectConfig()); + configObj = projectConfig.createProjectConfig(cloneDeep(testData) as JSON); + }); + + it('should retrieve traffic allocation given valid experiment key in getTrafficAllocation', function() { + expect(projectConfig.getTrafficAllocation(configObj, testData.experiments[0].id)).toEqual( + testData.experiments[0].trafficAllocation + ); + }); + + it('should throw error for invalid experient key in getTrafficAllocation', function() { + expect(() => projectConfig.getTrafficAllocation(configObj, 'invalidExperimentId')).toThrowError( + sprintf(INVALID_EXPERIMENT_ID, 'PROJECT_CONFIG', 'invalidExperimentId') + ); + }); +}); From ceb610ef4c1edac2fd38da71730359c67303d199 Mon Sep 17 00:00:00 2001 From: Md Junaed Hossain <169046794+junaed-optimizely@users.noreply.github.com> Date: Wed, 22 Jan 2025 01:34:17 +0600 Subject: [PATCH 2/9] [FSSDK-11034] project config test in progress --- lib/project_config/project_config.spec.ts | 578 +++++++++++++++++++++- 1 file changed, 569 insertions(+), 9 deletions(-) diff --git a/lib/project_config/project_config.spec.ts b/lib/project_config/project_config.spec.ts index 0d798e8ff..50deb3108 100644 --- a/lib/project_config/project_config.spec.ts +++ b/lib/project_config/project_config.spec.ts @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { describe, it, expect, beforeEach, vi } from 'vitest'; +import { describe, it, expect, beforeEach, afterEach, vi, assert } from 'vitest'; import { forEach, cloneDeep } from 'lodash'; import { sprintf } from '../utils/fns'; import fns from '../utils/fns'; @@ -31,6 +31,8 @@ import { UNABLE_TO_CAST_VALUE, } from '../error_messages'; import exp from 'constants'; +import { VariableType } from '../shared_types'; +import { OptimizelyError } from '../error/optimizly_error'; const createLogger = (...args: any) => ({ debug: () => {}, @@ -268,8 +270,13 @@ describe('getExperimentId', () => { }); it('should throw error for invalid experiment key in getExperimentId', function() { - expect(() => projectConfig.getExperimentId(configObj, 'invalidExperimentKey')).toThrowError( - sprintf(INVALID_EXPERIMENT_KEY, 'PROJECT_CONFIG', 'invalidExperimentKey') + expect(() => { + projectConfig.getExperimentId(configObj, 'invalidExperimentId'); + }).toThrowError( + expect.objectContaining({ + baseMessage: INVALID_EXPERIMENT_KEY, + params: ['invalidExperimentId'], + }) ); }); }); @@ -288,8 +295,14 @@ describe('getLayerId', () => { }); it('should throw error for invalid experiment key in getLayerId', function() { + // expect(() => projectConfig.getLayerId(configObj, 'invalidExperimentKey')).toThrowError( + // sprintf(INVALID_EXPERIMENT_ID, 'PROJECT_CONFIG', 'invalidExperimentKey') + // ); expect(() => projectConfig.getLayerId(configObj, 'invalidExperimentKey')).toThrowError( - sprintf(INVALID_EXPERIMENT_ID, 'PROJECT_CONFIG', 'invalidExperimentKey') + expect.objectContaining({ + baseMessage: INVALID_EXPERIMENT_ID, + params: ['invalidExperimentKey'], + }) ); }); }); @@ -368,9 +381,9 @@ describe('getExperimentStatus', () => { }); it('should throw error for invalid experiment key in getExperimentStatus', function() { - expect(() => projectConfig.getExperimentStatus(configObj, 'invalidExperimentKey')).toThrowError( - sprintf(INVALID_EXPERIMENT_KEY, 'PROJECT_CONFIG', 'invalidExperimentKey') - ); + expect(() => { + projectConfig.getExperimentStatus(configObj, 'invalidExeprimentKey'); + }).toThrowError(OptimizelyError); }); }); @@ -441,8 +454,555 @@ describe('getTrafficAllocation', () => { }); it('should throw error for invalid experient key in getTrafficAllocation', function() { - expect(() => projectConfig.getTrafficAllocation(configObj, 'invalidExperimentId')).toThrowError( - sprintf(INVALID_EXPERIMENT_ID, 'PROJECT_CONFIG', 'invalidExperimentId') + expect(() => { + projectConfig.getTrafficAllocation(configObj, 'invalidExperimentId'); + }).toThrowError( + expect.objectContaining({ + baseMessage: INVALID_EXPERIMENT_ID, + params: ['invalidExperimentId'], + }) + ); + }); +}); + +describe('getVariationIdFromExperimentAndVariationKey', () => { + let testData: Record; + let configObj: ProjectConfig; + + beforeEach(function() { + testData = cloneDeep(testDatafile.getTestProjectConfig()); + configObj = projectConfig.createProjectConfig(cloneDeep(testData) as JSON); + }); + + it('should return the variation id for the given experiment key and variation key', () => { + expect( + projectConfig.getVariationIdFromExperimentAndVariationKey( + configObj, + testData.experiments[0].key, + testData.experiments[0].variations[0].key + ) + ).toBe(testData.experiments[0].variations[0].id); + }); +}); + +describe('getSendFlagDecisionsValue', () => { + let testData: Record; + let configObj: ProjectConfig; + + beforeEach(function() { + testData = cloneDeep(testDatafile.getTestProjectConfig()); + configObj = projectConfig.createProjectConfig(cloneDeep(testData) as JSON); + }); + + it('should return false when sendFlagDecisions is undefined', () => { + configObj.sendFlagDecisions = undefined; + + expect(projectConfig.getSendFlagDecisionsValue(configObj)).toBe(false); + }); + + it('should return false when sendFlagDecisions is set to false', () => { + configObj.sendFlagDecisions = false; + + expect(projectConfig.getSendFlagDecisionsValue(configObj)).toBe(false); + }); + + it('should return true when sendFlagDecisions is set to true', () => { + configObj.sendFlagDecisions = true; + + expect(projectConfig.getSendFlagDecisionsValue(configObj)).toBe(true); + }); +}); + +describe('getVariableForFeature', function() { + let featureManagementLogger: ReturnType; + let configObj: ProjectConfig; + + beforeEach(() => { + featureManagementLogger = createLogger({ logLevel: LOG_LEVEL.INFO }); + configObj = projectConfig.createProjectConfig(testDatafile.getTestProjectConfigWithFeatures()); + vi.spyOn(featureManagementLogger, 'warn'); + vi.spyOn(featureManagementLogger, 'error'); + vi.spyOn(featureManagementLogger, 'info'); + vi.spyOn(featureManagementLogger, 'debug'); + }); + + afterEach(() => { + vi.restoreAllMocks(); + }); + + it('should return a variable object for a valid variable and feature key', function() { + const featureKey = 'test_feature_for_experiment'; + const variableKey = 'num_buttons'; + const result = projectConfig.getVariableForFeature(configObj, featureKey, variableKey, featureManagementLogger); + + expect(result).toEqual({ + type: 'integer', + key: 'num_buttons', + id: '4792309476491264', + defaultValue: '10', + }); + }); + + it('should return null for an invalid variable key and a valid feature key', function() { + const featureKey = 'test_feature_for_experiment'; + const variableKey = 'notARealVariable____'; + const result = projectConfig.getVariableForFeature(configObj, featureKey, variableKey, featureManagementLogger); + + expect(result).toBe(null); + expect(featureManagementLogger.error).toHaveBeenCalledOnce(); + expect(featureManagementLogger.error).toHaveBeenCalledWith( + VARIABLE_KEY_NOT_IN_DATAFILE, + 'notARealVariable____', + 'test_feature_for_experiment' + ); + }); + + it('should return null for an invalid feature key', function() { + const featureKey = 'notARealFeature_____'; + const variableKey = 'num_buttons'; + const result = projectConfig.getVariableForFeature(configObj, featureKey, variableKey, featureManagementLogger); + + expect(result).toBe(null); + expect(featureManagementLogger.error).toHaveBeenCalledOnce(); + expect(featureManagementLogger.error).toHaveBeenCalledWith(FEATURE_NOT_IN_DATAFILE, 'notARealFeature_____'); + }); + + it('should return null for an invalid variable key and an invalid feature key', function() { + const featureKey = 'notARealFeature_____'; + const variableKey = 'notARealVariable____'; + const result = projectConfig.getVariableForFeature(configObj, featureKey, variableKey, featureManagementLogger); + + expect(result).toBe(null); + expect(featureManagementLogger.error).toHaveBeenCalledOnce(); + expect(featureManagementLogger.error).toHaveBeenCalledWith(FEATURE_NOT_IN_DATAFILE, 'notARealFeature_____'); + }); +}); + +describe('getVariableValueForVariation', () => { + let featureManagementLogger: ReturnType; + let configObj: ProjectConfig; + + beforeEach(() => { + featureManagementLogger = createLogger({ logLevel: LOG_LEVEL.INFO }); + configObj = projectConfig.createProjectConfig(testDatafile.getTestProjectConfigWithFeatures()); + vi.spyOn(featureManagementLogger, 'warn'); + vi.spyOn(featureManagementLogger, 'error'); + vi.spyOn(featureManagementLogger, 'info'); + vi.spyOn(featureManagementLogger, 'debug'); + }); + + afterEach(() => { + vi.restoreAllMocks(); + }); + + it('returns a value for a valid variation and variable', () => { + const variation = configObj.variationIdMap['594096']; + let variable = configObj.featureKeyMap.test_feature_for_experiment.variableKeyMap.num_buttons; + let result = projectConfig.getVariableValueForVariation(configObj, variable, variation, featureManagementLogger); + expect(result).toBe('2'); + + variable = configObj.featureKeyMap.test_feature_for_experiment.variableKeyMap.is_button_animated; + result = projectConfig.getVariableValueForVariation(configObj, variable, variation, featureManagementLogger); + + expect(result).toBe('true'); + + variable = configObj.featureKeyMap.test_feature_for_experiment.variableKeyMap.button_txt; + result = projectConfig.getVariableValueForVariation(configObj, variable, variation, featureManagementLogger); + + expect(result).toBe('Buy me NOW'); + + variable = configObj.featureKeyMap.test_feature_for_experiment.variableKeyMap.button_width; + result = projectConfig.getVariableValueForVariation(configObj, variable, variation, featureManagementLogger); + + expect(result).toBe('20.25'); + }); + + it('returns null for a null variation', () => { + const variation = null; + const variable = configObj.featureKeyMap.test_feature_for_experiment.variableKeyMap.num_buttons; + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + const result = projectConfig.getVariableValueForVariation(configObj, variable, variation, featureManagementLogger); + + expect(result).toBe(null); + }); + + it('returns null for a null variable', () => { + const variation = configObj.variationIdMap['594096']; + const variable = null; + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + const result = projectConfig.getVariableValueForVariation(configObj, variable, variation, featureManagementLogger); + + expect(result).toBe(null); + }); + + it('returns null for a null variation and null variable', () => { + const variation = null; + const variable = null; + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + const result = projectConfig.getVariableValueForVariation(configObj, variable, variation, featureManagementLogger); + + expect(result).toBe(null); + }); + + it('returns null for a variation whose id is not in the datafile', () => { + const variation = { + key: 'some_variation', + id: '999999999999', + variables: [], + }; + const variable = configObj.featureKeyMap.test_feature_for_experiment.variableKeyMap.num_buttons; + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + const result = projectConfig.getVariableValueForVariation(configObj, variable, variation, featureManagementLogger); + + expect(result).toBe(null); + }); + + it('returns null if the variation does not have a value for this variable', () => { + const variation = configObj.variationIdMap['595008']; // This variation has no variable values associated with it + const variable = configObj.featureKeyMap.test_feature_for_experiment.variableKeyMap.num_buttons; + const result = projectConfig.getVariableValueForVariation(configObj, variable, variation, featureManagementLogger); + + expect(result).toBe(null); + }); +}); + +describe('getTypeCastValue', () => { + let featureManagementLogger: ReturnType; + let configObj: ProjectConfig; + + beforeEach(() => { + featureManagementLogger = createLogger({ logLevel: LOG_LEVEL.INFO }); + configObj = projectConfig.createProjectConfig(testDatafile.getTestProjectConfigWithFeatures()); + vi.spyOn(featureManagementLogger, 'warn'); + vi.spyOn(featureManagementLogger, 'error'); + vi.spyOn(featureManagementLogger, 'info'); + vi.spyOn(featureManagementLogger, 'debug'); + }); + + afterEach(() => { + vi.restoreAllMocks(); + }); + + it('can cast a boolean', () => { + let result = projectConfig.getTypeCastValue( + 'true', + FEATURE_VARIABLE_TYPES.BOOLEAN as VariableType, + featureManagementLogger + ); + + expect(result).toBe(true); + + result = projectConfig.getTypeCastValue( + 'false', + FEATURE_VARIABLE_TYPES.BOOLEAN as VariableType, + featureManagementLogger + ); + + expect(result).toBe(false); + }); + + it('can cast an integer', () => { + let result = projectConfig.getTypeCastValue( + '50', + FEATURE_VARIABLE_TYPES.INTEGER as VariableType, + featureManagementLogger + ); + + expect(result).toBe(50); + + result = projectConfig.getTypeCastValue( + '-7', + FEATURE_VARIABLE_TYPES.INTEGER as VariableType, + featureManagementLogger ); + + expect(result).toBe(-7); + + result = projectConfig.getTypeCastValue( + '0', + FEATURE_VARIABLE_TYPES.INTEGER as VariableType, + featureManagementLogger + ); + + expect(result).toBe(0); + }); + + it('can cast a double', () => { + let result = projectConfig.getTypeCastValue( + '89.99', + FEATURE_VARIABLE_TYPES.DOUBLE as VariableType, + featureManagementLogger + ); + + expect(result).toBe(89.99); + + result = projectConfig.getTypeCastValue( + '-257.21', + FEATURE_VARIABLE_TYPES.DOUBLE as VariableType, + featureManagementLogger + ); + + expect(result).toBe(-257.21); + + result = projectConfig.getTypeCastValue( + '0', + FEATURE_VARIABLE_TYPES.DOUBLE as VariableType, + featureManagementLogger + ); + + expect(result).toBe(0); + + result = projectConfig.getTypeCastValue( + '10', + FEATURE_VARIABLE_TYPES.DOUBLE as VariableType, + featureManagementLogger + ); + + expect(result).toBe(10); + }); + + it('can return a string unmodified', () => { + const result = projectConfig.getTypeCastValue( + 'message', + FEATURE_VARIABLE_TYPES.STRING as VariableType, + featureManagementLogger + ); + + expect(result).toBe('message'); + }); + + it('returns null and logs an error for an invalid boolean', () => { + const result = projectConfig.getTypeCastValue( + 'notabool', + FEATURE_VARIABLE_TYPES.BOOLEAN as VariableType, + featureManagementLogger + ); + + expect(result).toBe(null); + expect(featureManagementLogger.error).toHaveBeenCalledWith(UNABLE_TO_CAST_VALUE, 'notabool', 'boolean'); + }); + + it('returns null and logs an error for an invalid integer', () => { + const result = projectConfig.getTypeCastValue( + 'notanint', + FEATURE_VARIABLE_TYPES.INTEGER as VariableType, + featureManagementLogger + ); + + expect(result).toBe(null); + expect(featureManagementLogger.error).toHaveBeenCalledWith(UNABLE_TO_CAST_VALUE, 'notanint', 'integer'); + }); + + it('returns null and logs an error for an invalid double', () => { + const result = projectConfig.getTypeCastValue( + 'notadouble', + FEATURE_VARIABLE_TYPES.DOUBLE as VariableType, + featureManagementLogger + ); + + expect(result).toBe(null); + expect(featureManagementLogger.error).toHaveBeenCalledWith(UNABLE_TO_CAST_VALUE, 'notadouble', 'double'); + }); +}); + +describe('getAudiencesById', () => { + let configObj: ProjectConfig; + + beforeEach(() => { + configObj = projectConfig.createProjectConfig(testDatafile.getTypedAudiencesConfig()); + }); + + it('should retrieve audiences by checking first in typedAudiences, and then second in audiences', () => { + expect(projectConfig.getAudiencesById(configObj)).toEqual(testDatafile.typedAudiencesById); + }); +}); + +describe('getExperimentAudienceConditions', () => { + let configObj: ProjectConfig; + let testData: Record; + + beforeEach(() => { + testData = cloneDeep(testDatafile.getTestProjectConfig()); + }); + + it('should retrieve audiences for valid experiment key', () => { + configObj = projectConfig.createProjectConfig(cloneDeep(testData) as JSON); + + expect(projectConfig.getExperimentAudienceConditions(configObj, testData.experiments[1].id)).toEqual(['11154']); + }); + + it('should throw error for invalid experiment key', () => { + configObj = projectConfig.createProjectConfig(cloneDeep(testData) as JSON); + + expect(() => { + projectConfig.getExperimentAudienceConditions(configObj, 'invalidExperimentId'); + }).toThrowError( + expect.objectContaining({ + baseMessage: INVALID_EXPERIMENT_ID, + params: ['invalidExperimentId'], + }) + ); + }); + + it('should return experiment audienceIds if experiment has no audienceConditions', () => { + configObj = projectConfig.createProjectConfig(testDatafile.getTypedAudiencesConfig()); + const result = projectConfig.getExperimentAudienceConditions(configObj, '11564051718'); + + expect(result).toEqual([ + '3468206642', + '3988293898', + '3988293899', + '3468206646', + '3468206647', + '3468206644', + '3468206643', + ]); + }); + + it('should return experiment audienceConditions if experiment has audienceConditions', () => { + configObj = projectConfig.createProjectConfig(testDatafile.getTypedAudiencesConfig()); + // audience_combinations_experiment has both audienceConditions and audienceIds + // audienceConditions should be preferred over audienceIds + const result = projectConfig.getExperimentAudienceConditions(configObj, '1323241598'); + + expect(result).toEqual([ + 'and', + ['or', '3468206642', '3988293898'], + ['or', '3988293899', '3468206646', '3468206647', '3468206644', '3468206643'], + ]); + }); +}); + +describe('isFeatureExperiment', () => { + it('returns true for a feature test', () => { + const config = projectConfig.createProjectConfig(testDatafile.getTestProjectConfigWithFeatures()); + const result = projectConfig.isFeatureExperiment(config, '594098'); // id of 'testing_my_feature' + + expect(result).toBe(true); + }); + + it('returns false for an A/B test', () => { + const config = projectConfig.createProjectConfig(testDatafile.getTestProjectConfig()); + const result = projectConfig.isFeatureExperiment(config, '111127'); // id of 'testExperiment' + + expect(result).toBe(false); + }); + + it('returns true for a feature test in a mutex group', () => { + const config = projectConfig.createProjectConfig(testDatafile.getMutexFeatureTestsConfig()); + let result = projectConfig.isFeatureExperiment(config, '17128410791'); // id of 'f_test1' + + expect(result).toBe(true); + + result = projectConfig.isFeatureExperiment(config, '17139931304'); // id of 'f_test2' + + expect(result).toBe(true); + }); +}); + +describe('getAudienceSegments', () => { + it('returns all qualified segments from an audience', () => { + const dummyQualifiedAudienceJson = { + id: '13389142234', + conditions: [ + 'and', + [ + 'or', + [ + 'or', + { + value: 'odp-segment-1', + type: 'third_party_dimension', + name: 'odp.audiences', + match: 'qualified', + }, + ], + ], + ], + name: 'odp-segment-1', + }; + + const dummyQualifiedAudienceJsonSegments = projectConfig.getAudienceSegments(dummyQualifiedAudienceJson); + + expect(dummyQualifiedAudienceJsonSegments).toEqual(['odp-segment-1']); + + const dummyUnqualifiedAudienceJson = { + id: '13389142234', + conditions: [ + 'and', + [ + 'or', + [ + 'or', + { + value: 'odp-segment-1', + type: 'third_party_dimension', + name: 'odp.audiences', + match: 'invalid', + }, + ], + ], + ], + name: 'odp-segment-1', + }; + + const dummyUnqualifiedAudienceJsonSegments = projectConfig.getAudienceSegments(dummyUnqualifiedAudienceJson); + + expect(dummyUnqualifiedAudienceJsonSegments).toEqual([]); + }); +}); + +describe('integrations: with segments', () => { + let configObj: ProjectConfig; + + beforeEach(() => { + configObj = projectConfig.createProjectConfig(testDatafile.getOdpIntegratedConfigWithSegments()); + }); + + it('should convert integrations from the datafile into the project config', () => { + expect(configObj.integrations).toBeDefined(); + expect(configObj.integrations.length).toBe(4); + }); + + it('should populate odpIntegrationConfig', () => { + expect(configObj.odpIntegrationConfig.integrated).toBe(true); + + assert(configObj.odpIntegrationConfig.integrated); + + expect(configObj.odpIntegrationConfig.odpConfig.apiKey).toBe('W4WzcEs-ABgXorzY7h1LCQ'); + expect(configObj.odpIntegrationConfig.odpConfig.apiHost).toBe('https://api.zaius.com'); + expect(configObj.odpIntegrationConfig.odpConfig.pixelUrl).toBe('https://jumbe.zaius.com'); + expect(configObj.odpIntegrationConfig.odpConfig.segmentsToCheck).toEqual([ + 'odp-segment-1', + 'odp-segment-2', + 'odp-segment-3', + ]); + }); +}); + +describe('withoutSegments', () => { + let config: ProjectConfig; + beforeEach(() => { + config = projectConfig.createProjectConfig(testDatafile.getOdpIntegratedConfigWithoutSegments()); + }); + + it('should convert integrations from the datafile into the project config', () => { + expect(config.integrations).toBeDefined(); + expect(config.integrations.length).toBe(3); + }); + + it('should populate odpIntegrationConfig', () => { + expect(config.odpIntegrationConfig.integrated).toBe(true); + + assert(config.odpIntegrationConfig.integrated); + + expect(config.odpIntegrationConfig.odpConfig.apiKey).toBe('W4WzcEs-ABgXorzY7h1LCQ'); + expect(config.odpIntegrationConfig.odpConfig.apiHost).toBe('https://api.zaius.com'); + expect(config.odpIntegrationConfig.odpConfig.pixelUrl).toBe('https://jumbe.zaius.com'); + expect(config.odpIntegrationConfig.odpConfig.segmentsToCheck).toEqual([]); }); }); From b8dc2adaf477d2e28523f0d36de0d773ed88762b Mon Sep 17 00:00:00 2001 From: Md Junaed Hossain <169046794+junaed-optimizely@users.noreply.github.com> Date: Thu, 23 Jan 2025 01:19:03 +0600 Subject: [PATCH 3/9] [FSSDK-11034] project config test completion --- lib/project_config/project_config.spec.ts | 172 +++- lib/project_config/project_config.tests.js | 974 --------------------- package.json | 2 +- 3 files changed, 150 insertions(+), 998 deletions(-) delete mode 100644 lib/project_config/project_config.tests.js diff --git a/lib/project_config/project_config.spec.ts b/lib/project_config/project_config.spec.ts index 50deb3108..8ebcbae84 100644 --- a/lib/project_config/project_config.spec.ts +++ b/lib/project_config/project_config.spec.ts @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { describe, it, expect, beforeEach, afterEach, vi, assert } from 'vitest'; +import { describe, it, expect, beforeEach, afterEach, vi, assert, Mock } from 'vitest'; import { forEach, cloneDeep } from 'lodash'; import { sprintf } from '../utils/fns'; import fns from '../utils/fns'; @@ -33,6 +33,7 @@ import { import exp from 'constants'; import { VariableType } from '../shared_types'; import { OptimizelyError } from '../error/optimizly_error'; +import { J } from 'vitest/dist/chunks/environment.0M5R1SX_.js'; const createLogger = (...args: any) => ({ debug: () => {}, @@ -162,17 +163,17 @@ describe('createProjectConfig - feature management', () => { expect(configObj.rolloutIdMap).toEqual(testDatafile.datafileWithFeaturesExpectedData.rolloutIdMap); }); - it('creates a variationVariableUsageMap from rollouts and experiments with features in the datafile', () => { + it('should create a variationVariableUsageMap from rollouts and experiments with features in the datafile', () => { expect(configObj.variationVariableUsageMap).toEqual( testDatafile.datafileWithFeaturesExpectedData.variationVariableUsageMap ); }); - it('creates a featureKeyMap from features in the datafile', () => { + it('should create a featureKeyMap from features in the datafile', () => { expect(configObj.featureKeyMap).toEqual(testDatafile.datafileWithFeaturesExpectedData.featureKeyMap); }); - it('adds variations from rollout experiements to the variationKeyMap', () => { + it('should add variations from rollout experiements to the variationKeyMap', () => { expect(configObj.variationIdMap['594032']).toEqual({ variables: [ { value: 'true', id: '4919852825313280' }, @@ -232,7 +233,7 @@ describe('createProjectConfig - flag variations', () => { configObj = projectConfig.createProjectConfig(testDatafile.getTestDecideProjectConfig()); }); - it('it should populate flagVariationsMap correctly', function() { + it('should populate flagVariationsMap correctly', function() { const allVariationsForFlag = configObj.flagVariationsMap; const feature1Variations = allVariationsForFlag.feature_1; const feature2Variations = allVariationsForFlag.feature_2; @@ -595,7 +596,7 @@ describe('getVariableValueForVariation', () => { vi.restoreAllMocks(); }); - it('returns a value for a valid variation and variable', () => { + it('should return a value for a valid variation and variable', () => { const variation = configObj.variationIdMap['594096']; let variable = configObj.featureKeyMap.test_feature_for_experiment.variableKeyMap.num_buttons; let result = projectConfig.getVariableValueForVariation(configObj, variable, variation, featureManagementLogger); @@ -617,7 +618,7 @@ describe('getVariableValueForVariation', () => { expect(result).toBe('20.25'); }); - it('returns null for a null variation', () => { + it('should return null for a null variation', () => { const variation = null; const variable = configObj.featureKeyMap.test_feature_for_experiment.variableKeyMap.num_buttons; // eslint-disable-next-line @typescript-eslint/ban-ts-comment @@ -627,7 +628,7 @@ describe('getVariableValueForVariation', () => { expect(result).toBe(null); }); - it('returns null for a null variable', () => { + it('should return null for a null variable', () => { const variation = configObj.variationIdMap['594096']; const variable = null; // eslint-disable-next-line @typescript-eslint/ban-ts-comment @@ -637,7 +638,7 @@ describe('getVariableValueForVariation', () => { expect(result).toBe(null); }); - it('returns null for a null variation and null variable', () => { + it('should return null for a null variation and null variable', () => { const variation = null; const variable = null; // eslint-disable-next-line @typescript-eslint/ban-ts-comment @@ -647,7 +648,7 @@ describe('getVariableValueForVariation', () => { expect(result).toBe(null); }); - it('returns null for a variation whose id is not in the datafile', () => { + it('should return null for a variation whose id is not in the datafile', () => { const variation = { key: 'some_variation', id: '999999999999', @@ -661,7 +662,7 @@ describe('getVariableValueForVariation', () => { expect(result).toBe(null); }); - it('returns null if the variation does not have a value for this variable', () => { + it('should return null if the variation does not have a value for this variable', () => { const variation = configObj.variationIdMap['595008']; // This variation has no variable values associated with it const variable = configObj.featureKeyMap.test_feature_for_experiment.variableKeyMap.num_buttons; const result = projectConfig.getVariableValueForVariation(configObj, variable, variation, featureManagementLogger); @@ -687,7 +688,7 @@ describe('getTypeCastValue', () => { vi.restoreAllMocks(); }); - it('can cast a boolean', () => { + it('should cast a boolean', () => { let result = projectConfig.getTypeCastValue( 'true', FEATURE_VARIABLE_TYPES.BOOLEAN as VariableType, @@ -705,7 +706,7 @@ describe('getTypeCastValue', () => { expect(result).toBe(false); }); - it('can cast an integer', () => { + it('should cast an integer', () => { let result = projectConfig.getTypeCastValue( '50', FEATURE_VARIABLE_TYPES.INTEGER as VariableType, @@ -731,7 +732,7 @@ describe('getTypeCastValue', () => { expect(result).toBe(0); }); - it('can cast a double', () => { + it('should cast a double', () => { let result = projectConfig.getTypeCastValue( '89.99', FEATURE_VARIABLE_TYPES.DOUBLE as VariableType, @@ -765,7 +766,7 @@ describe('getTypeCastValue', () => { expect(result).toBe(10); }); - it('can return a string unmodified', () => { + it('should return a string unmodified', () => { const result = projectConfig.getTypeCastValue( 'message', FEATURE_VARIABLE_TYPES.STRING as VariableType, @@ -775,7 +776,7 @@ describe('getTypeCastValue', () => { expect(result).toBe('message'); }); - it('returns null and logs an error for an invalid boolean', () => { + it('should return null and logs an error for an invalid boolean', () => { const result = projectConfig.getTypeCastValue( 'notabool', FEATURE_VARIABLE_TYPES.BOOLEAN as VariableType, @@ -786,7 +787,7 @@ describe('getTypeCastValue', () => { expect(featureManagementLogger.error).toHaveBeenCalledWith(UNABLE_TO_CAST_VALUE, 'notabool', 'boolean'); }); - it('returns null and logs an error for an invalid integer', () => { + it('should return null and logs an error for an invalid integer', () => { const result = projectConfig.getTypeCastValue( 'notanint', FEATURE_VARIABLE_TYPES.INTEGER as VariableType, @@ -797,7 +798,7 @@ describe('getTypeCastValue', () => { expect(featureManagementLogger.error).toHaveBeenCalledWith(UNABLE_TO_CAST_VALUE, 'notanint', 'integer'); }); - it('returns null and logs an error for an invalid double', () => { + it('should return null and logs an error for an invalid double', () => { const result = projectConfig.getTypeCastValue( 'notadouble', FEATURE_VARIABLE_TYPES.DOUBLE as VariableType, @@ -878,21 +879,21 @@ describe('getExperimentAudienceConditions', () => { }); describe('isFeatureExperiment', () => { - it('returns true for a feature test', () => { + it('should return true for a feature test', () => { const config = projectConfig.createProjectConfig(testDatafile.getTestProjectConfigWithFeatures()); const result = projectConfig.isFeatureExperiment(config, '594098'); // id of 'testing_my_feature' expect(result).toBe(true); }); - it('returns false for an A/B test', () => { + it('should return false for an A/B test', () => { const config = projectConfig.createProjectConfig(testDatafile.getTestProjectConfig()); const result = projectConfig.isFeatureExperiment(config, '111127'); // id of 'testExperiment' expect(result).toBe(false); }); - it('returns true for a feature test in a mutex group', () => { + it('should return true for a feature test in a mutex group', () => { const config = projectConfig.createProjectConfig(testDatafile.getMutexFeatureTestsConfig()); let result = projectConfig.isFeatureExperiment(config, '17128410791'); // id of 'f_test1' @@ -905,7 +906,7 @@ describe('isFeatureExperiment', () => { }); describe('getAudienceSegments', () => { - it('returns all qualified segments from an audience', () => { + it('should return all qualified segments from an audience', () => { const dummyQualifiedAudienceJson = { id: '13389142234', conditions: [ @@ -984,7 +985,7 @@ describe('integrations: with segments', () => { }); }); -describe('withoutSegments', () => { +describe('integrations: without segments', () => { let config: ProjectConfig; beforeEach(() => { config = projectConfig.createProjectConfig(testDatafile.getOdpIntegratedConfigWithoutSegments()); @@ -1006,3 +1007,128 @@ describe('withoutSegments', () => { expect(config.odpIntegrationConfig.odpConfig.segmentsToCheck).toEqual([]); }); }); + +describe('without valid integration key', () => { + it('should throw an error when parsing the project config due to integrations not containing a key', () => { + const odpIntegratedConfigWithoutKey = testDatafile.getOdpIntegratedConfigWithoutKey(); + + expect(() => projectConfig.createProjectConfig(odpIntegratedConfigWithoutKey)).toThrowError(OptimizelyError); + }); +}); + +describe('without integrations', () => { + let config: ProjectConfig; + + beforeEach(() => { + const odpIntegratedConfigWithSegments = testDatafile.getOdpIntegratedConfigWithSegments(); + const noIntegrationsConfigWithSegments = { ...odpIntegratedConfigWithSegments, integrations: [] }; + config = projectConfig.createProjectConfig(noIntegrationsConfigWithSegments); + }); + + it('should convert integrations from the datafile into the project config', () => { + expect(config.integrations.length).toBe(0); + }); + + it('should populate odpIntegrationConfig', () => { + expect(config.odpIntegrationConfig.integrated).toBe(false); + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + expect(config.odpIntegrationConfig.odpConfig).toBeUndefined(); + }); +}); + +describe('tryCreatingProjectConfig', () => { + let mockJsonSchemaValidator: Mock; + beforeEach(() => { + mockJsonSchemaValidator = vi.fn().mockReturnValue(true); + vi.spyOn(configValidator, 'validateDatafile').mockReturnValue(true); + vi.spyOn(logger, 'error'); + }); + + afterEach(() => { + vi.restoreAllMocks(); + }); + + it('returns a project config object created by createProjectConfig when all validation is applied and there are no errors', () => { + const configDatafile = { + foo: 'bar', + experiments: [{ key: 'a' }, { key: 'b' }], + }; + + vi.spyOn(configValidator, 'validateDatafile').mockReturnValueOnce(configDatafile); + + const configObj = { + foo: 'bar', + experimentKeyMap: { + a: { key: 'a', variationKeyMap: {} }, + b: { key: 'b', variationKeyMap: {} }, + }, + }; + + // stubJsonSchemaValidator.returns(true); + mockJsonSchemaValidator.mockReturnValueOnce(true); + + const result = projectConfig.tryCreatingProjectConfig({ + datafile: configDatafile, + jsonSchemaValidator: mockJsonSchemaValidator, + logger: logger, + }); + + expect(result).toMatchObject(configObj); + }); + + it('throws an error when validateDatafile throws', function() { + vi.spyOn(configValidator, 'validateDatafile').mockImplementationOnce(() => { + throw new Error(); + }); + mockJsonSchemaValidator.mockReturnValueOnce(true); + + expect(() => + projectConfig.tryCreatingProjectConfig({ + datafile: { foo: 'bar' }, + jsonSchemaValidator: mockJsonSchemaValidator, + logger: logger, + }) + ).toThrowError(); + }); + + it('throws an error when jsonSchemaValidator.validate throws', function() { + vi.spyOn(configValidator, 'validateDatafile').mockReturnValueOnce(true); + mockJsonSchemaValidator.mockImplementationOnce(() => { + throw new Error(); + }) + + expect(() => + projectConfig.tryCreatingProjectConfig({ + datafile: { foo: 'bar' }, + jsonSchemaValidator: mockJsonSchemaValidator, + logger: logger, + }) + ).toThrowError(); + }); + + it('skips json validation when jsonSchemaValidator is not provided', function() { + const configDatafile = { + foo: 'bar', + experiments: [{ key: 'a' }, { key: 'b' }], + }; + + vi.spyOn(configValidator, 'validateDatafile').mockReturnValueOnce(configDatafile); + + const configObj = { + foo: 'bar', + experimentKeyMap: { + a: { key: 'a', variationKeyMap: {} }, + b: { key: 'b', variationKeyMap: {} }, + }, + }; + + const result = projectConfig.tryCreatingProjectConfig({ + datafile: configDatafile, + logger: logger, + }); + + expect(result).toMatchObject(configObj); + expect(logger.error).not.toHaveBeenCalled(); + }); +}); diff --git a/lib/project_config/project_config.tests.js b/lib/project_config/project_config.tests.js deleted file mode 100644 index ff8e18624..000000000 --- a/lib/project_config/project_config.tests.js +++ /dev/null @@ -1,974 +0,0 @@ -/** - * Copyright 2016-2024, Optimizely - * - * 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 - * - * https://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. - */ -import sinon from 'sinon'; -import { assert } from 'chai'; -import { forEach, cloneDeep } from 'lodash'; -import { sprintf } from '../utils/fns'; -import fns from '../utils/fns'; -import projectConfig from './project_config'; -import { FEATURE_VARIABLE_TYPES, LOG_LEVEL } from '../utils/enums'; -import testDatafile from '../tests/test_data'; -import configValidator from '../utils/config_validator'; -import { - INVALID_EXPERIMENT_ID, - INVALID_EXPERIMENT_KEY, - UNEXPECTED_RESERVED_ATTRIBUTE_PREFIX, - UNRECOGNIZED_ATTRIBUTE, - VARIABLE_KEY_NOT_IN_DATAFILE, - FEATURE_NOT_IN_DATAFILE, - UNABLE_TO_CAST_VALUE -} from '../error_messages'; - -var createLogger = () => ({ - debug: () => {}, - info: () => {}, - warn: () => {}, - error: () => {}, - child: () => createLogger(), -}) - -var buildLogMessageFromArgs = args => sprintf(args[1], ...args.splice(2)); -var logger = createLogger(); - -describe('lib/core/project_config', function() { - describe('createProjectConfig method', function() { - it('should set properties correctly when createProjectConfig is called', function() { - var testData = testDatafile.getTestProjectConfig(); - var configObj = projectConfig.createProjectConfig(testData); - - forEach(testData.audiences, function(audience) { - audience.conditions = JSON.parse(audience.conditions); - }); - - assert.strictEqual(configObj.accountId, testData.accountId); - assert.strictEqual(configObj.projectId, testData.projectId); - assert.strictEqual(configObj.revision, testData.revision); - assert.deepEqual(configObj.events, testData.events); - assert.deepEqual(configObj.audiences, testData.audiences); - testData.groups.forEach(function(group) { - group.experiments.forEach(function(experiment) { - experiment.groupId = group.id; - experiment.variationKeyMap = fns.keyBy(experiment.variations, 'key'); - }); - }); - assert.deepEqual(configObj.groups, testData.groups); - - var expectedGroupIdMap = { - 666: testData.groups[0], - 667: testData.groups[1], - }; - - assert.deepEqual(configObj.groupIdMap, expectedGroupIdMap); - - var expectedExperiments = testData.experiments; - forEach(configObj.groupIdMap, function(group, Id) { - forEach(group.experiments, function(experiment) { - experiment.groupId = Id; - expectedExperiments.push(experiment); - }); - }); - - forEach(expectedExperiments, function(experiment) { - experiment.variationKeyMap = fns.keyBy(experiment.variations, 'key'); - }); - - assert.deepEqual(configObj.experiments, expectedExperiments); - - var expectedAttributeKeyMap = { - browser_type: testData.attributes[0], - boolean_key: testData.attributes[1], - integer_key: testData.attributes[2], - double_key: testData.attributes[3], - valid_positive_number: testData.attributes[4], - valid_negative_number: testData.attributes[5], - invalid_number: testData.attributes[6], - array: testData.attributes[7], - }; - - assert.deepEqual(configObj.attributeKeyMap, expectedAttributeKeyMap); - - var expectedExperimentKeyMap = { - testExperiment: configObj.experiments[0], - testExperimentWithAudiences: configObj.experiments[1], - testExperimentNotRunning: configObj.experiments[2], - testExperimentLaunched: configObj.experiments[3], - groupExperiment1: configObj.experiments[4], - groupExperiment2: configObj.experiments[5], - overlappingGroupExperiment1: configObj.experiments[6], - }; - - assert.deepEqual(configObj.experimentKeyMap, expectedExperimentKeyMap); - - var expectedEventKeyMap = { - testEvent: testData.events[0], - 'Total Revenue': testData.events[1], - testEventWithAudiences: testData.events[2], - testEventWithoutExperiments: testData.events[3], - testEventWithExperimentNotRunning: testData.events[4], - testEventWithMultipleExperiments: testData.events[5], - testEventLaunched: testData.events[6], - }; - - assert.deepEqual(configObj.eventKeyMap, expectedEventKeyMap); - - var expectedExperimentIdMap = { - '111127': configObj.experiments[0], - '122227': configObj.experiments[1], - '133337': configObj.experiments[2], - '144447': configObj.experiments[3], - '442': configObj.experiments[4], - '443': configObj.experiments[5], - '444': configObj.experiments[6], - }; - - assert.deepEqual(configObj.experimentIdMap, expectedExperimentIdMap); - - var expectedVariationKeyMap = {}; - expectedVariationKeyMap[testData.experiments[0].key + testData.experiments[0].variations[0].key] = - testData.experiments[0].variations[0]; - expectedVariationKeyMap[testData.experiments[0].key + testData.experiments[0].variations[1].key] = - testData.experiments[0].variations[1]; - expectedVariationKeyMap[testData.experiments[1].key + testData.experiments[1].variations[0].key] = - testData.experiments[1].variations[0]; - expectedVariationKeyMap[testData.experiments[1].key + testData.experiments[1].variations[1].key] = - testData.experiments[1].variations[1]; - expectedVariationKeyMap[testData.experiments[2].key + testData.experiments[2].variations[0].key] = - testData.experiments[2].variations[0]; - expectedVariationKeyMap[testData.experiments[2].key + testData.experiments[2].variations[1].key] = - testData.experiments[2].variations[1]; - expectedVariationKeyMap[configObj.experiments[3].key + configObj.experiments[3].variations[0].key] = - configObj.experiments[3].variations[0]; - expectedVariationKeyMap[configObj.experiments[3].key + configObj.experiments[3].variations[1].key] = - configObj.experiments[3].variations[1]; - expectedVariationKeyMap[configObj.experiments[4].key + configObj.experiments[4].variations[0].key] = - configObj.experiments[4].variations[0]; - expectedVariationKeyMap[configObj.experiments[4].key + configObj.experiments[4].variations[1].key] = - configObj.experiments[4].variations[1]; - expectedVariationKeyMap[configObj.experiments[5].key + configObj.experiments[5].variations[0].key] = - configObj.experiments[5].variations[0]; - expectedVariationKeyMap[configObj.experiments[5].key + configObj.experiments[5].variations[1].key] = - configObj.experiments[5].variations[1]; - expectedVariationKeyMap[configObj.experiments[6].key + configObj.experiments[6].variations[0].key] = - configObj.experiments[6].variations[0]; - expectedVariationKeyMap[configObj.experiments[6].key + configObj.experiments[6].variations[1].key] = - configObj.experiments[6].variations[1]; - - var expectedVariationIdMap = { - '111128': testData.experiments[0].variations[0], - '111129': testData.experiments[0].variations[1], - '122228': testData.experiments[1].variations[0], - '122229': testData.experiments[1].variations[1], - '133338': testData.experiments[2].variations[0], - '133339': testData.experiments[2].variations[1], - '144448': testData.experiments[3].variations[0], - '144449': testData.experiments[3].variations[1], - '551': configObj.experiments[4].variations[0], - '552': configObj.experiments[4].variations[1], - '661': configObj.experiments[5].variations[0], - '662': configObj.experiments[5].variations[1], - '553': configObj.experiments[6].variations[0], - '554': configObj.experiments[6].variations[1], - }; - }); - - it('should not mutate the datafile', function() { - var datafile = testDatafile.getTypedAudiencesConfig(); - var datafileClone = cloneDeep(datafile); - projectConfig.createProjectConfig(datafile); - assert.deepEqual(datafileClone, datafile); - }); - - describe('feature management', function() { - var configObj; - beforeEach(function() { - configObj = projectConfig.createProjectConfig(testDatafile.getTestProjectConfigWithFeatures()); - }); - - it('creates a rolloutIdMap from rollouts in the datafile', function() { - assert.deepEqual(configObj.rolloutIdMap, testDatafile.datafileWithFeaturesExpectedData.rolloutIdMap); - }); - - it('creates a variationVariableUsageMap from rollouts and experiments with features in the datafile', function() { - assert.deepEqual( - configObj.variationVariableUsageMap, - testDatafile.datafileWithFeaturesExpectedData.variationVariableUsageMap - ); - }); - - it('creates a featureKeyMap from feature flags in the datafile', function() { - assert.deepEqual(configObj.featureKeyMap, testDatafile.datafileWithFeaturesExpectedData.featureKeyMap); - }); - - it('adds variations from rollout experiments to variationIdMap', function() { - assert.deepEqual(configObj.variationIdMap['594032'], { - variables: [ - { value: 'true', id: '4919852825313280' }, - { value: '395', id: '5482802778734592' }, - { value: '4.99', id: '6045752732155904' }, - { value: 'Hello audience', id: '6327227708866560' }, - { value: '{ "count": 2, "message": "Hello audience" }', id: '8765345281230956' }, - ], - featureEnabled: true, - key: '594032', - id: '594032', - }); - assert.deepEqual(configObj.variationIdMap['594038'], { - variables: [ - { value: 'false', id: '4919852825313280' }, - { value: '400', id: '5482802778734592' }, - { value: '14.99', id: '6045752732155904' }, - { value: 'Hello', id: '6327227708866560' }, - { value: '{ "count": 1, "message": "Hello" }', id: '8765345281230956' }, - ], - featureEnabled: false, - key: '594038', - id: '594038', - }); - assert.deepEqual(configObj.variationIdMap['594061'], { - variables: [ - { value: '27.34', id: '5060590313668608' }, - { value: 'Winter is NOT coming', id: '5342065290379264' }, - { value: '10003', id: '6186490220511232' }, - { value: 'false', id: '6467965197221888' }, - ], - featureEnabled: true, - key: '594061', - id: '594061', - }); - assert.deepEqual(configObj.variationIdMap['594067'], { - variables: [ - { value: '30.34', id: '5060590313668608' }, - { value: 'Winter is coming definitely', id: '5342065290379264' }, - { value: '500', id: '6186490220511232' }, - { value: 'true', id: '6467965197221888' }, - ], - featureEnabled: true, - key: '594067', - id: '594067', - }); - }); - }); - - describe('flag variations', function() { - var configObj; - beforeEach(function() { - configObj = projectConfig.createProjectConfig(testDatafile.getTestDecideProjectConfig()); - }); - - it('it should populate flagVariationsMap correctly', function() { - var allVariationsForFlag = configObj.flagVariationsMap; - var feature1Variations = allVariationsForFlag.feature_1; - var feature2Variations = allVariationsForFlag.feature_2; - var feature3Variations = allVariationsForFlag.feature_3; - var feature1VariationsKeys = feature1Variations.map(variation => { - return variation.key; - }, {}); - var feature2VariationsKeys = feature2Variations.map(variation => { - return variation.key; - }, {}); - var feature3VariationsKeys = feature3Variations.map(variation => { - return variation.key; - }, {}); - - assert.deepEqual(feature1VariationsKeys, ['a', 'b', '3324490633', '3324490562', '18257766532']); - assert.deepEqual(feature2VariationsKeys, ['variation_with_traffic', 'variation_no_traffic']); - assert.deepEqual(feature3VariationsKeys, []); - }); - }); - }); - - describe('projectConfig helper methods', function() { - var testData = cloneDeep(testDatafile.getTestProjectConfig()); - var configObj; - var createdLogger = createLogger({ logLevel: LOG_LEVEL.INFO }); - - beforeEach(function() { - configObj = projectConfig.createProjectConfig(cloneDeep(testData)); - sinon.stub(createdLogger, 'warn'); - }); - - afterEach(function() { - createdLogger.warn.restore(); - }); - - it('should retrieve experiment ID for valid experiment key in getExperimentId', function() { - assert.strictEqual( - projectConfig.getExperimentId(configObj, testData.experiments[0].key), - testData.experiments[0].id - ); - }); - - it('should throw error for invalid experiment key in getExperimentId', function() { - const ex = assert.throws(function() { - projectConfig.getExperimentId(configObj, 'invalidExperimentKey'); - }); - assert.equal(ex.baseMessage, INVALID_EXPERIMENT_KEY); - assert.deepEqual(ex.params, ['invalidExperimentKey']); - }); - - it('should retrieve layer ID for valid experiment key in getLayerId', function() { - assert.strictEqual(projectConfig.getLayerId(configObj, '111127'), '4'); - }); - - it('should throw error for invalid experiment key in getLayerId', function() { - const ex = assert.throws(function() { - projectConfig.getLayerId(configObj, 'invalidExperimentKey'); - }); - assert.equal(ex.baseMessage, INVALID_EXPERIMENT_ID); - assert.deepEqual(ex.params, ['invalidExperimentKey']); - }); - - it('should retrieve attribute ID for valid attribute key in getAttributeId', function() { - assert.strictEqual(projectConfig.getAttributeId(configObj, 'browser_type'), '111094'); - }); - - it('should retrieve attribute ID for reserved attribute key in getAttributeId', function() { - assert.strictEqual(projectConfig.getAttributeId(configObj, '$opt_user_agent'), '$opt_user_agent'); - }); - - it('should return null for invalid attribute key in getAttributeId', function() { - assert.isNull(projectConfig.getAttributeId(configObj, 'invalidAttributeKey', createdLogger)); - - assert.deepEqual(createdLogger.warn.lastCall.args, [UNRECOGNIZED_ATTRIBUTE, 'invalidAttributeKey']); - }); - - it('should return null for invalid attribute key in getAttributeId', function() { - // Adding attribute in key map with reserved prefix - configObj.attributeKeyMap['$opt_some_reserved_attribute'] = { - id: '42', - key: '$opt_some_reserved_attribute', - }; - assert.strictEqual(projectConfig.getAttributeId(configObj, '$opt_some_reserved_attribute', createdLogger), '42'); - - assert.deepEqual(createdLogger.warn.lastCall.args, [UNEXPECTED_RESERVED_ATTRIBUTE_PREFIX, '$opt_some_reserved_attribute', '$opt_']); - }); - - it('should retrieve event ID for valid event key in getEventId', function() { - assert.strictEqual(projectConfig.getEventId(configObj, 'testEvent'), '111095'); - }); - - it('should return null for invalid event key in getEventId', function() { - assert.isNull(projectConfig.getEventId(configObj, 'invalidEventKey')); - }); - - it('should retrieve experiment status for valid experiment key in getExperimentStatus', function() { - assert.strictEqual( - projectConfig.getExperimentStatus(configObj, testData.experiments[0].key), - testData.experiments[0].status - ); - }); - - it('should throw error for invalid experiment key in getExperimentStatus', function() { - const ex = assert.throws(function() { - projectConfig.getExperimentStatus(configObj, 'invalidExperimentKey'); - }); - assert.equal(ex.baseMessage, INVALID_EXPERIMENT_KEY); - assert.deepEqual(ex.params, ['invalidExperimentKey']); - }); - - it('should return true if experiment status is set to Running in isActive', function() { - assert.isTrue(projectConfig.isActive(configObj, 'testExperiment')); - }); - - it('should return false if experiment status is not set to Running in isActive', function() { - assert.isFalse(projectConfig.isActive(configObj, 'testExperimentNotRunning')); - }); - - it('should return true if experiment status is set to Running in isRunning', function() { - assert.isTrue(projectConfig.isRunning(configObj, 'testExperiment')); - }); - - it('should return false if experiment status is not set to Running in isRunning', function() { - assert.isFalse(projectConfig.isRunning(configObj, 'testExperimentLaunched')); - }); - - it('should retrieve variation key for valid experiment key and variation ID in getVariationKeyFromId', function() { - assert.deepEqual( - projectConfig.getVariationKeyFromId(configObj, testData.experiments[0].variations[0].id), - testData.experiments[0].variations[0].key - ); - }); - - it('should retrieve traffic allocation given valid experiment key in getTrafficAllocation', function() { - assert.deepEqual( - projectConfig.getTrafficAllocation(configObj, testData.experiments[0].id), - testData.experiments[0].trafficAllocation - ); - }); - - it('should throw error for invalid experient key in getTrafficAllocation', function() { - const ex = assert.throws(function() { - projectConfig.getTrafficAllocation(configObj, 'invalidExperimentId'); - }); - assert.equal(ex.baseMessage, INVALID_EXPERIMENT_ID); - assert.deepEqual(ex.params, ['invalidExperimentId']); - }); - - describe('#getVariationIdFromExperimentAndVariationKey', function() { - it('should return the variation id for the given experiment key and variation key', function() { - assert.strictEqual( - projectConfig.getVariationIdFromExperimentAndVariationKey( - configObj, - testData.experiments[0].key, - testData.experiments[0].variations[0].key - ), - testData.experiments[0].variations[0].id - ); - }); - }); - - describe('#getSendFlagDecisionsValue', function() { - it('should return false when sendFlagDecisions is undefined', function() { - configObj.sendFlagDecisions = undefined; - assert.deepEqual(projectConfig.getSendFlagDecisionsValue(configObj), false); - }); - - it('should return false when sendFlagDecisions is set to false', function() { - configObj.sendFlagDecisions = false; - assert.deepEqual(projectConfig.getSendFlagDecisionsValue(configObj), false); - }); - - it('should return true when sendFlagDecisions is set to true', function() { - configObj.sendFlagDecisions = true; - assert.deepEqual(projectConfig.getSendFlagDecisionsValue(configObj), true); - }); - }); - - describe('feature management', function() { - var featureManagementLogger = createLogger({ logLevel: LOG_LEVEL.INFO }); - beforeEach(function() { - configObj = projectConfig.createProjectConfig(testDatafile.getTestProjectConfigWithFeatures()); - sinon.stub(featureManagementLogger, 'warn'); - sinon.stub(featureManagementLogger, 'error'); - sinon.stub(featureManagementLogger, 'info'); - sinon.stub(featureManagementLogger, 'debug'); - }); - - afterEach(function() { - featureManagementLogger.warn.restore(); - featureManagementLogger.error.restore(); - featureManagementLogger.info.restore(); - featureManagementLogger.debug.restore(); - }); - - describe('getVariableForFeature', function() { - it('should return a variable object for a valid variable and feature key', function() { - var featureKey = 'test_feature_for_experiment'; - var variableKey = 'num_buttons'; - var result = projectConfig.getVariableForFeature(configObj, featureKey, variableKey, featureManagementLogger); - assert.deepEqual(result, { - type: 'integer', - key: 'num_buttons', - id: '4792309476491264', - defaultValue: '10', - }); - }); - - it('should return null for an invalid variable key and a valid feature key', function() { - var featureKey = 'test_feature_for_experiment'; - var variableKey = 'notARealVariable____'; - var result = projectConfig.getVariableForFeature(configObj, featureKey, variableKey, featureManagementLogger); - assert.strictEqual(result, null); - sinon.assert.calledOnce(featureManagementLogger.error); - - assert.deepEqual(featureManagementLogger.error.lastCall.args, [VARIABLE_KEY_NOT_IN_DATAFILE, 'notARealVariable____', 'test_feature_for_experiment']); - }); - - it('should return null for an invalid feature key', function() { - var featureKey = 'notARealFeature_____'; - var variableKey = 'num_buttons'; - var result = projectConfig.getVariableForFeature(configObj, featureKey, variableKey, featureManagementLogger); - assert.strictEqual(result, null); - sinon.assert.calledOnce(featureManagementLogger.error); - - assert.deepEqual(featureManagementLogger.error.lastCall.args, [FEATURE_NOT_IN_DATAFILE, 'notARealFeature_____']); - }); - - it('should return null for an invalid variable key and an invalid feature key', function() { - var featureKey = 'notARealFeature_____'; - var variableKey = 'notARealVariable____'; - var result = projectConfig.getVariableForFeature(configObj, featureKey, variableKey, featureManagementLogger); - assert.strictEqual(result, null); - sinon.assert.calledOnce(featureManagementLogger.error); - - assert.deepEqual(featureManagementLogger.error.lastCall.args, [FEATURE_NOT_IN_DATAFILE, 'notARealFeature_____']); - }); - }); - - describe('getVariableValueForVariation', function() { - it('returns a value for a valid variation and variable', function() { - var variation = configObj.variationIdMap['594096']; - var variable = configObj.featureKeyMap.test_feature_for_experiment.variableKeyMap.num_buttons; - var result = projectConfig.getVariableValueForVariation( - configObj, - variable, - variation, - featureManagementLogger - ); - assert.strictEqual(result, '2'); - - variable = configObj.featureKeyMap.test_feature_for_experiment.variableKeyMap.is_button_animated; - result = projectConfig.getVariableValueForVariation(configObj, variable, variation, featureManagementLogger); - assert.strictEqual(result, 'true'); - - variable = configObj.featureKeyMap.test_feature_for_experiment.variableKeyMap.button_txt; - result = projectConfig.getVariableValueForVariation(configObj, variable, variation, featureManagementLogger); - assert.strictEqual(result, 'Buy me NOW'); - - variable = configObj.featureKeyMap.test_feature_for_experiment.variableKeyMap.button_width; - result = projectConfig.getVariableValueForVariation(configObj, variable, variation, featureManagementLogger); - assert.strictEqual(result, '20.25'); - }); - - it('returns null for a null variation', function() { - var variation = null; - var variable = configObj.featureKeyMap.test_feature_for_experiment.variableKeyMap.num_buttons; - var result = projectConfig.getVariableValueForVariation( - configObj, - variable, - variation, - featureManagementLogger - ); - assert.strictEqual(result, null); - }); - - it('returns null for a null variable', function() { - var variation = configObj.variationIdMap['594096']; - var variable = null; - var result = projectConfig.getVariableValueForVariation( - configObj, - variable, - variation, - featureManagementLogger - ); - assert.strictEqual(result, null); - }); - - it('returns null for a null variation and null variable', function() { - var variation = null; - var variable = null; - var result = projectConfig.getVariableValueForVariation( - configObj, - variable, - variation, - featureManagementLogger - ); - assert.strictEqual(result, null); - }); - - it('returns null for a variation whose id is not in the datafile', function() { - var variation = { - key: 'some_variation', - id: '999999999999', - variables: [], - }; - var variable = configObj.featureKeyMap.test_feature_for_experiment.variableKeyMap.num_buttons; - var result = projectConfig.getVariableValueForVariation( - configObj, - variable, - variation, - featureManagementLogger - ); - assert.strictEqual(result, null); - }); - - it('returns null if the variation does not have a value for this variable', function() { - var variation = configObj.variationIdMap['595008']; // This variation has no variable values associated with it - var variable = configObj.featureKeyMap.test_feature_for_experiment.variableKeyMap.num_buttons; - var result = projectConfig.getVariableValueForVariation( - configObj, - variable, - variation, - featureManagementLogger - ); - assert.isNull(result); - }); - }); - - describe('getTypeCastValue', function() { - it('can cast a boolean', function() { - var result = projectConfig.getTypeCastValue('true', FEATURE_VARIABLE_TYPES.BOOLEAN, featureManagementLogger); - assert.strictEqual(result, true); - result = projectConfig.getTypeCastValue('false', FEATURE_VARIABLE_TYPES.BOOLEAN, featureManagementLogger); - assert.strictEqual(result, false); - }); - - it('can cast an integer', function() { - var result = projectConfig.getTypeCastValue('50', FEATURE_VARIABLE_TYPES.INTEGER, featureManagementLogger); - assert.strictEqual(result, 50); - var result = projectConfig.getTypeCastValue('-7', FEATURE_VARIABLE_TYPES.INTEGER, featureManagementLogger); - assert.strictEqual(result, -7); - var result = projectConfig.getTypeCastValue('0', FEATURE_VARIABLE_TYPES.INTEGER, featureManagementLogger); - assert.strictEqual(result, 0); - }); - - it('can cast a double', function() { - var result = projectConfig.getTypeCastValue('89.99', FEATURE_VARIABLE_TYPES.DOUBLE, featureManagementLogger); - assert.strictEqual(result, 89.99); - var result = projectConfig.getTypeCastValue( - '-257.21', - FEATURE_VARIABLE_TYPES.DOUBLE, - featureManagementLogger - ); - assert.strictEqual(result, -257.21); - var result = projectConfig.getTypeCastValue('0', FEATURE_VARIABLE_TYPES.DOUBLE, featureManagementLogger); - assert.strictEqual(result, 0); - var result = projectConfig.getTypeCastValue('10', FEATURE_VARIABLE_TYPES.DOUBLE, featureManagementLogger); - assert.strictEqual(result, 10); - }); - - it('can return a string unmodified', function() { - var result = projectConfig.getTypeCastValue( - 'message', - FEATURE_VARIABLE_TYPES.STRING, - featureManagementLogger - ); - assert.strictEqual(result, 'message'); - }); - - it('returns null and logs an error for an invalid boolean', function() { - var result = projectConfig.getTypeCastValue( - 'notabool', - FEATURE_VARIABLE_TYPES.BOOLEAN, - featureManagementLogger - ); - assert.strictEqual(result, null); - - assert.deepEqual(featureManagementLogger.error.lastCall.args, [UNABLE_TO_CAST_VALUE, 'notabool', 'boolean']); - }); - - it('returns null and logs an error for an invalid integer', function() { - var result = projectConfig.getTypeCastValue( - 'notanint', - FEATURE_VARIABLE_TYPES.INTEGER, - featureManagementLogger - ); - assert.strictEqual(result, null); - - assert.deepEqual(featureManagementLogger.error.lastCall.args, [UNABLE_TO_CAST_VALUE, 'notanint', 'integer']); - }); - - it('returns null and logs an error for an invalid double', function() { - var result = projectConfig.getTypeCastValue( - 'notadouble', - FEATURE_VARIABLE_TYPES.DOUBLE, - featureManagementLogger - ); - assert.strictEqual(result, null); - - assert.deepEqual(featureManagementLogger.error.lastCall.args, [UNABLE_TO_CAST_VALUE, 'notadouble', 'double']); - }); - }); - }); - - describe('#getAudiencesById', function() { - beforeEach(function() { - configObj = projectConfig.createProjectConfig(testDatafile.getTypedAudiencesConfig()); - }); - - it('should retrieve audiences by checking first in typedAudiences, and then second in audiences', function() { - assert.deepEqual(projectConfig.getAudiencesById(configObj), testDatafile.typedAudiencesById); - }); - }); - - describe('#getExperimentAudienceConditions', function() { - it('should retrieve audiences for valid experiment key', function() { - configObj = projectConfig.createProjectConfig(cloneDeep(testData)); - assert.deepEqual(projectConfig.getExperimentAudienceConditions(configObj, testData.experiments[1].id), [ - '11154', - ]); - }); - - it('should throw error for invalid experiment key', function() { - configObj = projectConfig.createProjectConfig(cloneDeep(testData)); - const ex = assert.throws(function() { - projectConfig.getExperimentAudienceConditions(configObj, 'invalidExperimentId'); - }); - assert.equal(ex.baseMessage, INVALID_EXPERIMENT_ID); - assert.deepEqual(ex.params, ['invalidExperimentId']); - }); - - it('should return experiment audienceIds if experiment has no audienceConditions', function() { - configObj = projectConfig.createProjectConfig(testDatafile.getTypedAudiencesConfig()); - var result = projectConfig.getExperimentAudienceConditions(configObj, '11564051718'); - assert.deepEqual(result, [ - '3468206642', - '3988293898', - '3988293899', - '3468206646', - '3468206647', - '3468206644', - '3468206643', - ]); - }); - - it('should return experiment audienceConditions if experiment has audienceConditions', function() { - configObj = projectConfig.createProjectConfig(testDatafile.getTypedAudiencesConfig()); - // audience_combinations_experiment has both audienceConditions and audienceIds - // audienceConditions should be preferred over audienceIds - var result = projectConfig.getExperimentAudienceConditions(configObj, '1323241598'); - assert.deepEqual(result, [ - 'and', - ['or', '3468206642', '3988293898'], - ['or', '3988293899', '3468206646', '3468206647', '3468206644', '3468206643'], - ]); - }); - }); - - describe('#isFeatureExperiment', function() { - it('returns true for a feature test', function() { - var config = projectConfig.createProjectConfig(testDatafile.getTestProjectConfigWithFeatures()); - var result = projectConfig.isFeatureExperiment(config, '594098'); // id of 'testing_my_feature' - assert.isTrue(result); - }); - - it('returns false for an A/B test', function() { - var config = projectConfig.createProjectConfig(testDatafile.getTestProjectConfig()); - var result = projectConfig.isFeatureExperiment(config, '111127'); // id of 'testExperiment' - assert.isFalse(result); - }); - - it('returns true for a feature test in a mutex group', function() { - var config = projectConfig.createProjectConfig(testDatafile.getMutexFeatureTestsConfig()); - var result = projectConfig.isFeatureExperiment(config, '17128410791'); // id of 'f_test1' - assert.isTrue(result); - result = projectConfig.isFeatureExperiment(config, '17139931304'); // id of 'f_test2' - assert.isTrue(result); - }); - }); - - describe('#getAudienceSegments', function() { - it('returns all qualified segments from an audience', function() { - const dummyQualifiedAudienceJson = { - id: '13389142234', - conditions: [ - 'and', - [ - 'or', - [ - 'or', - { - value: 'odp-segment-1', - type: 'third_party_dimension', - name: 'odp.audiences', - match: 'qualified', - }, - ], - ], - ], - name: 'odp-segment-1', - }; - - const dummyQualifiedAudienceJsonSegments = projectConfig.getAudienceSegments(dummyQualifiedAudienceJson); - assert.deepEqual(dummyQualifiedAudienceJsonSegments, ['odp-segment-1']); - - const dummyUnqualifiedAudienceJson = { - id: '13389142234', - conditions: [ - 'and', - [ - 'or', - [ - 'or', - { - value: 'odp-segment-1', - type: 'third_party_dimension', - name: 'odp.audiences', - match: 'invalid', - }, - ], - ], - ], - name: 'odp-segment-1', - }; - - const dummyUnqualifiedAudienceJsonSegments = projectConfig.getAudienceSegments(dummyUnqualifiedAudienceJson); - assert.deepEqual(dummyUnqualifiedAudienceJsonSegments, []); - }); - - it('returns false for an A/B test', function() { - var config = projectConfig.createProjectConfig(testDatafile.getTestProjectConfig()); - var result = projectConfig.isFeatureExperiment(config, '111127'); // id of 'testExperiment' - assert.isFalse(result); - }); - - it('returns true for a feature test in a mutex group', function() { - var config = projectConfig.createProjectConfig(testDatafile.getMutexFeatureTestsConfig()); - var result = projectConfig.isFeatureExperiment(config, '17128410791'); // id of 'f_test1' - assert.isTrue(result); - result = projectConfig.isFeatureExperiment(config, '17139931304'); // id of 'f_test2' - assert.isTrue(result); - }); - }); - }); - - describe('integrations', () => { - describe('#withSegments', () => { - var config; - beforeEach(() => { - config = projectConfig.createProjectConfig(testDatafile.getOdpIntegratedConfigWithSegments()); - }); - - it('should convert integrations from the datafile into the project config', () => { - assert.exists(config.integrations); - assert.equal(config.integrations.length, 4); - }); - - it('should populate odpIntegrationConfig', () => { - assert.isTrue(config.odpIntegrationConfig.integrated); - assert.equal(config.odpIntegrationConfig.odpConfig.apiKey, 'W4WzcEs-ABgXorzY7h1LCQ'); - assert.equal(config.odpIntegrationConfig.odpConfig.apiHost, 'https://api.zaius.com'); - assert.equal(config.odpIntegrationConfig.odpConfig.pixelUrl, 'https://jumbe.zaius.com'); - assert.deepEqual(config.odpIntegrationConfig.odpConfig.segmentsToCheck, ['odp-segment-1', 'odp-segment-2', 'odp-segment-3']); - }); - }); - - describe('#withoutSegments', () => { - var config; - beforeEach(() => { - config = projectConfig.createProjectConfig(testDatafile.getOdpIntegratedConfigWithoutSegments()); - }); - - it('should convert integrations from the datafile into the project config', () => { - assert.exists(config.integrations); - assert.equal(config.integrations.length, 3); - }); - - it('should populate odpIntegrationConfig', () => { - assert.isTrue(config.odpIntegrationConfig.integrated); - assert.equal(config.odpIntegrationConfig.odpConfig.apiKey, 'W4WzcEs-ABgXorzY7h1LCQ'); - assert.equal(config.odpIntegrationConfig.odpConfig.apiHost, 'https://api.zaius.com'); - assert.equal(config.odpIntegrationConfig.odpConfig.pixelUrl, 'https://jumbe.zaius.com'); - assert.deepEqual(config.odpIntegrationConfig.odpConfig.segmentsToCheck, []); - }); - }); - - describe('#withoutValidIntegrationKey', () => { - it('should throw an error when parsing the project config due to integrations not containing a key', () => { - const odpIntegratedConfigWithoutKey = testDatafile.getOdpIntegratedConfigWithoutKey(); - assert.throws(() => { - projectConfig.createProjectConfig(odpIntegratedConfigWithoutKey); - }); - }); - }); - - describe('#withoutIntegrations', () => { - var config; - beforeEach(() => { - const odpIntegratedConfigWithSegments = testDatafile.getOdpIntegratedConfigWithSegments(); - const noIntegrationsConfigWithSegments = { ...odpIntegratedConfigWithSegments, integrations: [] }; - config = projectConfig.createProjectConfig(noIntegrationsConfigWithSegments); - }); - - it('should convert integrations from the datafile into the project config', () => { - assert.equal(config.integrations.length, 0); - }); - - it('should populate odpIntegrationConfig', () => { - assert.isFalse(config.odpIntegrationConfig.integrated); - assert.isUndefined(config.odpIntegrationConfig.odpConfig); - }); - }); - }); -}); - -describe('#tryCreatingProjectConfig', function() { - var stubJsonSchemaValidator; - beforeEach(function() { - stubJsonSchemaValidator = sinon.stub().returns(true); - sinon.stub(configValidator, 'validateDatafile').returns(true); - sinon.spy(logger, 'error'); - }); - - afterEach(function() { - configValidator.validateDatafile.restore(); - logger.error.restore(); - }); - - it('returns a project config object created by createProjectConfig when all validation is applied and there are no errors', function() { - var configDatafile = { - foo: 'bar', - experiments: [{ key: 'a' }, { key: 'b' }], - }; - configValidator.validateDatafile.returns(configDatafile); - var configObj = { - foo: 'bar', - experimentKeyMap: { - a: { key: 'a', variationKeyMap: {} }, - b: { key: 'b', variationKeyMap: {} }, - }, - }; - - stubJsonSchemaValidator.returns(true); - - var result = projectConfig.tryCreatingProjectConfig({ - datafile: configDatafile, - jsonSchemaValidator: stubJsonSchemaValidator, - logger: logger, - }); - - assert.deepInclude(result, configObj); - }); - - it('throws an error when validateDatafile throws', function() { - configValidator.validateDatafile.throws(); - stubJsonSchemaValidator.returns(true); - assert.throws(() => { - projectConfig.tryCreatingProjectConfig({ - datafile: { foo: 'bar' }, - jsonSchemaValidator: stubJsonSchemaValidator, - logger: logger, - }); - }); - }); - - it('throws an error when jsonSchemaValidator.validate throws', function() { - configValidator.validateDatafile.returns(true); - stubJsonSchemaValidator.throws(); - assert.throws(() => { - projectConfig.tryCreatingProjectConfig({ - datafile: { foo: 'bar' }, - jsonSchemaValidator: stubJsonSchemaValidator, - logger: logger, - }); - }); - }); - - it('skips json validation when jsonSchemaValidator is not provided', function() { - var configDatafile = { - foo: 'bar', - experiments: [{ key: 'a' }, { key: 'b' }], - }; - - configValidator.validateDatafile.returns(configDatafile); - - var configObj = { - foo: 'bar', - experimentKeyMap: { - a: { key: 'a', variationKeyMap: {} }, - b: { key: 'b', variationKeyMap: {} }, - }, - }; - - var result = projectConfig.tryCreatingProjectConfig({ - datafile: configDatafile, - logger: logger, - }); - - assert.deepInclude(result, configObj); - sinon.assert.notCalled(logger.error); - }); -}); diff --git a/package.json b/package.json index 367d40125..170ddc291 100644 --- a/package.json +++ b/package.json @@ -74,7 +74,7 @@ "clean:win": "(if exist dist rd /s/q dist)", "lint": "tsc --noEmit && eslint 'lib/**/*.js' 'lib/**/*.ts'", "test-vitest": "tsc --noEmit --p tsconfig.spec.json && vitest run", - "test-mocha": "TS_NODE_COMPILER_OPTIONS='{\"module\": \"commonjs\" }' mocha -r ts-node/register -r lib/tests/exit_on_unhandled_rejection.js 'lib/**/*.tests.ts' 'lib/**/*.tests.js'", + "test-mocha": "TS_NODE_COMPILER_OPTIONS='{\"module\": \"commonjs\" }' mocha -r ts-node/register -r lib/tests/exit_on_unhandled_rejection.js 'lib/**/*.tests.js'", "test": "npm run test-mocha && npm run test-vitest", "posttest": "npm run lint", "test-ci": "npm run test-xbrowser && npm run test-umdbrowser", From 1c19944457b276a570b7e847feaa8f4f3458e245 Mon Sep 17 00:00:00 2001 From: Md Junaed Hossain <169046794+junaed-optimizely@users.noreply.github.com> Date: Tue, 28 Jan 2025 23:27:52 +0600 Subject: [PATCH 4/9] [FSSDK-11071] minor update --- lib/project_config/project_config.spec.ts | 22 ++++++++++------------ package-lock.json | 7 +++++++ package.json | 1 + 3 files changed, 18 insertions(+), 12 deletions(-) diff --git a/lib/project_config/project_config.spec.ts b/lib/project_config/project_config.spec.ts index 8ebcbae84..73ca7c054 100644 --- a/lib/project_config/project_config.spec.ts +++ b/lib/project_config/project_config.spec.ts @@ -29,11 +29,9 @@ import { VARIABLE_KEY_NOT_IN_DATAFILE, FEATURE_NOT_IN_DATAFILE, UNABLE_TO_CAST_VALUE, -} from '../error_messages'; -import exp from 'constants'; +} from 'error_message'; import { VariableType } from '../shared_types'; import { OptimizelyError } from '../error/optimizly_error'; -import { J } from 'vitest/dist/chunks/environment.0M5R1SX_.js'; const createLogger = (...args: any) => ({ debug: () => {}, @@ -53,7 +51,7 @@ describe('createProjectConfig', () => { const testData: Record = testDatafile.getTestProjectConfig(); configObj = projectConfig.createProjectConfig(testData as JSON); - forEach(testData.audiences, audience => { + forEach(testData.audiences, (audience: any) => { audience.conditions = JSON.parse(audience.conditions); }); @@ -80,14 +78,14 @@ describe('createProjectConfig', () => { expect(configObj.groupIdMap).toEqual(expectedGroupIdMap); const expectedExperiments = testData.experiments.slice(); - forEach(configObj.groupIdMap, (group, groupId) => { - forEach(group.experiments, experiment => { + forEach(configObj.groupIdMap, (group: any, groupId: any) => { + forEach(group.experiments, (experiment: any) => { experiment.groupId = groupId; expectedExperiments.push(experiment); }); }); - forEach(expectedExperiments, experiment => { + forEach(expectedExperiments, (experiment: any) => { experiment.variationKeyMap = fns.keyBy(experiment.variations, 'key'); }); @@ -1049,7 +1047,7 @@ describe('tryCreatingProjectConfig', () => { vi.restoreAllMocks(); }); - it('returns a project config object created by createProjectConfig when all validation is applied and there are no errors', () => { + it('should return a project config object created by createProjectConfig when all validation is applied and there are no errors', () => { const configDatafile = { foo: 'bar', experiments: [{ key: 'a' }, { key: 'b' }], @@ -1077,7 +1075,7 @@ describe('tryCreatingProjectConfig', () => { expect(result).toMatchObject(configObj); }); - it('throws an error when validateDatafile throws', function() { + it('should throw an error when validateDatafile throws', function() { vi.spyOn(configValidator, 'validateDatafile').mockImplementationOnce(() => { throw new Error(); }); @@ -1092,11 +1090,11 @@ describe('tryCreatingProjectConfig', () => { ).toThrowError(); }); - it('throws an error when jsonSchemaValidator.validate throws', function() { + it('should throw an error when jsonSchemaValidator.validate throws', function() { vi.spyOn(configValidator, 'validateDatafile').mockReturnValueOnce(true); mockJsonSchemaValidator.mockImplementationOnce(() => { throw new Error(); - }) + }); expect(() => projectConfig.tryCreatingProjectConfig({ @@ -1107,7 +1105,7 @@ describe('tryCreatingProjectConfig', () => { ).toThrowError(); }); - it('skips json validation when jsonSchemaValidator is not provided', function() { + it('should skip json validation when jsonSchemaValidator is not provided', function() { const configDatafile = { foo: 'bar', experiments: [{ key: 'a' }, { key: 'b' }], diff --git a/package-lock.json b/package-lock.json index 4cfbad348..c3c12d2d3 100644 --- a/package-lock.json +++ b/package-lock.json @@ -21,6 +21,7 @@ "@rollup/plugin-commonjs": "^11.0.2", "@rollup/plugin-node-resolve": "^7.1.1", "@types/chai": "^4.2.11", + "@types/lodash": "^4.17.15", "@types/mocha": "^5.2.7", "@types/nise": "^1.4.0", "@types/node": "^18.7.18", @@ -5154,6 +5155,12 @@ "integrity": "sha512-RbSSoHliUbnXj3ny0CNFOoxrIDV6SUGyStHsvDqosw6CkdPV8TtWGlfecuK4ToyMEAql6pzNxgCFKanovUzlgQ==", "dev": true }, + "node_modules/@types/lodash": { + "version": "4.17.15", + "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.17.15.tgz", + "integrity": "sha512-w/P33JFeySuhN6JLkysYUK2gEmy9kHHFN7E8ro0tkfmlDOgxBDzWEZ/J8cWA+fHqFevpswDTFZnDx+R9lbL6xw==", + "dev": true + }, "node_modules/@types/mocha": { "version": "5.2.7", "resolved": "https://registry.npmjs.org/@types/mocha/-/mocha-5.2.7.tgz", diff --git a/package.json b/package.json index 2d97998df..8797f4d92 100644 --- a/package.json +++ b/package.json @@ -119,6 +119,7 @@ "@rollup/plugin-commonjs": "^11.0.2", "@rollup/plugin-node-resolve": "^7.1.1", "@types/chai": "^4.2.11", + "@types/lodash": "^4.17.15", "@types/mocha": "^5.2.7", "@types/nise": "^1.4.0", "@types/node": "^18.7.18", From 5dadd7fff71ea73fee50697a8236b3f0699440a8 Mon Sep 17 00:00:00 2001 From: Md Junaed Hossain <169046794+junaed-optimizely@users.noreply.github.com> Date: Thu, 6 Feb 2025 23:26:23 +0600 Subject: [PATCH 5/9] [FSSDK-11034] optimizely config test addition --- lib/project_config/optimizely_config.spec.ts | 948 +++++++++++++++++++ 1 file changed, 948 insertions(+) create mode 100644 lib/project_config/optimizely_config.spec.ts diff --git a/lib/project_config/optimizely_config.spec.ts b/lib/project_config/optimizely_config.spec.ts new file mode 100644 index 000000000..c9c3e3c17 --- /dev/null +++ b/lib/project_config/optimizely_config.spec.ts @@ -0,0 +1,948 @@ +/** + * Copyright 2024, Optimizely + * + * 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 + * + * https://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. + */ +import { describe, it, expect, beforeEach, afterEach, vi, assert, Mock } from 'vitest'; +import { createOptimizelyConfig, OptimizelyConfig } from './optimizely_config'; +import { createProjectConfig, ProjectConfig } from './project_config'; +import { + getTestProjectConfigWithFeatures, + getTypedAudiencesConfig, + getSimilarRuleKeyConfig, + getSimilarExperimentKeyConfig, + getDuplicateExperimentKeyConfig, +} from '../tests/test_data'; +import { cloneDeep } from 'lodash'; +import { Experiment } from '../shared_types'; +import { LoggerFacade } from '../logging/logger'; + +const datafile: ProjectConfig = getTestProjectConfigWithFeatures(); +const typedAudienceDatafile = getTypedAudiencesConfig(); +const similarRuleKeyDatafile = getSimilarRuleKeyConfig(); +const similarExperimentKeyDatafile = getSimilarExperimentKeyConfig(); + +const getAllExperimentsFromDatafile = (datafile: ProjectConfig) => { + const allExperiments: Experiment[] = []; + datafile.groups.forEach(group => { + group.experiments.forEach(experiment => { + allExperiments.push(experiment); + }); + }); + datafile.experiments.forEach(experiment => { + allExperiments.push(experiment); + }); + return allExperiments; +}; + +describe('Optimizely Config', () => { + let optimizelyConfigObject: OptimizelyConfig; + let projectConfigObject: ProjectConfig; + let optimizelyTypedAudienceConfigObject; + let projectTypedAudienceConfigObject: ProjectConfig; + let optimizelySimilarRuleKeyConfigObject: OptimizelyConfig; + let projectSimilarRuleKeyConfigObject: ProjectConfig; + let optimizelySimilarExperimentkeyConfigObject: OptimizelyConfig; + let projectSimilarExperimentKeyConfigObject: ProjectConfig; + + const logger: LoggerFacade = { + info: vi.fn(), + debug: vi.fn(), + warn: vi.fn(), + error: vi.fn(), + child: vi.fn().mockReturnValue(this), + }; + + beforeEach(() => { + projectConfigObject = createProjectConfig(cloneDeep(datafile as any)); + optimizelyConfigObject = createOptimizelyConfig(projectConfigObject, JSON.stringify(datafile)); + projectTypedAudienceConfigObject = createProjectConfig(cloneDeep(typedAudienceDatafile)); + optimizelyTypedAudienceConfigObject = createOptimizelyConfig( + projectTypedAudienceConfigObject, + JSON.stringify(typedAudienceDatafile) + ); + projectSimilarRuleKeyConfigObject = createProjectConfig(cloneDeep(similarRuleKeyDatafile)); + optimizelySimilarRuleKeyConfigObject = createOptimizelyConfig( + projectSimilarRuleKeyConfigObject, + JSON.stringify(similarRuleKeyDatafile) + ); + projectSimilarExperimentKeyConfigObject = createProjectConfig(cloneDeep(similarExperimentKeyDatafile)); + optimizelySimilarExperimentkeyConfigObject = createOptimizelyConfig( + projectSimilarExperimentKeyConfigObject, + JSON.stringify(similarExperimentKeyDatafile) + ); + }); + + it('should return all experiments except rollouts', () => { + const experimentsMap = optimizelyConfigObject.experimentsMap; + const experimentsCount = Object.keys(experimentsMap).length; + + expect(experimentsCount).toBe(12); + + const allExperiments: Experiment[] = getAllExperimentsFromDatafile(datafile); + + allExperiments.forEach(experiment => { + expect(experimentsMap[experiment.key]).toMatchObject({ + id: experiment.id, + key: experiment.key, + }); + + const variationsMap = experimentsMap[experiment.key].variationsMap; + + experiment.variations.forEach(variation => { + expect(variationsMap[variation.key]).toMatchObject({ + id: variation.id, + key: variation.key, + }); + }); + }); + }); + + it('should keep the last experiment in case of duplicate key and log a warning', () => { + const datafile = getDuplicateExperimentKeyConfig(); + const configObj = createProjectConfig(datafile, JSON.stringify(datafile)); + const optimizelyConfig = createOptimizelyConfig(configObj, JSON.stringify(datafile), logger); + const experimentsMap = optimizelyConfig.experimentsMap; + const duplicateKey = 'experiment_rule'; + const lastExperiment = datafile.experiments[datafile.experiments.length - 1]; + + expect(experimentsMap['experiment_rule'].id).toBe(lastExperiment.id); + expect(logger.warn).toHaveBeenCalledWith(`Duplicate experiment keys found in datafile: ${duplicateKey}`); + }); + + it('should return all the feature flags', function() { + const featureFlagsCount = Object.keys(optimizelyConfigObject.featuresMap).length; + assert.equal(featureFlagsCount, 9); + + const featuresMap = optimizelyConfigObject.featuresMap; + const expectedDeliveryRules = [ + [ + { + id: '594031', + key: '594031', + audiences: '', + variationsMap: { + '594032': { + id: '594032', + key: '594032', + featureEnabled: true, + variablesMap: { + new_content: { + id: '4919852825313280', + key: 'new_content', + type: 'boolean', + value: 'true', + }, + lasers: { + id: '5482802778734592', + key: 'lasers', + type: 'integer', + value: '395', + }, + price: { + id: '6045752732155904', + key: 'price', + type: 'double', + value: '4.99', + }, + message: { + id: '6327227708866560', + key: 'message', + type: 'string', + value: 'Hello audience', + }, + message_info: { + id: '8765345281230956', + key: 'message_info', + type: 'json', + value: '{ "count": 2, "message": "Hello audience" }', + }, + }, + }, + }, + }, + { + id: '594037', + key: '594037', + audiences: '', + variationsMap: { + '594038': { + id: '594038', + key: '594038', + featureEnabled: false, + variablesMap: { + new_content: { + id: '4919852825313280', + key: 'new_content', + type: 'boolean', + value: 'false', + }, + lasers: { + id: '5482802778734592', + key: 'lasers', + type: 'integer', + value: '400', + }, + price: { + id: '6045752732155904', + key: 'price', + type: 'double', + value: '14.99', + }, + message: { + id: '6327227708866560', + key: 'message', + type: 'string', + value: 'Hello', + }, + message_info: { + id: '8765345281230956', + key: 'message_info', + type: 'json', + value: '{ "count": 1, "message": "Hello" }', + }, + }, + }, + }, + }, + ], + [ + { + id: '594060', + key: '594060', + audiences: '', + variationsMap: { + '594061': { + id: '594061', + key: '594061', + featureEnabled: true, + variablesMap: { + miles_to_the_wall: { + id: '5060590313668608', + key: 'miles_to_the_wall', + type: 'double', + value: '27.34', + }, + motto: { + id: '5342065290379264', + key: 'motto', + type: 'string', + value: 'Winter is NOT coming', + }, + soldiers_available: { + id: '6186490220511232', + key: 'soldiers_available', + type: 'integer', + value: '10003', + }, + is_winter_coming: { + id: '6467965197221888', + key: 'is_winter_coming', + type: 'boolean', + value: 'false', + }, + }, + }, + }, + }, + { + id: '594066', + key: '594066', + audiences: '', + variationsMap: { + '594067': { + id: '594067', + key: '594067', + featureEnabled: true, + variablesMap: { + miles_to_the_wall: { + id: '5060590313668608', + key: 'miles_to_the_wall', + type: 'double', + value: '30.34', + }, + motto: { + id: '5342065290379264', + key: 'motto', + type: 'string', + value: 'Winter is coming definitely', + }, + soldiers_available: { + id: '6186490220511232', + key: 'soldiers_available', + type: 'integer', + value: '500', + }, + is_winter_coming: { + id: '6467965197221888', + key: 'is_winter_coming', + type: 'boolean', + value: 'true', + }, + }, + }, + }, + }, + ], + [], + [], + [ + { + id: '599056', + key: '599056', + audiences: '', + variationsMap: { + '599057': { + id: '599057', + key: '599057', + featureEnabled: true, + variablesMap: { + lasers: { + id: '4937719889264640', + key: 'lasers', + type: 'integer', + value: '200', + }, + message: { + id: '6345094772817920', + key: 'message', + type: 'string', + value: "i'm a rollout", + }, + }, + }, + }, + }, + ], + [], + [], + [ + { + id: '594060', + key: '594060', + audiences: '', + variationsMap: { + '594061': { + id: '594061', + key: '594061', + featureEnabled: true, + variablesMap: { + miles_to_the_wall: { + id: '5060590313668608', + key: 'miles_to_the_wall', + type: 'double', + value: '27.34', + }, + motto: { + id: '5342065290379264', + key: 'motto', + type: 'string', + value: 'Winter is NOT coming', + }, + soldiers_available: { + id: '6186490220511232', + key: 'soldiers_available', + type: 'integer', + value: '10003', + }, + is_winter_coming: { + id: '6467965197221888', + key: 'is_winter_coming', + type: 'boolean', + value: 'false', + }, + }, + }, + }, + }, + { + id: '594066', + key: '594066', + audiences: '', + variationsMap: { + '594067': { + id: '594067', + key: '594067', + featureEnabled: true, + variablesMap: { + miles_to_the_wall: { + id: '5060590313668608', + key: 'miles_to_the_wall', + type: 'double', + value: '30.34', + }, + motto: { + id: '5342065290379264', + key: 'motto', + type: 'string', + value: 'Winter is coming definitely', + }, + soldiers_available: { + id: '6186490220511232', + key: 'soldiers_available', + type: 'integer', + value: '500', + }, + is_winter_coming: { + id: '6467965197221888', + key: 'is_winter_coming', + type: 'boolean', + value: 'true', + }, + }, + }, + }, + }, + ], + [ + { + id: '594060', + key: '594060', + audiences: '', + variationsMap: { + '594061': { + id: '594061', + key: '594061', + featureEnabled: true, + variablesMap: { + miles_to_the_wall: { + id: '5060590313668608', + key: 'miles_to_the_wall', + type: 'double', + value: '27.34', + }, + motto: { + id: '5342065290379264', + key: 'motto', + type: 'string', + value: 'Winter is NOT coming', + }, + soldiers_available: { + id: '6186490220511232', + key: 'soldiers_available', + type: 'integer', + value: '10003', + }, + is_winter_coming: { + id: '6467965197221888', + key: 'is_winter_coming', + type: 'boolean', + value: 'false', + }, + }, + }, + }, + }, + { + id: '594066', + key: '594066', + audiences: '', + variationsMap: { + '594067': { + id: '594067', + key: '594067', + featureEnabled: true, + variablesMap: { + miles_to_the_wall: { + id: '5060590313668608', + key: 'miles_to_the_wall', + type: 'double', + value: '30.34', + }, + motto: { + id: '5342065290379264', + key: 'motto', + type: 'string', + value: 'Winter is coming definitely', + }, + soldiers_available: { + id: '6186490220511232', + key: 'soldiers_available', + type: 'integer', + value: '500', + }, + is_winter_coming: { + id: '6467965197221888', + key: 'is_winter_coming', + type: 'boolean', + value: 'true', + }, + }, + }, + }, + }, + ], + ]; + const expectedExperimentRules = [ + [], + [], + [ + { + id: '594098', + key: 'testing_my_feature', + audiences: '', + variationsMap: { + variation: { + id: '594096', + key: 'variation', + featureEnabled: true, + variablesMap: { + num_buttons: { + id: '4792309476491264', + key: 'num_buttons', + type: 'integer', + value: '2', + }, + is_button_animated: { + id: '5073784453201920', + key: 'is_button_animated', + type: 'boolean', + value: 'true', + }, + button_txt: { + id: '5636734406623232', + key: 'button_txt', + type: 'string', + value: 'Buy me NOW', + }, + button_width: { + id: '6199684360044544', + key: 'button_width', + type: 'double', + value: '20.25', + }, + button_info: { + id: '1547854156498475', + key: 'button_info', + type: 'json', + value: '{ "num_buttons": 1, "text": "first variation"}', + }, + }, + }, + control: { + id: '594097', + key: 'control', + featureEnabled: true, + variablesMap: { + num_buttons: { + id: '4792309476491264', + key: 'num_buttons', + type: 'integer', + value: '10', + }, + is_button_animated: { + id: '5073784453201920', + key: 'is_button_animated', + type: 'boolean', + value: 'false', + }, + button_txt: { + id: '5636734406623232', + key: 'button_txt', + type: 'string', + value: 'Buy me', + }, + button_width: { + id: '6199684360044544', + key: 'button_width', + type: 'double', + value: '50.55', + }, + button_info: { + id: '1547854156498475', + key: 'button_info', + type: 'json', + value: '{ "num_buttons": 2, "text": "second variation"}', + }, + }, + }, + variation2: { + id: '594099', + key: 'variation2', + featureEnabled: false, + variablesMap: { + num_buttons: { + id: '4792309476491264', + key: 'num_buttons', + type: 'integer', + value: '10', + }, + is_button_animated: { + id: '5073784453201920', + key: 'is_button_animated', + type: 'boolean', + value: 'false', + }, + button_txt: { + id: '5636734406623232', + key: 'button_txt', + type: 'string', + value: 'Buy me', + }, + button_width: { + id: '6199684360044544', + key: 'button_width', + type: 'double', + value: '50.55', + }, + button_info: { + id: '1547854156498475', + key: 'button_info', + type: 'json', + value: '{ "num_buttons": 0, "text": "default value"}', + }, + }, + }, + }, + }, + ], + [ + { + id: '595010', + key: 'exp_with_group', + audiences: '', + variationsMap: { + var: { + featureEnabled: undefined, + id: '595008', + key: 'var', + variablesMap: {}, + }, + con: { + featureEnabled: undefined, + id: '595009', + key: 'con', + variablesMap: {}, + }, + }, + }, + ], + [ + { + id: '599028', + key: 'test_shared_feature', + audiences: '', + variationsMap: { + treatment: { + id: '599026', + key: 'treatment', + featureEnabled: true, + variablesMap: { + lasers: { + id: '4937719889264640', + key: 'lasers', + type: 'integer', + value: '100', + }, + message: { + id: '6345094772817920', + key: 'message', + type: 'string', + value: 'shared', + }, + }, + }, + control: { + id: '599027', + key: 'control', + featureEnabled: false, + variablesMap: { + lasers: { + id: '4937719889264640', + key: 'lasers', + type: 'integer', + value: '100', + }, + message: { + id: '6345094772817920', + key: 'message', + type: 'string', + value: 'shared', + }, + }, + }, + }, + }, + ], + [], + [ + { + id: '12115595439', + key: 'no_traffic_experiment', + audiences: '', + variationsMap: { + variation_5000: { + featureEnabled: undefined, + id: '12098126629', + key: 'variation_5000', + variablesMap: {}, + }, + variation_10000: { + featureEnabled: undefined, + id: '12098126630', + key: 'variation_10000', + variablesMap: {}, + }, + }, + }, + ], + [ + { + id: '42222', + key: 'group_2_exp_1', + audiences: '"Test attribute users 3"', + variationsMap: { + var_1: { + id: '38901', + key: 'var_1', + featureEnabled: false, + variablesMap: {}, + }, + }, + }, + { + id: '42223', + key: 'group_2_exp_2', + audiences: '"Test attribute users 3"', + variationsMap: { + var_1: { + id: '38905', + key: 'var_1', + featureEnabled: false, + variablesMap: {}, + }, + }, + }, + { + id: '42224', + key: 'group_2_exp_3', + audiences: '"Test attribute users 3"', + variationsMap: { + var_1: { + id: '38906', + key: 'var_1', + featureEnabled: false, + variablesMap: {}, + }, + }, + }, + ], + [ + { + id: '111134', + key: 'test_experiment3', + audiences: '"Test attribute users 3"', + variationsMap: { + control: { + id: '222239', + key: 'control', + featureEnabled: false, + variablesMap: {}, + }, + }, + }, + { + id: '111135', + key: 'test_experiment4', + audiences: '"Test attribute users 3"', + variationsMap: { + control: { + id: '222240', + key: 'control', + featureEnabled: false, + variablesMap: {}, + }, + }, + }, + { + id: '111136', + key: 'test_experiment5', + audiences: '"Test attribute users 3"', + variationsMap: { + control: { + id: '222241', + key: 'control', + featureEnabled: false, + variablesMap: {}, + }, + }, + }, + ], + ]; + + datafile.featureFlags.forEach((featureFlag, index) => { + expect(featuresMap[featureFlag.key]).toMatchObject({ + id: featureFlag.id, + key: featureFlag.key, + }); + + featureFlag.experimentIds.forEach(experimentId => { + const experimentKey = projectConfigObject.experimentIdMap[experimentId].key; + + expect(!!featuresMap[featureFlag.key].experimentsMap[experimentKey]).toBe(true); + }); + + const variablesMap = featuresMap[featureFlag.key].variablesMap; + const deliveryRules = featuresMap[featureFlag.key].deliveryRules; + const experimentRules = featuresMap[featureFlag.key].experimentRules; + + expect(deliveryRules).toEqual(expectedDeliveryRules[index]); + expect(experimentRules).toEqual(expectedExperimentRules[index]); + + featureFlag.variables.forEach(variable => { + // json is represented as sub type of string to support backwards compatibility in datafile. + // project config treats it as a first-class type. + const expectedVariableType = variable.type === 'string' && variable.subType === 'json' ? 'json' : variable.type; + + expect(variablesMap[variable.key]).toMatchObject({ + id: variable.id, + key: variable.key, + type: expectedVariableType, + value: variable.defaultValue, + }); + }); + }); + }); + + it('should correctly merge all feature variables', () => { + const featureFlags = datafile.featureFlags; + const datafileExperimentsMap: Record = getAllExperimentsFromDatafile(datafile).reduce( + (experiments, experiment) => { + experiments[experiment.key] = experiment; + return experiments; + }, + {} as Record + ); + + featureFlags.forEach(featureFlag => { + const experimentIds = featureFlag.experimentIds; + experimentIds.forEach(experimentId => { + const experimentKey = projectConfigObject.experimentIdMap[experimentId].key; + const experiment = optimizelyConfigObject.experimentsMap[experimentKey]; + const variations = datafileExperimentsMap[experimentKey].variations; + const variationsMap = experiment.variationsMap; + variations.forEach(variation => { + featureFlag.variables.forEach(variable => { + const variableToAssert = variationsMap[variation.key].variablesMap[variable.key]; + // json is represented as sub type of string to support backwards compatibility in datafile. + // project config treats it as a first-class type. + const expectedVariableType = + variable.type === 'string' && variable.subType === 'json' ? 'json' : variable.type; + + expect({ + id: variable.id, + key: variable.key, + type: expectedVariableType, + }).toMatchObject({ + id: variableToAssert.id, + key: variableToAssert.key, + type: variableToAssert.type, + }); + + if (!variation.featureEnabled) { + expect(variable.defaultValue).toBe(variableToAssert.value); + } + }); + }); + }); + }); + }); + + it('should return correct config revision', () => { + expect(optimizelyConfigObject.revision).toBe(datafile.revision); + }); + + it('should return correct config sdkKey ', () => { + expect(optimizelyConfigObject.sdkKey).toBe(datafile.sdkKey); + }); + + it('should return correct config environmentKey ', () => { + expect(optimizelyConfigObject.environmentKey).toBe(datafile.environmentKey); + }); + + it('should return serialized audiences', () => { + const audiencesById = projectTypedAudienceConfigObject.audiencesById; + const audienceConditions = [ + ['or', '3468206642', '3988293898'], + ['or', '3468206642', '3988293898', '3468206646'], + ['not', '3468206642'], + ['or', '3468206642'], + ['and', '3468206642'], + ['3468206642'], + ['3468206642', '3988293898'], + ['and', ['or', '3468206642', '3988293898'], '3468206646'], + [ + 'and', + ['or', '3468206642', ['and', '3988293898', '3468206646']], + ['and', '3988293899', ['or', '3468206647', '3468206643']], + ], + ['and', 'and'], + ['not', ['and', '3468206642', '3988293898']], + [], + ['or', '3468206642', '999999999'], + ]; + + const expectedAudienceOutputs = [ + '"exactString" OR "substringString"', + '"exactString" OR "substringString" OR "exactNumber"', + 'NOT "exactString"', + '"exactString"', + '"exactString"', + '"exactString"', + '"exactString" OR "substringString"', + '("exactString" OR "substringString") AND "exactNumber"', + '("exactString" OR ("substringString" AND "exactNumber")) AND ("exists" AND ("gtNumber" OR "exactBoolean"))', + '', + 'NOT ("exactString" AND "substringString")', + '', + '"exactString" OR "999999999"', + ]; + + for (let testNo = 0; testNo < audienceConditions.length; testNo++) { + const serializedAudiences = OptimizelyConfig.getSerializedAudiences( + audienceConditions[testNo] as string[], + audiencesById + ); + + expect(serializedAudiences).toBe(expectedAudienceOutputs[testNo]); + } + }); + + it('should return correct rollouts', () => { + const rolloutFlag1 = optimizelySimilarRuleKeyConfigObject.featuresMap['flag_1'].deliveryRules[0]; + const rolloutFlag2 = optimizelySimilarRuleKeyConfigObject.featuresMap['flag_2'].deliveryRules[0]; + const rolloutFlag3 = optimizelySimilarRuleKeyConfigObject.featuresMap['flag_3'].deliveryRules[0]; + + expect(rolloutFlag1.id).toBe('9300000004977'); + expect(rolloutFlag1.key).toBe('targeted_delivery'); + expect(rolloutFlag2.id).toBe('9300000004979'); + expect(rolloutFlag2.key).toBe('targeted_delivery'); + expect(rolloutFlag3.id).toBe('9300000004981'); + expect(rolloutFlag3.key).toBe('targeted_delivery'); + }); + + it('should return default SDK and environment key', () => { + expect(optimizelySimilarRuleKeyConfigObject.sdkKey).toBe(''); + expect(optimizelySimilarRuleKeyConfigObject.environmentKey).toBe(''); + }); + + it('should return correct experiments with similar keys', function() { + expect(Object.keys(optimizelySimilarExperimentkeyConfigObject.experimentsMap).length).toBe(1); + + const experimentMapFlag1 = optimizelySimilarExperimentkeyConfigObject.featuresMap['flag1'].experimentsMap; + const experimentMapFlag2 = optimizelySimilarExperimentkeyConfigObject.featuresMap['flag2'].experimentsMap; + + expect(experimentMapFlag1['targeted_delivery'].id).toBe('9300000007569'); + expect(experimentMapFlag2['targeted_delivery'].id).toBe('9300000007573'); + }); +}); From fd1896619d80bb982fd98810c1535541d5ad09ff Mon Sep 17 00:00:00 2001 From: Md Junaed Hossain <169046794+junaed-optimizely@users.noreply.github.com> Date: Thu, 6 Feb 2025 23:30:15 +0600 Subject: [PATCH 6/9] [FSSDK-11034] old test cleanup --- lib/project_config/optimizely_config.tests.js | 916 ---------------- lib/project_config/project_config.tests.js | 974 ------------------ 2 files changed, 1890 deletions(-) delete mode 100644 lib/project_config/optimizely_config.tests.js delete mode 100644 lib/project_config/project_config.tests.js diff --git a/lib/project_config/optimizely_config.tests.js b/lib/project_config/optimizely_config.tests.js deleted file mode 100644 index 22d2b95f3..000000000 --- a/lib/project_config/optimizely_config.tests.js +++ /dev/null @@ -1,916 +0,0 @@ -/** - * Copyright 2019-2021, 2024, Optimizely - * - * 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. - */ -import { assert } from 'chai'; -import { cloneDeep } from 'lodash'; -import sinon from 'sinon'; - -import { createOptimizelyConfig, OptimizelyConfig } from './optimizely_config'; -import { createProjectConfig } from './project_config'; -import { - getTestProjectConfigWithFeatures, - getTypedAudiencesConfig, - getSimilarRuleKeyConfig, - getSimilarExperimentKeyConfig, - getDuplicateExperimentKeyConfig, -} from '../tests/test_data'; - -var datafile = getTestProjectConfigWithFeatures(); -var typedAudienceDatafile = getTypedAudiencesConfig(); -var similarRuleKeyDatafile = getSimilarRuleKeyConfig(); -var similarExperimentKeyDatafile = getSimilarExperimentKeyConfig(); - -var getAllExperimentsFromDatafile = function(datafile) { - var allExperiments = []; - datafile.groups.forEach(function(group) { - group.experiments.forEach(function(experiment) { - allExperiments.push(experiment); - }); - }); - datafile.experiments.forEach(function(experiment) { - allExperiments.push(experiment); - }); - return allExperiments; -}; - -describe('lib/core/optimizely_config', function() { - describe('Optimizely Config', function() { - var optimizelyConfigObject; - var projectConfigObject; - var optimizelyTypedAudienceConfigObject; - var projectTypedAudienceConfigObject; - var optimizelySimilarRuleKeyConfigObject; - var projectSimilarRuleKeyConfigObject; - var optimizelySimilarExperimentkeyConfigObject; - var projectSimilarExperimentKeyConfigObject; - - beforeEach(function() { - projectConfigObject = createProjectConfig(cloneDeep(datafile)); - optimizelyConfigObject = createOptimizelyConfig(projectConfigObject, JSON.stringify(datafile)); - projectTypedAudienceConfigObject = createProjectConfig(cloneDeep(typedAudienceDatafile)); - optimizelyTypedAudienceConfigObject = createOptimizelyConfig(projectTypedAudienceConfigObject, JSON.stringify(typedAudienceDatafile)); - projectSimilarRuleKeyConfigObject = createProjectConfig(cloneDeep(similarRuleKeyDatafile)); - optimizelySimilarRuleKeyConfigObject = createOptimizelyConfig(projectSimilarRuleKeyConfigObject, JSON.stringify(similarRuleKeyDatafile)); - projectSimilarExperimentKeyConfigObject = createProjectConfig(cloneDeep(similarExperimentKeyDatafile)); - optimizelySimilarExperimentkeyConfigObject = createOptimizelyConfig(projectSimilarExperimentKeyConfigObject, JSON.stringify(similarExperimentKeyDatafile)); - }); - - it('should return all experiments except rollouts', function() { - var experimentsMap = optimizelyConfigObject.experimentsMap; - var experimentsCount = Object.keys(optimizelyConfigObject.experimentsMap).length; - assert.equal(experimentsCount, 12); - - var allExperiments = getAllExperimentsFromDatafile(datafile); - allExperiments.forEach(function(experiment) { - assert.include(experimentsMap[experiment.key], { - id: experiment.id, - key: experiment.key, - }); - var variationsMap = experimentsMap[experiment.key].variationsMap; - experiment.variations.forEach(function(variation) { - assert.include(variationsMap[variation.key], { - id: variation.id, - key: variation.key, - }); - }); - }); - }); - - it('should keep the last experiment in case of duplicate key and log a warning', function() { - const datafile = getDuplicateExperimentKeyConfig(); - const configObj = createProjectConfig(datafile, JSON.stringify(datafile)); - - const logger = { - warn: sinon.spy(), - } - - const optimizelyConfig = createOptimizelyConfig(configObj, JSON.stringify(datafile), logger); - const experimentsMap = optimizelyConfig.experimentsMap; - - const duplicateKey = 'experiment_rule'; - const lastExperiment = datafile.experiments[datafile.experiments.length - 1]; - - assert.equal(experimentsMap['experiment_rule'].id, lastExperiment.id); - assert.isTrue(logger.warn.calledWithExactly(`Duplicate experiment keys found in datafile: ${duplicateKey}`)); - }); - - it('should return all the feature flags', function() { - var featureFlagsCount = Object.keys(optimizelyConfigObject.featuresMap).length; - assert.equal(featureFlagsCount, 9); - - var featuresMap = optimizelyConfigObject.featuresMap; - var expectedDeliveryRules = [ - [ - { - id: "594031", - key: "594031", - audiences: "", - variationsMap: { - "594032": { - id: "594032", - key: "594032", - featureEnabled: true, - variablesMap: { - new_content: { - id: "4919852825313280", - key: "new_content", - type: "boolean", - value: "true" - }, - lasers: { - id: "5482802778734592", - key: "lasers", - type: "integer", - value: "395" - }, - price: { - id: "6045752732155904", - key: "price", - type: "double", - value: "4.99" - }, - message: { - id: "6327227708866560", - key: "message", - type: "string", - value: "Hello audience" - }, - message_info: { - id: "8765345281230956", - key: "message_info", - type: "json", - value: "{ \"count\": 2, \"message\": \"Hello audience\" }" - } - } - } - } - }, { - id: "594037", - key: "594037", - audiences: "", - variationsMap: { - "594038": { - id: "594038", - key: "594038", - featureEnabled: false, - variablesMap: { - new_content: { - id: "4919852825313280", - key: "new_content", - type: "boolean", - value: "false" - }, - lasers: { - id: "5482802778734592", - key: "lasers", - type: "integer", - value: "400" - }, - price: { - id: "6045752732155904", - key: "price", - type: "double", - value: "14.99" - }, - message: { - id: "6327227708866560", - key: "message", - type: "string", - value: "Hello" - }, - message_info: { - id: "8765345281230956", - key: "message_info", - type: "json", - value: "{ \"count\": 1, \"message\": \"Hello\" }" - } - } - } - } - } - ], - [ - { - id: "594060", - key: "594060", - audiences: "", - variationsMap: { - "594061": { - id: "594061", - key: "594061", - featureEnabled: true, - variablesMap: { - miles_to_the_wall: { - id: "5060590313668608", - key: "miles_to_the_wall", - type: "double", - value: "27.34" - }, - motto: { - id: "5342065290379264", - key: "motto", - type: "string", - value: "Winter is NOT coming" - }, - soldiers_available: { - id: "6186490220511232", - key: "soldiers_available", - type: "integer", - value: "10003" - }, - is_winter_coming: { - id: "6467965197221888", - key: "is_winter_coming", - type: "boolean", - value: "false" - } - } - } - } - }, { - id: "594066", - key: "594066", - audiences: "", - variationsMap: { - "594067": { - id: "594067", - key: "594067", - featureEnabled: true, - variablesMap: { - miles_to_the_wall: { - id: "5060590313668608", - key: "miles_to_the_wall", - type: "double", - value: "30.34" - }, - motto: { - id: "5342065290379264", - key: "motto", - type: "string", - value: "Winter is coming definitely" - }, - soldiers_available: { - id: "6186490220511232", - key: "soldiers_available", - type: "integer", - value: "500" - }, - is_winter_coming: { - id: "6467965197221888", - key: "is_winter_coming", - type: "boolean", - value: "true" - } - } - } - } - } - ], - [], - [], - [ - { - id: "599056", - key: "599056", - audiences: "", - variationsMap: { - "599057": { - id: "599057", - key: "599057", - featureEnabled: true, - variablesMap: { - lasers: { - id: "4937719889264640", - key: "lasers", - type: "integer", - value: "200" - }, - message: { - id: "6345094772817920", - key: "message", - type: "string", - value: "i'm a rollout" - } - } - } - } - } - ], - [], - [], - [ - { - id: "594060", - key: "594060", - audiences: "", - variationsMap: { - "594061": { - id: "594061", - key: "594061", - featureEnabled: true, - variablesMap: { - miles_to_the_wall: { - id: "5060590313668608", - key: "miles_to_the_wall", - type: "double", - value: "27.34" - }, - motto: { - id: "5342065290379264", - key: "motto", - type: "string", - value: "Winter is NOT coming" - }, - soldiers_available: { - id: "6186490220511232", - key: "soldiers_available", - type: "integer", - value: "10003" - }, - is_winter_coming: { - id: "6467965197221888", - key: "is_winter_coming", - type: "boolean", - value: "false" - } - } - } - } - }, { - id: "594066", - key: "594066", - audiences: "", - variationsMap: { - "594067": { - id: "594067", - key: "594067", - featureEnabled: true, - variablesMap: { - miles_to_the_wall: { - id: "5060590313668608", - key: "miles_to_the_wall", - type: "double", - value: "30.34" - }, - motto: { - id: "5342065290379264", - key: "motto", - type: "string", - value: "Winter is coming definitely" - }, - soldiers_available: { - id: "6186490220511232", - key: "soldiers_available", - type: "integer", - value: "500" - }, - is_winter_coming: { - id: "6467965197221888", - key: "is_winter_coming", - type: "boolean", - value: "true" - } - } - } - } - } - ], - [ - { - id: "594060", - key: "594060", - audiences: "", - variationsMap: { - "594061": { - id: "594061", - key: "594061", - featureEnabled: true, - variablesMap: { - miles_to_the_wall: { - id: "5060590313668608", - key: "miles_to_the_wall", - type: "double", - value: "27.34" - }, - motto: { - id: "5342065290379264", - key: "motto", - type: "string", - value: "Winter is NOT coming" - }, - soldiers_available: { - id: "6186490220511232", - key: "soldiers_available", - type: "integer", - value: "10003" - }, - is_winter_coming: { - id: "6467965197221888", - key: "is_winter_coming", - type: "boolean", - value: "false" - } - } - } - } - }, { - id: "594066", - key: "594066", - audiences: "", - variationsMap: { - "594067": { - id: "594067", - key: "594067", - featureEnabled: true, - variablesMap: { - miles_to_the_wall: { - id: "5060590313668608", - key: "miles_to_the_wall", - type: "double", - value: "30.34" - }, - motto: { - id: "5342065290379264", - key: "motto", - type: "string", - value: "Winter is coming definitely" - }, - soldiers_available: { - id: "6186490220511232", - key: "soldiers_available", - type: "integer", - value: "500" - }, - is_winter_coming: { - id: "6467965197221888", - key: "is_winter_coming", - type: "boolean", - value: "true" - } - } - } - } - } - ] - ] - var expectedExperimentRules = [ - [], - [], - [ - { - id: "594098", - key: "testing_my_feature", - audiences: "", - variationsMap: { - variation: { - id: "594096", - key: "variation", - featureEnabled: true, - variablesMap: { - num_buttons: { - id: "4792309476491264", - key: "num_buttons", - type: "integer", - value: "2" - }, - is_button_animated: { - id: "5073784453201920", - key: "is_button_animated", - type: "boolean", - value: "true" - }, - button_txt: { - id: "5636734406623232", - key: "button_txt", - type: "string", - value: "Buy me NOW" - }, - button_width: { - id: "6199684360044544", - key: "button_width", - type: "double", - value: "20.25" - }, - button_info: { - id: "1547854156498475", - key: "button_info", - type: "json", - value: "{ \"num_buttons\": 1, \"text\": \"first variation\"}" - } - } - }, - control: { - id: "594097", - key: "control", - featureEnabled: true, - variablesMap: { - num_buttons: { - id: "4792309476491264", - key: "num_buttons", - type: "integer", - value: "10" - }, - is_button_animated: { - id: "5073784453201920", - key: "is_button_animated", - type: "boolean", - value: "false" - }, - button_txt: { - id: "5636734406623232", - key: "button_txt", - type: "string", - value: "Buy me" - }, - button_width: { - id: "6199684360044544", - key: "button_width", - type: "double", - value: "50.55" - }, - button_info: { - id: "1547854156498475", - key: "button_info", - type: "json", - value: "{ \"num_buttons\": 2, \"text\": \"second variation\"}" - } - } - }, - "variation2": { - id: "594099", - key: "variation2", - featureEnabled: false, - variablesMap: { - num_buttons: { - id: "4792309476491264", - key: "num_buttons", - type: "integer", - value: "10" - }, - is_button_animated: { - id: "5073784453201920", - key: "is_button_animated", - type: "boolean", - value: "false" - }, - button_txt: { - id: "5636734406623232", - key: "button_txt", - type: "string", - value: "Buy me" - }, - button_width: { - id: "6199684360044544", - key: "button_width", - type: "double", - value: "50.55" - }, - button_info: { - id: "1547854156498475", - key: "button_info", - type: "json", - value: "{ \"num_buttons\": 0, \"text\": \"default value\"}" - } - } - } - } - } - ], - [ - { - id: "595010", - key: "exp_with_group", - audiences: "", - variationsMap: { - var: { - featureEnabled: undefined, - id: "595008", - key: "var", - variablesMap: {} - }, - con: { - featureEnabled: undefined, - id: "595009", - key: "con", - variablesMap: {} - } - } - } - ], - [ - { - id: "599028", - key: "test_shared_feature", - audiences: "", - variationsMap: { - treatment: { - id: "599026", - key: "treatment", - featureEnabled: true, - variablesMap: { - lasers: { - id: "4937719889264640", - key: "lasers", - type: "integer", - value: "100" - }, - message: { - id: "6345094772817920", - key: "message", - type: "string", - value: "shared" - } - } - }, - control: { - id: "599027", - key: "control", - featureEnabled: false, - variablesMap: { - lasers: { - id: "4937719889264640", - key: "lasers", - type: "integer", - value: "100" - }, - message: { - id: "6345094772817920", - key: "message", - type: "string", - value: "shared" - } - } - } - } - } - ], - [], - [ - { - id: "12115595439", - key: "no_traffic_experiment", - audiences: "", - variationsMap: { - "variation_5000": { - "featureEnabled": undefined, - id: "12098126629", - key: "variation_5000", - variablesMap: {} - }, - "variation_10000": { - "featureEnabled": undefined, - id: "12098126630", - key: "variation_10000", - variablesMap: {} - } - } - } - ], - [ - { - id: "42222", - key: "group_2_exp_1", - audiences: "\"Test attribute users 3\"", - variationsMap: { - "var_1": { - id: "38901", - key: "var_1", - featureEnabled: false, - variablesMap: {} - } - } - }, { - id: "42223", - key: "group_2_exp_2", - audiences: "\"Test attribute users 3\"", - variationsMap: { - "var_1": { - id: "38905", - key: "var_1", - featureEnabled: false, - variablesMap: {} - } - } - }, { - id: "42224", - key: "group_2_exp_3", - audiences: "\"Test attribute users 3\"", - variationsMap: { - "var_1": { - id: "38906", - key: "var_1", - featureEnabled: false, - variablesMap: {} - } - } - } - ], - [ - { - id: "111134", - key: "test_experiment3", - audiences: "\"Test attribute users 3\"", - variationsMap: { - control: { - id: "222239", - key: "control", - featureEnabled: false, - variablesMap: {} - } - } - }, { - id: "111135", - key: "test_experiment4", - audiences: "\"Test attribute users 3\"", - variationsMap: { - control: { - id: "222240", - key: "control", - featureEnabled: false, - variablesMap: {} - } - } - }, { - id: "111136", - key: "test_experiment5", - audiences: "\"Test attribute users 3\"", - variationsMap: { - control: { - id: "222241", - key: "control", - featureEnabled: false, - variablesMap: {} - } - } - } - ] - ] - - datafile.featureFlags.forEach(function(featureFlag, index) { - assert.include(featuresMap[featureFlag.key], { - id: featureFlag.id, - key: featureFlag.key, - }); - featureFlag.experimentIds.forEach(function(experimentId) { - var experimentKey = projectConfigObject.experimentIdMap[experimentId].key; - assert.isTrue(!!featuresMap[featureFlag.key].experimentsMap[experimentKey]); - }); - var variablesMap = featuresMap[featureFlag.key].variablesMap; - var deliveryRules = featuresMap[featureFlag.key].deliveryRules; - var experimentRules = featuresMap[featureFlag.key].experimentRules; - assert.deepEqual(deliveryRules, expectedDeliveryRules[index]); - assert.deepEqual(experimentRules, expectedExperimentRules[index]); - featureFlag.variables.forEach(function(variable) { - // json is represented as sub type of string to support backwards compatibility in datafile. - // project config treats it as a first-class type. - var expectedVariableType = (variable.type === "string" && variable.subType === "json") ? "json" : variable.type; - assert.include(variablesMap[variable.key], { - id: variable.id, - key: variable.key, - type: expectedVariableType, - value: variable.defaultValue, - }); - }); - }); - }); - - it('should correctly merge all feature variables', function() { - var featureFlags = datafile.featureFlags; - var datafileExperimentsMap = getAllExperimentsFromDatafile(datafile).reduce(function(experiments, experiment) { - experiments[experiment.key] = experiment; - return experiments; - }, {}); - featureFlags.forEach(function(featureFlag) { - var experimentIds = featureFlag.experimentIds; - experimentIds.forEach(function(experimentId) { - var experimentKey = projectConfigObject.experimentIdMap[experimentId].key; - var experiment = optimizelyConfigObject.experimentsMap[experimentKey]; - var variations = datafileExperimentsMap[experimentKey].variations; - var variationsMap = experiment.variationsMap; - variations.forEach(function(variation) { - featureFlag.variables.forEach(function(variable) { - var variableToAssert = variationsMap[variation.key].variablesMap[variable.key]; - // json is represented as sub type of string to support backwards compatibility in datafile. - // project config treats it as a first-class type. - var expectedVariableType = (variable.type === "string" && variable.subType === "json") ? "json" : variable.type; - assert.include( - { - id: variable.id, - key: variable.key, - type: expectedVariableType, - }, - { - id: variableToAssert.id, - key: variableToAssert.key, - type: variableToAssert.type, - } - ); - if (!variation.featureEnabled) { - assert.equal(variable.defaultValue, variableToAssert.value); - } - }); - }); - }); - }); - }); - - it('should return correct config revision', function() { - assert.equal(optimizelyConfigObject.revision, datafile.revision); - }); - - it('should return correct config sdkKey ', function() { - assert.equal(optimizelyConfigObject.sdkKey, datafile.sdkKey); - }); - - it('should return correct config environmentKey ', function() { - assert.equal(optimizelyConfigObject.environmentKey, datafile.environmentKey); - }); - - it('should return serialized audiences', function () { - const audiencesById = projectTypedAudienceConfigObject.audiencesById; - const audienceConditions = [ - ['or', '3468206642', '3988293898'], - ['or', '3468206642', '3988293898', '3468206646'], - ['not', '3468206642'], - ['or', '3468206642'], - ['and', '3468206642'], - ['3468206642'], - ['3468206642', '3988293898'], - ['and', ['or', '3468206642', '3988293898'], '3468206646'], - [ - 'and', - ['or', '3468206642', ['and', '3988293898', '3468206646']], - ['and', '3988293899', ['or', '3468206647', '3468206643']], - ], - ['and', 'and'], - ['not', ['and', '3468206642', '3988293898']], - [], - ['or', '3468206642', '999999999'], - ]; - - const expectedAudienceOutputs = [ - '"exactString" OR "substringString"', - '"exactString" OR "substringString" OR "exactNumber"', - 'NOT "exactString"', - '"exactString"', - '"exactString"', - '"exactString"', - '"exactString" OR "substringString"', - '("exactString" OR "substringString") AND "exactNumber"', - '("exactString" OR ("substringString" AND "exactNumber")) AND ("exists" AND ("gtNumber" OR "exactBoolean"))', - '', - 'NOT ("exactString" AND "substringString")', - '', - '"exactString" OR "999999999"', - ]; - - for (let testNo = 0; testNo < audienceConditions.length; testNo++) { - const serializedAudiences = OptimizelyConfig.getSerializedAudiences(audienceConditions[testNo], audiencesById); - assert.equal(serializedAudiences, expectedAudienceOutputs[testNo]); - } - }); - - it('should return correct rollouts', function () { - const rolloutFlag1 = optimizelySimilarRuleKeyConfigObject.featuresMap['flag_1'].deliveryRules[0]; - const rolloutFlag2 = optimizelySimilarRuleKeyConfigObject.featuresMap['flag_2'].deliveryRules[0]; - const rolloutFlag3 = optimizelySimilarRuleKeyConfigObject.featuresMap['flag_3'].deliveryRules[0]; - - assert.equal(rolloutFlag1.id, '9300000004977'); - assert.equal(rolloutFlag1.key, 'targeted_delivery'); - assert.equal(rolloutFlag2.id, '9300000004979'); - assert.equal(rolloutFlag2.key, 'targeted_delivery'); - assert.equal(rolloutFlag3.id, '9300000004981'); - assert.equal(rolloutFlag3.key, 'targeted_delivery'); - - }); - - it('should return default SDK and environment key', function() { - - assert.equal(optimizelySimilarRuleKeyConfigObject.sdkKey, ""); - assert.equal(optimizelySimilarRuleKeyConfigObject.environmentKey, ""); - - }); - - it('should return correct experiments with similar keys', function() { - - assert.equal(Object.keys(optimizelySimilarExperimentkeyConfigObject.experimentsMap).length, 1); - const experimentMapFlag1 = optimizelySimilarExperimentkeyConfigObject.featuresMap["flag1"].experimentsMap; - const experimentMapFlag2 = optimizelySimilarExperimentkeyConfigObject.featuresMap["flag2"].experimentsMap; - assert.equal(experimentMapFlag1["targeted_delivery"].id, "9300000007569"); - assert.equal(experimentMapFlag2["targeted_delivery"].id, "9300000007573"); - - }); - }); -}); diff --git a/lib/project_config/project_config.tests.js b/lib/project_config/project_config.tests.js deleted file mode 100644 index 6e93327cc..000000000 --- a/lib/project_config/project_config.tests.js +++ /dev/null @@ -1,974 +0,0 @@ -/** - * Copyright 2016-2024, Optimizely - * - * 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 - * - * https://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. - */ -import sinon from 'sinon'; -import { assert } from 'chai'; -import { forEach, cloneDeep } from 'lodash'; -import { sprintf } from '../utils/fns'; -import fns from '../utils/fns'; -import projectConfig from './project_config'; -import { FEATURE_VARIABLE_TYPES, LOG_LEVEL } from '../utils/enums'; -import testDatafile from '../tests/test_data'; -import configValidator from '../utils/config_validator'; -import { - INVALID_EXPERIMENT_ID, - INVALID_EXPERIMENT_KEY, - UNEXPECTED_RESERVED_ATTRIBUTE_PREFIX, - UNRECOGNIZED_ATTRIBUTE, - VARIABLE_KEY_NOT_IN_DATAFILE, - FEATURE_NOT_IN_DATAFILE, - UNABLE_TO_CAST_VALUE -} from 'error_message'; - -var createLogger = () => ({ - debug: () => {}, - info: () => {}, - warn: () => {}, - error: () => {}, - child: () => createLogger(), -}) - -var buildLogMessageFromArgs = args => sprintf(args[1], ...args.splice(2)); -var logger = createLogger(); - -describe('lib/core/project_config', function() { - describe('createProjectConfig method', function() { - it('should set properties correctly when createProjectConfig is called', function() { - var testData = testDatafile.getTestProjectConfig(); - var configObj = projectConfig.createProjectConfig(testData); - - forEach(testData.audiences, function(audience) { - audience.conditions = JSON.parse(audience.conditions); - }); - - assert.strictEqual(configObj.accountId, testData.accountId); - assert.strictEqual(configObj.projectId, testData.projectId); - assert.strictEqual(configObj.revision, testData.revision); - assert.deepEqual(configObj.events, testData.events); - assert.deepEqual(configObj.audiences, testData.audiences); - testData.groups.forEach(function(group) { - group.experiments.forEach(function(experiment) { - experiment.groupId = group.id; - experiment.variationKeyMap = fns.keyBy(experiment.variations, 'key'); - }); - }); - assert.deepEqual(configObj.groups, testData.groups); - - var expectedGroupIdMap = { - 666: testData.groups[0], - 667: testData.groups[1], - }; - - assert.deepEqual(configObj.groupIdMap, expectedGroupIdMap); - - var expectedExperiments = testData.experiments; - forEach(configObj.groupIdMap, function(group, Id) { - forEach(group.experiments, function(experiment) { - experiment.groupId = Id; - expectedExperiments.push(experiment); - }); - }); - - forEach(expectedExperiments, function(experiment) { - experiment.variationKeyMap = fns.keyBy(experiment.variations, 'key'); - }); - - assert.deepEqual(configObj.experiments, expectedExperiments); - - var expectedAttributeKeyMap = { - browser_type: testData.attributes[0], - boolean_key: testData.attributes[1], - integer_key: testData.attributes[2], - double_key: testData.attributes[3], - valid_positive_number: testData.attributes[4], - valid_negative_number: testData.attributes[5], - invalid_number: testData.attributes[6], - array: testData.attributes[7], - }; - - assert.deepEqual(configObj.attributeKeyMap, expectedAttributeKeyMap); - - var expectedExperimentKeyMap = { - testExperiment: configObj.experiments[0], - testExperimentWithAudiences: configObj.experiments[1], - testExperimentNotRunning: configObj.experiments[2], - testExperimentLaunched: configObj.experiments[3], - groupExperiment1: configObj.experiments[4], - groupExperiment2: configObj.experiments[5], - overlappingGroupExperiment1: configObj.experiments[6], - }; - - assert.deepEqual(configObj.experimentKeyMap, expectedExperimentKeyMap); - - var expectedEventKeyMap = { - testEvent: testData.events[0], - 'Total Revenue': testData.events[1], - testEventWithAudiences: testData.events[2], - testEventWithoutExperiments: testData.events[3], - testEventWithExperimentNotRunning: testData.events[4], - testEventWithMultipleExperiments: testData.events[5], - testEventLaunched: testData.events[6], - }; - - assert.deepEqual(configObj.eventKeyMap, expectedEventKeyMap); - - var expectedExperimentIdMap = { - '111127': configObj.experiments[0], - '122227': configObj.experiments[1], - '133337': configObj.experiments[2], - '144447': configObj.experiments[3], - '442': configObj.experiments[4], - '443': configObj.experiments[5], - '444': configObj.experiments[6], - }; - - assert.deepEqual(configObj.experimentIdMap, expectedExperimentIdMap); - - var expectedVariationKeyMap = {}; - expectedVariationKeyMap[testData.experiments[0].key + testData.experiments[0].variations[0].key] = - testData.experiments[0].variations[0]; - expectedVariationKeyMap[testData.experiments[0].key + testData.experiments[0].variations[1].key] = - testData.experiments[0].variations[1]; - expectedVariationKeyMap[testData.experiments[1].key + testData.experiments[1].variations[0].key] = - testData.experiments[1].variations[0]; - expectedVariationKeyMap[testData.experiments[1].key + testData.experiments[1].variations[1].key] = - testData.experiments[1].variations[1]; - expectedVariationKeyMap[testData.experiments[2].key + testData.experiments[2].variations[0].key] = - testData.experiments[2].variations[0]; - expectedVariationKeyMap[testData.experiments[2].key + testData.experiments[2].variations[1].key] = - testData.experiments[2].variations[1]; - expectedVariationKeyMap[configObj.experiments[3].key + configObj.experiments[3].variations[0].key] = - configObj.experiments[3].variations[0]; - expectedVariationKeyMap[configObj.experiments[3].key + configObj.experiments[3].variations[1].key] = - configObj.experiments[3].variations[1]; - expectedVariationKeyMap[configObj.experiments[4].key + configObj.experiments[4].variations[0].key] = - configObj.experiments[4].variations[0]; - expectedVariationKeyMap[configObj.experiments[4].key + configObj.experiments[4].variations[1].key] = - configObj.experiments[4].variations[1]; - expectedVariationKeyMap[configObj.experiments[5].key + configObj.experiments[5].variations[0].key] = - configObj.experiments[5].variations[0]; - expectedVariationKeyMap[configObj.experiments[5].key + configObj.experiments[5].variations[1].key] = - configObj.experiments[5].variations[1]; - expectedVariationKeyMap[configObj.experiments[6].key + configObj.experiments[6].variations[0].key] = - configObj.experiments[6].variations[0]; - expectedVariationKeyMap[configObj.experiments[6].key + configObj.experiments[6].variations[1].key] = - configObj.experiments[6].variations[1]; - - var expectedVariationIdMap = { - '111128': testData.experiments[0].variations[0], - '111129': testData.experiments[0].variations[1], - '122228': testData.experiments[1].variations[0], - '122229': testData.experiments[1].variations[1], - '133338': testData.experiments[2].variations[0], - '133339': testData.experiments[2].variations[1], - '144448': testData.experiments[3].variations[0], - '144449': testData.experiments[3].variations[1], - '551': configObj.experiments[4].variations[0], - '552': configObj.experiments[4].variations[1], - '661': configObj.experiments[5].variations[0], - '662': configObj.experiments[5].variations[1], - '553': configObj.experiments[6].variations[0], - '554': configObj.experiments[6].variations[1], - }; - }); - - it('should not mutate the datafile', function() { - var datafile = testDatafile.getTypedAudiencesConfig(); - var datafileClone = cloneDeep(datafile); - projectConfig.createProjectConfig(datafile); - assert.deepEqual(datafileClone, datafile); - }); - - describe('feature management', function() { - var configObj; - beforeEach(function() { - configObj = projectConfig.createProjectConfig(testDatafile.getTestProjectConfigWithFeatures()); - }); - - it('creates a rolloutIdMap from rollouts in the datafile', function() { - assert.deepEqual(configObj.rolloutIdMap, testDatafile.datafileWithFeaturesExpectedData.rolloutIdMap); - }); - - it('creates a variationVariableUsageMap from rollouts and experiments with features in the datafile', function() { - assert.deepEqual( - configObj.variationVariableUsageMap, - testDatafile.datafileWithFeaturesExpectedData.variationVariableUsageMap - ); - }); - - it('creates a featureKeyMap from feature flags in the datafile', function() { - assert.deepEqual(configObj.featureKeyMap, testDatafile.datafileWithFeaturesExpectedData.featureKeyMap); - }); - - it('adds variations from rollout experiments to variationIdMap', function() { - assert.deepEqual(configObj.variationIdMap['594032'], { - variables: [ - { value: 'true', id: '4919852825313280' }, - { value: '395', id: '5482802778734592' }, - { value: '4.99', id: '6045752732155904' }, - { value: 'Hello audience', id: '6327227708866560' }, - { value: '{ "count": 2, "message": "Hello audience" }', id: '8765345281230956' }, - ], - featureEnabled: true, - key: '594032', - id: '594032', - }); - assert.deepEqual(configObj.variationIdMap['594038'], { - variables: [ - { value: 'false', id: '4919852825313280' }, - { value: '400', id: '5482802778734592' }, - { value: '14.99', id: '6045752732155904' }, - { value: 'Hello', id: '6327227708866560' }, - { value: '{ "count": 1, "message": "Hello" }', id: '8765345281230956' }, - ], - featureEnabled: false, - key: '594038', - id: '594038', - }); - assert.deepEqual(configObj.variationIdMap['594061'], { - variables: [ - { value: '27.34', id: '5060590313668608' }, - { value: 'Winter is NOT coming', id: '5342065290379264' }, - { value: '10003', id: '6186490220511232' }, - { value: 'false', id: '6467965197221888' }, - ], - featureEnabled: true, - key: '594061', - id: '594061', - }); - assert.deepEqual(configObj.variationIdMap['594067'], { - variables: [ - { value: '30.34', id: '5060590313668608' }, - { value: 'Winter is coming definitely', id: '5342065290379264' }, - { value: '500', id: '6186490220511232' }, - { value: 'true', id: '6467965197221888' }, - ], - featureEnabled: true, - key: '594067', - id: '594067', - }); - }); - }); - - describe('flag variations', function() { - var configObj; - beforeEach(function() { - configObj = projectConfig.createProjectConfig(testDatafile.getTestDecideProjectConfig()); - }); - - it('it should populate flagVariationsMap correctly', function() { - var allVariationsForFlag = configObj.flagVariationsMap; - var feature1Variations = allVariationsForFlag.feature_1; - var feature2Variations = allVariationsForFlag.feature_2; - var feature3Variations = allVariationsForFlag.feature_3; - var feature1VariationsKeys = feature1Variations.map(variation => { - return variation.key; - }, {}); - var feature2VariationsKeys = feature2Variations.map(variation => { - return variation.key; - }, {}); - var feature3VariationsKeys = feature3Variations.map(variation => { - return variation.key; - }, {}); - - assert.deepEqual(feature1VariationsKeys, ['a', 'b', '3324490633', '3324490562', '18257766532']); - assert.deepEqual(feature2VariationsKeys, ['variation_with_traffic', 'variation_no_traffic']); - assert.deepEqual(feature3VariationsKeys, []); - }); - }); - }); - - describe('projectConfig helper methods', function() { - var testData = cloneDeep(testDatafile.getTestProjectConfig()); - var configObj; - var createdLogger = createLogger({ logLevel: LOG_LEVEL.INFO }); - - beforeEach(function() { - configObj = projectConfig.createProjectConfig(cloneDeep(testData)); - sinon.stub(createdLogger, 'warn'); - }); - - afterEach(function() { - createdLogger.warn.restore(); - }); - - it('should retrieve experiment ID for valid experiment key in getExperimentId', function() { - assert.strictEqual( - projectConfig.getExperimentId(configObj, testData.experiments[0].key), - testData.experiments[0].id - ); - }); - - it('should throw error for invalid experiment key in getExperimentId', function() { - const ex = assert.throws(function() { - projectConfig.getExperimentId(configObj, 'invalidExperimentKey'); - }); - assert.equal(ex.baseMessage, INVALID_EXPERIMENT_KEY); - assert.deepEqual(ex.params, ['invalidExperimentKey']); - }); - - it('should retrieve layer ID for valid experiment key in getLayerId', function() { - assert.strictEqual(projectConfig.getLayerId(configObj, '111127'), '4'); - }); - - it('should throw error for invalid experiment key in getLayerId', function() { - const ex = assert.throws(function() { - projectConfig.getLayerId(configObj, 'invalidExperimentKey'); - }); - assert.equal(ex.baseMessage, INVALID_EXPERIMENT_ID); - assert.deepEqual(ex.params, ['invalidExperimentKey']); - }); - - it('should retrieve attribute ID for valid attribute key in getAttributeId', function() { - assert.strictEqual(projectConfig.getAttributeId(configObj, 'browser_type'), '111094'); - }); - - it('should retrieve attribute ID for reserved attribute key in getAttributeId', function() { - assert.strictEqual(projectConfig.getAttributeId(configObj, '$opt_user_agent'), '$opt_user_agent'); - }); - - it('should return null for invalid attribute key in getAttributeId', function() { - assert.isNull(projectConfig.getAttributeId(configObj, 'invalidAttributeKey', createdLogger)); - - assert.deepEqual(createdLogger.warn.lastCall.args, [UNRECOGNIZED_ATTRIBUTE, 'invalidAttributeKey']); - }); - - it('should return null for invalid attribute key in getAttributeId', function() { - // Adding attribute in key map with reserved prefix - configObj.attributeKeyMap['$opt_some_reserved_attribute'] = { - id: '42', - key: '$opt_some_reserved_attribute', - }; - assert.strictEqual(projectConfig.getAttributeId(configObj, '$opt_some_reserved_attribute', createdLogger), '42'); - - assert.deepEqual(createdLogger.warn.lastCall.args, [UNEXPECTED_RESERVED_ATTRIBUTE_PREFIX, '$opt_some_reserved_attribute', '$opt_']); - }); - - it('should retrieve event ID for valid event key in getEventId', function() { - assert.strictEqual(projectConfig.getEventId(configObj, 'testEvent'), '111095'); - }); - - it('should return null for invalid event key in getEventId', function() { - assert.isNull(projectConfig.getEventId(configObj, 'invalidEventKey')); - }); - - it('should retrieve experiment status for valid experiment key in getExperimentStatus', function() { - assert.strictEqual( - projectConfig.getExperimentStatus(configObj, testData.experiments[0].key), - testData.experiments[0].status - ); - }); - - it('should throw error for invalid experiment key in getExperimentStatus', function() { - const ex = assert.throws(function() { - projectConfig.getExperimentStatus(configObj, 'invalidExperimentKey'); - }); - assert.equal(ex.baseMessage, INVALID_EXPERIMENT_KEY); - assert.deepEqual(ex.params, ['invalidExperimentKey']); - }); - - it('should return true if experiment status is set to Running in isActive', function() { - assert.isTrue(projectConfig.isActive(configObj, 'testExperiment')); - }); - - it('should return false if experiment status is not set to Running in isActive', function() { - assert.isFalse(projectConfig.isActive(configObj, 'testExperimentNotRunning')); - }); - - it('should return true if experiment status is set to Running in isRunning', function() { - assert.isTrue(projectConfig.isRunning(configObj, 'testExperiment')); - }); - - it('should return false if experiment status is not set to Running in isRunning', function() { - assert.isFalse(projectConfig.isRunning(configObj, 'testExperimentLaunched')); - }); - - it('should retrieve variation key for valid experiment key and variation ID in getVariationKeyFromId', function() { - assert.deepEqual( - projectConfig.getVariationKeyFromId(configObj, testData.experiments[0].variations[0].id), - testData.experiments[0].variations[0].key - ); - }); - - it('should retrieve traffic allocation given valid experiment key in getTrafficAllocation', function() { - assert.deepEqual( - projectConfig.getTrafficAllocation(configObj, testData.experiments[0].id), - testData.experiments[0].trafficAllocation - ); - }); - - it('should throw error for invalid experient key in getTrafficAllocation', function() { - const ex = assert.throws(function() { - projectConfig.getTrafficAllocation(configObj, 'invalidExperimentId'); - }); - assert.equal(ex.baseMessage, INVALID_EXPERIMENT_ID); - assert.deepEqual(ex.params, ['invalidExperimentId']); - }); - - describe('#getVariationIdFromExperimentAndVariationKey', function() { - it('should return the variation id for the given experiment key and variation key', function() { - assert.strictEqual( - projectConfig.getVariationIdFromExperimentAndVariationKey( - configObj, - testData.experiments[0].key, - testData.experiments[0].variations[0].key - ), - testData.experiments[0].variations[0].id - ); - }); - }); - - describe('#getSendFlagDecisionsValue', function() { - it('should return false when sendFlagDecisions is undefined', function() { - configObj.sendFlagDecisions = undefined; - assert.deepEqual(projectConfig.getSendFlagDecisionsValue(configObj), false); - }); - - it('should return false when sendFlagDecisions is set to false', function() { - configObj.sendFlagDecisions = false; - assert.deepEqual(projectConfig.getSendFlagDecisionsValue(configObj), false); - }); - - it('should return true when sendFlagDecisions is set to true', function() { - configObj.sendFlagDecisions = true; - assert.deepEqual(projectConfig.getSendFlagDecisionsValue(configObj), true); - }); - }); - - describe('feature management', function() { - var featureManagementLogger = createLogger({ logLevel: LOG_LEVEL.INFO }); - beforeEach(function() { - configObj = projectConfig.createProjectConfig(testDatafile.getTestProjectConfigWithFeatures()); - sinon.stub(featureManagementLogger, 'warn'); - sinon.stub(featureManagementLogger, 'error'); - sinon.stub(featureManagementLogger, 'info'); - sinon.stub(featureManagementLogger, 'debug'); - }); - - afterEach(function() { - featureManagementLogger.warn.restore(); - featureManagementLogger.error.restore(); - featureManagementLogger.info.restore(); - featureManagementLogger.debug.restore(); - }); - - describe('getVariableForFeature', function() { - it('should return a variable object for a valid variable and feature key', function() { - var featureKey = 'test_feature_for_experiment'; - var variableKey = 'num_buttons'; - var result = projectConfig.getVariableForFeature(configObj, featureKey, variableKey, featureManagementLogger); - assert.deepEqual(result, { - type: 'integer', - key: 'num_buttons', - id: '4792309476491264', - defaultValue: '10', - }); - }); - - it('should return null for an invalid variable key and a valid feature key', function() { - var featureKey = 'test_feature_for_experiment'; - var variableKey = 'notARealVariable____'; - var result = projectConfig.getVariableForFeature(configObj, featureKey, variableKey, featureManagementLogger); - assert.strictEqual(result, null); - sinon.assert.calledOnce(featureManagementLogger.error); - - assert.deepEqual(featureManagementLogger.error.lastCall.args, [VARIABLE_KEY_NOT_IN_DATAFILE, 'notARealVariable____', 'test_feature_for_experiment']); - }); - - it('should return null for an invalid feature key', function() { - var featureKey = 'notARealFeature_____'; - var variableKey = 'num_buttons'; - var result = projectConfig.getVariableForFeature(configObj, featureKey, variableKey, featureManagementLogger); - assert.strictEqual(result, null); - sinon.assert.calledOnce(featureManagementLogger.error); - - assert.deepEqual(featureManagementLogger.error.lastCall.args, [FEATURE_NOT_IN_DATAFILE, 'notARealFeature_____']); - }); - - it('should return null for an invalid variable key and an invalid feature key', function() { - var featureKey = 'notARealFeature_____'; - var variableKey = 'notARealVariable____'; - var result = projectConfig.getVariableForFeature(configObj, featureKey, variableKey, featureManagementLogger); - assert.strictEqual(result, null); - sinon.assert.calledOnce(featureManagementLogger.error); - - assert.deepEqual(featureManagementLogger.error.lastCall.args, [FEATURE_NOT_IN_DATAFILE, 'notARealFeature_____']); - }); - }); - - describe('getVariableValueForVariation', function() { - it('returns a value for a valid variation and variable', function() { - var variation = configObj.variationIdMap['594096']; - var variable = configObj.featureKeyMap.test_feature_for_experiment.variableKeyMap.num_buttons; - var result = projectConfig.getVariableValueForVariation( - configObj, - variable, - variation, - featureManagementLogger - ); - assert.strictEqual(result, '2'); - - variable = configObj.featureKeyMap.test_feature_for_experiment.variableKeyMap.is_button_animated; - result = projectConfig.getVariableValueForVariation(configObj, variable, variation, featureManagementLogger); - assert.strictEqual(result, 'true'); - - variable = configObj.featureKeyMap.test_feature_for_experiment.variableKeyMap.button_txt; - result = projectConfig.getVariableValueForVariation(configObj, variable, variation, featureManagementLogger); - assert.strictEqual(result, 'Buy me NOW'); - - variable = configObj.featureKeyMap.test_feature_for_experiment.variableKeyMap.button_width; - result = projectConfig.getVariableValueForVariation(configObj, variable, variation, featureManagementLogger); - assert.strictEqual(result, '20.25'); - }); - - it('returns null for a null variation', function() { - var variation = null; - var variable = configObj.featureKeyMap.test_feature_for_experiment.variableKeyMap.num_buttons; - var result = projectConfig.getVariableValueForVariation( - configObj, - variable, - variation, - featureManagementLogger - ); - assert.strictEqual(result, null); - }); - - it('returns null for a null variable', function() { - var variation = configObj.variationIdMap['594096']; - var variable = null; - var result = projectConfig.getVariableValueForVariation( - configObj, - variable, - variation, - featureManagementLogger - ); - assert.strictEqual(result, null); - }); - - it('returns null for a null variation and null variable', function() { - var variation = null; - var variable = null; - var result = projectConfig.getVariableValueForVariation( - configObj, - variable, - variation, - featureManagementLogger - ); - assert.strictEqual(result, null); - }); - - it('returns null for a variation whose id is not in the datafile', function() { - var variation = { - key: 'some_variation', - id: '999999999999', - variables: [], - }; - var variable = configObj.featureKeyMap.test_feature_for_experiment.variableKeyMap.num_buttons; - var result = projectConfig.getVariableValueForVariation( - configObj, - variable, - variation, - featureManagementLogger - ); - assert.strictEqual(result, null); - }); - - it('returns null if the variation does not have a value for this variable', function() { - var variation = configObj.variationIdMap['595008']; // This variation has no variable values associated with it - var variable = configObj.featureKeyMap.test_feature_for_experiment.variableKeyMap.num_buttons; - var result = projectConfig.getVariableValueForVariation( - configObj, - variable, - variation, - featureManagementLogger - ); - assert.isNull(result); - }); - }); - - describe('getTypeCastValue', function() { - it('can cast a boolean', function() { - var result = projectConfig.getTypeCastValue('true', FEATURE_VARIABLE_TYPES.BOOLEAN, featureManagementLogger); - assert.strictEqual(result, true); - result = projectConfig.getTypeCastValue('false', FEATURE_VARIABLE_TYPES.BOOLEAN, featureManagementLogger); - assert.strictEqual(result, false); - }); - - it('can cast an integer', function() { - var result = projectConfig.getTypeCastValue('50', FEATURE_VARIABLE_TYPES.INTEGER, featureManagementLogger); - assert.strictEqual(result, 50); - var result = projectConfig.getTypeCastValue('-7', FEATURE_VARIABLE_TYPES.INTEGER, featureManagementLogger); - assert.strictEqual(result, -7); - var result = projectConfig.getTypeCastValue('0', FEATURE_VARIABLE_TYPES.INTEGER, featureManagementLogger); - assert.strictEqual(result, 0); - }); - - it('can cast a double', function() { - var result = projectConfig.getTypeCastValue('89.99', FEATURE_VARIABLE_TYPES.DOUBLE, featureManagementLogger); - assert.strictEqual(result, 89.99); - var result = projectConfig.getTypeCastValue( - '-257.21', - FEATURE_VARIABLE_TYPES.DOUBLE, - featureManagementLogger - ); - assert.strictEqual(result, -257.21); - var result = projectConfig.getTypeCastValue('0', FEATURE_VARIABLE_TYPES.DOUBLE, featureManagementLogger); - assert.strictEqual(result, 0); - var result = projectConfig.getTypeCastValue('10', FEATURE_VARIABLE_TYPES.DOUBLE, featureManagementLogger); - assert.strictEqual(result, 10); - }); - - it('can return a string unmodified', function() { - var result = projectConfig.getTypeCastValue( - 'message', - FEATURE_VARIABLE_TYPES.STRING, - featureManagementLogger - ); - assert.strictEqual(result, 'message'); - }); - - it('returns null and logs an error for an invalid boolean', function() { - var result = projectConfig.getTypeCastValue( - 'notabool', - FEATURE_VARIABLE_TYPES.BOOLEAN, - featureManagementLogger - ); - assert.strictEqual(result, null); - - assert.deepEqual(featureManagementLogger.error.lastCall.args, [UNABLE_TO_CAST_VALUE, 'notabool', 'boolean']); - }); - - it('returns null and logs an error for an invalid integer', function() { - var result = projectConfig.getTypeCastValue( - 'notanint', - FEATURE_VARIABLE_TYPES.INTEGER, - featureManagementLogger - ); - assert.strictEqual(result, null); - - assert.deepEqual(featureManagementLogger.error.lastCall.args, [UNABLE_TO_CAST_VALUE, 'notanint', 'integer']); - }); - - it('returns null and logs an error for an invalid double', function() { - var result = projectConfig.getTypeCastValue( - 'notadouble', - FEATURE_VARIABLE_TYPES.DOUBLE, - featureManagementLogger - ); - assert.strictEqual(result, null); - - assert.deepEqual(featureManagementLogger.error.lastCall.args, [UNABLE_TO_CAST_VALUE, 'notadouble', 'double']); - }); - }); - }); - - describe('#getAudiencesById', function() { - beforeEach(function() { - configObj = projectConfig.createProjectConfig(testDatafile.getTypedAudiencesConfig()); - }); - - it('should retrieve audiences by checking first in typedAudiences, and then second in audiences', function() { - assert.deepEqual(projectConfig.getAudiencesById(configObj), testDatafile.typedAudiencesById); - }); - }); - - describe('#getExperimentAudienceConditions', function() { - it('should retrieve audiences for valid experiment key', function() { - configObj = projectConfig.createProjectConfig(cloneDeep(testData)); - assert.deepEqual(projectConfig.getExperimentAudienceConditions(configObj, testData.experiments[1].id), [ - '11154', - ]); - }); - - it('should throw error for invalid experiment key', function() { - configObj = projectConfig.createProjectConfig(cloneDeep(testData)); - const ex = assert.throws(function() { - projectConfig.getExperimentAudienceConditions(configObj, 'invalidExperimentId'); - }); - assert.equal(ex.baseMessage, INVALID_EXPERIMENT_ID); - assert.deepEqual(ex.params, ['invalidExperimentId']); - }); - - it('should return experiment audienceIds if experiment has no audienceConditions', function() { - configObj = projectConfig.createProjectConfig(testDatafile.getTypedAudiencesConfig()); - var result = projectConfig.getExperimentAudienceConditions(configObj, '11564051718'); - assert.deepEqual(result, [ - '3468206642', - '3988293898', - '3988293899', - '3468206646', - '3468206647', - '3468206644', - '3468206643', - ]); - }); - - it('should return experiment audienceConditions if experiment has audienceConditions', function() { - configObj = projectConfig.createProjectConfig(testDatafile.getTypedAudiencesConfig()); - // audience_combinations_experiment has both audienceConditions and audienceIds - // audienceConditions should be preferred over audienceIds - var result = projectConfig.getExperimentAudienceConditions(configObj, '1323241598'); - assert.deepEqual(result, [ - 'and', - ['or', '3468206642', '3988293898'], - ['or', '3988293899', '3468206646', '3468206647', '3468206644', '3468206643'], - ]); - }); - }); - - describe('#isFeatureExperiment', function() { - it('returns true for a feature test', function() { - var config = projectConfig.createProjectConfig(testDatafile.getTestProjectConfigWithFeatures()); - var result = projectConfig.isFeatureExperiment(config, '594098'); // id of 'testing_my_feature' - assert.isTrue(result); - }); - - it('returns false for an A/B test', function() { - var config = projectConfig.createProjectConfig(testDatafile.getTestProjectConfig()); - var result = projectConfig.isFeatureExperiment(config, '111127'); // id of 'testExperiment' - assert.isFalse(result); - }); - - it('returns true for a feature test in a mutex group', function() { - var config = projectConfig.createProjectConfig(testDatafile.getMutexFeatureTestsConfig()); - var result = projectConfig.isFeatureExperiment(config, '17128410791'); // id of 'f_test1' - assert.isTrue(result); - result = projectConfig.isFeatureExperiment(config, '17139931304'); // id of 'f_test2' - assert.isTrue(result); - }); - }); - - describe('#getAudienceSegments', function() { - it('returns all qualified segments from an audience', function() { - const dummyQualifiedAudienceJson = { - id: '13389142234', - conditions: [ - 'and', - [ - 'or', - [ - 'or', - { - value: 'odp-segment-1', - type: 'third_party_dimension', - name: 'odp.audiences', - match: 'qualified', - }, - ], - ], - ], - name: 'odp-segment-1', - }; - - const dummyQualifiedAudienceJsonSegments = projectConfig.getAudienceSegments(dummyQualifiedAudienceJson); - assert.deepEqual(dummyQualifiedAudienceJsonSegments, ['odp-segment-1']); - - const dummyUnqualifiedAudienceJson = { - id: '13389142234', - conditions: [ - 'and', - [ - 'or', - [ - 'or', - { - value: 'odp-segment-1', - type: 'third_party_dimension', - name: 'odp.audiences', - match: 'invalid', - }, - ], - ], - ], - name: 'odp-segment-1', - }; - - const dummyUnqualifiedAudienceJsonSegments = projectConfig.getAudienceSegments(dummyUnqualifiedAudienceJson); - assert.deepEqual(dummyUnqualifiedAudienceJsonSegments, []); - }); - - it('returns false for an A/B test', function() { - var config = projectConfig.createProjectConfig(testDatafile.getTestProjectConfig()); - var result = projectConfig.isFeatureExperiment(config, '111127'); // id of 'testExperiment' - assert.isFalse(result); - }); - - it('returns true for a feature test in a mutex group', function() { - var config = projectConfig.createProjectConfig(testDatafile.getMutexFeatureTestsConfig()); - var result = projectConfig.isFeatureExperiment(config, '17128410791'); // id of 'f_test1' - assert.isTrue(result); - result = projectConfig.isFeatureExperiment(config, '17139931304'); // id of 'f_test2' - assert.isTrue(result); - }); - }); - }); - - describe('integrations', () => { - describe('#withSegments', () => { - var config; - beforeEach(() => { - config = projectConfig.createProjectConfig(testDatafile.getOdpIntegratedConfigWithSegments()); - }); - - it('should convert integrations from the datafile into the project config', () => { - assert.exists(config.integrations); - assert.equal(config.integrations.length, 4); - }); - - it('should populate odpIntegrationConfig', () => { - assert.isTrue(config.odpIntegrationConfig.integrated); - assert.equal(config.odpIntegrationConfig.odpConfig.apiKey, 'W4WzcEs-ABgXorzY7h1LCQ'); - assert.equal(config.odpIntegrationConfig.odpConfig.apiHost, 'https://api.zaius.com'); - assert.equal(config.odpIntegrationConfig.odpConfig.pixelUrl, 'https://jumbe.zaius.com'); - assert.deepEqual(config.odpIntegrationConfig.odpConfig.segmentsToCheck, ['odp-segment-1', 'odp-segment-2', 'odp-segment-3']); - }); - }); - - describe('#withoutSegments', () => { - var config; - beforeEach(() => { - config = projectConfig.createProjectConfig(testDatafile.getOdpIntegratedConfigWithoutSegments()); - }); - - it('should convert integrations from the datafile into the project config', () => { - assert.exists(config.integrations); - assert.equal(config.integrations.length, 3); - }); - - it('should populate odpIntegrationConfig', () => { - assert.isTrue(config.odpIntegrationConfig.integrated); - assert.equal(config.odpIntegrationConfig.odpConfig.apiKey, 'W4WzcEs-ABgXorzY7h1LCQ'); - assert.equal(config.odpIntegrationConfig.odpConfig.apiHost, 'https://api.zaius.com'); - assert.equal(config.odpIntegrationConfig.odpConfig.pixelUrl, 'https://jumbe.zaius.com'); - assert.deepEqual(config.odpIntegrationConfig.odpConfig.segmentsToCheck, []); - }); - }); - - describe('#withoutValidIntegrationKey', () => { - it('should throw an error when parsing the project config due to integrations not containing a key', () => { - const odpIntegratedConfigWithoutKey = testDatafile.getOdpIntegratedConfigWithoutKey(); - assert.throws(() => { - projectConfig.createProjectConfig(odpIntegratedConfigWithoutKey); - }); - }); - }); - - describe('#withoutIntegrations', () => { - var config; - beforeEach(() => { - const odpIntegratedConfigWithSegments = testDatafile.getOdpIntegratedConfigWithSegments(); - const noIntegrationsConfigWithSegments = { ...odpIntegratedConfigWithSegments, integrations: [] }; - config = projectConfig.createProjectConfig(noIntegrationsConfigWithSegments); - }); - - it('should convert integrations from the datafile into the project config', () => { - assert.equal(config.integrations.length, 0); - }); - - it('should populate odpIntegrationConfig', () => { - assert.isFalse(config.odpIntegrationConfig.integrated); - assert.isUndefined(config.odpIntegrationConfig.odpConfig); - }); - }); - }); -}); - -describe('#tryCreatingProjectConfig', function() { - var stubJsonSchemaValidator; - beforeEach(function() { - stubJsonSchemaValidator = sinon.stub().returns(true); - sinon.stub(configValidator, 'validateDatafile').returns(true); - sinon.spy(logger, 'error'); - }); - - afterEach(function() { - configValidator.validateDatafile.restore(); - logger.error.restore(); - }); - - it('returns a project config object created by createProjectConfig when all validation is applied and there are no errors', function() { - var configDatafile = { - foo: 'bar', - experiments: [{ key: 'a' }, { key: 'b' }], - }; - configValidator.validateDatafile.returns(configDatafile); - var configObj = { - foo: 'bar', - experimentKeyMap: { - a: { key: 'a', variationKeyMap: {} }, - b: { key: 'b', variationKeyMap: {} }, - }, - }; - - stubJsonSchemaValidator.returns(true); - - var result = projectConfig.tryCreatingProjectConfig({ - datafile: configDatafile, - jsonSchemaValidator: stubJsonSchemaValidator, - logger: logger, - }); - - assert.deepInclude(result, configObj); - }); - - it('throws an error when validateDatafile throws', function() { - configValidator.validateDatafile.throws(); - stubJsonSchemaValidator.returns(true); - assert.throws(() => { - projectConfig.tryCreatingProjectConfig({ - datafile: { foo: 'bar' }, - jsonSchemaValidator: stubJsonSchemaValidator, - logger: logger, - }); - }); - }); - - it('throws an error when jsonSchemaValidator.validate throws', function() { - configValidator.validateDatafile.returns(true); - stubJsonSchemaValidator.throws(); - assert.throws(() => { - projectConfig.tryCreatingProjectConfig({ - datafile: { foo: 'bar' }, - jsonSchemaValidator: stubJsonSchemaValidator, - logger: logger, - }); - }); - }); - - it('skips json validation when jsonSchemaValidator is not provided', function() { - var configDatafile = { - foo: 'bar', - experiments: [{ key: 'a' }, { key: 'b' }], - }; - - configValidator.validateDatafile.returns(configDatafile); - - var configObj = { - foo: 'bar', - experimentKeyMap: { - a: { key: 'a', variationKeyMap: {} }, - b: { key: 'b', variationKeyMap: {} }, - }, - }; - - var result = projectConfig.tryCreatingProjectConfig({ - datafile: configDatafile, - logger: logger, - }); - - assert.deepInclude(result, configObj); - sinon.assert.notCalled(logger.error); - }); -}); From cfa444da67af04ebbf713435dc9c4610dacec02e Mon Sep 17 00:00:00 2001 From: Md Junaed Hossain <169046794+junaed-optimizely@users.noreply.github.com> Date: Fri, 7 Feb 2025 21:44:18 +0600 Subject: [PATCH 7/9] [FSSDK-11034] old test readdition --- lib/project_config/optimizely_config.tests.js | 916 ++++++++++++++++ lib/project_config/project_config.tests.js | 974 ++++++++++++++++++ 2 files changed, 1890 insertions(+) create mode 100644 lib/project_config/optimizely_config.tests.js create mode 100644 lib/project_config/project_config.tests.js diff --git a/lib/project_config/optimizely_config.tests.js b/lib/project_config/optimizely_config.tests.js new file mode 100644 index 000000000..22d2b95f3 --- /dev/null +++ b/lib/project_config/optimizely_config.tests.js @@ -0,0 +1,916 @@ +/** + * Copyright 2019-2021, 2024, Optimizely + * + * 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. + */ +import { assert } from 'chai'; +import { cloneDeep } from 'lodash'; +import sinon from 'sinon'; + +import { createOptimizelyConfig, OptimizelyConfig } from './optimizely_config'; +import { createProjectConfig } from './project_config'; +import { + getTestProjectConfigWithFeatures, + getTypedAudiencesConfig, + getSimilarRuleKeyConfig, + getSimilarExperimentKeyConfig, + getDuplicateExperimentKeyConfig, +} from '../tests/test_data'; + +var datafile = getTestProjectConfigWithFeatures(); +var typedAudienceDatafile = getTypedAudiencesConfig(); +var similarRuleKeyDatafile = getSimilarRuleKeyConfig(); +var similarExperimentKeyDatafile = getSimilarExperimentKeyConfig(); + +var getAllExperimentsFromDatafile = function(datafile) { + var allExperiments = []; + datafile.groups.forEach(function(group) { + group.experiments.forEach(function(experiment) { + allExperiments.push(experiment); + }); + }); + datafile.experiments.forEach(function(experiment) { + allExperiments.push(experiment); + }); + return allExperiments; +}; + +describe('lib/core/optimizely_config', function() { + describe('Optimizely Config', function() { + var optimizelyConfigObject; + var projectConfigObject; + var optimizelyTypedAudienceConfigObject; + var projectTypedAudienceConfigObject; + var optimizelySimilarRuleKeyConfigObject; + var projectSimilarRuleKeyConfigObject; + var optimizelySimilarExperimentkeyConfigObject; + var projectSimilarExperimentKeyConfigObject; + + beforeEach(function() { + projectConfigObject = createProjectConfig(cloneDeep(datafile)); + optimizelyConfigObject = createOptimizelyConfig(projectConfigObject, JSON.stringify(datafile)); + projectTypedAudienceConfigObject = createProjectConfig(cloneDeep(typedAudienceDatafile)); + optimizelyTypedAudienceConfigObject = createOptimizelyConfig(projectTypedAudienceConfigObject, JSON.stringify(typedAudienceDatafile)); + projectSimilarRuleKeyConfigObject = createProjectConfig(cloneDeep(similarRuleKeyDatafile)); + optimizelySimilarRuleKeyConfigObject = createOptimizelyConfig(projectSimilarRuleKeyConfigObject, JSON.stringify(similarRuleKeyDatafile)); + projectSimilarExperimentKeyConfigObject = createProjectConfig(cloneDeep(similarExperimentKeyDatafile)); + optimizelySimilarExperimentkeyConfigObject = createOptimizelyConfig(projectSimilarExperimentKeyConfigObject, JSON.stringify(similarExperimentKeyDatafile)); + }); + + it('should return all experiments except rollouts', function() { + var experimentsMap = optimizelyConfigObject.experimentsMap; + var experimentsCount = Object.keys(optimizelyConfigObject.experimentsMap).length; + assert.equal(experimentsCount, 12); + + var allExperiments = getAllExperimentsFromDatafile(datafile); + allExperiments.forEach(function(experiment) { + assert.include(experimentsMap[experiment.key], { + id: experiment.id, + key: experiment.key, + }); + var variationsMap = experimentsMap[experiment.key].variationsMap; + experiment.variations.forEach(function(variation) { + assert.include(variationsMap[variation.key], { + id: variation.id, + key: variation.key, + }); + }); + }); + }); + + it('should keep the last experiment in case of duplicate key and log a warning', function() { + const datafile = getDuplicateExperimentKeyConfig(); + const configObj = createProjectConfig(datafile, JSON.stringify(datafile)); + + const logger = { + warn: sinon.spy(), + } + + const optimizelyConfig = createOptimizelyConfig(configObj, JSON.stringify(datafile), logger); + const experimentsMap = optimizelyConfig.experimentsMap; + + const duplicateKey = 'experiment_rule'; + const lastExperiment = datafile.experiments[datafile.experiments.length - 1]; + + assert.equal(experimentsMap['experiment_rule'].id, lastExperiment.id); + assert.isTrue(logger.warn.calledWithExactly(`Duplicate experiment keys found in datafile: ${duplicateKey}`)); + }); + + it('should return all the feature flags', function() { + var featureFlagsCount = Object.keys(optimizelyConfigObject.featuresMap).length; + assert.equal(featureFlagsCount, 9); + + var featuresMap = optimizelyConfigObject.featuresMap; + var expectedDeliveryRules = [ + [ + { + id: "594031", + key: "594031", + audiences: "", + variationsMap: { + "594032": { + id: "594032", + key: "594032", + featureEnabled: true, + variablesMap: { + new_content: { + id: "4919852825313280", + key: "new_content", + type: "boolean", + value: "true" + }, + lasers: { + id: "5482802778734592", + key: "lasers", + type: "integer", + value: "395" + }, + price: { + id: "6045752732155904", + key: "price", + type: "double", + value: "4.99" + }, + message: { + id: "6327227708866560", + key: "message", + type: "string", + value: "Hello audience" + }, + message_info: { + id: "8765345281230956", + key: "message_info", + type: "json", + value: "{ \"count\": 2, \"message\": \"Hello audience\" }" + } + } + } + } + }, { + id: "594037", + key: "594037", + audiences: "", + variationsMap: { + "594038": { + id: "594038", + key: "594038", + featureEnabled: false, + variablesMap: { + new_content: { + id: "4919852825313280", + key: "new_content", + type: "boolean", + value: "false" + }, + lasers: { + id: "5482802778734592", + key: "lasers", + type: "integer", + value: "400" + }, + price: { + id: "6045752732155904", + key: "price", + type: "double", + value: "14.99" + }, + message: { + id: "6327227708866560", + key: "message", + type: "string", + value: "Hello" + }, + message_info: { + id: "8765345281230956", + key: "message_info", + type: "json", + value: "{ \"count\": 1, \"message\": \"Hello\" }" + } + } + } + } + } + ], + [ + { + id: "594060", + key: "594060", + audiences: "", + variationsMap: { + "594061": { + id: "594061", + key: "594061", + featureEnabled: true, + variablesMap: { + miles_to_the_wall: { + id: "5060590313668608", + key: "miles_to_the_wall", + type: "double", + value: "27.34" + }, + motto: { + id: "5342065290379264", + key: "motto", + type: "string", + value: "Winter is NOT coming" + }, + soldiers_available: { + id: "6186490220511232", + key: "soldiers_available", + type: "integer", + value: "10003" + }, + is_winter_coming: { + id: "6467965197221888", + key: "is_winter_coming", + type: "boolean", + value: "false" + } + } + } + } + }, { + id: "594066", + key: "594066", + audiences: "", + variationsMap: { + "594067": { + id: "594067", + key: "594067", + featureEnabled: true, + variablesMap: { + miles_to_the_wall: { + id: "5060590313668608", + key: "miles_to_the_wall", + type: "double", + value: "30.34" + }, + motto: { + id: "5342065290379264", + key: "motto", + type: "string", + value: "Winter is coming definitely" + }, + soldiers_available: { + id: "6186490220511232", + key: "soldiers_available", + type: "integer", + value: "500" + }, + is_winter_coming: { + id: "6467965197221888", + key: "is_winter_coming", + type: "boolean", + value: "true" + } + } + } + } + } + ], + [], + [], + [ + { + id: "599056", + key: "599056", + audiences: "", + variationsMap: { + "599057": { + id: "599057", + key: "599057", + featureEnabled: true, + variablesMap: { + lasers: { + id: "4937719889264640", + key: "lasers", + type: "integer", + value: "200" + }, + message: { + id: "6345094772817920", + key: "message", + type: "string", + value: "i'm a rollout" + } + } + } + } + } + ], + [], + [], + [ + { + id: "594060", + key: "594060", + audiences: "", + variationsMap: { + "594061": { + id: "594061", + key: "594061", + featureEnabled: true, + variablesMap: { + miles_to_the_wall: { + id: "5060590313668608", + key: "miles_to_the_wall", + type: "double", + value: "27.34" + }, + motto: { + id: "5342065290379264", + key: "motto", + type: "string", + value: "Winter is NOT coming" + }, + soldiers_available: { + id: "6186490220511232", + key: "soldiers_available", + type: "integer", + value: "10003" + }, + is_winter_coming: { + id: "6467965197221888", + key: "is_winter_coming", + type: "boolean", + value: "false" + } + } + } + } + }, { + id: "594066", + key: "594066", + audiences: "", + variationsMap: { + "594067": { + id: "594067", + key: "594067", + featureEnabled: true, + variablesMap: { + miles_to_the_wall: { + id: "5060590313668608", + key: "miles_to_the_wall", + type: "double", + value: "30.34" + }, + motto: { + id: "5342065290379264", + key: "motto", + type: "string", + value: "Winter is coming definitely" + }, + soldiers_available: { + id: "6186490220511232", + key: "soldiers_available", + type: "integer", + value: "500" + }, + is_winter_coming: { + id: "6467965197221888", + key: "is_winter_coming", + type: "boolean", + value: "true" + } + } + } + } + } + ], + [ + { + id: "594060", + key: "594060", + audiences: "", + variationsMap: { + "594061": { + id: "594061", + key: "594061", + featureEnabled: true, + variablesMap: { + miles_to_the_wall: { + id: "5060590313668608", + key: "miles_to_the_wall", + type: "double", + value: "27.34" + }, + motto: { + id: "5342065290379264", + key: "motto", + type: "string", + value: "Winter is NOT coming" + }, + soldiers_available: { + id: "6186490220511232", + key: "soldiers_available", + type: "integer", + value: "10003" + }, + is_winter_coming: { + id: "6467965197221888", + key: "is_winter_coming", + type: "boolean", + value: "false" + } + } + } + } + }, { + id: "594066", + key: "594066", + audiences: "", + variationsMap: { + "594067": { + id: "594067", + key: "594067", + featureEnabled: true, + variablesMap: { + miles_to_the_wall: { + id: "5060590313668608", + key: "miles_to_the_wall", + type: "double", + value: "30.34" + }, + motto: { + id: "5342065290379264", + key: "motto", + type: "string", + value: "Winter is coming definitely" + }, + soldiers_available: { + id: "6186490220511232", + key: "soldiers_available", + type: "integer", + value: "500" + }, + is_winter_coming: { + id: "6467965197221888", + key: "is_winter_coming", + type: "boolean", + value: "true" + } + } + } + } + } + ] + ] + var expectedExperimentRules = [ + [], + [], + [ + { + id: "594098", + key: "testing_my_feature", + audiences: "", + variationsMap: { + variation: { + id: "594096", + key: "variation", + featureEnabled: true, + variablesMap: { + num_buttons: { + id: "4792309476491264", + key: "num_buttons", + type: "integer", + value: "2" + }, + is_button_animated: { + id: "5073784453201920", + key: "is_button_animated", + type: "boolean", + value: "true" + }, + button_txt: { + id: "5636734406623232", + key: "button_txt", + type: "string", + value: "Buy me NOW" + }, + button_width: { + id: "6199684360044544", + key: "button_width", + type: "double", + value: "20.25" + }, + button_info: { + id: "1547854156498475", + key: "button_info", + type: "json", + value: "{ \"num_buttons\": 1, \"text\": \"first variation\"}" + } + } + }, + control: { + id: "594097", + key: "control", + featureEnabled: true, + variablesMap: { + num_buttons: { + id: "4792309476491264", + key: "num_buttons", + type: "integer", + value: "10" + }, + is_button_animated: { + id: "5073784453201920", + key: "is_button_animated", + type: "boolean", + value: "false" + }, + button_txt: { + id: "5636734406623232", + key: "button_txt", + type: "string", + value: "Buy me" + }, + button_width: { + id: "6199684360044544", + key: "button_width", + type: "double", + value: "50.55" + }, + button_info: { + id: "1547854156498475", + key: "button_info", + type: "json", + value: "{ \"num_buttons\": 2, \"text\": \"second variation\"}" + } + } + }, + "variation2": { + id: "594099", + key: "variation2", + featureEnabled: false, + variablesMap: { + num_buttons: { + id: "4792309476491264", + key: "num_buttons", + type: "integer", + value: "10" + }, + is_button_animated: { + id: "5073784453201920", + key: "is_button_animated", + type: "boolean", + value: "false" + }, + button_txt: { + id: "5636734406623232", + key: "button_txt", + type: "string", + value: "Buy me" + }, + button_width: { + id: "6199684360044544", + key: "button_width", + type: "double", + value: "50.55" + }, + button_info: { + id: "1547854156498475", + key: "button_info", + type: "json", + value: "{ \"num_buttons\": 0, \"text\": \"default value\"}" + } + } + } + } + } + ], + [ + { + id: "595010", + key: "exp_with_group", + audiences: "", + variationsMap: { + var: { + featureEnabled: undefined, + id: "595008", + key: "var", + variablesMap: {} + }, + con: { + featureEnabled: undefined, + id: "595009", + key: "con", + variablesMap: {} + } + } + } + ], + [ + { + id: "599028", + key: "test_shared_feature", + audiences: "", + variationsMap: { + treatment: { + id: "599026", + key: "treatment", + featureEnabled: true, + variablesMap: { + lasers: { + id: "4937719889264640", + key: "lasers", + type: "integer", + value: "100" + }, + message: { + id: "6345094772817920", + key: "message", + type: "string", + value: "shared" + } + } + }, + control: { + id: "599027", + key: "control", + featureEnabled: false, + variablesMap: { + lasers: { + id: "4937719889264640", + key: "lasers", + type: "integer", + value: "100" + }, + message: { + id: "6345094772817920", + key: "message", + type: "string", + value: "shared" + } + } + } + } + } + ], + [], + [ + { + id: "12115595439", + key: "no_traffic_experiment", + audiences: "", + variationsMap: { + "variation_5000": { + "featureEnabled": undefined, + id: "12098126629", + key: "variation_5000", + variablesMap: {} + }, + "variation_10000": { + "featureEnabled": undefined, + id: "12098126630", + key: "variation_10000", + variablesMap: {} + } + } + } + ], + [ + { + id: "42222", + key: "group_2_exp_1", + audiences: "\"Test attribute users 3\"", + variationsMap: { + "var_1": { + id: "38901", + key: "var_1", + featureEnabled: false, + variablesMap: {} + } + } + }, { + id: "42223", + key: "group_2_exp_2", + audiences: "\"Test attribute users 3\"", + variationsMap: { + "var_1": { + id: "38905", + key: "var_1", + featureEnabled: false, + variablesMap: {} + } + } + }, { + id: "42224", + key: "group_2_exp_3", + audiences: "\"Test attribute users 3\"", + variationsMap: { + "var_1": { + id: "38906", + key: "var_1", + featureEnabled: false, + variablesMap: {} + } + } + } + ], + [ + { + id: "111134", + key: "test_experiment3", + audiences: "\"Test attribute users 3\"", + variationsMap: { + control: { + id: "222239", + key: "control", + featureEnabled: false, + variablesMap: {} + } + } + }, { + id: "111135", + key: "test_experiment4", + audiences: "\"Test attribute users 3\"", + variationsMap: { + control: { + id: "222240", + key: "control", + featureEnabled: false, + variablesMap: {} + } + } + }, { + id: "111136", + key: "test_experiment5", + audiences: "\"Test attribute users 3\"", + variationsMap: { + control: { + id: "222241", + key: "control", + featureEnabled: false, + variablesMap: {} + } + } + } + ] + ] + + datafile.featureFlags.forEach(function(featureFlag, index) { + assert.include(featuresMap[featureFlag.key], { + id: featureFlag.id, + key: featureFlag.key, + }); + featureFlag.experimentIds.forEach(function(experimentId) { + var experimentKey = projectConfigObject.experimentIdMap[experimentId].key; + assert.isTrue(!!featuresMap[featureFlag.key].experimentsMap[experimentKey]); + }); + var variablesMap = featuresMap[featureFlag.key].variablesMap; + var deliveryRules = featuresMap[featureFlag.key].deliveryRules; + var experimentRules = featuresMap[featureFlag.key].experimentRules; + assert.deepEqual(deliveryRules, expectedDeliveryRules[index]); + assert.deepEqual(experimentRules, expectedExperimentRules[index]); + featureFlag.variables.forEach(function(variable) { + // json is represented as sub type of string to support backwards compatibility in datafile. + // project config treats it as a first-class type. + var expectedVariableType = (variable.type === "string" && variable.subType === "json") ? "json" : variable.type; + assert.include(variablesMap[variable.key], { + id: variable.id, + key: variable.key, + type: expectedVariableType, + value: variable.defaultValue, + }); + }); + }); + }); + + it('should correctly merge all feature variables', function() { + var featureFlags = datafile.featureFlags; + var datafileExperimentsMap = getAllExperimentsFromDatafile(datafile).reduce(function(experiments, experiment) { + experiments[experiment.key] = experiment; + return experiments; + }, {}); + featureFlags.forEach(function(featureFlag) { + var experimentIds = featureFlag.experimentIds; + experimentIds.forEach(function(experimentId) { + var experimentKey = projectConfigObject.experimentIdMap[experimentId].key; + var experiment = optimizelyConfigObject.experimentsMap[experimentKey]; + var variations = datafileExperimentsMap[experimentKey].variations; + var variationsMap = experiment.variationsMap; + variations.forEach(function(variation) { + featureFlag.variables.forEach(function(variable) { + var variableToAssert = variationsMap[variation.key].variablesMap[variable.key]; + // json is represented as sub type of string to support backwards compatibility in datafile. + // project config treats it as a first-class type. + var expectedVariableType = (variable.type === "string" && variable.subType === "json") ? "json" : variable.type; + assert.include( + { + id: variable.id, + key: variable.key, + type: expectedVariableType, + }, + { + id: variableToAssert.id, + key: variableToAssert.key, + type: variableToAssert.type, + } + ); + if (!variation.featureEnabled) { + assert.equal(variable.defaultValue, variableToAssert.value); + } + }); + }); + }); + }); + }); + + it('should return correct config revision', function() { + assert.equal(optimizelyConfigObject.revision, datafile.revision); + }); + + it('should return correct config sdkKey ', function() { + assert.equal(optimizelyConfigObject.sdkKey, datafile.sdkKey); + }); + + it('should return correct config environmentKey ', function() { + assert.equal(optimizelyConfigObject.environmentKey, datafile.environmentKey); + }); + + it('should return serialized audiences', function () { + const audiencesById = projectTypedAudienceConfigObject.audiencesById; + const audienceConditions = [ + ['or', '3468206642', '3988293898'], + ['or', '3468206642', '3988293898', '3468206646'], + ['not', '3468206642'], + ['or', '3468206642'], + ['and', '3468206642'], + ['3468206642'], + ['3468206642', '3988293898'], + ['and', ['or', '3468206642', '3988293898'], '3468206646'], + [ + 'and', + ['or', '3468206642', ['and', '3988293898', '3468206646']], + ['and', '3988293899', ['or', '3468206647', '3468206643']], + ], + ['and', 'and'], + ['not', ['and', '3468206642', '3988293898']], + [], + ['or', '3468206642', '999999999'], + ]; + + const expectedAudienceOutputs = [ + '"exactString" OR "substringString"', + '"exactString" OR "substringString" OR "exactNumber"', + 'NOT "exactString"', + '"exactString"', + '"exactString"', + '"exactString"', + '"exactString" OR "substringString"', + '("exactString" OR "substringString") AND "exactNumber"', + '("exactString" OR ("substringString" AND "exactNumber")) AND ("exists" AND ("gtNumber" OR "exactBoolean"))', + '', + 'NOT ("exactString" AND "substringString")', + '', + '"exactString" OR "999999999"', + ]; + + for (let testNo = 0; testNo < audienceConditions.length; testNo++) { + const serializedAudiences = OptimizelyConfig.getSerializedAudiences(audienceConditions[testNo], audiencesById); + assert.equal(serializedAudiences, expectedAudienceOutputs[testNo]); + } + }); + + it('should return correct rollouts', function () { + const rolloutFlag1 = optimizelySimilarRuleKeyConfigObject.featuresMap['flag_1'].deliveryRules[0]; + const rolloutFlag2 = optimizelySimilarRuleKeyConfigObject.featuresMap['flag_2'].deliveryRules[0]; + const rolloutFlag3 = optimizelySimilarRuleKeyConfigObject.featuresMap['flag_3'].deliveryRules[0]; + + assert.equal(rolloutFlag1.id, '9300000004977'); + assert.equal(rolloutFlag1.key, 'targeted_delivery'); + assert.equal(rolloutFlag2.id, '9300000004979'); + assert.equal(rolloutFlag2.key, 'targeted_delivery'); + assert.equal(rolloutFlag3.id, '9300000004981'); + assert.equal(rolloutFlag3.key, 'targeted_delivery'); + + }); + + it('should return default SDK and environment key', function() { + + assert.equal(optimizelySimilarRuleKeyConfigObject.sdkKey, ""); + assert.equal(optimizelySimilarRuleKeyConfigObject.environmentKey, ""); + + }); + + it('should return correct experiments with similar keys', function() { + + assert.equal(Object.keys(optimizelySimilarExperimentkeyConfigObject.experimentsMap).length, 1); + const experimentMapFlag1 = optimizelySimilarExperimentkeyConfigObject.featuresMap["flag1"].experimentsMap; + const experimentMapFlag2 = optimizelySimilarExperimentkeyConfigObject.featuresMap["flag2"].experimentsMap; + assert.equal(experimentMapFlag1["targeted_delivery"].id, "9300000007569"); + assert.equal(experimentMapFlag2["targeted_delivery"].id, "9300000007573"); + + }); + }); +}); diff --git a/lib/project_config/project_config.tests.js b/lib/project_config/project_config.tests.js new file mode 100644 index 000000000..6e93327cc --- /dev/null +++ b/lib/project_config/project_config.tests.js @@ -0,0 +1,974 @@ +/** + * Copyright 2016-2024, Optimizely + * + * 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 + * + * https://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. + */ +import sinon from 'sinon'; +import { assert } from 'chai'; +import { forEach, cloneDeep } from 'lodash'; +import { sprintf } from '../utils/fns'; +import fns from '../utils/fns'; +import projectConfig from './project_config'; +import { FEATURE_VARIABLE_TYPES, LOG_LEVEL } from '../utils/enums'; +import testDatafile from '../tests/test_data'; +import configValidator from '../utils/config_validator'; +import { + INVALID_EXPERIMENT_ID, + INVALID_EXPERIMENT_KEY, + UNEXPECTED_RESERVED_ATTRIBUTE_PREFIX, + UNRECOGNIZED_ATTRIBUTE, + VARIABLE_KEY_NOT_IN_DATAFILE, + FEATURE_NOT_IN_DATAFILE, + UNABLE_TO_CAST_VALUE +} from 'error_message'; + +var createLogger = () => ({ + debug: () => {}, + info: () => {}, + warn: () => {}, + error: () => {}, + child: () => createLogger(), +}) + +var buildLogMessageFromArgs = args => sprintf(args[1], ...args.splice(2)); +var logger = createLogger(); + +describe('lib/core/project_config', function() { + describe('createProjectConfig method', function() { + it('should set properties correctly when createProjectConfig is called', function() { + var testData = testDatafile.getTestProjectConfig(); + var configObj = projectConfig.createProjectConfig(testData); + + forEach(testData.audiences, function(audience) { + audience.conditions = JSON.parse(audience.conditions); + }); + + assert.strictEqual(configObj.accountId, testData.accountId); + assert.strictEqual(configObj.projectId, testData.projectId); + assert.strictEqual(configObj.revision, testData.revision); + assert.deepEqual(configObj.events, testData.events); + assert.deepEqual(configObj.audiences, testData.audiences); + testData.groups.forEach(function(group) { + group.experiments.forEach(function(experiment) { + experiment.groupId = group.id; + experiment.variationKeyMap = fns.keyBy(experiment.variations, 'key'); + }); + }); + assert.deepEqual(configObj.groups, testData.groups); + + var expectedGroupIdMap = { + 666: testData.groups[0], + 667: testData.groups[1], + }; + + assert.deepEqual(configObj.groupIdMap, expectedGroupIdMap); + + var expectedExperiments = testData.experiments; + forEach(configObj.groupIdMap, function(group, Id) { + forEach(group.experiments, function(experiment) { + experiment.groupId = Id; + expectedExperiments.push(experiment); + }); + }); + + forEach(expectedExperiments, function(experiment) { + experiment.variationKeyMap = fns.keyBy(experiment.variations, 'key'); + }); + + assert.deepEqual(configObj.experiments, expectedExperiments); + + var expectedAttributeKeyMap = { + browser_type: testData.attributes[0], + boolean_key: testData.attributes[1], + integer_key: testData.attributes[2], + double_key: testData.attributes[3], + valid_positive_number: testData.attributes[4], + valid_negative_number: testData.attributes[5], + invalid_number: testData.attributes[6], + array: testData.attributes[7], + }; + + assert.deepEqual(configObj.attributeKeyMap, expectedAttributeKeyMap); + + var expectedExperimentKeyMap = { + testExperiment: configObj.experiments[0], + testExperimentWithAudiences: configObj.experiments[1], + testExperimentNotRunning: configObj.experiments[2], + testExperimentLaunched: configObj.experiments[3], + groupExperiment1: configObj.experiments[4], + groupExperiment2: configObj.experiments[5], + overlappingGroupExperiment1: configObj.experiments[6], + }; + + assert.deepEqual(configObj.experimentKeyMap, expectedExperimentKeyMap); + + var expectedEventKeyMap = { + testEvent: testData.events[0], + 'Total Revenue': testData.events[1], + testEventWithAudiences: testData.events[2], + testEventWithoutExperiments: testData.events[3], + testEventWithExperimentNotRunning: testData.events[4], + testEventWithMultipleExperiments: testData.events[5], + testEventLaunched: testData.events[6], + }; + + assert.deepEqual(configObj.eventKeyMap, expectedEventKeyMap); + + var expectedExperimentIdMap = { + '111127': configObj.experiments[0], + '122227': configObj.experiments[1], + '133337': configObj.experiments[2], + '144447': configObj.experiments[3], + '442': configObj.experiments[4], + '443': configObj.experiments[5], + '444': configObj.experiments[6], + }; + + assert.deepEqual(configObj.experimentIdMap, expectedExperimentIdMap); + + var expectedVariationKeyMap = {}; + expectedVariationKeyMap[testData.experiments[0].key + testData.experiments[0].variations[0].key] = + testData.experiments[0].variations[0]; + expectedVariationKeyMap[testData.experiments[0].key + testData.experiments[0].variations[1].key] = + testData.experiments[0].variations[1]; + expectedVariationKeyMap[testData.experiments[1].key + testData.experiments[1].variations[0].key] = + testData.experiments[1].variations[0]; + expectedVariationKeyMap[testData.experiments[1].key + testData.experiments[1].variations[1].key] = + testData.experiments[1].variations[1]; + expectedVariationKeyMap[testData.experiments[2].key + testData.experiments[2].variations[0].key] = + testData.experiments[2].variations[0]; + expectedVariationKeyMap[testData.experiments[2].key + testData.experiments[2].variations[1].key] = + testData.experiments[2].variations[1]; + expectedVariationKeyMap[configObj.experiments[3].key + configObj.experiments[3].variations[0].key] = + configObj.experiments[3].variations[0]; + expectedVariationKeyMap[configObj.experiments[3].key + configObj.experiments[3].variations[1].key] = + configObj.experiments[3].variations[1]; + expectedVariationKeyMap[configObj.experiments[4].key + configObj.experiments[4].variations[0].key] = + configObj.experiments[4].variations[0]; + expectedVariationKeyMap[configObj.experiments[4].key + configObj.experiments[4].variations[1].key] = + configObj.experiments[4].variations[1]; + expectedVariationKeyMap[configObj.experiments[5].key + configObj.experiments[5].variations[0].key] = + configObj.experiments[5].variations[0]; + expectedVariationKeyMap[configObj.experiments[5].key + configObj.experiments[5].variations[1].key] = + configObj.experiments[5].variations[1]; + expectedVariationKeyMap[configObj.experiments[6].key + configObj.experiments[6].variations[0].key] = + configObj.experiments[6].variations[0]; + expectedVariationKeyMap[configObj.experiments[6].key + configObj.experiments[6].variations[1].key] = + configObj.experiments[6].variations[1]; + + var expectedVariationIdMap = { + '111128': testData.experiments[0].variations[0], + '111129': testData.experiments[0].variations[1], + '122228': testData.experiments[1].variations[0], + '122229': testData.experiments[1].variations[1], + '133338': testData.experiments[2].variations[0], + '133339': testData.experiments[2].variations[1], + '144448': testData.experiments[3].variations[0], + '144449': testData.experiments[3].variations[1], + '551': configObj.experiments[4].variations[0], + '552': configObj.experiments[4].variations[1], + '661': configObj.experiments[5].variations[0], + '662': configObj.experiments[5].variations[1], + '553': configObj.experiments[6].variations[0], + '554': configObj.experiments[6].variations[1], + }; + }); + + it('should not mutate the datafile', function() { + var datafile = testDatafile.getTypedAudiencesConfig(); + var datafileClone = cloneDeep(datafile); + projectConfig.createProjectConfig(datafile); + assert.deepEqual(datafileClone, datafile); + }); + + describe('feature management', function() { + var configObj; + beforeEach(function() { + configObj = projectConfig.createProjectConfig(testDatafile.getTestProjectConfigWithFeatures()); + }); + + it('creates a rolloutIdMap from rollouts in the datafile', function() { + assert.deepEqual(configObj.rolloutIdMap, testDatafile.datafileWithFeaturesExpectedData.rolloutIdMap); + }); + + it('creates a variationVariableUsageMap from rollouts and experiments with features in the datafile', function() { + assert.deepEqual( + configObj.variationVariableUsageMap, + testDatafile.datafileWithFeaturesExpectedData.variationVariableUsageMap + ); + }); + + it('creates a featureKeyMap from feature flags in the datafile', function() { + assert.deepEqual(configObj.featureKeyMap, testDatafile.datafileWithFeaturesExpectedData.featureKeyMap); + }); + + it('adds variations from rollout experiments to variationIdMap', function() { + assert.deepEqual(configObj.variationIdMap['594032'], { + variables: [ + { value: 'true', id: '4919852825313280' }, + { value: '395', id: '5482802778734592' }, + { value: '4.99', id: '6045752732155904' }, + { value: 'Hello audience', id: '6327227708866560' }, + { value: '{ "count": 2, "message": "Hello audience" }', id: '8765345281230956' }, + ], + featureEnabled: true, + key: '594032', + id: '594032', + }); + assert.deepEqual(configObj.variationIdMap['594038'], { + variables: [ + { value: 'false', id: '4919852825313280' }, + { value: '400', id: '5482802778734592' }, + { value: '14.99', id: '6045752732155904' }, + { value: 'Hello', id: '6327227708866560' }, + { value: '{ "count": 1, "message": "Hello" }', id: '8765345281230956' }, + ], + featureEnabled: false, + key: '594038', + id: '594038', + }); + assert.deepEqual(configObj.variationIdMap['594061'], { + variables: [ + { value: '27.34', id: '5060590313668608' }, + { value: 'Winter is NOT coming', id: '5342065290379264' }, + { value: '10003', id: '6186490220511232' }, + { value: 'false', id: '6467965197221888' }, + ], + featureEnabled: true, + key: '594061', + id: '594061', + }); + assert.deepEqual(configObj.variationIdMap['594067'], { + variables: [ + { value: '30.34', id: '5060590313668608' }, + { value: 'Winter is coming definitely', id: '5342065290379264' }, + { value: '500', id: '6186490220511232' }, + { value: 'true', id: '6467965197221888' }, + ], + featureEnabled: true, + key: '594067', + id: '594067', + }); + }); + }); + + describe('flag variations', function() { + var configObj; + beforeEach(function() { + configObj = projectConfig.createProjectConfig(testDatafile.getTestDecideProjectConfig()); + }); + + it('it should populate flagVariationsMap correctly', function() { + var allVariationsForFlag = configObj.flagVariationsMap; + var feature1Variations = allVariationsForFlag.feature_1; + var feature2Variations = allVariationsForFlag.feature_2; + var feature3Variations = allVariationsForFlag.feature_3; + var feature1VariationsKeys = feature1Variations.map(variation => { + return variation.key; + }, {}); + var feature2VariationsKeys = feature2Variations.map(variation => { + return variation.key; + }, {}); + var feature3VariationsKeys = feature3Variations.map(variation => { + return variation.key; + }, {}); + + assert.deepEqual(feature1VariationsKeys, ['a', 'b', '3324490633', '3324490562', '18257766532']); + assert.deepEqual(feature2VariationsKeys, ['variation_with_traffic', 'variation_no_traffic']); + assert.deepEqual(feature3VariationsKeys, []); + }); + }); + }); + + describe('projectConfig helper methods', function() { + var testData = cloneDeep(testDatafile.getTestProjectConfig()); + var configObj; + var createdLogger = createLogger({ logLevel: LOG_LEVEL.INFO }); + + beforeEach(function() { + configObj = projectConfig.createProjectConfig(cloneDeep(testData)); + sinon.stub(createdLogger, 'warn'); + }); + + afterEach(function() { + createdLogger.warn.restore(); + }); + + it('should retrieve experiment ID for valid experiment key in getExperimentId', function() { + assert.strictEqual( + projectConfig.getExperimentId(configObj, testData.experiments[0].key), + testData.experiments[0].id + ); + }); + + it('should throw error for invalid experiment key in getExperimentId', function() { + const ex = assert.throws(function() { + projectConfig.getExperimentId(configObj, 'invalidExperimentKey'); + }); + assert.equal(ex.baseMessage, INVALID_EXPERIMENT_KEY); + assert.deepEqual(ex.params, ['invalidExperimentKey']); + }); + + it('should retrieve layer ID for valid experiment key in getLayerId', function() { + assert.strictEqual(projectConfig.getLayerId(configObj, '111127'), '4'); + }); + + it('should throw error for invalid experiment key in getLayerId', function() { + const ex = assert.throws(function() { + projectConfig.getLayerId(configObj, 'invalidExperimentKey'); + }); + assert.equal(ex.baseMessage, INVALID_EXPERIMENT_ID); + assert.deepEqual(ex.params, ['invalidExperimentKey']); + }); + + it('should retrieve attribute ID for valid attribute key in getAttributeId', function() { + assert.strictEqual(projectConfig.getAttributeId(configObj, 'browser_type'), '111094'); + }); + + it('should retrieve attribute ID for reserved attribute key in getAttributeId', function() { + assert.strictEqual(projectConfig.getAttributeId(configObj, '$opt_user_agent'), '$opt_user_agent'); + }); + + it('should return null for invalid attribute key in getAttributeId', function() { + assert.isNull(projectConfig.getAttributeId(configObj, 'invalidAttributeKey', createdLogger)); + + assert.deepEqual(createdLogger.warn.lastCall.args, [UNRECOGNIZED_ATTRIBUTE, 'invalidAttributeKey']); + }); + + it('should return null for invalid attribute key in getAttributeId', function() { + // Adding attribute in key map with reserved prefix + configObj.attributeKeyMap['$opt_some_reserved_attribute'] = { + id: '42', + key: '$opt_some_reserved_attribute', + }; + assert.strictEqual(projectConfig.getAttributeId(configObj, '$opt_some_reserved_attribute', createdLogger), '42'); + + assert.deepEqual(createdLogger.warn.lastCall.args, [UNEXPECTED_RESERVED_ATTRIBUTE_PREFIX, '$opt_some_reserved_attribute', '$opt_']); + }); + + it('should retrieve event ID for valid event key in getEventId', function() { + assert.strictEqual(projectConfig.getEventId(configObj, 'testEvent'), '111095'); + }); + + it('should return null for invalid event key in getEventId', function() { + assert.isNull(projectConfig.getEventId(configObj, 'invalidEventKey')); + }); + + it('should retrieve experiment status for valid experiment key in getExperimentStatus', function() { + assert.strictEqual( + projectConfig.getExperimentStatus(configObj, testData.experiments[0].key), + testData.experiments[0].status + ); + }); + + it('should throw error for invalid experiment key in getExperimentStatus', function() { + const ex = assert.throws(function() { + projectConfig.getExperimentStatus(configObj, 'invalidExperimentKey'); + }); + assert.equal(ex.baseMessage, INVALID_EXPERIMENT_KEY); + assert.deepEqual(ex.params, ['invalidExperimentKey']); + }); + + it('should return true if experiment status is set to Running in isActive', function() { + assert.isTrue(projectConfig.isActive(configObj, 'testExperiment')); + }); + + it('should return false if experiment status is not set to Running in isActive', function() { + assert.isFalse(projectConfig.isActive(configObj, 'testExperimentNotRunning')); + }); + + it('should return true if experiment status is set to Running in isRunning', function() { + assert.isTrue(projectConfig.isRunning(configObj, 'testExperiment')); + }); + + it('should return false if experiment status is not set to Running in isRunning', function() { + assert.isFalse(projectConfig.isRunning(configObj, 'testExperimentLaunched')); + }); + + it('should retrieve variation key for valid experiment key and variation ID in getVariationKeyFromId', function() { + assert.deepEqual( + projectConfig.getVariationKeyFromId(configObj, testData.experiments[0].variations[0].id), + testData.experiments[0].variations[0].key + ); + }); + + it('should retrieve traffic allocation given valid experiment key in getTrafficAllocation', function() { + assert.deepEqual( + projectConfig.getTrafficAllocation(configObj, testData.experiments[0].id), + testData.experiments[0].trafficAllocation + ); + }); + + it('should throw error for invalid experient key in getTrafficAllocation', function() { + const ex = assert.throws(function() { + projectConfig.getTrafficAllocation(configObj, 'invalidExperimentId'); + }); + assert.equal(ex.baseMessage, INVALID_EXPERIMENT_ID); + assert.deepEqual(ex.params, ['invalidExperimentId']); + }); + + describe('#getVariationIdFromExperimentAndVariationKey', function() { + it('should return the variation id for the given experiment key and variation key', function() { + assert.strictEqual( + projectConfig.getVariationIdFromExperimentAndVariationKey( + configObj, + testData.experiments[0].key, + testData.experiments[0].variations[0].key + ), + testData.experiments[0].variations[0].id + ); + }); + }); + + describe('#getSendFlagDecisionsValue', function() { + it('should return false when sendFlagDecisions is undefined', function() { + configObj.sendFlagDecisions = undefined; + assert.deepEqual(projectConfig.getSendFlagDecisionsValue(configObj), false); + }); + + it('should return false when sendFlagDecisions is set to false', function() { + configObj.sendFlagDecisions = false; + assert.deepEqual(projectConfig.getSendFlagDecisionsValue(configObj), false); + }); + + it('should return true when sendFlagDecisions is set to true', function() { + configObj.sendFlagDecisions = true; + assert.deepEqual(projectConfig.getSendFlagDecisionsValue(configObj), true); + }); + }); + + describe('feature management', function() { + var featureManagementLogger = createLogger({ logLevel: LOG_LEVEL.INFO }); + beforeEach(function() { + configObj = projectConfig.createProjectConfig(testDatafile.getTestProjectConfigWithFeatures()); + sinon.stub(featureManagementLogger, 'warn'); + sinon.stub(featureManagementLogger, 'error'); + sinon.stub(featureManagementLogger, 'info'); + sinon.stub(featureManagementLogger, 'debug'); + }); + + afterEach(function() { + featureManagementLogger.warn.restore(); + featureManagementLogger.error.restore(); + featureManagementLogger.info.restore(); + featureManagementLogger.debug.restore(); + }); + + describe('getVariableForFeature', function() { + it('should return a variable object for a valid variable and feature key', function() { + var featureKey = 'test_feature_for_experiment'; + var variableKey = 'num_buttons'; + var result = projectConfig.getVariableForFeature(configObj, featureKey, variableKey, featureManagementLogger); + assert.deepEqual(result, { + type: 'integer', + key: 'num_buttons', + id: '4792309476491264', + defaultValue: '10', + }); + }); + + it('should return null for an invalid variable key and a valid feature key', function() { + var featureKey = 'test_feature_for_experiment'; + var variableKey = 'notARealVariable____'; + var result = projectConfig.getVariableForFeature(configObj, featureKey, variableKey, featureManagementLogger); + assert.strictEqual(result, null); + sinon.assert.calledOnce(featureManagementLogger.error); + + assert.deepEqual(featureManagementLogger.error.lastCall.args, [VARIABLE_KEY_NOT_IN_DATAFILE, 'notARealVariable____', 'test_feature_for_experiment']); + }); + + it('should return null for an invalid feature key', function() { + var featureKey = 'notARealFeature_____'; + var variableKey = 'num_buttons'; + var result = projectConfig.getVariableForFeature(configObj, featureKey, variableKey, featureManagementLogger); + assert.strictEqual(result, null); + sinon.assert.calledOnce(featureManagementLogger.error); + + assert.deepEqual(featureManagementLogger.error.lastCall.args, [FEATURE_NOT_IN_DATAFILE, 'notARealFeature_____']); + }); + + it('should return null for an invalid variable key and an invalid feature key', function() { + var featureKey = 'notARealFeature_____'; + var variableKey = 'notARealVariable____'; + var result = projectConfig.getVariableForFeature(configObj, featureKey, variableKey, featureManagementLogger); + assert.strictEqual(result, null); + sinon.assert.calledOnce(featureManagementLogger.error); + + assert.deepEqual(featureManagementLogger.error.lastCall.args, [FEATURE_NOT_IN_DATAFILE, 'notARealFeature_____']); + }); + }); + + describe('getVariableValueForVariation', function() { + it('returns a value for a valid variation and variable', function() { + var variation = configObj.variationIdMap['594096']; + var variable = configObj.featureKeyMap.test_feature_for_experiment.variableKeyMap.num_buttons; + var result = projectConfig.getVariableValueForVariation( + configObj, + variable, + variation, + featureManagementLogger + ); + assert.strictEqual(result, '2'); + + variable = configObj.featureKeyMap.test_feature_for_experiment.variableKeyMap.is_button_animated; + result = projectConfig.getVariableValueForVariation(configObj, variable, variation, featureManagementLogger); + assert.strictEqual(result, 'true'); + + variable = configObj.featureKeyMap.test_feature_for_experiment.variableKeyMap.button_txt; + result = projectConfig.getVariableValueForVariation(configObj, variable, variation, featureManagementLogger); + assert.strictEqual(result, 'Buy me NOW'); + + variable = configObj.featureKeyMap.test_feature_for_experiment.variableKeyMap.button_width; + result = projectConfig.getVariableValueForVariation(configObj, variable, variation, featureManagementLogger); + assert.strictEqual(result, '20.25'); + }); + + it('returns null for a null variation', function() { + var variation = null; + var variable = configObj.featureKeyMap.test_feature_for_experiment.variableKeyMap.num_buttons; + var result = projectConfig.getVariableValueForVariation( + configObj, + variable, + variation, + featureManagementLogger + ); + assert.strictEqual(result, null); + }); + + it('returns null for a null variable', function() { + var variation = configObj.variationIdMap['594096']; + var variable = null; + var result = projectConfig.getVariableValueForVariation( + configObj, + variable, + variation, + featureManagementLogger + ); + assert.strictEqual(result, null); + }); + + it('returns null for a null variation and null variable', function() { + var variation = null; + var variable = null; + var result = projectConfig.getVariableValueForVariation( + configObj, + variable, + variation, + featureManagementLogger + ); + assert.strictEqual(result, null); + }); + + it('returns null for a variation whose id is not in the datafile', function() { + var variation = { + key: 'some_variation', + id: '999999999999', + variables: [], + }; + var variable = configObj.featureKeyMap.test_feature_for_experiment.variableKeyMap.num_buttons; + var result = projectConfig.getVariableValueForVariation( + configObj, + variable, + variation, + featureManagementLogger + ); + assert.strictEqual(result, null); + }); + + it('returns null if the variation does not have a value for this variable', function() { + var variation = configObj.variationIdMap['595008']; // This variation has no variable values associated with it + var variable = configObj.featureKeyMap.test_feature_for_experiment.variableKeyMap.num_buttons; + var result = projectConfig.getVariableValueForVariation( + configObj, + variable, + variation, + featureManagementLogger + ); + assert.isNull(result); + }); + }); + + describe('getTypeCastValue', function() { + it('can cast a boolean', function() { + var result = projectConfig.getTypeCastValue('true', FEATURE_VARIABLE_TYPES.BOOLEAN, featureManagementLogger); + assert.strictEqual(result, true); + result = projectConfig.getTypeCastValue('false', FEATURE_VARIABLE_TYPES.BOOLEAN, featureManagementLogger); + assert.strictEqual(result, false); + }); + + it('can cast an integer', function() { + var result = projectConfig.getTypeCastValue('50', FEATURE_VARIABLE_TYPES.INTEGER, featureManagementLogger); + assert.strictEqual(result, 50); + var result = projectConfig.getTypeCastValue('-7', FEATURE_VARIABLE_TYPES.INTEGER, featureManagementLogger); + assert.strictEqual(result, -7); + var result = projectConfig.getTypeCastValue('0', FEATURE_VARIABLE_TYPES.INTEGER, featureManagementLogger); + assert.strictEqual(result, 0); + }); + + it('can cast a double', function() { + var result = projectConfig.getTypeCastValue('89.99', FEATURE_VARIABLE_TYPES.DOUBLE, featureManagementLogger); + assert.strictEqual(result, 89.99); + var result = projectConfig.getTypeCastValue( + '-257.21', + FEATURE_VARIABLE_TYPES.DOUBLE, + featureManagementLogger + ); + assert.strictEqual(result, -257.21); + var result = projectConfig.getTypeCastValue('0', FEATURE_VARIABLE_TYPES.DOUBLE, featureManagementLogger); + assert.strictEqual(result, 0); + var result = projectConfig.getTypeCastValue('10', FEATURE_VARIABLE_TYPES.DOUBLE, featureManagementLogger); + assert.strictEqual(result, 10); + }); + + it('can return a string unmodified', function() { + var result = projectConfig.getTypeCastValue( + 'message', + FEATURE_VARIABLE_TYPES.STRING, + featureManagementLogger + ); + assert.strictEqual(result, 'message'); + }); + + it('returns null and logs an error for an invalid boolean', function() { + var result = projectConfig.getTypeCastValue( + 'notabool', + FEATURE_VARIABLE_TYPES.BOOLEAN, + featureManagementLogger + ); + assert.strictEqual(result, null); + + assert.deepEqual(featureManagementLogger.error.lastCall.args, [UNABLE_TO_CAST_VALUE, 'notabool', 'boolean']); + }); + + it('returns null and logs an error for an invalid integer', function() { + var result = projectConfig.getTypeCastValue( + 'notanint', + FEATURE_VARIABLE_TYPES.INTEGER, + featureManagementLogger + ); + assert.strictEqual(result, null); + + assert.deepEqual(featureManagementLogger.error.lastCall.args, [UNABLE_TO_CAST_VALUE, 'notanint', 'integer']); + }); + + it('returns null and logs an error for an invalid double', function() { + var result = projectConfig.getTypeCastValue( + 'notadouble', + FEATURE_VARIABLE_TYPES.DOUBLE, + featureManagementLogger + ); + assert.strictEqual(result, null); + + assert.deepEqual(featureManagementLogger.error.lastCall.args, [UNABLE_TO_CAST_VALUE, 'notadouble', 'double']); + }); + }); + }); + + describe('#getAudiencesById', function() { + beforeEach(function() { + configObj = projectConfig.createProjectConfig(testDatafile.getTypedAudiencesConfig()); + }); + + it('should retrieve audiences by checking first in typedAudiences, and then second in audiences', function() { + assert.deepEqual(projectConfig.getAudiencesById(configObj), testDatafile.typedAudiencesById); + }); + }); + + describe('#getExperimentAudienceConditions', function() { + it('should retrieve audiences for valid experiment key', function() { + configObj = projectConfig.createProjectConfig(cloneDeep(testData)); + assert.deepEqual(projectConfig.getExperimentAudienceConditions(configObj, testData.experiments[1].id), [ + '11154', + ]); + }); + + it('should throw error for invalid experiment key', function() { + configObj = projectConfig.createProjectConfig(cloneDeep(testData)); + const ex = assert.throws(function() { + projectConfig.getExperimentAudienceConditions(configObj, 'invalidExperimentId'); + }); + assert.equal(ex.baseMessage, INVALID_EXPERIMENT_ID); + assert.deepEqual(ex.params, ['invalidExperimentId']); + }); + + it('should return experiment audienceIds if experiment has no audienceConditions', function() { + configObj = projectConfig.createProjectConfig(testDatafile.getTypedAudiencesConfig()); + var result = projectConfig.getExperimentAudienceConditions(configObj, '11564051718'); + assert.deepEqual(result, [ + '3468206642', + '3988293898', + '3988293899', + '3468206646', + '3468206647', + '3468206644', + '3468206643', + ]); + }); + + it('should return experiment audienceConditions if experiment has audienceConditions', function() { + configObj = projectConfig.createProjectConfig(testDatafile.getTypedAudiencesConfig()); + // audience_combinations_experiment has both audienceConditions and audienceIds + // audienceConditions should be preferred over audienceIds + var result = projectConfig.getExperimentAudienceConditions(configObj, '1323241598'); + assert.deepEqual(result, [ + 'and', + ['or', '3468206642', '3988293898'], + ['or', '3988293899', '3468206646', '3468206647', '3468206644', '3468206643'], + ]); + }); + }); + + describe('#isFeatureExperiment', function() { + it('returns true for a feature test', function() { + var config = projectConfig.createProjectConfig(testDatafile.getTestProjectConfigWithFeatures()); + var result = projectConfig.isFeatureExperiment(config, '594098'); // id of 'testing_my_feature' + assert.isTrue(result); + }); + + it('returns false for an A/B test', function() { + var config = projectConfig.createProjectConfig(testDatafile.getTestProjectConfig()); + var result = projectConfig.isFeatureExperiment(config, '111127'); // id of 'testExperiment' + assert.isFalse(result); + }); + + it('returns true for a feature test in a mutex group', function() { + var config = projectConfig.createProjectConfig(testDatafile.getMutexFeatureTestsConfig()); + var result = projectConfig.isFeatureExperiment(config, '17128410791'); // id of 'f_test1' + assert.isTrue(result); + result = projectConfig.isFeatureExperiment(config, '17139931304'); // id of 'f_test2' + assert.isTrue(result); + }); + }); + + describe('#getAudienceSegments', function() { + it('returns all qualified segments from an audience', function() { + const dummyQualifiedAudienceJson = { + id: '13389142234', + conditions: [ + 'and', + [ + 'or', + [ + 'or', + { + value: 'odp-segment-1', + type: 'third_party_dimension', + name: 'odp.audiences', + match: 'qualified', + }, + ], + ], + ], + name: 'odp-segment-1', + }; + + const dummyQualifiedAudienceJsonSegments = projectConfig.getAudienceSegments(dummyQualifiedAudienceJson); + assert.deepEqual(dummyQualifiedAudienceJsonSegments, ['odp-segment-1']); + + const dummyUnqualifiedAudienceJson = { + id: '13389142234', + conditions: [ + 'and', + [ + 'or', + [ + 'or', + { + value: 'odp-segment-1', + type: 'third_party_dimension', + name: 'odp.audiences', + match: 'invalid', + }, + ], + ], + ], + name: 'odp-segment-1', + }; + + const dummyUnqualifiedAudienceJsonSegments = projectConfig.getAudienceSegments(dummyUnqualifiedAudienceJson); + assert.deepEqual(dummyUnqualifiedAudienceJsonSegments, []); + }); + + it('returns false for an A/B test', function() { + var config = projectConfig.createProjectConfig(testDatafile.getTestProjectConfig()); + var result = projectConfig.isFeatureExperiment(config, '111127'); // id of 'testExperiment' + assert.isFalse(result); + }); + + it('returns true for a feature test in a mutex group', function() { + var config = projectConfig.createProjectConfig(testDatafile.getMutexFeatureTestsConfig()); + var result = projectConfig.isFeatureExperiment(config, '17128410791'); // id of 'f_test1' + assert.isTrue(result); + result = projectConfig.isFeatureExperiment(config, '17139931304'); // id of 'f_test2' + assert.isTrue(result); + }); + }); + }); + + describe('integrations', () => { + describe('#withSegments', () => { + var config; + beforeEach(() => { + config = projectConfig.createProjectConfig(testDatafile.getOdpIntegratedConfigWithSegments()); + }); + + it('should convert integrations from the datafile into the project config', () => { + assert.exists(config.integrations); + assert.equal(config.integrations.length, 4); + }); + + it('should populate odpIntegrationConfig', () => { + assert.isTrue(config.odpIntegrationConfig.integrated); + assert.equal(config.odpIntegrationConfig.odpConfig.apiKey, 'W4WzcEs-ABgXorzY7h1LCQ'); + assert.equal(config.odpIntegrationConfig.odpConfig.apiHost, 'https://api.zaius.com'); + assert.equal(config.odpIntegrationConfig.odpConfig.pixelUrl, 'https://jumbe.zaius.com'); + assert.deepEqual(config.odpIntegrationConfig.odpConfig.segmentsToCheck, ['odp-segment-1', 'odp-segment-2', 'odp-segment-3']); + }); + }); + + describe('#withoutSegments', () => { + var config; + beforeEach(() => { + config = projectConfig.createProjectConfig(testDatafile.getOdpIntegratedConfigWithoutSegments()); + }); + + it('should convert integrations from the datafile into the project config', () => { + assert.exists(config.integrations); + assert.equal(config.integrations.length, 3); + }); + + it('should populate odpIntegrationConfig', () => { + assert.isTrue(config.odpIntegrationConfig.integrated); + assert.equal(config.odpIntegrationConfig.odpConfig.apiKey, 'W4WzcEs-ABgXorzY7h1LCQ'); + assert.equal(config.odpIntegrationConfig.odpConfig.apiHost, 'https://api.zaius.com'); + assert.equal(config.odpIntegrationConfig.odpConfig.pixelUrl, 'https://jumbe.zaius.com'); + assert.deepEqual(config.odpIntegrationConfig.odpConfig.segmentsToCheck, []); + }); + }); + + describe('#withoutValidIntegrationKey', () => { + it('should throw an error when parsing the project config due to integrations not containing a key', () => { + const odpIntegratedConfigWithoutKey = testDatafile.getOdpIntegratedConfigWithoutKey(); + assert.throws(() => { + projectConfig.createProjectConfig(odpIntegratedConfigWithoutKey); + }); + }); + }); + + describe('#withoutIntegrations', () => { + var config; + beforeEach(() => { + const odpIntegratedConfigWithSegments = testDatafile.getOdpIntegratedConfigWithSegments(); + const noIntegrationsConfigWithSegments = { ...odpIntegratedConfigWithSegments, integrations: [] }; + config = projectConfig.createProjectConfig(noIntegrationsConfigWithSegments); + }); + + it('should convert integrations from the datafile into the project config', () => { + assert.equal(config.integrations.length, 0); + }); + + it('should populate odpIntegrationConfig', () => { + assert.isFalse(config.odpIntegrationConfig.integrated); + assert.isUndefined(config.odpIntegrationConfig.odpConfig); + }); + }); + }); +}); + +describe('#tryCreatingProjectConfig', function() { + var stubJsonSchemaValidator; + beforeEach(function() { + stubJsonSchemaValidator = sinon.stub().returns(true); + sinon.stub(configValidator, 'validateDatafile').returns(true); + sinon.spy(logger, 'error'); + }); + + afterEach(function() { + configValidator.validateDatafile.restore(); + logger.error.restore(); + }); + + it('returns a project config object created by createProjectConfig when all validation is applied and there are no errors', function() { + var configDatafile = { + foo: 'bar', + experiments: [{ key: 'a' }, { key: 'b' }], + }; + configValidator.validateDatafile.returns(configDatafile); + var configObj = { + foo: 'bar', + experimentKeyMap: { + a: { key: 'a', variationKeyMap: {} }, + b: { key: 'b', variationKeyMap: {} }, + }, + }; + + stubJsonSchemaValidator.returns(true); + + var result = projectConfig.tryCreatingProjectConfig({ + datafile: configDatafile, + jsonSchemaValidator: stubJsonSchemaValidator, + logger: logger, + }); + + assert.deepInclude(result, configObj); + }); + + it('throws an error when validateDatafile throws', function() { + configValidator.validateDatafile.throws(); + stubJsonSchemaValidator.returns(true); + assert.throws(() => { + projectConfig.tryCreatingProjectConfig({ + datafile: { foo: 'bar' }, + jsonSchemaValidator: stubJsonSchemaValidator, + logger: logger, + }); + }); + }); + + it('throws an error when jsonSchemaValidator.validate throws', function() { + configValidator.validateDatafile.returns(true); + stubJsonSchemaValidator.throws(); + assert.throws(() => { + projectConfig.tryCreatingProjectConfig({ + datafile: { foo: 'bar' }, + jsonSchemaValidator: stubJsonSchemaValidator, + logger: logger, + }); + }); + }); + + it('skips json validation when jsonSchemaValidator is not provided', function() { + var configDatafile = { + foo: 'bar', + experiments: [{ key: 'a' }, { key: 'b' }], + }; + + configValidator.validateDatafile.returns(configDatafile); + + var configObj = { + foo: 'bar', + experimentKeyMap: { + a: { key: 'a', variationKeyMap: {} }, + b: { key: 'b', variationKeyMap: {} }, + }, + }; + + var result = projectConfig.tryCreatingProjectConfig({ + datafile: configDatafile, + logger: logger, + }); + + assert.deepInclude(result, configObj); + sinon.assert.notCalled(logger.error); + }); +}); From 5b7f342e11395063686e9f5dbd8b11db7b04f1c9 Mon Sep 17 00:00:00 2001 From: Md Junaed Hossain <169046794+junaed-optimizely@users.noreply.github.com> Date: Fri, 7 Feb 2025 21:53:56 +0600 Subject: [PATCH 8/9] [FSSDK-11034] review fix --- lib/project_config/project_config.spec.ts | 22 +++++++++++----------- package-lock.json | 7 ------- package.json | 1 - 3 files changed, 11 insertions(+), 19 deletions(-) diff --git a/lib/project_config/project_config.spec.ts b/lib/project_config/project_config.spec.ts index 73ca7c054..2ab002bca 100644 --- a/lib/project_config/project_config.spec.ts +++ b/lib/project_config/project_config.spec.ts @@ -14,9 +14,8 @@ * limitations under the License. */ import { describe, it, expect, beforeEach, afterEach, vi, assert, Mock } from 'vitest'; -import { forEach, cloneDeep } from 'lodash'; import { sprintf } from '../utils/fns'; -import fns from '../utils/fns'; +import { keyBy } from '../utils/fns'; import projectConfig, { ProjectConfig } from './project_config'; import { FEATURE_VARIABLE_TYPES, LOG_LEVEL } from '../utils/enums'; import testDatafile from '../tests/test_data'; @@ -42,6 +41,7 @@ const createLogger = (...args: any) => ({ }); const buildLogMessageFromArgs = (args: any[]) => sprintf(args[1], ...args.splice(2)); +const cloneDeep = (obj: any) => JSON.parse(JSON.stringify(obj)); const logger = createLogger(); describe('createProjectConfig', () => { @@ -51,7 +51,7 @@ describe('createProjectConfig', () => { const testData: Record = testDatafile.getTestProjectConfig(); configObj = projectConfig.createProjectConfig(testData as JSON); - forEach(testData.audiences, (audience: any) => { + testData.audiences.forEach((audience: any) => { audience.conditions = JSON.parse(audience.conditions); }); @@ -64,7 +64,7 @@ describe('createProjectConfig', () => { testData.groups.forEach((group: any) => { group.experiments.forEach((experiment: any) => { experiment.groupId = group.id; - experiment.variationKeyMap = fns.keyBy(experiment.variations, 'key'); + experiment.variationKeyMap = keyBy(experiment.variations, 'key'); }); }); @@ -78,16 +78,16 @@ describe('createProjectConfig', () => { expect(configObj.groupIdMap).toEqual(expectedGroupIdMap); const expectedExperiments = testData.experiments.slice(); - forEach(configObj.groupIdMap, (group: any, groupId: any) => { - forEach(group.experiments, (experiment: any) => { + + Object.entries(configObj.groupIdMap).forEach(([groupId, group]) => { + group.experiments.forEach((experiment: any) => { experiment.groupId = groupId; expectedExperiments.push(experiment); }); - }); - - forEach(expectedExperiments, (experiment: any) => { - experiment.variationKeyMap = fns.keyBy(experiment.variations, 'key'); - }); + }) + expectedExperiments.forEach((experiment: any) => { + experiment.variationKeyMap = keyBy(experiment.variations, 'key'); + }) expect(configObj.experiments).toEqual(expectedExperiments); diff --git a/package-lock.json b/package-lock.json index c3c12d2d3..4cfbad348 100644 --- a/package-lock.json +++ b/package-lock.json @@ -21,7 +21,6 @@ "@rollup/plugin-commonjs": "^11.0.2", "@rollup/plugin-node-resolve": "^7.1.1", "@types/chai": "^4.2.11", - "@types/lodash": "^4.17.15", "@types/mocha": "^5.2.7", "@types/nise": "^1.4.0", "@types/node": "^18.7.18", @@ -5155,12 +5154,6 @@ "integrity": "sha512-RbSSoHliUbnXj3ny0CNFOoxrIDV6SUGyStHsvDqosw6CkdPV8TtWGlfecuK4ToyMEAql6pzNxgCFKanovUzlgQ==", "dev": true }, - "node_modules/@types/lodash": { - "version": "4.17.15", - "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.17.15.tgz", - "integrity": "sha512-w/P33JFeySuhN6JLkysYUK2gEmy9kHHFN7E8ro0tkfmlDOgxBDzWEZ/J8cWA+fHqFevpswDTFZnDx+R9lbL6xw==", - "dev": true - }, "node_modules/@types/mocha": { "version": "5.2.7", "resolved": "https://registry.npmjs.org/@types/mocha/-/mocha-5.2.7.tgz", diff --git a/package.json b/package.json index 8797f4d92..2d97998df 100644 --- a/package.json +++ b/package.json @@ -119,7 +119,6 @@ "@rollup/plugin-commonjs": "^11.0.2", "@rollup/plugin-node-resolve": "^7.1.1", "@types/chai": "^4.2.11", - "@types/lodash": "^4.17.15", "@types/mocha": "^5.2.7", "@types/nise": "^1.4.0", "@types/node": "^18.7.18", From ef5ab3b044af2a4f0345106ab50c1d1d0d0ef5dd Mon Sep 17 00:00:00 2001 From: Md Junaed Hossain <169046794+junaed-optimizely@users.noreply.github.com> Date: Fri, 7 Feb 2025 21:57:20 +0600 Subject: [PATCH 9/9] [FSSDK-11034] review fix --- lib/project_config/optimizely_config.spec.ts | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/lib/project_config/optimizely_config.spec.ts b/lib/project_config/optimizely_config.spec.ts index c9c3e3c17..3e7288a8e 100644 --- a/lib/project_config/optimizely_config.spec.ts +++ b/lib/project_config/optimizely_config.spec.ts @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { describe, it, expect, beforeEach, afterEach, vi, assert, Mock } from 'vitest'; +import { describe, it, expect, beforeEach, vi, assert } from 'vitest'; import { createOptimizelyConfig, OptimizelyConfig } from './optimizely_config'; import { createProjectConfig, ProjectConfig } from './project_config'; import { @@ -23,7 +23,6 @@ import { getSimilarExperimentKeyConfig, getDuplicateExperimentKeyConfig, } from '../tests/test_data'; -import { cloneDeep } from 'lodash'; import { Experiment } from '../shared_types'; import { LoggerFacade } from '../logging/logger'; @@ -31,7 +30,7 @@ const datafile: ProjectConfig = getTestProjectConfigWithFeatures(); const typedAudienceDatafile = getTypedAudiencesConfig(); const similarRuleKeyDatafile = getSimilarRuleKeyConfig(); const similarExperimentKeyDatafile = getSimilarExperimentKeyConfig(); - +const cloneDeep = (obj: any) => JSON.parse(JSON.stringify(obj)); const getAllExperimentsFromDatafile = (datafile: ProjectConfig) => { const allExperiments: Experiment[] = []; datafile.groups.forEach(group => { @@ -48,7 +47,6 @@ const getAllExperimentsFromDatafile = (datafile: ProjectConfig) => { describe('Optimizely Config', () => { let optimizelyConfigObject: OptimizelyConfig; let projectConfigObject: ProjectConfig; - let optimizelyTypedAudienceConfigObject; let projectTypedAudienceConfigObject: ProjectConfig; let optimizelySimilarRuleKeyConfigObject: OptimizelyConfig; let projectSimilarRuleKeyConfigObject: ProjectConfig; @@ -67,10 +65,6 @@ describe('Optimizely Config', () => { projectConfigObject = createProjectConfig(cloneDeep(datafile as any)); optimizelyConfigObject = createOptimizelyConfig(projectConfigObject, JSON.stringify(datafile)); projectTypedAudienceConfigObject = createProjectConfig(cloneDeep(typedAudienceDatafile)); - optimizelyTypedAudienceConfigObject = createOptimizelyConfig( - projectTypedAudienceConfigObject, - JSON.stringify(typedAudienceDatafile) - ); projectSimilarRuleKeyConfigObject = createProjectConfig(cloneDeep(similarRuleKeyDatafile)); optimizelySimilarRuleKeyConfigObject = createOptimizelyConfig( projectSimilarRuleKeyConfigObject,