diff --git a/lib/core/bucketer/index.spec.ts b/lib/core/bucketer/index.spec.ts index 4a6998597..a00c860f0 100644 --- a/lib/core/bucketer/index.spec.ts +++ b/lib/core/bucketer/index.spec.ts @@ -74,7 +74,7 @@ describe('excluding groups', () => { vi.clearAllMocks(); setLogSpy(mockLogger); - configObj = projectConfig.createProjectConfig(cloneDeep(testData)); + configObj = projectConfig.createProjectConfig(JSON.stringify(testData)); // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore @@ -125,7 +125,7 @@ describe('including groups: random', () => { vi.clearAllMocks(); setLogSpy(mockLogger); - configObj = projectConfig.createProjectConfig(cloneDeep(testData)); + configObj = projectConfig.createProjectConfig(JSON.stringify(testData)); // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore bucketerParams = { @@ -231,7 +231,7 @@ describe('including groups: overlapping', () => { vi.clearAllMocks(); setLogSpy(mockLogger); - configObj = projectConfig.createProjectConfig(cloneDeep(testData)); + configObj = projectConfig.createProjectConfig(JSON.stringify(testData)); // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore bucketerParams = { @@ -276,7 +276,7 @@ describe('bucket value falls into empty traffic allocation ranges', () => { beforeEach(() => { setLogSpy(mockLogger); - configObj = projectConfig.createProjectConfig(cloneDeep(testData)); + configObj = projectConfig.createProjectConfig(JSON.stringify(testData)); // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore bucketerParams = { @@ -326,7 +326,7 @@ describe('traffic allocation has invalid variation ids', () => { beforeEach(() => { setLogSpy(mockLogger); - configObj = projectConfig.createProjectConfig(cloneDeep(testData)); + configObj = projectConfig.createProjectConfig(JSON.stringify(testData)); // eslint-disable-next-line @typescript-eslint/ban-ts-comment //@ts-ignore bucketerParams = { @@ -369,7 +369,7 @@ describe('testBucketWithBucketingId', () => { let bucketerParams: BucketerParams; beforeEach(() => { - const configObj = projectConfig.createProjectConfig(cloneDeep(testData)); + const configObj = projectConfig.createProjectConfig(JSON.stringify(testData)); // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore bucketerParams = { diff --git a/lib/core/bucketer/index.tests.js b/lib/core/bucketer/index.tests.js index a1e046088..976ac82ab 100644 --- a/lib/core/bucketer/index.tests.js +++ b/lib/core/bucketer/index.tests.js @@ -65,7 +65,7 @@ describe('lib/core/bucketer', function () { describe('return values for bucketing (excluding groups)', function () { beforeEach(function () { - configObj = projectConfig.createProjectConfig(cloneDeep(testData)); + configObj = projectConfig.createProjectConfig(JSON.stringify(testData)); bucketerParams = { experimentId: configObj.experiments[0].id, experimentKey: configObj.experiments[0].key, @@ -107,7 +107,7 @@ describe('lib/core/bucketer', function () { describe('return values for bucketing (including groups)', function () { var bucketerStub; beforeEach(function () { - configObj = projectConfig.createProjectConfig(cloneDeep(testData)); + configObj = projectConfig.createProjectConfig(JSON.stringify(testData)); bucketerParams = { experimentId: configObj.experiments[0].id, experimentKey: configObj.experiments[0].key, @@ -255,7 +255,7 @@ describe('lib/core/bucketer', function () { describe('when the bucket value falls into empty traffic allocation ranges', function () { beforeEach(function () { - configObj = projectConfig.createProjectConfig(cloneDeep(testData)); + configObj = projectConfig.createProjectConfig(JSON.stringify(testData)); bucketerParams = { experimentId: configObj.experiments[0].id, experimentKey: configObj.experiments[0].key, @@ -303,7 +303,7 @@ describe('lib/core/bucketer', function () { describe('when the traffic allocation has invalid variation ids', function () { beforeEach(function () { - configObj = projectConfig.createProjectConfig(cloneDeep(testData)); + configObj = projectConfig.createProjectConfig(JSON.stringify(testData)); bucketerParams = { experimentId: configObj.experiments[0].id, experimentKey: configObj.experiments[0].key, @@ -364,7 +364,7 @@ describe('lib/core/bucketer', function () { }); beforeEach(function () { - var configObj = projectConfig.createProjectConfig(cloneDeep(testData)); + var configObj = projectConfig.createProjectConfig(JSON.stringify(testData)); bucketerParams = { trafficAllocationConfig: configObj.experiments[0].trafficAllocation, variationIdMap: configObj.variationIdMap, diff --git a/lib/core/decision_service/index.spec.ts b/lib/core/decision_service/index.spec.ts index d3b405284..2334a386b 100644 --- a/lib/core/decision_service/index.spec.ts +++ b/lib/core/decision_service/index.spec.ts @@ -212,7 +212,7 @@ describe('DecisionService', () => { userId: 'tester' }); - const config = createProjectConfig(cloneDeep(testData)); + const config = createProjectConfig(JSON.stringify(testData)); const fakeDecisionResponse = { result: '111128', @@ -242,7 +242,7 @@ describe('DecisionService', () => { }, }); - const config = createProjectConfig(cloneDeep(testData)); + const config = createProjectConfig(JSON.stringify(testData)); const fakeDecisionResponse = { result: '111128', @@ -269,7 +269,7 @@ describe('DecisionService', () => { userId: 'user2' }); - const config = createProjectConfig(cloneDeep(testData)); + const config = createProjectConfig(JSON.stringify(testData)); const experiment = config.experimentIdMap['122227']; @@ -292,7 +292,7 @@ describe('DecisionService', () => { userId: 'user3' }); - const config = createProjectConfig(cloneDeep(testData)); + const config = createProjectConfig(JSON.stringify(testData)); const experiment = config.experimentIdMap['122227']; @@ -317,7 +317,7 @@ describe('DecisionService', () => { userId: 'user2' }); - const config = createProjectConfig(cloneDeep(testData)); + const config = createProjectConfig(JSON.stringify(testData)); const experiment = config.experimentIdMap['122227']; @@ -342,7 +342,7 @@ describe('DecisionService', () => { userId: 'user3', // no attributes are set, should not satisfy audience condition 11154 }); - const config = createProjectConfig(cloneDeep(testData)); + const config = createProjectConfig(JSON.stringify(testData)); const experiment = config.experimentIdMap['122227']; @@ -362,7 +362,7 @@ describe('DecisionService', () => { userId: 'user1' }); - const config = createProjectConfig(cloneDeep(testData)); + const config = createProjectConfig(JSON.stringify(testData)); const experiment = config.experimentIdMap['133337']; @@ -382,7 +382,7 @@ describe('DecisionService', () => { reasons: [], }; - const config = createProjectConfig(cloneDeep(testData)); + const config = createProjectConfig(JSON.stringify(testData)); const experiment = config.experimentIdMap['111127']; const attributes: UserAttributes = { @@ -431,7 +431,7 @@ describe('DecisionService', () => { reasons: [], }); - const config = createProjectConfig(cloneDeep(testData)); + const config = createProjectConfig(JSON.stringify(testData)); const experiment = config.experimentIdMap['111127']; const user = new OptimizelyUserContext({ @@ -466,7 +466,7 @@ describe('DecisionService', () => { experiment_bucket_map: {}, }); - const config = createProjectConfig(cloneDeep(testData)); + const config = createProjectConfig(JSON.stringify(testData)); const experiment = config.experimentIdMap['111127']; const user = new OptimizelyUserContext({ @@ -504,7 +504,7 @@ describe('DecisionService', () => { userProfileService?.lookup.mockReturnValue(null); - const config = createProjectConfig(cloneDeep(testData)); + const config = createProjectConfig(JSON.stringify(testData)); const experiment = config.experimentIdMap['111127']; const user = new OptimizelyUserContext({ @@ -549,7 +549,7 @@ describe('DecisionService', () => { }, }); - const config = createProjectConfig(cloneDeep(testData)); + const config = createProjectConfig(JSON.stringify(testData)); const experiment = config.experimentIdMap['111127']; const user = new OptimizelyUserContext({ @@ -593,7 +593,7 @@ describe('DecisionService', () => { experiment_bucket_map: {}, }); - const config = createProjectConfig(cloneDeep(testData)); + const config = createProjectConfig(JSON.stringify(testData)); const experiment = config.experimentIdMap['111127']; const user = new OptimizelyUserContext({ @@ -634,7 +634,7 @@ describe('DecisionService', () => { throw new Error('I am an error'); }); - const config = createProjectConfig(cloneDeep(testData)); + const config = createProjectConfig(JSON.stringify(testData)); const experiment = config.experimentIdMap['111127']; const user = new OptimizelyUserContext({ @@ -678,7 +678,7 @@ describe('DecisionService', () => { throw new Error('I am an error'); }); - const config = createProjectConfig(cloneDeep(testData)); + const config = createProjectConfig(JSON.stringify(testData)); const experiment = config.experimentIdMap['111127']; const user = new OptimizelyUserContext({ @@ -720,7 +720,7 @@ describe('DecisionService', () => { }, }); - const config = createProjectConfig(cloneDeep(testData)); + const config = createProjectConfig(JSON.stringify(testData)); const experiment = config.experimentIdMap['111127']; const attributes: UserAttributes = { @@ -753,7 +753,7 @@ describe('DecisionService', () => { }, }); - const config = createProjectConfig(cloneDeep(testData)); + const config = createProjectConfig(JSON.stringify(testData)); const experiment = config.experimentIdMap['111127']; const attributes: UserAttributes = { @@ -786,7 +786,7 @@ describe('DecisionService', () => { }, }); - const config = createProjectConfig(cloneDeep(testData)); + const config = createProjectConfig(JSON.stringify(testData)); const experiment = config.experimentIdMap['111127']; const attributes: UserAttributes = { @@ -812,7 +812,7 @@ describe('DecisionService', () => { userProfileService?.lookup.mockReturnValue(null); - const config = createProjectConfig(cloneDeep(testData)); + const config = createProjectConfig(JSON.stringify(testData)); const experiment = config.experimentIdMap['111127']; const attributes: UserAttributes = { @@ -864,7 +864,7 @@ describe('DecisionService', () => { }); }); - const config = createProjectConfig(getDecisionTestDatafile()); + const config = createProjectConfig(JSON.stringify(getDecisionTestDatafile())); const user = new OptimizelyUserContext({ optimizely: {} as any, @@ -914,7 +914,7 @@ describe('DecisionService', () => { }); }); - const config = createProjectConfig(getDecisionTestDatafile()); + const config = createProjectConfig(JSON.stringify(getDecisionTestDatafile())); const user = new OptimizelyUserContext({ optimizely: {} as any, @@ -970,7 +970,7 @@ describe('DecisionService', () => { }); }); - const config = createProjectConfig(getDecisionTestDatafile()); + const config = createProjectConfig(JSON.stringify(getDecisionTestDatafile())); const user = new OptimizelyUserContext({ optimizely: {} as any, @@ -1037,7 +1037,7 @@ describe('DecisionService', () => { reasons: [], })); - const config = createProjectConfig(getDecisionTestDatafile()); + const config = createProjectConfig(JSON.stringify(getDecisionTestDatafile())); const user = new OptimizelyUserContext({ optimizely: {} as any, @@ -1091,7 +1091,7 @@ describe('DecisionService', () => { reasons: [], })); - const config = createProjectConfig(getDecisionTestDatafile()); + const config = createProjectConfig(JSON.stringify(getDecisionTestDatafile())); const user = new OptimizelyUserContext({ optimizely: {} as any, @@ -1147,7 +1147,7 @@ describe('DecisionService', () => { reasons: [], })); - const config = createProjectConfig(getDecisionTestDatafile()); + const config = createProjectConfig(JSON.stringify(getDecisionTestDatafile())); const user = new OptimizelyUserContext({ optimizely: {} as any, @@ -1214,7 +1214,7 @@ describe('DecisionService', () => { }); }); - const config = createProjectConfig(getDecisionTestDatafile()); + const config = createProjectConfig(JSON.stringify(getDecisionTestDatafile())); const user = new OptimizelyUserContext({ optimizely: {} as any, @@ -1246,7 +1246,7 @@ describe('DecisionService', () => { reasons: [], })); - const config = createProjectConfig(getDecisionTestDatafile()); + const config = createProjectConfig(JSON.stringify(getDecisionTestDatafile())); const user = new OptimizelyUserContext({ optimizely: {} as any, @@ -1300,7 +1300,7 @@ describe('DecisionService', () => { reasons: [], })); - const config = createProjectConfig(getDecisionTestDatafile()); + const config = createProjectConfig(JSON.stringify(getDecisionTestDatafile())); const rolloutId = config.featureKeyMap['flag_1'].rolloutId; config.rolloutIdMap[rolloutId].experiments = []; // remove the experiments from the rollout @@ -1340,7 +1340,7 @@ describe('DecisionService', () => { it('should return variation from the first experiment for which a variation is available', async () => { const { decisionService } = getDecisionService(); - const config = createProjectConfig(getDecisionTestDatafile()); + const config = createProjectConfig(JSON.stringify(getDecisionTestDatafile())); const user = new OptimizelyUserContext({ optimizely: {} as any, @@ -1379,7 +1379,7 @@ describe('DecisionService', () => { it('should not return variation and should not call cmab service \ for cmab experiment if user is not bucketed into it', async () => { const { decisionService, cmabService } = getDecisionService(); - const config = createProjectConfig(getDecisionTestDatafile()); + const config = createProjectConfig(JSON.stringify(getDecisionTestDatafile())); const user = new OptimizelyUserContext({ optimizely: {} as any, @@ -1419,7 +1419,7 @@ describe('DecisionService', () => { it('should get decision from the cmab service if the experiment is a cmab experiment \ and user is bucketed into it', async () => { const { decisionService, cmabService } = getDecisionService(); - const config = createProjectConfig(getDecisionTestDatafile()); + const config = createProjectConfig(JSON.stringify(getDecisionTestDatafile())); const user = new OptimizelyUserContext({ optimizely: {} as any, @@ -1471,7 +1471,7 @@ describe('DecisionService', () => { it('should pass the correct DecideOptionMap to cmabService', async () => { const { decisionService, cmabService } = getDecisionService(); - const config = createProjectConfig(getDecisionTestDatafile()); + const config = createProjectConfig(JSON.stringify(getDecisionTestDatafile())); const user = new OptimizelyUserContext({ optimizely: {} as any, @@ -1527,7 +1527,7 @@ describe('DecisionService', () => { it('should return error if cmab getDecision fails', async () => { const { decisionService, cmabService } = getDecisionService(); - const config = createProjectConfig(getDecisionTestDatafile()); + const config = createProjectConfig(JSON.stringify(getDecisionTestDatafile())); const user = new OptimizelyUserContext({ optimizely: {} as any, @@ -1610,7 +1610,7 @@ describe('DecisionService', () => { } }); - const config = createProjectConfig(getDecisionTestDatafile()); + const config = createProjectConfig(JSON.stringify(getDecisionTestDatafile())); const user1 = new OptimizelyUserContext({ optimizely: {} as any, @@ -1697,7 +1697,7 @@ describe('DecisionService', () => { cmabUuid: 'uuid-test', }); - const config = createProjectConfig(getDecisionTestDatafile()); + const config = createProjectConfig(JSON.stringify(getDecisionTestDatafile())); const user = new OptimizelyUserContext({ optimizely: {} as any, @@ -1756,7 +1756,7 @@ describe('DecisionService', () => { } }); - const config = createProjectConfig(getDecisionTestDatafile()); + const config = createProjectConfig(JSON.stringify(getDecisionTestDatafile())); const user = new OptimizelyUserContext({ optimizely: {} as any, @@ -1817,7 +1817,7 @@ describe('DecisionService', () => { } }); - const config = createProjectConfig(getDecisionTestDatafile()); + const config = createProjectConfig(JSON.stringify(getDecisionTestDatafile())); const user = new OptimizelyUserContext({ optimizely: {} as any, @@ -1893,7 +1893,7 @@ describe('DecisionService', () => { cmabUuid: 'uuid-test-2', }); - const config = createProjectConfig(getDecisionTestDatafile()); + const config = createProjectConfig(JSON.stringify(getDecisionTestDatafile())); const user = new OptimizelyUserContext({ optimizely: {} as any, @@ -1938,7 +1938,7 @@ describe('DecisionService', () => { describe('holdout', () => { it('should return holdout variation when user is bucketed into running holdout', async () => { const { decisionService } = getDecisionService(); - const config = createProjectConfig(getHoldoutTestDatafile()); + const config = createProjectConfig(JSON.stringify(getHoldoutTestDatafile())); const user = new OptimizelyUserContext({ optimizely: {} as any, userId: 'tester', @@ -1986,7 +1986,7 @@ describe('DecisionService', () => { } ] }); - const config = createProjectConfig(datafile); + const config = createProjectConfig(JSON.stringify(datafile)); const user = new OptimizelyUserContext({ optimizely: {} as any, userId: 'test_holdout_user', @@ -2022,7 +2022,7 @@ describe('DecisionService', () => { return holdout; }); - const config = createProjectConfig(datafile); + const config = createProjectConfig(JSON.stringify(datafile)); const user = new OptimizelyUserContext({ optimizely: {} as any, userId: 'tester', @@ -2046,7 +2046,7 @@ describe('DecisionService', () => { it('should fallback to experiment when user does not meet holdout audience conditions', async () => { const { decisionService } = getDecisionService(); - const config = createProjectConfig(getHoldoutTestDatafile()); + const config = createProjectConfig(JSON.stringify(getHoldoutTestDatafile())); const user = new OptimizelyUserContext({ optimizely: {} as any, userId: 'tester', @@ -2070,7 +2070,7 @@ describe('DecisionService', () => { it('should fallback to experiment when user is not bucketed into holdout traffic', async () => { const { decisionService } = getDecisionService(); - const config = createProjectConfig(getHoldoutTestDatafile()); + const config = createProjectConfig(JSON.stringify(getHoldoutTestDatafile())); const user = new OptimizelyUserContext({ optimizely: {} as any, userId: 'tester', @@ -2125,7 +2125,7 @@ describe('DecisionService', () => { return experiment; }); - const config = createProjectConfig(datafile); + const config = createProjectConfig(JSON.stringify(datafile)); const user = new OptimizelyUserContext({ optimizely: {} as any, userId: 'tester', @@ -2174,7 +2174,7 @@ describe('DecisionService', () => { ] }); - const config = createProjectConfig(datafile); + const config = createProjectConfig(JSON.stringify(datafile)); const user = new OptimizelyUserContext({ optimizely: {} as any, userId: 'tester', @@ -2206,7 +2206,7 @@ describe('DecisionService', () => { it('should skip cmab experiments', async () => { const { decisionService, cmabService } = getDecisionService(); - const config = createProjectConfig(getDecisionTestDatafile()); + const config = createProjectConfig(JSON.stringify(getDecisionTestDatafile())); const user = new OptimizelyUserContext({ optimizely: {} as any, @@ -2279,7 +2279,7 @@ describe('DecisionService', () => { }; }); - const config = createProjectConfig(getDecisionTestDatafile()); + const config = createProjectConfig(JSON.stringify(getDecisionTestDatafile())); const user = new OptimizelyUserContext({ optimizely: {} as any, @@ -2341,7 +2341,7 @@ describe('DecisionService', () => { }; }); - const config = createProjectConfig(getDecisionTestDatafile()); + const config = createProjectConfig(JSON.stringify(getDecisionTestDatafile())); const user1 = new OptimizelyUserContext({ optimizely: {} as any, @@ -2432,7 +2432,7 @@ describe('DecisionService', () => { }); }); - const config = createProjectConfig(getDecisionTestDatafile()); + const config = createProjectConfig(JSON.stringify(getDecisionTestDatafile())); const user = new OptimizelyUserContext({ optimizely: {} as any, @@ -2515,7 +2515,7 @@ describe('DecisionService', () => { }); }); - const config = createProjectConfig(getDecisionTestDatafile()); + const config = createProjectConfig(JSON.stringify(getDecisionTestDatafile())); const user = new OptimizelyUserContext({ optimizely: {} as any, @@ -2565,7 +2565,7 @@ describe('DecisionService', () => { describe('forced variation management', () => { it('should return true for a valid forcedVariation in setForcedVariation', function() { - const config = createProjectConfig(cloneDeep(testData)); + const config = createProjectConfig(JSON.stringify(testData)); const { decisionService } = getDecisionService(); const didSetVariation = decisionService.setForcedVariation( @@ -2578,7 +2578,7 @@ describe('DecisionService', () => { }); it('should return the same variation from getVariation as was set in setVariation', function() { - const config = createProjectConfig(cloneDeep(testData)); + const config = createProjectConfig(JSON.stringify(testData)); const { decisionService } = getDecisionService(); decisionService.setForcedVariation( config, @@ -2592,7 +2592,7 @@ describe('DecisionService', () => { }); it('should return null from getVariation if no forced variation was set for a valid experimentKey', function() { - const config = createProjectConfig(cloneDeep(testData)); + const config = createProjectConfig(JSON.stringify(testData)); const { decisionService } = getDecisionService(); expect(config.experimentKeyMap['testExperiment']).toBeDefined(); @@ -2602,7 +2602,7 @@ describe('DecisionService', () => { }); it('should return null from getVariation for an invalid experimentKey', function() { - const config = createProjectConfig(cloneDeep(testData)); + const config = createProjectConfig(JSON.stringify(testData)); const { decisionService } = getDecisionService(); expect(config.experimentKeyMap['definitely_not_valid_exp_key']).not.toBeDefined(); @@ -2612,7 +2612,7 @@ describe('DecisionService', () => { }); it('should return null when a forced decision is set on another experiment key', function() { - const config = createProjectConfig(cloneDeep(testData)); + const config = createProjectConfig(JSON.stringify(testData)); const { decisionService } = getDecisionService(); decisionService.setForcedVariation(config, 'testExperiment', 'user1', 'control'); @@ -2621,7 +2621,7 @@ describe('DecisionService', () => { }); it('should not set forced variation for an invalid variation key and return false', function() { - const config = createProjectConfig(cloneDeep(testData)); + const config = createProjectConfig(JSON.stringify(testData)); const { decisionService } = getDecisionService(); const wasSet = decisionService.setForcedVariation( @@ -2637,7 +2637,7 @@ describe('DecisionService', () => { }); it('should reset the forcedVariation if null is passed to setForcedVariation', function() { - const config = createProjectConfig(cloneDeep(testData)); + const config = createProjectConfig(JSON.stringify(testData)); const { decisionService } = getDecisionService(); const didSetVariation = decisionService.setForcedVariation( @@ -2666,7 +2666,7 @@ describe('DecisionService', () => { }); it('should be able to add variations for multiple experiments for one user', function() { - const config = createProjectConfig(cloneDeep(testData)); + const config = createProjectConfig(JSON.stringify(testData)); const { decisionService } = getDecisionService(); const didSetVariation1 = decisionService.setForcedVariation( @@ -2692,7 +2692,7 @@ describe('DecisionService', () => { }); it('should be able to forced variation to same experiment for multiple users', function() { - const config = createProjectConfig(cloneDeep(testData)); + const config = createProjectConfig(JSON.stringify(testData)); const { decisionService } = getDecisionService(); const didSetVariation1 = decisionService.setForcedVariation( @@ -2719,7 +2719,7 @@ describe('DecisionService', () => { }); it('should be able to reset a variation for a user with multiple experiments', function() { - const config = createProjectConfig(cloneDeep(testData)); + const config = createProjectConfig(JSON.stringify(testData)); const { decisionService } = getDecisionService(); // Set the first time @@ -2762,7 +2762,7 @@ describe('DecisionService', () => { }); it('should be able to unset a variation for a user with multiple experiments', function() { - const config = createProjectConfig(cloneDeep(testData)); + const config = createProjectConfig(JSON.stringify(testData)); const { decisionService } = getDecisionService(); // Set the first time @@ -2799,7 +2799,7 @@ describe('DecisionService', () => { }); it('should return false for an empty variation key', function() { - const config = createProjectConfig(cloneDeep(testData)); + const config = createProjectConfig(JSON.stringify(testData)); const { decisionService } = getDecisionService(); const didSetVariation = decisionService.setForcedVariation(config, 'testExperiment', 'user1', ''); @@ -2807,7 +2807,7 @@ describe('DecisionService', () => { }); it('should return null when a variation was previously set, and that variation no longer exists on the config object', function() { - const config = createProjectConfig(cloneDeep(testData)); + const config = createProjectConfig(JSON.stringify(testData)); const { decisionService } = getDecisionService(); const didSetVariation = decisionService.setForcedVariation( @@ -2837,13 +2837,13 @@ describe('DecisionService', () => { user2: 'variation', }; // Now the only variation in testExperiment is 'variation' - const newConfigObj = createProjectConfig(newDatafile); + const newConfigObj = createProjectConfig(JSON.stringify(newDatafile)); const forcedVar = decisionService.getForcedVariation(newConfigObj, 'testExperiment', 'user1').result; expect(forcedVar).toBe(null); }); it("should return null when a variation was previously set, and that variation's experiment no longer exists on the config object", function() { - const config = createProjectConfig(cloneDeep(testData)); + const config = createProjectConfig(JSON.stringify(testData)); const { decisionService } = getDecisionService(); const didSetVariation = decisionService.setForcedVariation( @@ -2854,13 +2854,13 @@ describe('DecisionService', () => { ); expect(didSetVariation).toBe(true); - const newConfigObj = createProjectConfig(cloneDeep(testDataWithFeatures)); + const newConfigObj = createProjectConfig(JSON.stringify(testDataWithFeatures)); const forcedVar = decisionService.getForcedVariation(newConfigObj, 'testExperiment', 'user1').result; expect(forcedVar).toBe(null); }); it('should return false from setForcedVariation and not set for invalid experiment key', function() { - const config = createProjectConfig(cloneDeep(testData)); + const config = createProjectConfig(JSON.stringify(testData)); const { decisionService } = getDecisionService(); const didSetVariation = decisionService.setForcedVariation( @@ -2934,7 +2934,7 @@ describe('DecisionService', () => { trafficAllocation: [{ entityId: 'global_holdout_var_id', endOfRange: 10000 }] } ]; - const config = createProjectConfig(datafile); + const config = createProjectConfig(JSON.stringify(datafile)); const { decisionService } = getDecisionService(); // bucket returns the global holdout variation for the holdout, nothing for experiments @@ -2956,7 +2956,7 @@ describe('DecisionService', () => { it('local holdout hit branch: user bucketed into local holdout for experiment rule returns holdout variation; audience and traffic not evaluated for that rule', async () => { // exp_1 has id '2001' - const config = createProjectConfig(makeLocalHoldoutDatafile('2001')); + const config = createProjectConfig(JSON.stringify(makeLocalHoldoutDatafile('2001'))); const { decisionService } = getDecisionService(); // bucket returns holdout variation when evaluating the local holdout @@ -2979,7 +2979,7 @@ describe('DecisionService', () => { it('local holdout miss branch: user not bucketed into local holdout falls through to regular rule evaluation', async () => { // exp_1 has id '2001' and audience 4001 (age <= 22) - const config = createProjectConfig(makeLocalHoldoutDatafile('2001')); + const config = createProjectConfig(JSON.stringify(makeLocalHoldoutDatafile('2001'))); const { decisionService } = getDecisionService(); // bucket returns null for the local holdout, then succeeds for the experiment @@ -3009,7 +3009,7 @@ describe('DecisionService', () => { it('rule specificity: local holdout targeting experiment rule X does not affect experiment rule Y', async () => { // exp_1 = '2001', exp_2 = '2002'. Local holdout targets only '2002' (exp_2). // Audience for exp_1: 4001 (age <= 22). User satisfies exp_1 audience but not exp_2. - const config = createProjectConfig(makeLocalHoldoutDatafile('2002')); + const config = createProjectConfig(JSON.stringify(makeLocalHoldoutDatafile('2002'))); const { decisionService } = getDecisionService(); // bucket returns holdout variation only for the local holdout when evaluating for '2002', @@ -3040,7 +3040,7 @@ describe('DecisionService', () => { it('local holdout applies to delivery rules (rollouts) as well as experiment rules', async () => { // delivery_1 has id '3001' - const config = createProjectConfig(makeLocalHoldoutDatafile('3001')); + const config = createProjectConfig(JSON.stringify(makeLocalHoldoutDatafile('3001'))); const { decisionService } = getDecisionService(); // bucket returns null for all experiments and the local holdout variation for delivery rule @@ -3070,7 +3070,7 @@ describe('DecisionService', () => { // Local holdout targets '2001' with 100% traffic allocation. // User also has a forced decision set for exp_1. // Expected: forced decision wins; decisionSource is FEATURE_TEST, not HOLDOUT. - const config = createProjectConfig(makeLocalHoldoutDatafile('2001')); + const config = createProjectConfig(JSON.stringify(makeLocalHoldoutDatafile('2001'))); const { decisionService } = getDecisionService(); // bucket should NOT be called for local_holdout_id because forced decision short-circuits first diff --git a/lib/core/decision_service/index.tests.js b/lib/core/decision_service/index.tests.js index 84c4f8c5f..0559460c3 100644 --- a/lib/core/decision_service/index.tests.js +++ b/lib/core/decision_service/index.tests.js @@ -82,7 +82,7 @@ var createLogger = () => ({ describe('lib/core/decision_service', function() { describe('APIs', function() { - var configObj = projectConfig.createProjectConfig(cloneDeep(testData)); + var configObj = projectConfig.createProjectConfig(JSON.stringify(testData)); var decisionServiceInstance; var mockLogger = createLogger({ logLevel: LOG_LEVEL.INFO }); var bucketerStub; @@ -991,7 +991,7 @@ describe('lib/core/decision_service', function() { user2: 'variation', }; // Now the only variation in testExperiment is 'variation' - var newConfigObj = projectConfig.createProjectConfig(newDatafile); + var newConfigObj = projectConfig.createProjectConfig(JSON.stringify(newDatafile)); var forcedVar = decisionServiceInstance.getForcedVariation(newConfigObj, 'testExperiment', 'user1').result; assert.strictEqual(forcedVar, null); }); @@ -1004,7 +1004,7 @@ describe('lib/core/decision_service', function() { 'control' ); assert.strictEqual(didSetVariation, true); - var newConfigObj = projectConfig.createProjectConfig(cloneDeep(testDataWithFeatures)); + var newConfigObj = projectConfig.createProjectConfig(JSON.stringify(testDataWithFeatures)); var forcedVar = decisionServiceInstance.getForcedVariation(newConfigObj, 'testExperiment', 'user1').result; assert.strictEqual(forcedVar, null); }); @@ -1029,7 +1029,7 @@ describe('lib/core/decision_service', function() { // TODO: Move tests that test methods of Optimizely to lib/optimizely/index.tests.js describe('when a bucketingID is provided', function() { - var configObj = projectConfig.createProjectConfig(cloneDeep(testData)); + var configObj = projectConfig.createProjectConfig(JSON.stringify(testData)); var createdLogger = createLogger({ logLevel: LOG_LEVEL.DEBUG, logToConsole: false, @@ -1040,7 +1040,7 @@ describe('lib/core/decision_service', function() { optlyInstance = new Optimizely({ clientEngine: 'node-sdk', projectConfigManager: getMockProjectConfigManager({ - initConfig: createProjectConfig(cloneDeep(testData)) + initConfig: createProjectConfig(JSON.stringify(testData)) }), jsonSchemaValidator: jsonSchemaValidator, isValidInstance: true, @@ -1173,7 +1173,7 @@ describe('lib/core/decision_service', function() { sinon.stub(mockLogger, 'warn'); sinon.stub(mockLogger, 'error'); - configObj = projectConfig.createProjectConfig(cloneDeep(testData)); + configObj = projectConfig.createProjectConfig(JSON.stringify(testData)); decisionService = createDecisionService({ logger: mockLogger, }); @@ -1216,7 +1216,7 @@ describe('lib/core/decision_service', function() { }); var user; beforeEach(function() { - configObj = projectConfig.createProjectConfig(cloneDeep(testDataWithFeatures)); + configObj = projectConfig.createProjectConfig(JSON.stringify(testDataWithFeatures)); sandbox = sinon.sandbox.create(); sandbox.stub(mockLogger, 'debug'); sandbox.stub(mockLogger, 'info'); @@ -1941,7 +1941,7 @@ describe('lib/core/decision_service', function() { var user; beforeEach(function() { - configObj = projectConfig.createProjectConfig(cloneDeep(testDataWithFeatures)); + configObj = projectConfig.createProjectConfig(JSON.stringify(testDataWithFeatures)); feature = configObj.featureKeyMap.test_feature; decisionService = createDecisionService({ logger: createLogger({ logLevel: LOG_LEVEL.INFO }), diff --git a/lib/event_processor/event_builder/user_event.spec.ts b/lib/event_processor/event_builder/user_event.spec.ts index e8cb373b3..7bd55ce76 100644 --- a/lib/event_processor/event_builder/user_event.spec.ts +++ b/lib/event_processor/event_builder/user_event.spec.ts @@ -7,7 +7,7 @@ import testData from '../../tests/test_data'; describe('buildImpressionEvent', () => { it('should use correct region from projectConfig in event context', () => { const projectConfig = createProjectConfig( - testData.getTestProjectConfig(), + JSON.stringify(testData.getTestProjectConfig()), ) const experiment = projectConfig.experiments[0]; @@ -51,7 +51,7 @@ describe('buildImpressionEvent', () => { describe('buildConversionEvent', () => { it('should use correct region from projectConfig in event context', () => { const projectConfig = createProjectConfig( - testData.getTestProjectConfig(), + JSON.stringify(testData.getTestProjectConfig()), ) const conversionEvent = buildConversionEvent({ diff --git a/lib/index.browser.tests.js b/lib/index.browser.tests.js index 0a6feb28b..0f032bef9 100644 --- a/lib/index.browser.tests.js +++ b/lib/index.browser.tests.js @@ -19,7 +19,6 @@ import Optimizely from './optimizely'; import testData from './tests/test_data'; import packageJSON from '../package.json'; import * as optimizelyFactory from './index.browser'; -import configValidator from './utils/config_validator'; import { getMockProjectConfigManager } from './tests/mock/mock_project_config_manager'; import { createProjectConfig } from './project_config/project_config'; import { wrapConfigManager } from './project_config/config_manager_factory'; @@ -180,7 +179,7 @@ describe('javascript-sdk (Browser)', function() { it('should activate with provided event dispatcher', function() { var optlyInstance = optimizelyFactory.createInstance({ projectConfigManager: wrapConfigManager(getMockProjectConfigManager({ - initConfig: createProjectConfig(testData.getTestProjectConfig()), + initConfig: createProjectConfig(JSON.stringify(testData.getTestProjectConfig())), })), logger: wrapLogger(mockLogger), }); @@ -191,7 +190,7 @@ describe('javascript-sdk (Browser)', function() { it('should be able to set and get a forced variation', function() { var optlyInstance = optimizelyFactory.createInstance({ projectConfigManager: wrapConfigManager(getMockProjectConfigManager({ - initConfig: createProjectConfig(testData.getTestProjectConfig()), + initConfig: createProjectConfig(JSON.stringify(testData.getTestProjectConfig())), })), logger: wrapLogger(mockLogger), }); @@ -206,7 +205,7 @@ describe('javascript-sdk (Browser)', function() { it('should be able to set and unset a forced variation', function() { var optlyInstance = optimizelyFactory.createInstance({ projectConfigManager: wrapConfigManager(getMockProjectConfigManager({ - initConfig: createProjectConfig(testData.getTestProjectConfig()), + initConfig: createProjectConfig(JSON.stringify(testData.getTestProjectConfig())), })), logger: wrapLogger(mockLogger), }); @@ -227,7 +226,7 @@ describe('javascript-sdk (Browser)', function() { it('should be able to set multiple experiments for one user', function() { var optlyInstance = optimizelyFactory.createInstance({ projectConfigManager: wrapConfigManager(getMockProjectConfigManager({ - initConfig: createProjectConfig(testData.getTestProjectConfig()), + initConfig: createProjectConfig(JSON.stringify(testData.getTestProjectConfig())), })), logger: wrapLogger(mockLogger), }); @@ -252,7 +251,7 @@ describe('javascript-sdk (Browser)', function() { it('should be able to set multiple experiments for one user, and unset one', function() { var optlyInstance = optimizelyFactory.createInstance({ projectConfigManager: wrapConfigManager(getMockProjectConfigManager({ - initConfig: createProjectConfig(testData.getTestProjectConfig()), + initConfig: createProjectConfig(JSON.stringify(testData.getTestProjectConfig())), })), logger: wrapLogger(mockLogger), }); @@ -280,7 +279,7 @@ describe('javascript-sdk (Browser)', function() { it('should be able to set multiple experiments for one user, and reset one', function() { var optlyInstance = optimizelyFactory.createInstance({ projectConfigManager: wrapConfigManager(getMockProjectConfigManager({ - initConfig: createProjectConfig(testData.getTestProjectConfig()), + initConfig: createProjectConfig(JSON.stringify(testData.getTestProjectConfig())), })), logger: wrapLogger(mockLogger), }); @@ -312,7 +311,7 @@ describe('javascript-sdk (Browser)', function() { it('should override bucketing when setForcedVariation is called', function() { var optlyInstance = optimizelyFactory.createInstance({ projectConfigManager: wrapConfigManager(getMockProjectConfigManager({ - initConfig: createProjectConfig(testData.getTestProjectConfig()), + initConfig: createProjectConfig(JSON.stringify(testData.getTestProjectConfig())), })), logger: wrapLogger(mockLogger), }); @@ -333,7 +332,7 @@ describe('javascript-sdk (Browser)', function() { it('should override bucketing when setForcedVariation is called for a not running experiment', function() { var optlyInstance = optimizelyFactory.createInstance({ projectConfigManager: wrapConfigManager(getMockProjectConfigManager({ - initConfig: createProjectConfig(testData.getTestProjectConfig()), + initConfig: createProjectConfig(JSON.stringify(testData.getTestProjectConfig())), })), logger: wrapLogger(mockLogger), }); diff --git a/lib/index.browser.umdtests.js b/lib/index.browser.umdtests.js index a13f5046b..ac8b4906c 100644 --- a/lib/index.browser.umdtests.js +++ b/lib/index.browser.umdtests.js @@ -16,14 +16,12 @@ import { assert } from 'chai'; import sinon from 'sinon'; -import configValidator from './utils/config_validator'; import * as enums from './utils/enums'; import * as logger from './plugins/logger'; import Optimizely from './optimizely'; import testData from './tests/test_data'; import packageJSON from '../package.json'; import eventDispatcher from './plugins/event_dispatcher/index.browser'; -import { INVALID_CONFIG_OR_SOMETHING } from './exception_messages'; describe('javascript-sdk', function() { describe('APIs', function() { @@ -37,7 +35,6 @@ describe('javascript-sdk', function() { logLevel: enums.LOG_LEVEL.INFO, logToConsole: false, }); - sinon.stub(configValidator, 'validate'); sinon.stub(Optimizely.prototype, 'close'); global.XMLHttpRequest = sinon.useFakeXMLHttpRequest(); @@ -56,7 +53,6 @@ describe('javascript-sdk', function() { console.warn.restore(); console.error.restore(); window.addEventListener.restore(); - configValidator.validate.restore(); Optimizely.prototype.close.restore(); delete global.XMLHttpRequest }); @@ -93,7 +89,6 @@ describe('javascript-sdk', function() { }); it('should not throw if the provided config is not valid', function() { - configValidator.validate.throws(new Error(INVALID_CONFIG_OR_SOMETHING)); assert.doesNotThrow(function() { var optlyInstance = window.optimizelySdk.createInstance({ datafile: {}, diff --git a/lib/index.node.tests.js b/lib/index.node.tests.js index 1b4b6f185..eb4bd13f5 100644 --- a/lib/index.node.tests.js +++ b/lib/index.node.tests.js @@ -19,7 +19,6 @@ import sinon from 'sinon'; import Optimizely from './optimizely'; import testData from './tests/test_data'; import * as optimizelyFactory from './index.node'; -import configValidator from './utils/config_validator'; import { getMockProjectConfigManager } from './tests/mock/mock_project_config_manager'; import { wrapConfigManager } from './project_config/config_manager_factory'; import { wrapLogger } from './logging/logger_factory'; @@ -105,7 +104,7 @@ describe('optimizelyFactory', function() { // it('should ignore invalid event flush interval and use default instead', function() { // optimizelyFactory.createInstance({ // projectConfigManager: getMockProjectConfigManager({ - // initConfig: createProjectConfig(testData.getTestProjectConfigWithFeatures()), + // initConfig: createProjectConfig(JSON.stringify(testData.getTestProjectConfigWithFeatures())), // }), // errorHandler: fakeErrorHandler, // eventDispatcher: fakeEventDispatcher, @@ -123,7 +122,7 @@ describe('optimizelyFactory', function() { // it('should use default event flush interval when none is provided', function() { // optimizelyFactory.createInstance({ // projectConfigManager: getMockProjectConfigManager({ - // initConfig: createProjectConfig(testData.getTestProjectConfigWithFeatures()), + // initConfig: createProjectConfig(JSON.stringify(testData.getTestProjectConfigWithFeatures())), // }), // errorHandler: fakeErrorHandler, // eventDispatcher: fakeEventDispatcher, @@ -140,7 +139,7 @@ describe('optimizelyFactory', function() { // it('should use provided event flush interval when valid', function() { // optimizelyFactory.createInstance({ // projectConfigManager: getMockProjectConfigManager({ - // initConfig: createProjectConfig(testData.getTestProjectConfigWithFeatures()), + // initConfig: createProjectConfig(JSON.stringify(testData.getTestProjectConfigWithFeatures())), // }), // errorHandler: fakeErrorHandler, // eventDispatcher: fakeEventDispatcher, @@ -158,7 +157,7 @@ describe('optimizelyFactory', function() { // it('should ignore invalid event batch size and use default instead', function() { // optimizelyFactory.createInstance({ // projectConfigManager: getMockProjectConfigManager({ - // initConfig: createProjectConfig(testData.getTestProjectConfigWithFeatures()), + // initConfig: createProjectConfig(JSON.stringify(testData.getTestProjectConfigWithFeatures())), // }), // errorHandler: fakeErrorHandler, // eventDispatcher: fakeEventDispatcher, @@ -176,7 +175,7 @@ describe('optimizelyFactory', function() { // it('should use default event batch size when none is provided', function() { // optimizelyFactory.createInstance({ // projectConfigManager: getMockProjectConfigManager({ - // initConfig: createProjectConfig(testData.getTestProjectConfigWithFeatures()), + // initConfig: createProjectConfig(JSON.stringify(testData.getTestProjectConfigWithFeatures())), // }), // errorHandler: fakeErrorHandler, // eventDispatcher: fakeEventDispatcher, @@ -193,7 +192,7 @@ describe('optimizelyFactory', function() { // it('should use provided event batch size when valid', function() { // optimizelyFactory.createInstance({ // projectConfigManager: getMockProjectConfigManager({ - // initConfig: createProjectConfig(testData.getTestProjectConfigWithFeatures()), + // initConfig: createProjectConfig(JSON.stringify(testData.getTestProjectConfigWithFeatures())), // }), // errorHandler: fakeErrorHandler, // eventDispatcher: fakeEventDispatcher, diff --git a/lib/index.react_native.spec.ts b/lib/index.react_native.spec.ts index 20493ab11..9f9e16d63 100644 --- a/lib/index.react_native.spec.ts +++ b/lib/index.react_native.spec.ts @@ -19,7 +19,6 @@ import Optimizely from './optimizely'; import testData from './tests/test_data'; import packageJSON from '../package.json'; import * as optimizelyFactory from './index.react_native'; -import configValidator from './utils/config_validator'; import { getMockProjectConfigManager } from './tests/mock/mock_project_config_manager'; import { createProjectConfig } from './project_config/project_config'; import { getMockLogger } from './tests/mock/mock_logger'; @@ -137,7 +136,7 @@ describe('javascript-sdk/react-native', () => { // it('should call logging.setLogLevel', () => { // optimizelyFactory.createInstance({ // projectConfigManager: getMockProjectConfigManager({ - // initConfig: createProjectConfig(testData.getTestProjectConfig()), + // initConfig: createProjectConfig(JSON.stringify(testData.getTestProjectConfig())), // }), // logLevel: optimizelyFactory.enums.LOG_LEVEL.ERROR, // }); @@ -159,7 +158,7 @@ describe('javascript-sdk/react-native', () => { // const fakeLogger = { log: function() {} }; // optimizelyFactory.createInstance({ // projectConfigManager: getMockProjectConfigManager({ - // initConfig: createProjectConfig(testData.getTestProjectConfig()), + // initConfig: createProjectConfig(JSON.stringify(testData.getTestProjectConfig())), // }), // // eslint-disable-next-line @typescript-eslint/ban-ts-comment // // @ts-ignore diff --git a/lib/optimizely/index.spec.ts b/lib/optimizely/index.spec.ts index 4ed119fba..d891e283a 100644 --- a/lib/optimizely/index.spec.ts +++ b/lib/optimizely/index.spec.ts @@ -75,7 +75,7 @@ describe('Optimizely', () => { it('should pass disposable options to the respective services', () => { const projectConfigManager = getMockProjectConfigManager({ - initConfig: createProjectConfig(testData.getTestProjectConfig()), + initConfig: createProjectConfig(JSON.stringify(testData.getTestProjectConfig())), }); vi.spyOn(projectConfigManager, 'makeDisposable'); @@ -100,7 +100,7 @@ describe('Optimizely', () => { it('should set child logger to respective services', () => { const projectConfigManager = getMockProjectConfigManager({ - initConfig: createProjectConfig(testData.getTestProjectConfig()), + initConfig: createProjectConfig(JSON.stringify(testData.getTestProjectConfig())), }); const eventProcessor = getForwardingEventProcessor(eventDispatcher); @@ -136,7 +136,7 @@ describe('Optimizely', () => { describe('decideAsync', () => { it('should return an error decision with correct reasons if decisionService returns error', async () => { - const projectConfig = createProjectConfig(getDecisionTestDatafile()); + const projectConfig = createProjectConfig(JSON.stringify(getDecisionTestDatafile())); const projectConfigManager = getMockProjectConfigManager({ initConfig: projectConfig, @@ -186,7 +186,7 @@ describe('Optimizely', () => { }); it('should include cmab uuid in dispatched event if decisionService returns a cmab uuid', async () => { - const projectConfig = createProjectConfig(getDecisionTestDatafile()); + const projectConfig = createProjectConfig(JSON.stringify(getDecisionTestDatafile())); const projectConfigManager = getMockProjectConfigManager({ initConfig: projectConfig, @@ -257,7 +257,7 @@ describe('Optimizely', () => { beforeEach(() => { const datafile = getDecisionTestDatafile(); datafile.holdouts = JSON.parse(JSON.stringify(holdoutData)); // Deep copy to avoid mutations - projectConfig = createProjectConfig(datafile); + projectConfig = createProjectConfig(JSON.stringify(datafile)); const projectConfigManager = getMockProjectConfigManager({ initConfig: projectConfig, @@ -876,7 +876,7 @@ describe('Optimizely', () => { it('should flush eventProcessor and odpManager on flushImmediately()', async () => { const projectConfigManager = getMockProjectConfigManager({ - initConfig: createProjectConfig(testData.getTestProjectConfig()), + initConfig: createProjectConfig(JSON.stringify(testData.getTestProjectConfig())), }); const eventProcessor = getForwardingEventProcessor(eventDispatcher); @@ -909,7 +909,7 @@ describe('Optimizely', () => { it('should log error when sendOdpEvent is called without odpManager', () => { const projectConfigManager = getMockProjectConfigManager({ - initConfig: createProjectConfig(testData.getTestProjectConfig()), + initConfig: createProjectConfig(JSON.stringify(testData.getTestProjectConfig())), }); const mockLogger = getMockLogger(); @@ -931,7 +931,7 @@ describe('Optimizely', () => { it('should log error when fetchQualifiedSegments is called without odpManager', async () => { const projectConfigManager = getMockProjectConfigManager({ - initConfig: createProjectConfig(testData.getTestProjectConfig()), + initConfig: createProjectConfig(JSON.stringify(testData.getTestProjectConfig())), }); const mockLogger = getMockLogger(); diff --git a/lib/optimizely/index.tests.js b/lib/optimizely/index.tests.js index 50eba745a..d1ea1b8b3 100644 --- a/lib/optimizely/index.tests.js +++ b/lib/optimizely/index.tests.js @@ -103,7 +103,7 @@ var createLogger = () => ({ const getOptlyInstance = ({ datafileObj, defaultDecideOptions }) => { const mockConfigManager = getMockProjectConfigManager({ - initConfig: createProjectConfig(datafileObj), + initConfig: createProjectConfig(JSON.stringify(datafileObj)), }); const eventDispatcher = getMockEventDispatcher(); const eventProcessor = getForwardingEventProcessor(eventDispatcher); @@ -302,7 +302,7 @@ describe('lib/optimizely', function() { }); beforeEach(function() { const mockConfigManager = getMockProjectConfigManager({ - initConfig: createProjectConfig(testData.getTestProjectConfig()), + initConfig: createProjectConfig(JSON.stringify(testData.getTestProjectConfig())), }); optlyInstance = new Optimizely({ @@ -922,7 +922,7 @@ describe('lib/optimizely', function() { bucketStub.returns(fakeDecisionResponse); const mockConfigManager = getMockProjectConfigManager({ - initConfig: createProjectConfig(testData.getTestProjectConfig()), + initConfig: createProjectConfig(JSON.stringify(testData.getTestProjectConfig())), }); var instance = new Optimizely({ @@ -1675,7 +1675,7 @@ describe('lib/optimizely', function() { it('should track when logger is in DEBUG mode', function() { const mockConfigManager = getMockProjectConfigManager({ - initConfig: createProjectConfig(testData.getTestProjectConfig()), + initConfig: createProjectConfig(JSON.stringify(testData.getTestProjectConfig())), }); var instance = new Optimizely({ @@ -2535,7 +2535,7 @@ describe('lib/optimizely', function() { describe('activate', function() { beforeEach(function() { const mockConfigManager = getMockProjectConfigManager({ - initConfig: createProjectConfig(testData.getTestProjectConfig()), + initConfig: createProjectConfig(JSON.stringify(testData.getTestProjectConfig())), }); optlyInstance = new Optimizely({ @@ -2598,7 +2598,7 @@ describe('lib/optimizely', function() { describe('getVariation', function() { beforeEach(function() { const mockConfigManager = getMockProjectConfigManager({ - initConfig: createProjectConfig(testData.getTestProjectConfig()), + initConfig: createProjectConfig(JSON.stringify(testData.getTestProjectConfig())), }); optlyInstance = new Optimizely({ @@ -2655,7 +2655,7 @@ describe('lib/optimizely', function() { it('should send notification with variation key and type feature-test when getVariation returns feature experiment variation', function() { const mockConfigManager = getMockProjectConfigManager({ - initConfig: createProjectConfig(testData.getTestProjectConfigWithFeatures()), + initConfig: createProjectConfig(JSON.stringify(testData.getTestProjectConfigWithFeatures())), }); var optly = new Optimizely({ @@ -2696,7 +2696,7 @@ describe('lib/optimizely', function() { beforeEach(function() { const mockConfigManager = getMockProjectConfigManager({ - initConfig: createProjectConfig(testData.getTestProjectConfigWithFeatures()), + initConfig: createProjectConfig(JSON.stringify(testData.getTestProjectConfigWithFeatures())), }); optlyInstance = new Optimizely({ @@ -4319,7 +4319,7 @@ describe('lib/optimizely', function() { describe('#createUserContext', function() { beforeEach(function() { const mockConfigManager = getMockProjectConfigManager({ - initConfig: createProjectConfig(testData.getTestDecideProjectConfig()), + initConfig: createProjectConfig(JSON.stringify(testData.getTestDecideProjectConfig())), }); optlyInstance = new Optimizely({ @@ -4986,7 +4986,7 @@ describe('lib/optimizely', function() { describe('with DISABLE_DECISION_EVENT flag in default decide options', function() { beforeEach(function() { const mockConfigManager = getMockProjectConfigManager({ - initConfig: createProjectConfig(testData.getTestDecideProjectConfig()), + initConfig: createProjectConfig(JSON.stringify(testData.getTestDecideProjectConfig())), }); optlyInstance = new Optimizely({ @@ -5057,7 +5057,7 @@ describe('lib/optimizely', function() { describe('with INCLUDE_REASONS flag in default decide options', function() { beforeEach(function() { const mockConfigManager = getMockProjectConfigManager({ - initConfig: createProjectConfig(testData.getTestDecideProjectConfig()), + initConfig: createProjectConfig(JSON.stringify(testData.getTestDecideProjectConfig())), }); optlyInstance = new Optimizely({ @@ -5115,7 +5115,7 @@ describe('lib/optimizely', function() { save: sinon.stub(), }; const mockConfigManager = getMockProjectConfigManager({ - initConfig: createProjectConfig(testData.getTestDecideProjectConfig()), + initConfig: createProjectConfig(JSON.stringify(testData.getTestDecideProjectConfig())), }); var optlyInstanceWithUserProfile = new Optimizely({ @@ -5543,7 +5543,7 @@ describe('lib/optimizely', function() { save: sinon.stub(), }; const mockConfigManager = getMockProjectConfigManager({ - initConfig: createProjectConfig(testData.getTestDecideProjectConfig()), + initConfig: createProjectConfig(JSON.stringify(testData.getTestDecideProjectConfig())), }); optlyInstanceWithUserProfile = new Optimizely({ @@ -5590,7 +5590,7 @@ describe('lib/optimizely', function() { save: sinon.stub(), }; const mockConfigManager = getMockProjectConfigManager({ - initConfig: createProjectConfig(testData.getTestDecideProjectConfig()), + initConfig: createProjectConfig(JSON.stringify(testData.getTestDecideProjectConfig())), }); optlyInstanceWithUserProfile = new Optimizely({ @@ -5641,7 +5641,7 @@ describe('lib/optimizely', function() { save: sinon.stub(), }; const mockConfigManager = getMockProjectConfigManager({ - initConfig: createProjectConfig(testData.getTestDecideProjectConfig()), + initConfig: createProjectConfig(JSON.stringify(testData.getTestDecideProjectConfig())), }); optlyInstanceWithUserProfile = new Optimizely({ @@ -5771,7 +5771,7 @@ describe('lib/optimizely', function() { clientEngine: 'node-sdk', datafile: testData.getTestDecideProjectConfig(), projectConfigManager: getMockProjectConfigManager({ - initConfig: createProjectConfig(testData.getTestDecideProjectConfig()), + initConfig: createProjectConfig(JSON.stringify(testData.getTestDecideProjectConfig())), }), userProfileService: userProfileServiceInstance, @@ -6013,7 +6013,7 @@ describe('lib/optimizely', function() { clientEngine: 'node-sdk', datafile: testData.getTestDecideProjectConfig(), projectConfigManager: getMockProjectConfigManager({ - initConfig: createProjectConfig(testData.getTestDecideProjectConfig()), + initConfig: createProjectConfig(JSON.stringify(testData.getTestDecideProjectConfig())), }), userProfileService: userProfileServiceInstance, @@ -6084,7 +6084,7 @@ describe('lib/optimizely', function() { ); beforeEach(function() { const mockConfigManager = getMockProjectConfigManager({ - initConfig: createProjectConfig(testData.getTestProjectConfig()), + initConfig: createProjectConfig(JSON.stringify(testData.getTestProjectConfig())), }); optlyInstance = new Optimizely({ @@ -6157,7 +6157,7 @@ describe('lib/optimizely', function() { beforeEach(function() { const mockConfigManager = getMockProjectConfigManager({ - initConfig: createProjectConfig(testData.getTestProjectConfigWithFeatures()), + initConfig: createProjectConfig(JSON.stringify(testData.getTestProjectConfigWithFeatures())), }); optlyInstance = new Optimizely({ @@ -6787,7 +6787,7 @@ describe('lib/optimizely', function() { it('return features that are enabled for the user and send notification for every feature', function() { const mockConfigManager = getMockProjectConfigManager({ - initConfig: createProjectConfig(testData.getTestProjectConfigWithFeatures()), + initConfig: createProjectConfig(JSON.stringify(testData.getTestProjectConfigWithFeatures())), }); optlyInstance = new Optimizely({ @@ -8882,7 +8882,7 @@ describe('lib/optimizely', function() { ); beforeEach(function() { const mockConfigManager = getMockProjectConfigManager({ - initConfig: createProjectConfig(testData.getTypedAudiencesConfig()), + initConfig: createProjectConfig(JSON.stringify(testData.getTypedAudiencesConfig())), }); optlyInstance = new Optimizely({ @@ -9027,7 +9027,7 @@ describe('lib/optimizely', function() { ); beforeEach(function() { const mockConfigManager = getMockProjectConfigManager({ - initConfig: createProjectConfig(testData.getTypedAudiencesConfig()), + initConfig: createProjectConfig(JSON.stringify(testData.getTypedAudiencesConfig())), }); optlyInstance = new Optimizely({ @@ -9266,7 +9266,7 @@ describe('lib/optimizely', function() { eventProcessorStopPromise = Promise.resolve(); mockEventProcessor.onTerminated.returns(eventProcessorStopPromise); const mockConfigManager = getMockProjectConfigManager({ - initConfig: createProjectConfig(testData.getTestProjectConfig()), + initConfig: createProjectConfig(JSON.stringify(testData.getTestProjectConfig())), }); optlyInstance = new Optimizely({ @@ -9303,7 +9303,7 @@ describe('lib/optimizely', function() { eventProcessorStopPromise.catch(() => {}); mockEventProcessor.onTerminated.returns(eventProcessorStopPromise); const mockConfigManager = getMockProjectConfigManager({ - initConfig: createProjectConfig(testData.getTestProjectConfig()), + initConfig: createProjectConfig(JSON.stringify(testData.getTestProjectConfig())), }); optlyInstance = new Optimizely({ @@ -9613,7 +9613,7 @@ describe('lib/optimizely', function() { const datafile = testData.getTestProjectConfigWithFeatures(); - const newConfig = createProjectConfig(datafile, JSON.stringify(datafile)); + const newConfig = createProjectConfig(JSON.stringify(datafile)); fakeProjectConfigManager.setConfig(newConfig); fakeProjectConfigManager.pushUpdate(newConfig); @@ -9644,7 +9644,7 @@ describe('lib/optimizely', function() { ], }); differentDatafile.revision = '44'; - var differentConfig = createProjectConfig(differentDatafile, JSON.stringify(differentDatafile)); + var differentConfig = createProjectConfig(JSON.stringify(differentDatafile)); fakeProjectConfigManager.setConfig(differentConfig); fakeProjectConfigManager.pushUpdate(differentConfig); @@ -9658,7 +9658,7 @@ describe('lib/optimizely', function() { NOTIFICATION_TYPES.OPTIMIZELY_CONFIG_UPDATE, listener ); - var newConfig = createProjectConfig(testData.getTestProjectConfigWithFeatures()); + var newConfig = createProjectConfig(JSON.stringify(testData.getTestProjectConfigWithFeatures())); fakeProjectConfigManager.pushUpdate(newConfig); sinon.assert.calledOnce(listener); @@ -9683,7 +9683,7 @@ describe('lib/optimizely', function() { const datafile = testData.getTestProjectConfig(); const mockConfigManager = getMockProjectConfigManager(); - mockConfigManager.setConfig(createProjectConfig(datafile, JSON.stringify(datafile))); + mockConfigManager.setConfig(createProjectConfig(JSON.stringify(datafile))); optlyInstance = new Optimizely({ clientEngine: 'node-sdk', diff --git a/lib/optimizely_user_context/index.spec.ts b/lib/optimizely_user_context/index.spec.ts index 5f451ccb4..097d0ae1d 100644 --- a/lib/optimizely_user_context/index.spec.ts +++ b/lib/optimizely_user_context/index.spec.ts @@ -51,7 +51,7 @@ interface GetOptlyInstanceParams { const getOptlyInstance = ({ datafileObj, defaultDecideOptions }: GetOptlyInstanceParams) => { const createdLogger = getMockLogger(); const mockConfigManager = getMockProjectConfigManager({ - initConfig: createProjectConfig(datafileObj), + initConfig: createProjectConfig(JSON.stringify(datafileObj)), }); const eventDispatcher = getMockEventDispatcher(); const eventProcessor = getForwardingEventProcessor(eventDispatcher); diff --git a/lib/optimizely_user_context/index.tests.js b/lib/optimizely_user_context/index.tests.js index 6eed2437e..97b07634b 100644 --- a/lib/optimizely_user_context/index.tests.js +++ b/lib/optimizely_user_context/index.tests.js @@ -54,7 +54,7 @@ var createLogger = () => ({ const getOptlyInstance = ({ datafileObj, defaultDecideOptions }) => { const createdLogger = createLogger({ logLevel: LOG_LEVEL.INFO }); const mockConfigManager = getMockProjectConfigManager({ - initConfig: createProjectConfig(datafileObj), + initConfig: createProjectConfig(JSON.stringify(datafileObj)), }); const eventDispatcher = getMockEventDispatcher(); const eventProcessor = getForwardingEventProcessor(eventDispatcher); @@ -400,7 +400,7 @@ describe('lib/optimizely_user_context', function() { optlyInstance = new Optimizely({ clientEngine: 'node-sdk', projectConfigManager: getMockProjectConfigManager({ - initConfig: createProjectConfig(testData.getTestDecideProjectConfig()) + initConfig: createProjectConfig(JSON.stringify(testData.getTestDecideProjectConfig())) }), eventProcessor, isValidInstance: true, @@ -759,7 +759,7 @@ describe('lib/optimizely_user_context', function() { optlyInstance = new Optimizely({ clientEngine: 'node-sdk', projectConfigManager: getMockProjectConfigManager({ - initConfig: createProjectConfig(testData.getTestDecideProjectConfig()) + initConfig: createProjectConfig(JSON.stringify(testData.getTestDecideProjectConfig())) }), eventProcessor, isValidInstance: true, @@ -865,7 +865,7 @@ describe('lib/optimizely_user_context', function() { optlyInstance = new Optimizely({ clientEngine: 'node-sdk', projectConfigManager: getMockProjectConfigManager({ - initConfig: createProjectConfig(testData.getTestDecideProjectConfig()) + initConfig: createProjectConfig(JSON.stringify(testData.getTestDecideProjectConfig())) }), eventProcessor, isValidInstance: true, @@ -911,7 +911,7 @@ describe('lib/optimizely_user_context', function() { var optlyInstance = new Optimizely({ clientEngine: 'node-sdk', projectConfigManager: getMockProjectConfigManager({ - initConfig: createProjectConfig(testData.getTestDecideProjectConfig()) + initConfig: createProjectConfig(JSON.stringify(testData.getTestDecideProjectConfig())) }), eventProcessor, isValidInstance: true, diff --git a/lib/project_config/optimizely_config.spec.ts b/lib/project_config/optimizely_config.spec.ts index 6e9e6747b..bd48a68a2 100644 --- a/lib/project_config/optimizely_config.spec.ts +++ b/lib/project_config/optimizely_config.spec.ts @@ -31,7 +31,6 @@ 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 => { @@ -57,15 +56,15 @@ describe('Optimizely Config', () => { const logger = getMockLogger(); beforeEach(() => { - projectConfigObject = createProjectConfig(cloneDeep(datafile as any)); + projectConfigObject = createProjectConfig(JSON.stringify(datafile)); optimizelyConfigObject = createOptimizelyConfig(projectConfigObject, JSON.stringify(datafile)); - projectTypedAudienceConfigObject = createProjectConfig(cloneDeep(typedAudienceDatafile)); - projectSimilarRuleKeyConfigObject = createProjectConfig(cloneDeep(similarRuleKeyDatafile)); + projectTypedAudienceConfigObject = createProjectConfig(JSON.stringify(typedAudienceDatafile)); + projectSimilarRuleKeyConfigObject = createProjectConfig(JSON.stringify(similarRuleKeyDatafile)); optimizelySimilarRuleKeyConfigObject = createOptimizelyConfig( projectSimilarRuleKeyConfigObject, JSON.stringify(similarRuleKeyDatafile) ); - projectSimilarExperimentKeyConfigObject = createProjectConfig(cloneDeep(similarExperimentKeyDatafile)); + projectSimilarExperimentKeyConfigObject = createProjectConfig(JSON.stringify(similarExperimentKeyDatafile)); optimizelySimilarExperimentkeyConfigObject = createOptimizelyConfig( projectSimilarExperimentKeyConfigObject, JSON.stringify(similarExperimentKeyDatafile) @@ -99,7 +98,7 @@ describe('Optimizely Config', () => { 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 configObj = createProjectConfig(JSON.stringify(datafile)); const optimizelyConfig = createOptimizelyConfig(configObj, JSON.stringify(datafile), logger); const experimentsMap = optimizelyConfig.experimentsMap; const duplicateKey = 'experiment_rule'; diff --git a/lib/project_config/optimizely_config.tests.js b/lib/project_config/optimizely_config.tests.js index 22d2b95f3..244fb632f 100644 --- a/lib/project_config/optimizely_config.tests.js +++ b/lib/project_config/optimizely_config.tests.js @@ -14,7 +14,6 @@ * limitations under the License. */ import { assert } from 'chai'; -import { cloneDeep } from 'lodash'; import sinon from 'sinon'; import { createOptimizelyConfig, OptimizelyConfig } from './optimizely_config'; @@ -57,13 +56,13 @@ describe('lib/core/optimizely_config', function() { var projectSimilarExperimentKeyConfigObject; beforeEach(function() { - projectConfigObject = createProjectConfig(cloneDeep(datafile)); + projectConfigObject = createProjectConfig(JSON.stringify(datafile)); optimizelyConfigObject = createOptimizelyConfig(projectConfigObject, JSON.stringify(datafile)); - projectTypedAudienceConfigObject = createProjectConfig(cloneDeep(typedAudienceDatafile)); + projectTypedAudienceConfigObject = createProjectConfig(JSON.stringify(typedAudienceDatafile)); optimizelyTypedAudienceConfigObject = createOptimizelyConfig(projectTypedAudienceConfigObject, JSON.stringify(typedAudienceDatafile)); - projectSimilarRuleKeyConfigObject = createProjectConfig(cloneDeep(similarRuleKeyDatafile)); + projectSimilarRuleKeyConfigObject = createProjectConfig(JSON.stringify(similarRuleKeyDatafile)); optimizelySimilarRuleKeyConfigObject = createOptimizelyConfig(projectSimilarRuleKeyConfigObject, JSON.stringify(similarRuleKeyDatafile)); - projectSimilarExperimentKeyConfigObject = createProjectConfig(cloneDeep(similarExperimentKeyDatafile)); + projectSimilarExperimentKeyConfigObject = createProjectConfig(JSON.stringify(similarExperimentKeyDatafile)); optimizelySimilarExperimentkeyConfigObject = createOptimizelyConfig(projectSimilarExperimentKeyConfigObject, JSON.stringify(similarExperimentKeyDatafile)); }); @@ -90,7 +89,7 @@ describe('lib/core/optimizely_config', function() { 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 configObj = createProjectConfig(JSON.stringify(datafile)); const logger = { warn: sinon.spy(), diff --git a/lib/project_config/project_config.spec.ts b/lib/project_config/project_config.spec.ts index 6cf68a82d..5fff664b7 100644 --- a/lib/project_config/project_config.spec.ts +++ b/lib/project_config/project_config.spec.ts @@ -28,7 +28,6 @@ import { OptimizelyError } from '../error/optimizly_error'; import { VariableType } from '../shared_types'; import { getMockLogger } from '../tests/mock/mock_logger'; import testDatafile from '../tests/test_data'; -import configValidator from '../utils/config_validator'; import { FEATURE_VARIABLE_TYPES } from '../utils/enums'; import { keyBy, sprintf } from '../utils/fns'; import projectConfig, { ProjectConfig, getGlobalHoldouts, getHoldoutsForRule } from './project_config'; @@ -42,7 +41,7 @@ describe('createProjectConfig', () => { it('should use US region when no region is specified in datafile', () => { const datafile = testDatafile.getTestProjectConfig(); - const config = projectConfig.createProjectConfig(datafile); + const config = projectConfig.createProjectConfig(JSON.stringify(datafile)); expect(config.region).toBe('US'); }); @@ -51,19 +50,19 @@ describe('createProjectConfig', () => { const datafileUs = testDatafile.getTestProjectConfig(); datafileUs.region = 'US'; - const configUs = projectConfig.createProjectConfig(datafileUs); + const configUs = projectConfig.createProjectConfig(JSON.stringify(datafileUs)); expect(configUs.region).toBe('US'); const datafileEu = testDatafile.getTestProjectConfig(); datafileEu.region = 'EU'; - const configEu = projectConfig.createProjectConfig(datafileEu); + const configEu = projectConfig.createProjectConfig(JSON.stringify(datafileEu)); expect(configEu.region).toBe('EU'); }); it('should set properties correctly when createProjectConfig is called', () => { const testData: Record = testDatafile.getTestProjectConfig(); - configObj = projectConfig.createProjectConfig(testData as JSON); + configObj = projectConfig.createProjectConfig(JSON.stringify(testData)); testData.audiences.forEach((audience: any) => { audience.conditions = JSON.parse(audience.conditions); @@ -158,7 +157,7 @@ describe('createProjectConfig', () => { it('should not mutate the datafile', () => { const datafile = testDatafile.getTypedAudiencesConfig(); const datafileClone = cloneDeep(datafile); - projectConfig.createProjectConfig(datafile as any); + projectConfig.createProjectConfig(JSON.stringify(datafile)); expect(datafile).toEqual(datafileClone); }); @@ -168,7 +167,7 @@ describe('createProjectConfig - feature management', () => { let configObj: ProjectConfig; beforeEach(() => { - configObj = projectConfig.createProjectConfig(testDatafile.getTestProjectConfigWithFeatures()); + configObj = projectConfig.createProjectConfig(JSON.stringify(testDatafile.getTestProjectConfigWithFeatures())); }); it('should create a rolloutIdMap from rollouts in the datafile', () => { @@ -242,7 +241,7 @@ describe('createProjectConfig - flag variations', () => { let configObj: ProjectConfig; beforeEach(() => { - configObj = projectConfig.createProjectConfig(testDatafile.getTestDecideProjectConfig()); + configObj = projectConfig.createProjectConfig(JSON.stringify(testDatafile.getTestDecideProjectConfig())); }); it('should populate flagVariationsMap correctly', function() { @@ -260,8 +259,8 @@ describe('createProjectConfig - flag variations', () => { return variation.key; }, {}); - expect(feature1VariationsKeys).toEqual(['a', 'b', '3324490633', '3324490562', '18257766532']); - expect(feature2VariationsKeys).toEqual(['variation_with_traffic', 'variation_no_traffic']); + expect(feature1VariationsKeys.sort()).toEqual(['18257766532', '3324490562', '3324490633', 'a', 'b']); + expect(feature2VariationsKeys.sort()).toEqual(['variation_no_traffic', 'variation_with_traffic']); expect(feature3VariationsKeys).toEqual([]); }); }); @@ -279,7 +278,7 @@ describe('createProjectConfig - cmab experiments', () => { trafficAllocation: 1414, }; - const configObj = projectConfig.createProjectConfig(datafile); + const configObj = projectConfig.createProjectConfig(JSON.stringify(datafile)); const experiment0 = configObj.experiments[0]; expect(experiment0.cmab).toEqual({ @@ -378,7 +377,7 @@ describe('createProjectConfig - holdouts', () => { it('should populate holdouts fields correctly', function() { const datafile = getHoldoutDatafile(); - const configObj = projectConfig.createProjectConfig(JSON.parse(JSON.stringify(datafile))); + const configObj = projectConfig.createProjectConfig(JSON.stringify(datafile)); expect(configObj.holdouts).toHaveLength(3); configObj.holdouts.forEach((holdout, i) => { @@ -398,7 +397,7 @@ describe('createProjectConfig - holdouts', () => { it('should handle empty holdouts array', function() { const datafile = testDatafile.getTestProjectConfig(); - const configObj = projectConfig.createProjectConfig(datafile); + const configObj = projectConfig.createProjectConfig(JSON.stringify(datafile)); expect(configObj.holdouts).toEqual([]); expect(configObj.holdoutIdMap).toEqual({}); @@ -409,7 +408,7 @@ describe('createProjectConfig - holdouts', () => { datafile.holdouts[0].includedFlags = undefined; datafile.holdouts[0].excludedFlags = undefined; - const configObj = projectConfig.createProjectConfig(JSON.parse(JSON.stringify(datafile))); + const configObj = projectConfig.createProjectConfig(JSON.stringify(datafile)); expect(configObj.holdouts).toHaveLength(3); expect(configObj.holdouts[0].includedFlags).toEqual([]); @@ -455,19 +454,19 @@ describe('createProjectConfig - local holdouts (FSSDK-12369, FSSDK-12760)', () = }; it('should set isGlobal=true for entries in the holdouts section (backward compat with old datafiles)', () => { - const config = projectConfig.createProjectConfig(cloneDeep(makeHoldoutsDatafile()) as any); + const config = projectConfig.createProjectConfig(JSON.stringify(makeHoldoutsDatafile())); const holdout = config.holdoutIdMap!['global_holdout_id']; expect(holdout.isGlobal).toBe(true); }); it('should set isGlobal=false for entries in the localHoldouts section', () => { - const config = projectConfig.createProjectConfig(cloneDeep(makeHoldoutsDatafile()) as any); + const config = projectConfig.createProjectConfig(JSON.stringify(makeHoldoutsDatafile())); const holdout = config.holdoutIdMap!['local_holdout_rule_a_id']; expect(holdout.isGlobal).toBe(false); }); it('getGlobalHoldouts should return only entries from the holdouts section', () => { - const config = projectConfig.createProjectConfig(cloneDeep(makeHoldoutsDatafile()) as any); + const config = projectConfig.createProjectConfig(JSON.stringify(makeHoldoutsDatafile())); const globals = getGlobalHoldouts(config); const globalIds = globals.map(h => h.id); expect(globalIds).toContain('global_holdout_id'); @@ -475,7 +474,7 @@ describe('createProjectConfig - local holdouts (FSSDK-12369, FSSDK-12760)', () = }); it('getHoldoutsForRule should return local holdouts targeting the given rule ID', () => { - const config = projectConfig.createProjectConfig(cloneDeep(makeHoldoutsDatafile()) as any); + const config = projectConfig.createProjectConfig(JSON.stringify(makeHoldoutsDatafile())); const forRuleA = getHoldoutsForRule(config, 'rule_a'); expect(forRuleA).toHaveLength(1); expect(forRuleA[0].id).toBe('local_holdout_rule_a_id'); @@ -525,7 +524,7 @@ describe('createProjectConfig - local holdouts (FSSDK-12369, FSSDK-12760)', () = trafficAllocation: [{ entityId: 'var_both', endOfRange: 5000 }], }, ]; - const config = projectConfig.createProjectConfig(datafile as any); + const config = projectConfig.createProjectConfig(JSON.stringify(datafile)); const forRuleX = getHoldoutsForRule(config, 'rule_x'); const forRuleXIds = forRuleX.map(h => h.id).sort(); @@ -556,7 +555,7 @@ describe('createProjectConfig - local holdouts (FSSDK-12369, FSSDK-12760)', () = trafficAllocation: [{ entityId: 'var_local', endOfRange: 5000 }], }, ]; - const config = projectConfig.createProjectConfig(datafile as any); + const config = projectConfig.createProjectConfig(JSON.stringify(datafile)); const forRuleZ = getHoldoutsForRule(config, 'rule_z'); expect(forRuleZ).toHaveLength(1); @@ -567,7 +566,7 @@ describe('createProjectConfig - local holdouts (FSSDK-12369, FSSDK-12760)', () = }); it('getHoldoutsForRule should return empty array for an unknown rule ID', () => { - const config = projectConfig.createProjectConfig(cloneDeep(makeHoldoutsDatafile()) as any); + const config = projectConfig.createProjectConfig(JSON.stringify(makeHoldoutsDatafile())); const forUnknown = getHoldoutsForRule(config, 'nonexistent_rule'); expect(forUnknown).toHaveLength(0); }); @@ -589,19 +588,19 @@ describe('createProjectConfig - local holdouts (FSSDK-12369, FSSDK-12760)', () = trafficAllocation: [{ entityId: 'only_local_var_id', endOfRange: 5000 }], }, ]; - const config = projectConfig.createProjectConfig(datafile as any); + const config = projectConfig.createProjectConfig(JSON.stringify(datafile)); expect(getGlobalHoldouts(config)).toHaveLength(0); }); it('should handle datafile with no holdouts gracefully', () => { const datafile = testDatafile.getTestProjectConfig(); - const config = projectConfig.createProjectConfig(datafile as any); + const config = projectConfig.createProjectConfig(JSON.stringify(datafile)); expect(getGlobalHoldouts(config)).toHaveLength(0); expect(getHoldoutsForRule(config, 'any_rule')).toHaveLength(0); }); it('a single local holdout targeting multiple rules should appear for each targeted rule', () => { - const config = projectConfig.createProjectConfig(cloneDeep(makeHoldoutsDatafile()) as any); + const config = projectConfig.createProjectConfig(JSON.stringify(makeHoldoutsDatafile())); const forRuleA = getHoldoutsForRule(config, 'rule_a'); const forRuleB = getHoldoutsForRule(config, 'rule_b'); // Both rule_a and rule_b point to the same holdout @@ -646,7 +645,7 @@ describe('createProjectConfig - localHoldouts section (FSSDK-12760)', () => { const datafile = cloneDeep(makeBaseDatafile()) as any; datafile.holdouts = []; datafile.localHoldouts = [makeLocal('l1', 'local_h', ['rule_x'])]; - const config = projectConfig.createProjectConfig(datafile); + const config = projectConfig.createProjectConfig(JSON.stringify(datafile)); expect(Array.isArray(config.localHoldouts)).toBe(true); expect(config.localHoldouts).toHaveLength(1); expect(config.localHoldouts[0].id).toBe('l1'); @@ -656,7 +655,7 @@ describe('createProjectConfig - localHoldouts section (FSSDK-12760)', () => { const datafile = cloneDeep(makeBaseDatafile()) as any; datafile.holdouts = [makeGlobal('g1', 'global_h')]; // No localHoldouts key at all - const config = projectConfig.createProjectConfig(datafile); + const config = projectConfig.createProjectConfig(JSON.stringify(datafile)); expect(config.localHoldouts).toEqual([]); expect(getGlobalHoldouts(config).map(h => h.id)).toContain('g1'); expect(getHoldoutsForRule(config, 'any_rule')).toEqual([]); @@ -670,7 +669,7 @@ describe('createProjectConfig - localHoldouts section (FSSDK-12760)', () => { makeGlobal('stray', 'stray_global', { includedRules: ['rule_should_be_ignored'] }), ]; datafile.localHoldouts = []; - const config = projectConfig.createProjectConfig(datafile); + const config = projectConfig.createProjectConfig(JSON.stringify(datafile)); const stray = config.holdoutIdMap!['stray']; // includedRules must be stripped at parse time @@ -689,7 +688,7 @@ describe('createProjectConfig - localHoldouts section (FSSDK-12760)', () => { datafile.holdouts = [ makeGlobal('stray', 'stray_global', { includedRules: ['rule_x'] }), ]; - projectConfig.createProjectConfig(datafile); + projectConfig.createProjectConfig(JSON.stringify(datafile)); // Original datafile still has includedRules on the entry expect(datafile.holdouts[0].includedRules).toEqual(['rule_x']); }); @@ -701,7 +700,7 @@ describe('createProjectConfig - localHoldouts section (FSSDK-12760)', () => { makeLocal('l1', 'l1', ['rule_a']), makeLocal('l2', 'l2', ['rule_b']), ]; - const config = projectConfig.createProjectConfig(datafile); + const config = projectConfig.createProjectConfig(JSON.stringify(datafile)); expect(getGlobalHoldouts(config).map(h => h.id).sort()).toEqual(['g1', 'g2']); expect(getHoldoutsForRule(config, 'rule_a').map(h => h.id)).toEqual(['l1']); @@ -719,7 +718,7 @@ describe('createProjectConfig - localHoldouts section (FSSDK-12760)', () => { delete invalid.includedRules; datafile.localHoldouts = [invalid]; - const config = projectConfig.createProjectConfig(datafile, null, logger); + const config = projectConfig.createProjectConfig(JSON.stringify(datafile), { logger }); expect(getGlobalHoldouts(config)).toEqual([]); expect(getHoldoutsForRule(config, 'any_rule')).toEqual([]); @@ -734,7 +733,7 @@ describe('createProjectConfig - localHoldouts section (FSSDK-12760)', () => { datafile.holdouts = []; datafile.localHoldouts = [makeLocal('bad_null', 'null_local', null)]; - const config = projectConfig.createProjectConfig(datafile, null, logger); + const config = projectConfig.createProjectConfig(JSON.stringify(datafile), { logger }); expect(getHoldoutsForRule(config, 'any_rule')).toEqual([]); expect(config.holdoutIdMap!['bad_null']).toBeUndefined(); @@ -748,7 +747,7 @@ describe('createProjectConfig - localHoldouts section (FSSDK-12760)', () => { datafile.holdouts = []; datafile.localHoldouts = [makeLocal('bad_empty', 'empty_local', [])]; - const config = projectConfig.createProjectConfig(datafile, null, logger); + const config = projectConfig.createProjectConfig(JSON.stringify(datafile), { logger }); expect(getHoldoutsForRule(config, 'any_rule')).toEqual([]); expect(config.holdoutIdMap!['bad_empty']).toBeUndefined(); @@ -761,7 +760,7 @@ describe('createProjectConfig - localHoldouts section (FSSDK-12760)', () => { const datafile = cloneDeep(makeBaseDatafile()) as any; datafile.holdouts = [makeGlobal('g1', 'g')]; datafile.localHoldouts = [makeLocal('l1', 'l', ['rule_x'])]; - const config = projectConfig.createProjectConfig(datafile); + const config = projectConfig.createProjectConfig(JSON.stringify(datafile)); expect(config.variationIdMap['g1_var']).toBeDefined(); expect(config.variationIdMap['l1_var']).toBeDefined(); }); @@ -774,7 +773,7 @@ describe('getExperimentId', () => { beforeEach(function() { testData = cloneDeep(testDatafile.getTestProjectConfig()); - configObj = projectConfig.createProjectConfig(cloneDeep(testData) as JSON); + configObj = projectConfig.createProjectConfig(JSON.stringify(testData)); createdLogger = getMockLogger(); }); @@ -800,7 +799,7 @@ describe('getLayerId', () => { beforeEach(function() { testData = cloneDeep(testDatafile.getTestProjectConfig()); - configObj = projectConfig.createProjectConfig(cloneDeep(testData) as JSON); + configObj = projectConfig.createProjectConfig(JSON.stringify(testData)); }); it('should retrieve layer ID for valid experiment key in getLayerId', function() { @@ -824,7 +823,7 @@ describe('getAttributeId', () => { beforeEach(function() { testData = cloneDeep(testDatafile.getTestProjectConfig()); - configObj = projectConfig.createProjectConfig(cloneDeep(testData) as JSON); + configObj = projectConfig.createProjectConfig(JSON.stringify(testData)); createdLogger = getMockLogger(); }); @@ -862,7 +861,7 @@ describe('getEventId', () => { beforeEach(function() { testData = cloneDeep(testDatafile.getTestProjectConfig()); - configObj = projectConfig.createProjectConfig(cloneDeep(testData) as JSON); + configObj = projectConfig.createProjectConfig(JSON.stringify(testData)); }); it('should retrieve event ID for valid event key in getEventId', function() { @@ -880,7 +879,7 @@ describe('getExperimentStatus', () => { beforeEach(function() { testData = cloneDeep(testDatafile.getTestProjectConfig()); - configObj = projectConfig.createProjectConfig(cloneDeep(testData) as JSON); + configObj = projectConfig.createProjectConfig(JSON.stringify(testData)); }); it('should retrieve experiment status for valid experiment key in getExperimentStatus', function() { @@ -902,7 +901,7 @@ describe('isActive', () => { beforeEach(function() { testData = cloneDeep(testDatafile.getTestProjectConfig()); - configObj = projectConfig.createProjectConfig(cloneDeep(testData) as JSON); + configObj = projectConfig.createProjectConfig(JSON.stringify(testData)); }); it('should return true if experiment status is set to Running in isActive', function() { @@ -920,7 +919,7 @@ describe('isRunning', () => { beforeEach(() => { testData = cloneDeep(testDatafile.getTestProjectConfig()); - configObj = projectConfig.createProjectConfig(cloneDeep(testData) as JSON); + configObj = projectConfig.createProjectConfig(JSON.stringify(testData)); }); it('should return true if experiment status is set to Running in isRunning', function() { @@ -938,7 +937,7 @@ describe('getVariationKeyFromId', () => { beforeEach(function() { testData = cloneDeep(testDatafile.getTestProjectConfig()); - configObj = projectConfig.createProjectConfig(cloneDeep(testData) as JSON); + configObj = projectConfig.createProjectConfig(JSON.stringify(testData)); }); 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( @@ -953,7 +952,7 @@ describe('getTrafficAllocation', () => { beforeEach(function() { testData = cloneDeep(testDatafile.getTestProjectConfig()); - configObj = projectConfig.createProjectConfig(cloneDeep(testData) as JSON); + configObj = projectConfig.createProjectConfig(JSON.stringify(testData)); }); it('should retrieve traffic allocation given valid experiment key in getTrafficAllocation', function() { @@ -980,7 +979,7 @@ describe('getVariationIdFromExperimentAndVariationKey', () => { beforeEach(function() { testData = cloneDeep(testDatafile.getTestProjectConfig()); - configObj = projectConfig.createProjectConfig(cloneDeep(testData) as JSON); + configObj = projectConfig.createProjectConfig(JSON.stringify(testData)); }); it('should return the variation id for the given experiment key and variation key', () => { @@ -1000,7 +999,7 @@ describe('getSendFlagDecisionsValue', () => { beforeEach(function() { testData = cloneDeep(testDatafile.getTestProjectConfig()); - configObj = projectConfig.createProjectConfig(cloneDeep(testData) as JSON); + configObj = projectConfig.createProjectConfig(JSON.stringify(testData)); }); it('should return false when sendFlagDecisions is undefined', () => { @@ -1028,7 +1027,7 @@ describe('getVariableForFeature', function() { beforeEach(() => { featureManagementLogger = getMockLogger(); - configObj = projectConfig.createProjectConfig(testDatafile.getTestProjectConfigWithFeatures()); + configObj = projectConfig.createProjectConfig(JSON.stringify(testDatafile.getTestProjectConfigWithFeatures())); }); afterEach(() => { @@ -1089,7 +1088,7 @@ describe('getVariableValueForVariation', () => { beforeEach(() => { featureManagementLogger = getMockLogger(); - configObj = projectConfig.createProjectConfig(testDatafile.getTestProjectConfigWithFeatures()); + configObj = projectConfig.createProjectConfig(JSON.stringify(testDatafile.getTestProjectConfigWithFeatures())); }); afterEach(() => { @@ -1177,7 +1176,7 @@ describe('getTypeCastValue', () => { beforeEach(() => { featureManagementLogger = getMockLogger(); - configObj = projectConfig.createProjectConfig(testDatafile.getTestProjectConfigWithFeatures()); + configObj = projectConfig.createProjectConfig(JSON.stringify(testDatafile.getTestProjectConfigWithFeatures())); }); afterEach(() => { @@ -1310,7 +1309,7 @@ describe('getAudiencesById', () => { let configObj: ProjectConfig; beforeEach(() => { - configObj = projectConfig.createProjectConfig(testDatafile.getTypedAudiencesConfig()); + configObj = projectConfig.createProjectConfig(JSON.stringify(testDatafile.getTypedAudiencesConfig())); }); it('should retrieve audiences by checking first in typedAudiences, and then second in audiences', () => { @@ -1327,13 +1326,13 @@ describe('getExperimentAudienceConditions', () => { }); it('should retrieve audiences for valid experiment key', () => { - configObj = projectConfig.createProjectConfig(cloneDeep(testData) as JSON); + configObj = projectConfig.createProjectConfig(JSON.stringify(testData)); 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); + configObj = projectConfig.createProjectConfig(JSON.stringify(testData)); expect(() => { projectConfig.getExperimentAudienceConditions(configObj, 'invalidExperimentId'); @@ -1346,7 +1345,7 @@ describe('getExperimentAudienceConditions', () => { }); it('should return experiment audienceIds if experiment has no audienceConditions', () => { - configObj = projectConfig.createProjectConfig(testDatafile.getTypedAudiencesConfig()); + configObj = projectConfig.createProjectConfig(JSON.stringify(testDatafile.getTypedAudiencesConfig())); const result = projectConfig.getExperimentAudienceConditions(configObj, '11564051718'); expect(result).toEqual([ @@ -1361,7 +1360,7 @@ describe('getExperimentAudienceConditions', () => { }); it('should return experiment audienceConditions if experiment has audienceConditions', () => { - configObj = projectConfig.createProjectConfig(testDatafile.getTypedAudiencesConfig()); + configObj = projectConfig.createProjectConfig(JSON.stringify(testDatafile.getTypedAudiencesConfig())); // audience_combinations_experiment has both audienceConditions and audienceIds // audienceConditions should be preferred over audienceIds const result = projectConfig.getExperimentAudienceConditions(configObj, '1323241598'); @@ -1376,21 +1375,21 @@ describe('getExperimentAudienceConditions', () => { describe('isFeatureExperiment', () => { it('should return true for a feature test', () => { - const config = projectConfig.createProjectConfig(testDatafile.getTestProjectConfigWithFeatures()); + const config = projectConfig.createProjectConfig(JSON.stringify(testDatafile.getTestProjectConfigWithFeatures())); const result = projectConfig.isFeatureExperiment(config, '594098'); // id of 'testing_my_feature' expect(result).toBe(true); }); it('should return false for an A/B test', () => { - const config = projectConfig.createProjectConfig(testDatafile.getTestProjectConfig()); + const config = projectConfig.createProjectConfig(JSON.stringify(testDatafile.getTestProjectConfig())); const result = projectConfig.isFeatureExperiment(config, '111127'); // id of 'testExperiment' expect(result).toBe(false); }); it('should return true for a feature test in a mutex group', () => { - const config = projectConfig.createProjectConfig(testDatafile.getMutexFeatureTestsConfig()); + const config = projectConfig.createProjectConfig(JSON.stringify(testDatafile.getMutexFeatureTestsConfig())); let result = projectConfig.isFeatureExperiment(config, '17128410791'); // id of 'f_test1' expect(result).toBe(true); @@ -1457,7 +1456,7 @@ describe('integrations: with segments', () => { let configObj: ProjectConfig; beforeEach(() => { - configObj = projectConfig.createProjectConfig(testDatafile.getOdpIntegratedConfigWithSegments()); + configObj = projectConfig.createProjectConfig(JSON.stringify(testDatafile.getOdpIntegratedConfigWithSegments())); }); it('should convert integrations from the datafile into the project config', () => { @@ -1484,7 +1483,7 @@ describe('integrations: with segments', () => { describe('integrations: without segments', () => { let config: ProjectConfig; beforeEach(() => { - config = projectConfig.createProjectConfig(testDatafile.getOdpIntegratedConfigWithoutSegments()); + config = projectConfig.createProjectConfig(JSON.stringify(testDatafile.getOdpIntegratedConfigWithoutSegments())); }); it('should convert integrations from the datafile into the project config', () => { @@ -1508,7 +1507,7 @@ 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); + expect(() => projectConfig.createProjectConfig(JSON.stringify(odpIntegratedConfigWithoutKey))).toThrowError(OptimizelyError); }); }); @@ -1518,7 +1517,7 @@ describe('without integrations', () => { beforeEach(() => { const odpIntegratedConfigWithSegments = testDatafile.getOdpIntegratedConfigWithSegments(); const noIntegrationsConfigWithSegments = { ...odpIntegratedConfigWithSegments, integrations: [] }; - config = projectConfig.createProjectConfig(noIntegrationsConfigWithSegments); + config = projectConfig.createProjectConfig(JSON.stringify(noIntegrationsConfigWithSegments)); }); it('should convert integrations from the datafile into the project config', () => { @@ -1533,25 +1532,23 @@ describe('without integrations', () => { }); }); -describe('tryCreatingProjectConfig', () => { +describe('createProjectConfig with options', () => { let mockJsonSchemaValidator: Mock; beforeEach(() => { mockJsonSchemaValidator = vi.fn().mockReturnValue(true); - vi.spyOn(configValidator, 'validateDatafile').mockReturnValue(true); }); afterEach(() => { vi.restoreAllMocks(); }); - it('should return a project config object created by createProjectConfig when all validation is applied and there are no errors', () => { + it('should return a project config object when all validation is applied and there are no errors', () => { const configDatafile = { + version: '4', foo: 'bar', experiments: [{ key: 'a' }, { key: 'b' }], }; - vi.spyOn(configValidator, 'validateDatafile').mockReturnValueOnce(configDatafile); - const configObj = { foo: 'bar', experimentKeyMap: { @@ -1560,11 +1557,9 @@ describe('tryCreatingProjectConfig', () => { }, }; - // stubJsonSchemaValidator.returns(true); mockJsonSchemaValidator.mockReturnValueOnce(true); - const result = projectConfig.tryCreatingProjectConfig({ - datafile: configDatafile, + const result = projectConfig.createProjectConfig(JSON.stringify(configDatafile), { jsonSchemaValidator: mockJsonSchemaValidator, logger: logger, }); @@ -1572,15 +1567,11 @@ describe('tryCreatingProjectConfig', () => { expect(result).toMatchObject(configObj); }); - it('should throw an error when validateDatafile throws', function() { - vi.spyOn(configValidator, 'validateDatafile').mockImplementationOnce(() => { - throw new Error(); - }); + it('should throw an error when datafile is invalid JSON', function() { mockJsonSchemaValidator.mockReturnValueOnce(true); expect(() => - projectConfig.tryCreatingProjectConfig({ - datafile: { foo: 'bar' }, + projectConfig.createProjectConfig('invalid json', { jsonSchemaValidator: mockJsonSchemaValidator, logger: logger, }) @@ -1588,14 +1579,13 @@ describe('tryCreatingProjectConfig', () => { }); it('should throw an error when jsonSchemaValidator.validate throws', function() { - vi.spyOn(configValidator, 'validateDatafile').mockReturnValueOnce(true); + const configDatafile = { version: '4', foo: 'bar' }; mockJsonSchemaValidator.mockImplementationOnce(() => { throw new Error(); }); expect(() => - projectConfig.tryCreatingProjectConfig({ - datafile: { foo: 'bar' }, + projectConfig.createProjectConfig(JSON.stringify(configDatafile), { jsonSchemaValidator: mockJsonSchemaValidator, logger: logger, }) @@ -1604,12 +1594,11 @@ describe('tryCreatingProjectConfig', () => { it('should skip json validation when jsonSchemaValidator is not provided', function() { const configDatafile = { + version: '4', foo: 'bar', experiments: [{ key: 'a' }, { key: 'b' }], }; - vi.spyOn(configValidator, 'validateDatafile').mockReturnValueOnce(configDatafile); - const configObj = { foo: 'bar', experimentKeyMap: { @@ -1618,8 +1607,7 @@ describe('tryCreatingProjectConfig', () => { }, }; - const result = projectConfig.tryCreatingProjectConfig({ - datafile: configDatafile, + const result = projectConfig.createProjectConfig(JSON.stringify(configDatafile), { logger: logger, }); @@ -1714,14 +1702,14 @@ describe('Feature Rollout support', () => { it('should preserve type=undefined for experiments without type field (backward compatibility)', () => { const datafile = makeDatafile(); - const config = projectConfig.createProjectConfig(datafile as any); + const config = projectConfig.createProjectConfig(JSON.stringify(datafile)); const abExperiment = config.experimentIdMap['exp_ab']; expect(abExperiment.type).toBeUndefined(); }); it('should inject everyone else variation into fr (feature rollout) experiments', () => { const datafile = makeDatafile(); - const config = projectConfig.createProjectConfig(datafile as any); + const config = projectConfig.createProjectConfig(JSON.stringify(datafile)); const rolloutExperiment = config.experimentIdMap['exp_rollout']; // Should have 2 variations: original + injected everyone else @@ -1737,7 +1725,7 @@ describe('Feature Rollout support', () => { it('should update variation lookup maps with injected variation', () => { const datafile = makeDatafile(); - const config = projectConfig.createProjectConfig(datafile as any); + const config = projectConfig.createProjectConfig(JSON.stringify(datafile)); const rolloutExperiment = config.experimentIdMap['exp_rollout']; // variationKeyMap on the experiment should contain the injected variation @@ -1751,7 +1739,7 @@ describe('Feature Rollout support', () => { it('should not modify non-rollout experiments (A/B, MAB, CMAB)', () => { const datafile = makeDatafile(); - const config = projectConfig.createProjectConfig(datafile as any); + const config = projectConfig.createProjectConfig(JSON.stringify(datafile)); const abExperiment = config.experimentIdMap['exp_ab']; // A/B experiment should still have only 1 variation @@ -1772,7 +1760,7 @@ describe('Feature Rollout support', () => { }, ], }); - const config = projectConfig.createProjectConfig(datafile as any); + const config = projectConfig.createProjectConfig(JSON.stringify(datafile)); const rolloutExperiment = config.experimentIdMap['exp_rollout']; // Should still have only 1 variation (no injection) @@ -1782,7 +1770,7 @@ describe('Feature Rollout support', () => { it('should correctly preserve experiment type field from datafile', () => { const datafile = makeDatafile(); - const config = projectConfig.createProjectConfig(datafile as any); + const config = projectConfig.createProjectConfig(JSON.stringify(datafile)); const rolloutExperiment = config.experimentIdMap['exp_rollout']; expect(rolloutExperiment.type).toBe('fr'); }); diff --git a/lib/project_config/project_config.tests.js b/lib/project_config/project_config.tests.js index d69afda46..fb129a450 100644 --- a/lib/project_config/project_config.tests.js +++ b/lib/project_config/project_config.tests.js @@ -21,7 +21,6 @@ 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, @@ -47,7 +46,7 @@ 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); + var configObj = projectConfig.createProjectConfig(JSON.stringify(testData)); forEach(testData.audiences, function(audience) { audience.conditions = JSON.parse(audience.conditions); @@ -187,14 +186,14 @@ describe('lib/core/project_config', function() { it('should not mutate the datafile', function() { var datafile = testDatafile.getTypedAudiencesConfig(); var datafileClone = cloneDeep(datafile); - projectConfig.createProjectConfig(datafile); + projectConfig.createProjectConfig(JSON.stringify(datafile)); assert.deepEqual(datafileClone, datafile); }); describe('feature management', function() { var configObj; beforeEach(function() { - configObj = projectConfig.createProjectConfig(testDatafile.getTestProjectConfigWithFeatures()); + configObj = projectConfig.createProjectConfig(JSON.stringify(testDatafile.getTestProjectConfigWithFeatures())); }); it('creates a rolloutIdMap from rollouts in the datafile', function() { @@ -265,7 +264,7 @@ describe('lib/core/project_config', function() { describe('flag variations', function() { var configObj; beforeEach(function() { - configObj = projectConfig.createProjectConfig(testDatafile.getTestDecideProjectConfig()); + configObj = projectConfig.createProjectConfig(JSON.stringify(testDatafile.getTestDecideProjectConfig())); }); it('it should populate flagVariationsMap correctly', function() { @@ -283,8 +282,8 @@ describe('lib/core/project_config', function() { return variation.key; }, {}); - assert.deepEqual(feature1VariationsKeys, ['a', 'b', '3324490633', '3324490562', '18257766532']); - assert.deepEqual(feature2VariationsKeys, ['variation_with_traffic', 'variation_no_traffic']); + assert.deepEqual(feature1VariationsKeys.sort(), ['18257766532', '3324490562', '3324490633', 'a', 'b']); + assert.deepEqual(feature2VariationsKeys.sort(), ['variation_no_traffic', 'variation_with_traffic']); assert.deepEqual(feature3VariationsKeys, []); }); }); @@ -296,7 +295,7 @@ describe('lib/core/project_config', function() { var createdLogger = createLogger({ logLevel: LOG_LEVEL.INFO }); beforeEach(function() { - configObj = projectConfig.createProjectConfig(cloneDeep(testData)); + configObj = projectConfig.createProjectConfig(JSON.stringify(testData)); sinon.stub(createdLogger, 'warn'); }); @@ -450,7 +449,7 @@ describe('lib/core/project_config', function() { describe('feature management', function() { var featureManagementLogger = createLogger({ logLevel: LOG_LEVEL.INFO }); beforeEach(function() { - configObj = projectConfig.createProjectConfig(testDatafile.getTestProjectConfigWithFeatures()); + configObj = projectConfig.createProjectConfig(JSON.stringify(testDatafile.getTestProjectConfigWithFeatures())); sinon.stub(featureManagementLogger, 'warn'); sinon.stub(featureManagementLogger, 'error'); sinon.stub(featureManagementLogger, 'info'); @@ -676,7 +675,7 @@ describe('lib/core/project_config', function() { describe('#getAudiencesById', function() { beforeEach(function() { - configObj = projectConfig.createProjectConfig(testDatafile.getTypedAudiencesConfig()); + configObj = projectConfig.createProjectConfig(JSON.stringify(testDatafile.getTypedAudiencesConfig())); }); it('should retrieve audiences by checking first in typedAudiences, and then second in audiences', function() { @@ -686,14 +685,14 @@ describe('lib/core/project_config', function() { describe('#getExperimentAudienceConditions', function() { it('should retrieve audiences for valid experiment key', function() { - configObj = projectConfig.createProjectConfig(cloneDeep(testData)); + configObj = projectConfig.createProjectConfig(JSON.stringify(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)); + configObj = projectConfig.createProjectConfig(JSON.stringify(testData)); const ex = assert.throws(function() { projectConfig.getExperimentAudienceConditions(configObj, 'invalidExperimentId'); }); @@ -702,7 +701,7 @@ describe('lib/core/project_config', function() { }); it('should return experiment audienceIds if experiment has no audienceConditions', function() { - configObj = projectConfig.createProjectConfig(testDatafile.getTypedAudiencesConfig()); + configObj = projectConfig.createProjectConfig(JSON.stringify(testDatafile.getTypedAudiencesConfig())); var result = projectConfig.getExperimentAudienceConditions(configObj, '11564051718'); assert.deepEqual(result, [ '3468206642', @@ -716,7 +715,7 @@ describe('lib/core/project_config', function() { }); it('should return experiment audienceConditions if experiment has audienceConditions', function() { - configObj = projectConfig.createProjectConfig(testDatafile.getTypedAudiencesConfig()); + configObj = projectConfig.createProjectConfig(JSON.stringify(testDatafile.getTypedAudiencesConfig())); // audience_combinations_experiment has both audienceConditions and audienceIds // audienceConditions should be preferred over audienceIds var result = projectConfig.getExperimentAudienceConditions(configObj, '1323241598'); @@ -730,19 +729,19 @@ describe('lib/core/project_config', function() { describe('#isFeatureExperiment', function() { it('returns true for a feature test', function() { - var config = projectConfig.createProjectConfig(testDatafile.getTestProjectConfigWithFeatures()); + var config = projectConfig.createProjectConfig(JSON.stringify(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 config = projectConfig.createProjectConfig(JSON.stringify(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 config = projectConfig.createProjectConfig(JSON.stringify(testDatafile.getMutexFeatureTestsConfig())); var result = projectConfig.isFeatureExperiment(config, '17128410791'); // id of 'f_test1' assert.isTrue(result); result = projectConfig.isFeatureExperiment(config, '17139931304'); // id of 'f_test2' @@ -800,13 +799,13 @@ describe('lib/core/project_config', function() { }); it('returns false for an A/B test', function() { - var config = projectConfig.createProjectConfig(testDatafile.getTestProjectConfig()); + var config = projectConfig.createProjectConfig(JSON.stringify(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 config = projectConfig.createProjectConfig(JSON.stringify(testDatafile.getMutexFeatureTestsConfig())); var result = projectConfig.isFeatureExperiment(config, '17128410791'); // id of 'f_test1' assert.isTrue(result); result = projectConfig.isFeatureExperiment(config, '17139931304'); // id of 'f_test2' @@ -819,7 +818,7 @@ describe('lib/core/project_config', function() { describe('#withSegments', () => { var config; beforeEach(() => { - config = projectConfig.createProjectConfig(testDatafile.getOdpIntegratedConfigWithSegments()); + config = projectConfig.createProjectConfig(JSON.stringify(testDatafile.getOdpIntegratedConfigWithSegments())); }); it('should convert integrations from the datafile into the project config', () => { @@ -839,7 +838,7 @@ describe('lib/core/project_config', function() { describe('#withoutSegments', () => { var config; beforeEach(() => { - config = projectConfig.createProjectConfig(testDatafile.getOdpIntegratedConfigWithoutSegments()); + config = projectConfig.createProjectConfig(JSON.stringify(testDatafile.getOdpIntegratedConfigWithoutSegments())); }); it('should convert integrations from the datafile into the project config', () => { @@ -860,7 +859,7 @@ describe('lib/core/project_config', function() { 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); + projectConfig.createProjectConfig(JSON.stringify(odpIntegratedConfigWithoutKey)); }); }); }); @@ -870,7 +869,7 @@ describe('lib/core/project_config', function() { beforeEach(() => { const odpIntegratedConfigWithSegments = testDatafile.getOdpIntegratedConfigWithSegments(); const noIntegrationsConfigWithSegments = { ...odpIntegratedConfigWithSegments, integrations: [] }; - config = projectConfig.createProjectConfig(noIntegrationsConfigWithSegments); + config = projectConfig.createProjectConfig(JSON.stringify(noIntegrationsConfigWithSegments)); }); it('should convert integrations from the datafile into the project config', () => { @@ -885,25 +884,23 @@ describe('lib/core/project_config', function() { }); }); -describe('#tryCreatingProjectConfig', function() { +describe('#createProjectConfig with options', 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() { + it('returns a project config object when all validation is applied and there are no errors', function() { var configDatafile = { + version: '4', foo: 'bar', experiments: [{ key: 'a' }, { key: 'b' }], }; - configValidator.validateDatafile.returns(configDatafile); var configObj = { foo: 'bar', experimentKeyMap: { @@ -914,8 +911,7 @@ describe('#tryCreatingProjectConfig', function() { stubJsonSchemaValidator.returns(true); - var result = projectConfig.tryCreatingProjectConfig({ - datafile: configDatafile, + var result = projectConfig.createProjectConfig(JSON.stringify(configDatafile), { jsonSchemaValidator: stubJsonSchemaValidator, logger: logger, }); @@ -923,12 +919,10 @@ describe('#tryCreatingProjectConfig', function() { assert.deepInclude(result, configObj); }); - it('throws an error when validateDatafile throws', function() { - configValidator.validateDatafile.throws(); + it('throws an error when datafile is invalid JSON', function() { stubJsonSchemaValidator.returns(true); assert.throws(() => { - projectConfig.tryCreatingProjectConfig({ - datafile: { foo: 'bar' }, + projectConfig.createProjectConfig('invalid json', { jsonSchemaValidator: stubJsonSchemaValidator, logger: logger, }); @@ -936,11 +930,10 @@ describe('#tryCreatingProjectConfig', function() { }); it('throws an error when jsonSchemaValidator.validate throws', function() { - configValidator.validateDatafile.returns(true); + var configDatafile = { version: '4', foo: 'bar' }; stubJsonSchemaValidator.throws(); assert.throws(() => { - projectConfig.tryCreatingProjectConfig({ - datafile: { foo: 'bar' }, + projectConfig.createProjectConfig(JSON.stringify(configDatafile), { jsonSchemaValidator: stubJsonSchemaValidator, logger: logger, }); @@ -949,12 +942,11 @@ describe('#tryCreatingProjectConfig', function() { it('skips json validation when jsonSchemaValidator is not provided', function() { var configDatafile = { + version: '4', foo: 'bar', experiments: [{ key: 'a' }, { key: 'b' }], }; - configValidator.validateDatafile.returns(configDatafile); - var configObj = { foo: 'bar', experimentKeyMap: { @@ -963,8 +955,7 @@ describe('#tryCreatingProjectConfig', function() { }, }; - var result = projectConfig.tryCreatingProjectConfig({ - datafile: configDatafile, + var result = projectConfig.createProjectConfig(JSON.stringify(configDatafile), { logger: logger, }); diff --git a/lib/project_config/project_config.ts b/lib/project_config/project_config.ts index 41f8fb6d0..5496e1d01 100644 --- a/lib/project_config/project_config.ts +++ b/lib/project_config/project_config.ts @@ -13,10 +13,13 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { find, objectEntries, objectValues, keyBy, assignBy } from '../utils/fns'; +import { find, keyBy, assignBy } from '../utils/fns'; -import { EXPERIMENT_TYPES, FEATURE_VARIABLE_TYPES } from '../utils/enums'; -import configValidator from '../utils/config_validator'; +import { DATAFILE_VERSIONS, EXPERIMENT_TYPES, FEATURE_VARIABLE_TYPES } from '../utils/enums'; +import { + INVALID_DATAFILE_MALFORMED, + INVALID_DATAFILE_VERSION, +} from 'error_message'; import { LoggerFacade } from '../logging/logger'; @@ -54,10 +57,7 @@ import { import { SKIPPING_JSON_VALIDATION, VALID_DATAFILE } from 'log_message'; import { OptimizelyError } from '../error/optimizly_error'; import { Platform } from '../platform_support'; -interface TryCreatingProjectConfigConfig { - // TODO[OASIS-6649]: Don't use object type - // eslint-disable-next-line @typescript-eslint/ban-types - datafile: string | object; +interface CreateProjectConfigOptions { jsonSchemaValidator?: Transformer; logger?: LoggerFacade; } @@ -110,7 +110,7 @@ export interface ProjectConfig { flagRulesMap: { [key: string]: Experiment[] }; flagVariationsMap: { [key: string]: Variation[] }; integrations: Integration[]; - integrationKeyMap?: { [key: string]: Integration }; + integrationKeyMap: { [key: string]: Integration }; odpIntegrationConfig: OdpIntegrationConfig; holdouts: Holdout[]; /** @@ -118,7 +118,7 @@ export interface ProjectConfig { * datafile section. Absent in older datafiles — defaults to an empty array. */ localHoldouts: Holdout[]; - holdoutIdMap?: { [id: string]: Holdout }; + holdoutIdMap: { [id: string]: Holdout }; /** Maps a rule ID to the local holdouts that target it. */ ruleHoldoutsMap: { [ruleId: string]: Holdout[] }; } @@ -126,71 +126,9 @@ export interface ProjectConfig { const EXPERIMENT_RUNNING_STATUS = 'Running'; const RESERVED_ATTRIBUTE_PREFIX = '$opt_'; -// eslint-disable-next-line @typescript-eslint/no-explicit-any -function createMutationSafeDatafileCopy(datafile: any): ProjectConfig { - const datafileCopy = { ...datafile }; - - datafileCopy.audiences = (datafile.audiences || []).map((audience: Audience) => { - return { ...audience }; - }); - datafileCopy.experiments = (datafile.experiments || []).map((experiment: Experiment) => { - return { ...experiment }; - }); - datafileCopy.featureFlags = (datafile.featureFlags || []).map((featureFlag: FeatureFlag) => { - return { ...featureFlag }; - }); - datafileCopy.groups = (datafile.groups || []).map((group: Group) => { - const groupCopy = { ...group }; - groupCopy.experiments = (group.experiments || []).map(experiment => { - return { ...experiment }; - }); - return groupCopy; - }); - datafileCopy.rollouts = (datafile.rollouts || []).map((rollout: Rollout) => { - const rolloutCopy = { ...rollout }; - rolloutCopy.experiments = (rollout.experiments || []).map(experiment => { - return { ...experiment }; - }); - return rolloutCopy; - }); - - // Shallow-copy each holdout entry so per-entry mutations (e.g. stripping - // `includedRules` from global-section entries) don't bleed into the caller's - // datafile object. - datafileCopy.holdouts = (datafile.holdouts || []).map((holdout: Holdout) => { - return { ...holdout }; - }); - datafileCopy.localHoldouts = (datafile.localHoldouts || []).map((holdout: Holdout) => { - return { ...holdout }; - }); - - datafileCopy.environmentKey = datafile.environmentKey ?? ''; - datafileCopy.sdkKey = datafile.sdkKey ?? ''; - - return datafileCopy; -} - -/** - * Creates projectConfig object to be used for quick project property lookup - * @param {Object} datafileObj JSON datafile representing the project - * @param {string|null} datafileStr JSON string representation of the datafile - * @param {LoggerFacade} logger Optional logger for parse-time diagnostics - * (e.g. invalid local-holdout entries). - * @return {ProjectConfig} Object representing project configuration - */ -export const createProjectConfig = function( - datafileObj?: JSON, - datafileStr: string | null = null, - logger?: LoggerFacade -): ProjectConfig { - const projectConfig = createMutationSafeDatafileCopy(datafileObj); - - if (!projectConfig.region) { - projectConfig.region = 'US'; // Default to US region if not specified - } - - projectConfig.__datafileStr = datafileStr === null ? JSON.stringify(datafileObj) : datafileStr; +const SUPPORTED_DATAFILE_VERSIONS = Object.values(DATAFILE_VERSIONS); +const parseAudienceConfig = (projectConfig: ProjectConfig) => { /* * Conditions of audiences in projectConfig.typedAudiences are not * expected to be string-encoded as they are here in projectConfig.audiences. @@ -203,7 +141,9 @@ export const createProjectConfig = function( projectConfig.audiencesById = {}; assignBy(projectConfig.audiences, 'id', projectConfig.audiencesById); assignBy(projectConfig.typedAudiences, 'id', projectConfig.audiencesById); +} +const parseAttributeConfig = (projectConfig: ProjectConfig) => { projectConfig.attributes = projectConfig.attributes || []; projectConfig.attributeKeyMap = {}; projectConfig.attributeIdMap = {}; @@ -211,38 +151,40 @@ export const createProjectConfig = function( projectConfig.attributeKeyMap[attribute.key] = attribute; projectConfig.attributeIdMap[attribute.id] = attribute; }); +} - projectConfig.eventKeyMap = keyBy(projectConfig.events, 'key'); - projectConfig.groupIdMap = keyBy(projectConfig.groups, 'id'); +const parseGroupConfig = (projectConfig: ProjectConfig) => { + projectConfig.groupIdMap = {}; - let experiments; - Object.keys(projectConfig.groupIdMap || {}).forEach(Id => { - experiments = projectConfig.groupIdMap[Id].experiments; - (experiments || []).forEach(experiment => { - experiment.groupId = Id; + (projectConfig.groups || []).forEach((group) => { + projectConfig.groupIdMap[group.id] = group; + + (group.experiments || []).forEach((experiment) => { + experiment.groupId = group.id; projectConfig.experiments.push(experiment); - }); + }) }); +} - projectConfig.rolloutIdMap = keyBy(projectConfig.rollouts || [], 'id'); - objectValues(projectConfig.rolloutIdMap || {}).forEach(rollout => { - (rollout.experiments || []).forEach(experiment => { +const parseRolloutConfig = (projectConfig: ProjectConfig) => { + projectConfig.rolloutIdMap = {}; + (projectConfig.rollouts || []).forEach((rollout) => { + projectConfig.rolloutIdMap[rollout.id] = rollout; + (rollout.experiments || []).forEach((experiment) => { experiment.isRollout = true projectConfig.experiments.push(experiment); - // Creates { : } map inside of the experiment - experiment.variationKeyMap = keyBy(experiment.variations, 'key'); }); }); +} +const parseIntegrationConfig = (projectConfig: ProjectConfig) => { const allSegmentsSet = new Set(); - Object.keys(projectConfig.audiencesById) - .map(audience => getAudienceSegments(projectConfig.audiencesById[audience])) - .forEach(audienceSegments => { - audienceSegments.forEach(segment => { - allSegmentsSet.add(segment); - }); + Object.keys(projectConfig.audiencesById).forEach(id => { + getAudienceSegments(projectConfig.audiencesById[id]).forEach(segment => { + allSegmentsSet.add(segment); }); + }); const allSegments = Array.from(allSegmentsSet); @@ -251,22 +193,22 @@ export const createProjectConfig = function( let odpApiKey = ''; let odpPixelUrl = ''; - if (projectConfig.integrations) { - projectConfig.integrationKeyMap = keyBy(projectConfig.integrations, 'key'); + projectConfig.integrationKeyMap = {}; - projectConfig.integrations.forEach(integration => { - if (!('key' in integration)) { - throw new OptimizelyError(MISSING_INTEGRATION_KEY); - } + (projectConfig.integrations || []).forEach(integration => { + if (!('key' in integration)) { + throw new OptimizelyError(MISSING_INTEGRATION_KEY); + } - if (integration.key === 'odp') { - odpIntegrated = true; - odpApiKey = odpApiKey || integration.publicKey || ''; - odpApiHost = odpApiHost || integration.host || ''; - odpPixelUrl = odpPixelUrl || integration.pixelUrl || ''; - } - }); - } + projectConfig.integrationKeyMap[integration.key] = integration + + if (integration.key === 'odp') { + odpIntegrated = true; + odpApiKey = odpApiKey || integration.publicKey || ''; + odpApiHost = odpApiHost || integration.host || ''; + odpPixelUrl = odpPixelUrl || integration.pixelUrl || ''; + } + }); if (odpIntegrated) { projectConfig.odpIntegrationConfig = { @@ -276,61 +218,76 @@ export const createProjectConfig = function( } else { projectConfig.odpIntegrationConfig = { integrated: false }; } +} - projectConfig.experimentKeyMap = keyBy(projectConfig.experiments, 'key'); - projectConfig.experimentIdMap = keyBy(projectConfig.experiments, 'id'); - +const parseExperimentConfig = (projectConfig: ProjectConfig) => { + projectConfig.experimentKeyMap = {}; + projectConfig.experimentIdMap = {}; projectConfig.variationIdMap = {}; projectConfig.variationVariableUsageMap = {}; - (projectConfig.experiments || []).forEach(experiment => { - // Creates { : } map inside of the experiment - experiment.variationKeyMap = keyBy(experiment.variations, 'key'); - assignBy(experiment.variations, 'id', projectConfig.variationIdMap); + (projectConfig.experiments || []).forEach((experiment) => { + projectConfig.experimentKeyMap[experiment.key] = experiment; + projectConfig.experimentIdMap[experiment.id] = experiment; - objectValues(experiment.variationKeyMap || {}).forEach(variation => { + experiment.variationKeyMap = {}; + (experiment.variations || []).forEach((variation) => { + experiment.variationKeyMap[variation.key] = variation; + projectConfig.variationIdMap[variation.id] = variation; if (variation.variables) { projectConfig.variationVariableUsageMap[variation.id] = keyBy(variation.variables, 'id'); } }); }); +} +const parseFeatureFlagConfig = (projectConfig: ProjectConfig) => { // Object containing experiment Ids that exist in any feature // for checking that experiment is a feature experiment or not. projectConfig.experimentFeatureMap = {}; + projectConfig.featureKeyMap = {} + + // all rules (experiment rules and delivery rules) for each flag + projectConfig.flagRulesMap = {}; + projectConfig.flagVariationsMap = {}; + + (projectConfig.featureFlags || []).forEach((feature) => { + projectConfig.featureKeyMap[feature.key] = feature; + + feature.variableKeyMap = {} - projectConfig.featureKeyMap = keyBy(projectConfig.featureFlags || [], 'key'); - objectValues(projectConfig.featureKeyMap || {}).forEach(feature => { - // Json type is represented in datafile as a subtype of string for the sake of backwards compatibility. - // Converting it to a first-class json type while creating Project Config feature.variables.forEach(variable => { + // Json type is represented in datafile as a subtype of string for the sake of backwards compatibility. + // Converting it to a first-class json type while creating Project Config if (variable.type === FEATURE_VARIABLE_TYPES.STRING && variable.subType === FEATURE_VARIABLE_TYPES.JSON) { variable.type = FEATURE_VARIABLE_TYPES.JSON as VariableType; delete variable.subType; } + + feature.variableKeyMap[variable.key] = variable; }); - feature.variableKeyMap = keyBy(feature.variables, 'key'); + + const flagRuleExperiments: Experiment[] = []; + + // all variations for the flag mapped by id + const flagVariationsById: Record = {}; + + const everyoneElseVariation = getEveryoneElseVariation(projectConfig, feature); + (feature.experimentIds || []).forEach(experimentId => { - // Add this experiment in experiment-feature map. - if (projectConfig.experimentFeatureMap[experimentId]) { - projectConfig.experimentFeatureMap[experimentId].push(feature.id); - } else { - projectConfig.experimentFeatureMap[experimentId] = [feature.id]; - } - }); - }); - // Inject "everyone else" variation into feature rollout (FR) experiments - (projectConfig.featureFlags || []).forEach(featureFlag => { - const everyoneElseVariation = getEveryoneElseVariation(projectConfig, featureFlag); - if (!everyoneElseVariation) { - return; - } + projectConfig.experimentFeatureMap[experimentId] = + projectConfig.experimentFeatureMap[experimentId] || []; - (featureFlag.experimentIds || []).forEach(experimentId => { + projectConfig.experimentFeatureMap[experimentId].push(feature.id); + const experiment = projectConfig.experimentIdMap[experimentId]; - if (experiment && experiment.type === EXPERIMENT_TYPES.FR) { + + flagRuleExperiments.push(experiment); + + // Inject "everyone else" variation into feature rollout (FR) experiments + if (experiment && experiment.type === EXPERIMENT_TYPES.FR && everyoneElseVariation) { experiment.variations.push(everyoneElseVariation); experiment.trafficAllocation.push({ entityId: everyoneElseVariation.id, @@ -340,47 +297,78 @@ export const createProjectConfig = function( // Update variation lookup map experiment.variationKeyMap[everyoneElseVariation.key] = everyoneElseVariation; } - }); - }); - - // all rules (experiment rules and delivery rules) for each flag - projectConfig.flagRulesMap = {}; - (projectConfig.featureFlags || []).forEach(featureFlag => { - const flagRuleExperiments: Experiment[] = []; - featureFlag.experimentIds.forEach(experimentId => { - const experiment = projectConfig.experimentIdMap[experimentId]; - if (experiment) { - flagRuleExperiments.push(experiment); - } + (experiment.variations || []).forEach((variation) => { + flagVariationsById[variation.id] = variation; + }); }); - const rollout = projectConfig.rolloutIdMap[featureFlag.rolloutId]; + // add all rollout experiments to the flagRuleExperiments list + const rollout = projectConfig.rolloutIdMap[feature.rolloutId]; if (rollout) { - flagRuleExperiments.push(...rollout.experiments); - } + (rollout.experiments || []).forEach((experiment) => { + flagRuleExperiments.push(experiment); + (experiment.variations || []).forEach((variation) => { + flagVariationsById[variation.id] = variation; + }); + }); + } - projectConfig.flagRulesMap[featureFlag.key] = flagRuleExperiments; + projectConfig.flagRulesMap[feature.key] = flagRuleExperiments; + projectConfig.flagVariationsMap[feature.key] = Object.values(flagVariationsById) }); +} - // all variations for each flag - // - datafile does not contain a separate entity for this. - // - we collect variations used in each rule (experiment rules and delivery rules) - projectConfig.flagVariationsMap = {}; +/** + * Creates projectConfig object to be used for quick project property lookup + * @param {string} datafileStr JSON string representation of the datafile + * @return {ProjectConfig} Object representing project configuration + */ +export const createProjectConfig = function(datafileStr: string, options?: CreateProjectConfigOptions): ProjectConfig { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + let projectConfig: ProjectConfig; - objectEntries(projectConfig.flagRulesMap || {}).forEach(([flagKey, rules]) => { - const variations: OptimizelyVariation[] = []; - rules.forEach(rule => { - rule.variations.forEach(variation => { - if (!find(variations, item => item.id === variation.id)) { - variations.push(variation); - } - }); - }); - projectConfig.flagVariationsMap[flagKey] = variations; - }); + try { + projectConfig = JSON.parse(datafileStr); + } catch { + throw new OptimizelyError(INVALID_DATAFILE_MALFORMED); + } + + const version = (projectConfig as unknown as { version: string }).version + + if (SUPPORTED_DATAFILE_VERSIONS.indexOf(version) === -1) { + throw new OptimizelyError(INVALID_DATAFILE_VERSION, version); + } + + if (options?.jsonSchemaValidator) { + options.jsonSchemaValidator(projectConfig); + options.logger?.info(VALID_DATAFILE); + } else { + options?.logger?.info(SKIPPING_JSON_VALIDATION); + } + + if (!projectConfig.region) { + projectConfig.region = 'US'; + } + + projectConfig.__datafileStr = datafileStr; + + parseAudienceConfig(projectConfig); + parseAttributeConfig(projectConfig); + + projectConfig.eventKeyMap = keyBy(projectConfig.events, 'key'); + + parseGroupConfig(projectConfig); - parseHoldoutsConfig(projectConfig, logger); + parseRolloutConfig(projectConfig); + + parseIntegrationConfig(projectConfig); + + parseExperimentConfig(projectConfig); + + parseFeatureFlagConfig(projectConfig); + + parseHoldoutsConfig(projectConfig, options?.logger); return projectConfig; }; @@ -984,43 +972,6 @@ export const toDatafile = function(projectConfig: ProjectConfig): string { return projectConfig.__datafileStr; }; -/** - * @typedef {Object} - * @property {Object|null} configObj - * @property {Error|null} error - */ - -/** - * Try to create a project config object from the given datafile and - * configuration properties. - * Returns a ProjectConfig if successful. - * Otherwise, throws an error. - * @param {Object} config - * @param {Object|string} config.datafile - * @param {Object} config.jsonSchemaValidator - * @param {Object} config.logger - * @returns {Object} ProjectConfig - * @throws {Error} - */ -export const tryCreatingProjectConfig = function( - config: TryCreatingProjectConfigConfig -): ProjectConfig { - const newDatafileObj = configValidator.validateDatafile(config.datafile); - - if (config.jsonSchemaValidator) { - config.jsonSchemaValidator(newDatafileObj); - config.logger?.info(VALID_DATAFILE); - } else { - config.logger?.info(SKIPPING_JSON_VALIDATION); - } - - // Pass the datafile string (when available) and the logger so parse-time - // diagnostics (e.g. invalid local-holdout entries) are surfaced. - const datafileStr = typeof config.datafile === 'string' ? config.datafile : null; - const newConfigObj = createProjectConfig(newDatafileObj, datafileStr, config.logger); - return newConfigObj; -}; - /** * Get the send flag decisions value * @param {ProjectConfig} projectConfig @@ -1056,7 +1007,6 @@ export default { eventWithKeyExists, isFeatureExperiment, toDatafile, - tryCreatingProjectConfig, getTrafficAllocation, }; diff --git a/lib/project_config/project_config_manager.spec.ts b/lib/project_config/project_config_manager.spec.ts index 507c90b46..8fb63dc98 100644 --- a/lib/project_config/project_config_manager.spec.ts +++ b/lib/project_config/project_config_manager.spec.ts @@ -23,8 +23,6 @@ import { resolvablePromise } from '../utils/promise/resolvablePromise'; import { getMockDatafileManager } from '../tests/mock/mock_datafile_manager'; import { wait } from '../tests/testUtils'; -const cloneDeep = (x: any) => JSON.parse(JSON.stringify(x)); - describe('ProjectConfigManagerImpl', () => { describe('a logger is passed in the constructor', () => { it('should set name on the logger passed into the constructor', () => { @@ -92,7 +90,7 @@ describe('ProjectConfigManagerImpl', () => { describe('when constructed with only a datafile', () => { it('should reject onRunning() and log error if the datafile is invalid', async () => { const logger = getMockLogger(); - const manager = new ProjectConfigManagerImpl({ logger, datafile: {}}); + const manager = new ProjectConfigManagerImpl({ logger, datafile: JSON.stringify({})}); manager.start(); await expect(manager.onRunning()).rejects.toThrow(); expect(logger.error).toHaveBeenCalled(); @@ -100,7 +98,7 @@ describe('ProjectConfigManagerImpl', () => { it('should set status to Failed if the datafile is invalid', async () => { const logger = getMockLogger(); - const manager = new ProjectConfigManagerImpl({ logger, datafile: {}}); + const manager = new ProjectConfigManagerImpl({ logger, datafile: JSON.stringify({})}); manager.start(); await expect(manager.onRunning()).rejects.toThrow(); expect(manager.getState()).toBe(ServiceState.Failed); @@ -115,7 +113,7 @@ describe('ProjectConfigManagerImpl', () => { it('should fulfill onRunning() and set status to Running if the datafile is valid', async () => { const logger = getMockLogger(); - const manager = new ProjectConfigManagerImpl({ logger, datafile: testData.getTestProjectConfig()}); + const manager = new ProjectConfigManagerImpl({ logger, datafile: JSON.stringify(testData.getTestProjectConfig())}); manager.start(); await expect(manager.onRunning()).resolves.not.toThrow(); expect(manager.getState()).toBe(ServiceState.Running); @@ -123,7 +121,7 @@ describe('ProjectConfigManagerImpl', () => { it('should call onUpdate listeners registered before start() with the project config', async () => { const logger = getMockLogger(); - const manager = new ProjectConfigManagerImpl({ logger, datafile: testData.getTestProjectConfig()}); + const manager = new ProjectConfigManagerImpl({ logger, datafile: JSON.stringify(testData.getTestProjectConfig())}); const listener = vi.fn(); manager.onUpdate(listener); manager.start(); @@ -131,16 +129,16 @@ describe('ProjectConfigManagerImpl', () => { await manager.onRunning(); expect(listener).toHaveBeenCalledOnce(); - expect(listener).toHaveBeenCalledWith(createProjectConfig(testData.getTestProjectConfig())); + expect(listener).toHaveBeenCalledWith(createProjectConfig(JSON.stringify(testData.getTestProjectConfig()))); }); it('should return the correct config from getConfig() both before or after onRunning() resolves', async () => { const logger = getMockLogger(); - const manager = new ProjectConfigManagerImpl({ logger, datafile: testData.getTestProjectConfig()}); + const manager = new ProjectConfigManagerImpl({ logger, datafile: JSON.stringify(testData.getTestProjectConfig())}); manager.start(); - expect(manager.getConfig()).toEqual(createProjectConfig(testData.getTestProjectConfig())); + expect(manager.getConfig()).toEqual(createProjectConfig(JSON.stringify(testData.getTestProjectConfig()))); await manager.onRunning(); - expect(manager.getConfig()).toEqual(createProjectConfig(testData.getTestProjectConfig())); + expect(manager.getConfig()).toEqual(createProjectConfig(JSON.stringify(testData.getTestProjectConfig()))); }); }); @@ -152,7 +150,7 @@ describe('ProjectConfigManagerImpl', () => { onRunning: resolvablePromise().promise, // this will not be resolved }); vi.spyOn(datafileManager, 'onRunning'); - const manager = new ProjectConfigManagerImpl({ datafile: testData.getTestProjectConfig(), datafileManager }); + const manager = new ProjectConfigManagerImpl({ datafile: JSON.stringify(testData.getTestProjectConfig()), datafileManager }); manager.start(); expect(datafileManager.onRunning).toHaveBeenCalled(); @@ -165,7 +163,7 @@ describe('ProjectConfigManagerImpl', () => { onRunning, }); vi.spyOn(datafileManager, 'onRunning'); - const manager = new ProjectConfigManagerImpl({ datafile: testData.getTestProjectConfig(), datafileManager }); + const manager = new ProjectConfigManagerImpl({ datafile: JSON.stringify(testData.getTestProjectConfig()), datafileManager }); manager.start(); expect(datafileManager.onRunning).toHaveBeenCalled(); @@ -179,11 +177,11 @@ describe('ProjectConfigManagerImpl', () => { const listener = vi.fn(); - const manager = new ProjectConfigManagerImpl({ datafile: testData.getTestProjectConfig(), datafileManager }); + const manager = new ProjectConfigManagerImpl({ datafile: JSON.stringify(testData.getTestProjectConfig()), datafileManager }); manager.onUpdate(listener); manager.start(); await expect(manager.onRunning()).resolves.not.toThrow(); - expect(listener).toHaveBeenCalledWith(createProjectConfig(testData.getTestProjectConfig())); + expect(listener).toHaveBeenCalledWith(createProjectConfig(JSON.stringify(testData.getTestProjectConfig()))); }); it('should return the correct config from getConfig() both before or after onRunning() resolves', async () => { @@ -191,60 +189,60 @@ describe('ProjectConfigManagerImpl', () => { onRunning: resolvablePromise().promise, // this will not be resolved }); - const manager = new ProjectConfigManagerImpl({ datafile: testData.getTestProjectConfig(), datafileManager }); + const manager = new ProjectConfigManagerImpl({ datafile: JSON.stringify(testData.getTestProjectConfig()), datafileManager }); manager.start(); - expect(manager.getConfig()).toEqual(createProjectConfig(testData.getTestProjectConfig())); + expect(manager.getConfig()).toEqual(createProjectConfig(JSON.stringify(testData.getTestProjectConfig()))); await manager.onRunning(); - expect(manager.getConfig()).toEqual(createProjectConfig(testData.getTestProjectConfig())); + expect(manager.getConfig()).toEqual(createProjectConfig(JSON.stringify(testData.getTestProjectConfig()))); }); }); describe('when datafile is invalid', () => { it('should reject onRunning() with the same error if datafileManager.onRunning() rejects', async () => { const datafileManager = getMockDatafileManager({ onRunning: Promise.reject(new Error('test error')) }); - const manager = new ProjectConfigManagerImpl({ datafile: {}, datafileManager }); + const manager = new ProjectConfigManagerImpl({ datafile: JSON.stringify({}), datafileManager }); manager.start(); await expect(manager.onRunning()).rejects.toThrow('DatafileManager failed to start, reason: test error'); }); it('should resolve onRunning() if datafileManager.onUpdate() is fired and should update config', async () => { const datafileManager = getMockDatafileManager({ onRunning: Promise.resolve() }); - const manager = new ProjectConfigManagerImpl({ datafile: {}, datafileManager }); + const manager = new ProjectConfigManagerImpl({ datafile: JSON.stringify({}), datafileManager }); manager.start(); - datafileManager.pushUpdate(testData.getTestProjectConfig()); + datafileManager.pushUpdate(JSON.stringify(testData.getTestProjectConfig())); await expect(manager.onRunning()).resolves.not.toThrow(); - expect(manager.getConfig()).toEqual(createProjectConfig(testData.getTestProjectConfig())); + expect(manager.getConfig()).toEqual(createProjectConfig(JSON.stringify(testData.getTestProjectConfig()))); }); it('should resolve onRunning(), update config and call onUpdate listeners if datafileManager.onUpdate() is fired', async () => { const datafileManager = getMockDatafileManager({ onRunning: Promise.resolve() }); - const manager = new ProjectConfigManagerImpl({ datafile: {}, datafileManager }); + const manager = new ProjectConfigManagerImpl({ datafile: JSON.stringify({}), datafileManager }); manager.start(); const listener = vi.fn(); manager.onUpdate(listener); - datafileManager.pushUpdate(testData.getTestProjectConfig()); + datafileManager.pushUpdate(JSON.stringify(testData.getTestProjectConfig())); await expect(manager.onRunning()).resolves.not.toThrow(); - expect(manager.getConfig()).toEqual(createProjectConfig(testData.getTestProjectConfig())); - expect(listener).toHaveBeenCalledWith(createProjectConfig(testData.getTestProjectConfig())); + expect(manager.getConfig()).toEqual(createProjectConfig(JSON.stringify(testData.getTestProjectConfig()))); + expect(listener).toHaveBeenCalledWith(createProjectConfig(JSON.stringify(testData.getTestProjectConfig()))); }); it('should return undefined from getConfig() before onRunning() resolves', async () => { const datafileManager = getMockDatafileManager({ onRunning: Promise.resolve() }); - const manager = new ProjectConfigManagerImpl({ datafile: {}, datafileManager }); + const manager = new ProjectConfigManagerImpl({ datafile: JSON.stringify({}), datafileManager }); manager.start(); expect(manager.getConfig()).toBeUndefined(); }); it('should return the correct config from getConfig() after onRunning() resolves', async () => { const datafileManager = getMockDatafileManager({ onRunning: Promise.resolve() }); - const manager = new ProjectConfigManagerImpl({ datafile: {}, datafileManager }); + const manager = new ProjectConfigManagerImpl({ datafile: JSON.stringify({}), datafileManager }); manager.start(); - datafileManager.pushUpdate(testData.getTestProjectConfig()); + datafileManager.pushUpdate(JSON.stringify(testData.getTestProjectConfig())); await expect(manager.onRunning()).resolves.not.toThrow(); - expect(manager.getConfig()).toEqual(createProjectConfig(testData.getTestProjectConfig())); + expect(manager.getConfig()).toEqual(createProjectConfig(JSON.stringify(testData.getTestProjectConfig()))); }); }); }); @@ -273,10 +271,10 @@ describe('ProjectConfigManagerImpl', () => { const listener = vi.fn(); manager.onUpdate(listener); - datafileManager.pushUpdate(testData.getTestProjectConfig()); + datafileManager.pushUpdate(JSON.stringify(testData.getTestProjectConfig())); await expect(manager.onRunning()).resolves.not.toThrow(); - expect(manager.getConfig()).toEqual(createProjectConfig(testData.getTestProjectConfig())); - expect(listener).toHaveBeenCalledWith(createProjectConfig(testData.getTestProjectConfig())); + expect(manager.getConfig()).toEqual(createProjectConfig(JSON.stringify(testData.getTestProjectConfig()))); + expect(listener).toHaveBeenCalledWith(createProjectConfig(JSON.stringify(testData.getTestProjectConfig()))); }); it('should return undefined from getConfig() before onRunning() resolves', async () => { @@ -291,9 +289,9 @@ describe('ProjectConfigManagerImpl', () => { const manager = new ProjectConfigManagerImpl({ datafileManager }); manager.start(); - datafileManager.pushUpdate(testData.getTestProjectConfig()); + datafileManager.pushUpdate(JSON.stringify(testData.getTestProjectConfig())); await expect(manager.onRunning()).resolves.not.toThrow(); - expect(manager.getConfig()).toEqual(createProjectConfig(testData.getTestProjectConfig())); + expect(manager.getConfig()).toEqual(createProjectConfig(JSON.stringify(testData.getTestProjectConfig()))); }); }); @@ -301,25 +299,25 @@ describe('ProjectConfigManagerImpl', () => { const datafileManager = getMockDatafileManager({}); const datafile = testData.getTestProjectConfig(); - const manager = new ProjectConfigManagerImpl({ datafile, datafileManager }); + const manager = new ProjectConfigManagerImpl({ datafile: JSON.stringify(datafile), datafileManager }); const listener = vi.fn(); manager.onUpdate(listener); manager.start(); - expect(manager.getConfig()).toEqual(createProjectConfig(datafile)); + expect(manager.getConfig()).toEqual(createProjectConfig(JSON.stringify(datafile))); await manager.onRunning(); - expect(manager.getConfig()).toEqual(createProjectConfig(datafile)); - expect(listener).toHaveBeenNthCalledWith(1, createProjectConfig(datafile)); + expect(manager.getConfig()).toEqual(createProjectConfig(JSON.stringify(datafile))); + expect(listener).toHaveBeenNthCalledWith(1, createProjectConfig(JSON.stringify(datafile))); - const updatedDatafile = cloneDeep(datafile); + const updatedDatafile = testData.getTestProjectConfig(); updatedDatafile['revision'] = '99'; - datafileManager.pushUpdate(updatedDatafile); + datafileManager.pushUpdate(JSON.stringify(updatedDatafile)); await Promise.resolve(); - expect(manager.getConfig()).toEqual(createProjectConfig(updatedDatafile)); - expect(listener).toHaveBeenNthCalledWith(2, createProjectConfig(updatedDatafile)); + expect(manager.getConfig()).toEqual(createProjectConfig(JSON.stringify(updatedDatafile))); + expect(listener).toHaveBeenNthCalledWith(2, createProjectConfig(JSON.stringify(updatedDatafile))); }); it('should not call onUpdate handlers and should log error when datafileManager onUpdate is fired with invalid datafile', async () => { @@ -327,24 +325,24 @@ describe('ProjectConfigManagerImpl', () => { const logger = getMockLogger(); const datafile = testData.getTestProjectConfig(); - const manager = new ProjectConfigManagerImpl({ logger, datafile, datafileManager }); + const manager = new ProjectConfigManagerImpl({ logger, datafile: JSON.stringify(datafile), datafileManager }); const listener = vi.fn(); manager.onUpdate(listener); manager.start(); - expect(manager.getConfig()).toEqual(createProjectConfig(testData.getTestProjectConfig())); + expect(manager.getConfig()).toEqual(createProjectConfig(JSON.stringify(testData.getTestProjectConfig()))); await manager.onRunning(); - expect(manager.getConfig()).toEqual(createProjectConfig(testData.getTestProjectConfig())); + expect(manager.getConfig()).toEqual(createProjectConfig(JSON.stringify(testData.getTestProjectConfig()))); - expect(listener).toHaveBeenCalledWith(createProjectConfig(datafile)); + expect(listener).toHaveBeenCalledWith(createProjectConfig(JSON.stringify(datafile))); - const updatedDatafile = {}; - datafileManager.pushUpdate(updatedDatafile); + const updatedDatafile = {}; + datafileManager.pushUpdate(JSON.stringify(updatedDatafile)); await Promise.resolve(); - expect(manager.getConfig()).toEqual(createProjectConfig(datafile)); + expect(manager.getConfig()).toEqual(createProjectConfig(JSON.stringify(datafile))); expect(listener).toHaveBeenCalledTimes(1); expect(logger.error).toHaveBeenCalled(); }); @@ -354,41 +352,41 @@ describe('ProjectConfigManagerImpl', () => { const datafile = testData.getTestProjectConfig(); const jsonSchemaValidator = vi.fn().mockReturnValue(true); - const manager = new ProjectConfigManagerImpl({ datafile, datafileManager, jsonSchemaValidator }); + const manager = new ProjectConfigManagerImpl({ datafile: JSON.stringify(datafile), datafileManager, jsonSchemaValidator }); manager.start(); await manager.onRunning(); - const updatedDatafile = cloneDeep(datafile); + const updatedDatafile = testData.getTestProjectConfig(); updatedDatafile['revision'] = '99'; - datafileManager.pushUpdate(updatedDatafile); + datafileManager.pushUpdate(JSON.stringify(updatedDatafile)); await Promise.resolve(); expect(jsonSchemaValidator).toHaveBeenCalledTimes(2); - expect(jsonSchemaValidator).toHaveBeenNthCalledWith(1, datafile); - expect(jsonSchemaValidator).toHaveBeenNthCalledWith(2, updatedDatafile); + expect(jsonSchemaValidator.mock.calls[0][0].version).toBe(datafile.version); + expect(jsonSchemaValidator.mock.calls[1][0].revision).toBe('99'); }); it('should not call onUpdate handlers when datafileManager onUpdate is fired with the same datafile', async () => { const datafileManager = getMockDatafileManager({}); const datafile = testData.getTestProjectConfig(); - const manager = new ProjectConfigManagerImpl({ datafile, datafileManager }); + const manager = new ProjectConfigManagerImpl({ datafile: JSON.stringify(datafile), datafileManager }); const listener = vi.fn(); manager.onUpdate(listener); manager.start(); - expect(manager.getConfig()).toEqual(createProjectConfig(datafile)); + expect(manager.getConfig()).toEqual(createProjectConfig(JSON.stringify(datafile))); await manager.onRunning(); - expect(manager.getConfig()).toEqual(createProjectConfig(datafile)); - expect(listener).toHaveBeenNthCalledWith(1, createProjectConfig(datafile)); + expect(manager.getConfig()).toEqual(createProjectConfig(JSON.stringify(datafile))); + expect(listener).toHaveBeenNthCalledWith(1, createProjectConfig(JSON.stringify(datafile))); - datafileManager.pushUpdate(cloneDeep(datafile)); + datafileManager.pushUpdate(JSON.stringify(datafile)); await Promise.resolve(); - expect(manager.getConfig()).toEqual(createProjectConfig(datafile)); + expect(manager.getConfig()).toEqual(createProjectConfig(JSON.stringify(datafile))); expect(listener).toHaveBeenCalledTimes(1); }); @@ -396,7 +394,7 @@ describe('ProjectConfigManagerImpl', () => { const datafile = testData.getTestProjectConfig(); const datafileManager = getMockDatafileManager({}); - const manager = new ProjectConfigManagerImpl({ datafile }); + const manager = new ProjectConfigManagerImpl({ datafile: JSON.stringify(datafile) }); const listener = vi.fn(); const dispose = manager.onUpdate(listener); @@ -404,11 +402,11 @@ describe('ProjectConfigManagerImpl', () => { manager.start(); await manager.onRunning(); - expect(listener).toHaveBeenNthCalledWith(1, createProjectConfig(datafile)); + expect(listener).toHaveBeenNthCalledWith(1, createProjectConfig(JSON.stringify(datafile))); dispose(); - datafileManager.pushUpdate(cloneDeep(testData.getTestProjectConfigWithFeatures())); + datafileManager.pushUpdate(JSON.stringify(testData.getTestProjectConfigWithFeatures())); await Promise.resolve(); expect(listener).toHaveBeenCalledTimes(1); }); @@ -424,8 +422,8 @@ describe('ProjectConfigManagerImpl', () => { manager.start(); await manager.onRunning(); - expect(listener).toHaveBeenCalledWith(createProjectConfig(datafile)); - expect(manager.getConfig()).toEqual(createProjectConfig(datafile)); + expect(listener).toHaveBeenCalledWith(createProjectConfig(JSON.stringify(datafile))); + expect(manager.getConfig()).toEqual(createProjectConfig(JSON.stringify(datafile))); }); it('should reject onRunning() and log error if the datafile string is an invalid json', async () => { @@ -439,7 +437,7 @@ describe('ProjectConfigManagerImpl', () => { it('should reject onRunning() and log error if the datafile version is not supported', async () => { const logger = getMockLogger(); const datafile = testData.getUnsupportedVersionConfig(); - const manager = new ProjectConfigManagerImpl({ logger, datafile }); + const manager = new ProjectConfigManagerImpl({ logger, datafile: JSON.stringify(datafile) }); manager.start(); await expect(manager.onRunning()).rejects.toThrow(); @@ -476,7 +474,7 @@ describe('ProjectConfigManagerImpl', () => { }); it('should set status to Terminated immediately if no datafile manager is provided and resolve onTerminated', async () => { - const manager = new ProjectConfigManagerImpl({ datafile: testData.getTestProjectConfig() }); + const manager = new ProjectConfigManagerImpl({ datafile: JSON.stringify(testData.getTestProjectConfig()) }); manager.stop(); expect(manager.getState()).toBe(ServiceState.Terminated); await expect(manager.onTerminated()).resolves.not.toThrow(); @@ -487,7 +485,7 @@ describe('ProjectConfigManagerImpl', () => { const datafileManager = getMockDatafileManager({ onRunning: Promise.resolve(), onTerminated: datafileManagerTerminated.promise }); const manager = new ProjectConfigManagerImpl({ datafileManager }); manager.start(); - datafileManager.pushUpdate(testData.getTestProjectConfig()); + datafileManager.pushUpdate(JSON.stringify(testData.getTestProjectConfig())); await manager.onRunning(); manager.stop(); @@ -502,7 +500,7 @@ describe('ProjectConfigManagerImpl', () => { const datafileManager = getMockDatafileManager({ onRunning: Promise.resolve(), onTerminated: datafileManagerTerminated.promise }); const manager = new ProjectConfigManagerImpl({ datafileManager }); manager.start(); - datafileManager.pushUpdate(testData.getTestProjectConfig()); + datafileManager.pushUpdate(JSON.stringify(testData.getTestProjectConfig())); await manager.onRunning(); manager.stop(); @@ -521,7 +519,7 @@ describe('ProjectConfigManagerImpl', () => { const datafileManager = getMockDatafileManager({ onRunning: Promise.resolve(), onTerminated: datafileManagerTerminated.promise }); const manager = new ProjectConfigManagerImpl({ datafileManager }); manager.start(); - datafileManager.pushUpdate(testData.getTestProjectConfig()); + datafileManager.pushUpdate(JSON.stringify(testData.getTestProjectConfig())); await manager.onRunning(); manager.stop(); @@ -544,12 +542,12 @@ describe('ProjectConfigManagerImpl', () => { const listener = vi.fn(); manager.onUpdate(listener); - datafileManager.pushUpdate(testData.getTestProjectConfig()); + datafileManager.pushUpdate(JSON.stringify(testData.getTestProjectConfig())); await manager.onRunning(); expect(listener).toHaveBeenCalledTimes(1); manager.stop(); - datafileManager.pushUpdate(testData.getTestProjectConfigWithFeatures()); + datafileManager.pushUpdate(JSON.stringify(testData.getTestProjectConfigWithFeatures())); datafileManagerTerminated.resolve(); await expect(manager.onTerminated()).resolves.not.toThrow(); diff --git a/lib/project_config/project_config_manager.ts b/lib/project_config/project_config_manager.ts index b7d527a23..0985c5c3a 100644 --- a/lib/project_config/project_config_manager.ts +++ b/lib/project_config/project_config_manager.ts @@ -17,7 +17,7 @@ import { LoggerFacade } from '../logging/logger'; import { createOptimizelyConfig } from './optimizely_config'; import { OptimizelyConfig } from '../shared_types'; import { DatafileManager } from './datafile_manager'; -import { ProjectConfig, toDatafile, tryCreatingProjectConfig } from './project_config'; +import { ProjectConfig, toDatafile, createProjectConfig } from './project_config'; import { Service, ServiceState, BaseService } from '../service'; import { Consumer, Fn, Transformer } from '../utils/type'; import { EventEmitter } from '../utils/event_emitter/event_emitter'; @@ -33,7 +33,7 @@ export const GOT_INVALID_DATAFILE = 'got invalid datafile'; import { sprintf } from '../utils/fns'; import { Platform } from '../platform_support'; interface ProjectConfigManagerConfig { - datafile?: string | Record; + datafile?: string; jsonSchemaValidator?: Transformer, datafileManager?: DatafileManager; logger?: LoggerFacade; @@ -57,7 +57,7 @@ export interface ProjectConfigManager extends Service { export const LOGGER_NAME = 'ProjectConfigManager'; export class ProjectConfigManagerImpl extends BaseService implements ProjectConfigManager { - private datafile?: string | object; + private datafile?: string; private projectConfig?: ProjectConfig; private optimizelyConfig?: OptimizelyConfig; public jsonSchemaValidator?: Transformer; @@ -146,14 +146,13 @@ export class ProjectConfigManagerImpl extends BaseService implements ProjectConf * the project config and optimizely config objects will not be updated. If the error * is fatal, handleInitError will be called. */ - private handleNewDatafile(newDatafile: string | object, fromConfig = false): void { + private handleNewDatafile(newDatafile: string, fromConfig = false): void { if (this.isDone()) { return; } try { - const config = tryCreatingProjectConfig({ - datafile: newDatafile, + const config = createProjectConfig(newDatafile, { jsonSchemaValidator: this.jsonSchemaValidator, logger: this.logger, }); diff --git a/lib/tests/mock/mock_datafile_manager.ts b/lib/tests/mock/mock_datafile_manager.ts index 1c9d66b38..2a48f3660 100644 --- a/lib/tests/mock/mock_datafile_manager.ts +++ b/lib/tests/mock/mock_datafile_manager.ts @@ -21,14 +21,14 @@ import { BaseService } from '../../service'; import { LoggerFacade } from '../../logging/logger'; type MockConfig = { - datafile?: string | object; + datafile?: string; onRunning?: Promise, onTerminated?: Promise, } class MockDatafileManager extends BaseService implements DatafileManager { eventEmitter: EventEmitter<{ update: string}> = new EventEmitter(); - datafile: string | object | undefined; + datafile: string | undefined; constructor(opt: MockConfig) { super(); @@ -49,9 +49,6 @@ class MockDatafileManager extends BaseService implements DatafileManager { } get(): string | undefined { - if (typeof this.datafile === 'object') { - return JSON.stringify(this.datafile); - } return this.datafile; } @@ -63,10 +60,7 @@ class MockDatafileManager extends BaseService implements DatafileManager { return this.eventEmitter.on('update', listener) } - pushUpdate(datafile: string | object): void { - if (typeof datafile === 'object') { - datafile = JSON.stringify(datafile); - } + pushUpdate(datafile: string): void { this.datafile = datafile; this.eventEmitter.emit('update', datafile); } diff --git a/lib/utils/config_validator/index.spec.ts b/lib/utils/config_validator/index.spec.ts deleted file mode 100644 index c8496ecc4..000000000 --- a/lib/utils/config_validator/index.spec.ts +++ /dev/null @@ -1,65 +0,0 @@ -/** - * Copyright 2025, 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 { describe, it, expect } from 'vitest'; -import configValidator from './'; -import testData from '../../tests/test_data'; -import { INVALID_DATAFILE_MALFORMED, INVALID_DATAFILE_VERSION, NO_DATAFILE_SPECIFIED } from 'error_message'; -import { OptimizelyError } from '../../error/optimizly_error'; - -describe('validate', () => { - it('should complain if datafile is not provided', () => { - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - expect(() => configValidator.validateDatafile()).toThrow(OptimizelyError); - - try { - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - configValidator.validateDatafile(); - } catch (err) { - expect(err).toBeInstanceOf(OptimizelyError); - expect(err.baseMessage).toBe(NO_DATAFILE_SPECIFIED); - } - }); - - it('should complain if datafile is malformed', () => { - expect(() => configValidator.validateDatafile('abc')).toThrow( OptimizelyError); - - try { - configValidator.validateDatafile('abc'); - } catch(err) { - expect(err).toBeInstanceOf(OptimizelyError); - expect(err.baseMessage).toBe(INVALID_DATAFILE_MALFORMED); - } - }); - - it('should complain if datafile version is not supported', () => { - expect(() => configValidator.validateDatafile(JSON.stringify(testData.getUnsupportedVersionConfig())).toThrow(OptimizelyError)); - - try { - configValidator.validateDatafile(JSON.stringify(testData.getUnsupportedVersionConfig())); - } catch(err) { - expect(err).toBeInstanceOf(OptimizelyError); - expect(err.baseMessage).toBe(INVALID_DATAFILE_VERSION); - expect(err.params).toEqual(['5']); - } - }); - - it('should not complain if datafile is valid', () => { - expect(() => configValidator.validateDatafile(JSON.stringify(testData.getTestProjectConfig())).not.toThrowError()); - }); -}); diff --git a/lib/utils/config_validator/index.tests.js b/lib/utils/config_validator/index.tests.js deleted file mode 100644 index 2680a07da..000000000 --- a/lib/utils/config_validator/index.tests.js +++ /dev/null @@ -1,89 +0,0 @@ -/** - * Copyright 2016, 2018-2020, 2022, 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 { sprintf } from '../../utils/fns'; - -import configValidator from './'; -import testData from '../../tests/test_data'; -import { - INVALID_DATAFILE_MALFORMED, - INVALID_DATAFILE_VERSION, - INVALID_ERROR_HANDLER, - INVALID_EVENT_DISPATCHER, - INVALID_LOGGER, - NO_DATAFILE_SPECIFIED, -} from 'error_message'; - -describe('lib/utils/config_validator', function() { - describe('APIs', function() { - describe('validate', function() { - it.skip('should complain if the provided error handler is invalid', function() { - const ex = assert.throws(function() { - configValidator.validate({ - errorHandler: {}, - }); - }); - assert.equal(ex.baseMessage, INVALID_ERROR_HANDLER); - }); - - it.skip('should complain if the provided event dispatcher is invalid', function() { - const ex = assert.throws(function() { - configValidator.validate({ - eventDispatcher: {}, - }); - }); - assert.equal(ex.baseMessage, INVALID_EVENT_DISPATCHER); - }); - - it.skip('should complain if the provided logger is invalid', function() { - const ex = assert.throws(function() { - configValidator.validate({ - logger: {}, - }); - }); - assert.equal(ex.baseMessage, INVALID_LOGGER); - }); - - it('should complain if datafile is not provided', function() { - const ex = assert.throws(function() { - configValidator.validateDatafile(); - }); - assert.equal(ex.baseMessage, NO_DATAFILE_SPECIFIED); - }); - - it('should complain if datafile is malformed', function() { - const ex = assert.throws(function() { - configValidator.validateDatafile('abc'); - }); - assert.equal(ex.baseMessage, INVALID_DATAFILE_MALFORMED); - }); - - it('should complain if datafile version is not supported', function() { - const ex = assert.throws(function() { - configValidator.validateDatafile(JSON.stringify(testData.getUnsupportedVersionConfig())); - }); - assert.equal(ex.baseMessage, INVALID_DATAFILE_VERSION); - assert.deepEqual(ex.params, ['5']); - }); - - it('should not complain if datafile is valid', function() { - assert.doesNotThrow(function() { - configValidator.validateDatafile(JSON.stringify(testData.getTestProjectConfig())); - }); - }); - }); - }); -}); diff --git a/lib/utils/config_validator/index.ts b/lib/utils/config_validator/index.ts deleted file mode 100644 index 4b59b066d..000000000 --- a/lib/utils/config_validator/index.ts +++ /dev/null @@ -1,65 +0,0 @@ -/** - * Copyright 2016, 2018-2020, 2022, 2025, 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 { - DATAFILE_VERSIONS, -} from '../enums'; -import { - INVALID_DATAFILE_MALFORMED, - INVALID_DATAFILE_VERSION, - NO_DATAFILE_SPECIFIED, -} from 'error_message'; -import { OptimizelyError } from '../../error/optimizly_error'; -import { Platform } from '../../platform_support'; -const SUPPORTED_VERSIONS = [DATAFILE_VERSIONS.V2, DATAFILE_VERSIONS.V3, DATAFILE_VERSIONS.V4]; - -/** - * Validates the datafile - * @param {Object|string} datafile - * @return {Object} The datafile object if the datafile is valid - * @throws If the datafile is not valid for any of the following reasons: - - The datafile string is undefined - - The datafile string cannot be parsed as a JSON object - - The datafile version is not supported - */ -// eslint-disable-next-line -export const validateDatafile = function(datafile: unknown): any { - if (!datafile) { - throw new OptimizelyError(NO_DATAFILE_SPECIFIED); - } - if (typeof datafile === 'string') { - // Attempt to parse the datafile string - try { - datafile = JSON.parse(datafile); - } catch (ex) { - throw new OptimizelyError(INVALID_DATAFILE_MALFORMED); - } - } - if (typeof datafile === 'object' && !Array.isArray(datafile) && datafile !== null) { - if (SUPPORTED_VERSIONS.indexOf(datafile['version' as keyof unknown]) === -1) { - throw new OptimizelyError(INVALID_DATAFILE_VERSION, datafile['version' as keyof unknown]); - } - } else { - throw new OptimizelyError(INVALID_DATAFILE_MALFORMED); - } - - return datafile; -}; - -export default { - validateDatafile: validateDatafile, -} - -export const __platforms: Platform[] = ['__universal__']; diff --git a/package-lock.json b/package-lock.json index ec9915af4..f0e86527e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -38,7 +38,6 @@ "eslint-plugin-prettier": "^3.1.2", "happy-dom": "^20.0.11", "jiti": "^2.4.1", - "lodash": "^4.17.11", "minimatch": "^9.0.5", "mocha": "^10.2.0", "mocha-lcov-reporter": "^1.3.0", @@ -1136,9 +1135,9 @@ } }, "node_modules/@eslint/eslintrc/node_modules/brace-expansion": { - "version": "1.1.12", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", - "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.15.tgz", + "integrity": "sha512-EwOCDEex4quD37XhqM3omwtMoJjr//isUZz1JopUNWms+4Z2ViyM/k1YIRePpoVNnQhENnxtFjLaxNHrT7xIUg==", "dev": true, "license": "MIT", "dependencies": { @@ -1147,9 +1146,9 @@ } }, "node_modules/@eslint/eslintrc/node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", "dev": true, "license": "ISC", "dependencies": { @@ -1186,9 +1185,9 @@ } }, "node_modules/@humanwhocodes/config-array/node_modules/brace-expansion": { - "version": "1.1.12", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", - "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.15.tgz", + "integrity": "sha512-EwOCDEex4quD37XhqM3omwtMoJjr//isUZz1JopUNWms+4Z2ViyM/k1YIRePpoVNnQhENnxtFjLaxNHrT7xIUg==", "dev": true, "license": "MIT", "dependencies": { @@ -1197,9 +1196,9 @@ } }, "node_modules/@humanwhocodes/config-array/node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", "dev": true, "license": "ISC", "dependencies": { @@ -1301,13 +1300,13 @@ } }, "node_modules/@isaacs/cliui/node_modules/strip-ansi": { - "version": "7.1.2", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.2.tgz", - "integrity": "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==", + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.2.0.tgz", + "integrity": "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==", "dev": true, "license": "MIT", "dependencies": { - "ansi-regex": "^6.0.1" + "ansi-regex": "^6.2.2" }, "engines": { "node": ">=12" @@ -1387,9 +1386,9 @@ } }, "node_modules/@istanbuljs/load-nyc-config/node_modules/js-yaml": { - "version": "3.14.2", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.2.tgz", - "integrity": "sha512-PMSmkqxr106Xa156c2M265Z+FTrPl+oxd/rgOQy2tijQeK5TxQ43psO1ZCwhVOSdnn+RzkzlRz/eY4BgJBYVpg==", + "version": "3.15.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.15.0.tgz", + "integrity": "sha512-ttBQIIQPDeLjpPOohtUdXuXUVoA2uIB6fEH9HyJ7234s5mBJ5wTx20njxplLZQgLaOfpmPQA7X2t5AX6tIPbog==", "dev": true, "license": "MIT", "dependencies": { @@ -1782,9 +1781,9 @@ } }, "node_modules/@puppeteer/browsers/node_modules/yargs": { - "version": "17.7.2", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", - "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "version": "17.7.3", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.3.tgz", + "integrity": "sha512-GZtjxm/J/4TSxuL3FNYjCmLktBTnIw/rVmKSIyKeYAZpmJB2ig9VauCC5xsa82GNKVKDAqpOn3KVzNt0zmrU0g==", "dev": true, "license": "MIT", "dependencies": { @@ -1884,9 +1883,9 @@ } }, "node_modules/@react-native/codegen/node_modules/yargs": { - "version": "17.7.2", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", - "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "version": "17.7.3", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.3.tgz", + "integrity": "sha512-GZtjxm/J/4TSxuL3FNYjCmLktBTnIw/rVmKSIyKeYAZpmJB2ig9VauCC5xsa82GNKVKDAqpOn3KVzNt0zmrU0g==", "dev": true, "license": "MIT", "peer": true, @@ -1998,9 +1997,9 @@ } }, "node_modules/@react-native/dev-middleware/node_modules/ws": { - "version": "7.5.10", - "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.10.tgz", - "integrity": "sha512-+dbF1tHwZpXcbOJdVOkzLDxZP1ailvSxM6ZweXTegylPny803bFhA+vqBYw4s31NSAk4S2Qz+AKXK9a4wkdjcQ==", + "version": "7.5.11", + "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.11.tgz", + "integrity": "sha512-zS54Oen9bITtp7kp2XM3AydrCIq1D+HwJOuH+c+e4LfpL/lotP5osijd+UoMnxwAam1GN8R4KtLAyIrIcBNpiA==", "dev": true, "license": "MIT", "peer": true, @@ -3457,13 +3456,13 @@ } }, "node_modules/@wdio/logger/node_modules/strip-ansi": { - "version": "7.1.2", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.2.tgz", - "integrity": "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==", + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.2.0.tgz", + "integrity": "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==", "dev": true, "license": "MIT", "dependencies": { - "ansi-regex": "^6.0.1" + "ansi-regex": "^6.2.2" }, "engines": { "node": ">=12" @@ -3493,9 +3492,9 @@ } }, "node_modules/@wdio/repl/node_modules/@types/node": { - "version": "20.19.28", - "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.28.tgz", - "integrity": "sha512-VyKBr25BuFDzBFCK5sUM6ZXiWfqgCTwTAOK8qzGV/m9FCirXYDlmczJ+d5dXBAQALGCdRRdbteKYfJ84NGEusw==", + "version": "20.19.43", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.43.tgz", + "integrity": "sha512-6oYBAi5ikg4Pl+kGsoYtawUMBT2zZMCvPNF7pVLnHZfd1zf38DRiWn/gT01RYCdUqkv7Fhr+C9ot4/tb+2sVvA==", "dev": true, "license": "MIT", "dependencies": { @@ -3523,9 +3522,9 @@ } }, "node_modules/@wdio/types/node_modules/@types/node": { - "version": "20.19.28", - "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.28.tgz", - "integrity": "sha512-VyKBr25BuFDzBFCK5sUM6ZXiWfqgCTwTAOK8qzGV/m9FCirXYDlmczJ+d5dXBAQALGCdRRdbteKYfJ84NGEusw==", + "version": "20.19.43", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.43.tgz", + "integrity": "sha512-6oYBAi5ikg4Pl+kGsoYtawUMBT2zZMCvPNF7pVLnHZfd1zf38DRiWn/gT01RYCdUqkv7Fhr+C9ot4/tb+2sVvA==", "dev": true, "license": "MIT", "dependencies": { @@ -3868,9 +3867,9 @@ } }, "node_modules/ajv": { - "version": "6.12.6", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", - "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "version": "6.15.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.15.0.tgz", + "integrity": "sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==", "dev": true, "license": "MIT", "dependencies": { @@ -3903,9 +3902,9 @@ } }, "node_modules/ajv-formats/node_modules/ajv": { - "version": "8.17.1", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", - "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", + "version": "8.20.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.20.0.tgz", + "integrity": "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==", "dev": true, "license": "MIT", "dependencies": { @@ -3985,9 +3984,9 @@ } }, "node_modules/anymatch/node_modules/picomatch": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", - "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", + "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", "dev": true, "license": "MIT", "engines": { @@ -4503,9 +4502,9 @@ "license": "ISC" }, "node_modules/brace-expansion": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", - "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.1.tgz", + "integrity": "sha512-WR1cURNjuvBLMZBMbqM0UoE+WAfdUcEV1ccD8PVBVOI+Z3ND4+SZbN8RsfT2bMuG1qwz5RFvPukSZm5fF2D5eA==", "dev": true, "license": "MIT", "dependencies": { @@ -5328,9 +5327,9 @@ } }, "node_modules/diff": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/diff/-/diff-5.2.0.tgz", - "integrity": "sha512-uIFDxqpRZGZ6ThOk84hEfqWoHx2devRFvpTZcTHur85vImfaxUbTW9Ryh4CpCuDnToOP1CEtXKIgytHBPVff5A==", + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/diff/-/diff-5.2.2.tgz", + "integrity": "sha512-vtcDfH3TOjP8UekytvnHH1o1P4FcUdt4eQ1Y+Abap1tk/OB2MWQvcwS2ClCd1zuIhc3JKOx6p3kod8Vfys3E+A==", "dev": true, "license": "BSD-3-Clause", "engines": { @@ -5515,23 +5514,23 @@ } }, "node_modules/edgedriver/node_modules/isexe": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/isexe/-/isexe-3.1.1.tgz", - "integrity": "sha512-LpB/54B+/2J5hqQ7imZHfdU31OlgQqx7ZicVlkm9kzg9/w8GKLEcFfJl/t7DCEDueOyBAD6zCCwTO6Fzs0NoEQ==", + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-4.0.0.tgz", + "integrity": "sha512-FFUtZMpoZ8RqHS3XeXEmHWLA4thH+ZxCv2lOiPIn1Xc7CxrqhWzNSDzD+/chS/zbYezmiwWLdQC09JdQKmthOw==", "dev": true, - "license": "ISC", + "license": "BlueOak-1.0.0", "engines": { - "node": ">=16" + "node": ">=20" } }, "node_modules/edgedriver/node_modules/which": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/which/-/which-6.0.0.tgz", - "integrity": "sha512-f+gEpIKMR9faW/JgAgPK1D7mekkFoqbmiwvNzuhsHetni20QSgzg9Vhn0g2JSJkkfehQnqdUAx7/e15qS1lPxg==", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/which/-/which-6.0.1.tgz", + "integrity": "sha512-oGLe46MIrCRqX7ytPUf66EAYvdeMIZYn3WaocqqKZAxrBpkqHfL/qvTyJ/bTk5+AqHCjXmrv3CEWgy368zhRUg==", "dev": true, "license": "ISC", "dependencies": { - "isexe": "^3.1.1" + "isexe": "^4.0.0" }, "bin": { "node-which": "bin/which.js" @@ -5587,19 +5586,6 @@ "url": "https://github.com/fb55/encoding-sniffer?sponsor=1" } }, - "node_modules/encoding-sniffer/node_modules/iconv-lite": { - "version": "0.6.3", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", - "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", - "dev": true, - "license": "MIT", - "dependencies": { - "safer-buffer": ">= 2.1.2 < 3.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/end-of-stream": { "version": "1.4.5", "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz", @@ -5897,9 +5883,9 @@ } }, "node_modules/eslint/node_modules/brace-expansion": { - "version": "1.1.12", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", - "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.15.tgz", + "integrity": "sha512-EwOCDEex4quD37XhqM3omwtMoJjr//isUZz1JopUNWms+4Z2ViyM/k1YIRePpoVNnQhENnxtFjLaxNHrT7xIUg==", "dev": true, "license": "MIT", "dependencies": { @@ -5935,9 +5921,9 @@ } }, "node_modules/eslint/node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", "dev": true, "license": "ISC", "dependencies": { @@ -6735,9 +6721,9 @@ "license": "BSD-2-Clause" }, "node_modules/glob/node_modules/brace-expansion": { - "version": "1.1.12", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", - "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.15.tgz", + "integrity": "sha512-EwOCDEex4quD37XhqM3omwtMoJjr//isUZz1JopUNWms+4Z2ViyM/k1YIRePpoVNnQhENnxtFjLaxNHrT7xIUg==", "dev": true, "license": "MIT", "dependencies": { @@ -6746,9 +6732,9 @@ } }, "node_modules/glob/node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", "dev": true, "license": "ISC", "dependencies": { @@ -6834,9 +6820,9 @@ } }, "node_modules/happy-dom/node_modules/@types/node": { - "version": "20.19.28", - "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.28.tgz", - "integrity": "sha512-VyKBr25BuFDzBFCK5sUM6ZXiWfqgCTwTAOK8qzGV/m9FCirXYDlmczJ+d5dXBAQALGCdRRdbteKYfJ84NGEusw==", + "version": "20.19.43", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.43.tgz", + "integrity": "sha512-6oYBAi5ikg4Pl+kGsoYtawUMBT2zZMCvPNF7pVLnHZfd1zf38DRiWn/gT01RYCdUqkv7Fhr+C9ot4/tb+2sVvA==", "dev": true, "license": "MIT", "dependencies": { @@ -7054,6 +7040,19 @@ "node": ">= 6" } }, + "node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/ieee754": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", @@ -7717,9 +7716,9 @@ } }, "node_modules/jest-util/node_modules/picomatch": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", - "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", + "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", "dev": true, "license": "MIT", "peer": true, @@ -8810,9 +8809,9 @@ } }, "node_modules/metro/node_modules/ws": { - "version": "7.5.10", - "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.10.tgz", - "integrity": "sha512-+dbF1tHwZpXcbOJdVOkzLDxZP1ailvSxM6ZweXTegylPny803bFhA+vqBYw4s31NSAk4S2Qz+AKXK9a4wkdjcQ==", + "version": "7.5.11", + "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.11.tgz", + "integrity": "sha512-zS54Oen9bITtp7kp2XM3AydrCIq1D+HwJOuH+c+e4LfpL/lotP5osijd+UoMnxwAam1GN8R4KtLAyIrIcBNpiA==", "dev": true, "license": "MIT", "peer": true, @@ -8833,9 +8832,9 @@ } }, "node_modules/metro/node_modules/yargs": { - "version": "17.7.2", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", - "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "version": "17.7.3", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.3.tgz", + "integrity": "sha512-GZtjxm/J/4TSxuL3FNYjCmLktBTnIw/rVmKSIyKeYAZpmJB2ig9VauCC5xsa82GNKVKDAqpOn3KVzNt0zmrU0g==", "dev": true, "license": "MIT", "peer": true, @@ -8878,9 +8877,9 @@ } }, "node_modules/micromatch/node_modules/picomatch": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", - "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", + "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", "dev": true, "license": "MIT", "engines": { @@ -8926,13 +8925,13 @@ } }, "node_modules/minimatch": { - "version": "9.0.5", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", - "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "version": "9.0.9", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz", + "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==", "dev": true, "license": "ISC", "dependencies": { - "brace-expansion": "^2.0.1" + "brace-expansion": "^2.0.2" }, "engines": { "node": ">=16 || 14 >=14.17" @@ -9049,9 +9048,9 @@ } }, "node_modules/mocha/node_modules/minimatch": { - "version": "5.1.6", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", - "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", + "version": "5.1.9", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.9.tgz", + "integrity": "sha512-7o1wEA2RyMP7Iu7GNba9vc0RWWGACJOCZBJX2GJWip0ikV+wcOsgVuY9uE8CPiyQhkGFSlhuSkZPavN7u1c2Fw==", "dev": true, "license": "ISC", "dependencies": { @@ -9803,9 +9802,9 @@ "license": "ISC" }, "node_modules/path-to-regexp": { - "version": "8.3.0", - "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.3.0.tgz", - "integrity": "sha512-7jdwVIRtsP8MYpdXSwOS0YdD0Du+qOoF/AEPIt88PcCFrZCzx41oxku1jD88hZBwbNUIEfpqvuhjFaMAqMTWnA==", + "version": "8.4.2", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.4.2.tgz", + "integrity": "sha512-qRcuIdP69NPm4qbACK+aDogI5CBDMi1jKe0ry5rSQJz8JVLsC7jV8XpiJjGRLLol3N+R5ihGYcrPLTno6pAdBA==", "dev": true, "license": "MIT", "funding": { @@ -9855,9 +9854,9 @@ "license": "ISC" }, "node_modules/picomatch": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", - "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.5.tgz", + "integrity": "sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==", "dev": true, "license": "MIT", "engines": { @@ -10264,9 +10263,9 @@ } }, "node_modules/react-devtools-core/node_modules/ws": { - "version": "7.5.10", - "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.10.tgz", - "integrity": "sha512-+dbF1tHwZpXcbOJdVOkzLDxZP1ailvSxM6ZweXTegylPny803bFhA+vqBYw4s31NSAk4S2Qz+AKXK9a4wkdjcQ==", + "version": "7.5.11", + "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.11.tgz", + "integrity": "sha512-zS54Oen9bITtp7kp2XM3AydrCIq1D+HwJOuH+c+e4LfpL/lotP5osijd+UoMnxwAam1GN8R4KtLAyIrIcBNpiA==", "dev": true, "license": "MIT", "peer": true, @@ -10408,9 +10407,9 @@ "peer": true }, "node_modules/react-native/node_modules/ws": { - "version": "7.5.10", - "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.10.tgz", - "integrity": "sha512-+dbF1tHwZpXcbOJdVOkzLDxZP1ailvSxM6ZweXTegylPny803bFhA+vqBYw4s31NSAk4S2Qz+AKXK9a4wkdjcQ==", + "version": "7.5.11", + "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.11.tgz", + "integrity": "sha512-zS54Oen9bITtp7kp2XM3AydrCIq1D+HwJOuH+c+e4LfpL/lotP5osijd+UoMnxwAam1GN8R4KtLAyIrIcBNpiA==", "dev": true, "license": "MIT", "peer": true, @@ -10431,9 +10430,9 @@ } }, "node_modules/react-native/node_modules/yargs": { - "version": "17.7.2", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", - "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "version": "17.7.3", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.3.tgz", + "integrity": "sha512-GZtjxm/J/4TSxuL3FNYjCmLktBTnIw/rVmKSIyKeYAZpmJB2ig9VauCC5xsa82GNKVKDAqpOn3KVzNt0zmrU0g==", "dev": true, "license": "MIT", "peer": true, @@ -10500,9 +10499,9 @@ } }, "node_modules/readdir-glob/node_modules/minimatch": { - "version": "5.1.6", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", - "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", + "version": "5.1.9", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.9.tgz", + "integrity": "sha512-7o1wEA2RyMP7Iu7GNba9vc0RWWGACJOCZBJX2GJWip0ikV+wcOsgVuY9uE8CPiyQhkGFSlhuSkZPavN7u1c2Fw==", "dev": true, "license": "ISC", "dependencies": { @@ -10526,9 +10525,9 @@ } }, "node_modules/readdirp/node_modules/picomatch": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", - "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", + "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", "dev": true, "license": "MIT", "engines": { @@ -10843,9 +10842,9 @@ } }, "node_modules/schema-utils/node_modules/ajv": { - "version": "8.17.1", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", - "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", + "version": "8.20.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.20.0.tgz", + "integrity": "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==", "dev": true, "license": "MIT", "dependencies": { @@ -10880,9 +10879,9 @@ "license": "MIT" }, "node_modules/semver": { - "version": "7.7.3", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", - "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", + "version": "7.8.5", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz", + "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==", "dev": true, "license": "ISC", "bin": { @@ -11117,9 +11116,9 @@ } }, "node_modules/sinon/node_modules/diff": { - "version": "3.5.0", - "resolved": "https://registry.npmjs.org/diff/-/diff-3.5.0.tgz", - "integrity": "sha512-A46qtFgd+g7pDZinpnwiRJtxbC1hpgf0uzP3iG89scHk0AUC7A1TGxf5OiiOUv/JMZR8GOt8hL900hV0bOy5xA==", + "version": "3.5.1", + "resolved": "https://registry.npmjs.org/diff/-/diff-3.5.1.tgz", + "integrity": "sha512-Z3u54A8qGyqFOSr2pk0ijYs8mOE9Qz8kTvtKeBI+upoG9j04Sq+oI7W8zAJiQybDcESET8/uIdHzs0p3k4fZlw==", "dev": true, "license": "BSD-3-Clause", "engines": { @@ -11703,9 +11702,9 @@ } }, "node_modules/test-exclude/node_modules/brace-expansion": { - "version": "1.1.12", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", - "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.15.tgz", + "integrity": "sha512-EwOCDEex4quD37XhqM3omwtMoJjr//isUZz1JopUNWms+4Z2ViyM/k1YIRePpoVNnQhENnxtFjLaxNHrT7xIUg==", "dev": true, "license": "MIT", "dependencies": { @@ -11714,9 +11713,9 @@ } }, "node_modules/test-exclude/node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", "dev": true, "license": "ISC", "dependencies": { @@ -11946,9 +11945,9 @@ } }, "node_modules/ts-node/node_modules/diff": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz", - "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==", + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.4.tgz", + "integrity": "sha512-X07nttJQkwkfKfvTPG/KSnE2OMdcUCao6+eXF3wmnIQRn2aPAHH3VxDbDOdegkd6JbPsXqShpvEOHfAT+nCNwQ==", "dev": true, "license": "BSD-3-Clause", "engines": { @@ -12071,9 +12070,9 @@ } }, "node_modules/undici": { - "version": "7.18.2", - "resolved": "https://registry.npmjs.org/undici/-/undici-7.18.2.tgz", - "integrity": "sha512-y+8YjDFzWdQlSE9N5nzKMT3g4a5UBX1HKowfdXh0uvAnTaqqwqB92Jt4UXBAeKekDs5IaDKyJFR4X1gYVCgXcw==", + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/undici/-/undici-7.28.0.tgz", + "integrity": "sha512-cRZYrTDwWznlnRiPjggAGxZXanty6M8RV1ff8Wm4LWXBp7/IG8v5DnOm74DtUBp9OONpK75YlPnIjQqX0dBDtA==", "dev": true, "license": "MIT", "engines": { @@ -12526,9 +12525,9 @@ } }, "node_modules/webdriver/node_modules/@types/node": { - "version": "20.19.28", - "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.28.tgz", - "integrity": "sha512-VyKBr25BuFDzBFCK5sUM6ZXiWfqgCTwTAOK8qzGV/m9FCirXYDlmczJ+d5dXBAQALGCdRRdbteKYfJ84NGEusw==", + "version": "20.19.43", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.43.tgz", + "integrity": "sha512-6oYBAi5ikg4Pl+kGsoYtawUMBT2zZMCvPNF7pVLnHZfd1zf38DRiWn/gT01RYCdUqkv7Fhr+C9ot4/tb+2sVvA==", "dev": true, "license": "MIT", "dependencies": { @@ -12560,9 +12559,9 @@ } }, "node_modules/webdriver/node_modules/undici": { - "version": "6.23.0", - "resolved": "https://registry.npmjs.org/undici/-/undici-6.23.0.tgz", - "integrity": "sha512-VfQPToRA5FZs/qJxLIinmU59u0r7LXqoJkCzinq3ckNJp3vKEh7jTWN589YQ5+aoAC/TGRLyJLCPKcLQbM8r9g==", + "version": "6.27.0", + "resolved": "https://registry.npmjs.org/undici/-/undici-6.27.0.tgz", + "integrity": "sha512-YmfV3YnEDzXRC5lZ2jWtWWHKGUm1zIt8AhesR1tens+HTNv+YZlN/dp6G727LOvMJ8xjP9Be7Y2Sdr96LDm+pg==", "dev": true, "license": "MIT", "engines": { @@ -12622,9 +12621,9 @@ } }, "node_modules/webdriverio/node_modules/@types/node": { - "version": "20.19.28", - "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.28.tgz", - "integrity": "sha512-VyKBr25BuFDzBFCK5sUM6ZXiWfqgCTwTAOK8qzGV/m9FCirXYDlmczJ+d5dXBAQALGCdRRdbteKYfJ84NGEusw==", + "version": "20.19.43", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.43.tgz", + "integrity": "sha512-6oYBAi5ikg4Pl+kGsoYtawUMBT2zZMCvPNF7pVLnHZfd1zf38DRiWn/gT01RYCdUqkv7Fhr+C9ot4/tb+2sVvA==", "dev": true, "license": "MIT", "dependencies": { @@ -12740,9 +12739,9 @@ } }, "node_modules/webpack/node_modules/es-module-lexer": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.0.0.tgz", - "integrity": "sha512-5POEcUuZybH7IdmGsD8wlf0AI55wMecM9rVBTI/qEAy2c1kTOm3DjFYjrBdI2K3BaJjJYfYFeRtM0t9ssnRuxw==", + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.3.0.tgz", + "integrity": "sha512-KLdwQm2NvGLDkQDCGvmiQrhkd0JbMzXthwQAUgWjQuQdBLFa3eiBP5arXZyA+f8x+x7OXgud6bq2rxjGtHV2tw==", "dev": true, "license": "MIT" }, @@ -12760,19 +12759,6 @@ "node": ">=18" } }, - "node_modules/whatwg-encoding/node_modules/iconv-lite": { - "version": "0.6.3", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", - "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", - "dev": true, - "license": "MIT", - "dependencies": { - "safer-buffer": ">= 2.1.2 < 3.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/whatwg-fetch": { "version": "3.6.20", "resolved": "https://registry.npmjs.org/whatwg-fetch/-/whatwg-fetch-3.6.20.tgz", @@ -12906,9 +12892,9 @@ } }, "node_modules/ws": { - "version": "8.19.0", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.19.0.tgz", - "integrity": "sha512-blAT2mjOEIi0ZzruJfIhb3nps74PRWTCz1IjglWEEpQl5XS/UNama6u2/rjFkDDouqr4L67ry+1aGIALViWjDg==", + "version": "8.21.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.21.0.tgz", + "integrity": "sha512-Vsp28b7DRcimFQvrqu2Wek3z1iYxDCWqHYB8Qsnk/S4RfaCQzPGPyBNuVjJV3cd6UiKtUtp6sNM77gWvzcCH+g==", "dev": true, "license": "MIT", "engines": { @@ -12962,9 +12948,9 @@ } }, "node_modules/yargs": { - "version": "16.2.0", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz", - "integrity": "sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==", + "version": "16.2.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.2.tgz", + "integrity": "sha512-Nt9ZJjXTv5R8MHbqby/wXQ6Gi0Bb3TcYZkR1bzuL4yB2OxWPkXknz513gEF0GoA6tn00UpbPvERW8rzCuWCA6w==", "dev": true, "license": "MIT", "dependencies": { diff --git a/package.json b/package.json index cbc2eebe8..00480aa9d 100644 --- a/package.json +++ b/package.json @@ -127,7 +127,6 @@ "eslint-plugin-prettier": "^3.1.2", "happy-dom": "^20.0.11", "jiti": "^2.4.1", - "lodash": "^4.17.11", "minimatch": "^9.0.5", "mocha": "^10.2.0", "mocha-lcov-reporter": "^1.3.0",