From 024178978bf531bc008d1dae3d1ded9fac9ff9f6 Mon Sep 17 00:00:00 2001 From: esrakartalOpt Date: Fri, 7 Feb 2025 08:44:39 -0600 Subject: [PATCH 1/8] [FSSDK-11100] JS - rewrite audience_avaluator tests in TypeScript --- lib/core/audience_evaluator/index.spec.ts | 708 ++++++++++++++++++ .../index.spec.ts | 88 +++ 2 files changed, 796 insertions(+) create mode 100644 lib/core/audience_evaluator/index.spec.ts create mode 100644 lib/core/audience_evaluator/odp_segment_condition_evaluator/index.spec.ts diff --git a/lib/core/audience_evaluator/index.spec.ts b/lib/core/audience_evaluator/index.spec.ts new file mode 100644 index 000000000..9de270306 --- /dev/null +++ b/lib/core/audience_evaluator/index.spec.ts @@ -0,0 +1,708 @@ +/** + * 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 + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { describe, it, vi, expect } from 'vitest'; + +import AudienceEvaluator, { createAudienceEvaluator } from './index'; +import * as conditionTreeEvaluator from '../condition_tree_evaluator'; +import * as customAttributeConditionEvaluator from '../custom_attribute_condition_evaluator'; +import { AUDIENCE_EVALUATION_RESULT, EVALUATING_AUDIENCE } from '../../message/log_message'; +import { getMockRequestHandler } from '../../tests/mock/mock_request_handler'; +import { getMockLogger } from '../../tests/mock/mock_logger'; +import { OptimizelyDecideOption, OptimizelyDecision } from '../../shared_types'; +import { IOptimizelyUserContext } from '../../optimizely_user_context'; + +let mockLogger = getMockLogger(); + +const getMockUserContext = (attributes?: unknown, segments?: string[]): IOptimizelyUserContext => ({ + getAttributes: () => ({ ...(attributes || {}) }), + isQualifiedFor: segment => segments ? segments.indexOf(segment) > -1 : false, + qualifiedSegments: segments || [], + getUserId: () => 'mockUserId', + setAttribute: (key: string, value: any) => {}, + + decide: (key: string, options?: OptimizelyDecideOption[]): OptimizelyDecision => ({ + variationKey: 'mockVariationKey', + enabled: true, + variables: { mockVariable: 'mockValue' }, + ruleKey: 'mockRuleKey', + reasons: ['mockReason'], + flagKey: 'flagKey', + userContext: getMockUserContext() + }), +}) as IOptimizelyUserContext; + +const chromeUserAudience = { + conditions: [ + 'and', + { + name: 'browser_type', + value: 'chrome', + type: 'custom_attribute', + }, + ], +}; +const iphoneUserAudience = { + conditions: [ + 'and', + { + name: 'device_model', + value: 'iphone', + type: 'custom_attribute', + }, + ], +}; +const specialConditionTypeAudience = { + conditions: [ + 'and', + { + match: 'interest_level', + value: 'special', + type: 'special_condition_type', + }, + ], +}; +const conditionsPassingWithNoAttrs = [ + 'not', + { + match: 'exists', + name: 'input_value', + type: 'custom_attribute', + }, +]; +const conditionsPassingWithNoAttrsAudience = { + conditions: conditionsPassingWithNoAttrs, +}; + +type Condition = +| string +| { + match?: string; + value?: string; + type?: string; + name?: string; + }; + +type Audience = { + id?: string, + name?: string, + conditions: Condition[]; +}; + +const audiencesById: { +[id: string]: Audience; +} = { + "0": chromeUserAudience, + "1": iphoneUserAudience, + "2": conditionsPassingWithNoAttrsAudience, + "3": specialConditionTypeAudience, +}; + + +describe('lib/core/audience_evaluator', () => { + let audienceEvaluator; + + beforeEach(() => { + mockLogger = getMockLogger(); + }); + + afterEach(() => { + vi.restoreAllMocks(); + }); + + describe('APIs', () => { + context('with default condition evaluator', () => { + beforeEach(() => { + audienceEvaluator = vi.mocked(createAudienceEvaluator); + }); + describe('evaluate', () => { + it('should return true if there are no audiences', () => { + expect(audienceEvaluator.evaluate([], audiencesById, getMockUserContext({}))).toBe(true); + }); + + it('should return false if there are audiences but no attributes', () => { + expect(audienceEvaluator.evaluate(['0'], audiencesById, getMockUserContext({}))).toBe(false); + }); + + it('should return true if any of the audience conditions are met', () => { + const iphoneUsers = { + device_model: 'iphone', + }; + + const chromeUsers = { + browser_type: 'chrome', + }; + + const iphoneChromeUsers = { + browser_type: 'chrome', + device_model: 'iphone', + }; + + expect(audienceEvaluator.evaluate(['0', '1'], audiencesById, getMockUserContext(iphoneUsers))).toBe(true); + expect(audienceEvaluator.evaluate(['0', '1'], audiencesById, getMockUserContext(chromeUsers))).toBe(true); + expect(audienceEvaluator.evaluate(['0', '1'], audiencesById, getMockUserContext(iphoneChromeUsers))).toBe( + true + ); + }); + + it('should return false if none of the audience conditions are met', () => { + const nexusUsers = { + device_model: 'nexus5', + }; + + const safariUsers = { + browser_type: 'safari', + }; + + const nexusSafariUsers = { + browser_type: 'safari', + device_model: 'nexus5', + }; + + expect(audienceEvaluator.evaluate(['0', '1'], audiencesById, getMockUserContext(nexusUsers))).toBe(false); + expect(audienceEvaluator.evaluate(['0', '1'], audiencesById, getMockUserContext(safariUsers))).toBe(false); + expect(audienceEvaluator.evaluate(['0', '1'], audiencesById, getMockUserContext(nexusSafariUsers))).toBe( + false + ); + }); + + it('should return true if no attributes are passed and the audience conditions evaluate to true in the absence of attributes', () => { + expect(audienceEvaluator.evaluate(['2'], audiencesById, getMockUserContext({}))).toBe(true); + }); + + describe('complex audience conditions', () => { + it('should return true if any of the audiences in an "OR" condition pass', () => { + const result = audienceEvaluator.evaluate( + ['or', '0', '1'], + audiencesById, + getMockUserContext({ browser_type: 'chrome' }) + ); + expect(result).toBe(true); + }); + + it('should return true if all of the audiences in an "AND" condition pass', () => { + const result = audienceEvaluator.evaluate( + ['and', '0', '1'], + audiencesById, + getMockUserContext({ + browser_type: 'chrome', + device_model: 'iphone', + }) + ); + expect(result).toBe(true); + }); + + it('should return true if the audience in a "NOT" condition does not pass', () => { + const result = audienceEvaluator.evaluate( + ['not', '1'], + audiencesById, + getMockUserContext({ device_model: 'android' }) + ); + expect(result).toBe(true); + }); + }); + + describe('integration with dependencies', () => { + vi.mock('../condition_tree_evaluator', () => ({ + evaluate: vi.fn(), + })); + + beforeEach(() => { + vi.clearAllMocks(); + vi.spyOn(conditionTreeEvaluator, 'evaluate').mockImplementation(() => true); + }); + + afterEach(() => { + vi.resetAllMocks(); + }); + + it('returns true if conditionTreeEvaluator.evaluate returns true', () => { + vi.spyOn(conditionTreeEvaluator, 'evaluate').mockReturnValue(true); + const result = audienceEvaluator.evaluate( + ['or', '0', '1'], + audiencesById, + getMockUserContext({ browser_type: 'chrome' }) + ); + expect(result).toBe(true); + }); + + it('returns false if conditionTreeEvaluator.evaluate returns false', () => { + vi.spyOn(conditionTreeEvaluator, 'evaluate').mockReturnValue(false); + const result = audienceEvaluator.evaluate( + ['or', '0', '1'], + audiencesById, + getMockUserContext({ browser_type: 'safari' }) + ); + expect(result).toBe(false); + }); + + it('returns false if conditionTreeEvaluator.evaluate returns null', () => { + vi.spyOn(conditionTreeEvaluator, 'evaluate').mockReturnValue(null); + const result = audienceEvaluator.evaluate( + ['or', '0', '1'], + audiencesById, + getMockUserContext({ state: 'California' }) + ); + expect(result).toBe(false); + }); + + it('calls customAttributeConditionEvaluator.evaluate in the leaf evaluator for audience conditions', () => { + const conditionTreeEvaluator = { + evaluate: vi.fn((conditions, leafEvaluator) => leafEvaluator(conditions[1])), + }; + + const mockCustomAttributeConditionEvaluator = vi.fn().mockReturnValue(false); + + vi.spyOn(customAttributeConditionEvaluator, 'getEvaluator').mockReturnValue({ + evaluate: mockCustomAttributeConditionEvaluator, + }); + + const audienceEvaluator = createAudienceEvaluator(conditionTreeEvaluator); + + const userAttributes = { device_model: 'android' }; + const user = getMockUserContext(userAttributes); + const result = audienceEvaluator.evaluate(['or', '1'], audiencesById, user); + + expect(mockCustomAttributeConditionEvaluator).toHaveBeenCalledTimes(1); + expect(mockCustomAttributeConditionEvaluator).toHaveBeenCalledWith( + iphoneUserAudience.conditions[1], + user, + ); + + expect(result).toBe(false); + + vi.restoreAllMocks(); + }); + }); + + describe('Audience evaluation logging', () => { + let mockCustomAttributeConditionEvaluator; + + beforeEach(() => { + mockCustomAttributeConditionEvaluator = vi.fn(); + vi.spyOn(conditionTreeEvaluator, 'evaluate'); + vi.spyOn(customAttributeConditionEvaluator, 'getEvaluator').mockReturnValue({ + evaluate: mockCustomAttributeConditionEvaluator, + }); + }); + + afterEach(() => { + vi.restoreAllMocks(); + }); + + it('logs correctly when conditionTreeEvaluator.evaluate returns null', () => { + vi.spyOn(conditionTreeEvaluator, 'evaluate').mockImplementationOnce((conditions: any, leafEvaluator) => { + return leafEvaluator(conditions[1]); + }); + + mockCustomAttributeConditionEvaluator.returns(null); + const userAttributes = { device_model: 5.5 }; + const user = getMockUserContext(userAttributes); + + const audienceEvaluator = createAudienceEvaluator({}, mockLogger); + + const result = audienceEvaluator.evaluate(['or', '1'], audiencesById, user); + + expect(mockCustomAttributeConditionEvaluator).toHaveBeenCalledTimes(1); + expect(mockCustomAttributeConditionEvaluator).toHaveBeenCalledWith(iphoneUserAudience.conditions[1], user); + expect(result).toBe(false); + expect(2).toStrictEqual(mockLogger.debug.caller); + + expect(mockLogger.debug).toHaveBeenCalledWith( + EVALUATING_AUDIENCE, + '1', + JSON.stringify(['and', iphoneUserAudience.conditions[1]]) + ); + + expect(mockLogger.debug).toHaveBeenCalledWith(AUDIENCE_EVALUATION_RESULT, '1', 'UNKNOWN'); + }); + + it('logs correctly when conditionTreeEvaluator.evaluate returns true', () => { + vi.spyOn(conditionTreeEvaluator, 'evaluate').mockImplementationOnce((conditions: any, leafEvaluator) => { + return leafEvaluator(conditions[1]); + }); + + mockCustomAttributeConditionEvaluator.returns(true); + + const userAttributes = { device_model: 'iphone' }; + const user = getMockUserContext(userAttributes); + + const audienceEvaluator = createAudienceEvaluator({}, mockLogger); + + const result = audienceEvaluator.evaluate(['or', '1'], audiencesById, user); + expect(mockCustomAttributeConditionEvaluator).toHaveBeenCalledTimes(1); + expect(mockCustomAttributeConditionEvaluator).toHaveBeenCalledWith(iphoneUserAudience.conditions[1], user); + expect(result).toBe(true); + expect(2).toStrictEqual(mockLogger.debug.call.length); + expect(mockLogger.debug).toHaveBeenCalledWith( + EVALUATING_AUDIENCE, + '1', + JSON.stringify(['and', iphoneUserAudience.conditions[1]]) + ); + + expect(mockLogger.debug).toHaveBeenCalledWith(AUDIENCE_EVALUATION_RESULT, '1', 'TRUE'); + }); + + it('logs correctly when conditionTreeEvaluator.evaluate returns false', () => { + vi.spyOn(conditionTreeEvaluator, 'evaluate').mockImplementationOnce((conditions: any, leafEvaluator) => { + return leafEvaluator(conditions[1]); + }); + + mockCustomAttributeConditionEvaluator.returns(false); + + const userAttributes = { device_model: 'android' }; + const user = getMockUserContext(userAttributes); + + const audienceEvaluator = createAudienceEvaluator({}, mockLogger); + + const result = audienceEvaluator.evaluate(['or', '1'], audiencesById, user); + expect(mockCustomAttributeConditionEvaluator).toHaveBeenCalledTimes(1); + expect(mockCustomAttributeConditionEvaluator).toHaveBeenCalledWith(iphoneUserAudience.conditions[1], user); + expect(result).toBe(false); + expect(2).toStrictEqual(mockLogger.debug.call.length); + expect(mockLogger.debug).toHaveBeenCalledWith( + EVALUATING_AUDIENCE, + '1', + JSON.stringify(['and', iphoneUserAudience.conditions[1]]) + ); + + expect(mockLogger.debug).toHaveBeenCalledWith(AUDIENCE_EVALUATION_RESULT, '1', 'FALSE'); + }); + }); + }); + }); + + context('with additional custom condition evaluator', () => { + describe('when passing a valid additional evaluator', () => { + beforeEach(() => { + const mockEnvironment = { + special: true, + }; + audienceEvaluator = createAudienceEvaluator({ + special_condition_type: { + evaluate: (condition, user) => { + const result = mockEnvironment[condition.value] && user.getAttributes()[condition.match] > 0; + return result; + }, + }, + }); + }); + + it('should evaluate an audience properly using the custom condition evaluator', () => { + expect(audienceEvaluator.evaluate(['3'], audiencesById, getMockUserContext({ interest_level: 0 }))).toBe( + false + ); + expect(audienceEvaluator.evaluate(['3'], audiencesById, getMockUserContext({ interest_level: 1 }))).toBe( + true + ); + }); + }); + + describe('when passing an invalid additional evaluator', () => { + beforeEach(() => { + audienceEvaluator = createAudienceEvaluator({ + custom_attribute: { + evaluate: () => { + return false; + }, + }, + }); + }); + + it('should not be able to overwrite built in `custom_attribute` evaluator', () => { + expect( + audienceEvaluator.evaluate( + ['0'], + audiencesById, + getMockUserContext({ + browser_type: 'chrome', + }) + ) + ).toBe(true); + }); + }); + }); + + context('with odp segment evaluator', () => { + describe('Single ODP Audience', () => { + const singleAudience = { + conditions: [ + 'and', + { + value: 'odp-segment-1', + type: 'third_party_dimension', + name: 'odp.audiences', + match: 'qualified', + }, + ], + }; + const audiencesById = { + 0: singleAudience, + }; + const audience = new AudienceEvaluator(); + + it('should evaluate to true if segment is found', () => { + expect(audience.evaluate(['or', '0'], audiencesById, getMockUserContext({}, ['odp-segment-1']))).toBe(true); + }); + + it('should evaluate to false if segment is not found', () => { + expect(audience.evaluate(['or', '0'], audiencesById, getMockUserContext({}, ['odp-segment-2']))).toBe(false); + }); + + it('should evaluate to false if not segments are provided', () => { + expect(audience.evaluate(['or', '0'], audiencesById, getMockUserContext({}))).toBe(false); + }); + }); + + describe('Multiple ODP conditions in one Audience', () => { + const singleAudience = { + conditions: [ + 'and', + { + value: 'odp-segment-1', + type: 'third_party_dimension', + name: 'odp.audiences', + match: 'qualified', + }, + { + value: 'odp-segment-2', + type: 'third_party_dimension', + name: 'odp.audiences', + match: 'qualified', + }, + [ + 'or', + { + value: 'odp-segment-3', + type: 'third_party_dimension', + name: 'odp.audiences', + match: 'qualified', + }, + { + value: 'odp-segment-4', + type: 'third_party_dimension', + name: 'odp.audiences', + match: 'qualified', + }, + ], + ], + }; + const audiencesById = { + 0: singleAudience, + }; + const audience = new AudienceEvaluator(); + + it('should evaluate correctly based on the given segments', () => { + expect( + audience.evaluate( + ['or', '0'], + audiencesById, + getMockUserContext({}, ['odp-segment-1', 'odp-segment-2', 'odp-segment-3']) + ) + ).toBe(true); + expect( + audience.evaluate( + ['or', '0'], + audiencesById, + getMockUserContext({}, ['odp-segment-1', 'odp-segment-2', 'odp-segment-4']) + ) + ).toBe(true); + expect( + audience.evaluate( + ['or', '0'], + audiencesById, + getMockUserContext({}, ['odp-segment-1', 'odp-segment-2', 'odp-segment-3', 'odp-segment-4']) + ) + ).toBe(true); + expect( + audience.evaluate( + ['or', '0'], + audiencesById, + getMockUserContext({}, ['odp-segment-1', 'odp-segment-3', 'odp-segment-4']) + ) + ).toBe(false); + expect( + audience.evaluate( + ['or', '0'], + audiencesById, + getMockUserContext({}, ['odp-segment-2', 'odp-segment-3', 'odp-segment-4']) + ) + ).toBe(false); + }); + }); + + describe('Multiple ODP conditions in multiple Audience', () => { + const audience1And2 = { + conditions: [ + 'and', + { + value: 'odp-segment-1', + type: 'third_party_dimension', + name: 'odp.audiences', + match: 'qualified', + }, + { + value: 'odp-segment-2', + type: 'third_party_dimension', + name: 'odp.audiences', + match: 'qualified', + }, + ], + }; + + const audience3And4 = { + conditions: [ + 'and', + { + value: 'odp-segment-3', + type: 'third_party_dimension', + name: 'odp.audiences', + match: 'qualified', + }, + { + value: 'odp-segment-4', + type: 'third_party_dimension', + name: 'odp.audiences', + match: 'qualified', + }, + ], + }; + + const audience5And6 = { + conditions: [ + 'or', + { + value: 'odp-segment-5', + type: 'third_party_dimension', + name: 'odp.audiences', + match: 'qualified', + }, + { + value: 'odp-segment-6', + type: 'third_party_dimension', + name: 'odp.audiences', + match: 'qualified', + }, + ], + }; + const audiencesById = { + 0: audience1And2, + 1: audience3And4, + 2: audience5And6, + }; + const audience = new AudienceEvaluator(); + + it('should evaluate correctly based on the given segments', () => { + expect( + audience.evaluate( + ['or', '0', '1', '2'], + audiencesById, + getMockUserContext({}, ['odp-segment-1', 'odp-segment-2']) + ) + ).toBe(true); + expect( + audience.evaluate( + ['and', '0', '1', '2'], + audiencesById, + getMockUserContext({}, ['odp-segment-1', 'odp-segment-2']) + ) + ).toBe(false); + expect( + audience.evaluate( + ['and', '0', '1', '2'], + audiencesById, + getMockUserContext({}, [ + 'odp-segment-1', + 'odp-segment-2', + 'odp-segment-3', + 'odp-segment-4', + 'odp-segment-6', + ]) + ) + ).toBe(true); + expect( + audience.evaluate( + ['and', '0', '1', ['not', '2']], + audiencesById, + getMockUserContext({}, ['odp-segment-1', 'odp-segment-2', 'odp-segment-3', 'odp-segment-4']) + ) + ).toBe(true); + }); + }); + }); + + context('with multiple types of evaluators', () => { + const audience1And2 = { + conditions: [ + 'and', + { + value: 'odp-segment-1', + type: 'third_party_dimension', + name: 'odp.audiences', + match: 'qualified', + }, + { + value: 'odp-segment-2', + type: 'third_party_dimension', + name: 'odp.audiences', + match: 'qualified', + }, + ], + }; + const audience3Or4 = { + conditions: [ + 'or', + { + value: 'odp-segment-3', + type: 'third_party_dimension', + name: 'odp.audiences', + match: 'qualified', + }, + { + value: 'odp-segment-4', + type: 'third_party_dimension', + name: 'odp.audiences', + match: 'qualified', + }, + ], + }; + + const audiencesById = { + 0: audience1And2, + 1: audience3Or4, + 2: chromeUserAudience, + }; + + const audience = new AudienceEvaluator(); + + it('should evaluate correctly based on the given segments', () => { + expect( + audience.evaluate( + ['and', '0', '1', '2'], + audiencesById, + getMockUserContext({ browser_type: 'not_chrome' }, ['odp-segment-1', 'odp-segment-2', 'odp-segment-4']) + ) + ).toBe(false); + expect( + audience.evaluate( + ['and', '0', '1', '2'], + audiencesById, + getMockUserContext({ browser_type: 'chrome' }, ['odp-segment-1', 'odp-segment-2', 'odp-segment-4']) + ) + ).toBe(true); + }); + }); + }); +}); diff --git a/lib/core/audience_evaluator/odp_segment_condition_evaluator/index.spec.ts b/lib/core/audience_evaluator/odp_segment_condition_evaluator/index.spec.ts new file mode 100644 index 000000000..0c3edc229 --- /dev/null +++ b/lib/core/audience_evaluator/odp_segment_condition_evaluator/index.spec.ts @@ -0,0 +1,88 @@ +/** + * 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 + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { beforeEach, afterEach, describe, it, vi, expect } from 'vitest'; + +import * as odpSegmentEvalutor from '.'; +import { UNKNOWN_MATCH_TYPE } from '../../../message/error_message'; +import { IOptimizelyUserContext } from '../../../optimizely_user_context'; +import { OptimizelyDecideOption, OptimizelyDecision } from '../../../shared_types'; + +const odpSegment1Condition = { + "value": "odp-segment-1", + "type": "third_party_dimension", + "name": "odp.audiences", + "match": "qualified" +}; + +const getMockUserContext = (attributes?: unknown, segments?: string[]): IOptimizelyUserContext => ({ + getAttributes: () => ({ ...(attributes || {}) }), + isQualifiedFor: segment => segments ? segments.indexOf(segment) > -1 : false, + qualifiedSegments: segments || [], + getUserId: () => 'mockUserId', + setAttribute: (key: string, value: any) => {}, + + decide: (key: string, options?: OptimizelyDecideOption[]): OptimizelyDecision => ({ + variationKey: 'mockVariationKey', + enabled: true, + variables: { mockVariable: 'mockValue' }, + ruleKey: 'mockRuleKey', + reasons: ['mockReason'], + flagKey: 'flagKey', + userContext: getMockUserContext() + }), +}) as IOptimizelyUserContext; + + +const createLogger = () => ({ + debug: () => {}, + info: () => {}, + warn: () => {}, + error: () => {}, + child: () => createLogger(), +}) + +describe('lib/core/audience_evaluator/odp_segment_condition_evaluator', function() { + const mockLogger = createLogger(); + const { evaluate } = odpSegmentEvalutor.getEvaluator(mockLogger); + + beforeEach(function() { + vi.fn(mockLogger.warn); + vi.fn(mockLogger.error); + }); + + afterEach(function() { + vi.restoreAllMocks(); + }); + + it('should return true when segment qualifies and known match type is provided', () => { + expect(evaluate(odpSegment1Condition, getMockUserContext({}, ['odp-segment-1']))).toBe(true); + }); + + it('should return false when segment does not qualify and known match type is provided', () => { + expect(evaluate(odpSegment1Condition, getMockUserContext({}, ['odp-segment-2']))).toBe(false); + }) + + it('should return null when segment qualifies but unknown match type is provided', () => { + const invalidOdpMatchCondition = { + ... odpSegment1Condition, + "match": 'unknown', + }; + expect(evaluate(invalidOdpMatchCondition, getMockUserContext({}, ['odp-segment-1']))).toBeNull(); + expect(mockLogger.warn).toHaveBeenCalledTimes(1); + expect(mockLogger.warn.arguments[0][0]).toStrictEqual(UNKNOWN_MATCH_TYPE); + expect(mockLogger.warn.arguments[0][1]).toStrictEqual(JSON.stringify(invalidOdpMatchCondition)); + }); +}); From 4f836ec10b883fc2408d3a5e137fc4fcc9d4c578 Mon Sep 17 00:00:00 2001 From: esrakartalOpt Date: Fri, 7 Feb 2025 09:18:15 -0600 Subject: [PATCH 2/8] Fix test case --- lib/core/audience_evaluator/index.spec.ts | 56 +++++++++++++---------- 1 file changed, 31 insertions(+), 25 deletions(-) diff --git a/lib/core/audience_evaluator/index.spec.ts b/lib/core/audience_evaluator/index.spec.ts index 9de270306..fb12425be 100644 --- a/lib/core/audience_evaluator/index.spec.ts +++ b/lib/core/audience_evaluator/index.spec.ts @@ -13,15 +13,14 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { describe, it, vi, expect } from 'vitest'; +import { beforeEach, afterEach, describe, it, vi, expect } from 'vitest'; import AudienceEvaluator, { createAudienceEvaluator } from './index'; import * as conditionTreeEvaluator from '../condition_tree_evaluator'; import * as customAttributeConditionEvaluator from '../custom_attribute_condition_evaluator'; import { AUDIENCE_EVALUATION_RESULT, EVALUATING_AUDIENCE } from '../../message/log_message'; -import { getMockRequestHandler } from '../../tests/mock/mock_request_handler'; import { getMockLogger } from '../../tests/mock/mock_logger'; -import { OptimizelyDecideOption, OptimizelyDecision } from '../../shared_types'; +import { Audience, OptimizelyDecideOption, OptimizelyDecision } from '../../shared_types'; import { IOptimizelyUserContext } from '../../optimizely_user_context'; let mockLogger = getMockLogger(); @@ -45,6 +44,8 @@ const getMockUserContext = (attributes?: unknown, segments?: string[]): IOptimiz }) as IOptimizelyUserContext; const chromeUserAudience = { + id: '0', + name: 'chromeUserAudience', conditions: [ 'and', { @@ -55,6 +56,8 @@ const chromeUserAudience = { ], }; const iphoneUserAudience = { + id: '1', + name: 'iphoneUserAudience', conditions: [ 'and', { @@ -65,6 +68,8 @@ const iphoneUserAudience = { ], }; const specialConditionTypeAudience = { + id: '3', + name: 'specialConditionTypeAudience', conditions: [ 'and', { @@ -83,24 +88,11 @@ const conditionsPassingWithNoAttrs = [ }, ]; const conditionsPassingWithNoAttrsAudience = { + id: '2', + name: 'conditionsPassingWithNoAttrsAudience', conditions: conditionsPassingWithNoAttrs, }; -type Condition = -| string -| { - match?: string; - value?: string; - type?: string; - name?: string; - }; - -type Audience = { - id?: string, - name?: string, - conditions: Condition[]; -}; - const audiencesById: { [id: string]: Audience; } = { @@ -384,7 +376,7 @@ describe('lib/core/audience_evaluator', () => { }); }); - context('with additional custom condition evaluator', () => { + describe('with additional custom condition evaluator', () => { describe('when passing a valid additional evaluator', () => { beforeEach(() => { const mockEnvironment = { @@ -435,9 +427,11 @@ describe('lib/core/audience_evaluator', () => { }); }); - context('with odp segment evaluator', () => { + describe('with odp segment evaluator', () => { describe('Single ODP Audience', () => { const singleAudience = { + id: '0', + name: 'singleAudience', conditions: [ 'and', { @@ -451,7 +445,7 @@ describe('lib/core/audience_evaluator', () => { const audiencesById = { 0: singleAudience, }; - const audience = new AudienceEvaluator(); + const audience = new AudienceEvaluator({}); it('should evaluate to true if segment is found', () => { expect(audience.evaluate(['or', '0'], audiencesById, getMockUserContext({}, ['odp-segment-1']))).toBe(true); @@ -468,6 +462,8 @@ describe('lib/core/audience_evaluator', () => { describe('Multiple ODP conditions in one Audience', () => { const singleAudience = { + id: '0', + name: 'singleAudience', conditions: [ 'and', { @@ -502,7 +498,7 @@ describe('lib/core/audience_evaluator', () => { const audiencesById = { 0: singleAudience, }; - const audience = new AudienceEvaluator(); + const audience = new AudienceEvaluator({}); it('should evaluate correctly based on the given segments', () => { expect( @@ -545,6 +541,8 @@ describe('lib/core/audience_evaluator', () => { describe('Multiple ODP conditions in multiple Audience', () => { const audience1And2 = { + id: '0', + name: 'audience1And2', conditions: [ 'and', { @@ -563,6 +561,8 @@ describe('lib/core/audience_evaluator', () => { }; const audience3And4 = { + id: '1', + name: 'audience3And4', conditions: [ 'and', { @@ -581,6 +581,8 @@ describe('lib/core/audience_evaluator', () => { }; const audience5And6 = { + id: '2', + name: 'audience5And6', conditions: [ 'or', { @@ -602,7 +604,7 @@ describe('lib/core/audience_evaluator', () => { 1: audience3And4, 2: audience5And6, }; - const audience = new AudienceEvaluator(); + const audience = new AudienceEvaluator({}); it('should evaluate correctly based on the given segments', () => { expect( @@ -643,8 +645,10 @@ describe('lib/core/audience_evaluator', () => { }); }); - context('with multiple types of evaluators', () => { + it('with multiple types of evaluators', () => { const audience1And2 = { + id: '0', + name: 'audience1And2', conditions: [ 'and', { @@ -662,6 +666,8 @@ describe('lib/core/audience_evaluator', () => { ], }; const audience3Or4 = { + id: '', + name: 'audience3And4', conditions: [ 'or', { @@ -685,7 +691,7 @@ describe('lib/core/audience_evaluator', () => { 2: chromeUserAudience, }; - const audience = new AudienceEvaluator(); + const audience = new AudienceEvaluator({}); it('should evaluate correctly based on the given segments', () => { expect( From aa4d61f50e36fb773565e8ec4142813691083bf2 Mon Sep 17 00:00:00 2001 From: esrakartalOpt Date: Fri, 7 Feb 2025 09:55:02 -0600 Subject: [PATCH 3/8] Fix test cases --- lib/core/audience_evaluator/index.spec.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/core/audience_evaluator/index.spec.ts b/lib/core/audience_evaluator/index.spec.ts index fb12425be..fb640f787 100644 --- a/lib/core/audience_evaluator/index.spec.ts +++ b/lib/core/audience_evaluator/index.spec.ts @@ -115,7 +115,7 @@ describe('lib/core/audience_evaluator', () => { }); describe('APIs', () => { - context('with default condition evaluator', () => { + describe('with default condition evaluator', () => { beforeEach(() => { audienceEvaluator = vi.mocked(createAudienceEvaluator); }); From 99b7a594f107b802ea4e049161595025991854f1 Mon Sep 17 00:00:00 2001 From: Raju Ahmed Date: Fri, 7 Feb 2025 22:16:01 +0600 Subject: [PATCH 4/8] type fix --- lib/core/audience_evaluator/index.spec.ts | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/lib/core/audience_evaluator/index.spec.ts b/lib/core/audience_evaluator/index.spec.ts index fb640f787..cfa2daaa6 100644 --- a/lib/core/audience_evaluator/index.spec.ts +++ b/lib/core/audience_evaluator/index.spec.ts @@ -104,7 +104,7 @@ const audiencesById: { describe('lib/core/audience_evaluator', () => { - let audienceEvaluator; + let audienceEvaluator: AudienceEvaluator; beforeEach(() => { mockLogger = getMockLogger(); @@ -117,7 +117,7 @@ describe('lib/core/audience_evaluator', () => { describe('APIs', () => { describe('with default condition evaluator', () => { beforeEach(() => { - audienceEvaluator = vi.mocked(createAudienceEvaluator); + audienceEvaluator = createAudienceEvaluator({}); }); describe('evaluate', () => { it('should return true if there are no audiences', () => { @@ -280,7 +280,7 @@ describe('lib/core/audience_evaluator', () => { }); describe('Audience evaluation logging', () => { - let mockCustomAttributeConditionEvaluator; + let mockCustomAttributeConditionEvaluator: ReturnType; beforeEach(() => { mockCustomAttributeConditionEvaluator = vi.fn(); @@ -299,7 +299,7 @@ describe('lib/core/audience_evaluator', () => { return leafEvaluator(conditions[1]); }); - mockCustomAttributeConditionEvaluator.returns(null); + mockCustomAttributeConditionEvaluator.mockReturnValue(null); const userAttributes = { device_model: 5.5 }; const user = getMockUserContext(userAttributes); @@ -326,7 +326,7 @@ describe('lib/core/audience_evaluator', () => { return leafEvaluator(conditions[1]); }); - mockCustomAttributeConditionEvaluator.returns(true); + mockCustomAttributeConditionEvaluator.mockReturnValue(true); const userAttributes = { device_model: 'iphone' }; const user = getMockUserContext(userAttributes); @@ -352,7 +352,7 @@ describe('lib/core/audience_evaluator', () => { return leafEvaluator(conditions[1]); }); - mockCustomAttributeConditionEvaluator.returns(false); + mockCustomAttributeConditionEvaluator.mockReturnValue(false); const userAttributes = { device_model: 'android' }; const user = getMockUserContext(userAttributes); @@ -384,7 +384,8 @@ describe('lib/core/audience_evaluator', () => { }; audienceEvaluator = createAudienceEvaluator({ special_condition_type: { - evaluate: (condition, user) => { + evaluate: (condition: any, user: any) => { + // @ts-ignore const result = mockEnvironment[condition.value] && user.getAttributes()[condition.match] > 0; return result; }, From 7f74ca5c9c2380a61391cf555cee6df5e23467d0 Mon Sep 17 00:00:00 2001 From: esrakartalOpt Date: Wed, 12 Feb 2025 14:09:14 -0600 Subject: [PATCH 5/8] Fix failed test case --- lib/core/audience_evaluator/index.spec.ts | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/lib/core/audience_evaluator/index.spec.ts b/lib/core/audience_evaluator/index.spec.ts index cfa2daaa6..d573f63e5 100644 --- a/lib/core/audience_evaluator/index.spec.ts +++ b/lib/core/audience_evaluator/index.spec.ts @@ -207,10 +207,6 @@ describe('lib/core/audience_evaluator', () => { }); describe('integration with dependencies', () => { - vi.mock('../condition_tree_evaluator', () => ({ - evaluate: vi.fn(), - })); - beforeEach(() => { vi.clearAllMocks(); vi.spyOn(conditionTreeEvaluator, 'evaluate').mockImplementation(() => true); @@ -385,6 +381,7 @@ describe('lib/core/audience_evaluator', () => { audienceEvaluator = createAudienceEvaluator({ special_condition_type: { evaluate: (condition: any, user: any) => { + // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore const result = mockEnvironment[condition.value] && user.getAttributes()[condition.match] > 0; return result; @@ -646,7 +643,7 @@ describe('lib/core/audience_evaluator', () => { }); }); - it('with multiple types of evaluators', () => { + describe('with multiple types of evaluators', () => { const audience1And2 = { id: '0', name: 'audience1And2', From e1929c5ba758d15c8230f9675884ed906dc80ebf Mon Sep 17 00:00:00 2001 From: Md Junaed Hossain <169046794+junaed-optimizely@users.noreply.github.com> Date: Thu, 13 Feb 2025 22:19:32 +0600 Subject: [PATCH 6/8] [FSSDK-11100] test fix --- lib/core/audience_evaluator/index.spec.ts | 7 +++--- .../index.spec.ts | 22 ++++--------------- 2 files changed, 7 insertions(+), 22 deletions(-) diff --git a/lib/core/audience_evaluator/index.spec.ts b/lib/core/audience_evaluator/index.spec.ts index d573f63e5..b82d9734c 100644 --- a/lib/core/audience_evaluator/index.spec.ts +++ b/lib/core/audience_evaluator/index.spec.ts @@ -209,7 +209,6 @@ describe('lib/core/audience_evaluator', () => { describe('integration with dependencies', () => { beforeEach(() => { vi.clearAllMocks(); - vi.spyOn(conditionTreeEvaluator, 'evaluate').mockImplementation(() => true); }); afterEach(() => { @@ -306,7 +305,7 @@ describe('lib/core/audience_evaluator', () => { expect(mockCustomAttributeConditionEvaluator).toHaveBeenCalledTimes(1); expect(mockCustomAttributeConditionEvaluator).toHaveBeenCalledWith(iphoneUserAudience.conditions[1], user); expect(result).toBe(false); - expect(2).toStrictEqual(mockLogger.debug.caller); + expect(mockLogger.debug).toHaveBeenCalledTimes(2); expect(mockLogger.debug).toHaveBeenCalledWith( EVALUATING_AUDIENCE, @@ -333,7 +332,7 @@ describe('lib/core/audience_evaluator', () => { expect(mockCustomAttributeConditionEvaluator).toHaveBeenCalledTimes(1); expect(mockCustomAttributeConditionEvaluator).toHaveBeenCalledWith(iphoneUserAudience.conditions[1], user); expect(result).toBe(true); - expect(2).toStrictEqual(mockLogger.debug.call.length); + expect(mockLogger.debug).toHaveBeenCalledTimes(2) expect(mockLogger.debug).toHaveBeenCalledWith( EVALUATING_AUDIENCE, '1', @@ -359,7 +358,7 @@ describe('lib/core/audience_evaluator', () => { expect(mockCustomAttributeConditionEvaluator).toHaveBeenCalledTimes(1); expect(mockCustomAttributeConditionEvaluator).toHaveBeenCalledWith(iphoneUserAudience.conditions[1], user); expect(result).toBe(false); - expect(2).toStrictEqual(mockLogger.debug.call.length); + expect(mockLogger.debug).toHaveBeenCalledTimes(2) expect(mockLogger.debug).toHaveBeenCalledWith( EVALUATING_AUDIENCE, '1', diff --git a/lib/core/audience_evaluator/odp_segment_condition_evaluator/index.spec.ts b/lib/core/audience_evaluator/odp_segment_condition_evaluator/index.spec.ts index 0c3edc229..f42d07cb4 100644 --- a/lib/core/audience_evaluator/odp_segment_condition_evaluator/index.spec.ts +++ b/lib/core/audience_evaluator/odp_segment_condition_evaluator/index.spec.ts @@ -13,12 +13,12 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { beforeEach, afterEach, describe, it, vi, expect } from 'vitest'; - +import { afterEach, describe, it, vi, expect } from 'vitest'; import * as odpSegmentEvalutor from '.'; import { UNKNOWN_MATCH_TYPE } from '../../../message/error_message'; import { IOptimizelyUserContext } from '../../../optimizely_user_context'; import { OptimizelyDecideOption, OptimizelyDecision } from '../../../shared_types'; +import { getMockLogger } from '../../../tests/mock/mock_logger'; const odpSegment1Condition = { "value": "odp-segment-1", @@ -46,23 +46,10 @@ const getMockUserContext = (attributes?: unknown, segments?: string[]): IOptimiz }) as IOptimizelyUserContext; -const createLogger = () => ({ - debug: () => {}, - info: () => {}, - warn: () => {}, - error: () => {}, - child: () => createLogger(), -}) - describe('lib/core/audience_evaluator/odp_segment_condition_evaluator', function() { - const mockLogger = createLogger(); + const mockLogger = getMockLogger(); const { evaluate } = odpSegmentEvalutor.getEvaluator(mockLogger); - beforeEach(function() { - vi.fn(mockLogger.warn); - vi.fn(mockLogger.error); - }); - afterEach(function() { vi.restoreAllMocks(); }); @@ -82,7 +69,6 @@ describe('lib/core/audience_evaluator/odp_segment_condition_evaluator', function }; expect(evaluate(invalidOdpMatchCondition, getMockUserContext({}, ['odp-segment-1']))).toBeNull(); expect(mockLogger.warn).toHaveBeenCalledTimes(1); - expect(mockLogger.warn.arguments[0][0]).toStrictEqual(UNKNOWN_MATCH_TYPE); - expect(mockLogger.warn.arguments[0][1]).toStrictEqual(JSON.stringify(invalidOdpMatchCondition)); + expect(mockLogger.warn).toHaveBeenCalledWith(UNKNOWN_MATCH_TYPE, JSON.stringify(invalidOdpMatchCondition)); }); }); From 60716f26fb3469eb1631cb4fdccacd7e56bbcc43 Mon Sep 17 00:00:00 2001 From: esrakartalOpt Date: Thu, 13 Feb 2025 12:50:57 -0600 Subject: [PATCH 7/8] remove unnecessary restore --- lib/core/audience_evaluator/index.spec.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/lib/core/audience_evaluator/index.spec.ts b/lib/core/audience_evaluator/index.spec.ts index b82d9734c..18972fab5 100644 --- a/lib/core/audience_evaluator/index.spec.ts +++ b/lib/core/audience_evaluator/index.spec.ts @@ -269,8 +269,6 @@ describe('lib/core/audience_evaluator', () => { ); expect(result).toBe(false); - - vi.restoreAllMocks(); }); }); From 1761d4a39030651d0820ac008ed95ce339807f0e Mon Sep 17 00:00:00 2001 From: Raju Ahmed Date: Fri, 14 Feb 2025 02:37:24 +0600 Subject: [PATCH 8/8] update --- lib/core/audience_evaluator/index.spec.ts | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/lib/core/audience_evaluator/index.spec.ts b/lib/core/audience_evaluator/index.spec.ts index 18972fab5..e22654144 100644 --- a/lib/core/audience_evaluator/index.spec.ts +++ b/lib/core/audience_evaluator/index.spec.ts @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { beforeEach, afterEach, describe, it, vi, expect } from 'vitest'; +import { beforeEach, afterEach, describe, it, vi, expect, afterAll } from 'vitest'; import AudienceEvaluator, { createAudienceEvaluator } from './index'; import * as conditionTreeEvaluator from '../condition_tree_evaluator'; @@ -215,6 +215,10 @@ describe('lib/core/audience_evaluator', () => { vi.resetAllMocks(); }); + afterAll(() => { + vi.resetAllMocks(); + }); + it('returns true if conditionTreeEvaluator.evaluate returns true', () => { vi.spyOn(conditionTreeEvaluator, 'evaluate').mockReturnValue(true); const result = audienceEvaluator.evaluate( @@ -246,9 +250,9 @@ describe('lib/core/audience_evaluator', () => { }); it('calls customAttributeConditionEvaluator.evaluate in the leaf evaluator for audience conditions', () => { - const conditionTreeEvaluator = { - evaluate: vi.fn((conditions, leafEvaluator) => leafEvaluator(conditions[1])), - }; + vi.spyOn(conditionTreeEvaluator, 'evaluate').mockImplementation((conditions: any, leafEvaluator) => { + return leafEvaluator(conditions[1]); + }); const mockCustomAttributeConditionEvaluator = vi.fn().mockReturnValue(false); @@ -256,7 +260,7 @@ describe('lib/core/audience_evaluator', () => { evaluate: mockCustomAttributeConditionEvaluator, }); - const audienceEvaluator = createAudienceEvaluator(conditionTreeEvaluator); + const audienceEvaluator = createAudienceEvaluator({}); const userAttributes = { device_model: 'android' }; const user = getMockUserContext(userAttributes);