From 646f8658082fccc963e4c575f79e95ab51e0364c Mon Sep 17 00:00:00 2001 From: Raju Ahmed Date: Wed, 21 May 2025 20:40:03 +0600 Subject: [PATCH 1/3] [FSSDK-11510] add validation to factories also updated createInstance to throw in case of validation errors --- lib/client_factory.spec.ts | 61 ++++++++++++ lib/client_factory.ts | 99 ++++++++----------- lib/core/decision_service/index.tests.js | 2 +- lib/entrypoint.test-d.ts | 2 +- lib/entrypoint.universal.test-d.ts | 2 +- lib/error/error_notifier_factory.spec.ts | 33 +++++++ lib/error/error_notifier_factory.ts | 18 +++- .../event_dispatcher_factory.ts | 3 + .../event_processor_factory.browser.spec.ts | 16 ++- .../event_processor_factory.browser.ts | 3 +- .../event_processor_factory.node.spec.ts | 12 +-- .../event_processor_factory.node.ts | 2 +- ...ent_processor_factory.react_native.spec.ts | 11 +-- .../event_processor_factory.react_native.ts | 2 +- .../event_processor_factory.spec.ts | 32 ++++++ .../event_processor_factory.ts | 27 ++++- .../event_processor_factory.universal.ts | 2 +- .../forwarding_event_processor.spec.ts | 16 +-- .../forwarding_event_processor.ts | 8 +- lib/index.browser.tests.js | 22 ++--- lib/index.browser.ts | 2 +- lib/index.node.tests.js | 24 +++-- lib/index.node.ts | 2 +- lib/index.react_native.spec.ts | 28 +++--- lib/index.react_native.ts | 2 +- lib/index.universal.ts | 2 +- lib/logging/logger_factory.spec.ts | 41 +++++++- lib/logging/logger_factory.ts | 26 ++++- lib/message/error_message.ts | 3 - lib/odp/odp_manager_factory.spec.ts | 44 ++++++++- lib/odp/odp_manager_factory.ts | 36 ++++++- lib/odp/odp_manager_factory.universal.ts | 2 + lib/optimizely/index.spec.ts | 15 ++- lib/optimizely/index.tests.js | 4 +- lib/optimizely_user_context/index.tests.js | 4 +- lib/project_config/config_manager_factory.ts | 13 ++- .../config_manager_factory.universal.ts | 5 +- lib/utils/config_validator/index.ts | 30 +----- .../request_handler_validator.ts | 28 ++++++ lib/vuid/vuid_manager_factory.ts | 6 +- 40 files changed, 480 insertions(+), 210 deletions(-) create mode 100644 lib/client_factory.spec.ts create mode 100644 lib/error/error_notifier_factory.spec.ts create mode 100644 lib/utils/http_request_handler/request_handler_validator.ts diff --git a/lib/client_factory.spec.ts b/lib/client_factory.spec.ts new file mode 100644 index 000000000..1aa09bda0 --- /dev/null +++ b/lib/client_factory.spec.ts @@ -0,0 +1,61 @@ +/** + * 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, expect } from 'vitest'; + +import { getOptimizelyInstance } from './client_factory'; +import { createStaticProjectConfigManager } from './project_config/config_manager_factory'; +import Optimizely from './optimizely'; + +describe('getOptimizelyInstance', () => { + it('should throw if the projectConfigManager is not a valid ProjectConfigManager', () => { + expect(() => getOptimizelyInstance({ + projectConfigManager: undefined as any, + requestHandler: {} as any, + })).toThrow('Invalid config manager'); + + expect(() => getOptimizelyInstance({ + projectConfigManager: null as any, + requestHandler: {} as any, + })).toThrow('Invalid config manager'); + + expect(() => getOptimizelyInstance({ + projectConfigManager: 'abc' as any, + requestHandler: {} as any, + })).toThrow('Invalid config manager'); + + expect(() => getOptimizelyInstance({ + projectConfigManager: 123 as any, + requestHandler: {} as any, + })).toThrow('Invalid config manager'); + + expect(() => getOptimizelyInstance({ + projectConfigManager: {} as any, + requestHandler: {} as any, + })).toThrow('Invalid config manager'); + }); + + it('should return an instance of Optimizely if a valid projectConfigManager is provided', () => { + const optimizelyInstance = getOptimizelyInstance({ + projectConfigManager: createStaticProjectConfigManager({ + datafile: '{}', + }), + requestHandler: {} as any, + }); + + expect(optimizelyInstance).toBeInstanceOf(Optimizely); + }); +}); diff --git a/lib/client_factory.ts b/lib/client_factory.ts index 42c650fd6..7307075cf 100644 --- a/lib/client_factory.ts +++ b/lib/client_factory.ts @@ -13,11 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ - -import { LoggerFacade } from "./logging/logger"; import { Client, Config } from "./shared_types"; -import { Maybe } from "./utils/type"; -import configValidator from './utils/config_validator'; import { extractLogger } from "./logging/logger_factory"; import { extractErrorNotifier } from "./error/error_notifier_factory"; import { extractConfigManager } from "./project_config/config_manager_factory"; @@ -35,61 +31,50 @@ export type OptimizelyFactoryConfig = Config & { requestHandler: RequestHandler; } -export const getOptimizelyInstance = (config: OptimizelyFactoryConfig): Client | null => { - let logger: Maybe; - - try { - logger = config.logger ? extractLogger(config.logger) : undefined; - - configValidator.validate(config); - - const { - clientEngine, - clientVersion, - jsonSchemaValidator, - userProfileService, - userProfileServiceAsync, - defaultDecideOptions, - disposable, - requestHandler, - } = config; - - const errorNotifier = config.errorNotifier ? extractErrorNotifier(config.errorNotifier) : undefined; - - const projectConfigManager = extractConfigManager(config.projectConfigManager); - const eventProcessor = config.eventProcessor ? extractEventProcessor(config.eventProcessor) : undefined; - const odpManager = config.odpManager ? extractOdpManager(config.odpManager) : undefined; - const vuidManager = config.vuidManager ? extractVuidManager(config.vuidManager) : undefined; +export const getOptimizelyInstance = (config: OptimizelyFactoryConfig): Client => { + const { + clientEngine, + clientVersion, + jsonSchemaValidator, + userProfileService, + userProfileServiceAsync, + defaultDecideOptions, + disposable, + requestHandler, + } = config; + + const projectConfigManager = extractConfigManager(config.projectConfigManager); + const eventProcessor = extractEventProcessor(config.eventProcessor); + const odpManager = extractOdpManager(config.odpManager); + const vuidManager = extractVuidManager(config.vuidManager); + const errorNotifier = extractErrorNotifier(config.errorNotifier); + const logger = extractLogger(config.logger); - const cmabClient = new DefaultCmabClient({ - requestHandler, - }); + const cmabClient = new DefaultCmabClient({ + requestHandler, + }); - const cmabService = new DefaultCmabService({ - cmabClient, - cmabCache: new InMemoryLruCache(DEFAULT_CMAB_CACHE_SIZE, DEFAULT_CMAB_CACHE_TIMEOUT), - }); + const cmabService = new DefaultCmabService({ + cmabClient, + cmabCache: new InMemoryLruCache(DEFAULT_CMAB_CACHE_SIZE, DEFAULT_CMAB_CACHE_TIMEOUT), + }); - const optimizelyOptions = { - cmabService, - clientEngine: clientEngine || JAVASCRIPT_CLIENT_ENGINE, - clientVersion: clientVersion || CLIENT_VERSION, - jsonSchemaValidator, - userProfileService, - userProfileServiceAsync, - defaultDecideOptions, - disposable, - logger, - errorNotifier, - projectConfigManager, - eventProcessor, - odpManager, - vuidManager, - }; + const optimizelyOptions = { + cmabService, + clientEngine: clientEngine || JAVASCRIPT_CLIENT_ENGINE, + clientVersion: clientVersion || CLIENT_VERSION, + jsonSchemaValidator, + userProfileService, + userProfileServiceAsync, + defaultDecideOptions, + disposable, + logger, + errorNotifier, + projectConfigManager, + eventProcessor, + odpManager, + vuidManager, + }; - return new Optimizely(optimizelyOptions); - } catch (e) { - logger?.error(e); - return null; - } + return new Optimizely(optimizelyOptions); } diff --git a/lib/core/decision_service/index.tests.js b/lib/core/decision_service/index.tests.js index 98f9dde70..cdc4dc7c7 100644 --- a/lib/core/decision_service/index.tests.js +++ b/lib/core/decision_service/index.tests.js @@ -25,7 +25,7 @@ import { LOG_LEVEL, DECISION_SOURCES, } from '../../utils/enums'; -import { getForwardingEventProcessor } from '../../event_processor/forwarding_event_processor'; +import { getForwardingEventProcessor } from '../../event_processor/event_processor_factory'; import { createNotificationCenter } from '../../notification_center'; import Optimizely from '../../optimizely'; import OptimizelyUserContext from '../../optimizely_user_context'; diff --git a/lib/entrypoint.test-d.ts b/lib/entrypoint.test-d.ts index 3dd2f3c06..366889ea8 100644 --- a/lib/entrypoint.test-d.ts +++ b/lib/entrypoint.test-d.ts @@ -59,7 +59,7 @@ import { Maybe } from './utils/type'; export type Entrypoint = { // client factory - createInstance: (config: Config) => Client | null; + createInstance: (config: Config) => Client; // config manager related exports createStaticProjectConfigManager: (config: StaticConfigManagerConfig) => OpaqueConfigManager; diff --git a/lib/entrypoint.universal.test-d.ts b/lib/entrypoint.universal.test-d.ts index 2fa1891d4..184583a35 100644 --- a/lib/entrypoint.universal.test-d.ts +++ b/lib/entrypoint.universal.test-d.ts @@ -55,7 +55,7 @@ import { UniversalOdpManagerOptions } from './odp/odp_manager_factory.universal' export type UniversalEntrypoint = { // client factory - createInstance: (config: UniversalConfig) => Client | null; + createInstance: (config: UniversalConfig) => Client; // config manager related exports createStaticProjectConfigManager: (config: StaticConfigManagerConfig) => OpaqueConfigManager; diff --git a/lib/error/error_notifier_factory.spec.ts b/lib/error/error_notifier_factory.spec.ts new file mode 100644 index 000000000..556d7f2af --- /dev/null +++ b/lib/error/error_notifier_factory.spec.ts @@ -0,0 +1,33 @@ +/** + * 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, expect } from 'vitest'; +import { createErrorNotifier } from './error_notifier_factory'; + +describe('createErrorNotifier', () => { + it('should throw errors for invalid error handlers', () => { + expect(() => createErrorNotifier(null as any)).toThrow('Invalid error handler'); + expect(() => createErrorNotifier(undefined as any)).toThrow('Invalid error handler'); + + + expect(() => createErrorNotifier('abc' as any)).toThrow('Invalid error handler'); + expect(() => createErrorNotifier(123 as any)).toThrow('Invalid error handler'); + + expect(() => createErrorNotifier({} as any)).toThrow('Invalid error handler'); + + expect(() => createErrorNotifier({ handleError: 'abc' } as any)).toThrow('Invalid error handler'); + }); +}); diff --git a/lib/error/error_notifier_factory.ts b/lib/error/error_notifier_factory.ts index 970723591..994564f1a 100644 --- a/lib/error/error_notifier_factory.ts +++ b/lib/error/error_notifier_factory.ts @@ -14,21 +14,35 @@ * limitations under the License. */ import { errorResolver } from "../message/message_resolver"; +import { Maybe } from "../utils/type"; import { ErrorHandler } from "./error_handler"; import { DefaultErrorNotifier } from "./error_notifier"; +export const INVALID_ERROR_HANDLER = 'Invalid error handler'; + const errorNotifierSymbol = Symbol(); export type OpaqueErrorNotifier = { [errorNotifierSymbol]: unknown; }; +const validateErrorHandler = (errorHandler: ErrorHandler) => { + if (!errorHandler || typeof errorHandler !== 'object' || typeof errorHandler.handleError !== 'function') { + throw new Error(INVALID_ERROR_HANDLER); + } +} + export const createErrorNotifier = (errorHandler: ErrorHandler): OpaqueErrorNotifier => { + validateErrorHandler(errorHandler); return { [errorNotifierSymbol]: new DefaultErrorNotifier(errorHandler, errorResolver), } } -export const extractErrorNotifier = (errorNotifier: OpaqueErrorNotifier): DefaultErrorNotifier => { - return errorNotifier[errorNotifierSymbol] as DefaultErrorNotifier; +export const extractErrorNotifier = (errorNotifier: Maybe): Maybe => { + if (!errorNotifier || typeof errorNotifier !== 'object') { + return undefined; + } + + return errorNotifier[errorNotifierSymbol] as Maybe; } diff --git a/lib/event_processor/event_dispatcher/event_dispatcher_factory.ts b/lib/event_processor/event_dispatcher/event_dispatcher_factory.ts index 035fb7e49..383ad8380 100644 --- a/lib/event_processor/event_dispatcher/event_dispatcher_factory.ts +++ b/lib/event_processor/event_dispatcher/event_dispatcher_factory.ts @@ -18,6 +18,9 @@ import { RequestHandler } from '../../utils/http_request_handler/http'; import { DefaultEventDispatcher } from './default_dispatcher'; import { EventDispatcher } from './event_dispatcher'; +import { validateRequestHandler } from '../../utils/http_request_handler/request_handler_validator'; + export const createEventDispatcher = (requestHander: RequestHandler): EventDispatcher => { + validateRequestHandler(requestHander); return new DefaultEventDispatcher(requestHander); } diff --git a/lib/event_processor/event_processor_factory.browser.spec.ts b/lib/event_processor/event_processor_factory.browser.spec.ts index 475b36353..8db4a461e 100644 --- a/lib/event_processor/event_processor_factory.browser.spec.ts +++ b/lib/event_processor/event_processor_factory.browser.spec.ts @@ -19,13 +19,6 @@ vi.mock('./default_dispatcher.browser', () => { return { default: {} }; }); -vi.mock('./forwarding_event_processor', () => { - const getForwardingEventProcessor = vi.fn().mockImplementation(() => { - return {}; - }); - return { getForwardingEventProcessor }; -}); - vi.mock('./event_processor_factory', async (importOriginal) => { const getBatchEventProcessor = vi.fn().mockImplementation(() => { return {}; @@ -33,8 +26,11 @@ vi.mock('./event_processor_factory', async (importOriginal) => { const getOpaqueBatchEventProcessor = vi.fn().mockImplementation(() => { return {}; }); + const getForwardingEventProcessor = vi.fn().mockImplementation(() => { + return {}; + }); const original: any = await importOriginal(); - return { ...original, getBatchEventProcessor, getOpaqueBatchEventProcessor }; + return { ...original, getBatchEventProcessor, getOpaqueBatchEventProcessor, getForwardingEventProcessor }; }); vi.mock('../utils/cache/local_storage_cache.browser', () => { @@ -50,11 +46,11 @@ import defaultEventDispatcher from './event_dispatcher/default_dispatcher.browse import { LocalStorageCache } from '../utils/cache/local_storage_cache.browser'; import { SyncPrefixStore } from '../utils/cache/store'; import { createForwardingEventProcessor, createBatchEventProcessor } from './event_processor_factory.browser'; -import { EVENT_STORE_PREFIX, extractEventProcessor, FAILED_EVENT_RETRY_INTERVAL } from './event_processor_factory'; +import { EVENT_STORE_PREFIX, extractEventProcessor, getForwardingEventProcessor, FAILED_EVENT_RETRY_INTERVAL } from './event_processor_factory'; import sendBeaconEventDispatcher from './event_dispatcher/send_beacon_dispatcher.browser'; -import { getForwardingEventProcessor } from './forwarding_event_processor'; import browserDefaultEventDispatcher from './event_dispatcher/default_dispatcher.browser'; import { getOpaqueBatchEventProcessor } from './event_processor_factory'; +import { get } from 'http'; describe('createForwardingEventProcessor', () => { const mockGetForwardingEventProcessor = vi.mocked(getForwardingEventProcessor); diff --git a/lib/event_processor/event_processor_factory.browser.ts b/lib/event_processor/event_processor_factory.browser.ts index ff53b0298..e73b8bf24 100644 --- a/lib/event_processor/event_processor_factory.browser.ts +++ b/lib/event_processor/event_processor_factory.browser.ts @@ -13,8 +13,6 @@ * See the License for the specific language governing permissions and * limitations under the License. */ - -import { getForwardingEventProcessor } from './forwarding_event_processor'; import { EventDispatcher } from './event_dispatcher/event_dispatcher'; import { EventProcessor } from './event_processor'; import { EventWithId } from './batch_event_processor'; @@ -23,6 +21,7 @@ import { BatchEventProcessorOptions, OpaqueEventProcessor, wrapEventProcessor, + getForwardingEventProcessor, } from './event_processor_factory'; import defaultEventDispatcher from './event_dispatcher/default_dispatcher.browser'; import sendBeaconEventDispatcher from './event_dispatcher/send_beacon_dispatcher.browser'; diff --git a/lib/event_processor/event_processor_factory.node.spec.ts b/lib/event_processor/event_processor_factory.node.spec.ts index 512865381..191461e72 100644 --- a/lib/event_processor/event_processor_factory.node.spec.ts +++ b/lib/event_processor/event_processor_factory.node.spec.ts @@ -19,11 +19,6 @@ vi.mock('./default_dispatcher.node', () => { return { default: {} }; }); -vi.mock('./forwarding_event_processor', () => { - const getForwardingEventProcessor = vi.fn().mockReturnValue({}); - return { getForwardingEventProcessor }; -}); - vi.mock('./event_processor_factory', async (importOriginal) => { const getBatchEventProcessor = vi.fn().mockImplementation(() => { return {}; @@ -31,8 +26,9 @@ vi.mock('./event_processor_factory', async (importOriginal) => { const getOpaqueBatchEventProcessor = vi.fn().mockImplementation(() => { return {}; }); + const getForwardingEventProcessor = vi.fn().mockReturnValue({}); const original: any = await importOriginal(); - return { ...original, getBatchEventProcessor, getOpaqueBatchEventProcessor }; + return { ...original, getBatchEventProcessor, getOpaqueBatchEventProcessor, getForwardingEventProcessor }; }); vi.mock('../utils/cache/async_storage_cache.react_native', () => { @@ -44,12 +40,12 @@ vi.mock('../utils/cache/store', () => { }); import { createBatchEventProcessor, createForwardingEventProcessor } from './event_processor_factory.node'; -import { getForwardingEventProcessor } from './forwarding_event_processor'; import nodeDefaultEventDispatcher from './event_dispatcher/default_dispatcher.node'; -import { EVENT_STORE_PREFIX, extractEventProcessor, FAILED_EVENT_RETRY_INTERVAL } from './event_processor_factory'; +import { EVENT_STORE_PREFIX, extractEventProcessor, getForwardingEventProcessor, FAILED_EVENT_RETRY_INTERVAL } from './event_processor_factory'; import { getOpaqueBatchEventProcessor } from './event_processor_factory'; import { AsyncStore, AsyncPrefixStore, SyncStore, SyncPrefixStore } from '../utils/cache/store'; import { AsyncStorageCache } from '../utils/cache/async_storage_cache.react_native'; +import { get } from 'http'; describe('createForwardingEventProcessor', () => { const mockGetForwardingEventProcessor = vi.mocked(getForwardingEventProcessor); diff --git a/lib/event_processor/event_processor_factory.node.ts b/lib/event_processor/event_processor_factory.node.ts index cdcb533a1..b0ed4ffde 100644 --- a/lib/event_processor/event_processor_factory.node.ts +++ b/lib/event_processor/event_processor_factory.node.ts @@ -13,7 +13,6 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { getForwardingEventProcessor } from './forwarding_event_processor'; import { EventDispatcher } from './event_dispatcher/event_dispatcher'; import defaultEventDispatcher from './event_dispatcher/default_dispatcher.node'; import { @@ -23,6 +22,7 @@ import { getPrefixEventStore, OpaqueEventProcessor, wrapEventProcessor, + getForwardingEventProcessor, } from './event_processor_factory'; export const DEFAULT_EVENT_BATCH_SIZE = 10; diff --git a/lib/event_processor/event_processor_factory.react_native.spec.ts b/lib/event_processor/event_processor_factory.react_native.spec.ts index 6065e16de..cad875830 100644 --- a/lib/event_processor/event_processor_factory.react_native.spec.ts +++ b/lib/event_processor/event_processor_factory.react_native.spec.ts @@ -20,12 +20,9 @@ vi.mock('./default_dispatcher.browser', () => { return { default: {} }; }); -vi.mock('./forwarding_event_processor', () => { - const getForwardingEventProcessor = vi.fn().mockReturnValue({}); - return { getForwardingEventProcessor }; -}); vi.mock('./event_processor_factory', async importOriginal => { + const getForwardingEventProcessor = vi.fn().mockReturnValue({}); const getBatchEventProcessor = vi.fn().mockImplementation(() => { return {}; }); @@ -33,7 +30,7 @@ vi.mock('./event_processor_factory', async importOriginal => { return {}; }); const original: any = await importOriginal(); - return { ...original, getBatchEventProcessor, getOpaqueBatchEventProcessor }; + return { ...original, getBatchEventProcessor, getOpaqueBatchEventProcessor, getForwardingEventProcessor }; }); vi.mock('../utils/cache/async_storage_cache.react_native', () => { @@ -68,13 +65,13 @@ async function mockRequireNetInfo() { } import { createForwardingEventProcessor, createBatchEventProcessor } from './event_processor_factory.react_native'; -import { getForwardingEventProcessor } from './forwarding_event_processor'; import defaultEventDispatcher from './event_dispatcher/default_dispatcher.browser'; -import { EVENT_STORE_PREFIX, extractEventProcessor, FAILED_EVENT_RETRY_INTERVAL } from './event_processor_factory'; +import { EVENT_STORE_PREFIX, extractEventProcessor, getForwardingEventProcessor, FAILED_EVENT_RETRY_INTERVAL } from './event_processor_factory'; import { getOpaqueBatchEventProcessor } from './event_processor_factory'; import { AsyncStore, AsyncPrefixStore, SyncStore, SyncPrefixStore } from '../utils/cache/store'; import { AsyncStorageCache } from '../utils/cache/async_storage_cache.react_native'; import { MODULE_NOT_FOUND_REACT_NATIVE_ASYNC_STORAGE } from '../utils/import.react_native/@react-native-async-storage/async-storage'; +import { get } from 'http'; describe('createForwardingEventProcessor', () => { const mockGetForwardingEventProcessor = vi.mocked(getForwardingEventProcessor); diff --git a/lib/event_processor/event_processor_factory.react_native.ts b/lib/event_processor/event_processor_factory.react_native.ts index 99019eff0..b46b594a4 100644 --- a/lib/event_processor/event_processor_factory.react_native.ts +++ b/lib/event_processor/event_processor_factory.react_native.ts @@ -13,7 +13,6 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { getForwardingEventProcessor } from './forwarding_event_processor'; import { EventDispatcher } from './event_dispatcher/event_dispatcher'; import defaultEventDispatcher from './event_dispatcher/default_dispatcher.browser'; import { @@ -22,6 +21,7 @@ import { getPrefixEventStore, OpaqueEventProcessor, wrapEventProcessor, + getForwardingEventProcessor, } from './event_processor_factory'; import { EVENT_STORE_PREFIX, FAILED_EVENT_RETRY_INTERVAL } from './event_processor_factory'; import { AsyncPrefixStore } from '../utils/cache/store'; diff --git a/lib/event_processor/event_processor_factory.spec.ts b/lib/event_processor/event_processor_factory.spec.ts index fc57a5097..de9ad8d41 100644 --- a/lib/event_processor/event_processor_factory.spec.ts +++ b/lib/event_processor/event_processor_factory.spec.ts @@ -41,6 +41,38 @@ describe('getBatchEventProcessor', () => { MockIntervalRepeater.mockReset(); }); + it('should throw an error if provided eventDispatcher is not valid', () => { + expect(() => getBatchEventProcessor({ + eventDispatcher: undefined as any, + defaultFlushInterval: 10000, + defaultBatchSize: 10, + })).toThrow('Invalid event dispatcher'); + + expect(() => getBatchEventProcessor({ + eventDispatcher: null as any, + defaultFlushInterval: 10000, + defaultBatchSize: 10, + })).toThrow('Invalid event dispatcher'); + + expect(() => getBatchEventProcessor({ + eventDispatcher: 'abc' as any, + defaultFlushInterval: 10000, + defaultBatchSize: 10, + })).toThrow('Invalid event dispatcher'); + + expect(() => getBatchEventProcessor({ + eventDispatcher: {} as any, + defaultFlushInterval: 10000, + defaultBatchSize: 10, + })).toThrow('Invalid event dispatcher'); + + expect(() => getBatchEventProcessor({ + eventDispatcher: { dispatchEvent: 'abc' } as any, + defaultFlushInterval: 10000, + defaultBatchSize: 10, + })).toThrow('Invalid event dispatcher'); + }); + it('returns an instane of BatchEventProcessor if no subclass constructor is provided', () => { const options = { eventDispatcher: getMockEventDispatcher(), diff --git a/lib/event_processor/event_processor_factory.ts b/lib/event_processor/event_processor_factory.ts index 3fff90c9f..16ae6fe08 100644 --- a/lib/event_processor/event_processor_factory.ts +++ b/lib/event_processor/event_processor_factory.ts @@ -19,9 +19,12 @@ import { StartupLog } from "../service"; import { ExponentialBackoff, IntervalRepeater } from "../utils/repeater/repeater"; import { EventDispatcher } from "./event_dispatcher/event_dispatcher"; import { EventProcessor } from "./event_processor"; +import { ForwardingEventProcessor } from "./forwarding_event_processor"; import { BatchEventProcessor, DEFAULT_MAX_BACKOFF, DEFAULT_MIN_BACKOFF, EventWithId, RetryConfig } from "./batch_event_processor"; import { AsyncPrefixStore, Store, SyncPrefixStore } from "../utils/cache/store"; +import { Maybe } from "../utils/type"; +export const INVALID_EVENT_DISPATCHER = 'Invalid event dispatcher'; export const FAILED_EVENT_RETRY_INTERVAL = 20 * 1000; export const EVENT_STORE_PREFIX = 'optly_event:'; @@ -72,12 +75,23 @@ export type BatchEventProcessorFactoryOptions = Omit { + if (!eventDispatcher || typeof eventDispatcher !== 'object' || typeof eventDispatcher.dispatchEvent !== 'function') { + throw new Error(INVALID_EVENT_DISPATCHER); + } +} + export const getBatchEventProcessor = ( options: BatchEventProcessorFactoryOptions, EventProcessorConstructor: typeof BatchEventProcessor = BatchEventProcessor ): EventProcessor => { const { eventDispatcher, closingEventDispatcher, retryOptions, eventStore } = options; + validateEventDispatcher(eventDispatcher); + if (closingEventDispatcher) { + validateEventDispatcher(closingEventDispatcher); + } + const retryConfig: RetryConfig | undefined = retryOptions ? { maxRetries: retryOptions.maxRetries, backoffProvider: () => { @@ -142,6 +156,15 @@ export const getOpaqueBatchEventProcessor = ( return wrapEventProcessor(getBatchEventProcessor(options, EventProcessorConstructor)); } -export const extractEventProcessor = (eventProcessor: OpaqueEventProcessor): EventProcessor => { - return eventProcessor[eventProcessorSymbol] as EventProcessor; +export const extractEventProcessor = (eventProcessor: Maybe): Maybe => { + if (!eventProcessor || typeof eventProcessor !== 'object') { + return undefined; + } + return eventProcessor[eventProcessorSymbol] as Maybe; +} + + +export function getForwardingEventProcessor(dispatcher: EventDispatcher): EventProcessor { + validateEventDispatcher(dispatcher); + return new ForwardingEventProcessor(dispatcher); } diff --git a/lib/event_processor/event_processor_factory.universal.ts b/lib/event_processor/event_processor_factory.universal.ts index 7b192f96a..0a3b2ec56 100644 --- a/lib/event_processor/event_processor_factory.universal.ts +++ b/lib/event_processor/event_processor_factory.universal.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { getForwardingEventProcessor } from './forwarding_event_processor'; +import { getForwardingEventProcessor } from './event_processor_factory'; import { EventDispatcher } from './event_dispatcher/event_dispatcher'; import { diff --git a/lib/event_processor/forwarding_event_processor.spec.ts b/lib/event_processor/forwarding_event_processor.spec.ts index 76b69a185..65d571cb9 100644 --- a/lib/event_processor/forwarding_event_processor.spec.ts +++ b/lib/event_processor/forwarding_event_processor.spec.ts @@ -1,5 +1,5 @@ /** - * Copyright 2021, 2024 Optimizely + * Copyright 2021, 2024-2025 Optimizely * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -15,11 +15,11 @@ */ import { expect, describe, it, vi } from 'vitest'; -import { getForwardingEventProcessor } from './forwarding_event_processor'; import { EventDispatcher } from './event_dispatcher/event_dispatcher'; import { buildLogEvent, makeEventBatch } from './event_builder/log_event'; import { createImpressionEvent } from '../tests/mock/create_event'; import { ServiceState } from '../service'; +import { ForwardingEventProcessor } from './forwarding_event_processor'; const getMockEventDispatcher = (): EventDispatcher => { return { @@ -31,7 +31,7 @@ describe('ForwardingEventProcessor', () => { it('should resolve onRunning() when start is called', async () => { const dispatcher = getMockEventDispatcher(); - const processor = getForwardingEventProcessor(dispatcher); + const processor = new ForwardingEventProcessor(dispatcher); processor.start(); await expect(processor.onRunning()).resolves.not.toThrow(); @@ -41,7 +41,7 @@ describe('ForwardingEventProcessor', () => { const dispatcher = getMockEventDispatcher(); const mockDispatch = vi.mocked(dispatcher.dispatchEvent); - const processor = getForwardingEventProcessor(dispatcher); + const processor = new ForwardingEventProcessor(dispatcher); processor.start(); await processor.onRunning(); @@ -56,7 +56,7 @@ describe('ForwardingEventProcessor', () => { it('should emit dispatch event when event is dispatched', async() => { const dispatcher = getMockEventDispatcher(); - const processor = getForwardingEventProcessor(dispatcher); + const processor = new ForwardingEventProcessor(dispatcher); processor.start(); await processor.onRunning(); @@ -75,7 +75,7 @@ describe('ForwardingEventProcessor', () => { it('should remove dispatch listener when the function returned from onDispatch is called', async() => { const dispatcher = getMockEventDispatcher(); - const processor = getForwardingEventProcessor(dispatcher); + const processor = new ForwardingEventProcessor(dispatcher); processor.start(); await processor.onRunning(); @@ -98,7 +98,7 @@ describe('ForwardingEventProcessor', () => { it('should resolve onTerminated promise when stop is called', async () => { const dispatcher = getMockEventDispatcher(); - const processor = getForwardingEventProcessor(dispatcher); + const processor = new ForwardingEventProcessor(dispatcher); processor.start(); await processor.onRunning(); @@ -110,7 +110,7 @@ describe('ForwardingEventProcessor', () => { it('should reject onRunning promise when stop is called in New state', async () => { const dispatcher = getMockEventDispatcher(); - const processor = getForwardingEventProcessor(dispatcher); + const processor = new ForwardingEventProcessor(dispatcher); expect(processor.getState()).toEqual(ServiceState.New); diff --git a/lib/event_processor/forwarding_event_processor.ts b/lib/event_processor/forwarding_event_processor.ts index a0587ab6a..80ce1c763 100644 --- a/lib/event_processor/forwarding_event_processor.ts +++ b/lib/event_processor/forwarding_event_processor.ts @@ -1,5 +1,5 @@ /** - * Copyright 2021-2024, Optimizely + * Copyright 2021-2025, Optimizely * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -26,7 +26,7 @@ import { Consumer, Fn } from '../utils/type'; import { SERVICE_STOPPED_BEFORE_RUNNING } from '../service'; import { sprintf } from '../utils/fns'; -class ForwardingEventProcessor extends BaseService implements EventProcessor { +export class ForwardingEventProcessor extends BaseService implements EventProcessor { private dispatcher: EventDispatcher; private eventEmitter: EventEmitter<{ dispatch: LogEvent }>; @@ -70,7 +70,3 @@ class ForwardingEventProcessor extends BaseService implements EventProcessor { return this.eventEmitter.on('dispatch', handler); } } - -export function getForwardingEventProcessor(dispatcher: EventDispatcher): EventProcessor { - return new ForwardingEventProcessor(dispatcher); -} diff --git a/lib/index.browser.tests.js b/lib/index.browser.tests.js index 3ea249904..28b94a9d0 100644 --- a/lib/index.browser.tests.js +++ b/lib/index.browser.tests.js @@ -1,5 +1,5 @@ /** - * Copyright 2016-2020, 2022-2024 Optimizely + * Copyright 2016-2020, 2022-2025 Optimizely * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -104,13 +104,11 @@ describe('javascript-sdk (Browser)', function() { mockLogger = getLogger(); sinon.stub(mockLogger, 'error'); - sinon.stub(configValidator, 'validate'); global.XMLHttpRequest = sinon.useFakeXMLHttpRequest(); }); afterEach(function() { mockLogger.error.restore(); - configValidator.validate.restore(); delete global.XMLHttpRequest; }); @@ -136,15 +134,15 @@ describe('javascript-sdk (Browser)', function() { // sinon.assert.calledOnce(LocalStoragePendingEventsDispatcher.prototype.sendPendingEvents); // }); - it('should not throw if the provided config is not valid', function() { - configValidator.validate.throws(new Error('INVALID_CONFIG_OR_SOMETHING')); - assert.doesNotThrow(function() { - var optlyInstance = optimizelyFactory.createInstance({ - projectConfigManager: wrapConfigManager(getMockProjectConfigManager()), - logger: wrapLogger(mockLogger), - }); - }); - }); + // it('should not throw if the provided config is not valid', function() { + // configValidator.validate.throws(new Error('INVALID_CONFIG_OR_SOMETHING')); + // assert.doesNotThrow(function() { + // var optlyInstance = optimizelyFactory.createInstance({ + // projectConfigManager: wrapConfigManager(getMockProjectConfigManager()), + // logger: wrapLogger(mockLogger), + // }); + // }); + // }); it('should create an instance of optimizely', function() { var optlyInstance = optimizelyFactory.createInstance({ diff --git a/lib/index.browser.ts b/lib/index.browser.ts index 96249c6a9..4cbfc7c69 100644 --- a/lib/index.browser.ts +++ b/lib/index.browser.ts @@ -26,7 +26,7 @@ import { BrowserRequestHandler } from './utils/http_request_handler/request_hand * @return {Client|null} the Optimizely client object * null on error */ -export const createInstance = function(config: Config): Client | null { +export const createInstance = function(config: Config): Client { const client = getOptimizelyInstance({ ...config, requestHandler: new BrowserRequestHandler(), diff --git a/lib/index.node.tests.js b/lib/index.node.tests.js index 0146fffab..8279c5110 100644 --- a/lib/index.node.tests.js +++ b/lib/index.node.tests.js @@ -1,5 +1,5 @@ /** - * Copyright 2016-2020, 2022-2024 Optimizely + * Copyright 2016-2020, 2022-2025 Optimizely * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -49,12 +49,10 @@ describe('optimizelyFactory', function() { var fakeLogger = createLogger(); beforeEach(function() { - sinon.stub(configValidator, 'validate'); sinon.stub(fakeLogger, 'error'); }); afterEach(function() { - configValidator.validate.restore(); fakeLogger.error.restore(); }); @@ -70,16 +68,16 @@ describe('optimizelyFactory', function() { // sinon.assert.calledWith(localLogger.log, enums.LOG_LEVEL.ERROR); // }); - it('should not throw if the provided config is not valid', function() { - configValidator.validate.throws(new Error('INVALID_CONFIG_OR_SOMETHING')); - assert.doesNotThrow(function() { - var optlyInstance = optimizelyFactory.createInstance({ - projectConfigManager: wrapConfigManager(getMockProjectConfigManager()), - logger: wrapLogger(fakeLogger), - }); - }); - // sinon.assert.calledOnce(fakeLogger.error); - }); + // it('should not throw if the provided config is not valid', function() { + // configValidator.validate.throws(new Error('INVALID_CONFIG_OR_SOMETHING')); + // assert.doesNotThrow(function() { + // var optlyInstance = optimizelyFactory.createInstance({ + // projectConfigManager: wrapConfigManager(getMockProjectConfigManager()), + // logger: wrapLogger(fakeLogger), + // }); + // }); + // // sinon.assert.calledOnce(fakeLogger.error); + // }); // it('should create an instance of optimizely', function() { // var optlyInstance = optimizelyFactory.createInstance({ diff --git a/lib/index.node.ts b/lib/index.node.ts index b911d0d6a..cd12b25fb 100644 --- a/lib/index.node.ts +++ b/lib/index.node.ts @@ -25,7 +25,7 @@ import { NodeRequestHandler } from './utils/http_request_handler/request_handler * @return {Client|null} the Optimizely client object * null on error */ -export const createInstance = function(config: Config): Client | null { +export const createInstance = function(config: Config): Client { const nodeConfig = { ...config, clientEnging: config.clientEngine || NODE_CLIENT_ENGINE, diff --git a/lib/index.react_native.spec.ts b/lib/index.react_native.spec.ts index d091e889a..d46311278 100644 --- a/lib/index.react_native.spec.ts +++ b/lib/index.react_native.spec.ts @@ -1,5 +1,5 @@ /** - * Copyright 2019-2020, 2022-2024 Optimizely + * Copyright 2019-2020, 2022-2025 Optimizely * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -66,19 +66,19 @@ describe('javascript-sdk/react-native', () => { vi.resetAllMocks(); }); - it('should not throw if the provided config is not valid', () => { - vi.spyOn(configValidator, 'validate').mockImplementation(() => { - throw new Error('Invalid config or something'); - }); - expect(function() { - const optlyInstance = optimizelyFactory.createInstance({ - projectConfigManager: wrapConfigManager(getMockProjectConfigManager()), - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - logger: wrapLogger(mockLogger), - }); - }).not.toThrow(); - }); + // it('should not throw if the provided config is not valid', () => { + // vi.spyOn(configValidator, 'validate').mockImplementation(() => { + // throw new Error('Invalid config or something'); + // }); + // expect(function() { + // const optlyInstance = optimizelyFactory.createInstance({ + // projectConfigManager: wrapConfigManager(getMockProjectConfigManager()), + // // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // // @ts-ignore + // logger: wrapLogger(mockLogger), + // }); + // }).not.toThrow(); + // }); it('should create an instance of optimizely', () => { const optlyInstance = optimizelyFactory.createInstance({ diff --git a/lib/index.react_native.ts b/lib/index.react_native.ts index df386fa66..a556290ba 100644 --- a/lib/index.react_native.ts +++ b/lib/index.react_native.ts @@ -28,7 +28,7 @@ import { BrowserRequestHandler } from './utils/http_request_handler/request_hand * @return {Client|null} the Optimizely client object * null on error */ -export const createInstance = function(config: Config): Client | null { +export const createInstance = function(config: Config): Client { const rnConfig = { ...config, clientEngine: config.clientEngine || REACT_NATIVE_JS_CLIENT_ENGINE, diff --git a/lib/index.universal.ts b/lib/index.universal.ts index 5cc64f51e..4ef8cc20f 100644 --- a/lib/index.universal.ts +++ b/lib/index.universal.ts @@ -29,7 +29,7 @@ export type UniversalConfig = Config & { * @return {Client|null} the Optimizely client object * null on error */ -export const createInstance = function(config: UniversalConfig): Client | null { +export const createInstance = function(config: UniversalConfig): Client { return getOptimizelyInstance(config); }; diff --git a/lib/logging/logger_factory.spec.ts b/lib/logging/logger_factory.spec.ts index 6910ab67a..bc7671008 100644 --- a/lib/logging/logger_factory.spec.ts +++ b/lib/logging/logger_factory.spec.ts @@ -28,7 +28,7 @@ import { OptimizelyLogger, ConsoleLogHandler, LogLevel } from './logger'; import { createLogger, extractLogger, INFO } from './logger_factory'; import { errorResolver, infoResolver } from '../message/message_resolver'; -describe('create', () => { +describe('createLogger', () => { const MockedOptimizelyLogger = vi.mocked(OptimizelyLogger); const MockedConsoleLogHandler = vi.mocked(ConsoleLogHandler); @@ -37,6 +37,45 @@ describe('create', () => { MockedOptimizelyLogger.mockClear(); }); + it('should throw an error if the provided logHandler is not a valid LogHandler', () => { + expect(() => createLogger({ + level: INFO, + logHandler: {} as any, + })).toThrow('Invalid log handler'); + + expect(() => createLogger({ + level: INFO, + logHandler: { log: 'abc' } as any, + })).toThrow('Invalid log handler'); + + expect(() => createLogger({ + level: INFO, + logHandler: 'abc' as any, + })).toThrow('Invalid log handler'); + }); + + it('should throw an error if the level is not a valid level preset', () => { + expect(() => createLogger({ + level: null as any, + })).toThrow('Invalid level preset'); + + expect(() => createLogger({ + level: undefined as any, + })).toThrow('Invalid level preset'); + + expect(() => createLogger({ + level: 'abc' as any, + })).toThrow('Invalid level preset'); + + expect(() => createLogger({ + level: 123 as any, + })).toThrow('Invalid level preset'); + + expect(() => createLogger({ + level: {} as any, + })).toThrow('Invalid level preset'); + }); + it('should use the passed in options and a default name Optimizely', () => { const mockLogHandler = { log: vi.fn() }; diff --git a/lib/logging/logger_factory.ts b/lib/logging/logger_factory.ts index 9830acd48..2aee1b535 100644 --- a/lib/logging/logger_factory.ts +++ b/lib/logging/logger_factory.ts @@ -15,6 +15,10 @@ */ import { ConsoleLogHandler, LogHandler, LogLevel, OptimizelyLogger } from './logger'; import { errorResolver, infoResolver, MessageResolver } from '../message/message_resolver'; +import { Maybe } from '../utils/type'; + +export const INVALID_LOG_HANDLER = 'Invalid log handler'; +export const INVALID_LEVEL_PRESET = 'Invalid level preset'; type LevelPreset = { level: LogLevel, @@ -67,6 +71,9 @@ export const ERROR: OpaqueLevelPreset = { }; export const extractLevelPreset = (preset: OpaqueLevelPreset): LevelPreset => { + if (!preset || typeof preset !== 'object' || !preset[levelPresetSymbol]) { + throw new Error(INVALID_LEVEL_PRESET); + } return preset[levelPresetSymbol] as LevelPreset; } @@ -81,8 +88,18 @@ export type LoggerConfig = { logHandler?: LogHandler, }; +const validateLogHandler = (logHandler: any) => { + if (typeof logHandler !== 'object' || typeof logHandler.log !== 'function') { + throw new Error(INVALID_LOG_HANDLER); + } +} + export const createLogger = (config: LoggerConfig): OpaqueLogger => { const { level, infoResolver, errorResolver } = extractLevelPreset(config.level); + + if (config.logHandler) { + validateLogHandler(config.logHandler); + } const loggerName = 'Optimizely'; @@ -103,7 +120,10 @@ export const wrapLogger = (logger: OptimizelyLogger): OpaqueLogger => { }; }; -export const extractLogger = (logger: OpaqueLogger): OptimizelyLogger => { - return logger[loggerSymbol] as OptimizelyLogger; -}; +export const extractLogger = (logger: Maybe): Maybe => { + if (!logger || typeof logger !== 'object') { + return undefined; + } + return logger[loggerSymbol] as Maybe; +}; diff --git a/lib/message/error_message.ts b/lib/message/error_message.ts index b47e718bf..720baa377 100644 --- a/lib/message/error_message.ts +++ b/lib/message/error_message.ts @@ -23,14 +23,11 @@ export const INVALID_DATAFILE = 'Datafile is invalid - property %s: %s'; export const INVALID_DATAFILE_MALFORMED = 'Datafile is invalid because it is malformed.'; export const INVALID_CONFIG = 'Provided Optimizely config is in an invalid format.'; export const INVALID_JSON = 'JSON object is not valid.'; -export const INVALID_ERROR_HANDLER = 'Provided "errorHandler" is in an invalid format.'; -export const INVALID_EVENT_DISPATCHER = 'Provided "eventDispatcher" is in an invalid format.'; export const INVALID_EVENT_TAGS = 'Provided event tags are in an invalid format.'; export const INVALID_EXPERIMENT_KEY = 'Experiment key %s is not in datafile. It is either invalid, paused, or archived.'; export const INVALID_EXPERIMENT_ID = 'Experiment ID %s is not in datafile.'; export const INVALID_GROUP_ID = 'Group ID %s is not in datafile.'; -export const INVALID_LOGGER = 'Provided "logger" is in an invalid format.'; export const INVALID_USER_ID = 'Provided user ID is in an invalid format.'; export const INVALID_USER_PROFILE_SERVICE = 'Provided user profile service instance is in an invalid format: %s.'; export const MISSING_INTEGRATION_KEY = diff --git a/lib/odp/odp_manager_factory.spec.ts b/lib/odp/odp_manager_factory.spec.ts index 9815f3085..b80689b79 100644 --- a/lib/odp/odp_manager_factory.spec.ts +++ b/lib/odp/odp_manager_factory.spec.ts @@ -1,5 +1,5 @@ /** - * Copyright 2024, Optimizely + * Copyright 2024-2025, Optimizely * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -13,6 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ +import { getMockSyncCache } from '../tests/mock/mock_cache'; vi.mock('./odp_manager', () => { return { @@ -90,6 +91,45 @@ describe('getOdpManager', () => { MockExponentialBackoff.mockClear(); }); + it('should throw and error if provided segment cache is invalid', () => { + expect(() => getOdpManager({ + segmentRequestHandler: getMockRequestHandler(), + eventRequestHandler: getMockRequestHandler(), + eventRequestGenerator: vi.fn(), + segmentsCache: 'abc' as any + })).toThrow('Invalid cache'); + + expect(() => getOdpManager({ + segmentRequestHandler: getMockRequestHandler(), + eventRequestHandler: getMockRequestHandler(), + eventRequestGenerator: vi.fn(), + segmentsCache: {} as any, + })).toThrow('Invalid cache method save, Invalid cache method lookup, Invalid cache method reset'); + + expect(() => getOdpManager({ + segmentRequestHandler: getMockRequestHandler(), + eventRequestHandler: getMockRequestHandler(), + eventRequestGenerator: vi.fn(), + segmentsCache: { save: 'abc', lookup: 'abc', reset: 'abc' } as any, + })).toThrow('Invalid cache method save, Invalid cache method lookup, Invalid cache method reset'); + + const noop = () => {}; + + expect(() => getOdpManager({ + segmentRequestHandler: getMockRequestHandler(), + eventRequestHandler: getMockRequestHandler(), + eventRequestGenerator: vi.fn(), + segmentsCache: { save: noop, lookup: 'abc', reset: 'abc' } as any, + })).toThrow('Invalid cache method lookup, Invalid cache method reset'); + + expect(() => getOdpManager({ + segmentRequestHandler: getMockRequestHandler(), + eventRequestHandler: getMockRequestHandler(), + eventRequestGenerator: vi.fn(), + segmentsCache: { save: noop, lookup: noop, reset: 'abc' } as any, + })).toThrow('Invalid cache method reset'); + }); + describe('segment manager', () => { it('should create a default segment manager with default api manager using the passed eventRequestHandler', () => { const segmentRequestHandler = getMockRequestHandler(); @@ -109,7 +149,7 @@ describe('getOdpManager', () => { }); it('should create a default segment manager with the provided segment cache', () => { - const segmentsCache = {} as any; + const segmentsCache = getMockSyncCache(); const odpManager = getOdpManager({ segmentsCache, diff --git a/lib/odp/odp_manager_factory.ts b/lib/odp/odp_manager_factory.ts index 91504d5e6..d5178227e 100644 --- a/lib/odp/odp_manager_factory.ts +++ b/lib/odp/odp_manager_factory.ts @@ -1,5 +1,5 @@ /** - * Copyright 2024, Optimizely + * Copyright 2024-2025, Optimizely * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -18,6 +18,7 @@ import { RequestHandler } from "../shared_types"; import { Cache } from "../utils/cache/cache"; import { InMemoryLruCache } from "../utils/cache/in_memory_lru_cache"; import { ExponentialBackoff, IntervalRepeater } from "../utils/repeater/repeater"; +import { Maybe } from "../utils/type"; import { DefaultOdpEventApiManager, EventRequestGenerator } from "./event_manager/odp_event_api_manager"; import { DefaultOdpEventManager, OdpEventManager } from "./event_manager/odp_event_manager"; import { DefaultOdpManager, OdpManager } from "./odp_manager"; @@ -34,6 +35,9 @@ export const DEFAULT_EVENT_MAX_RETRIES = 5; export const DEFAULT_EVENT_MIN_BACKOFF = 1000; export const DEFAULT_EVENT_MAX_BACKOFF = 32_000; +export const INVALID_CACHE = 'Invalid cache'; +export const INVALID_CACHE_METHOD = 'Invalid cache method %s'; + const odpManagerSymbol: unique symbol = Symbol(); export type OpaqueOdpManager = { @@ -60,11 +64,33 @@ export type OdpManagerFactoryOptions = Omit { + const errors = []; + if (!cache || typeof cache !== 'object') { + console.log('what ', cache, typeof cache); + throw new Error(INVALID_CACHE); + } + + for (const method of ['save', 'lookup', 'reset']) { + if (typeof cache[method] !== 'function') { + errors.push(INVALID_CACHE_METHOD.replace('%s', method)); + } + } + + if (errors.length > 0) { + throw new Error(errors.join(', ')); + } +} + const getDefaultSegmentsCache = (cacheSize?: number, cacheTimeout?: number) => { return new InMemoryLruCache(cacheSize || DEFAULT_CACHE_SIZE, cacheTimeout || DEFAULT_CACHE_TIMEOUT); } const getDefaultSegmentManager = (options: OdpManagerFactoryOptions) => { + if (options.segmentsCache) { + validateCache(options.segmentsCache); + } + return new DefaultOdpSegmentManager( options.segmentsCache || getDefaultSegmentsCache(options.segmentsCacheSize, options.segmentsCacheTimeout), new DefaultOdpSegmentApiManager(options.segmentRequestHandler), @@ -104,6 +130,10 @@ export const getOpaqueOdpManager = (options: OdpManagerFactoryOptions): OpaqueOd }; }; -export const extractOdpManager = (manager: OpaqueOdpManager): OdpManager => { - return manager[odpManagerSymbol] as OdpManager; +export const extractOdpManager = (manager: Maybe): Maybe => { + if (!manager || typeof manager !== 'object') { + return undefined; + } + + return manager[odpManagerSymbol] as Maybe; } diff --git a/lib/odp/odp_manager_factory.universal.ts b/lib/odp/odp_manager_factory.universal.ts index 9ad2bc250..6bf509611 100644 --- a/lib/odp/odp_manager_factory.universal.ts +++ b/lib/odp/odp_manager_factory.universal.ts @@ -15,6 +15,7 @@ */ import { RequestHandler } from '../utils/http_request_handler/http'; +import { validateRequestHandler } from '../utils/http_request_handler/request_handler_validator'; import { eventApiRequestGenerator } from './event_manager/odp_event_api_manager'; import { getOpaqueOdpManager, OdpManagerOptions, OpaqueOdpManager } from './odp_manager_factory'; @@ -27,6 +28,7 @@ export type UniversalOdpManagerOptions = OdpManagerOptions & { }; export const createOdpManager = (options: UniversalOdpManagerOptions): OpaqueOdpManager => { + validateRequestHandler(options.requestHandler); return getOpaqueOdpManager({ ...options, segmentRequestHandler: options.requestHandler, diff --git a/lib/optimizely/index.spec.ts b/lib/optimizely/index.spec.ts index dfe708de4..165fae41b 100644 --- a/lib/optimizely/index.spec.ts +++ b/lib/optimizely/index.spec.ts @@ -1,5 +1,5 @@ /** - * Copyright 2024, Optimizely + * Copyright 2024-2025, Optimizely * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -17,10 +17,8 @@ import { describe, it, expect, vi, beforeEach } from 'vitest'; import Optimizely from '.'; import { getMockProjectConfigManager } from '../tests/mock/mock_project_config_manager'; import * as jsonSchemaValidator from '../utils/json_schema_validator'; -import { createNotificationCenter } from '../notification_center'; import testData from '../tests/test_data'; -import { getForwardingEventProcessor } from '../event_processor/forwarding_event_processor'; -import { LoggerFacade } from '../logging/logger'; +import { getForwardingEventProcessor } from '../event_processor/event_processor_factory'; import { createProjectConfig } from '../project_config/project_config'; import { getMockLogger } from '../tests/mock/mock_logger'; import { createOdpManager } from '../odp/odp_manager_factory.node'; @@ -30,7 +28,6 @@ import { getDecisionTestDatafile } from '../tests/decision_test_datafile'; import { DECISION_SOURCES } from '../utils/enums'; import OptimizelyUserContext from '../optimizely_user_context'; import { newErrorDecision } from '../optimizely_decision'; -import { EventDispatcher } from '../shared_types'; import { ImpressionEvent } from '../event_processor/event_builder/user_event'; describe('Optimizely', () => { @@ -53,7 +50,7 @@ describe('Optimizely', () => { vi.spyOn(projectConfigManager, 'makeDisposable'); vi.spyOn(eventProcessor, 'makeDisposable'); - vi.spyOn(odpManager, 'makeDisposable'); + vi.spyOn(odpManager!, 'makeDisposable'); new Optimizely({ clientEngine: 'node-sdk', @@ -68,7 +65,7 @@ describe('Optimizely', () => { expect(projectConfigManager.makeDisposable).toHaveBeenCalled(); expect(eventProcessor.makeDisposable).toHaveBeenCalled(); - expect(odpManager.makeDisposable).toHaveBeenCalled(); + expect(odpManager!.makeDisposable).toHaveBeenCalled(); }); it('should set child logger to respective services', () => { @@ -81,7 +78,7 @@ describe('Optimizely', () => { vi.spyOn(projectConfigManager, 'setLogger'); vi.spyOn(eventProcessor, 'setLogger'); - vi.spyOn(odpManager, 'setLogger'); + vi.spyOn(odpManager!, 'setLogger'); const logger = getMockLogger(); const configChildLogger = getMockLogger(); @@ -104,7 +101,7 @@ describe('Optimizely', () => { expect(projectConfigManager.setLogger).toHaveBeenCalledWith(configChildLogger); expect(eventProcessor.setLogger).toHaveBeenCalledWith(eventProcessorChildLogger); - expect(odpManager.setLogger).toHaveBeenCalledWith(odpManagerChildLogger); + expect(odpManager!.setLogger).toHaveBeenCalledWith(odpManagerChildLogger); }); describe('decideAsync', () => { diff --git a/lib/optimizely/index.tests.js b/lib/optimizely/index.tests.js index a7107c479..20debfe31 100644 --- a/lib/optimizely/index.tests.js +++ b/lib/optimizely/index.tests.js @@ -1,5 +1,5 @@ /** - * Copyright 2016-2024, Optimizely + * Copyright 2016-2025, Optimizely * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -28,7 +28,7 @@ import * as decisionService from '../core/decision_service'; import * as jsonSchemaValidator from '../utils/json_schema_validator'; import * as projectConfig from '../project_config/project_config'; import testData from '../tests/test_data'; -import { getForwardingEventProcessor } from '../event_processor/forwarding_event_processor'; +import { getForwardingEventProcessor } from '../event_processor/event_processor_factory'; import { createNotificationCenter } from '../notification_center'; import { createProjectConfig } from '../project_config/project_config'; import { getMockProjectConfigManager } from '../tests/mock/mock_project_config_manager'; diff --git a/lib/optimizely_user_context/index.tests.js b/lib/optimizely_user_context/index.tests.js index 56457a67c..9f3597187 100644 --- a/lib/optimizely_user_context/index.tests.js +++ b/lib/optimizely_user_context/index.tests.js @@ -1,5 +1,5 @@ /** - * Copyright 2020-2024, Optimizely + * Copyright 2020-2025, Optimizely * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -25,7 +25,7 @@ import testData from '../tests/test_data'; import { OptimizelyDecideOption } from '../shared_types'; import { getMockProjectConfigManager } from '../tests/mock/mock_project_config_manager'; import { createProjectConfig } from '../project_config/project_config'; -import { getForwardingEventProcessor } from '../event_processor/forwarding_event_processor'; +import { getForwardingEventProcessor } from '../event_processor/event_processor_factory'; import { FORCED_DECISION_NULL_RULE_KEY } from './index' import { diff --git a/lib/project_config/config_manager_factory.ts b/lib/project_config/config_manager_factory.ts index 763c235d0..89c60593c 100644 --- a/lib/project_config/config_manager_factory.ts +++ b/lib/project_config/config_manager_factory.ts @@ -15,7 +15,7 @@ */ import { RequestHandler } from "../utils/http_request_handler/http"; -import { Transformer } from "../utils/type"; +import { Maybe, Transformer } from "../utils/type"; import { DatafileManagerConfig } from "./datafile_manager"; import { ProjectConfigManagerImpl, ProjectConfigManager } from "./project_config_manager"; import { PollingDatafileManager } from "./polling_datafile_manager"; @@ -26,6 +26,8 @@ import { MIN_UPDATE_INTERVAL, UPDATE_INTERVAL_BELOW_MINIMUM_MESSAGE } from './co import { LogLevel } from '../logging/logger' import { Store } from "../utils/cache/store"; +export const INVALID_CONFIG_MANAGER = "Invalid config manager"; + const configManagerSymbol: unique symbol = Symbol(); export type OpaqueConfigManager = { @@ -109,5 +111,14 @@ export const wrapConfigManager = (configManager: ProjectConfigManager): OpaqueCo }; export const extractConfigManager = (opaqueConfigManager: OpaqueConfigManager): ProjectConfigManager => { + if (!opaqueConfigManager || typeof opaqueConfigManager !== 'object') { + throw new Error(INVALID_CONFIG_MANAGER); + } + + const configManager = opaqueConfigManager[configManagerSymbol]; + if (!configManager) { + throw new Error(INVALID_CONFIG_MANAGER); + } + return opaqueConfigManager[configManagerSymbol] as ProjectConfigManager; }; diff --git a/lib/project_config/config_manager_factory.universal.ts b/lib/project_config/config_manager_factory.universal.ts index bcbd4a310..bcc664082 100644 --- a/lib/project_config/config_manager_factory.universal.ts +++ b/lib/project_config/config_manager_factory.universal.ts @@ -15,16 +15,15 @@ */ import { getOpaquePollingConfigManager, OpaqueConfigManager, PollingConfigManagerConfig } from "./config_manager_factory"; -import { NodeRequestHandler } from "../utils/http_request_handler/request_handler.node"; -import { ProjectConfigManager } from "./project_config_manager"; -import { DEFAULT_URL_TEMPLATE, DEFAULT_AUTHENTICATED_URL_TEMPLATE } from './constant'; import { RequestHandler } from "../utils/http_request_handler/http"; +import { validateRequestHandler } from "../utils/http_request_handler/request_handler_validator"; export type UniversalPollingConfigManagerConfig = PollingConfigManagerConfig & { requestHandler: RequestHandler; } export const createPollingProjectConfigManager = (config: UniversalPollingConfigManagerConfig): OpaqueConfigManager => { + validateRequestHandler(config.requestHandler); const defaultConfig = { autoUpdate: true, }; diff --git a/lib/utils/config_validator/index.ts b/lib/utils/config_validator/index.ts index abd0a6967..a05c6d266 100644 --- a/lib/utils/config_validator/index.ts +++ b/lib/utils/config_validator/index.ts @@ -1,5 +1,5 @@ /** - * Copyright 2016, 2018-2020, 2022, Optimizely + * Copyright 2016, 2018-2020, 2022, 2025, Optimizely * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -13,42 +13,18 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { ObjectWithUnknownProperties } from '../../shared_types'; - import { DATAFILE_VERSIONS, } from '../enums'; import { - INVALID_CONFIG, INVALID_DATAFILE_MALFORMED, INVALID_DATAFILE_VERSION, - INVALID_ERROR_HANDLER, - INVALID_EVENT_DISPATCHER, - INVALID_LOGGER, NO_DATAFILE_SPECIFIED, } from 'error_message'; import { OptimizelyError } from '../../error/optimizly_error'; const SUPPORTED_VERSIONS = [DATAFILE_VERSIONS.V2, DATAFILE_VERSIONS.V3, DATAFILE_VERSIONS.V4]; -/** - * Validates the given config options - * @param {unknown} config - * @param {object} config.errorHandler - * @param {object} config.eventDispatcher - * @param {object} config.logger - * @return {boolean} true if the config options are valid - * @throws If any of the config options are not valid - */ -export const validate = function(config: unknown): boolean { - if (typeof config === 'object' && config !== null) { - const configObj = config as ObjectWithUnknownProperties; - // TODO: add validation - return true; - } - throw new OptimizelyError(INVALID_CONFIG); -} - /** * Validates the datafile * @param {Object|string} datafile @@ -80,10 +56,6 @@ export const validateDatafile = function(datafile: unknown): any { return datafile; }; -/** - * Provides utility methods for validating that the configuration options are valid - */ export default { - validate: validate, validateDatafile: validateDatafile, } diff --git a/lib/utils/http_request_handler/request_handler_validator.ts b/lib/utils/http_request_handler/request_handler_validator.ts new file mode 100644 index 000000000..a9df4cc7c --- /dev/null +++ b/lib/utils/http_request_handler/request_handler_validator.ts @@ -0,0 +1,28 @@ +/** + * 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 { RequestHandler } from './http'; + +export const INVALID_REQUEST_HANDLER = 'Invalid request handler'; + +export const validateRequestHandler = (requestHandler: RequestHandler): void => { + if (!requestHandler || typeof requestHandler !== 'object') { + throw new Error(INVALID_REQUEST_HANDLER); + } + + if (typeof requestHandler.makeRequest !== 'function') { + throw new Error(INVALID_REQUEST_HANDLER); + } +} diff --git a/lib/vuid/vuid_manager_factory.ts b/lib/vuid/vuid_manager_factory.ts index 94c777e26..f7f1b760f 100644 --- a/lib/vuid/vuid_manager_factory.ts +++ b/lib/vuid/vuid_manager_factory.ts @@ -29,7 +29,11 @@ export type OpaqueVuidManager = { [vuidManagerSymbol]: unknown; }; -export const extractVuidManager = (opaqueVuidManager: OpaqueVuidManager): Maybe => { +export const extractVuidManager = (opaqueVuidManager: Maybe): Maybe => { + if (!opaqueVuidManager || typeof opaqueVuidManager !== 'object') { + return undefined; + } + return opaqueVuidManager[vuidManagerSymbol] as Maybe; }; From 1759afef20cceea3fce9f9a8cb22a88c5bf7d68f Mon Sep 17 00:00:00 2001 From: Raju Ahmed Date: Wed, 21 May 2025 22:22:19 +0600 Subject: [PATCH 2/3] upd --- lib/event_processor/event_processor_factory.browser.spec.ts | 1 - lib/event_processor/event_processor_factory.node.spec.ts | 1 - .../event_processor_factory.react_native.spec.ts | 1 - lib/vuid/vuid_manager.spec.ts | 2 +- 4 files changed, 1 insertion(+), 4 deletions(-) diff --git a/lib/event_processor/event_processor_factory.browser.spec.ts b/lib/event_processor/event_processor_factory.browser.spec.ts index 8db4a461e..a5d2a6af3 100644 --- a/lib/event_processor/event_processor_factory.browser.spec.ts +++ b/lib/event_processor/event_processor_factory.browser.spec.ts @@ -50,7 +50,6 @@ import { EVENT_STORE_PREFIX, extractEventProcessor, getForwardingEventProcessor, import sendBeaconEventDispatcher from './event_dispatcher/send_beacon_dispatcher.browser'; import browserDefaultEventDispatcher from './event_dispatcher/default_dispatcher.browser'; import { getOpaqueBatchEventProcessor } from './event_processor_factory'; -import { get } from 'http'; describe('createForwardingEventProcessor', () => { const mockGetForwardingEventProcessor = vi.mocked(getForwardingEventProcessor); diff --git a/lib/event_processor/event_processor_factory.node.spec.ts b/lib/event_processor/event_processor_factory.node.spec.ts index 191461e72..22b943f19 100644 --- a/lib/event_processor/event_processor_factory.node.spec.ts +++ b/lib/event_processor/event_processor_factory.node.spec.ts @@ -45,7 +45,6 @@ import { EVENT_STORE_PREFIX, extractEventProcessor, getForwardingEventProcessor, import { getOpaqueBatchEventProcessor } from './event_processor_factory'; import { AsyncStore, AsyncPrefixStore, SyncStore, SyncPrefixStore } from '../utils/cache/store'; import { AsyncStorageCache } from '../utils/cache/async_storage_cache.react_native'; -import { get } from 'http'; describe('createForwardingEventProcessor', () => { const mockGetForwardingEventProcessor = vi.mocked(getForwardingEventProcessor); diff --git a/lib/event_processor/event_processor_factory.react_native.spec.ts b/lib/event_processor/event_processor_factory.react_native.spec.ts index cad875830..630417a5e 100644 --- a/lib/event_processor/event_processor_factory.react_native.spec.ts +++ b/lib/event_processor/event_processor_factory.react_native.spec.ts @@ -71,7 +71,6 @@ import { getOpaqueBatchEventProcessor } from './event_processor_factory'; import { AsyncStore, AsyncPrefixStore, SyncStore, SyncPrefixStore } from '../utils/cache/store'; import { AsyncStorageCache } from '../utils/cache/async_storage_cache.react_native'; import { MODULE_NOT_FOUND_REACT_NATIVE_ASYNC_STORAGE } from '../utils/import.react_native/@react-native-async-storage/async-storage'; -import { get } from 'http'; describe('createForwardingEventProcessor', () => { const mockGetForwardingEventProcessor = vi.mocked(getForwardingEventProcessor); diff --git a/lib/vuid/vuid_manager.spec.ts b/lib/vuid/vuid_manager.spec.ts index 5a4713d68..3cfb2a608 100644 --- a/lib/vuid/vuid_manager.spec.ts +++ b/lib/vuid/vuid_manager.spec.ts @@ -22,7 +22,7 @@ import { getMockAsyncCache } from '../tests/mock/mock_cache'; import { isVuid } from './vuid'; import { resolvablePromise } from '../utils/promise/resolvablePromise'; import { exhaustMicrotasks } from '../tests/testUtils'; -import { get } from 'http'; + const vuidCacheKey = 'optimizely-vuid'; From 7e839e4eb21706deda9fedc2df2d96c0b6457c39 Mon Sep 17 00:00:00 2001 From: Raju Ahmed Date: Wed, 21 May 2025 23:26:53 +0600 Subject: [PATCH 3/3] more validation --- .../event_processor_factory.spec.ts | 53 +++++++++++++++++++ .../event_processor_factory.ts | 24 +++++++++ lib/odp/odp_manager_factory.ts | 1 - 3 files changed, 77 insertions(+), 1 deletion(-) diff --git a/lib/event_processor/event_processor_factory.spec.ts b/lib/event_processor/event_processor_factory.spec.ts index de9ad8d41..31b1b62ed 100644 --- a/lib/event_processor/event_processor_factory.spec.ts +++ b/lib/event_processor/event_processor_factory.spec.ts @@ -73,6 +73,59 @@ describe('getBatchEventProcessor', () => { })).toThrow('Invalid event dispatcher'); }); + it('should throw and error if provided event store is invalid', () => { + expect(() => getBatchEventProcessor({ + eventDispatcher: getMockEventDispatcher(), + defaultFlushInterval: 10000, + defaultBatchSize: 10, + eventStore: 'abc' as any, + })).toThrow('Invalid event store'); + + expect(() => getBatchEventProcessor({ + eventDispatcher: getMockEventDispatcher(), + defaultFlushInterval: 10000, + defaultBatchSize: 10, + eventStore: 123 as any, + })).toThrow('Invalid event store'); + + expect(() => getBatchEventProcessor({ + eventDispatcher: getMockEventDispatcher(), + defaultFlushInterval: 10000, + defaultBatchSize: 10, + eventStore: {} as any, + })).toThrow('Invalid store method set, Invalid store method get, Invalid store method remove, Invalid store method getKeys'); + + expect(() => getBatchEventProcessor({ + eventDispatcher: getMockEventDispatcher(), + defaultFlushInterval: 10000, + defaultBatchSize: 10, + eventStore: { set: 'abc', get: 'abc', remove: 'abc', getKeys: 'abc' } as any, + })).toThrow('Invalid store method set, Invalid store method get, Invalid store method remove, Invalid store method getKeys'); + + const noop = () => {}; + + expect(() => getBatchEventProcessor({ + eventDispatcher: getMockEventDispatcher(), + defaultFlushInterval: 10000, + defaultBatchSize: 10, + eventStore: { set: noop, get: 'abc' } as any, + })).toThrow('Invalid store method get, Invalid store method remove, Invalid store method getKeys'); + + expect(() => getBatchEventProcessor({ + eventDispatcher: getMockEventDispatcher(), + defaultFlushInterval: 10000, + defaultBatchSize: 10, + eventStore: { set: noop, get: noop, remove: 'abc' } as any, + })).toThrow('Invalid store method remove, Invalid store method getKeys'); + + expect(() => getBatchEventProcessor({ + eventDispatcher: getMockEventDispatcher(), + defaultFlushInterval: 10000, + defaultBatchSize: 10, + eventStore: { set: noop, get: noop, remove: noop, getKeys: 'abc' } as any, + })).toThrow('Invalid store method getKeys'); + }); + it('returns an instane of BatchEventProcessor if no subclass constructor is provided', () => { const options = { eventDispatcher: getMockEventDispatcher(), diff --git a/lib/event_processor/event_processor_factory.ts b/lib/event_processor/event_processor_factory.ts index 16ae6fe08..dd50c72f2 100644 --- a/lib/event_processor/event_processor_factory.ts +++ b/lib/event_processor/event_processor_factory.ts @@ -29,6 +29,9 @@ export const INVALID_EVENT_DISPATCHER = 'Invalid event dispatcher'; export const FAILED_EVENT_RETRY_INTERVAL = 20 * 1000; export const EVENT_STORE_PREFIX = 'optly_event:'; +export const INVALID_STORE = 'Invalid event store'; +export const INVALID_STORE_METHOD = 'Invalid store method %s'; + export const getPrefixEventStore = (store: Store): Store => { if (store.operation === 'async') { return new AsyncPrefixStore( @@ -81,6 +84,23 @@ export const validateEventDispatcher = (eventDispatcher: EventDispatcher): void } } +const validateStore = (store: any) => { + const errors = []; + if (!store || typeof store !== 'object') { + throw new Error(INVALID_STORE); + } + + for (const method of ['set', 'get', 'remove', 'getKeys']) { + if (typeof store[method] !== 'function') { + errors.push(INVALID_STORE_METHOD.replace('%s', method)); + } + } + + if (errors.length > 0) { + throw new Error(errors.join(', ')); + } +} + export const getBatchEventProcessor = ( options: BatchEventProcessorFactoryOptions, EventProcessorConstructor: typeof BatchEventProcessor = BatchEventProcessor @@ -92,6 +112,10 @@ export const getBatchEventProcessor = ( validateEventDispatcher(closingEventDispatcher); } + if (eventStore) { + validateStore(eventStore); + } + const retryConfig: RetryConfig | undefined = retryOptions ? { maxRetries: retryOptions.maxRetries, backoffProvider: () => { diff --git a/lib/odp/odp_manager_factory.ts b/lib/odp/odp_manager_factory.ts index d5178227e..9fd689964 100644 --- a/lib/odp/odp_manager_factory.ts +++ b/lib/odp/odp_manager_factory.ts @@ -67,7 +67,6 @@ export type OdpManagerFactoryOptions = Omit { const errors = []; if (!cache || typeof cache !== 'object') { - console.log('what ', cache, typeof cache); throw new Error(INVALID_CACHE); }