diff --git a/lib/core/bucketer/bucket_value_generator.spec.ts b/lib/core/bucketer/bucket_value_generator.spec.ts index a7662e1f0..e68db6348 100644 --- a/lib/core/bucketer/bucket_value_generator.spec.ts +++ b/lib/core/bucketer/bucket_value_generator.spec.ts @@ -36,8 +36,14 @@ describe('generateBucketValue', () => { it('should return an error if it cannot generate the hash value', () => { // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore - expect(() => generateBucketValue(null)).toThrowError( - new OptimizelyError(INVALID_BUCKETING_ID) - ); + expect(() => generateBucketValue(null)).toThrow(OptimizelyError); + try { + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + generateBucketValue(null); + } catch (err) { + expect(err).toBeInstanceOf(OptimizelyError); + expect(err.baseMessage).toBe(INVALID_BUCKETING_ID); + } }); }); diff --git a/lib/core/bucketer/index.spec.ts b/lib/core/bucketer/index.spec.ts index 36f23b2eb..b3aac5158 100644 --- a/lib/core/bucketer/index.spec.ts +++ b/lib/core/bucketer/index.spec.ts @@ -198,9 +198,14 @@ describe('including groups: random', () => { const bucketerParamsWithInvalidGroupId = cloneDeep(bucketerParams); bucketerParamsWithInvalidGroupId.experimentIdMap[configObj.experiments[4].id].groupId = '6969'; - expect(() => bucketer.bucket(bucketerParamsWithInvalidGroupId)).toThrowError( - new OptimizelyError(INVALID_GROUP_ID, '6969') - ); + expect(()=> bucketer.bucket(bucketerParamsWithInvalidGroupId)).toThrow(OptimizelyError); + + try { + bucketer.bucket(bucketerParamsWithInvalidGroupId); + } catch(err) { + expect(err).toBeInstanceOf(OptimizelyError); + expect(err.baseMessage).toBe(INVALID_GROUP_ID); + } }); }); diff --git a/lib/notification_center/index.spec.ts b/lib/notification_center/index.spec.ts new file mode 100644 index 000000000..4ba54a0c3 --- /dev/null +++ b/lib/notification_center/index.spec.ts @@ -0,0 +1,606 @@ +/** + * 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, beforeEach, it, vi, expect } from 'vitest'; +import { createNotificationCenter, DefaultNotificationCenter } from './'; +import { + ActivateListenerPayload, + DecisionListenerPayload, + LogEventListenerPayload, + NOTIFICATION_TYPES, + TrackListenerPayload, + OptimizelyConfigUpdateListenerPayload, +} from './type'; +import { getMockLogger } from '../tests/mock/mock_logger'; +import { LoggerFacade } from '../logging/logger'; + +describe('addNotificationListener', () => { + let logger: LoggerFacade; + let notificationCenterInstance: DefaultNotificationCenter; + + beforeEach(() => { + logger = getMockLogger(); + notificationCenterInstance = createNotificationCenter({ logger }); + }); + + it('should return -1 if notification type is not a valid type', () => { + const INVALID_LISTENER_TYPE = 'INVALID_LISTENER_TYPE' as const; + const mockFn = vi.fn(); + + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + const listenerId = notificationCenterInstance.addNotificationListener(INVALID_LISTENER_TYPE, mockFn); + + expect(listenerId).toBe(-1); + }); + + it('should return an id (listernId) > 0 of the notification listener if callback is not already added', () => { + const activateCallback = vi.fn(); + const decisionCallback = vi.fn(); + const logEventCallback = vi.fn(); + const configUpdateCallback = vi.fn(); + const trackCallback = vi.fn(); + // store a listenerId for each type + const activateListenerId = notificationCenterInstance.addNotificationListener( + NOTIFICATION_TYPES.ACTIVATE, + activateCallback + ); + const decisionListenerId = notificationCenterInstance.addNotificationListener( + NOTIFICATION_TYPES.DECISION, + decisionCallback + ); + const logEventListenerId = notificationCenterInstance.addNotificationListener( + NOTIFICATION_TYPES.LOG_EVENT, + logEventCallback + ); + const configUpdateListenerId = notificationCenterInstance.addNotificationListener( + NOTIFICATION_TYPES.OPTIMIZELY_CONFIG_UPDATE, + configUpdateCallback + ); + const trackListenerId = notificationCenterInstance.addNotificationListener(NOTIFICATION_TYPES.TRACK, trackCallback); + + expect(activateListenerId).toBeGreaterThan(0); + expect(decisionListenerId).toBeGreaterThan(0); + expect(logEventListenerId).toBeGreaterThan(0); + expect(configUpdateListenerId).toBeGreaterThan(0); + expect(trackListenerId).toBeGreaterThan(0); + }); +}); + +describe('removeNotificationListener', () => { + let logger: LoggerFacade; + let notificationCenterInstance: DefaultNotificationCenter; + + beforeEach(() => { + logger = getMockLogger(); + notificationCenterInstance = createNotificationCenter({ logger }); + }); + + it('should return false if listernId does not exist', () => { + const notListenerId = notificationCenterInstance.removeNotificationListener(5); + + expect(notListenerId).toBe(false); + }); + + it('should return true when eixsting listener is removed', () => { + const activateCallback = vi.fn(); + const decisionCallback = vi.fn(); + const logEventCallback = vi.fn(); + const configUpdateCallback = vi.fn(); + const trackCallback = vi.fn(); + // add listeners for each type + const activateListenerId = notificationCenterInstance.addNotificationListener( + NOTIFICATION_TYPES.ACTIVATE, + activateCallback + ); + const decisionListenerId = notificationCenterInstance.addNotificationListener( + NOTIFICATION_TYPES.DECISION, + decisionCallback + ); + const logEventListenerId = notificationCenterInstance.addNotificationListener( + NOTIFICATION_TYPES.LOG_EVENT, + logEventCallback + ); + const configListenerId = notificationCenterInstance.addNotificationListener( + NOTIFICATION_TYPES.OPTIMIZELY_CONFIG_UPDATE, + configUpdateCallback + ); + const trackListenerId = notificationCenterInstance.addNotificationListener(NOTIFICATION_TYPES.TRACK, trackCallback); + // remove listeners for each type + const activateListenerRemoved = notificationCenterInstance.removeNotificationListener(activateListenerId); + const decisionListenerRemoved = notificationCenterInstance.removeNotificationListener(decisionListenerId); + const logEventListenerRemoved = notificationCenterInstance.removeNotificationListener(logEventListenerId); + const trackListenerRemoved = notificationCenterInstance.removeNotificationListener(trackListenerId); + const configListenerRemoved = notificationCenterInstance.removeNotificationListener(configListenerId); + + expect(activateListenerRemoved).toBe(true); + expect(decisionListenerRemoved).toBe(true); + expect(logEventListenerRemoved).toBe(true); + expect(trackListenerRemoved).toBe(true); + expect(configListenerRemoved).toBe(true); + }); + it('should only remove the specified listener', () => { + const activateCallbackSpy1 = vi.fn(); + const activateCallbackSpy2 = vi.fn(); + const decisionCallbackSpy1 = vi.fn(); + const decisionCallbackSpy2 = vi.fn(); + const logEventCallbackSpy1 = vi.fn(); + const logEventCallbackSpy2 = vi.fn(); + const configUpdateCallbackSpy1 = vi.fn(); + const configUpdateCallbackSpy2 = vi.fn(); + const trackCallbackSpy1 = vi.fn(); + const trackCallbackSpy2 = vi.fn(); + // register listeners for each type + const activateListenerId1 = notificationCenterInstance.addNotificationListener( + NOTIFICATION_TYPES.ACTIVATE, + activateCallbackSpy1 + ); + const decisionListenerId1 = notificationCenterInstance.addNotificationListener( + NOTIFICATION_TYPES.DECISION, + decisionCallbackSpy1 + ); + const logeventlistenerId1 = notificationCenterInstance.addNotificationListener( + NOTIFICATION_TYPES.LOG_EVENT, + logEventCallbackSpy1 + ); + const configUpdateListenerId1 = notificationCenterInstance.addNotificationListener( + NOTIFICATION_TYPES.OPTIMIZELY_CONFIG_UPDATE, + configUpdateCallbackSpy1 + ); + const trackListenerId1 = notificationCenterInstance.addNotificationListener( + NOTIFICATION_TYPES.TRACK, + trackCallbackSpy1 + ); + // register second listeners for each type + notificationCenterInstance.addNotificationListener(NOTIFICATION_TYPES.ACTIVATE, activateCallbackSpy2); + notificationCenterInstance.addNotificationListener(NOTIFICATION_TYPES.DECISION, decisionCallbackSpy2); + notificationCenterInstance.addNotificationListener(NOTIFICATION_TYPES.LOG_EVENT, logEventCallbackSpy2); + notificationCenterInstance.addNotificationListener( + NOTIFICATION_TYPES.OPTIMIZELY_CONFIG_UPDATE, + configUpdateCallbackSpy2 + ); + notificationCenterInstance.addNotificationListener(NOTIFICATION_TYPES.TRACK, trackCallbackSpy2); + // remove first listener + const activateListenerRemoved1 = notificationCenterInstance.removeNotificationListener(activateListenerId1); + const decisionListenerRemoved1 = notificationCenterInstance.removeNotificationListener(decisionListenerId1); + const logEventListenerRemoved1 = notificationCenterInstance.removeNotificationListener(logeventlistenerId1); + const configUpdateListenerRemoved1 = notificationCenterInstance.removeNotificationListener(configUpdateListenerId1); + const trackListenerRemoved1 = notificationCenterInstance.removeNotificationListener(trackListenerId1); + // send notifications + notificationCenterInstance.sendNotifications(NOTIFICATION_TYPES.ACTIVATE, {} as ActivateListenerPayload); + notificationCenterInstance.sendNotifications(NOTIFICATION_TYPES.DECISION, {} as DecisionListenerPayload); + notificationCenterInstance.sendNotifications(NOTIFICATION_TYPES.LOG_EVENT, {} as LogEventListenerPayload); + notificationCenterInstance.sendNotifications( + NOTIFICATION_TYPES.OPTIMIZELY_CONFIG_UPDATE, + ({} as unknown) as OptimizelyConfigUpdateListenerPayload + ); + notificationCenterInstance.sendNotifications(NOTIFICATION_TYPES.TRACK, {} as TrackListenerPayload); + + expect(activateListenerRemoved1).toBe(true); + expect(activateCallbackSpy1).not.toHaveBeenCalled(); + expect(activateCallbackSpy2).toHaveBeenCalledTimes(1); + expect(decisionListenerRemoved1).toBe(true); + expect(decisionCallbackSpy1).not.toHaveBeenCalled(); + expect(decisionCallbackSpy2).toHaveBeenCalledTimes(1); + expect(logEventListenerRemoved1).toBe(true); + expect(logEventCallbackSpy1).not.toHaveBeenCalled(); + expect(logEventCallbackSpy2).toHaveBeenCalledTimes(1); + expect(configUpdateListenerRemoved1).toBe(true); + expect(configUpdateCallbackSpy1).not.toHaveBeenCalled(); + expect(configUpdateCallbackSpy2).toHaveBeenCalledTimes(1); + expect(trackListenerRemoved1).toBe(true); + expect(trackCallbackSpy1).not.toHaveBeenCalled(); + expect(trackCallbackSpy2).toHaveBeenCalledTimes(1); + }); +}); + +describe('clearAllNotificationListeners', () => { + let logger: LoggerFacade; + let notificationCenterInstance: DefaultNotificationCenter; + + beforeEach(() => { + logger = getMockLogger(); + notificationCenterInstance = createNotificationCenter({ logger }); + }); + + it('should remove all notification listeners for all types', () => { + const activateCallbackSpy1 = vi.fn(); + const decisionCallbackSpy1 = vi.fn(); + const logEventCallbackSpy1 = vi.fn(); + const configUpdateCallbackSpy1 = vi.fn(); + const trackCallbackSpy1 = vi.fn(); + // add a listener for each notification type + notificationCenterInstance.addNotificationListener(NOTIFICATION_TYPES.ACTIVATE, activateCallbackSpy1); + notificationCenterInstance.addNotificationListener(NOTIFICATION_TYPES.DECISION, decisionCallbackSpy1); + notificationCenterInstance.addNotificationListener(NOTIFICATION_TYPES.LOG_EVENT, logEventCallbackSpy1); + notificationCenterInstance.addNotificationListener( + NOTIFICATION_TYPES.OPTIMIZELY_CONFIG_UPDATE, + configUpdateCallbackSpy1 + ); + notificationCenterInstance.addNotificationListener(NOTIFICATION_TYPES.TRACK, trackCallbackSpy1); + // remove all listeners + notificationCenterInstance.clearAllNotificationListeners(); + // trigger send notifications + notificationCenterInstance.sendNotifications(NOTIFICATION_TYPES.ACTIVATE, {} as ActivateListenerPayload); + notificationCenterInstance.sendNotifications(NOTIFICATION_TYPES.DECISION, {} as DecisionListenerPayload); + notificationCenterInstance.sendNotifications(NOTIFICATION_TYPES.LOG_EVENT, {} as LogEventListenerPayload); + notificationCenterInstance.sendNotifications( + NOTIFICATION_TYPES.OPTIMIZELY_CONFIG_UPDATE, + ({} as unknown) as OptimizelyConfigUpdateListenerPayload + ); + notificationCenterInstance.sendNotifications(NOTIFICATION_TYPES.TRACK, {} as TrackListenerPayload); + + expect(activateCallbackSpy1).not.toHaveBeenCalled(); + expect(decisionCallbackSpy1).not.toHaveBeenCalled(); + expect(logEventCallbackSpy1).not.toHaveBeenCalled(); + expect(configUpdateCallbackSpy1).not.toHaveBeenCalled(); + expect(trackCallbackSpy1).not.toHaveBeenCalled(); + }); +}); + +describe('clearNotificationListeners', () => { + let logger: LoggerFacade; + let notificationCenterInstance: DefaultNotificationCenter; + + beforeEach(() => { + logger = getMockLogger(); + notificationCenterInstance = createNotificationCenter({ logger }); + }); + + it('should remove all notification listeners for the ACTIVATE type', () => { + const activateCallbackSpy1 = vi.fn(); + const activateCallbackSpy2 = vi.fn(); + //add 2 different listeners for ACTIVATE + notificationCenterInstance.addNotificationListener(NOTIFICATION_TYPES.ACTIVATE, activateCallbackSpy1); + notificationCenterInstance.addNotificationListener(NOTIFICATION_TYPES.ACTIVATE, activateCallbackSpy2); + // remove ACTIVATE listeners + notificationCenterInstance.clearNotificationListeners(NOTIFICATION_TYPES.ACTIVATE); + // trigger send notifications + notificationCenterInstance.sendNotifications(NOTIFICATION_TYPES.ACTIVATE, {} as ActivateListenerPayload); + + expect(activateCallbackSpy1).not.toHaveBeenCalled(); + expect(activateCallbackSpy2).not.toHaveBeenCalled(); + }); + + it('should remove all notification listeners for the DECISION type', () => { + const decisionCallbackSpy1 = vi.fn(); + const decisionCallbackSpy2 = vi.fn(); + //add 2 different listeners for DECISION + notificationCenterInstance.addNotificationListener(NOTIFICATION_TYPES.DECISION, decisionCallbackSpy1); + notificationCenterInstance.addNotificationListener(NOTIFICATION_TYPES.DECISION, decisionCallbackSpy2); + // remove DECISION listeners + notificationCenterInstance.clearNotificationListeners(NOTIFICATION_TYPES.DECISION); + // trigger send notifications + notificationCenterInstance.sendNotifications(NOTIFICATION_TYPES.DECISION, {} as DecisionListenerPayload); + + expect(decisionCallbackSpy1).not.toHaveBeenCalled(); + expect(decisionCallbackSpy2).not.toHaveBeenCalled(); + }); + + it('should remove all notification listeners for the LOG_EVENT type', () => { + const logEventCallbackSpy1 = vi.fn(); + const logEventCallbackSpy2 = vi.fn(); + //add 2 different listeners for LOG_EVENT + notificationCenterInstance.addNotificationListener(NOTIFICATION_TYPES.LOG_EVENT, logEventCallbackSpy1); + notificationCenterInstance.addNotificationListener(NOTIFICATION_TYPES.LOG_EVENT, logEventCallbackSpy2); + // remove LOG_EVENT listeners + notificationCenterInstance.clearNotificationListeners(NOTIFICATION_TYPES.LOG_EVENT); + // trigger send notifications + notificationCenterInstance.sendNotifications(NOTIFICATION_TYPES.LOG_EVENT, {} as LogEventListenerPayload); + + expect(logEventCallbackSpy1).not.toHaveBeenCalled(); + expect(logEventCallbackSpy2).not.toHaveBeenCalled(); + }); + + it('should remove all notification listeners for the OPTIMIZELY_CONFIG_UPDATE type', () => { + const configUpdateCallbackSpy1 = vi.fn(); + const configUpdateCallbackSpy2 = vi.fn(); + //add 2 different listeners for OPTIMIZELY_CONFIG_UPDATE + notificationCenterInstance.addNotificationListener( + NOTIFICATION_TYPES.OPTIMIZELY_CONFIG_UPDATE, + configUpdateCallbackSpy1 + ); + notificationCenterInstance.addNotificationListener( + NOTIFICATION_TYPES.OPTIMIZELY_CONFIG_UPDATE, + configUpdateCallbackSpy2 + ); + // remove OPTIMIZELY_CONFIG_UPDATE listeners + notificationCenterInstance.clearNotificationListeners(NOTIFICATION_TYPES.OPTIMIZELY_CONFIG_UPDATE); + // trigger send notifications + notificationCenterInstance.sendNotifications( + NOTIFICATION_TYPES.OPTIMIZELY_CONFIG_UPDATE, + ({} as unknown) as OptimizelyConfigUpdateListenerPayload + ); + + expect(configUpdateCallbackSpy1).not.toHaveBeenCalled(); + expect(configUpdateCallbackSpy2).not.toHaveBeenCalled(); + }); + + it('should remove all notification listeners for the TRACK type', () => { + const trackCallbackSpy1 = vi.fn(); + const trackCallbackSpy2 = vi.fn(); + //add 2 different listeners for TRACK + notificationCenterInstance.addNotificationListener(NOTIFICATION_TYPES.TRACK, trackCallbackSpy1); + notificationCenterInstance.addNotificationListener(NOTIFICATION_TYPES.TRACK, trackCallbackSpy2); + // remove TRACK listeners + notificationCenterInstance.clearNotificationListeners(NOTIFICATION_TYPES.TRACK); + // trigger send notifications + notificationCenterInstance.sendNotifications(NOTIFICATION_TYPES.TRACK, {} as TrackListenerPayload); + + expect(trackCallbackSpy1).not.toHaveBeenCalled(); + expect(trackCallbackSpy2).not.toHaveBeenCalled(); + }); + + it('should only remove ACTIVATE type listeners and not any other types', () => { + const activateCallbackSpy1 = vi.fn(); + const activateCallbackSpy2 = vi.fn(); + const decisionCallbackSpy1 = vi.fn(); + const logEventCallbackSpy1 = vi.fn(); + const configUpdateCallbackSpy1 = vi.fn(); + const trackCallbackSpy1 = vi.fn(); + //add 2 different listeners for ACTIVATE + notificationCenterInstance.addNotificationListener(NOTIFICATION_TYPES.ACTIVATE, activateCallbackSpy1); + notificationCenterInstance.addNotificationListener(NOTIFICATION_TYPES.ACTIVATE, activateCallbackSpy2); + // add a listener for each notification type + notificationCenterInstance.addNotificationListener(NOTIFICATION_TYPES.DECISION, decisionCallbackSpy1); + notificationCenterInstance.addNotificationListener(NOTIFICATION_TYPES.LOG_EVENT, logEventCallbackSpy1); + notificationCenterInstance.addNotificationListener( + NOTIFICATION_TYPES.OPTIMIZELY_CONFIG_UPDATE, + configUpdateCallbackSpy1 + ); + notificationCenterInstance.addNotificationListener(NOTIFICATION_TYPES.TRACK, trackCallbackSpy1); + // remove only ACTIVATE type + notificationCenterInstance.clearNotificationListeners(NOTIFICATION_TYPES.ACTIVATE); + // trigger send notifications + notificationCenterInstance.sendNotifications(NOTIFICATION_TYPES.ACTIVATE, {} as ActivateListenerPayload); + notificationCenterInstance.sendNotifications(NOTIFICATION_TYPES.DECISION, {} as DecisionListenerPayload); + notificationCenterInstance.sendNotifications(NOTIFICATION_TYPES.LOG_EVENT, {} as LogEventListenerPayload); + notificationCenterInstance.sendNotifications( + NOTIFICATION_TYPES.OPTIMIZELY_CONFIG_UPDATE, + ({} as unknown) as OptimizelyConfigUpdateListenerPayload + ); + notificationCenterInstance.sendNotifications(NOTIFICATION_TYPES.TRACK, {} as TrackListenerPayload); + + expect(activateCallbackSpy1).not.toHaveBeenCalled(); + expect(activateCallbackSpy2).not.toHaveBeenCalled(); + expect(decisionCallbackSpy1).toHaveBeenCalledTimes(1); + expect(logEventCallbackSpy1).toHaveBeenCalledTimes(1); + expect(configUpdateCallbackSpy1).toHaveBeenCalledTimes(1); + expect(trackCallbackSpy1).toHaveBeenCalledTimes(1); + }); + + it('should only remove DECISION type listeners and not any other types', () => { + const decisionCallbackSpy1 = vi.fn(); + const decisionCallbackSpy2 = vi.fn(); + const activateCallbackSpy1 = vi.fn(); + const logEventCallbackSpy1 = vi.fn(); + const configUpdateCallbackSpy1 = vi.fn(); + const trackCallbackSpy1 = vi.fn(); + // add 2 different listeners for DECISION + notificationCenterInstance.addNotificationListener(NOTIFICATION_TYPES.DECISION, decisionCallbackSpy1); + notificationCenterInstance.addNotificationListener(NOTIFICATION_TYPES.DECISION, decisionCallbackSpy2); + // add a listener for each notification type + notificationCenterInstance.addNotificationListener(NOTIFICATION_TYPES.ACTIVATE, activateCallbackSpy1); + notificationCenterInstance.addNotificationListener(NOTIFICATION_TYPES.LOG_EVENT, logEventCallbackSpy1); + notificationCenterInstance.addNotificationListener( + NOTIFICATION_TYPES.OPTIMIZELY_CONFIG_UPDATE, + configUpdateCallbackSpy1 + ); + notificationCenterInstance.addNotificationListener(NOTIFICATION_TYPES.TRACK, trackCallbackSpy1); + // remove only DECISION type + notificationCenterInstance.clearNotificationListeners(NOTIFICATION_TYPES.DECISION); + // trigger send notifications + notificationCenterInstance.sendNotifications(NOTIFICATION_TYPES.ACTIVATE, {} as ActivateListenerPayload); + notificationCenterInstance.sendNotifications(NOTIFICATION_TYPES.DECISION, {} as DecisionListenerPayload); + notificationCenterInstance.sendNotifications(NOTIFICATION_TYPES.LOG_EVENT, {} as LogEventListenerPayload); + notificationCenterInstance.sendNotifications( + NOTIFICATION_TYPES.OPTIMIZELY_CONFIG_UPDATE, + ({} as unknown) as OptimizelyConfigUpdateListenerPayload + ); + notificationCenterInstance.sendNotifications(NOTIFICATION_TYPES.TRACK, {} as TrackListenerPayload); + + expect(decisionCallbackSpy1).not.toHaveBeenCalled(); + expect(decisionCallbackSpy2).not.toHaveBeenCalled(); + expect(activateCallbackSpy1).toHaveBeenCalledTimes(1); + expect(logEventCallbackSpy1).toHaveBeenCalledTimes(1); + expect(configUpdateCallbackSpy1).toHaveBeenCalledTimes(1); + expect(trackCallbackSpy1).toHaveBeenCalledTimes(1); + }); + + it('should only remove LOG_EVENT type listeners and not any other types', () => { + const logEventCallbackSpy1 = vi.fn(); + const logEventCallbackSpy2 = vi.fn(); + const activateCallbackSpy1 = vi.fn(); + const decisionCallbackSpy1 = vi.fn(); + const configUpdateCallbackSpy1 = vi.fn(); + const trackCallbackSpy1 = vi.fn(); + // add 2 different listeners for LOG_EVENT + notificationCenterInstance.addNotificationListener(NOTIFICATION_TYPES.LOG_EVENT, logEventCallbackSpy1); + notificationCenterInstance.addNotificationListener(NOTIFICATION_TYPES.LOG_EVENT, logEventCallbackSpy2); + // add a listener for each notification type + notificationCenterInstance.addNotificationListener(NOTIFICATION_TYPES.ACTIVATE, activateCallbackSpy1); + notificationCenterInstance.addNotificationListener(NOTIFICATION_TYPES.DECISION, decisionCallbackSpy1); + notificationCenterInstance.addNotificationListener( + NOTIFICATION_TYPES.OPTIMIZELY_CONFIG_UPDATE, + configUpdateCallbackSpy1 + ); + notificationCenterInstance.addNotificationListener(NOTIFICATION_TYPES.TRACK, trackCallbackSpy1); + // remove only LOG_EVENT type + notificationCenterInstance.clearNotificationListeners(NOTIFICATION_TYPES.LOG_EVENT); + // trigger send notifications + notificationCenterInstance.sendNotifications(NOTIFICATION_TYPES.ACTIVATE, {} as ActivateListenerPayload); + notificationCenterInstance.sendNotifications(NOTIFICATION_TYPES.DECISION, {} as DecisionListenerPayload); + notificationCenterInstance.sendNotifications(NOTIFICATION_TYPES.LOG_EVENT, {} as LogEventListenerPayload); + notificationCenterInstance.sendNotifications( + NOTIFICATION_TYPES.OPTIMIZELY_CONFIG_UPDATE, + ({} as unknown) as OptimizelyConfigUpdateListenerPayload + ); + notificationCenterInstance.sendNotifications(NOTIFICATION_TYPES.TRACK, {} as TrackListenerPayload); + + expect(logEventCallbackSpy1).not.toHaveBeenCalled(); + expect(logEventCallbackSpy2).not.toHaveBeenCalled(); + expect(activateCallbackSpy1).toHaveBeenCalledTimes(1); + expect(decisionCallbackSpy1).toHaveBeenCalledTimes(1); + expect(configUpdateCallbackSpy1).toHaveBeenCalledTimes(1); + expect(trackCallbackSpy1).toHaveBeenCalledTimes(1); + }); + + it('should only remove OPTIMIZELY_CONFIG_UPDATE type listeners and not any other types', () => { + const configUpdateCallbackSpy1 = vi.fn(); + const configUpdateCallbackSpy2 = vi.fn(); + const activateCallbackSpy1 = vi.fn(); + const decisionCallbackSpy1 = vi.fn(); + const logEventCallbackSpy1 = vi.fn(); + const trackCallbackSpy1 = vi.fn(); + // add 2 different listeners for OPTIMIZELY_CONFIG_UPDATE + notificationCenterInstance.addNotificationListener( + NOTIFICATION_TYPES.OPTIMIZELY_CONFIG_UPDATE, + configUpdateCallbackSpy1 + ); + notificationCenterInstance.addNotificationListener( + NOTIFICATION_TYPES.OPTIMIZELY_CONFIG_UPDATE, + configUpdateCallbackSpy2 + ); + // add a listener for each notification type + notificationCenterInstance.addNotificationListener(NOTIFICATION_TYPES.ACTIVATE, activateCallbackSpy1); + notificationCenterInstance.addNotificationListener(NOTIFICATION_TYPES.DECISION, decisionCallbackSpy1); + notificationCenterInstance.addNotificationListener(NOTIFICATION_TYPES.LOG_EVENT, logEventCallbackSpy1); + notificationCenterInstance.addNotificationListener(NOTIFICATION_TYPES.TRACK, trackCallbackSpy1); + // remove only OPTIMIZELY_CONFIG_UPDATE type + notificationCenterInstance.clearNotificationListeners(NOTIFICATION_TYPES.OPTIMIZELY_CONFIG_UPDATE); + // trigger send notifications + notificationCenterInstance.sendNotifications(NOTIFICATION_TYPES.ACTIVATE, {} as ActivateListenerPayload); + notificationCenterInstance.sendNotifications(NOTIFICATION_TYPES.DECISION, {} as DecisionListenerPayload); + notificationCenterInstance.sendNotifications(NOTIFICATION_TYPES.LOG_EVENT, {} as LogEventListenerPayload); + notificationCenterInstance.sendNotifications( + NOTIFICATION_TYPES.OPTIMIZELY_CONFIG_UPDATE, + ({} as unknown) as OptimizelyConfigUpdateListenerPayload + ); + notificationCenterInstance.sendNotifications(NOTIFICATION_TYPES.TRACK, {} as TrackListenerPayload); + + expect(configUpdateCallbackSpy1).not.toHaveBeenCalled(); + expect(configUpdateCallbackSpy2).not.toHaveBeenCalled(); + expect(activateCallbackSpy1).toHaveBeenCalledTimes(1); + expect(decisionCallbackSpy1).toHaveBeenCalledTimes(1); + expect(logEventCallbackSpy1).toHaveBeenCalledTimes(1); + expect(trackCallbackSpy1).toHaveBeenCalledTimes(1); + }); + + it('should only remove TRACK type listeners and not any other types', () => { + const trackCallbackSpy1 = vi.fn(); + const trackCallbackSpy2 = vi.fn(); + const activateCallbackSpy1 = vi.fn(); + const decisionCallbackSpy1 = vi.fn(); + const logEventCallbackSpy1 = vi.fn(); + const configUpdateCallbackSpy1 = vi.fn(); + // add 2 different listeners for TRACK + notificationCenterInstance.addNotificationListener(NOTIFICATION_TYPES.TRACK, trackCallbackSpy1); + notificationCenterInstance.addNotificationListener(NOTIFICATION_TYPES.TRACK, trackCallbackSpy2); + // add a listener for each notification type + notificationCenterInstance.addNotificationListener(NOTIFICATION_TYPES.ACTIVATE, activateCallbackSpy1); + notificationCenterInstance.addNotificationListener(NOTIFICATION_TYPES.DECISION, decisionCallbackSpy1); + notificationCenterInstance.addNotificationListener(NOTIFICATION_TYPES.LOG_EVENT, logEventCallbackSpy1); + notificationCenterInstance.addNotificationListener( + NOTIFICATION_TYPES.OPTIMIZELY_CONFIG_UPDATE, + configUpdateCallbackSpy1 + ); + // remove only TRACK type + notificationCenterInstance.clearNotificationListeners(NOTIFICATION_TYPES.TRACK); + // trigger send notifications + notificationCenterInstance.sendNotifications(NOTIFICATION_TYPES.ACTIVATE, {} as ActivateListenerPayload); + notificationCenterInstance.sendNotifications(NOTIFICATION_TYPES.DECISION, {} as DecisionListenerPayload); + notificationCenterInstance.sendNotifications(NOTIFICATION_TYPES.LOG_EVENT, {} as LogEventListenerPayload); + notificationCenterInstance.sendNotifications( + NOTIFICATION_TYPES.OPTIMIZELY_CONFIG_UPDATE, + ({} as unknown) as OptimizelyConfigUpdateListenerPayload + ); + notificationCenterInstance.sendNotifications(NOTIFICATION_TYPES.TRACK, {} as TrackListenerPayload); + + expect(trackCallbackSpy1).not.toHaveBeenCalled(); + expect(trackCallbackSpy2).not.toHaveBeenCalled(); + expect(activateCallbackSpy1).toHaveBeenCalledTimes(1); + expect(decisionCallbackSpy1).toHaveBeenCalledTimes(1); + expect(logEventCallbackSpy1).toHaveBeenCalledTimes(1); + expect(configUpdateCallbackSpy1).toHaveBeenCalledTimes(1); + }); +}); + +describe('sendNotifications', () => { + let logger: LoggerFacade; + let notificationCenterInstance: DefaultNotificationCenter; + + beforeEach(() => { + logger = getMockLogger(); + notificationCenterInstance = createNotificationCenter({ logger }); + }); + it('should call the listener callback with exact arguments', () => { + const activateCallbackSpy1 = vi.fn(); + const decisionCallbackSpy1 = vi.fn(); + const logEventCallbackSpy1 = vi.fn(); + const configUpdateCallbackSpy1 = vi.fn(); + const trackCallbackSpy1 = vi.fn(); + // listener object data for each type + const activateData = { + experiment: {}, + userId: '', + attributes: {}, + variation: {}, + logEvent: {}, + }; + const decisionData = { + type: '', + userId: 'use1', + attributes: {}, + decisionInfo: {}, + }; + const logEventData = { + url: '', + httpVerb: '', + params: {}, + }; + const configUpdateData = {}; + const trackData = { + eventKey: '', + userId: '', + attributes: {}, + eventTags: {}, + }; + // add listeners + notificationCenterInstance.addNotificationListener(NOTIFICATION_TYPES.ACTIVATE, activateCallbackSpy1); + notificationCenterInstance.addNotificationListener(NOTIFICATION_TYPES.DECISION, decisionCallbackSpy1); + notificationCenterInstance.addNotificationListener(NOTIFICATION_TYPES.LOG_EVENT, logEventCallbackSpy1); + notificationCenterInstance.addNotificationListener( + NOTIFICATION_TYPES.OPTIMIZELY_CONFIG_UPDATE, + configUpdateCallbackSpy1 + ); + notificationCenterInstance.addNotificationListener(NOTIFICATION_TYPES.TRACK, trackCallbackSpy1); + // send notifications + notificationCenterInstance.sendNotifications(NOTIFICATION_TYPES.ACTIVATE, activateData as ActivateListenerPayload); + notificationCenterInstance.sendNotifications(NOTIFICATION_TYPES.DECISION, decisionData as DecisionListenerPayload); + notificationCenterInstance.sendNotifications(NOTIFICATION_TYPES.LOG_EVENT, logEventData as LogEventListenerPayload); + notificationCenterInstance.sendNotifications( + NOTIFICATION_TYPES.OPTIMIZELY_CONFIG_UPDATE, + (configUpdateData as unknown) as OptimizelyConfigUpdateListenerPayload + ); + notificationCenterInstance.sendNotifications(NOTIFICATION_TYPES.TRACK, trackData as TrackListenerPayload); + + expect(activateCallbackSpy1).toHaveBeenCalledWith(activateData); + expect(decisionCallbackSpy1).toHaveBeenCalledWith(decisionData); + expect(logEventCallbackSpy1).toHaveBeenCalledWith(logEventData); + expect(configUpdateCallbackSpy1).toHaveBeenCalledWith(configUpdateData); + expect(trackCallbackSpy1).toHaveBeenCalledWith(trackData); + }); +}); diff --git a/lib/project_config/project_config.spec.ts b/lib/project_config/project_config.spec.ts index bb5370ef4..36ffbe89a 100644 --- a/lib/project_config/project_config.spec.ts +++ b/lib/project_config/project_config.spec.ts @@ -322,9 +322,6 @@ describe('getLayerId', () => { }); it('should throw error for invalid experiment key in getLayerId', function() { - // expect(() => projectConfig.getLayerId(configObj, 'invalidExperimentKey')).toThrowError( - // sprintf(INVALID_EXPERIMENT_ID, 'PROJECT_CONFIG', 'invalidExperimentKey') - // ); expect(() => projectConfig.getLayerId(configObj, 'invalidExperimentKey')).toThrowError( expect.objectContaining({ baseMessage: INVALID_EXPERIMENT_ID, diff --git a/lib/utils/attributes_validator/index.spec.ts b/lib/utils/attributes_validator/index.spec.ts new file mode 100644 index 000000000..645fa2113 --- /dev/null +++ b/lib/utils/attributes_validator/index.spec.ts @@ -0,0 +1,115 @@ +/** + * 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 * as attributesValidator from './'; +import { INVALID_ATTRIBUTES, UNDEFINED_ATTRIBUTE } from 'error_message'; +import { OptimizelyError } from '../../error/optimizly_error'; + +describe('validate', () => { + it('should validate the given attributes if attributes is an object', () => { + expect(attributesValidator.validate({ testAttribute: 'testValue' })).toBe(true); + }); + + it('should throw an error if attributes is an array', () => { + const attributesArray = ['notGonnaWork']; + + expect(() => attributesValidator.validate(attributesArray)).toThrow(OptimizelyError); + + try { + attributesValidator.validate(attributesArray); + } catch (err) { + expect(err).toBeInstanceOf(OptimizelyError); + expect(err.baseMessage).toBe(INVALID_ATTRIBUTES); + } + }); + + it('should throw an error if attributes is null', () => { + expect(() => attributesValidator.validate(null)).toThrowError(OptimizelyError); + + try { + attributesValidator.validate(null); + } catch (err) { + expect(err).toBeInstanceOf(OptimizelyError); + expect(err.baseMessage).toBe(INVALID_ATTRIBUTES); + } + }); + + it('should throw an error if attributes is a function', () => { + function invalidInput() { + console.log('This is an invalid input!'); + } + + expect(() => attributesValidator.validate(invalidInput)).toThrowError(OptimizelyError); + + try { + attributesValidator.validate(invalidInput); + } catch(err) { + expect(err).toBeInstanceOf(OptimizelyError); + expect(err.baseMessage).toBe(INVALID_ATTRIBUTES); + } + }); + + it('should throw an error if attributes contains a key with an undefined value', () => { + const attributeKey = 'testAttribute'; + const attributes: Record = {}; + attributes[attributeKey] = undefined; + + expect(() => attributesValidator.validate(attributes)).toThrowError(OptimizelyError); + + try { + attributesValidator.validate(attributes); + } catch(err) { + expect(err).toBeInstanceOf(OptimizelyError); + expect(err.baseMessage).toBe(UNDEFINED_ATTRIBUTE); + expect(err.params).toEqual([attributeKey]); + } + }); +}); + +describe('isAttributeValid', () => { + it('isAttributeValid returns true for valid values', () => { + const userAttributes: Record = { + browser_type: 'Chrome', + is_firefox: false, + num_users: 10, + pi_value: 3.14, + '': 'javascript', + }; + + Object.keys(userAttributes).forEach(key => { + const value = userAttributes[key]; + + expect(attributesValidator.isAttributeValid(key, value)).toBe(true); + }); + }); + it('isAttributeValid returns false for invalid values', () => { + const userAttributes: Record = { + null: null, + objects: { a: 'b' }, + array: [1, 2, 3], + infinity: Infinity, + negativeInfinity: -Infinity, + NaN: NaN, + }; + + Object.keys(userAttributes).forEach(key => { + const value = userAttributes[key]; + + expect(attributesValidator.isAttributeValid(key, value)).toBe(false); + }); + }); +}); diff --git a/lib/utils/config_validator/index.spec.ts b/lib/utils/config_validator/index.spec.ts new file mode 100644 index 000000000..c8496ecc4 --- /dev/null +++ b/lib/utils/config_validator/index.spec.ts @@ -0,0 +1,65 @@ +/** + * 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/event_tag_utils/index.spec.ts b/lib/utils/event_tag_utils/index.spec.ts new file mode 100644 index 000000000..a1208b601 --- /dev/null +++ b/lib/utils/event_tag_utils/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 + * + * 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, beforeEach } from 'vitest'; +import * as eventTagUtils from './'; +import { + FAILED_TO_PARSE_REVENUE, + PARSED_REVENUE_VALUE, + PARSED_NUMERIC_VALUE, + FAILED_TO_PARSE_VALUE, +} from 'log_message'; +import { getMockLogger } from '../../tests/mock/mock_logger'; +import { LoggerFacade } from '../../logging/logger'; + +describe('getRevenueValue', () => { + let logger: LoggerFacade; + + beforeEach(() => { + logger = getMockLogger(); + }); + + it('should return the parseed integer for a valid revenue value', () => { + let parsedRevenueValue = eventTagUtils.getRevenueValue({ revenue: '1337' }, logger); + + expect(parsedRevenueValue).toBe(1337); + expect(logger.info).toHaveBeenCalledWith(PARSED_REVENUE_VALUE, 1337); + + parsedRevenueValue = eventTagUtils.getRevenueValue({ revenue: '13.37' }, logger); + + expect(parsedRevenueValue).toBe(13); + }); + + it('should return null and log a message for invalid value', () => { + const parsedRevenueValue = eventTagUtils.getRevenueValue({ revenue: 'invalid' }, logger); + + expect(parsedRevenueValue).toBe(null); + expect(logger.info).toHaveBeenCalledWith(FAILED_TO_PARSE_REVENUE, 'invalid'); + }); + + it('should return null if the revenue value is not present in the event tags', () => { + const parsedRevenueValue = eventTagUtils.getRevenueValue({ not_revenue: '1337' }, logger); + + expect(parsedRevenueValue).toBe(null); + }); +}); + +describe('getEventValue', () => { + let logger: LoggerFacade; + + beforeEach(() => { + logger = getMockLogger(); + }); + + it('should return the parsed integer for a valid numeric value', () => { + let parsedEventValue = eventTagUtils.getEventValue({ value: '1337' }, logger); + + expect(parsedEventValue).toBe(1337); + expect(logger.info).toHaveBeenCalledWith(PARSED_NUMERIC_VALUE, 1337); + + parsedEventValue = eventTagUtils.getEventValue({ value: '13.37' }, logger); + expect(parsedEventValue).toBe(13.37); + }); + + it('should return null and log a message for invalid value', () => { + const parsedNumericValue = eventTagUtils.getEventValue({ value: 'invalid' }, logger); + + expect(parsedNumericValue).toBe(null); + expect(logger.info).toHaveBeenCalledWith(FAILED_TO_PARSE_VALUE, 'invalid'); + }); + + it('should return null if the value is not present in the event tags', () => { + const parsedNumericValue = eventTagUtils.getEventValue({ not_value: '13.37' }, logger); + + expect(parsedNumericValue).toBe(null); + }); +}) diff --git a/lib/utils/event_tags_validator/index.spec.ts b/lib/utils/event_tags_validator/index.spec.ts new file mode 100644 index 000000000..1b372ff0a --- /dev/null +++ b/lib/utils/event_tags_validator/index.spec.ts @@ -0,0 +1,63 @@ +/** + * 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, beforeEach } from 'vitest'; +import { validate } from '.'; +import { OptimizelyError } from '../../error/optimizly_error'; +import { INVALID_EVENT_TAGS } from 'error_message'; + +describe('validate', () => { + it('should validate the given event tags if event tag is an object', () => { + expect(validate({ testAttribute: 'testValue' })).toBe(true); + }); + + it('should throw an error if event tags is an array', () => { + const eventTagsArray = ['notGonnaWork']; + + expect(() => validate(eventTagsArray)).toThrow(OptimizelyError) + + try { + validate(eventTagsArray); + } catch(err) { + expect(err).toBeInstanceOf(OptimizelyError); + expect(err.baseMessage).toBe(INVALID_EVENT_TAGS); + } + }); + + it('should throw an error if event tags is null', () => { + expect(() => validate(null)).toThrow(OptimizelyError); + + try { + validate(null); + } catch(err) { + expect(err).toBeInstanceOf(OptimizelyError); + expect(err.baseMessage).toBe(INVALID_EVENT_TAGS); + } + }); + + it('should throw an error if event tags is a function', () => { + function invalidInput() { + console.log('This is an invalid input!'); + } + expect(() => validate(invalidInput)).toThrow(OptimizelyError); + + try { + validate(invalidInput); + } catch(err) { + expect(err).toBeInstanceOf(OptimizelyError); + expect(err.baseMessage).toBe(INVALID_EVENT_TAGS); + } + }); +}); diff --git a/lib/utils/json_schema_validator/index.spec.ts b/lib/utils/json_schema_validator/index.spec.ts new file mode 100644 index 000000000..20af5b51d --- /dev/null +++ b/lib/utils/json_schema_validator/index.spec.ts @@ -0,0 +1,49 @@ +/** + * 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 { validate } from '.'; +import testData from '../../tests/test_data'; +import { NO_JSON_PROVIDED, INVALID_DATAFILE } from 'error_message'; + +describe('validate', () => { + it('should throw an error if the object is not valid', () => { + expect(() => validate({})).toThrow(); + + try { + validate({}); + } catch (err) { + expect(err.baseMessage).toBe(INVALID_DATAFILE); + } + }); + + it('should throw an error if no json object is passed in', () => { + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + expect(() => validate()).toThrow(); + + try { + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + validate(); + } catch (err) { + expect(err.baseMessage).toBe(NO_JSON_PROVIDED); + } + }); + + it('should validate specified Optimizely datafile', () => { + expect(validate(testData.getTestProjectConfig())).toBe(true); + }); +}); diff --git a/lib/utils/semantic_version/index.spec.ts b/lib/utils/semantic_version/index.spec.ts new file mode 100644 index 000000000..15dbbdbb9 --- /dev/null +++ b/lib/utils/semantic_version/index.spec.ts @@ -0,0 +1,112 @@ +/** + * 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 * as semanticVersion from '.'; + +describe('compareVersion', () => { + it('should return 0 if user version and target version are equal', () => { + const versions = [ + ['2.0.1', '2.0.1'], + ['2.9.9-beta', '2.9.9-beta'], + ['2.1', '2.1.0'], + ['2', '2.12'], + ['2.9', '2.9.1'], + ['2.9+beta', '2.9+beta'], + ['2.9.9+beta', '2.9.9+beta'], + ['2.9.9+beta-alpha', '2.9.9+beta-alpha'], + ['2.2.3', '2.2.3+beta'], + ]; + + versions.forEach(([targetVersion, userVersion]) => { + const result = semanticVersion.compareVersion(targetVersion, userVersion); + + expect(result).toBe(0); + }) + }); + + it('should return 1 when user version is greater than target version', () => { + const versions = [ + ['2.0.0', '2.0.1'], + ['2.0', '3.0.1'], + ['2.0.0', '2.1'], + ['2.1.2-beta', '2.1.2-release'], + ['2.1.3-beta1', '2.1.3-beta2'], + ['2.9.9-beta', '2.9.9'], + ['2.9.9+beta', '2.9.9'], + ['2.0.0', '2.1'], + ['3.7.0-prerelease+build', '3.7.0-prerelease+rc'], + ['2.2.3-beta-beta1', '2.2.3-beta-beta2'], + ['2.2.3-beta+beta1', '2.2.3-beta+beta2'], + ['2.2.3+beta2-beta1', '2.2.3+beta3-beta2'], + ['2.2.3+beta', '2.2.3'], + ]; + + versions.forEach(([targetVersion, userVersion]) => { + const result = semanticVersion.compareVersion(targetVersion, userVersion); + + expect(result).toBe(1); + }) + }); + + it('should return -1 when user version is less than target version', () => { + const versions = [ + ['2.0.1', '2.0.0'], + ['3.0', '2.0.1'], + ['2.3', '2.0.1'], + ['2.3.5', '2.3.1'], + ['2.9.8', '2.9'], + ['3.1', '3'], + ['2.1.2-release', '2.1.2-beta'], + ['2.9.9+beta', '2.9.9-beta'], + ['3.7.0+build3.7.0-prerelease+build', '3.7.0-prerelease'], + ['2.1.3-beta-beta2', '2.1.3-beta'], + ['2.1.3-beta1+beta3', '2.1.3-beta1+beta2'], + ['2.1.3', '2.1.3-beta'], + ]; + + versions.forEach(([targetVersion, userVersion]) => { + const result = semanticVersion.compareVersion(targetVersion, userVersion); + + expect(result).toBe(-1); + }) + }); + + it('should return null when user version is invalid', () => { + const versions = [ + '-', + '.', + '..', + '+', + '+test', + ' ', + '2 .3. 0', + '2.', + '.2.2', + '3.7.2.2', + '3.x', + ',', + '+build-prerelease', + '2..2', + ]; + const targetVersion = '2.1.0'; + + versions.forEach((userVersion) => { + const result = semanticVersion.compareVersion(targetVersion, userVersion); + + expect(result).toBe(null); + }) + }); +}); diff --git a/lib/utils/string_value_validator/index.spec.ts b/lib/utils/string_value_validator/index.spec.ts new file mode 100644 index 000000000..a9c7f6a91 --- /dev/null +++ b/lib/utils/string_value_validator/index.spec.ts @@ -0,0 +1,32 @@ +/** + * 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 { validate } from './'; + +describe('validate', () => { + it('should validate the given value is valid string', () => { + expect(validate('validStringValue')).toBe(true); + }); + + it('should return false if given value is invalid string', () => { + expect(validate(null)).toBe(false); + expect(validate(undefined)).toBe(false); + expect(validate('')).toBe(false); + expect(validate(5)).toBe(false); + expect(validate(true)).toBe(false); + expect(validate([])).toBe(false); + }); +}); diff --git a/lib/utils/user_profile_service_validator/index.spec.ts b/lib/utils/user_profile_service_validator/index.spec.ts new file mode 100644 index 000000000..98a47ef60 --- /dev/null +++ b/lib/utils/user_profile_service_validator/index.spec.ts @@ -0,0 +1,96 @@ +/** + * 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 { validate } from './'; +import { INVALID_USER_PROFILE_SERVICE } from 'error_message'; +import { OptimizelyError } from '../../error/optimizly_error'; + +describe('validate', () => { + it("should throw if the instance does not provide a 'lookup' function", () => { + const missingLookupFunction = { + save: function() {}, + }; + + expect(() => validate(missingLookupFunction)).toThrowError(OptimizelyError); + + try { + validate(missingLookupFunction); + } catch (err) { + expect(err).instanceOf(OptimizelyError); + expect(err.baseMessage).toBe(INVALID_USER_PROFILE_SERVICE); + expect(err.params).toEqual(["Missing function 'lookup'"]); + } + }); + + it("should throw if 'lookup' is not a function", () => { + const lookupNotFunction = { + save: function() {}, + lookup: 'notGonnaWork', + }; + + expect(() => validate(lookupNotFunction)).toThrowError(OptimizelyError); + + try { + validate(lookupNotFunction); + } catch (err) { + expect(err).instanceOf(OptimizelyError); + expect(err.baseMessage).toBe(INVALID_USER_PROFILE_SERVICE); + expect(err.params).toEqual(["Missing function 'lookup'"]); + } + }); + + it("should throw if the instance does not provide a 'save' function", () => { + const missingSaveFunction = { + lookup: function() {}, + }; + + expect(() => validate(missingSaveFunction)).toThrowError(OptimizelyError); + + try { + validate(missingSaveFunction); + } catch (err) { + expect(err).instanceOf(OptimizelyError); + expect(err.baseMessage).toBe(INVALID_USER_PROFILE_SERVICE); + expect(err.params).toEqual(["Missing function 'save'"]); + } + }); + + it("should throw if 'save' is not a function", () => { + const saveNotFunction = { + lookup: function() {}, + save: 'notGonnaWork', + }; + + expect(() => validate(saveNotFunction)).toThrowError(OptimizelyError); + + try { + validate(saveNotFunction); + } catch (err) { + expect(err).instanceOf(OptimizelyError); + expect(err.baseMessage).toBe(INVALID_USER_PROFILE_SERVICE); + expect(err.params).toEqual(["Missing function 'save'"]); + } + }); + + it('should return true if the instance is valid', () => { + const validInstance = { + save: function() {}, + lookup: function() {}, + }; + + expect(validate(validInstance)).toBe(true); + }); +});