diff --git a/.github/workflows/master.yml b/.github/workflows/master.yml index 8e15fc6..2aaee93 100644 --- a/.github/workflows/master.yml +++ b/.github/workflows/master.yml @@ -18,10 +18,10 @@ jobs: with: fetch-depth: 0 - - name: Setup Deno v1.43.1 + - name: Setup Deno v1.43.3 uses: denoland/setup-deno@v1 with: - deno-version: v1.43.1 + deno-version: v1.43.3 - name: Setup LCOV run: sudo apt install -y lcov diff --git a/README.md b/README.md index b1fd0da..ae9bd0a 100644 --- a/README.md +++ b/README.md @@ -34,7 +34,7 @@ https://github.com/switcherapi/switcher-api ## Module initialization The context properties stores all information regarding connectivity. -(*) Requires Deno 1.4x or higher +(*) Requires Deno 1.4x or higher (use `--unstable` flag for Deno lower than 1.4x) > Flags required ``` @@ -54,11 +54,11 @@ const domain = 'My Domain'; const component = 'MyApp'; ``` -- **url**: Swither-API url. -- **apiKey**: Switcher-API key generated to your component. -- **environment**: (optional) Environment name. Production environment is named as 'default'. - **domain**: Domain name. -- **component**: Application name. +- **url**: (optional) Swither-API endpoint. +- **apiKey**: (optional) Switcher-API key generated to your component. +- **component**: (optional) Application name. +- **environment**: (optional) Environment name. Production environment is named as 'default'. ## Options You can also activate features such as local and silent mode: @@ -71,15 +71,15 @@ const snapshotAutoUpdateInterval = 3; const silentMode = '5m'; const certPath = './certs/ca.pem'; -Switcher.buildContext({ url, apiKey, domain, component, environment }, { +Client.buildContext({ url, apiKey, domain, component, environment }, { local, logger, snapshotLocation, snapshotAutoUpdateInterval, silentMode, certPath }); -const switcher = Switcher.factory(); +const switcher = Client.getSwitcher(); ``` - **local**: If activated, the client will only fetch the configuration inside your snapshot file. The default value is 'false'. -- **logger**: If activated, it is possible to retrieve the last results from a given Switcher key using Switcher.getLogger('KEY') +- **logger**: If activated, it is possible to retrieve the last results from a given Switcher key using Client.getLogger('KEY') - **snapshotLocation**: Location of snapshot files. - **snapshotAutoUpdateInterval**: Enable Snapshot Auto Update given an interval in seconds (default: 0 disabled). - **silentMode**: Enable contigency given the time for the client to retry - e.g. 5s (s: seconds - m: minutes - h: hours) @@ -99,7 +99,7 @@ Here are some examples: Invoking the API can be done by instantiating the switcher and calling *isItOn* passing its key as a parameter. ```ts -const switcher = Switcher.factory(); +const switcher = Client.getSwitcher(); await switcher.isItOn('FEATURE01') as boolean; // or const { result, reason, metadata } = await switcher.detail().isItOn('FEATURE01') as ResultDetail; @@ -137,7 +137,7 @@ Throttling is useful when placing Feature Flags at critical code blocks require API calls will happen asynchronously and the result returned is based on the last API response. ```ts -const switcher = Switcher.factory(); +const switcher = Client.getSwitcher(); await switcher .throttle(1000) .isItOn('FEATURE01'); @@ -146,7 +146,7 @@ await switcher In order to capture issues that may occur during the process, it is possible to log the error by subscribing to the error events. ```js -Switcher.subscribeNotifyError((error) => { +Client.subscribeNotifyError((error) => { console.log(error); }); ``` @@ -156,21 +156,21 @@ Forcing Switchers to resolve remotely can help you define exclusive features tha This feature is ideal if you want to run the SDK in local mode but still want to resolve a specific switcher remotely. ```ts -const switcher = Switcher.factory(); +const switcher = Client.getSwitcher(); await switcher.remote().isItOn('FEATURE01'); ``` ## Built-in mock feature -You can also bypass your switcher configuration by invoking 'Switcher.assume'. This is perfect for your test code where you want to test both scenarios when the switcher is true and false. +You can also bypass your switcher configuration by invoking 'Client.assume'. This is perfect for your test code where you want to test both scenarios when the switcher is true and false. ```ts -Switcher.assume('FEATURE01').true(); +Client.assume('FEATURE01').true(); switcher.isItOn('FEATURE01'); // true -Switcher.forget('FEATURE01'); +Client.forget('FEATURE01'); switcher.isItOn('FEATURE01'); // Now, it's going to return the result retrieved from the API or the Snaopshot file -Switcher.assume('FEATURE01').false().withMetadata({ message: 'Feature is disabled' }); // Include metadata to emulate Relay response +Client.assume('FEATURE01').false().withMetadata({ message: 'Feature is disabled' }); // Include metadata to emulate Relay response const response = await switcher.detail().isItOn('FEATURE01') as ResultDetail; // false console.log(response.metadata.message); // Feature is disabled ``` @@ -181,7 +181,7 @@ It prevents the Switcher Client from locking snapshot files even after the test To enable this feature, it is recommended to place the following on your test setup files: ```ts -Switcher.testMode(); +Client.testMode(); ``` **Smoke Test** @@ -191,7 +191,7 @@ Switcher Keys may not be configured correctly and can cause your code to have un This feature will validate using the context provided to check if everything is properly configured. In case something is missing, this operation will throw an exception pointing out which Switcher Keys are not configured. ```ts -await Switcher.checkSwitchers(['FEATURE01', 'FEATURE02']) +await Client.checkSwitchers(['FEATURE01', 'FEATURE02']) ``` ## Loading Snapshot from the API @@ -200,14 +200,14 @@ Activate watchSnapshot optionally passing true in the arguments.
Auto load Snapshot from API passing true as second argument. ```ts -const version = await Switcher.loadSnapshot(); +const version = await Client.loadSnapshot(); ``` ## Watch for Snapshot file changes Activate and monitor snapshot changes using this feature. Optionally, you can implement any action based on the callback response. ```ts -Switcher.watchSnapshot( +Client.watchSnapshot( () => console.log('In-memory snapshot updated'), (err: any) => console.log(err)); ``` @@ -216,7 +216,7 @@ Switcher.watchSnapshot( For convenience, an implementation of a domain version checker is available if you have external processes that manage snapshot files. ```ts -Switcher.checkSnapshot(); +Client.checkSnapshot(); ``` ## Snapshot Update Scheduler @@ -224,5 +224,5 @@ You can also schedule a snapshot update using the method below.
It allows you to run the Client SDK in local mode (zero latency) and still have the snapshot updated automatically. ```ts -Switcher.scheduleSnapshotAutoUpdate(1 * 60 * 60 * 24); // 24 hours +Client.scheduleSnapshotAutoUpdate(1 * 60 * 60 * 24); // 24 hours ``` \ No newline at end of file diff --git a/deno.jsonc b/deno.jsonc index f33d489..cf83bb8 100644 --- a/deno.jsonc +++ b/deno.jsonc @@ -1,6 +1,6 @@ { "name": "@switcherapi/switcher-client-deno", - "version": "1.2.0", + "version": "2.0.0", "description": "Switcher4Deno is a Feature Flag Deno SDK client for Switcher API", "tasks": { "cache-reload": "deno cache --reload --lock=deno.lock --lock-write mod.ts", diff --git a/mod.ts b/mod.ts index bff3329..225162b 100644 --- a/mod.ts +++ b/mod.ts @@ -4,14 +4,15 @@ * Switcher Clinet SDK for working with Switcher API * * ```ts - * Switcher.buildContext({ url, apiKey, domain, component, environment }); + * Client.buildContext({ url, apiKey, domain, component, environment }); * - * const switcher = Switcher.factory(); + * const switcher = Client.getSwitcher(); * await switcher.isItOn('SWITCHER_KEY')); * ``` * * @module */ -export { Switcher } from './src/switcher-client.ts'; +export { Switcher } from './src/switcher.ts'; +export { Client } from './src/client.ts'; export type { ResultDetail, SwitcherContext, SwitcherOptions } from './src/types/index.d.ts'; diff --git a/sonar-project.properties b/sonar-project.properties index a0482a3..4ae0b93 100644 --- a/sonar-project.properties +++ b/sonar-project.properties @@ -1,7 +1,7 @@ sonar.projectKey=switcherapi_switcher-client-deno sonar.projectName=switcher-client-deno sonar.organization=switcherapi -sonar.projectVersion=1.2.0 +sonar.projectVersion=2.0.0 sonar.javascript.lcov.reportPaths=coverage/report.lcov diff --git a/src/client.ts b/src/client.ts new file mode 100644 index 0000000..f0cee03 --- /dev/null +++ b/src/client.ts @@ -0,0 +1,355 @@ +import * as remote from './lib/remote.ts'; +import * as util from './lib/utils/index.ts'; +import Bypasser from './lib/bypasser/index.ts'; +import { + DEFAULT_ENVIRONMENT, + DEFAULT_LOCAL, + DEFAULT_LOGGER, + DEFAULT_REGEX_MAX_BLACKLISTED, + DEFAULT_REGEX_MAX_TIME_LIMIT, + DEFAULT_TEST_MODE, + SWITCHER_OPTIONS, +} from './lib/constants.ts'; +import type { Snapshot, SwitcherContext, SwitcherOptions } from './types/index.d.ts'; +import type Key from './lib/bypasser/key.ts'; +import TimedMatch from './lib/utils/timed-match/index.ts'; +import ExecutionLogger from './lib/utils/executionLogger.ts'; +import SnapshotAutoUpdater from './lib/utils/snapshotAutoUpdater.ts'; +import { SnapshotNotFoundError } from './lib/exceptions/index.ts'; +import { checkSwitchersLocal, loadDomain, validateSnapshot } from './lib/snapshot.ts'; +import { Switcher } from './switcher.ts'; +import { Auth } from './lib/remote-auth.ts'; + +/** + * Quick start with the following 3 steps. + * + * 1. Use Client.buildContext() to define the arguments to connect to the API. + * 2. Use Client.getSwitcher() to create a new instance of Switcher. + * 3. Use the instance created to call isItOn to query the API. + */ +export class Client { + private static _testEnabled = DEFAULT_TEST_MODE; + private static _watching = false; + private static _watcher: Deno.FsWatcher; + private static _watchDebounce = new Map(); + + private static _snapshot?: Snapshot; + private static _context: SwitcherContext; + private static _options: SwitcherOptions; + + /** + * Create client context to be used by Switcher + */ + static buildContext(context: SwitcherContext, options?: SwitcherOptions) { + this._testEnabled = DEFAULT_TEST_MODE; + + this._snapshot = undefined; + this._context = context; + this._context.environment = util.get(context.environment, DEFAULT_ENVIRONMENT); + + // Default values + this._options = { + snapshotAutoUpdateInterval: 0, + snapshotLocation: options?.snapshotLocation, + local: util.get(options?.local, DEFAULT_LOCAL), + logger: util.get(options?.logger, DEFAULT_LOGGER), + }; + + if (options) { + Client.buildOptions(options); + } + + // Initialize Auth + Auth.init(this._context); + } + + private static buildOptions(options: SwitcherOptions) { + if (SWITCHER_OPTIONS.CERT_PATH in options && options.certPath) { + remote.setCerts(options.certPath); + } + + if (SWITCHER_OPTIONS.SILENT_MODE in options && options.silentMode) { + this._initSilentMode(options.silentMode); + } + + if (SWITCHER_OPTIONS.SNAPSHOT_AUTO_UPDATE_INTERVAL in options) { + this._options.snapshotAutoUpdateInterval = options.snapshotAutoUpdateInterval; + this.scheduleSnapshotAutoUpdate(); + } + + this._initTimedMatch(options); + } + + private static _initSilentMode(silentMode: string) { + Auth.setRetryOptions(silentMode); + + this._options.silentMode = silentMode; + this.loadSnapshot(); + } + + private static _initTimedMatch(options: SwitcherOptions) { + if (SWITCHER_OPTIONS.REGEX_MAX_BLACK_LIST in options) { + TimedMatch.setMaxBlackListed(util.get(options.regexMaxBlackList, DEFAULT_REGEX_MAX_BLACKLISTED)); + } + + if (SWITCHER_OPTIONS.REGEX_MAX_TIME_LIMIT in options) { + TimedMatch.setMaxTimeLimit(util.get(options.regexMaxTimeLimit, DEFAULT_REGEX_MAX_TIME_LIMIT)); + } + + const hasRegexSafeOption = SWITCHER_OPTIONS.REGEX_SAFE in options; + if (!hasRegexSafeOption || (hasRegexSafeOption && options.regexSafe)) { + TimedMatch.initializeWorker(); + } + } + + /** + * Creates a new instance of Switcher + */ + static getSwitcher(key?: string): Switcher { + return new Switcher(util.get(key, '')); + } + + /** + * Verifies if the current snapshot file is updated. + * + * Return true if an update has been made. + */ + static async checkSnapshot(): Promise { + if (!Client._snapshot) { + throw new SnapshotNotFoundError('Snapshot is not loaded. Use Client.loadSnapshot()'); + } + + if (Auth.isTokenExpired()) { + await Auth.auth(); + } + + const snapshot = await validateSnapshot( + util.get(Client._context.domain, ''), + util.get(Client._context.environment, DEFAULT_ENVIRONMENT), + util.get(Client._context.component, ''), + Client._snapshot.data.domain.version, + ); + + if (snapshot) { + if (Client._options.snapshotLocation?.length) { + Deno.writeTextFileSync( + `${Client._options.snapshotLocation}/${Client._context.environment}.json`, + snapshot, + ); + } + + Client._snapshot = JSON.parse(snapshot); + return true; + } + + return false; + } + + /** + * Read snapshot and load it into memory + * + * @param watchSnapshot when true, it will watch for snapshot file modifications + * @param fetchRemote when true, it will initialize the snapshot from the API + */ + static async loadSnapshot( + watchSnapshot = false, + fetchRemote = false, + ): Promise { + Client._snapshot = loadDomain( + util.get(Client._options.snapshotLocation, ''), + util.get(Client._context.environment, DEFAULT_ENVIRONMENT), + ); + + if ( + Client._snapshot?.data.domain.version == 0 && + (fetchRemote || !Client._options.local) + ) { + await Client.checkSnapshot(); + } + + if (watchSnapshot) { + Client.watchSnapshot(); + } + + return Client._snapshot?.data.domain.version || 0; + } + + /** + * Start watching snapshot files for modifications + * + * @param success when snapshot has successfully updated + * @param error when any error has thrown when attempting to load snapshot + */ + static async watchSnapshot( + success: () => void | Promise = () => {}, + error: (err: Error) => void = () => {}, + ): Promise { + if (Client._testEnabled || !Client._options.snapshotLocation?.length) { + return error(new Error('Watch Snapshot cannot be used in test mode or without a snapshot location')); + } + + const snapshotFile = `${Client._options.snapshotLocation}/${Client._context.environment}.json`; + Client._watcher = Deno.watchFs(snapshotFile); + Client._watching = true; + for await (const event of Client._watcher) { + const dataString = JSON.stringify(event); + if (Client._watchDebounce.has(dataString)) { + clearTimeout(Client._watchDebounce.get(dataString)); + Client._watchDebounce.delete(dataString); + } + + Client._watchDebounce.set( + dataString, + setTimeout(() => Client._onModifySnapshot(dataString, event, success, error), 20), + ); + } + } + + private static _onModifySnapshot( + dataString: string, + event: Deno.FsEvent, + success: () => void | Promise, + error: (err: Error) => void, + ) { + Client._watchDebounce.delete(dataString); + if (event.kind === 'modify') { + try { + Client._snapshot = loadDomain( + util.get(Client._options.snapshotLocation, ''), + util.get(Client._context.environment, DEFAULT_ENVIRONMENT), + ); + + success(); + } catch (err) { + error(err); + } + } + } + + /** + * Terminate watching snapshot files + */ + static unloadSnapshot() { + if (Client._testEnabled) { + return; + } + + Client._snapshot = undefined; + if (Client._watcher && Client._watching) { + Client._watching = false; + Client._watcher.close(); + } + } + + /** + * Schedule Snapshot auto update. + * + * It can also be configured using SwitcherOptions 'snapshotAutoUpdateInterval' when + * building context + * + * @param interval in ms + */ + static scheduleSnapshotAutoUpdate( + interval?: number, + success?: (updated: boolean) => void, + reject?: (err: Error) => void, + ) { + if (interval) { + Client._options.snapshotAutoUpdateInterval = interval; + } + + if (Client._options.snapshotAutoUpdateInterval && Client._options.snapshotAutoUpdateInterval > 0) { + SnapshotAutoUpdater.schedule(Client._options.snapshotAutoUpdateInterval, this.checkSnapshot, success, reject); + } + } + + /** + * Terminates Snapshot Auto Update + */ + static terminateSnapshotAutoUpdate() { + SnapshotAutoUpdater.terminate(); + } + + /** + * Verifies if switchers are properly configured + * + * @param switcherKeys Client Keys + * @throws when one or more Client Keys were not found + */ + static async checkSwitchers(switcherKeys: string[]) { + if (Client._options.local && Client._snapshot) { + checkSwitchersLocal(Client._snapshot, switcherKeys); + } else { + await Client.checkSwitchersRemote(switcherKeys); + } + } + + private static async checkSwitchersRemote(switcherKeys: string[]) { + try { + await Auth.auth(); + await remote.checkSwitchers(switcherKeys); + } catch (err) { + if (Client._options.silentMode && Client._snapshot) { + checkSwitchersLocal(Client._snapshot, switcherKeys); + } else { + throw err; + } + } + } + + /** + * Force a switcher value to return a given value by calling one of both methods - true() false() + */ + static assume(key: string): Key { + return Bypasser.assume(key); + } + + /** + * Remove forced value from a switcher + */ + static forget(key: string): void { + return Bypasser.forget(key); + } + + /** + * Subscribe to notify when an asynchronous error is thrown. + * + * It is usually used when throttle and silent mode are enabled. + * + * @param callback function to be called when an error is thrown + */ + static subscribeNotifyError(callback: (err: Error) => void) { + ExecutionLogger.subscribeNotifyError(callback); + } + + /** + * Retrieve execution log given a switcher key + */ + static getLogger(key: string): ExecutionLogger[] { + return ExecutionLogger.getByKey(key); + } + + /** + * Clear all results from the execution log + */ + static clearLogger(): void { + ExecutionLogger.clearLogger(); + } + + /** + * Enable/Disable test mode. + * + * It prevents from watching Snapshots that may hold process + */ + static testMode(testEnabled: boolean = true): void { + Client._testEnabled = testEnabled; + } + + static get options(): SwitcherOptions { + return Client._options; + } + + static get snapshot(): Snapshot | undefined { + return Client._snapshot; + } +} diff --git a/src/lib/remote-auth.ts b/src/lib/remote-auth.ts new file mode 100644 index 0000000..bea2f02 --- /dev/null +++ b/src/lib/remote-auth.ts @@ -0,0 +1,91 @@ +import type { RetryOptions, SwitcherContext } from '../types/index.d.ts'; +import { auth, checkAPIHealth } from './remote.ts'; +import DateMoment from './utils/datemoment.ts'; +import * as util from './utils/index.ts'; + +/** + * Auth handles the authentication and API connectivity. + */ +export class Auth { + private static context: SwitcherContext; + private static retryOptions: RetryOptions; + private static token?: string; + private static exp?: number; + + static init(context: SwitcherContext) { + this.context = context; + this.token = undefined; + this.exp = undefined; + } + + static setRetryOptions(silentMode: string) { + this.retryOptions = { + retryTime: parseInt(silentMode.slice(0, -1)), + retryDurationIn: silentMode.slice(-1), + }; + } + + static async auth() { + const response = await auth(this.context); + this.token = response.token; + this.exp = response.exp; + } + + static checkHealth() { + if (this.token !== 'SILENT') { + return; + } + + if (this.isTokenExpired()) { + this.updateSilentToken(); + checkAPIHealth(util.get(this.getURL(), '')) + .then((isAlive) => { + if (isAlive) { + this.auth(); + } + }); + } + } + + static updateSilentToken() { + const expirationTime = new DateMoment(new Date()) + .add(this.retryOptions.retryTime, this.retryOptions.retryDurationIn).getDate(); + + this.token = 'SILENT'; + this.exp = Math.round(expirationTime.getTime() / 1000); + } + + static isTokenExpired() { + return !this.exp || Date.now() > (this.exp * 1000); + } + + static isValid() { + const errors = []; + + if (!this.context.url) { + errors.push('URL is required'); + } + + if (!this.context.component) { + errors.push('Component is required'); + } + + if (!this.context.apiKey) { + errors.push('API Key is required'); + } + + if (errors.length) { + throw new Error(`Something went wrong: ${errors.join(', ')}`); + } + + return true; + } + + static getToken() { + return this.token; + } + + static getURL() { + return this.context.url; + } +} diff --git a/src/lib/remote.ts b/src/lib/remote.ts index 38734e0..2c4393d 100644 --- a/src/lib/remote.ts +++ b/src/lib/remote.ts @@ -6,6 +6,8 @@ import type { ResultDetail, SwitcherContext, } from '../types/index.d.ts'; +import { Auth } from './remote-auth.ts'; +import * as util from './utils/index.ts'; let httpClient: Deno.HttpClient; @@ -41,6 +43,32 @@ export const getEntry = (input?: string[][]) => { return entry; }; +export const auth = async (context: SwitcherContext) => { + try { + const response = await fetch(`${context.url}/criteria/auth`, { + client: httpClient, + method: 'post', + body: JSON.stringify({ + domain: context.domain, + component: context.component, + environment: context.environment, + }), + headers: { + 'switcher-api-key': util.get(context.apiKey, ''), + 'Content-Type': 'application/json', + }, + }); + + if (response.status == 200) { + return response.json() as Promise; + } + + throw new Error(`[auth] failed with status ${response.status}`); + } catch (e) { + throw new AuthError(e.errno ? getConnectivityError(e.errno) : e.message); + } +}; + export const checkAPIHealth = async (url: string) => { try { const response = await fetch(`${url}/check`, { client: httpClient, method: 'get' }); @@ -51,7 +79,6 @@ export const checkAPIHealth = async (url: string) => { }; export const checkCriteria = async ( - context: SwitcherContext, key?: string, input?: string[][], showDetail = false, @@ -59,12 +86,12 @@ export const checkCriteria = async ( try { const entry = getEntry(input); const response = await fetch( - `${context.url}/criteria?showReason=${showDetail}&key=${key}`, + `${Auth.getURL()}/criteria?showReason=${showDetail}&key=${key}`, { client: httpClient, method: 'post', body: JSON.stringify({ entry }), - headers: getHeader(context.token), + headers: getHeader(Auth.getToken()), }, ); @@ -80,43 +107,15 @@ export const checkCriteria = async ( } }; -export const auth = async (context: SwitcherContext) => { - try { - const response = await fetch(`${context.url}/criteria/auth`, { - client: httpClient, - method: 'post', - body: JSON.stringify({ - domain: context.domain, - component: context.component, - environment: context.environment, - }), - headers: { - 'switcher-api-key': context.apiKey || '', - 'Content-Type': 'application/json', - }, - }); - - if (response.status == 200) { - return response.json() as Promise; - } - - throw new Error(`[auth] failed with status ${response.status}`); - } catch (e) { - throw new AuthError(e.errno ? getConnectivityError(e.errno) : e.message); - } -}; - export const checkSwitchers = async ( - url: string, - token: string | undefined, switcherKeys: string[], ) => { try { - const response = await fetch(`${url}/criteria/switchers_check`, { + const response = await fetch(`${Auth.getURL()}/criteria/switchers_check`, { client: httpClient, method: 'post', body: JSON.stringify({ switchers: switcherKeys }), - headers: getHeader(token), + headers: getHeader(Auth.getToken()), }); if (response.status != 200) { @@ -139,15 +138,13 @@ export const checkSwitchers = async ( }; export const checkSnapshotVersion = async ( - url: string, - token: string, version: number, ) => { try { - const response = await fetch(`${url}/criteria/snapshot_check/${version}`, { + const response = await fetch(`${Auth.getURL()}/criteria/snapshot_check/${version}`, { client: httpClient, method: 'get', - headers: getHeader(token), + headers: getHeader(Auth.getToken()), }); if (response.status == 200) { @@ -163,8 +160,6 @@ export const checkSnapshotVersion = async ( }; export const resolveSnapshot = async ( - url: string, - token: string, domain: string, environment: string, component: string, @@ -185,11 +180,11 @@ export const resolveSnapshot = async ( }; try { - const response = await fetch(`${url}/graphql`, { + const response = await fetch(`${Auth.getURL()}/graphql`, { client: httpClient, method: 'post', body: JSON.stringify(data), - headers: getHeader(token), + headers: getHeader(Auth.getToken()), }); if (response.status == 200) { diff --git a/src/lib/resolver.ts b/src/lib/resolver.ts index 6e1a275..09aa7ac 100644 --- a/src/lib/resolver.ts +++ b/src/lib/resolver.ts @@ -82,13 +82,13 @@ async function checkConfig(group: Group, config: Config, input?: string[][]) { } if (config.strategies) { - return await checkStrategy(config, input || []); + return await checkStrategy(config, input); } return true; } -async function checkStrategy(config: Config, input: string[][]) { +async function checkStrategy(config: Config, input?: string[][]) { const { strategies } = config; const entry = getEntry(input); @@ -133,7 +133,7 @@ export default async function checkCriteriaLocal( ) { if (!snapshot) { throw new Error( - "Snapshot not loaded. Try to use 'Switcher.loadSnapshot()'", + "Snapshot not loaded. Try to use 'Client.loadSnapshot()'", ); } diff --git a/src/lib/snapshot.ts b/src/lib/snapshot.ts index c0f0a43..d57ada8 100644 --- a/src/lib/snapshot.ts +++ b/src/lib/snapshot.ts @@ -40,17 +40,15 @@ export const loadDomain = (snapshotLocation: string, environment: string) => { }; export const validateSnapshot = async ( - url: string, - token: string, domain: string, environment: string, component: string, snapshotVersion: number, ) => { - const { status } = await checkSnapshotVersion(url, token, snapshotVersion); + const { status } = await checkSnapshotVersion(snapshotVersion); if (!status) { - const snapshot = await resolveSnapshot(url, token, domain, environment, component); + const snapshot = await resolveSnapshot(domain, environment, component); return snapshot; } diff --git a/src/lib/utils/index.ts b/src/lib/utils/index.ts new file mode 100644 index 0000000..affc419 --- /dev/null +++ b/src/lib/utils/index.ts @@ -0,0 +1,3 @@ +export function get(value: T | undefined, defaultValue: T): T { + return value ?? defaultValue; +} diff --git a/src/switcher-client.ts b/src/switcher-client.ts deleted file mode 100644 index b84854b..0000000 --- a/src/switcher-client.ts +++ /dev/null @@ -1,700 +0,0 @@ -import Bypasser from './lib/bypasser/index.ts'; -import ExecutionLogger from './lib/utils/executionLogger.ts'; -import DateMoment from './lib/utils/datemoment.ts'; -import TimedMatch from './lib/utils/timed-match/index.ts'; -import SnapshotAutoUpdater from './lib/utils/snapshotAutoUpdater.ts'; -import { checkSwitchersLocal, loadDomain, StrategiesType, validateSnapshot } from './lib/snapshot.ts'; -import * as remote from './lib/remote.ts'; -import checkCriteriaLocal from './lib/resolver.ts'; -import type { ResultDetail, RetryOptions, Snapshot, SwitcherContext, SwitcherOptions } from './types/index.d.ts'; -import type Key from './lib/bypasser/key.ts'; -import { SnapshotNotFoundError } from './lib/exceptions/index.ts'; -import { - DEFAULT_ENVIRONMENT, - DEFAULT_LOCAL, - DEFAULT_LOGGER, - DEFAULT_REGEX_MAX_BLACKLISTED, - DEFAULT_REGEX_MAX_TIME_LIMIT, - DEFAULT_TEST_MODE, - SWITCHER_OPTIONS, -} from './lib/constants.ts'; - -/** - * Quick start with the following 3 steps. - * - * 1. Use Switcher.buildContext() to define the arguments to connect to the API. - * 2. Use Switcher.factory() to create a new instance of Switcher. - * 3. Use the instance created to call isItOn to query the API. - */ -export class Switcher { - private static _testEnabled = DEFAULT_TEST_MODE; - private static _watching = false; - private static _watcher: Deno.FsWatcher; - private static _watchDebounce = new Map(); - - private static _snapshot?: Snapshot; - private static _context: SwitcherContext; - private static _options: SwitcherOptions; - private static _retryOptions: RetryOptions; - - private _delay = 0; - private _nextRun = 0; - private _input?: string[][]; - private _key = ''; - private _forceRemote = false; - private _showDetail = false; - - constructor(key: string) { - this._key = key; - } - - /** - * Create the necessary configuration to communicate with the API - * - * @param context Necessary arguments - * @param options - */ - static buildContext(context: SwitcherContext, options?: SwitcherOptions) { - this._testEnabled = DEFAULT_TEST_MODE; - - this._snapshot = undefined; - this._context = context; - this._context.url = context.url; - this._context.environment = Switcher._get(context.environment, DEFAULT_ENVIRONMENT); - - // Default values - this._options = { - snapshotAutoUpdateInterval: 0, - snapshotLocation: options?.snapshotLocation, - local: Switcher._get(options?.local, DEFAULT_LOCAL), - logger: Switcher._get(options?.logger, DEFAULT_LOGGER), - }; - - if (options) { - Switcher.buildOptions(options); - } - } - - private static buildOptions(options: SwitcherOptions) { - if (SWITCHER_OPTIONS.CERT_PATH in options && options.certPath) { - remote.setCerts(options.certPath); - } - - if (SWITCHER_OPTIONS.SILENT_MODE in options && options.silentMode) { - this._initSilentMode(options.silentMode); - } - - if (SWITCHER_OPTIONS.SNAPSHOT_AUTO_UPDATE_INTERVAL in options) { - this._options.snapshotAutoUpdateInterval = options.snapshotAutoUpdateInterval; - this.scheduleSnapshotAutoUpdate(); - } - - this._initTimedMatch(options); - } - - /** - * Creates a new instance of Switcher - */ - static factory(key?: string): Switcher { - return new Switcher(Switcher._get(key, '')); - } - - /** - * Verifies if the current snapshot file is updated. - * Return true if an update has been made. - */ - static async checkSnapshot(): Promise { - if (!Switcher._snapshot) { - throw new SnapshotNotFoundError('Snapshot is not loaded. Use Switcher.loadSnapshot()'); - } - - if ( - !Switcher._context.exp || - Date.now() > (Switcher._context.exp * 1000) - ) { - await Switcher._auth(); - } - - const snapshot = await validateSnapshot( - Switcher._get(Switcher._context.url, ''), - Switcher._get(Switcher._context.token, ''), - Switcher._get(Switcher._context.domain, ''), - Switcher._get(Switcher._context.environment, DEFAULT_ENVIRONMENT), - Switcher._get(Switcher._context.component, ''), - Switcher._snapshot.data.domain.version, - ); - - if (snapshot) { - if (Switcher._options.snapshotLocation?.length) { - Deno.writeTextFileSync( - `${Switcher._options.snapshotLocation}/${Switcher._context.environment}.json`, - snapshot, - ); - } - - Switcher._snapshot = JSON.parse(snapshot); - return true; - } - - return false; - } - - /** - * Read snapshot and load it into memory - * - * @param watchSnapshot enable watchSnapshot when true - */ - static async loadSnapshot( - watchSnapshot = false, - fetchRemote = false, - ): Promise { - Switcher._snapshot = loadDomain( - Switcher._get(Switcher._options.snapshotLocation, ''), - Switcher._get(Switcher._context.environment, DEFAULT_ENVIRONMENT), - ); - - if ( - Switcher._snapshot?.data.domain.version == 0 && - (fetchRemote || !Switcher._options.local) - ) { - await Switcher.checkSnapshot(); - } - - if (watchSnapshot) { - Switcher.watchSnapshot(); - } - - return Switcher._snapshot?.data.domain.version || 0; - } - - /** - * Start watching snapshot files for modifications - * - * @param success when snapshot has successfully updated - * @param error when any error has thrown when attempting to load snapshot - */ - static async watchSnapshot( - success: () => void | Promise = () => {}, - error: (err: Error) => void = () => {}, - ): Promise { - if (Switcher._testEnabled || !Switcher._options.snapshotLocation?.length) { - return error(new Error('Watch Snapshot cannot be used in test mode or without a snapshot location')); - } - - const snapshotFile = `${Switcher._options.snapshotLocation}/${Switcher._context.environment}.json`; - Switcher._watcher = Deno.watchFs(snapshotFile); - Switcher._watching = true; - for await (const event of Switcher._watcher) { - const dataString = JSON.stringify(event); - if (Switcher._watchDebounce.has(dataString)) { - clearTimeout(Switcher._watchDebounce.get(dataString)); - Switcher._watchDebounce.delete(dataString); - } - - Switcher._watchDebounce.set( - dataString, - setTimeout(() => Switcher._onModifySnapshot(dataString, event, success, error), 20), - ); - } - } - - /** - * Remove snapshot from real-time update - */ - static unloadSnapshot() { - if (Switcher._testEnabled) { - return; - } - - Switcher._snapshot = undefined; - if (Switcher._watcher && Switcher._watching) { - Switcher._watching = false; - Switcher._watcher.close(); - } - } - - /** - * Schedule Snapshot auto update. - * It can also be configured using SwitcherOptions 'snapshotAutoUpdateInterval' when - * building context - * - * @param interval in ms - */ - static scheduleSnapshotAutoUpdate( - interval?: number, - success?: (updated: boolean) => void, - reject?: (err: Error) => void, - ) { - if (interval) { - Switcher._options.snapshotAutoUpdateInterval = interval; - } - - if (Switcher._options.snapshotAutoUpdateInterval && Switcher._options.snapshotAutoUpdateInterval > 0) { - SnapshotAutoUpdater.schedule(Switcher._options.snapshotAutoUpdateInterval, this.checkSnapshot, success, reject); - } - } - - /** - * Terminates Snapshot Auto Update - */ - static terminateSnapshotAutoUpdate() { - SnapshotAutoUpdater.terminate(); - } - - /** - * Verifies if switchers are properly configured - * - * @param switcherKeys Switcher Keys - * @throws when one or more Switcher Keys were not found - */ - static async checkSwitchers(switcherKeys: string[]) { - if (Switcher._options.local && Switcher._snapshot) { - checkSwitchersLocal(Switcher._snapshot, switcherKeys); - } else { - await Switcher.checkSwitchersRemote(switcherKeys); - } - } - - private static async checkSwitchersRemote(switcherKeys: string[]) { - try { - await Switcher._auth(); - await remote.checkSwitchers( - Switcher._get(Switcher._context.url, ''), - Switcher._context.token, - switcherKeys, - ); - } catch (err) { - if (Switcher._options.silentMode && Switcher._snapshot) { - checkSwitchersLocal(Switcher._snapshot, switcherKeys); - } else { - throw err; - } - } - } - - private static _onModifySnapshot( - dataString: string, - event: Deno.FsEvent, - success: () => void | Promise, - error: (err: Error) => void, - ) { - Switcher._watchDebounce.delete(dataString); - if (event.kind === 'modify') { - try { - Switcher._snapshot = loadDomain( - Switcher._get(Switcher._options.snapshotLocation, ''), - Switcher._get(Switcher._context.environment, DEFAULT_ENVIRONMENT), - ); - - success(); - } catch (err) { - error(err); - } - } - } - - private static _initSilentMode(silentMode: string) { - this._retryOptions = { - retryTime: parseInt(silentMode.slice(0, -1)), - retryDurationIn: silentMode.slice(-1), - }; - - this._options.silentMode = silentMode; - this.loadSnapshot(); - } - - private static _initTimedMatch(options: SwitcherOptions) { - if (SWITCHER_OPTIONS.REGEX_MAX_BLACK_LIST in options) { - TimedMatch.setMaxBlackListed(Switcher._get(options.regexMaxBlackList, DEFAULT_REGEX_MAX_BLACKLISTED)); - } - - if (SWITCHER_OPTIONS.REGEX_MAX_TIME_LIMIT in options) { - TimedMatch.setMaxTimeLimit(Switcher._get(options.regexMaxTimeLimit, DEFAULT_REGEX_MAX_TIME_LIMIT)); - } - - const hasRegexSafeOption = SWITCHER_OPTIONS.REGEX_SAFE in options; - if (!hasRegexSafeOption || (hasRegexSafeOption && options.regexSafe)) { - TimedMatch.initializeWorker(); - } - } - - private static async _auth() { - const response = await remote.auth(Switcher._context); - Switcher._context.token = response.token; - Switcher._context.exp = response.exp; - } - - private static _checkHealth() { - if (Switcher._context.token !== 'SILENT') { - return; - } - - if (Switcher._isTokenExpired()) { - Switcher._updateSilentToken(); - remote.checkAPIHealth(Switcher._get(Switcher._context.url, '')) - .then((isAlive) => { - if (isAlive) { - Switcher._auth(); - } - }); - } - } - - private static _updateSilentToken() { - const expirationTime = new DateMoment(new Date()) - .add(Switcher._retryOptions.retryTime, Switcher._retryOptions.retryDurationIn).getDate(); - - Switcher._context.token = 'SILENT'; - Switcher._context.exp = Math.round(expirationTime.getTime() / 1000); - } - - private static _isTokenExpired() { - return !Switcher._context.exp || Date.now() > (Switcher._context.exp * 1000); - } - - private static _get(value: T | undefined, defaultValue: T): T { - return value ?? defaultValue; - } - - /** - * Force a switcher value to return a given value by calling one of both methods - true() false() - * - * @param key - */ - static assume(key: string): Key { - return Bypasser.assume(key); - } - - /** - * Remove forced value from a switcher - * - * @param key - */ - static forget(key: string): void { - return Bypasser.forget(key); - } - - /** - * Retrieve execution log given a switcher key - * - * @param key - */ - static getLogger(key: string): ExecutionLogger[] { - return ExecutionLogger.getByKey(key); - } - - /** - * Clear all results from the execution log - */ - static clearLogger(): void { - ExecutionLogger.clearLogger(); - } - - /** - * Enable/Disable test mode - * It prevents from watching Snapshots that may hold process - */ - static testMode(testEnabled: boolean = true): void { - Switcher._testEnabled = testEnabled; - } - - /** - * Pre-set input values before calling the API - * - * @param key - */ - async prepare(key: string): Promise { - this._key = key; - - if (!Switcher._options.local || this._forceRemote) { - await Switcher._auth(); - } - } - - /** - * Validate the input provided to access the API - */ - async validate(): Promise { - const errors = []; - - if (!Switcher._context.apiKey) { - errors.push('Missing API Key field'); - } - - if (!Switcher._context.component) { - errors.push('Missing component field'); - } - - if (!this._key) { - errors.push('Missing key field'); - } - - await this._executeApiValidation(); - if (!Switcher._context.token) { - errors.push('Missing token field'); - } - - if (errors.length) { - throw new Error(`Something went wrong: ${errors.join(', ')}`); - } - } - - /** - * Execute criteria - * - * @param key - */ - async isItOn(key?: string): Promise { - let result: boolean | ResultDetail; - this._validateArgs(key, this._input); - - // verify if query from Bypasser - const bypassKey = Bypasser.searchBypassed(this._key); - if (bypassKey) { - const response = bypassKey.getResponse(); - return this._showDetail ? response : response.result; - } - - // verify if query from snapshot - if (Switcher._options.local && !this._forceRemote) { - result = await this._executeLocalCriteria(); - } else { - try { - await this.validate(); - if (Switcher._context.token === 'SILENT') { - result = await this._executeLocalCriteria(); - } else { - result = await this._executeRemoteCriteria(); - } - } catch (err) { - Switcher._notifyError(err); - - if (Switcher._options.silentMode) { - Switcher._updateSilentToken(); - return this._executeLocalCriteria(); - } - - throw err; - } - } - - return result; - } - - /** - * Configure the time elapsed between each call to the API. - * Activating this option will enable loggers. - * - * @param delay in milliseconds - */ - throttle(delay: number): this { - this._delay = delay; - - if (delay > 0) { - Switcher._options.logger = true; - } - - return this; - } - - /** - * Force the use of the remote API when local is enabled - * - * @param forceRemote default true - */ - remote(forceRemote = true): this { - if (!Switcher._options.local) { - throw new Error('Local mode is not enabled'); - } - - this._forceRemote = forceRemote; - return this; - } - - /** - * When enabled, isItOn will return a ResultDetail object - */ - detail(showDetail = true): this { - this._showDetail = showDetail; - return this; - } - - /** - * Adds a strategy for validation - */ - check(startegyType: string, input: string): this { - if (!this._input) { - this._input = []; - } - - this._input.push([startegyType, input]); - return this; - } - - /** - * Adds VALUE_VALIDATION input for strategy validation - */ - checkValue(input: string): this { - return this.check(StrategiesType.VALUE, input); - } - - /** - * Adds NUMERIC_VALIDATION input for strategy validation - */ - checkNumeric(input: string): this { - return this.check(StrategiesType.NUMERIC, input); - } - - /** - * Adds NETWORK_VALIDATION input for strategy validation - */ - checkNetwork(input: string): this { - return this.check(StrategiesType.NETWORK, input); - } - - /** - * Adds DATE_VALIDATION input for strategy validation - */ - checkDate(input: string): this { - return this.check(StrategiesType.DATE, input); - } - - /** - * Adds TIME_VALIDATION input for strategy validation - */ - checkTime(input: string): this { - return this.check(StrategiesType.TIME, input); - } - - /** - * Adds REGEX_VALIDATION input for strategy validation - */ - checkRegex(input: string): this { - return this.check(StrategiesType.REGEX, input); - } - - /** - * Adds PAYLOAD_VALIDATION input for strategy validation - */ - checkPayload(input: string): this { - return this.check(StrategiesType.PAYLOAD, input); - } - - async _executeRemoteCriteria(): Promise { - let responseCriteria: ResultDetail; - - if (this._useSync()) { - responseCriteria = await remote.checkCriteria( - Switcher._context, - this._key, - this._input, - this._showDetail, - ); - - if (Switcher._options.logger && this._key) { - ExecutionLogger.add(responseCriteria, this._key, this._input); - } - } else { - responseCriteria = this._executeAsyncRemoteCriteria(); - } - - return this._showDetail ? responseCriteria : responseCriteria.result; - } - - _executeAsyncRemoteCriteria(): ResultDetail { - if (this._nextRun < Date.now()) { - this._nextRun = Date.now() + this._delay; - - if (Switcher._isTokenExpired()) { - this.prepare(this._key) - .then(() => this.executeAsyncCheckCriteria()) - .catch((err) => Switcher._notifyError(err)); - } else { - this.executeAsyncCheckCriteria(); - } - } - - const executionLog = ExecutionLogger.getExecution(this._key, this._input); - return executionLog.response; - } - - private executeAsyncCheckCriteria() { - remote.checkCriteria(Switcher._context, this._key, this._input, this._showDetail) - .then((response) => ExecutionLogger.add(response, this._key, this._input)) - .catch((err) => Switcher._notifyError(err)); - } - - private static _notifyError(err: Error) { - ExecutionLogger.notifyError(err); - } - - /** - * Subscribe to notify when an asynchronous error is thrown. - * - * It is usually used when throttle and silent mode are enabled. - * - * @param callback function to be called when an error is thrown - */ - static subscribeNotifyError(callback: (err: Error) => void) { - ExecutionLogger.subscribeNotifyError(callback); - } - - private async _executeApiValidation() { - if (!this._useSync()) { - return; - } - - Switcher._checkHealth(); - if (Switcher._isTokenExpired()) { - await this.prepare(this._key); - } - } - - async _executeLocalCriteria(): Promise< - boolean | { - result: boolean; - reason: string; - } - > { - const response = await checkCriteriaLocal( - Switcher._snapshot, - Switcher._get(this._key, ''), - Switcher._get(this._input, []), - ); - - if (Switcher._options.logger) { - ExecutionLogger.add(response, this._key, this._input); - } - - if (this._showDetail) { - return response; - } - - return response.result; - } - - private _validateArgs(key?: string, input?: string[][]) { - if (key) this._key = key; - if (input) this._input = input; - } - - private _useSync() { - return this._delay == 0 || !ExecutionLogger.getExecution(this._key, this._input); - } - - get key(): string { - return this._key; - } - - get input(): string[][] | undefined { - return this._input; - } - - get nextRun(): number { - return this._nextRun; - } - - static get snapshot(): Snapshot | undefined { - return Switcher._snapshot; - } -} diff --git a/src/switcher.ts b/src/switcher.ts new file mode 100644 index 0000000..84dfaa3 --- /dev/null +++ b/src/switcher.ts @@ -0,0 +1,299 @@ +import Bypasser from './lib/bypasser/index.ts'; +import ExecutionLogger from './lib/utils/executionLogger.ts'; +import checkCriteriaLocal from './lib/resolver.ts'; +import { StrategiesType } from './lib/snapshot.ts'; +import { Client } from './client.ts'; +import type { ResultDetail } from './types/index.d.ts'; +import * as remote from './lib/remote.ts'; +import * as util from './lib/utils/index.ts'; +import { Auth } from './lib/remote-auth.ts'; + +/** + * Switcher handles criteria execution and validations. + * + * Create a intance of Switcher using Client.getSwitcher() + */ +export class Switcher { + private _delay = 0; + private _nextRun = 0; + private _input?: string[][]; + private _key = ''; + private _forceRemote = false; + private _showDetail = false; + + constructor(key: string) { + this._validateArgs(key); + } + + /** + * Checks API credentials and connectivity + */ + async prepare(key: string): Promise { + this._validateArgs(key); + + if (!Client.options.local || this._forceRemote) { + await Auth.auth(); + } + } + + /** + * Validates client settings for remote API calls + */ + async validate(): Promise { + const errors = []; + + Auth.isValid(); + + if (!this._key) { + errors.push('Missing key field'); + } + + await this._executeApiValidation(); + if (!Auth.getToken()) { + errors.push('Missing token field'); + } + + if (errors.length) { + throw new Error(`Something went wrong: ${errors.join(', ')}`); + } + } + + /** + * Execute criteria + * + * @returns boolean or ResultDetail when detail() is used + */ + async isItOn(key?: string): Promise { + let result: boolean | ResultDetail; + this._validateArgs(key); + + // verify if query from Bypasser + const bypassKey = Bypasser.searchBypassed(this._key); + if (bypassKey) { + const response = bypassKey.getResponse(); + return this._showDetail ? response : response.result; + } + + // verify if query from snapshot + if (Client.options.local && !this._forceRemote) { + result = await this._executeLocalCriteria(); + } else { + try { + await this.validate(); + if (Auth.getToken() === 'SILENT') { + result = await this._executeLocalCriteria(); + } else { + result = await this._executeRemoteCriteria(); + } + } catch (err) { + this._notifyError(err); + + if (Client.options.silentMode) { + Auth.updateSilentToken(); + return this._executeLocalCriteria(); + } + + throw err; + } + } + + return result; + } + + /** + * Define a delay (ms) for the next async execution. + * + * Activating this option will enable logger by default + */ + throttle(delay: number): this { + this._delay = delay; + + if (delay > 0) { + Client.options.logger = true; + } + + return this; + } + + /** + * Force the use of the remote API when local is enabled + */ + remote(forceRemote = true): this { + if (!Client.options.local) { + throw new Error('Local mode is not enabled'); + } + + this._forceRemote = forceRemote; + return this; + } + + /** + * When enabled, isItOn will return a ResultDetail object + */ + detail(showDetail = true): this { + this._showDetail = showDetail; + return this; + } + + /** + * Adds a strategy for validation + */ + check(startegyType: string, input: string): this { + if (!this._input) { + this._input = []; + } + + this._input.push([startegyType, input]); + return this; + } + + /** + * Adds VALUE_VALIDATION input for strategy validation + */ + checkValue(input: string): this { + return this.check(StrategiesType.VALUE, input); + } + + /** + * Adds NUMERIC_VALIDATION input for strategy validation + */ + checkNumeric(input: string): this { + return this.check(StrategiesType.NUMERIC, input); + } + + /** + * Adds NETWORK_VALIDATION input for strategy validation + */ + checkNetwork(input: string): this { + return this.check(StrategiesType.NETWORK, input); + } + + /** + * Adds DATE_VALIDATION input for strategy validation + */ + checkDate(input: string): this { + return this.check(StrategiesType.DATE, input); + } + + /** + * Adds TIME_VALIDATION input for strategy validation + */ + checkTime(input: string): this { + return this.check(StrategiesType.TIME, input); + } + + /** + * Adds REGEX_VALIDATION input for strategy validation + */ + checkRegex(input: string): this { + return this.check(StrategiesType.REGEX, input); + } + + /** + * Adds PAYLOAD_VALIDATION input for strategy validation + */ + checkPayload(input: string): this { + return this.check(StrategiesType.PAYLOAD, input); + } + + /** + * Execute criteria from remote API + */ + async _executeRemoteCriteria(): Promise { + let responseCriteria: ResultDetail; + + if (this._useSync()) { + responseCriteria = await remote.checkCriteria( + this._key, + this._input, + this._showDetail, + ); + + if (Client.options.logger && this._key) { + ExecutionLogger.add(responseCriteria, this._key, this._input); + } + } else { + responseCriteria = this._executeAsyncRemoteCriteria(); + } + + return this._showDetail ? responseCriteria : responseCriteria.result; + } + + /** + * Execute criteria from remote API asynchronously + */ + _executeAsyncRemoteCriteria(): ResultDetail { + if (this._nextRun < Date.now()) { + this._nextRun = Date.now() + this._delay; + + if (Auth.isTokenExpired()) { + this.prepare(this._key) + .then(() => this.executeAsyncCheckCriteria()) + .catch((err) => this._notifyError(err)); + } else { + this.executeAsyncCheckCriteria(); + } + } + + const executionLog = ExecutionLogger.getExecution(this._key, this._input); + return executionLog.response; + } + + private executeAsyncCheckCriteria() { + remote.checkCriteria(this._key, this._input, this._showDetail) + .then((response) => ExecutionLogger.add(response, this._key, this._input)) + .catch((err) => this._notifyError(err)); + } + + private _notifyError(err: Error) { + ExecutionLogger.notifyError(err); + } + + private async _executeApiValidation() { + if (!this._useSync()) { + return; + } + + Auth.checkHealth(); + if (Auth.isTokenExpired()) { + await this.prepare(this._key); + } + } + + async _executeLocalCriteria(): Promise< + boolean | { + result: boolean; + reason: string; + } + > { + const response = await checkCriteriaLocal( + Client.snapshot, + util.get(this._key, ''), + util.get(this._input, []), + ); + + if (Client.options.logger) { + ExecutionLogger.add(response, this._key, this._input); + } + + if (this._showDetail) { + return response; + } + + return response.result; + } + + private _validateArgs(key?: string) { + if (key) { + this._key = key; + } + } + + private _useSync() { + return this._delay == 0 || !ExecutionLogger.getExecution(this._key, this._input); + } + + get input(): string[][] | undefined { + return this._input; + } +} diff --git a/src/types/index.d.ts b/src/types/index.d.ts index 2861c88..f179ce9 100644 --- a/src/types/index.d.ts +++ b/src/types/index.d.ts @@ -17,15 +17,13 @@ export type SwitcherContext = { domain: string; component?: string; environment?: string; - token?: string; - exp?: number; }; /** * SwitcherOptions is used to set optional settings * * @param local - When enabled it will use the local snapshot (file or in-memory) - * @param logger - When enabled it allows inspecting the result details with Switcher.getLogger(key) + * @param logger - When enabled it allows inspecting the result details with Client.getLogger(key) * @param snapshotLocation - When defined it will use file-managed snapshot * @param snapshotAutoUpdateInterval - The interval in milliseconds to auto-update the snapshot * @param silentMode - When defined it will switch to local during the specified time beofre it switches back to remote diff --git a/test/playground/index.ts b/test/playground/index.ts index 082ba13..ff50a5d 100644 --- a/test/playground/index.ts +++ b/test/playground/index.ts @@ -1,4 +1,4 @@ -import { Switcher } from '../../mod.ts' +import { Switcher, Client } from '../../mod.ts' import { sleep } from "../helper/utils.ts"; const SWITCHER_KEY = 'MY_SWITCHER'; @@ -15,8 +15,8 @@ let switcher: Switcher; * Playground environment for showcasing the API */ async function setupSwitcher(local: boolean) { - Switcher.buildContext({ url, apiKey, domain, component, environment }, { local, logger: true }); - await Switcher.loadSnapshot(false, local) + Client.buildContext({ url, apiKey, domain, component, environment }, { local, logger: true }); + await Client.loadSnapshot(false, local) .then(version => console.log('Snapshot loaded - version:', version)) .catch(() => console.log('Failed to load Snapshot')); } @@ -28,7 +28,7 @@ async function setupSwitcher(local: boolean) { * Snapshot is loaded from file at test/playground/snapshot/local.json */ const _testLocal = async () => { - Switcher.buildContext({ + Client.buildContext({ domain: 'Local Playground', environment: 'local' }, { @@ -36,11 +36,11 @@ const _testLocal = async () => { local: true }); - await Switcher.loadSnapshot() + await Client.loadSnapshot() .then(version => console.log('Snapshot loaded - version:', version)) .catch(() => console.log('Failed to load Snapshot')); - switcher = Switcher.factory(); + switcher = Client.getSwitcher(); setInterval(async () => { const time = Date.now(); @@ -56,11 +56,11 @@ const _testLocal = async () => { const _testSimpleAPICall = async (local: boolean) => { await setupSwitcher(local); - await Switcher.checkSwitchers([SWITCHER_KEY]) + await Client.checkSwitchers([SWITCHER_KEY]) .then(() => console.log('Switcher checked')) .catch(error => console.log(error)); - switcher = Switcher.factory(); + switcher = new Switcher(SWITCHER_KEY); setInterval(async () => { const time = Date.now(); @@ -73,9 +73,9 @@ const _testSimpleAPICall = async (local: boolean) => { const _testThrottledAPICall = async () => { setupSwitcher(false); - await Switcher.checkSwitchers([SWITCHER_KEY]); + await Client.checkSwitchers([SWITCHER_KEY]); - switcher = Switcher.factory(); + switcher = Client.getSwitcher(); switcher.throttle(1000); setInterval(async () => { @@ -84,7 +84,7 @@ const _testThrottledAPICall = async () => { console.log(`- ${Date.now() - time} ms - ${JSON.stringify(result)}`); }, 1000); - Switcher.unloadSnapshot(); + Client.unloadSnapshot(); }; // Requires remote API @@ -92,15 +92,15 @@ const _testSnapshotUpdate = async () => { setupSwitcher(false); await sleep(2000); - switcher = Switcher.factory(); - console.log('checkSnapshot:', await Switcher.checkSnapshot()); + switcher = Client.getSwitcher(); + console.log('checkSnapshot:', await Client.checkSnapshot()); - Switcher.unloadSnapshot(); + Client.unloadSnapshot(); }; const _testAsyncCall = async () => { setupSwitcher(true); - switcher = Switcher.factory(); + switcher = Client.getSwitcher(); console.log("Sync:", await switcher.isItOn(SWITCHER_KEY)); @@ -108,50 +108,50 @@ const _testAsyncCall = async () => { .then(res => console.log('Promise result:', res)) .catch(error => console.log(error)); - Switcher.unloadSnapshot(); + Client.unloadSnapshot(); }; const _testBypasser = async () => { setupSwitcher(true); - switcher = Switcher.factory(); + switcher = Client.getSwitcher(); let result = await switcher.isItOn(SWITCHER_KEY); console.log(result); - Switcher.assume(SWITCHER_KEY).true(); + Client.assume(SWITCHER_KEY).true(); result = await switcher.isItOn(SWITCHER_KEY); console.log(result); - Switcher.forget(SWITCHER_KEY); + Client.forget(SWITCHER_KEY); result = await switcher.isItOn(SWITCHER_KEY); console.log(result); - Switcher.unloadSnapshot(); + Client.unloadSnapshot(); }; // Requires remote API const _testWatchSnapshot = async () => { - Switcher.buildContext({ url, apiKey, domain, component, environment }, { snapshotLocation, local: true, logger: true }); - await Switcher.loadSnapshot(false, true) + Client.buildContext({ url, apiKey, domain, component, environment }, { snapshotLocation, local: true, logger: true }); + await Client.loadSnapshot(false, true) .then(() => console.log('Snapshot loaded')) .catch(() => console.log('Failed to load Snapshot')); - const switcher = Switcher.factory(); + const switcher = Client.getSwitcher(); - Switcher.watchSnapshot( + Client.watchSnapshot( async () => console.log('In-memory snapshot updated', await switcher.isItOn(SWITCHER_KEY)), (err: Error) => console.log(err)); }; // Requires remote API const _testSnapshotAutoUpdate = async () => { - Switcher.buildContext({ url, apiKey, domain, component, environment }, + Client.buildContext({ url, apiKey, domain, component, environment }, { local: true, logger: true }); - await Switcher.loadSnapshot(false, true); - const switcher = Switcher.factory(); + await Client.loadSnapshot(false, true); + const switcher = Client.getSwitcher(); - Switcher.scheduleSnapshotAutoUpdate(3, + Client.scheduleSnapshotAutoUpdate(3, (updated) => console.log('In-memory snapshot updated', updated), (err: Error) => console.log(err)); @@ -159,8 +159,8 @@ const _testSnapshotAutoUpdate = async () => { const time = Date.now(); await switcher.isItOn(SWITCHER_KEY); console.clear(); - console.log(Switcher.getLogger(SWITCHER_KEY), `executed in ${Date.now() - time}ms`); + console.log(Client.getLogger(SWITCHER_KEY), `executed in ${Date.now() - time}ms`); }, 2000); }; -_testLocal(); \ No newline at end of file +_testSimpleAPICall(false); \ No newline at end of file diff --git a/test/switcher-client.test.ts b/test/switcher-client.test.ts index dad03c4..f830e29 100644 --- a/test/switcher-client.test.ts +++ b/test/switcher-client.test.ts @@ -5,11 +5,11 @@ import { assertTrue } from './helper/utils.ts' import type { ResultDetail } from "../src/types/index.d.ts"; import TimedMatch from '../src/lib/utils/timed-match/index.ts'; import { StrategiesType } from '../src/lib/snapshot.ts'; -import { Switcher } from '../mod.ts'; +import { Client, type Switcher } from '../mod.ts'; const testSettings = { sanitizeOps: false, sanitizeResources: false, sanitizeExit: false }; -describe('E2E test - Switcher local:', function () { +describe('E2E test - Client local:', function () { let switcher: Switcher; const apiKey = '[api_key]'; const domain = 'Business'; @@ -19,22 +19,22 @@ describe('E2E test - Switcher local:', function () { const snapshotLocation = './test/snapshot/'; beforeAll(async function() { - Switcher.buildContext({ url, apiKey, domain, component, environment }, { + Client.buildContext({ url, apiKey, domain, component, environment }, { snapshotLocation, local: true, logger: true, regexMaxBlackList: 1, regexMaxTimeLimit: 500 }); - await Switcher.loadSnapshot(); - switcher = Switcher.factory(); + await Client.loadSnapshot(); + switcher = Client.getSwitcher(); }); afterAll(function() { - Switcher.unloadSnapshot(); + Client.unloadSnapshot(); TimedMatch.terminateWorker(); }); beforeEach(function() { - Switcher.clearLogger(); - switcher = Switcher.factory(); + Client.clearLogger(); + switcher = Client.getSwitcher(); }); it('should be valid - isItOn', testSettings, async function () { @@ -70,12 +70,12 @@ describe('E2E test - Switcher local:', function () { assertTrue(result); }); - it('should be valid - Switcher strategy disabled', testSettings, async function () { + it('should be valid - Client strategy disabled', testSettings, async function () { const result = await switcher.checkNetwork('192.168.0.1').isItOn('FF2FOR2021'); assertTrue(result); }); - it('should be valid - No Switcher strategy', testSettings, async function () { + it('should be valid - No Client strategy', testSettings, async function () { const result = await switcher.isItOn('FF2FOR2022'); assertTrue(result); }); @@ -121,7 +121,7 @@ describe('E2E test - Switcher local:', function () { .prepare('FF2FOR2023'); assertFalse(await switcher.isItOn()); - assertEquals(Switcher.getLogger('FF2FOR2023')[0].response.reason, + assertEquals(Client.getLogger('FF2FOR2023')[0].response.reason, `Strategy '${StrategiesType.PAYLOAD}' does not agree`); }); @@ -132,25 +132,25 @@ describe('E2E test - Switcher local:', function () { .prepare('FF2FOR2020'); assertFalse(await switcher.isItOn()); - assertEquals(Switcher.getLogger('FF2FOR2020')[0].response.reason, + assertEquals(Client.getLogger('FF2FOR2020')[0].response.reason, `Strategy '${StrategiesType.NETWORK}' does not agree`); }); it('should be invalid - Input not provided', testSettings, async function () { assertFalse(await switcher.isItOn('FF2FOR2020')); - assertEquals(Switcher.getLogger('FF2FOR2020')[0].response.reason, + assertEquals(Client.getLogger('FF2FOR2020')[0].response.reason, `Strategy '${StrategiesType.NETWORK}' did not receive any input`); }); - it('should be invalid - Switcher config disabled', testSettings, async function () { + it('should be invalid - Client config disabled', testSettings, async function () { assertFalse(await switcher.isItOn('FF2FOR2031')); - assertEquals(Switcher.getLogger('FF2FOR2031')[0].response.reason, + assertEquals(Client.getLogger('FF2FOR2031')[0].response.reason, 'Config disabled'); }); - it('should be invalid - Switcher group disabled', testSettings, async function () { + it('should be invalid - Client group disabled', testSettings, async function () { assertFalse(await switcher.isItOn('FF2FOR2040')); - assertEquals(Switcher.getLogger('FF2FOR2040')[0].response.reason, + assertEquals(Client.getLogger('FF2FOR2040')[0].response.reason, 'Group disabled'); }); @@ -161,15 +161,15 @@ describe('E2E test - Switcher local:', function () { .prepare('FF2FOR2020'); assertTrue(await switcher.isItOn()); - Switcher.assume('FF2FOR2020').false(); + Client.assume('FF2FOR2020').false(); assertFalse(await switcher.isItOn()); - Switcher.forget('FF2FOR2020'); + Client.forget('FF2FOR2020'); assertTrue(await switcher.isItOn()); }); it('should be valid assuming key to be false - with details', async function () { - Switcher.assume('FF2FOR2020').false(); + Client.assume('FF2FOR2020').false(); const { result, reason } = await switcher.detail().isItOn('FF2FOR2020') as ResultDetail; assertFalse(result); @@ -177,7 +177,7 @@ describe('E2E test - Switcher local:', function () { }); it('should be valid assuming key to be false - with metadata', async function () { - Switcher.assume('FF2FOR2020').false().withMetadata({ value: 'something' }); + Client.assume('FF2FOR2020').false().withMetadata({ value: 'something' }); const { result, reason, metadata } = await switcher.detail(true).isItOn('FF2FOR2020') as ResultDetail; assertFalse(result); @@ -191,10 +191,10 @@ describe('E2E test - Switcher local:', function () { .checkNetwork('10.0.0.3') .prepare('UNKNOWN'); - Switcher.assume('UNKNOWN').true(); + Client.assume('UNKNOWN').true(); assertTrue(await switcher.isItOn()); - Switcher.forget('UNKNOWN'); + Client.forget('UNKNOWN'); await assertRejects(async () => await switcher.isItOn(), Error, 'Something went wrong: {"error":"Unable to load a key UNKNOWN"}'); @@ -202,52 +202,52 @@ describe('E2E test - Switcher local:', function () { it('should enable test mode which will prevent a snapshot to be watchable', testSettings, async function () { //given - Switcher.buildContext({ url, apiKey, domain, component, environment }, { + Client.buildContext({ url, apiKey, domain, component, environment }, { local: true, logger: true, regexSafe: false }); - switcher = Switcher.factory(); + switcher = Client.getSwitcher(); //test - Switcher.assume('FF2FOR2020').false(); + Client.assume('FF2FOR2020').false(); assertFalse(await switcher.isItOn('FF2FOR2020')); - Switcher.assume('FF2FOR2020').true(); + Client.assume('FF2FOR2020').true(); assertTrue(await switcher.isItOn('FF2FOR2020')); }); it('should be valid - Local mode', testSettings, async function () { await delay(2000); - Switcher.buildContext({ url, apiKey, domain, component, environment }, { + Client.buildContext({ url, apiKey, domain, component, environment }, { local: true, regexSafe: false, snapshotLocation: 'generated-snapshots/' }); - const version = await Switcher.loadSnapshot(false, false); + const version = await Client.loadSnapshot(false, false); assertEquals(version, 0); - assertExists(Switcher.snapshot); + assertExists(Client.snapshot); }); it('should be invalid - Local mode cannot load snapshot from an invalid path', testSettings, async function () { await delay(2000); - Switcher.buildContext({ url, apiKey, domain, component, environment }, { + Client.buildContext({ url, apiKey, domain, component, environment }, { local: true, regexSafe: false, snapshotLocation: '//somewhere/' }); - Switcher.testMode(); + Client.testMode(); //test await assertRejects(async () => - await Switcher.loadSnapshot(), + await Client.loadSnapshot(), Error, 'Something went wrong: It was not possible to load the file at //somewhere/'); //or let error: Error | undefined; - await Switcher.loadSnapshot(false, false).catch((e) => error = e); + await Client.loadSnapshot(false, false).catch((e) => error = e); assertEquals(error?.message, 'Something went wrong: It was not possible to load the file at //somewhere/'); }); diff --git a/test/switcher-functional.test.ts b/test/switcher-functional.test.ts index c1eb6de..d57c585 100644 --- a/test/switcher-functional.test.ts +++ b/test/switcher-functional.test.ts @@ -3,23 +3,23 @@ import { describe, it, afterAll, afterEach, beforeEach, assertSpyCalls, spy } from './deps.ts'; import { given, givenError, tearDown, assertTrue, generateAuth, generateResult, generateDetailedResult, sleep } from './helper/utils.ts' -import { Switcher } from '../mod.ts'; +import { Client } from '../mod.ts'; import type { ResultDetail, SwitcherContext } from '../src/types/index.d.ts'; import TimedMatch from '../src/lib/utils/timed-match/index.ts'; import ExecutionLogger from "../src/lib/utils/executionLogger.ts"; -describe('Integrated test - Switcher:', function () { +describe('Integrated test - Client:', function () { let contextSettings: SwitcherContext; afterAll(function() { - Switcher.unloadSnapshot(); + Client.unloadSnapshot(); TimedMatch.terminateWorker(); }); beforeEach(function() { tearDown(); - Switcher.testMode(); + Client.testMode(); contextSettings = { url: 'http://localhost:3000', @@ -46,8 +46,8 @@ describe('Integrated test - Switcher:', function () { given('POST@/criteria', generateResult(true)); // test - Switcher.buildContext(contextSettings); - const switcher = Switcher.factory(); + Client.buildContext(contextSettings); + const switcher = Client.getSwitcher(); await switcher.prepare('FLAG_1'); assertTrue(await switcher.isItOn()); @@ -58,8 +58,8 @@ describe('Integrated test - Switcher:', function () { given('POST@/criteria/auth', undefined, 429); // test - Switcher.buildContext(contextSettings); - const switcher = Switcher.factory(); + Client.buildContext(contextSettings); + const switcher = Client.getSwitcher(); await assertRejects(async () => await switcher.isItOn('FLAG_1'), @@ -72,8 +72,8 @@ describe('Integrated test - Switcher:', function () { given('POST@/criteria', generateResult(true)); // test - Switcher.buildContext(contextSettings); - const switcher = Switcher.factory(); + Client.buildContext(contextSettings); + const switcher = Client.getSwitcher(); switcher.throttle(1000); const spyPrepare = spy(switcher, '_executeAsyncRemoteCriteria'); @@ -93,8 +93,8 @@ describe('Integrated test - Switcher:', function () { given('POST@/criteria', generateResult(true)); // test - Switcher.buildContext(contextSettings); - const switcher = Switcher.factory(); + Client.buildContext(contextSettings); + const switcher = Client.getSwitcher(); switcher.throttle(1000); // first API call - stores result in cache @@ -112,9 +112,9 @@ describe('Integrated test - Switcher:', function () { given('POST@/criteria', generateResult(true)); // before token expires // test - Switcher.buildContext(contextSettings); + Client.buildContext(contextSettings); - const switcher = Switcher.factory(); + const switcher = Client.getSwitcher(); switcher.throttle(500); const spyPrepare = spy(switcher, 'prepare'); @@ -156,10 +156,10 @@ describe('Integrated test - Switcher:', function () { // test let asyncErrorMessage = null; - Switcher.buildContext(contextSettings); - Switcher.subscribeNotifyError((error) => asyncErrorMessage = error.message); + Client.buildContext(contextSettings); + Client.subscribeNotifyError((error) => asyncErrorMessage = error.message); - const switcher = Switcher.factory(); + const switcher = Client.getSwitcher(); switcher.throttle(1000); assertTrue(await switcher.isItOn('FLAG_1')); // sync @@ -191,10 +191,10 @@ describe('Integrated test - Switcher:', function () { }); it('should return true - snapshot switcher is true', async function () { - Switcher.buildContext(contextSettings, forceRemoteOptions); + Client.buildContext(contextSettings, forceRemoteOptions); - const switcher = Switcher.factory(); - await Switcher.loadSnapshot(); + const switcher = Client.getSwitcher(); + await Client.loadSnapshot(); assertTrue(await switcher.isItOn('FF2FOR2030')); }); @@ -204,12 +204,12 @@ describe('Integrated test - Switcher:', function () { given('POST@/criteria', generateResult(false)); // test - Switcher.buildContext(contextSettings, forceRemoteOptions); + Client.buildContext(contextSettings, forceRemoteOptions); - const switcher = Switcher.factory(); + const switcher = Client.getSwitcher(); const executeRemoteCriteria = spy(switcher, '_executeRemoteCriteria'); - await Switcher.loadSnapshot(); + await Client.loadSnapshot(); assertFalse(await switcher.remote().isItOn('FF2FOR2030')); assertSpyCalls(executeRemoteCriteria, 1); }); @@ -226,9 +226,9 @@ describe('Integrated test - Switcher:', function () { })); // test - Switcher.buildContext(contextSettings); + Client.buildContext(contextSettings); - const switcher = Switcher.factory(); + const switcher = Client.getSwitcher(); const detailedResult = await switcher.detail().isItOn('FF2FOR2030') as ResultDetail; assertTrue(detailedResult.result); assertEquals(detailedResult.reason, 'Success'); @@ -236,9 +236,9 @@ describe('Integrated test - Switcher:', function () { }); it('should return error when local is not enabled', async function () { - Switcher.buildContext(contextSettings, { regexSafe: false, local: false }); + Client.buildContext(contextSettings, { regexSafe: false, local: false }); - const switcher = Switcher.factory(); + const switcher = Client.getSwitcher(); await assertRejects(async () => await switcher.remote().isItOn('FF2FOR2030'), @@ -252,12 +252,12 @@ describe('Integrated test - Switcher:', function () { let contextSettings: SwitcherContext; afterAll(function() { - Switcher.unloadSnapshot(); + Client.unloadSnapshot(); }); beforeEach(function() { tearDown(); - Switcher.testMode(); + Client.testMode(); contextSettings = { url: 'http://localhost:3000', @@ -273,8 +273,8 @@ describe('Integrated test - Switcher:', function () { given('POST@/criteria/auth', { error: 'Too many requests' }, 429); // test - Switcher.buildContext(contextSettings); - const switcher = Switcher.factory(); + Client.buildContext(contextSettings); + const switcher = Client.getSwitcher(); await assertRejects(async () => await switcher.isItOn('FLAG_1'), @@ -287,8 +287,8 @@ describe('Integrated test - Switcher:', function () { given('POST@/criteria', { error: 'Too many requests' }, 429); // test - Switcher.buildContext(contextSettings); - const switcher = Switcher.factory(); + Client.buildContext(contextSettings); + const switcher = Client.getSwitcher(); await assertRejects(async () => await switcher.isItOn('FLAG_1'), @@ -296,12 +296,12 @@ describe('Integrated test - Switcher:', function () { }); it('should use silent mode when fail to check switchers', async function() { - Switcher.buildContext(contextSettings, { silentMode: '5m', regexSafe: false, snapshotLocation: './test/snapshot/' }); + Client.buildContext(contextSettings, { silentMode: '5m', regexSafe: false, snapshotLocation: './test/snapshot/' }); await assertRejects(async () => - await Switcher.checkSwitchers(['FEATURE01', 'FEATURE02']), + await Client.checkSwitchers(['FEATURE01', 'FEATURE02']), Error, 'Something went wrong: [FEATURE01,FEATURE02] not found'); - await Switcher.checkSwitchers(['FF2FOR2021', 'FF2FOR2021']); + await Client.checkSwitchers(['FF2FOR2021', 'FF2FOR2021']); }); it('should use silent mode when fail to check criteria', async function () { @@ -311,10 +311,10 @@ describe('Integrated test - Switcher:', function () { // test let asyncErrorMessage = null; - Switcher.buildContext(contextSettings, { silentMode: '5m', regexSafe: false, snapshotLocation: './test/snapshot/' }); - Switcher.subscribeNotifyError((error) => asyncErrorMessage = error.message); + Client.buildContext(contextSettings, { silentMode: '5m', regexSafe: false, snapshotLocation: './test/snapshot/' }); + Client.subscribeNotifyError((error) => asyncErrorMessage = error.message); - const switcher = Switcher.factory(); + const switcher = Client.getSwitcher(); assertTrue(await switcher.isItOn('FF2FOR2022')); assertEquals(asyncErrorMessage, 'Something went wrong: [checkCriteria] failed with status 429'); @@ -334,8 +334,8 @@ describe('Integrated test - Switcher:', function () { given('POST@/criteria', generateResult(true)); // test - Switcher.buildContext(contextSettings); - const switcher = Switcher.factory(); + Client.buildContext(contextSettings); + const switcher = Client.getSwitcher(); await switcher .checkValue('User 1') @@ -345,7 +345,7 @@ describe('Integrated test - Switcher:', function () { .checkTime('08:00') .checkRegex('\\bUSER_[0-9]{1,2}\\b') .checkPayload(JSON.stringify({ name: 'User 1' })) - .prepare('FLAG_1'); + .prepare('SWITCHER_MULTIPLE_INPUT'); assertEquals(switcher.input, [ [ 'VALUE_VALIDATION', 'User 1' ], @@ -356,6 +356,7 @@ describe('Integrated test - Switcher:', function () { [ 'REGEX_VALIDATION', '\\bUSER_[0-9]{1,2}\\b' ], [ 'PAYLOAD_VALIDATION', '{"name":"User 1"}' ] ]); + assertTrue(await switcher.isItOn()); }); @@ -365,10 +366,10 @@ describe('Integrated test - Switcher:', function () { given('POST@/criteria/switchers_check', { not_found: [] }); //test - Switcher.buildContext(contextSettings); + Client.buildContext(contextSettings); let error: Error | undefined; - await Switcher.checkSwitchers(['FEATURE01', 'FEATURE02']).catch(err => error = err); + await Client.checkSwitchers(['FEATURE01', 'FEATURE02']).catch(err => error = err); assertEquals(error, undefined); }); @@ -378,9 +379,9 @@ describe('Integrated test - Switcher:', function () { given('POST@/criteria/switchers_check', { not_found: ['FEATURE02'] }); //test - Switcher.buildContext(contextSettings); + Client.buildContext(contextSettings); await assertRejects(async () => - await Switcher.checkSwitchers(['FEATURE01', 'FEATURE02']), + await Client.checkSwitchers(['FEATURE01', 'FEATURE02']), Error, 'Something went wrong: [FEATURE02] not found'); }); @@ -390,22 +391,22 @@ describe('Integrated test - Switcher:', function () { given('POST@/criteria/switchers_check', undefined, 422); //test - Switcher.buildContext(contextSettings); + Client.buildContext(contextSettings); await assertRejects(async () => - await Switcher.checkSwitchers([]), + await Client.checkSwitchers([]), Error, 'Something went wrong: [checkSwitchers] failed with status 422'); }); it('should throw when certPath is invalid', function() { - assertThrows(() => Switcher.buildContext(contextSettings, { certPath: 'invalid' })); + assertThrows(() => Client.buildContext(contextSettings, { certPath: 'invalid' })); }); it('should renew the token after expiration', async function () { // given API responding properly given('POST@/criteria/auth', generateAuth('[auth_token]', 1)); - Switcher.buildContext(contextSettings); - const switcher = Switcher.factory(); + Client.buildContext(contextSettings); + const switcher = Client.getSwitcher(); const spyPrepare = spy(switcher, 'prepare'); // Prepare the call generating the token @@ -436,8 +437,8 @@ describe('Integrated test - Switcher:', function () { given('POST@/criteria', generateResult(true)); // test - Switcher.buildContext(contextSettings); - const switcher = Switcher.factory(); + Client.buildContext(contextSettings); + const switcher = Client.getSwitcher(); assertTrue(await switcher .checkValue('User 1') .checkNetwork('192.168.0.1') @@ -450,8 +451,8 @@ describe('Integrated test - Switcher:', function () { given('POST@/criteria', generateResult(true)); // test - Switcher.buildContext(contextSettings); - const switcher = Switcher.factory(); + Client.buildContext(contextSettings); + const switcher = Client.getSwitcher(); await switcher.prepare('MY_FLAG'); assertTrue(await switcher @@ -465,7 +466,7 @@ describe('Integrated test - Switcher:', function () { given('POST@/criteria/auth', generateAuth('[auth_token]', 5)); // test - Switcher.buildContext({ + Client.buildContext({ url: undefined, apiKey: '[apiKey]', domain: '[domain]', @@ -473,11 +474,11 @@ describe('Integrated test - Switcher:', function () { environment: 'default' }); - const switcher = Switcher.factory(); + const switcher = Client.getSwitcher(); await assertRejects(async () => await switcher.isItOn(), - Error, 'Something went wrong: Invalid URL'); + Error, 'Something went wrong: URL is required'); }); it('should be invalid - Missing API Key field', async function () { @@ -485,7 +486,7 @@ describe('Integrated test - Switcher:', function () { given('POST@/criteria/auth', generateAuth('[auth_token]', 5)); // test - Switcher.buildContext({ + Client.buildContext({ url: 'http://localhost:3000', apiKey: undefined, domain: '[domain]', @@ -493,7 +494,7 @@ describe('Integrated test - Switcher:', function () { environment: 'default' }); - const switcher = Switcher.factory(); + const switcher = Client.getSwitcher(); await switcher .checkValue('User 1') @@ -502,7 +503,7 @@ describe('Integrated test - Switcher:', function () { await assertRejects(async () => await switcher.isItOn(), - Error, 'Something went wrong: Missing API Key field'); + Error, 'Something went wrong: API Key is required'); }); it('should be invalid - Missing key field', async function () { @@ -510,8 +511,8 @@ describe('Integrated test - Switcher:', function () { given('POST@/criteria/auth', generateAuth('[auth_token]', 5)); // test - Switcher.buildContext(contextSettings); - const switcher = Switcher.factory(); + Client.buildContext(contextSettings); + const switcher = Client.getSwitcher(); await assertRejects(async () => await switcher.isItOn(), @@ -523,7 +524,7 @@ describe('Integrated test - Switcher:', function () { given('POST@/criteria/auth', generateAuth('[auth_token]', 5)); // test - Switcher.buildContext({ + Client.buildContext({ url: 'http://localhost:3000', apiKey: '[apiKey]', domain: '[domain]', @@ -531,11 +532,11 @@ describe('Integrated test - Switcher:', function () { environment: 'default' }); - const switcher = Switcher.factory(); + const switcher = Client.getSwitcher(); await assertRejects(async () => await switcher.isItOn('MY_FLAG'), - Error, 'Something went wrong: Missing component field'); + Error, 'Something went wrong: Component is required'); }); it('should be invalid - Missing token field', async function () { @@ -543,8 +544,8 @@ describe('Integrated test - Switcher:', function () { given('POST@/criteria/auth', generateAuth(undefined, 5)); // test - Switcher.buildContext(contextSettings); - const switcher = Switcher.factory(); + Client.buildContext(contextSettings); + const switcher = Client.getSwitcher(); await assertRejects(async () => await switcher.isItOn('MY_FLAG'), @@ -553,13 +554,13 @@ describe('Integrated test - Switcher:', function () { it('should run in silent mode', async function () { // setup context to read the snapshot in case the API does not respond - Switcher.buildContext(contextSettings, { + Client.buildContext(contextSettings, { snapshotLocation: './test/snapshot/', regexSafe: false, silentMode: '2s', }); - const switcher = Switcher.factory(); + const switcher = Client.getSwitcher(); const spyRemote = spy(switcher, '_executeRemoteCriteria'); // First attempt to reach the API - Since it's configured to use silent mode, it should return true (according to the snapshot) @@ -590,8 +591,8 @@ describe('Integrated test - Switcher:', function () { givenError('POST@/criteria/auth', 'ECONNREFUSED'); // test - Switcher.buildContext(contextSettings); - const switcher = Switcher.factory(); + Client.buildContext(contextSettings); + const switcher = Client.getSwitcher(); await assertRejects(async () => await switcher.isItOn('FF2FOR2030'), @@ -599,13 +600,13 @@ describe('Integrated test - Switcher:', function () { }); it('should run in silent mode when API is unavailable', async function () { - Switcher.buildContext(contextSettings, { + Client.buildContext(contextSettings, { snapshotLocation: './test/snapshot/', regexSafe: false, silentMode: '5m' }); - const switcher = Switcher.factory(); + const switcher = Client.getSwitcher(); assertTrue(await switcher.isItOn('FF2FOR2030')); }); diff --git a/test/switcher-snapshot.test.ts b/test/switcher-snapshot.test.ts index 5178703..e249d5e 100644 --- a/test/switcher-snapshot.test.ts +++ b/test/switcher-snapshot.test.ts @@ -1,12 +1,12 @@ import { describe, it, afterAll, beforeEach, assertRejects, assertFalse, assertExists, assertEquals, delay, existsSync } from './deps.ts'; import { given, givenError, tearDown, generateAuth, generateStatus, assertTrue, WaitSafe } from './helper/utils.ts'; -import { Switcher } from '../mod.ts'; +import { Client } from '../mod.ts'; import type { SwitcherContext } from '../src/types/index.d.ts'; const testSettings = { sanitizeOps: false, sanitizeResources: false, sanitizeExit: false }; -describe('E2E test - Switcher local - Snapshot:', function () { +describe('E2E test - Client local - Snapshot:', function () { const token = '[token]'; let contextSettings: SwitcherContext; @@ -17,7 +17,7 @@ describe('E2E test - Switcher local - Snapshot:', function () { const dataJSONV2 = dataBufferV2.toString(); beforeEach(function() { - Switcher.unloadSnapshot(); + Client.unloadSnapshot(); contextSettings = { url: 'http://localhost:3000', @@ -27,18 +27,18 @@ describe('E2E test - Switcher local - Snapshot:', function () { environment: 'dev' }; - Switcher.buildContext(contextSettings, { + Client.buildContext(contextSettings, { snapshotLocation: './test/snapshot/', local: true, regexSafe: false }); - Switcher.testMode(); + Client.testMode(); tearDown(); }); afterAll(function() { - Switcher.unloadSnapshot(); + Client.unloadSnapshot(); if (existsSync('generated-snapshots/')) Deno.removeSync('generated-snapshots/', { recursive: true }); }); @@ -49,10 +49,10 @@ describe('E2E test - Switcher local - Snapshot:', function () { given('GET@/criteria/snapshot_check/:version', undefined, 429); //test - Switcher.testMode(); - await Switcher.loadSnapshot(); + Client.testMode(); + await Client.loadSnapshot(); await assertRejects(async () => - await Switcher.checkSnapshot(), + await Client.checkSnapshot(), Error, 'Something went wrong: [checkSnapshotVersion] failed with status 429'); }); @@ -63,13 +63,13 @@ describe('E2E test - Switcher local - Snapshot:', function () { given('POST@/graphql', undefined, 429); //test - Switcher.buildContext(contextSettings, { + Client.buildContext(contextSettings, { snapshotLocation: 'generated-snapshots/', regexSafe: false }); await assertRejects(async () => - await Switcher.loadSnapshot(), + await Client.loadSnapshot(), Error, 'Something went wrong: [resolveSnapshot] failed with status 429'); }); @@ -82,13 +82,13 @@ describe('E2E test - Switcher local - Snapshot:', function () { given('POST@/graphql', JSON.parse(dataJSON)); //test - Switcher.buildContext(contextSettings, { + Client.buildContext(contextSettings, { local: true, regexSafe: false }); - await Switcher.loadSnapshot(); - assertTrue(await Switcher.checkSnapshot()); + await Client.loadSnapshot(); + assertTrue(await Client.checkSnapshot()); }); it('should update snapshot - store file', testSettings, async function () { @@ -100,18 +100,40 @@ describe('E2E test - Switcher local - Snapshot:', function () { given('POST@/graphql', JSON.parse(dataJSON)); //test - Switcher.buildContext(contextSettings, { + Client.buildContext(contextSettings, { snapshotLocation: 'generated-snapshots/', local: true, regexSafe: false }); - await Switcher.loadSnapshot(true); - assertTrue(await Switcher.checkSnapshot()); + await Client.loadSnapshot(true); + assertTrue(await Client.checkSnapshot()); assertTrue(existsSync(`generated-snapshots/${contextSettings.environment}.json`)); //restore state to avoid process leakage - Switcher.unloadSnapshot(); + Client.unloadSnapshot(); + }); + + it('should update snapshot during load - store file', testSettings, async function () { + await delay(2000); + + //given + given('POST@/criteria/auth', generateAuth(token, 5)); + given('GET@/criteria/snapshot_check/:version', generateStatus(false)); + given('POST@/graphql', JSON.parse(dataJSON)); + + //test + Client.buildContext(contextSettings, { + snapshotLocation: 'generated-snapshots/', + local: true, + regexSafe: false + }); + + await Client.loadSnapshot(true, true); + assertTrue(existsSync(`generated-snapshots/${contextSettings.environment}.json`)); + + //restore state to avoid process leakage + Client.unloadSnapshot(); }); it('should auto update snapshot every second', testSettings, async function () { @@ -123,7 +145,7 @@ describe('E2E test - Switcher local - Snapshot:', function () { given('POST@/graphql', JSON.parse(dataJSON)); //test - Switcher.buildContext(contextSettings, { + Client.buildContext(contextSettings, { snapshotLocation: 'generated-snapshots/', local: true, regexSafe: false, @@ -131,11 +153,11 @@ describe('E2E test - Switcher local - Snapshot:', function () { }); let snapshotUpdated = false; - Switcher.scheduleSnapshotAutoUpdate(1, (updated) => snapshotUpdated = updated); + Client.scheduleSnapshotAutoUpdate(1, (updated) => snapshotUpdated = updated); - await Switcher.loadSnapshot(false, true); + await Client.loadSnapshot(false, true); - const switcher = Switcher.factory(); + const switcher = Client.getSwitcher(); assertFalse(await switcher.isItOn('FF2FOR2030')); //given new version @@ -147,7 +169,7 @@ describe('E2E test - Switcher local - Snapshot:', function () { assertTrue(snapshotUpdated); assertTrue(await switcher.isItOn('FF2FOR2030')); - Switcher.terminateSnapshotAutoUpdate(); + Client.terminateSnapshotAutoUpdate(); }); it('should NOT auto update snapshot ', testSettings, async function () { @@ -159,16 +181,16 @@ describe('E2E test - Switcher local - Snapshot:', function () { given('POST@/graphql', JSON.parse(dataJSON)); //test - Switcher.buildContext(contextSettings, { + Client.buildContext(contextSettings, { snapshotLocation: 'generated-snapshots/', local: true, regexSafe: false }); let error: Error | undefined; - Switcher.scheduleSnapshotAutoUpdate(1, undefined, (err: Error) => error = err); + Client.scheduleSnapshotAutoUpdate(1, undefined, (err: Error) => error = err); - await Switcher.loadSnapshot(false, true); + await Client.loadSnapshot(false, true); //next call will fail givenError('POST@/graphql', 'ECONNREFUSED'); @@ -180,7 +202,7 @@ describe('E2E test - Switcher local - Snapshot:', function () { assertEquals(error.message, 'Something went wrong: Connection has been refused - ECONNREFUSED'); //tearDown - Switcher.terminateSnapshotAutoUpdate(); + Client.terminateSnapshotAutoUpdate(); }); it('should NOT update snapshot', testSettings, async function () { @@ -191,8 +213,8 @@ describe('E2E test - Switcher local - Snapshot:', function () { given('GET@/criteria/snapshot_check/:version', generateStatus(true)); // No available update //test - await Switcher.loadSnapshot(); - assertFalse(await Switcher.checkSnapshot()); + await Client.loadSnapshot(); + assertFalse(await Client.checkSnapshot()); }); it('should NOT update snapshot - check Snapshot Error', testSettings, async function () { @@ -203,10 +225,10 @@ describe('E2E test - Switcher local - Snapshot:', function () { givenError('GET@/criteria/snapshot_check/:version', 'ECONNREFUSED'); //test - Switcher.testMode(); - await Switcher.loadSnapshot(); + Client.testMode(); + await Client.loadSnapshot(); await assertRejects(async () => - await Switcher.checkSnapshot(), + await Client.checkSnapshot(), Error, 'Something went wrong: Connection has been refused - ECONNREFUSED'); }); @@ -219,10 +241,10 @@ describe('E2E test - Switcher local - Snapshot:', function () { givenError('POST@/graphql', 'ECONNREFUSED'); //test - Switcher.testMode(); - await Switcher.loadSnapshot(); + Client.testMode(); + await Client.loadSnapshot(); await assertRejects(async () => - await Switcher.checkSnapshot(), + await Client.checkSnapshot(), Error, 'Something went wrong: Connection has been refused - ECONNREFUSED'); }); @@ -232,18 +254,18 @@ describe('E2E test - Switcher local - Snapshot:', function () { given('GET@/criteria/snapshot_check/:version', generateStatus(true)); //pre-load snapshot - Switcher.testMode(false); - await Switcher.loadSnapshot(); - assertFalse(await Switcher.checkSnapshot()); + Client.testMode(false); + await Client.loadSnapshot(); + assertFalse(await Client.checkSnapshot()); //unload snapshot - Switcher.unloadSnapshot(); + Client.unloadSnapshot(); //test let error: Error | undefined; - await Switcher.checkSnapshot().catch((err: Error) => error = err); + await Client.checkSnapshot().catch((err: Error) => error = err); assertExists(error); - assertEquals(error.message, 'Something went wrong: Snapshot is not loaded. Use Switcher.loadSnapshot()'); + assertEquals(error.message, 'Something went wrong: Snapshot is not loaded. Use Client.loadSnapshot()'); }); it('should update snapshot', testSettings, async function () { @@ -255,43 +277,43 @@ describe('E2E test - Switcher local - Snapshot:', function () { given('POST@/graphql', JSON.parse(dataJSON)); //test - Switcher.buildContext(contextSettings, { + Client.buildContext(contextSettings, { snapshotLocation: 'generated-snapshots/', regexSafe: false }); - await Switcher.loadSnapshot(); - assertExists(Switcher.snapshot); + await Client.loadSnapshot(); + assertExists(Client.snapshot); }); it('should not throw when switcher keys provided were configured properly', testSettings, async function () { await delay(2000); - await Switcher.loadSnapshot(); + await Client.loadSnapshot(); let error: Error | undefined; - await Switcher.checkSwitchers(['FF2FOR2030']).catch((err: Error) => error = err); + await Client.checkSwitchers(['FF2FOR2030']).catch((err: Error) => error = err); assertEquals(error, undefined); }); it('should throw when switcher keys provided were not configured properly', testSettings, async function () { await delay(2000); - await Switcher.loadSnapshot(); + await Client.loadSnapshot(); await assertRejects(async () => - await Switcher.checkSwitchers(['FEATURE02']), + await Client.checkSwitchers(['FEATURE02']), Error, 'Something went wrong: [FEATURE02] not found'); }); it('should be invalid - Load snapshot was not called', testSettings, async function () { - Switcher.buildContext(contextSettings, { + Client.buildContext(contextSettings, { local: true, logger: true, regexSafe: false }); - const switcher = Switcher.factory(); + const switcher = Client.getSwitcher(); await assertRejects(async () => await switcher.isItOn('FF2FOR2030'), - Error, 'Snapshot not loaded. Try to use \'Switcher.loadSnapshot()\''); + Error, 'Snapshot not loaded. Try to use \'Client.loadSnapshot()\''); }); }); \ No newline at end of file diff --git a/test/switcher-watch-snapshot.test.ts b/test/switcher-watch-snapshot.test.ts index 44714fe..9d7738a 100644 --- a/test/switcher-watch-snapshot.test.ts +++ b/test/switcher-watch-snapshot.test.ts @@ -2,7 +2,7 @@ import { describe, it, afterAll, beforeEach, assertEquals, assertFalse, existsSync } from './deps.ts'; import { assertTrue, WaitSafe } from './helper/utils.ts'; -import { Switcher } from '../mod.ts'; +import { Client } from '../mod.ts'; const updateSwitcher = (status: boolean) => { const dataBuffer = Deno.readTextFileSync('./test/snapshot/dev.json'); @@ -19,30 +19,30 @@ const invalidateJSON = () => { Deno.writeTextFileSync('generated-snapshots/watch.json', '[INVALID]'); }; -describe('E2E test - Switcher local - Watch Snapshot:', function () { +describe('E2E test - Client local - Watch Snapshot:', function () { const domain = 'Business'; const component = 'business-service'; const environment = 'watch'; beforeEach(async function() { updateSwitcher(true); - Switcher.buildContext({ domain, component, environment }, { + Client.buildContext({ domain, component, environment }, { snapshotLocation: 'generated-snapshots/', local: true, regexSafe: false }); - await Switcher.loadSnapshot(); + await Client.loadSnapshot(); }); afterAll(function() { - Switcher.unloadSnapshot(); + Client.unloadSnapshot(); if (existsSync('generated-snapshots/')) Deno.removeSync('generated-snapshots/', { recursive: true }); }); it('should read from snapshot - without watching', function () { - const switcher = Switcher.factory(); + const switcher = Client.getSwitcher(); switcher.isItOn('FF2FOR2030').then((val1) => { assertTrue(val1); updateSwitcher(false); @@ -54,8 +54,8 @@ describe('E2E test - Switcher local - Watch Snapshot:', function () { }); it('should read from updated snapshot', async function () { - const switcher = Switcher.factory(); - Switcher.watchSnapshot(async () => { + const switcher = Client.getSwitcher(); + Client.watchSnapshot(async () => { assertFalse(await switcher.isItOn('FF2FOR2030')); WaitSafe.finish(); }); @@ -66,12 +66,12 @@ describe('E2E test - Switcher local - Watch Snapshot:', function () { }); await WaitSafe.wait(); - Switcher.unloadSnapshot(); + Client.unloadSnapshot(); }); it('should NOT read from updated snapshot - invalid JSON', async function () { - const switcher = Switcher.factory(); - Switcher.watchSnapshot(undefined, (err: any) => { + const switcher = Client.getSwitcher(); + Client.watchSnapshot(undefined, (err: any) => { assertEquals(err.message, 'Something went wrong: It was not possible to load the file at generated-snapshots/'); WaitSafe.finish(); }); @@ -82,19 +82,19 @@ describe('E2E test - Switcher local - Watch Snapshot:', function () { }); await WaitSafe.wait(); - Switcher.unloadSnapshot(); + Client.unloadSnapshot(); }); - it('should NOT allow to watch snapshot - Switcher test is enabled', async function () { - Switcher.testMode(); - Switcher.watchSnapshot(undefined, (err: any) => { + it('should NOT allow to watch snapshot - Client test is enabled', async function () { + Client.testMode(); + Client.watchSnapshot(undefined, (err: any) => { assertEquals(err.message, 'Watch Snapshot cannot be used in test mode or without a snapshot location'); WaitSafe.finish(); }); await WaitSafe.wait(); - Switcher.unloadSnapshot(); - Switcher.testMode(false); + Client.unloadSnapshot(); + Client.testMode(false); }); }); \ No newline at end of file diff --git a/test/utils/executionLogger.test.ts b/test/utils/executionLogger.test.ts deleted file mode 100644 index 7d3c756..0000000 --- a/test/utils/executionLogger.test.ts +++ /dev/null @@ -1,67 +0,0 @@ -import { beforeEach, describe, it, assertEquals } from '../deps.ts'; - -import { Switcher } from '../../mod.ts'; -import ExecutionLogger from "../../src/lib/utils/executionLogger.ts"; - -describe('ExecutionLogger tests:', function () { - beforeEach(function () { - ExecutionLogger.clearLogger(); - }); - - it('should add a new execution and return result', function () { - //given - const switcher = Switcher.factory('SWITCHER_KEY').checkValue('test'); - const response = { result: true }; - - //test - ExecutionLogger.add(response, switcher.key, switcher.input); - const result = ExecutionLogger.getExecution(switcher.key, switcher.input); - assertEquals(result.response, response); - }); - - it('should replace an existing execution and return result', function () { - //given - const switcher = Switcher.factory('SWITCHER_KEY').checkValue('test'); - - //test - ExecutionLogger.add({ result: true }, switcher.key, switcher.input); - ExecutionLogger.add({ result: false }, switcher.key, switcher.input); - const result = ExecutionLogger.getExecution(switcher.key, switcher.input); - assertEquals(result.response, { result: false }); - }); - - it('should NOT return a result given a key', function () { - //given - const switcher = Switcher.factory('SWITCHER_KEY').checkValue('test'); - - //test - ExecutionLogger.add({ result: true }, switcher.key, switcher.input); - const result = ExecutionLogger.getExecution('DIFFERENT_KEY', switcher.input); - assertEquals(result, undefined); - }); - - it('should NOT return a result given a different input', function () { - //given - const switcher1 = Switcher.factory('SWITCHER_KEY').checkValue('test'); - const switcher2 = Switcher.factory('SWITCHER_KEY').checkValue('different'); - - //test - ExecutionLogger.add({ result: true }, switcher1.key, switcher1.input); - const result = ExecutionLogger.getExecution(switcher1.key, switcher2.input); - assertEquals(result, undefined); - }); - - it('should return all results given a key', function () { - //given - const key = 'SWITCHER_KEY'; - const switcher1 = Switcher.factory(key).checkValue('test_true'); - const switcher2 = Switcher.factory(key).checkValue('test_false'); - - //test - ExecutionLogger.add({ result: true }, switcher1.key, switcher1.input); - ExecutionLogger.add({ result: false }, switcher2.key, switcher2.input); - const result = ExecutionLogger.getByKey(key); - assertEquals(result.length, 2); - }); - -}); \ No newline at end of file