From e5507e40371b3bb182d43d5c380bf36be65977f4 Mon Sep 17 00:00:00 2001 From: Yusinto Ngadiman Date: Thu, 4 Jan 2024 12:10:32 -0800 Subject: [PATCH 01/44] chore: Added LDAutoEnv and modified LDContext to include that. Add that to internal options as well. --- packages/shared/common/src/Context.ts | 16 +++++++---- .../common/src/api/context/LDAutoEnv.ts | 28 +++++++++++++++++++ .../src/api/context/LDMultiKindContext.ts | 11 +++++--- .../src/api/context/LDSingleKindContext.ts | 3 +- .../shared/common/src/api/context/index.ts | 1 + .../src/internal/events/LDInternalOptions.ts | 3 ++ .../shared/sdk-client/src/LDClientImpl.ts | 14 +++++++--- .../src/configuration/Configuration.ts | 7 +++++ 8 files changed, 68 insertions(+), 15 deletions(-) create mode 100644 packages/shared/common/src/api/context/LDAutoEnv.ts diff --git a/packages/shared/common/src/Context.ts b/packages/shared/common/src/Context.ts index d371aaaf1..b459ab6f9 100644 --- a/packages/shared/common/src/Context.ts +++ b/packages/shared/common/src/Context.ts @@ -1,7 +1,10 @@ -/* eslint-disable no-underscore-dangle */ -// eslint-disable-next-line max-classes-per-file -import { LDContextCommon, LDMultiKindContext, LDSingleKindContext, LDUser } from './api/context'; -import { LDContext } from './api/context/LDContext'; +import type { + LDContext, + LDContextCommon, + LDMultiKindContext, + LDSingleKindContext, + LDUser, +} from './api'; import AttributeReference from './AttributeReference'; import { TypeValidators } from './validators'; @@ -84,9 +87,9 @@ function isLegacyUser(context: LDContext): context is LDUser { * that as well. */ function isContextCommon( - kindOrContext: 'multi' | LDContextCommon, + kindOrContext: 'multi' | LDContextCommon | undefined, ): kindOrContext is LDContextCommon { - return kindOrContext && TypeValidators.Object.is(kindOrContext); + return !!kindOrContext && TypeValidators.Object.is(kindOrContext); } /** @@ -266,6 +269,7 @@ export default class Context { const singleContext = context[kind]; if (isContextCommon(singleContext)) { acc[kind] = singleContext; + // eslint-disable-next-line no-underscore-dangle privateAttributes[kind] = processPrivateAttributes(singleContext._meta?.privateAttributes); } else { // No early break isn't the most efficient, but it is an error condition. diff --git a/packages/shared/common/src/api/context/LDAutoEnv.ts b/packages/shared/common/src/api/context/LDAutoEnv.ts new file mode 100644 index 000000000..6032dd345 --- /dev/null +++ b/packages/shared/common/src/api/context/LDAutoEnv.ts @@ -0,0 +1,28 @@ +export interface LDApplication { + key: string; + envAttributesVersion: string; + id: string; + name: string; + version: string; + versionName: string; + locale: string; +} + +export interface LDDevice { + key: string; + envAttributesVersion: string; + manufacturer: string; + model: string; + storageBytes: string; + memoryBytes: string; + os: { + family: string; + name: string; + version: string; + }; +} + +export interface LDAutoEnv { + ld_application?: LDApplication; + ld_device?: LDDevice; +} diff --git a/packages/shared/common/src/api/context/LDMultiKindContext.ts b/packages/shared/common/src/api/context/LDMultiKindContext.ts index 8edabde44..01ca1589b 100644 --- a/packages/shared/common/src/api/context/LDMultiKindContext.ts +++ b/packages/shared/common/src/api/context/LDMultiKindContext.ts @@ -1,3 +1,4 @@ +import { LDAutoEnv } from './LDAutoEnv'; import { LDContextCommon } from './LDContextCommon'; /** @@ -31,7 +32,7 @@ import { LDContextCommon } from './LDContextCommon'; * The above multi-context contains both an 'org' and a 'user'. Each with their own key, * attributes, and _meta attributes. */ -export interface LDMultiKindContext { +export interface LDMultiKindContext extends LDAutoEnv { /** * The kind of the context. */ @@ -40,8 +41,10 @@ export interface LDMultiKindContext { /** * The contexts which compose this multi-kind context. * - * These should be of type LDContextCommon. "multi" is to allow - * for the top level "kind" attribute. + * These should be of type LDContextCommon with these exceptions: + * "multi" is to allow for the top level "kind" attribute. + * "undefined" is to allow for the top level "ld_application" and ld_device + * attributes. */ - [kind: string]: 'multi' | LDContextCommon; + [attribute: string]: 'multi' | LDContextCommon | undefined; } diff --git a/packages/shared/common/src/api/context/LDSingleKindContext.ts b/packages/shared/common/src/api/context/LDSingleKindContext.ts index 6ee63907b..bec138508 100644 --- a/packages/shared/common/src/api/context/LDSingleKindContext.ts +++ b/packages/shared/common/src/api/context/LDSingleKindContext.ts @@ -1,3 +1,4 @@ +import { LDAutoEnv } from './LDAutoEnv'; import { LDContextCommon } from './LDContextCommon'; /** @@ -16,7 +17,7 @@ import { LDContextCommon } from './LDContextCommon'; * The above context would be a single kind context representing an organization. It has a key * for that organization, and a single attribute 'someAttribute'. */ -export interface LDSingleKindContext extends LDContextCommon { +export interface LDSingleKindContext extends LDContextCommon, LDAutoEnv { /** * The kind of the context. */ diff --git a/packages/shared/common/src/api/context/index.ts b/packages/shared/common/src/api/context/index.ts index 4edf6d88a..d4415c70b 100644 --- a/packages/shared/common/src/api/context/index.ts +++ b/packages/shared/common/src/api/context/index.ts @@ -4,3 +4,4 @@ export * from './LDMultiKindContext'; export * from './LDSingleKindContext'; export * from './LDUser'; export * from './LDContext'; +export * from './LDAutoEnv'; diff --git a/packages/shared/common/src/internal/events/LDInternalOptions.ts b/packages/shared/common/src/internal/events/LDInternalOptions.ts index 898197051..17e025d79 100644 --- a/packages/shared/common/src/internal/events/LDInternalOptions.ts +++ b/packages/shared/common/src/internal/events/LDInternalOptions.ts @@ -1,3 +1,5 @@ +import { LDAutoEnv } from '../../api'; + /** * This is for internal use only. * @@ -12,4 +14,5 @@ export type LDInternalOptions = { analyticsEventPath?: string; diagnosticEventPath?: string; includeAuthorizationHeader?: boolean; + ldAutoEnv?: LDAutoEnv; }; diff --git a/packages/shared/sdk-client/src/LDClientImpl.ts b/packages/shared/sdk-client/src/LDClientImpl.ts index cfa3a7179..c29f3b38a 100644 --- a/packages/shared/sdk-client/src/LDClientImpl.ts +++ b/packages/shared/sdk-client/src/LDClientImpl.ts @@ -193,7 +193,7 @@ export default class LDClientImpl implements LDClient { */ protected createStreamUriPath(_context: LDContext): string { throw new Error( - 'createStreamUriPath not implemented. client sdks must implement createStreamUriPath for streamer to work', + 'createStreamUriPath not implemented. Client sdks must implement createStreamUriPath for streamer to work', ); } @@ -231,15 +231,21 @@ export default class LDClientImpl implements LDClient { } // TODO: implement secure mode - async identify(context: LDContext, _hash?: string): Promise { - const checkedContext = Context.fromLDContext(context); + async identify(c: LDContext, _hash?: string): Promise { + const checkedContext = Context.fromLDContext(c); if (!checkedContext.valid) { const error = new Error('Context was unspecified or had no key'); this.logger.error(error); - this.emitter.emit('error', context, error); + this.emitter.emit('error', c, error); return Promise.reject(error); } + // TODO: add auto env attributes here + const context = { + ...c, + ...this.config.ldAutoEnv, + }; + const { identifyPromise, identifyResolve } = this.createPromiseWithListeners(); this.logger.debug(`Identifying ${JSON.stringify(context)}`); this.emitter.emit('identifying', context); diff --git a/packages/shared/sdk-client/src/configuration/Configuration.ts b/packages/shared/sdk-client/src/configuration/Configuration.ts index 52fe58799..40533d2c4 100644 --- a/packages/shared/sdk-client/src/configuration/Configuration.ts +++ b/packages/shared/sdk-client/src/configuration/Configuration.ts @@ -2,6 +2,7 @@ import { ApplicationTags, createSafeLogger, internal, + LDAutoEnv, LDFlagSet, NumberWithMinimum, OptionMessages, @@ -41,6 +42,8 @@ export default class Configuration { public readonly tags: ApplicationTags; public readonly application?: { id?: string; version?: string }; public readonly bootstrap?: 'localStorage' | LDFlagSet; + + // TODO: implement requestHeaderTransform public readonly requestHeaderTransform?: (headers: Map) => Map; public readonly stream?: boolean; public readonly hash?: string; @@ -48,6 +51,7 @@ export default class Configuration { public readonly wrapperVersion?: string; public readonly serviceEndpoints: ServiceEndpoints; + public readonly ldAutoEnv?: LDAutoEnv; // Allow indexing Configuration by a string [index: string]: any; @@ -65,6 +69,9 @@ export default class Configuration { internalOptions.includeAuthorizationHeader, ); this.tags = new ApplicationTags({ application: this.application, logger: this.logger }); + + // TODO: save auto env attributes + this.ldAutoEnv = internalOptions.ldAutoEnv; } validateTypesAndNames(pristineOptions: LDOptions): string[] { From 7a151e9ac515c5922aa3cfada7cab772d42330a1 Mon Sep 17 00:00:00 2001 From: Yusinto Ngadiman Date: Thu, 4 Jan 2024 12:22:38 -0800 Subject: [PATCH 02/44] chore: Renamed ldAutoEnv to just autoEnv. --- .../shared/common/src/internal/events/LDInternalOptions.ts | 2 +- packages/shared/sdk-client/src/LDClientImpl.ts | 2 +- packages/shared/sdk-client/src/configuration/Configuration.ts | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/shared/common/src/internal/events/LDInternalOptions.ts b/packages/shared/common/src/internal/events/LDInternalOptions.ts index 17e025d79..cbba8a42d 100644 --- a/packages/shared/common/src/internal/events/LDInternalOptions.ts +++ b/packages/shared/common/src/internal/events/LDInternalOptions.ts @@ -14,5 +14,5 @@ export type LDInternalOptions = { analyticsEventPath?: string; diagnosticEventPath?: string; includeAuthorizationHeader?: boolean; - ldAutoEnv?: LDAutoEnv; + autoEnv?: LDAutoEnv; }; diff --git a/packages/shared/sdk-client/src/LDClientImpl.ts b/packages/shared/sdk-client/src/LDClientImpl.ts index c29f3b38a..3b1f3075a 100644 --- a/packages/shared/sdk-client/src/LDClientImpl.ts +++ b/packages/shared/sdk-client/src/LDClientImpl.ts @@ -243,7 +243,7 @@ export default class LDClientImpl implements LDClient { // TODO: add auto env attributes here const context = { ...c, - ...this.config.ldAutoEnv, + ...this.config.autoEnv, }; const { identifyPromise, identifyResolve } = this.createPromiseWithListeners(); diff --git a/packages/shared/sdk-client/src/configuration/Configuration.ts b/packages/shared/sdk-client/src/configuration/Configuration.ts index 40533d2c4..ed9f9f2ff 100644 --- a/packages/shared/sdk-client/src/configuration/Configuration.ts +++ b/packages/shared/sdk-client/src/configuration/Configuration.ts @@ -51,7 +51,7 @@ export default class Configuration { public readonly wrapperVersion?: string; public readonly serviceEndpoints: ServiceEndpoints; - public readonly ldAutoEnv?: LDAutoEnv; + public readonly autoEnv?: LDAutoEnv; // Allow indexing Configuration by a string [index: string]: any; @@ -71,7 +71,7 @@ export default class Configuration { this.tags = new ApplicationTags({ application: this.application, logger: this.logger }); // TODO: save auto env attributes - this.ldAutoEnv = internalOptions.ldAutoEnv; + this.autoEnv = internalOptions.autoEnv; } validateTypesAndNames(pristineOptions: LDOptions): string[] { From 393f68b0ad411aca22693c6a448cfbcfc659b82b Mon Sep 17 00:00:00 2001 From: Yusinto Ngadiman Date: Thu, 4 Jan 2024 14:42:54 -0800 Subject: [PATCH 03/44] chore: add autoEnv to internal Context class. --- packages/shared/common/src/Context.ts | 12 +++++++++++- .../shared/common/src/api/context/LDUser.ts | 4 +++- packages/shared/sdk-client/src/LDClientImpl.ts | 18 +++++++++--------- .../src/configuration/Configuration.ts | 2 +- 4 files changed, 24 insertions(+), 12 deletions(-) diff --git a/packages/shared/common/src/Context.ts b/packages/shared/common/src/Context.ts index b459ab6f9..52b7adb28 100644 --- a/packages/shared/common/src/Context.ts +++ b/packages/shared/common/src/Context.ts @@ -1,4 +1,5 @@ import type { + LDAutoEnv, LDContext, LDContextCommon, LDMultiKindContext, @@ -177,6 +178,10 @@ function legacyToSingleKind(user: LDUser): LDSingleKindContext { return singleKindContext; } +function createAutoEnv({ ld_application, ld_device }: LDContext) { + return { ld_application, ld_device }; +} + /** * Container for a context/contexts. Because contexts come from external code * they must be thoroughly validated and then formed to comply with @@ -205,6 +210,8 @@ export default class Context { public readonly message?: string; + public autoEnv?: LDAutoEnv; + static readonly userKind: string = DEFAULT_KIND; /** @@ -292,6 +299,7 @@ export default class Context { const kind = kinds[0]; const created = new Context(true, kind); created.context = contexts[kind]; + created.autoEnv = createAutoEnv(context); created.privateAttributeReferences = privateAttributes; created.isUser = kind === 'user'; return created; @@ -299,8 +307,8 @@ export default class Context { const created = new Context(true, context.kind); created.contexts = contexts; + created.autoEnv = createAutoEnv(context); created.privateAttributeReferences = privateAttributes; - created.isMulti = true; return created; } @@ -324,6 +332,7 @@ export default class Context { const created = new Context(true, kind); created.isUser = kind === 'user'; created.context = context; + created.autoEnv = createAutoEnv(context); created.privateAttributeReferences = { [kind]: privateAttributeReferences, }; @@ -337,6 +346,7 @@ export default class Context { return Context.contextForError('user', 'The key for the context was not valid'); } const created = new Context(true, 'user'); + created.autoEnv = createAutoEnv(context); created.isUser = true; created.wasLegacy = true; created.context = legacyToSingleKind(context); diff --git a/packages/shared/common/src/api/context/LDUser.ts b/packages/shared/common/src/api/context/LDUser.ts index d1fb2598e..78bc5edfe 100644 --- a/packages/shared/common/src/api/context/LDUser.ts +++ b/packages/shared/common/src/api/context/LDUser.ts @@ -1,9 +1,11 @@ +import { LDAutoEnv } from './LDAutoEnv'; + /** * A LaunchDarkly user object. * * @deprecated */ -export interface LDUser { +export interface LDUser extends LDAutoEnv { /** * A unique string identifying a user. */ diff --git a/packages/shared/sdk-client/src/LDClientImpl.ts b/packages/shared/sdk-client/src/LDClientImpl.ts index 3b1f3075a..12c47efe1 100644 --- a/packages/shared/sdk-client/src/LDClientImpl.ts +++ b/packages/shared/sdk-client/src/LDClientImpl.ts @@ -231,21 +231,21 @@ export default class LDClientImpl implements LDClient { } // TODO: implement secure mode - async identify(c: LDContext, _hash?: string): Promise { - const checkedContext = Context.fromLDContext(c); + async identify(pristineContext: LDContext, _hash?: string): Promise { + // the original context is injected with auto env attributes + const context = { + ...pristineContext, + ...this.config.autoEnv, + }; + + const checkedContext = Context.fromLDContext(context); if (!checkedContext.valid) { const error = new Error('Context was unspecified or had no key'); this.logger.error(error); - this.emitter.emit('error', c, error); + this.emitter.emit('error', context, error); return Promise.reject(error); } - // TODO: add auto env attributes here - const context = { - ...c, - ...this.config.autoEnv, - }; - const { identifyPromise, identifyResolve } = this.createPromiseWithListeners(); this.logger.debug(`Identifying ${JSON.stringify(context)}`); this.emitter.emit('identifying', context); diff --git a/packages/shared/sdk-client/src/configuration/Configuration.ts b/packages/shared/sdk-client/src/configuration/Configuration.ts index ed9f9f2ff..4369e86b3 100644 --- a/packages/shared/sdk-client/src/configuration/Configuration.ts +++ b/packages/shared/sdk-client/src/configuration/Configuration.ts @@ -70,7 +70,7 @@ export default class Configuration { ); this.tags = new ApplicationTags({ application: this.application, logger: this.logger }); - // TODO: save auto env attributes + // TODO: add logic to process auto env attributes correctly this.autoEnv = internalOptions.autoEnv; } From 0f1700c7186d6e3b847048e58cebbf81d5e8da03 Mon Sep 17 00:00:00 2001 From: Yusinto Ngadiman Date: Thu, 4 Jan 2024 14:50:28 -0800 Subject: [PATCH 04/44] chore: Add skeletal rn autoEnv code. --- .../react-native/src/ReactNativeLDClient.ts | 2 ++ .../sdk/react-native/src/createAutoEnv.ts | 29 +++++++++++++++++++ 2 files changed, 31 insertions(+) create mode 100644 packages/sdk/react-native/src/createAutoEnv.ts diff --git a/packages/sdk/react-native/src/ReactNativeLDClient.ts b/packages/sdk/react-native/src/ReactNativeLDClient.ts index 5058b4b9a..17f819d22 100644 --- a/packages/sdk/react-native/src/ReactNativeLDClient.ts +++ b/packages/sdk/react-native/src/ReactNativeLDClient.ts @@ -7,6 +7,7 @@ import { type LDOptions, } from '@launchdarkly/js-client-sdk-common'; +import createAutoEnv from './createAutoEnv'; import createPlatform from './platform'; /** @@ -41,6 +42,7 @@ export default class ReactNativeLDClient extends LDClientImpl { const internalOptions: internal.LDInternalOptions = { analyticsEventPath: `/mobile`, diagnosticEventPath: `/mobile/events/diagnostic`, + autoEnv: createAutoEnv(), }; super(sdkKey, createPlatform(logger), { ...options, logger }, internalOptions); diff --git a/packages/sdk/react-native/src/createAutoEnv.ts b/packages/sdk/react-native/src/createAutoEnv.ts new file mode 100644 index 000000000..8f39ed9c9 --- /dev/null +++ b/packages/sdk/react-native/src/createAutoEnv.ts @@ -0,0 +1,29 @@ +import type { LDAutoEnv } from '@launchdarkly/js-sdk-common'; + +export default function createAutoEnv(): LDAutoEnv { + // TODO: populate env fields correctly + return { + ld_application: { + key: '', + envAttributesVersion: '', + id: '', + name: '', + version: '', + versionName: '', + locale: '', + }, + ld_device: { + key: '', + envAttributesVersion: '', + manufacturer: '', + model: '', + storageBytes: '', + memoryBytes: '', + os: { + family: '', + name: '', + version: '', + }, + }, + }; +} From 7bf8e043123822639797ac04abe4c08a1d054828 Mon Sep 17 00:00:00 2001 From: Yusinto Ngadiman Date: Fri, 5 Jan 2024 12:07:56 -0800 Subject: [PATCH 05/44] Update createAutoEnv.ts --- packages/sdk/react-native/src/createAutoEnv.ts | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/packages/sdk/react-native/src/createAutoEnv.ts b/packages/sdk/react-native/src/createAutoEnv.ts index 8f39ed9c9..40aecef7e 100644 --- a/packages/sdk/react-native/src/createAutoEnv.ts +++ b/packages/sdk/react-native/src/createAutoEnv.ts @@ -4,6 +4,13 @@ export default function createAutoEnv(): LDAutoEnv { // TODO: populate env fields correctly return { ld_application: { + /** + * Priority for id, version and versionName: + * + * 1. Customer provided values via configuration or provided otherwise + * 2. Application info collected from the platform via platform APIs + * 3. LaunchDarkly SDK info such as SDK name and version + */ key: '', envAttributesVersion: '', id: '', From 891768199e863983c581e245a4c47d765a141480 Mon Sep 17 00:00:00 2001 From: Yusinto Ngadiman Date: Sat, 13 Jan 2024 17:49:28 -0800 Subject: [PATCH 06/44] chore: move autoEnv to platform.info. --- packages/sdk/react-native/src/ReactNativeLDClient.ts | 3 +-- .../sdk/react-native/src/{ => platform}/createAutoEnv.ts | 0 packages/sdk/react-native/src/platform/index.ts | 2 ++ packages/shared/common/src/api/platform/Info.ts | 8 ++++++++ .../common/src/internal/events/LDInternalOptions.ts | 3 --- packages/shared/sdk-client/src/LDClientImpl.ts | 7 ++++++- .../shared/sdk-client/src/configuration/Configuration.ts | 4 ---- 7 files changed, 17 insertions(+), 10 deletions(-) rename packages/sdk/react-native/src/{ => platform}/createAutoEnv.ts (100%) diff --git a/packages/sdk/react-native/src/ReactNativeLDClient.ts b/packages/sdk/react-native/src/ReactNativeLDClient.ts index 17f819d22..663f0aee7 100644 --- a/packages/sdk/react-native/src/ReactNativeLDClient.ts +++ b/packages/sdk/react-native/src/ReactNativeLDClient.ts @@ -7,8 +7,8 @@ import { type LDOptions, } from '@launchdarkly/js-client-sdk-common'; -import createAutoEnv from './createAutoEnv'; import createPlatform from './platform'; +import createAutoEnv from './platform/createAutoEnv'; /** * The React Native LaunchDarkly client. Instantiate this class to create an @@ -42,7 +42,6 @@ export default class ReactNativeLDClient extends LDClientImpl { const internalOptions: internal.LDInternalOptions = { analyticsEventPath: `/mobile`, diagnosticEventPath: `/mobile/events/diagnostic`, - autoEnv: createAutoEnv(), }; super(sdkKey, createPlatform(logger), { ...options, logger }, internalOptions); diff --git a/packages/sdk/react-native/src/createAutoEnv.ts b/packages/sdk/react-native/src/platform/createAutoEnv.ts similarity index 100% rename from packages/sdk/react-native/src/createAutoEnv.ts rename to packages/sdk/react-native/src/platform/createAutoEnv.ts diff --git a/packages/sdk/react-native/src/platform/index.ts b/packages/sdk/react-native/src/platform/index.ts index ba2db6299..037086340 100644 --- a/packages/sdk/react-native/src/platform/index.ts +++ b/packages/sdk/react-native/src/platform/index.ts @@ -22,6 +22,7 @@ import { name, version } from '../../package.json'; import { btoa, uuidv4 } from '../polyfills'; import RNEventSource from '../react-native-sse'; import AsyncStorage from './ConditionalAsyncStorage'; +import createAutoEnv from './createAutoEnv'; class PlatformRequests implements Requests { createEventSource(url: string, eventSourceInitDict: EventSourceInitDict): EventSource { @@ -46,6 +47,7 @@ class PlatformInfo implements Info { platformData(): PlatformData { return { name: 'React Native', + autoEnv: createAutoEnv(), }; } diff --git a/packages/shared/common/src/api/platform/Info.ts b/packages/shared/common/src/api/platform/Info.ts index 9b0bf40f6..7a014781d 100644 --- a/packages/shared/common/src/api/platform/Info.ts +++ b/packages/shared/common/src/api/platform/Info.ts @@ -1,3 +1,5 @@ +import { LDAutoEnv } from '../context'; + /** * Information about the platform of the SDK and the environment it is executing. */ @@ -31,6 +33,12 @@ export interface PlatformData { * Any additional attributes associated with the platform. */ additional?: Record; + + /** + * Additional information about the executing environment. Should be populated + * when available. Not all platforms will make this data accessible. + */ + autoEnv?: LDAutoEnv; } export interface SdkData { diff --git a/packages/shared/common/src/internal/events/LDInternalOptions.ts b/packages/shared/common/src/internal/events/LDInternalOptions.ts index cbba8a42d..898197051 100644 --- a/packages/shared/common/src/internal/events/LDInternalOptions.ts +++ b/packages/shared/common/src/internal/events/LDInternalOptions.ts @@ -1,5 +1,3 @@ -import { LDAutoEnv } from '../../api'; - /** * This is for internal use only. * @@ -14,5 +12,4 @@ export type LDInternalOptions = { analyticsEventPath?: string; diagnosticEventPath?: string; includeAuthorizationHeader?: boolean; - autoEnv?: LDAutoEnv; }; diff --git a/packages/shared/sdk-client/src/LDClientImpl.ts b/packages/shared/sdk-client/src/LDClientImpl.ts index 12c47efe1..6b04e7cf0 100644 --- a/packages/shared/sdk-client/src/LDClientImpl.ts +++ b/packages/shared/sdk-client/src/LDClientImpl.ts @@ -3,6 +3,7 @@ import { clone, Context, internal, + LDAutoEnv, LDClientError, LDContext, LDEvaluationDetail, @@ -45,6 +46,7 @@ export default class LDClientImpl implements LDClient { private identifyErrorListener?: (c: LDContext, err: any) => void; private readonly clientContext: ClientContext; + private readonly autoEnv?: LDAutoEnv; /** * Creates the client object synchronously. No async, no network calls. @@ -74,6 +76,9 @@ export default class LDClientImpl implements LDClient { this.diagnosticsManager, ); this.emitter = new LDEmitter(); + + // TODO: add logic to process auto env attributes correctly + this.autoEnv = platform.info.platformData().autoEnv; } allFlags(): LDFlagSet { @@ -235,7 +240,7 @@ export default class LDClientImpl implements LDClient { // the original context is injected with auto env attributes const context = { ...pristineContext, - ...this.config.autoEnv, + ...this.autoEnv, }; const checkedContext = Context.fromLDContext(context); diff --git a/packages/shared/sdk-client/src/configuration/Configuration.ts b/packages/shared/sdk-client/src/configuration/Configuration.ts index 4369e86b3..bb1099e69 100644 --- a/packages/shared/sdk-client/src/configuration/Configuration.ts +++ b/packages/shared/sdk-client/src/configuration/Configuration.ts @@ -51,7 +51,6 @@ export default class Configuration { public readonly wrapperVersion?: string; public readonly serviceEndpoints: ServiceEndpoints; - public readonly autoEnv?: LDAutoEnv; // Allow indexing Configuration by a string [index: string]: any; @@ -69,9 +68,6 @@ export default class Configuration { internalOptions.includeAuthorizationHeader, ); this.tags = new ApplicationTags({ application: this.application, logger: this.logger }); - - // TODO: add logic to process auto env attributes correctly - this.autoEnv = internalOptions.autoEnv; } validateTypesAndNames(pristineOptions: LDOptions): string[] { From a36749cc44a0817a72d36becaf7ff832a7153d7e Mon Sep 17 00:00:00 2001 From: Yusinto Ngadiman Date: Sat, 13 Jan 2024 17:59:58 -0800 Subject: [PATCH 07/44] chore: rename createAutoEnv to reconstruct. --- packages/shared/common/src/Context.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/shared/common/src/Context.ts b/packages/shared/common/src/Context.ts index 52b7adb28..ee7a0e12b 100644 --- a/packages/shared/common/src/Context.ts +++ b/packages/shared/common/src/Context.ts @@ -178,7 +178,7 @@ function legacyToSingleKind(user: LDUser): LDSingleKindContext { return singleKindContext; } -function createAutoEnv({ ld_application, ld_device }: LDContext) { +function reconstructAutoEnv({ ld_application, ld_device }: LDContext) { return { ld_application, ld_device }; } @@ -299,7 +299,7 @@ export default class Context { const kind = kinds[0]; const created = new Context(true, kind); created.context = contexts[kind]; - created.autoEnv = createAutoEnv(context); + created.autoEnv = reconstructAutoEnv(context); created.privateAttributeReferences = privateAttributes; created.isUser = kind === 'user'; return created; @@ -307,7 +307,7 @@ export default class Context { const created = new Context(true, context.kind); created.contexts = contexts; - created.autoEnv = createAutoEnv(context); + created.autoEnv = reconstructAutoEnv(context); created.privateAttributeReferences = privateAttributes; created.isMulti = true; return created; @@ -332,7 +332,7 @@ export default class Context { const created = new Context(true, kind); created.isUser = kind === 'user'; created.context = context; - created.autoEnv = createAutoEnv(context); + created.autoEnv = reconstructAutoEnv(context); created.privateAttributeReferences = { [kind]: privateAttributeReferences, }; @@ -346,7 +346,7 @@ export default class Context { return Context.contextForError('user', 'The key for the context was not valid'); } const created = new Context(true, 'user'); - created.autoEnv = createAutoEnv(context); + created.autoEnv = reconstructAutoEnv(context); created.isUser = true; created.wasLegacy = true; created.context = legacyToSingleKind(context); From 0c5e20e933bec5e1423af48fb628851d04b57b40 Mon Sep 17 00:00:00 2001 From: Yusinto Ngadiman Date: Sat, 13 Jan 2024 18:53:39 -0800 Subject: [PATCH 08/44] chore: Added react-native-device-info. Populated autoEnv attributes. --- packages/sdk/react-native/package.json | 3 +- .../src/platform/createAutoEnv.ts | 42 ++++++++++++------- .../common/src/api/context/LDAutoEnv.ts | 21 ++++++++++ 3 files changed, 50 insertions(+), 16 deletions(-) diff --git a/packages/sdk/react-native/package.json b/packages/sdk/react-native/package.json index b41394993..8436c9364 100644 --- a/packages/sdk/react-native/package.json +++ b/packages/sdk/react-native/package.json @@ -47,7 +47,8 @@ "@launchdarkly/js-client-sdk-common": "0.1.1", "@react-native-async-storage/async-storage": "^1.21.0", "base64-js": "^1.5.1", - "event-target-shim": "^6.0.2" + "event-target-shim": "^6.0.2", + "react-native-device-info": "^10.12.0" }, "devDependencies": { "@testing-library/react": "^14.1.2", diff --git a/packages/sdk/react-native/src/platform/createAutoEnv.ts b/packages/sdk/react-native/src/platform/createAutoEnv.ts index 40aecef7e..846e689fd 100644 --- a/packages/sdk/react-native/src/platform/createAutoEnv.ts +++ b/packages/sdk/react-native/src/platform/createAutoEnv.ts @@ -1,3 +1,15 @@ +import { Platform } from 'react-native'; +import { + getApplicationName, + getBundleId, + getManufacturerSync, + getMaxMemorySync, + getModel, + getReadableVersion, + getTotalDiskCapacitySync, + getVersion, +} from 'react-native-device-info'; + import type { LDAutoEnv } from '@launchdarkly/js-sdk-common'; export default function createAutoEnv(): LDAutoEnv { @@ -11,25 +23,25 @@ export default function createAutoEnv(): LDAutoEnv { * 2. Application info collected from the platform via platform APIs * 3. LaunchDarkly SDK info such as SDK name and version */ - key: '', - envAttributesVersion: '', - id: '', - name: '', - version: '', - versionName: '', - locale: '', + key: '', // TODO: needs context key + envAttributesVersion: '1.0', + id: getBundleId(), + name: getApplicationName(), + version: getVersion(), + versionName: getReadableVersion(), + locale: '', // TODO: needs https://github.com/zoontek/react-native-localize }, ld_device: { key: '', - envAttributesVersion: '', - manufacturer: '', - model: '', - storageBytes: '', - memoryBytes: '', + envAttributesVersion: '1.0', + manufacturer: getManufacturerSync(), + model: getModel(), // "iPhone 11 Pro" or "Samsung Galaxy S10" + storageBytes: getTotalDiskCapacitySync().toString(), + memoryBytes: getMaxMemorySync().toString(), os: { - family: '', - name: '', - version: '', + family: Platform.OS, // TODO: investigate more + name: Platform.OS, + version: Platform.Version.toString(), }, }, }; diff --git a/packages/shared/common/src/api/context/LDAutoEnv.ts b/packages/shared/common/src/api/context/LDAutoEnv.ts index 6032dd345..4216ffd9b 100644 --- a/packages/shared/common/src/api/context/LDAutoEnv.ts +++ b/packages/shared/common/src/api/context/LDAutoEnv.ts @@ -1,6 +1,17 @@ export interface LDApplication { + /** + * Unique key for the context kind. + */ key: string; + + /** + * Version of the environment attributes schema being used. + */ envAttributesVersion: string; + + /** + * Unique identifier of the application. + */ id: string; name: string; version: string; @@ -9,13 +20,23 @@ export interface LDApplication { } export interface LDDevice { + /** + * Unique key for the context kind. + */ key: string; + + /** + * Version of the environment attributes schema being used. + */ envAttributesVersion: string; manufacturer: string; model: string; storageBytes: string; memoryBytes: string; os: { + /** + * The family of operating system. + */ family: string; name: string; version: string; From e7bff679ffecbc9db2a1bd39e0d9068e37b84653 Mon Sep 17 00:00:00 2001 From: Yusinto Ngadiman Date: Sat, 13 Jan 2024 21:49:28 -0800 Subject: [PATCH 09/44] chore: Added react-native-localize. TODO: dev-info and localize packages are not expo go compatible. --- .../sdk/react-native/example/package.json | 4 ++- packages/sdk/react-native/example/yarn.lock | 25 ++++++++++++++++ packages/sdk/react-native/package.json | 3 +- .../src/platform/createAutoEnv.ts | 30 +++++++++++++++---- 4 files changed, 55 insertions(+), 7 deletions(-) diff --git a/packages/sdk/react-native/example/package.json b/packages/sdk/react-native/example/package.json index df53d7ba1..6114fec43 100644 --- a/packages/sdk/react-native/example/package.json +++ b/packages/sdk/react-native/example/package.json @@ -27,7 +27,9 @@ "expo-splash-screen": "~0.20.5", "expo-status-bar": "~1.7.1", "react": "18.2.0", - "react-native": "0.72.6" + "react-native": "0.72.6", + "react-native-device-info": "^10.12.0", + "react-native-localize": "^3.0.5" }, "devDependencies": { "@babel/core": "^7.20.0", diff --git a/packages/sdk/react-native/example/yarn.lock b/packages/sdk/react-native/example/yarn.lock index caafa8c67..6f87b1ba4 100644 --- a/packages/sdk/react-native/example/yarn.lock +++ b/packages/sdk/react-native/example/yarn.lock @@ -7980,6 +7980,15 @@ __metadata: languageName: node linkType: hard +"react-native-device-info@npm:^10.12.0": + version: 10.12.0 + resolution: "react-native-device-info@npm:10.12.0" + peerDependencies: + react-native: "*" + checksum: dc3a4ada40f0de960f652f1e7962c5f2b538695cad5ad11ca85a6ab445a7f01a7123dceeb7ead234dd110a54935cb9c10fccc648cfd0d6f157041e9299075f74 + languageName: node + linkType: hard + "react-native-dotenv@npm:^3.4.9": version: 3.4.9 resolution: "react-native-dotenv@npm:3.4.9" @@ -8008,12 +8017,28 @@ __metadata: expo-status-bar: ~1.7.1 react: 18.2.0 react-native: 0.72.6 + react-native-device-info: ^10.12.0 react-native-dotenv: ^3.4.9 + react-native-localize: ^3.0.5 ts-jest: ^29.1.1 typescript: ^5.2.2 languageName: unknown linkType: soft +"react-native-localize@npm:^3.0.5": + version: 3.0.5 + resolution: "react-native-localize@npm:3.0.5" + peerDependencies: + react: ">=18.1.0" + react-native: ">=0.70.0" + react-native-macos: ">=0.70.0" + peerDependenciesMeta: + react-native-macos: + optional: true + checksum: 4c9f6daac12cdb8821d6e24be0bce7377286a3d9ece2f23d0906de260be344e7f928f80fe4f0dc36cefc715af8cbd46c507f004075d3906cb43b2df6ed1414b6 + languageName: node + linkType: hard + "react-native@npm:0.72.6": version: 0.72.6 resolution: "react-native@npm:0.72.6" diff --git a/packages/sdk/react-native/package.json b/packages/sdk/react-native/package.json index 8436c9364..f6ab36cf7 100644 --- a/packages/sdk/react-native/package.json +++ b/packages/sdk/react-native/package.json @@ -48,7 +48,8 @@ "@react-native-async-storage/async-storage": "^1.21.0", "base64-js": "^1.5.1", "event-target-shim": "^6.0.2", - "react-native-device-info": "^10.12.0" + "react-native-device-info": "^10.12.0", + "react-native-localize": "^3.0.5" }, "devDependencies": { "@testing-library/react": "^14.1.2", diff --git a/packages/sdk/react-native/src/platform/createAutoEnv.ts b/packages/sdk/react-native/src/platform/createAutoEnv.ts index 846e689fd..e51552cef 100644 --- a/packages/sdk/react-native/src/platform/createAutoEnv.ts +++ b/packages/sdk/react-native/src/platform/createAutoEnv.ts @@ -9,11 +9,31 @@ import { getTotalDiskCapacitySync, getVersion, } from 'react-native-device-info'; +import { getLocales } from 'react-native-localize'; import type { LDAutoEnv } from '@launchdarkly/js-sdk-common'; +// const sampleAutoEnv = { +// ld_application: { +// key: '', +// envAttributesVersion: '1.0', +// id: 'com.anonymous.reactnativeexample', +// name: 'react-native-example', +// version: '1.0.0', +// versionName: '1.0.0.1', +// locale: 'en-US', +// }, +// ld_device: { +// key: '', +// envAttributesVersion: '1.0', +// manufacturer: 'Apple', +// model: 'iPhone SE', +// storageBytes: '494384795648', +// memoryBytes: '-1', +// os: { family: 'ios', name: 'ios', version: '17.2' }, +// }, +// }; export default function createAutoEnv(): LDAutoEnv { - // TODO: populate env fields correctly return { ld_application: { /** @@ -23,23 +43,23 @@ export default function createAutoEnv(): LDAutoEnv { * 2. Application info collected from the platform via platform APIs * 3. LaunchDarkly SDK info such as SDK name and version */ - key: '', // TODO: needs context key + key: '', envAttributesVersion: '1.0', id: getBundleId(), name: getApplicationName(), version: getVersion(), versionName: getReadableVersion(), - locale: '', // TODO: needs https://github.com/zoontek/react-native-localize + locale: getLocales()[0].languageTag, }, ld_device: { key: '', envAttributesVersion: '1.0', manufacturer: getManufacturerSync(), - model: getModel(), // "iPhone 11 Pro" or "Samsung Galaxy S10" + model: getModel(), storageBytes: getTotalDiskCapacitySync().toString(), memoryBytes: getMaxMemorySync().toString(), os: { - family: Platform.OS, // TODO: investigate more + family: Platform.OS, name: Platform.OS, version: Platform.Version.toString(), }, From 00add5e6bab01b0808fb9dd0a489c5bae55d7839 Mon Sep 17 00:00:00 2001 From: Yusinto Ngadiman Date: Sun, 14 Jan 2024 16:21:06 -0800 Subject: [PATCH 10/44] chore: Conditionally import device-info and localize packages. Refactored common properties to its own interface. --- .../react-native/src/ReactNativeLDClient.ts | 1 - .../sdk/react-native/src/platform/AutoEnv.ts | 72 +++++++++++++++++++ .../src/platform/createAutoEnv.ts | 68 ------------------ .../sdk/react-native/src/platform/index.ts | 4 +- .../common/src/api/context/LDAutoEnv.ts | 35 ++++----- 5 files changed, 88 insertions(+), 92 deletions(-) create mode 100644 packages/sdk/react-native/src/platform/AutoEnv.ts delete mode 100644 packages/sdk/react-native/src/platform/createAutoEnv.ts diff --git a/packages/sdk/react-native/src/ReactNativeLDClient.ts b/packages/sdk/react-native/src/ReactNativeLDClient.ts index 663f0aee7..5058b4b9a 100644 --- a/packages/sdk/react-native/src/ReactNativeLDClient.ts +++ b/packages/sdk/react-native/src/ReactNativeLDClient.ts @@ -8,7 +8,6 @@ import { } from '@launchdarkly/js-client-sdk-common'; import createPlatform from './platform'; -import createAutoEnv from './platform/createAutoEnv'; /** * The React Native LaunchDarkly client. Instantiate this class to create an diff --git a/packages/sdk/react-native/src/platform/AutoEnv.ts b/packages/sdk/react-native/src/platform/AutoEnv.ts new file mode 100644 index 000000000..384272c0b --- /dev/null +++ b/packages/sdk/react-native/src/platform/AutoEnv.ts @@ -0,0 +1,72 @@ +/* eslint-disable import/no-mutable-exports,global-require */ +import { Platform } from 'react-native'; + +import type { LDAutoEnv, LDAutoEnvCommon } from '@launchdarkly/js-sdk-common'; + +/** + * Priority for id, version and versionName: + * + * 1. Customer provided values via configuration or provided otherwise + * 2. Application info collected from the platform via platform APIs + * 3. LaunchDarkly SDK info such as SDK name and version + */ +const common: LDAutoEnvCommon = { + key: '', + envAttributesVersion: '1.0', +}; + +const defaultAutoEnv: LDAutoEnv = { + ld_application: { + ...common, + }, + ld_device: { + ...common, + os: { + family: Platform.OS, + name: Platform.OS, + version: Platform.Version.toString(), + }, + }, +}; + +let autoEnv = defaultAutoEnv; + +try { + const { + getApplicationName, + getBundleId, + getManufacturerSync, + getMaxMemorySync, + getModel, + getReadableVersion, + getTotalDiskCapacitySync, + getVersion, + } = require('react-native-device-info'); + const { getLocales } = require('react-native-localize'); + + console.log(`======= DeviceInfo supported`); + + autoEnv = { + // @ts-ignore + ld_application: { + ...defaultAutoEnv.ld_application, + id: getBundleId(), + name: getApplicationName(), + version: getVersion(), + versionName: getReadableVersion(), + locale: getLocales()[0].languageTag, + }, + // @ts-ignore + ld_device: { + ...defaultAutoEnv.ld_device, + manufacturer: getManufacturerSync(), + model: getModel(), + storageBytes: getTotalDiskCapacitySync().toString(), + memoryBytes: getMaxMemorySync().toString(), + }, + }; +} catch (e) { + console.log(`======= DeviceInfo not supported, using default values. Error: ${e}`); +} + +export default autoEnv; diff --git a/packages/sdk/react-native/src/platform/createAutoEnv.ts b/packages/sdk/react-native/src/platform/createAutoEnv.ts deleted file mode 100644 index e51552cef..000000000 --- a/packages/sdk/react-native/src/platform/createAutoEnv.ts +++ /dev/null @@ -1,68 +0,0 @@ -import { Platform } from 'react-native'; -import { - getApplicationName, - getBundleId, - getManufacturerSync, - getMaxMemorySync, - getModel, - getReadableVersion, - getTotalDiskCapacitySync, - getVersion, -} from 'react-native-device-info'; -import { getLocales } from 'react-native-localize'; - -import type { LDAutoEnv } from '@launchdarkly/js-sdk-common'; - -// const sampleAutoEnv = { -// ld_application: { -// key: '', -// envAttributesVersion: '1.0', -// id: 'com.anonymous.reactnativeexample', -// name: 'react-native-example', -// version: '1.0.0', -// versionName: '1.0.0.1', -// locale: 'en-US', -// }, -// ld_device: { -// key: '', -// envAttributesVersion: '1.0', -// manufacturer: 'Apple', -// model: 'iPhone SE', -// storageBytes: '494384795648', -// memoryBytes: '-1', -// os: { family: 'ios', name: 'ios', version: '17.2' }, -// }, -// }; -export default function createAutoEnv(): LDAutoEnv { - return { - ld_application: { - /** - * Priority for id, version and versionName: - * - * 1. Customer provided values via configuration or provided otherwise - * 2. Application info collected from the platform via platform APIs - * 3. LaunchDarkly SDK info such as SDK name and version - */ - key: '', - envAttributesVersion: '1.0', - id: getBundleId(), - name: getApplicationName(), - version: getVersion(), - versionName: getReadableVersion(), - locale: getLocales()[0].languageTag, - }, - ld_device: { - key: '', - envAttributesVersion: '1.0', - manufacturer: getManufacturerSync(), - model: getModel(), - storageBytes: getTotalDiskCapacitySync().toString(), - memoryBytes: getMaxMemorySync().toString(), - os: { - family: Platform.OS, - name: Platform.OS, - version: Platform.Version.toString(), - }, - }, - }; -} diff --git a/packages/sdk/react-native/src/platform/index.ts b/packages/sdk/react-native/src/platform/index.ts index 037086340..9fc3c9c0f 100644 --- a/packages/sdk/react-native/src/platform/index.ts +++ b/packages/sdk/react-native/src/platform/index.ts @@ -21,8 +21,8 @@ import type { import { name, version } from '../../package.json'; import { btoa, uuidv4 } from '../polyfills'; import RNEventSource from '../react-native-sse'; +import AutoEnv from './AutoEnv'; import AsyncStorage from './ConditionalAsyncStorage'; -import createAutoEnv from './createAutoEnv'; class PlatformRequests implements Requests { createEventSource(url: string, eventSourceInitDict: EventSourceInitDict): EventSource { @@ -47,7 +47,7 @@ class PlatformInfo implements Info { platformData(): PlatformData { return { name: 'React Native', - autoEnv: createAutoEnv(), + autoEnv: AutoEnv, }; } diff --git a/packages/shared/common/src/api/context/LDAutoEnv.ts b/packages/shared/common/src/api/context/LDAutoEnv.ts index 4216ffd9b..33aa47b90 100644 --- a/packages/shared/common/src/api/context/LDAutoEnv.ts +++ b/packages/shared/common/src/api/context/LDAutoEnv.ts @@ -1,4 +1,4 @@ -export interface LDApplication { +export interface LDAutoEnvCommon { /** * Unique key for the context kind. */ @@ -8,32 +8,25 @@ export interface LDApplication { * Version of the environment attributes schema being used. */ envAttributesVersion: string; +} +export interface LDApplication extends LDAutoEnvCommon { /** * Unique identifier of the application. */ - id: string; - name: string; - version: string; - versionName: string; - locale: string; + id?: string; + name?: string; + version?: string; + versionName?: string; + locale?: string; } -export interface LDDevice { - /** - * Unique key for the context kind. - */ - key: string; - - /** - * Version of the environment attributes schema being used. - */ - envAttributesVersion: string; - manufacturer: string; - model: string; - storageBytes: string; - memoryBytes: string; - os: { +export interface LDDevice extends LDAutoEnvCommon { + manufacturer?: string; + model?: string; + storageBytes?: string; + memoryBytes?: string; + os?: { /** * The family of operating system. */ From f17be6b2ff214695405dca9c047362e31e154e9e Mon Sep 17 00:00:00 2001 From: Yusinto Ngadiman Date: Mon, 15 Jan 2024 12:19:20 -0800 Subject: [PATCH 11/44] chore: Removed 3rd party packages. Added locale. --- .../sdk/react-native/example/package.json | 4 +- packages/sdk/react-native/example/yarn.lock | 25 -------- packages/sdk/react-native/package.json | 4 +- .../sdk/react-native/src/platform/AutoEnv.ts | 59 ++++++------------- packages/sdk/react-native/src/utils/locale.ts | 12 ++++ 5 files changed, 31 insertions(+), 73 deletions(-) create mode 100644 packages/sdk/react-native/src/utils/locale.ts diff --git a/packages/sdk/react-native/example/package.json b/packages/sdk/react-native/example/package.json index 6114fec43..df53d7ba1 100644 --- a/packages/sdk/react-native/example/package.json +++ b/packages/sdk/react-native/example/package.json @@ -27,9 +27,7 @@ "expo-splash-screen": "~0.20.5", "expo-status-bar": "~1.7.1", "react": "18.2.0", - "react-native": "0.72.6", - "react-native-device-info": "^10.12.0", - "react-native-localize": "^3.0.5" + "react-native": "0.72.6" }, "devDependencies": { "@babel/core": "^7.20.0", diff --git a/packages/sdk/react-native/example/yarn.lock b/packages/sdk/react-native/example/yarn.lock index 6f87b1ba4..caafa8c67 100644 --- a/packages/sdk/react-native/example/yarn.lock +++ b/packages/sdk/react-native/example/yarn.lock @@ -7980,15 +7980,6 @@ __metadata: languageName: node linkType: hard -"react-native-device-info@npm:^10.12.0": - version: 10.12.0 - resolution: "react-native-device-info@npm:10.12.0" - peerDependencies: - react-native: "*" - checksum: dc3a4ada40f0de960f652f1e7962c5f2b538695cad5ad11ca85a6ab445a7f01a7123dceeb7ead234dd110a54935cb9c10fccc648cfd0d6f157041e9299075f74 - languageName: node - linkType: hard - "react-native-dotenv@npm:^3.4.9": version: 3.4.9 resolution: "react-native-dotenv@npm:3.4.9" @@ -8017,28 +8008,12 @@ __metadata: expo-status-bar: ~1.7.1 react: 18.2.0 react-native: 0.72.6 - react-native-device-info: ^10.12.0 react-native-dotenv: ^3.4.9 - react-native-localize: ^3.0.5 ts-jest: ^29.1.1 typescript: ^5.2.2 languageName: unknown linkType: soft -"react-native-localize@npm:^3.0.5": - version: 3.0.5 - resolution: "react-native-localize@npm:3.0.5" - peerDependencies: - react: ">=18.1.0" - react-native: ">=0.70.0" - react-native-macos: ">=0.70.0" - peerDependenciesMeta: - react-native-macos: - optional: true - checksum: 4c9f6daac12cdb8821d6e24be0bce7377286a3d9ece2f23d0906de260be344e7f928f80fe4f0dc36cefc715af8cbd46c507f004075d3906cb43b2df6ed1414b6 - languageName: node - linkType: hard - "react-native@npm:0.72.6": version: 0.72.6 resolution: "react-native@npm:0.72.6" diff --git a/packages/sdk/react-native/package.json b/packages/sdk/react-native/package.json index f6ab36cf7..b41394993 100644 --- a/packages/sdk/react-native/package.json +++ b/packages/sdk/react-native/package.json @@ -47,9 +47,7 @@ "@launchdarkly/js-client-sdk-common": "0.1.1", "@react-native-async-storage/async-storage": "^1.21.0", "base64-js": "^1.5.1", - "event-target-shim": "^6.0.2", - "react-native-device-info": "^10.12.0", - "react-native-localize": "^3.0.5" + "event-target-shim": "^6.0.2" }, "devDependencies": { "@testing-library/react": "^14.1.2", diff --git a/packages/sdk/react-native/src/platform/AutoEnv.ts b/packages/sdk/react-native/src/platform/AutoEnv.ts index 384272c0b..adc2adda6 100644 --- a/packages/sdk/react-native/src/platform/AutoEnv.ts +++ b/packages/sdk/react-native/src/platform/AutoEnv.ts @@ -1,8 +1,9 @@ -/* eslint-disable import/no-mutable-exports,global-require */ import { Platform } from 'react-native'; import type { LDAutoEnv, LDAutoEnvCommon } from '@launchdarkly/js-sdk-common'; +import locale from '../utils/locale'; + /** * Priority for id, version and versionName: * @@ -15,58 +16,32 @@ const common: LDAutoEnvCommon = { envAttributesVersion: '1.0', }; -const defaultAutoEnv: LDAutoEnv = { +const autoEnv: LDAutoEnv = { ld_application: { ...common, + // id: getBundleId(), + // name: getApplicationName(), + // version: getVersion(), + // versionName: getReadableVersion(), + locale, }, ld_device: { ...common, + // manufacturer: getManufacturerSync(), + // model: getModel(), + // storageBytes: getTotalDiskCapacitySync().toString() + // memoryBytes: getMaxMemorySync().toString(), os: { - family: Platform.OS, + family: Platform.select({ + ios: 'apple', + default: Platform.OS, + }), name: Platform.OS, version: Platform.Version.toString(), }, }, }; -let autoEnv = defaultAutoEnv; - -try { - const { - getApplicationName, - getBundleId, - getManufacturerSync, - getMaxMemorySync, - getModel, - getReadableVersion, - getTotalDiskCapacitySync, - getVersion, - } = require('react-native-device-info'); - const { getLocales } = require('react-native-localize'); - - console.log(`======= DeviceInfo supported`); - - autoEnv = { - // @ts-ignore - ld_application: { - ...defaultAutoEnv.ld_application, - id: getBundleId(), - name: getApplicationName(), - version: getVersion(), - versionName: getReadableVersion(), - locale: getLocales()[0].languageTag, - }, - // @ts-ignore - ld_device: { - ...defaultAutoEnv.ld_device, - manufacturer: getManufacturerSync(), - model: getModel(), - storageBytes: getTotalDiskCapacitySync().toString(), - memoryBytes: getMaxMemorySync().toString(), - }, - }; -} catch (e) { - console.log(`======= DeviceInfo not supported, using default values. Error: ${e}`); -} +console.log(`===== autoenv: ${JSON.stringify(autoEnv, null, 2)}`); export default autoEnv; diff --git a/packages/sdk/react-native/src/utils/locale.ts b/packages/sdk/react-native/src/utils/locale.ts new file mode 100644 index 000000000..d5a5fab5e --- /dev/null +++ b/packages/sdk/react-native/src/utils/locale.ts @@ -0,0 +1,12 @@ +import { NativeModules, Platform } from 'react-native'; + +/** + * Ripped from: + * https://dev.to/medaimane/localization-and-internationalization-in-react-native-reaching-global-audiences-3acj + */ +const locale = + Platform.OS === 'ios' + ? NativeModules.SettingsManager.settings.AppleLocale // iOS + : NativeModules.I18nManager.localeIdentifier; // Android and rest + +export default locale; From d8609b5c7264df94748b717a8d5a3bd1d182f2e8 Mon Sep 17 00:00:00 2001 From: Yusinto Ngadiman Date: Mon, 15 Jan 2024 12:38:03 -0800 Subject: [PATCH 12/44] chore: Add manufacturer and model. --- .../sdk/react-native/src/platform/AutoEnv.ts | 28 +++++++++++++++++-- 1 file changed, 25 insertions(+), 3 deletions(-) diff --git a/packages/sdk/react-native/src/platform/AutoEnv.ts b/packages/sdk/react-native/src/platform/AutoEnv.ts index adc2adda6..20ab69e51 100644 --- a/packages/sdk/react-native/src/platform/AutoEnv.ts +++ b/packages/sdk/react-native/src/platform/AutoEnv.ts @@ -1,4 +1,4 @@ -import { Platform } from 'react-native'; +import { Platform, PlatformAndroidStatic, PlatformIOSStatic } from 'react-native'; import type { LDAutoEnv, LDAutoEnvCommon } from '@launchdarkly/js-sdk-common'; @@ -16,6 +16,21 @@ const common: LDAutoEnvCommon = { envAttributesVersion: '1.0', }; +const iosDeviceSelector = () => { + const iosPlatform = Platform as PlatformIOSStatic; + console.log(`====== systemName: ${iosPlatform.constants.systemName}`); + + if (iosPlatform.isTV) { + return 'appleTV'; + } + + if (iosPlatform.isPad) { + return 'iPad'; + } + + return 'iPhone'; +}; + const autoEnv: LDAutoEnv = { ld_application: { ...common, @@ -27,8 +42,15 @@ const autoEnv: LDAutoEnv = { }, ld_device: { ...common, - // manufacturer: getManufacturerSync(), - // model: getModel(), + manufacturer: Platform.select({ + ios: 'apple', + android: (Platform as PlatformAndroidStatic).constants.Manufacturer, + }), + model: Platform.select({ + ios: iosDeviceSelector(), + android: (Platform as PlatformAndroidStatic).constants.Model, + macos: 'mac', + }), // storageBytes: getTotalDiskCapacitySync().toString() // memoryBytes: getMaxMemorySync().toString(), os: { From 3a7c733c63b835b2a90be1e62991d0f6e0e3b9e5 Mon Sep 17 00:00:00 2001 From: Yusinto Ngadiman Date: Mon, 15 Jan 2024 13:21:07 -0800 Subject: [PATCH 13/44] chore: remove iosDeviceSelector. --- .../sdk/react-native/src/platform/AutoEnv.ts | 19 ------------------- 1 file changed, 19 deletions(-) diff --git a/packages/sdk/react-native/src/platform/AutoEnv.ts b/packages/sdk/react-native/src/platform/AutoEnv.ts index 20ab69e51..b571f6405 100644 --- a/packages/sdk/react-native/src/platform/AutoEnv.ts +++ b/packages/sdk/react-native/src/platform/AutoEnv.ts @@ -16,21 +16,6 @@ const common: LDAutoEnvCommon = { envAttributesVersion: '1.0', }; -const iosDeviceSelector = () => { - const iosPlatform = Platform as PlatformIOSStatic; - console.log(`====== systemName: ${iosPlatform.constants.systemName}`); - - if (iosPlatform.isTV) { - return 'appleTV'; - } - - if (iosPlatform.isPad) { - return 'iPad'; - } - - return 'iPhone'; -}; - const autoEnv: LDAutoEnv = { ld_application: { ...common, @@ -47,12 +32,8 @@ const autoEnv: LDAutoEnv = { android: (Platform as PlatformAndroidStatic).constants.Manufacturer, }), model: Platform.select({ - ios: iosDeviceSelector(), android: (Platform as PlatformAndroidStatic).constants.Model, - macos: 'mac', }), - // storageBytes: getTotalDiskCapacitySync().toString() - // memoryBytes: getMaxMemorySync().toString(), os: { family: Platform.select({ ios: 'apple', From 0ba37cb112faef0d9069e9697583d77ff1ce2e38 Mon Sep 17 00:00:00 2001 From: Yusinto Ngadiman Date: Tue, 16 Jan 2024 14:58:34 -0800 Subject: [PATCH 14/44] chore: moved dotenv in example app to dep. Removed redundant application autoEnv. --- packages/sdk/react-native/example/package.json | 4 ++-- packages/sdk/react-native/src/platform/AutoEnv.ts | 6 +----- 2 files changed, 3 insertions(+), 7 deletions(-) diff --git a/packages/sdk/react-native/example/package.json b/packages/sdk/react-native/example/package.json index df53d7ba1..717389b22 100644 --- a/packages/sdk/react-native/example/package.json +++ b/packages/sdk/react-native/example/package.json @@ -27,7 +27,8 @@ "expo-splash-screen": "~0.20.5", "expo-status-bar": "~1.7.1", "react": "18.2.0", - "react-native": "0.72.6" + "react-native": "0.72.6", + "react-native-dotenv": "^3.4.9" }, "devDependencies": { "@babel/core": "^7.20.0", @@ -37,7 +38,6 @@ "@types/react": "~18.2.14", "@types/react-native-dotenv": "^0.2.1", "detox": "^20.14.7", - "react-native-dotenv": "^3.4.9", "ts-jest": "^29.1.1", "typescript": "^5.2.2" } diff --git a/packages/sdk/react-native/src/platform/AutoEnv.ts b/packages/sdk/react-native/src/platform/AutoEnv.ts index b571f6405..8be812462 100644 --- a/packages/sdk/react-native/src/platform/AutoEnv.ts +++ b/packages/sdk/react-native/src/platform/AutoEnv.ts @@ -1,4 +1,4 @@ -import { Platform, PlatformAndroidStatic, PlatformIOSStatic } from 'react-native'; +import { Platform, PlatformAndroidStatic } from 'react-native'; import type { LDAutoEnv, LDAutoEnvCommon } from '@launchdarkly/js-sdk-common'; @@ -19,10 +19,6 @@ const common: LDAutoEnvCommon = { const autoEnv: LDAutoEnv = { ld_application: { ...common, - // id: getBundleId(), - // name: getApplicationName(), - // version: getVersion(), - // versionName: getReadableVersion(), locale, }, ld_device: { From bf843c4a75196c104b626b08fe44c17160ee95b7 Mon Sep 17 00:00:00 2001 From: Yusinto Ngadiman Date: Thu, 18 Jan 2024 22:54:21 -0800 Subject: [PATCH 15/44] chore: Simplify auto env by using application and device directly. Removed LDAutoEnv interface. Remove auto env from LDUser and LDSingleKind. --- .../src/ReactNativeLDClient.test.ts | 2 + .../sdk/react-native/src/platform/AutoEnv.ts | 46 ------------------- .../sdk/react-native/src/platform/autoEnv.ts | 38 +++++++++++++++ .../sdk/react-native/src/platform/index.ts | 19 ++++++-- .../src/{utils => platform}/locale.ts | 0 packages/shared/common/src/Context.ts | 17 +++---- .../src/api/context/LDMultiKindContext.ts | 10 ++-- .../src/api/context/LDSingleKindContext.ts | 3 +- .../shared/common/src/api/context/LDUser.ts | 4 +- .../shared/common/src/api/context/index.ts | 1 - .../LDAutoEnv.ts => platform/AutoEnv.ts} | 11 ++--- .../shared/common/src/api/platform/Info.ts | 12 +++-- .../shared/common/src/api/platform/index.ts | 5 +- .../shared/sdk-client/src/LDClientImpl.ts | 12 ++--- .../src/configuration/Configuration.ts | 1 - 15 files changed, 87 insertions(+), 94 deletions(-) delete mode 100644 packages/sdk/react-native/src/platform/AutoEnv.ts create mode 100644 packages/sdk/react-native/src/platform/autoEnv.ts rename packages/sdk/react-native/src/{utils => platform}/locale.ts (100%) rename packages/shared/common/src/api/{context/LDAutoEnv.ts => platform/AutoEnv.ts} (70%) diff --git a/packages/sdk/react-native/src/ReactNativeLDClient.test.ts b/packages/sdk/react-native/src/ReactNativeLDClient.test.ts index f7c9f717f..7b18bea21 100644 --- a/packages/sdk/react-native/src/ReactNativeLDClient.test.ts +++ b/packages/sdk/react-native/src/ReactNativeLDClient.test.ts @@ -2,6 +2,8 @@ import { type LDContext } from '@launchdarkly/js-client-sdk-common'; import ReactNativeLDClient from './ReactNativeLDClient'; +// TODO: fix broken tests due to transform error after importing PlatformAndroidStatic in autoEnv.ts + describe('ReactNativeLDClient', () => { let ldc: ReactNativeLDClient; diff --git a/packages/sdk/react-native/src/platform/AutoEnv.ts b/packages/sdk/react-native/src/platform/AutoEnv.ts deleted file mode 100644 index 8be812462..000000000 --- a/packages/sdk/react-native/src/platform/AutoEnv.ts +++ /dev/null @@ -1,46 +0,0 @@ -import { Platform, PlatformAndroidStatic } from 'react-native'; - -import type { LDAutoEnv, LDAutoEnvCommon } from '@launchdarkly/js-sdk-common'; - -import locale from '../utils/locale'; - -/** - * Priority for id, version and versionName: - * - * 1. Customer provided values via configuration or provided otherwise - * 2. Application info collected from the platform via platform APIs - * 3. LaunchDarkly SDK info such as SDK name and version - */ -const common: LDAutoEnvCommon = { - key: '', - envAttributesVersion: '1.0', -}; - -const autoEnv: LDAutoEnv = { - ld_application: { - ...common, - locale, - }, - ld_device: { - ...common, - manufacturer: Platform.select({ - ios: 'apple', - android: (Platform as PlatformAndroidStatic).constants.Manufacturer, - }), - model: Platform.select({ - android: (Platform as PlatformAndroidStatic).constants.Model, - }), - os: { - family: Platform.select({ - ios: 'apple', - default: Platform.OS, - }), - name: Platform.OS, - version: Platform.Version.toString(), - }, - }, -}; - -console.log(`===== autoenv: ${JSON.stringify(autoEnv, null, 2)}`); - -export default autoEnv; diff --git a/packages/sdk/react-native/src/platform/autoEnv.ts b/packages/sdk/react-native/src/platform/autoEnv.ts new file mode 100644 index 000000000..bbbb5991c --- /dev/null +++ b/packages/sdk/react-native/src/platform/autoEnv.ts @@ -0,0 +1,38 @@ +import { Platform, type PlatformAndroidStatic } from 'react-native'; + +import type { LDApplication, LDDevice } from '@launchdarkly/js-sdk-common'; + +import locale from './locale'; + +export const ldApplication: LDApplication = { + key: '', + envAttributesVersion: '1.0', + + // TODO: populate application ID, name, version, versionName + id: '', + name: '', + version: '', + versionName: '', + locale, +}; + +export const ldDevice: LDDevice = { + key: '', + envAttributesVersion: '1.0', + manufacturer: Platform.select({ + ios: 'apple', + android: (Platform as PlatformAndroidStatic).constants.Manufacturer, + }), + model: Platform.select({ + // ios: model n/a from PlatformIOSStatic + android: (Platform as PlatformAndroidStatic).constants.Model, + }), + os: { + family: Platform.select({ + ios: 'apple', + default: Platform.OS, + }), + name: Platform.OS, + version: Platform.Version.toString(), + }, +}; diff --git a/packages/sdk/react-native/src/platform/index.ts b/packages/sdk/react-native/src/platform/index.ts index 9fc3c9c0f..2320da907 100644 --- a/packages/sdk/react-native/src/platform/index.ts +++ b/packages/sdk/react-native/src/platform/index.ts @@ -21,7 +21,7 @@ import type { import { name, version } from '../../package.json'; import { btoa, uuidv4 } from '../polyfills'; import RNEventSource from '../react-native-sse'; -import AutoEnv from './AutoEnv'; +import { ldApplication, ldDevice } from './autoEnv'; import AsyncStorage from './ConditionalAsyncStorage'; class PlatformRequests implements Requests { @@ -44,19 +44,28 @@ class PlatformEncoding implements Encoding { } class PlatformInfo implements Info { + constructor(private readonly logger: LDLogger) {} + platformData(): PlatformData { - return { + const data = { name: 'React Native', - autoEnv: AutoEnv, + ld_application: ldApplication, + ld_device: ldDevice, }; + + this.logger.debug(`platformData: ${JSON.stringify(data, null, 2)}`); + return data; } sdkData(): SdkData { - return { + const data = { name, version, userAgentBase: 'ReactNativeClient', }; + + this.logger.debug(`sdkData: ${JSON.stringify(data, null, 2)}`); + return data; } } @@ -101,7 +110,7 @@ class PlatformStorage implements Storage { const createPlatform = (logger: LDLogger): Platform => ({ crypto: new PlatformCrypto(), - info: new PlatformInfo(), + info: new PlatformInfo(logger), requests: new PlatformRequests(), encoding: new PlatformEncoding(), storage: new PlatformStorage(logger), diff --git a/packages/sdk/react-native/src/utils/locale.ts b/packages/sdk/react-native/src/platform/locale.ts similarity index 100% rename from packages/sdk/react-native/src/utils/locale.ts rename to packages/sdk/react-native/src/platform/locale.ts diff --git a/packages/shared/common/src/Context.ts b/packages/shared/common/src/Context.ts index ee7a0e12b..03dac5532 100644 --- a/packages/shared/common/src/Context.ts +++ b/packages/shared/common/src/Context.ts @@ -1,5 +1,4 @@ import type { - LDAutoEnv, LDContext, LDContextCommon, LDMultiKindContext, @@ -178,10 +177,6 @@ function legacyToSingleKind(user: LDUser): LDSingleKindContext { return singleKindContext; } -function reconstructAutoEnv({ ld_application, ld_device }: LDContext) { - return { ld_application, ld_device }; -} - /** * Container for a context/contexts. Because contexts come from external code * they must be thoroughly validated and then formed to comply with @@ -210,8 +205,6 @@ export default class Context { public readonly message?: string; - public autoEnv?: LDAutoEnv; - static readonly userKind: string = DEFAULT_KIND; /** @@ -299,7 +292,9 @@ export default class Context { const kind = kinds[0]; const created = new Context(true, kind); created.context = contexts[kind]; - created.autoEnv = reconstructAutoEnv(context); + + // TODO: make application and device first class citizens + // created.autoEnv = reconstructAutoEnv(context); created.privateAttributeReferences = privateAttributes; created.isUser = kind === 'user'; return created; @@ -307,7 +302,7 @@ export default class Context { const created = new Context(true, context.kind); created.contexts = contexts; - created.autoEnv = reconstructAutoEnv(context); + // created.autoEnv = reconstructAutoEnv(context); created.privateAttributeReferences = privateAttributes; created.isMulti = true; return created; @@ -332,7 +327,7 @@ export default class Context { const created = new Context(true, kind); created.isUser = kind === 'user'; created.context = context; - created.autoEnv = reconstructAutoEnv(context); + // created.autoEnv = reconstructAutoEnv(context); created.privateAttributeReferences = { [kind]: privateAttributeReferences, }; @@ -346,7 +341,7 @@ export default class Context { return Context.contextForError('user', 'The key for the context was not valid'); } const created = new Context(true, 'user'); - created.autoEnv = reconstructAutoEnv(context); + // created.autoEnv = reconstructAutoEnv(context); created.isUser = true; created.wasLegacy = true; created.context = legacyToSingleKind(context); diff --git a/packages/shared/common/src/api/context/LDMultiKindContext.ts b/packages/shared/common/src/api/context/LDMultiKindContext.ts index 01ca1589b..6153f132e 100644 --- a/packages/shared/common/src/api/context/LDMultiKindContext.ts +++ b/packages/shared/common/src/api/context/LDMultiKindContext.ts @@ -1,5 +1,5 @@ -import { LDAutoEnv } from './LDAutoEnv'; -import { LDContextCommon } from './LDContextCommon'; +import type { LDApplication, LDDevice } from '../platform'; +import type { LDContextCommon } from './LDContextCommon'; /** * A context which represents multiple kinds. Each kind having its own key and attributes. @@ -32,12 +32,16 @@ import { LDContextCommon } from './LDContextCommon'; * The above multi-context contains both an 'org' and a 'user'. Each with their own key, * attributes, and _meta attributes. */ -export interface LDMultiKindContext extends LDAutoEnv { +export interface LDMultiKindContext { /** * The kind of the context. */ kind: 'multi'; + ld_application?: LDApplication; + + ld_device?: LDDevice; + /** * The contexts which compose this multi-kind context. * diff --git a/packages/shared/common/src/api/context/LDSingleKindContext.ts b/packages/shared/common/src/api/context/LDSingleKindContext.ts index bec138508..6ee63907b 100644 --- a/packages/shared/common/src/api/context/LDSingleKindContext.ts +++ b/packages/shared/common/src/api/context/LDSingleKindContext.ts @@ -1,4 +1,3 @@ -import { LDAutoEnv } from './LDAutoEnv'; import { LDContextCommon } from './LDContextCommon'; /** @@ -17,7 +16,7 @@ import { LDContextCommon } from './LDContextCommon'; * The above context would be a single kind context representing an organization. It has a key * for that organization, and a single attribute 'someAttribute'. */ -export interface LDSingleKindContext extends LDContextCommon, LDAutoEnv { +export interface LDSingleKindContext extends LDContextCommon { /** * The kind of the context. */ diff --git a/packages/shared/common/src/api/context/LDUser.ts b/packages/shared/common/src/api/context/LDUser.ts index 78bc5edfe..d1fb2598e 100644 --- a/packages/shared/common/src/api/context/LDUser.ts +++ b/packages/shared/common/src/api/context/LDUser.ts @@ -1,11 +1,9 @@ -import { LDAutoEnv } from './LDAutoEnv'; - /** * A LaunchDarkly user object. * * @deprecated */ -export interface LDUser extends LDAutoEnv { +export interface LDUser { /** * A unique string identifying a user. */ diff --git a/packages/shared/common/src/api/context/index.ts b/packages/shared/common/src/api/context/index.ts index d4415c70b..4edf6d88a 100644 --- a/packages/shared/common/src/api/context/index.ts +++ b/packages/shared/common/src/api/context/index.ts @@ -4,4 +4,3 @@ export * from './LDMultiKindContext'; export * from './LDSingleKindContext'; export * from './LDUser'; export * from './LDContext'; -export * from './LDAutoEnv'; diff --git a/packages/shared/common/src/api/context/LDAutoEnv.ts b/packages/shared/common/src/api/platform/AutoEnv.ts similarity index 70% rename from packages/shared/common/src/api/context/LDAutoEnv.ts rename to packages/shared/common/src/api/platform/AutoEnv.ts index 33aa47b90..536374d14 100644 --- a/packages/shared/common/src/api/context/LDAutoEnv.ts +++ b/packages/shared/common/src/api/platform/AutoEnv.ts @@ -1,4 +1,4 @@ -export interface LDAutoEnvCommon { +interface AutoEnvCommon { /** * Unique key for the context kind. */ @@ -10,7 +10,7 @@ export interface LDAutoEnvCommon { envAttributesVersion: string; } -export interface LDApplication extends LDAutoEnvCommon { +export interface LDApplication extends AutoEnvCommon { /** * Unique identifier of the application. */ @@ -21,7 +21,7 @@ export interface LDApplication extends LDAutoEnvCommon { locale?: string; } -export interface LDDevice extends LDAutoEnvCommon { +export interface LDDevice extends AutoEnvCommon { manufacturer?: string; model?: string; storageBytes?: string; @@ -35,8 +35,3 @@ export interface LDDevice extends LDAutoEnvCommon { version: string; }; } - -export interface LDAutoEnv { - ld_application?: LDApplication; - ld_device?: LDDevice; -} diff --git a/packages/shared/common/src/api/platform/Info.ts b/packages/shared/common/src/api/platform/Info.ts index 7a014781d..9a9895ff5 100644 --- a/packages/shared/common/src/api/platform/Info.ts +++ b/packages/shared/common/src/api/platform/Info.ts @@ -1,4 +1,4 @@ -import { LDAutoEnv } from '../context'; +import type { LDApplication, LDDevice } from './AutoEnv'; /** * Information about the platform of the SDK and the environment it is executing. @@ -36,9 +36,15 @@ export interface PlatformData { /** * Additional information about the executing environment. Should be populated - * when available. Not all platforms will make this data accessible. + * when available. Not all platforms will have this data. + */ + ld_application?: LDApplication; + + /** + * Device hardware information. Should be populated when available. Not all + * platforms will have this data. */ - autoEnv?: LDAutoEnv; + ld_device?: LDDevice; } export interface SdkData { diff --git a/packages/shared/common/src/api/platform/index.ts b/packages/shared/common/src/api/platform/index.ts index ff46f3af4..bf787b595 100644 --- a/packages/shared/common/src/api/platform/index.ts +++ b/packages/shared/common/src/api/platform/index.ts @@ -1,8 +1,9 @@ -export * from './Encoding'; +export * from './AutoEnv'; export * from './Crypto'; +export * from './Encoding'; +export * from './EventSource'; export * from './Filesystem'; export * from './Info'; export * from './Platform'; export * from './Requests'; -export * from './EventSource'; export * from './Storage'; diff --git a/packages/shared/sdk-client/src/LDClientImpl.ts b/packages/shared/sdk-client/src/LDClientImpl.ts index 6b04e7cf0..1497ca69d 100644 --- a/packages/shared/sdk-client/src/LDClientImpl.ts +++ b/packages/shared/sdk-client/src/LDClientImpl.ts @@ -3,7 +3,6 @@ import { clone, Context, internal, - LDAutoEnv, LDClientError, LDContext, LDEvaluationDetail, @@ -46,7 +45,6 @@ export default class LDClientImpl implements LDClient { private identifyErrorListener?: (c: LDContext, err: any) => void; private readonly clientContext: ClientContext; - private readonly autoEnv?: LDAutoEnv; /** * Creates the client object synchronously. No async, no network calls. @@ -78,7 +76,7 @@ export default class LDClientImpl implements LDClient { this.emitter = new LDEmitter(); // TODO: add logic to process auto env attributes correctly - this.autoEnv = platform.info.platformData().autoEnv; + // this.autoEnv = platform.info.platformData().autoEnv; } allFlags(): LDFlagSet { @@ -236,12 +234,8 @@ export default class LDClientImpl implements LDClient { } // TODO: implement secure mode - async identify(pristineContext: LDContext, _hash?: string): Promise { - // the original context is injected with auto env attributes - const context = { - ...pristineContext, - ...this.autoEnv, - }; + async identify(context: LDContext, _hash?: string): Promise { + // TODO: inject auto env into context const checkedContext = Context.fromLDContext(context); if (!checkedContext.valid) { diff --git a/packages/shared/sdk-client/src/configuration/Configuration.ts b/packages/shared/sdk-client/src/configuration/Configuration.ts index bb1099e69..e4f25940e 100644 --- a/packages/shared/sdk-client/src/configuration/Configuration.ts +++ b/packages/shared/sdk-client/src/configuration/Configuration.ts @@ -2,7 +2,6 @@ import { ApplicationTags, createSafeLogger, internal, - LDAutoEnv, LDFlagSet, NumberWithMinimum, OptionMessages, From bf972c760a2409831b7e5053fd92636132017e49 Mon Sep 17 00:00:00 2001 From: Yusinto Ngadiman Date: Thu, 18 Jan 2024 23:38:59 -0800 Subject: [PATCH 16/44] chore: fix react native jest failures due to native dependencies re async-storage and auto env. --- packages/sdk/react-native/babel.config.js | 3 +++ packages/sdk/react-native/jest.config.ts | 8 +++++++- packages/sdk/react-native/jestSetupFile.ts | 19 +++++++++++++++++++ packages/sdk/react-native/package.json | 2 ++ .../src/ReactNativeLDClient.test.ts | 2 -- packages/sdk/react-native/tsconfig.json | 2 +- 6 files changed, 32 insertions(+), 4 deletions(-) create mode 100644 packages/sdk/react-native/babel.config.js create mode 100644 packages/sdk/react-native/jestSetupFile.ts diff --git a/packages/sdk/react-native/babel.config.js b/packages/sdk/react-native/babel.config.js new file mode 100644 index 000000000..f842b77fc --- /dev/null +++ b/packages/sdk/react-native/babel.config.js @@ -0,0 +1,3 @@ +module.exports = { + presets: ['module:metro-react-native-babel-preset'], +}; diff --git a/packages/sdk/react-native/jest.config.ts b/packages/sdk/react-native/jest.config.ts index a740765ad..ffc410840 100644 --- a/packages/sdk/react-native/jest.config.ts +++ b/packages/sdk/react-native/jest.config.ts @@ -1,9 +1,12 @@ import type { JestConfigWithTsJest } from 'ts-jest'; +import { defaults as tsjPreset } from 'ts-jest/presets'; const jestConfig: JestConfigWithTsJest = { - preset: 'ts-jest', + ...tsjPreset, + preset: 'react-native', testEnvironment: 'jsdom', transform: { + '^.+\\.jsx$': 'babel-jest', '^.+\\.tsx?$': [ 'ts-jest', { @@ -11,7 +14,10 @@ const jestConfig: JestConfigWithTsJest = { }, ], }, + moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'json', 'node'], + transformIgnorePatterns: ['node_modules/(?!(react-native|@react-native)/)'], testPathIgnorePatterns: ['node_modules', 'example', 'dist'], + setupFiles: ['./jestSetupFile.ts'], }; export default jestConfig; diff --git a/packages/sdk/react-native/jestSetupFile.ts b/packages/sdk/react-native/jestSetupFile.ts new file mode 100644 index 000000000..4b0720331 --- /dev/null +++ b/packages/sdk/react-native/jestSetupFile.ts @@ -0,0 +1,19 @@ +jest.mock('@react-native-async-storage/async-storage', () => + require('@react-native-async-storage/async-storage/jest/async-storage-mock'), +); + +jest.mock('react-native', () => { + const RN = jest.requireActual('react-native'); + RN.NativeModules.SettingsManager = { + settings: { + AppleLocale: 'en-us', + }, + }; + + // HACK: force set Platform which is read-only + Object.defineProperty(RN.Platform, 'Version', { + get: () => 21, + }); + + return RN; +}); diff --git a/packages/sdk/react-native/package.json b/packages/sdk/react-native/package.json index b41394993..aff43a453 100644 --- a/packages/sdk/react-native/package.json +++ b/packages/sdk/react-native/package.json @@ -56,6 +56,7 @@ "@types/react": "^18.2.31", "@typescript-eslint/eslint-plugin": "^6.1.0", "@typescript-eslint/parser": "^6.1.0", + "babel-jest": "^29.7.0", "eslint": "^8.45.0", "eslint-config-airbnb-base": "^15.0.0", "eslint-config-airbnb-typescript": "^17.1.0", @@ -64,6 +65,7 @@ "eslint-plugin-prettier": "^5.0.0", "jest": "^29.7.0", "launchdarkly-js-test-helpers": "^2.2.0", + "metro-react-native-babel-preset": "^0.77.0", "prettier": "^3.0.0", "react": "^18.2.0", "react-dom": "^18.2.0", diff --git a/packages/sdk/react-native/src/ReactNativeLDClient.test.ts b/packages/sdk/react-native/src/ReactNativeLDClient.test.ts index 7b18bea21..f7c9f717f 100644 --- a/packages/sdk/react-native/src/ReactNativeLDClient.test.ts +++ b/packages/sdk/react-native/src/ReactNativeLDClient.test.ts @@ -2,8 +2,6 @@ import { type LDContext } from '@launchdarkly/js-client-sdk-common'; import ReactNativeLDClient from './ReactNativeLDClient'; -// TODO: fix broken tests due to transform error after importing PlatformAndroidStatic in autoEnv.ts - describe('ReactNativeLDClient', () => { let ldc: ReactNativeLDClient; diff --git a/packages/sdk/react-native/tsconfig.json b/packages/sdk/react-native/tsconfig.json index e5a9204be..a79d5ca77 100644 --- a/packages/sdk/react-native/tsconfig.json +++ b/packages/sdk/react-native/tsconfig.json @@ -18,7 +18,7 @@ "strict": true, "stripInternal": true, "target": "ES2017", - "types": ["node"] + "types": ["node", "jest"] }, "exclude": ["**/*.test.ts*", "dist", "node_modules", "__tests__", "example"] } From 84ec9204e5d4e530c04ecf6fce19d480dc233121 Mon Sep 17 00:00:00 2001 From: Yusinto Ngadiman Date: Thu, 18 Jan 2024 23:43:35 -0800 Subject: [PATCH 17/44] chore: clean up jest config, remove babel-jest dep. --- packages/sdk/react-native/jest.config.ts | 3 --- packages/sdk/react-native/package.json | 1 - 2 files changed, 4 deletions(-) diff --git a/packages/sdk/react-native/jest.config.ts b/packages/sdk/react-native/jest.config.ts index ffc410840..68246526c 100644 --- a/packages/sdk/react-native/jest.config.ts +++ b/packages/sdk/react-native/jest.config.ts @@ -6,7 +6,6 @@ const jestConfig: JestConfigWithTsJest = { preset: 'react-native', testEnvironment: 'jsdom', transform: { - '^.+\\.jsx$': 'babel-jest', '^.+\\.tsx?$': [ 'ts-jest', { @@ -14,8 +13,6 @@ const jestConfig: JestConfigWithTsJest = { }, ], }, - moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'json', 'node'], - transformIgnorePatterns: ['node_modules/(?!(react-native|@react-native)/)'], testPathIgnorePatterns: ['node_modules', 'example', 'dist'], setupFiles: ['./jestSetupFile.ts'], }; diff --git a/packages/sdk/react-native/package.json b/packages/sdk/react-native/package.json index aff43a453..3994aba06 100644 --- a/packages/sdk/react-native/package.json +++ b/packages/sdk/react-native/package.json @@ -56,7 +56,6 @@ "@types/react": "^18.2.31", "@typescript-eslint/eslint-plugin": "^6.1.0", "@typescript-eslint/parser": "^6.1.0", - "babel-jest": "^29.7.0", "eslint": "^8.45.0", "eslint-config-airbnb-base": "^15.0.0", "eslint-config-airbnb-typescript": "^17.1.0", From 9f27fb370a4174f8cfca5dee05a7fe7d376dcd6a Mon Sep 17 00:00:00 2001 From: Yusinto Ngadiman Date: Fri, 19 Jan 2024 13:49:00 -0800 Subject: [PATCH 18/44] chore: Inject context with autoEnv. Revert multikind interface changes. Dry getOrGenerateKey. --- .../src/api/context/LDMultiKindContext.ts | 15 ++--- .../shared/sdk-client/src/LDClientImpl.ts | 5 +- .../shared/sdk-client/src/utils/ensureKey.ts | 23 +++---- .../sdk-client/src/utils/getOrGenerateKey.ts | 15 +++++ .../sdk-client/src/utils/injectAutoEnv.ts | 61 +++++++++++++++++++ 5 files changed, 92 insertions(+), 27 deletions(-) create mode 100644 packages/shared/sdk-client/src/utils/getOrGenerateKey.ts create mode 100644 packages/shared/sdk-client/src/utils/injectAutoEnv.ts diff --git a/packages/shared/common/src/api/context/LDMultiKindContext.ts b/packages/shared/common/src/api/context/LDMultiKindContext.ts index 6153f132e..8edabde44 100644 --- a/packages/shared/common/src/api/context/LDMultiKindContext.ts +++ b/packages/shared/common/src/api/context/LDMultiKindContext.ts @@ -1,5 +1,4 @@ -import type { LDApplication, LDDevice } from '../platform'; -import type { LDContextCommon } from './LDContextCommon'; +import { LDContextCommon } from './LDContextCommon'; /** * A context which represents multiple kinds. Each kind having its own key and attributes. @@ -38,17 +37,11 @@ export interface LDMultiKindContext { */ kind: 'multi'; - ld_application?: LDApplication; - - ld_device?: LDDevice; - /** * The contexts which compose this multi-kind context. * - * These should be of type LDContextCommon with these exceptions: - * "multi" is to allow for the top level "kind" attribute. - * "undefined" is to allow for the top level "ld_application" and ld_device - * attributes. + * These should be of type LDContextCommon. "multi" is to allow + * for the top level "kind" attribute. */ - [attribute: string]: 'multi' | LDContextCommon | undefined; + [kind: string]: 'multi' | LDContextCommon; } diff --git a/packages/shared/sdk-client/src/LDClientImpl.ts b/packages/shared/sdk-client/src/LDClientImpl.ts index 3aa01aa46..d1d2ce77f 100644 --- a/packages/shared/sdk-client/src/LDClientImpl.ts +++ b/packages/shared/sdk-client/src/LDClientImpl.ts @@ -25,6 +25,7 @@ import createEventProcessor from './events/createEventProcessor'; import EventFactory from './events/EventFactory'; import { DeleteFlag, Flags, PatchFlag } from './types'; import { calculateFlagChanges, ensureKey } from './utils'; +import injectAutoEnv from './utils/injectAutoEnv'; const { createErrorEvaluationDetail, createSuccessEvaluationDetail, ClientMessages, ErrorKinds } = internal; @@ -235,8 +236,8 @@ export default class LDClientImpl implements LDClient { // TODO: implement secure mode async identify(pristineContext: LDContext, _hash?: string): Promise { - // TODO: inject auto env into context - const context = await ensureKey(pristineContext, this.platform); + let context = await ensureKey(pristineContext, this.platform); + context = await injectAutoEnv(context, this.platform, this.config); const checkedContext = Context.fromLDContext(context); if (!checkedContext.valid) { diff --git a/packages/shared/sdk-client/src/utils/ensureKey.ts b/packages/shared/sdk-client/src/utils/ensureKey.ts index c293b42b5..e5314df58 100644 --- a/packages/shared/sdk-client/src/utils/ensureKey.ts +++ b/packages/shared/sdk-client/src/utils/ensureKey.ts @@ -9,21 +9,9 @@ import { Platform, } from '@launchdarkly/js-sdk-common'; -const { isLegacyUser, isMultiKind, isSingleKind } = internal; - -export const addNamespace = (s: string) => `LaunchDarkly_AnonKeys_${s}`; - -export const getOrGenerateKey = async (kind: string, { crypto, storage }: Platform) => { - const nsKind = addNamespace(kind); - let contextKey = await storage?.get(nsKind); - - if (!contextKey) { - contextKey = crypto.randomUUID(); - await storage?.set(nsKind, contextKey); - } +import { getOrGenerateKey } from './getOrGenerateKey'; - return contextKey; -}; +const { isLegacyUser, isMultiKind, isSingleKind } = internal; /** * This is the root ensureKey function. All other ensureKey functions reduce to this. @@ -66,6 +54,13 @@ const ensureKeyLegacy = async (c: LDUser, platform: Platform) => { await ensureKeyCommon('user', c, platform); }; +/** + * Ensure a key is always present in anonymous contexts. Non-anonymous contexts + * are not processed and will just be returned as is. + * + * @param context + * @param platform + */ const ensureKey = async (context: LDContext, platform: Platform) => { const cloned = clone(context); diff --git a/packages/shared/sdk-client/src/utils/getOrGenerateKey.ts b/packages/shared/sdk-client/src/utils/getOrGenerateKey.ts new file mode 100644 index 000000000..ee037197b --- /dev/null +++ b/packages/shared/sdk-client/src/utils/getOrGenerateKey.ts @@ -0,0 +1,15 @@ +import { Platform } from '@launchdarkly/js-sdk-common'; + +export const addNamespace = (s: string) => `LaunchDarkly_AnonKeys_${s}`; + +export const getOrGenerateKey = async (kind: string, { crypto, storage }: Platform) => { + const nsKind = addNamespace(kind); + let contextKey = await storage?.get(nsKind); + + if (!contextKey) { + contextKey = crypto.randomUUID(); + await storage?.set(nsKind, contextKey); + } + + return contextKey; +}; diff --git a/packages/shared/sdk-client/src/utils/injectAutoEnv.ts b/packages/shared/sdk-client/src/utils/injectAutoEnv.ts new file mode 100644 index 000000000..bf0c62599 --- /dev/null +++ b/packages/shared/sdk-client/src/utils/injectAutoEnv.ts @@ -0,0 +1,61 @@ +/* eslint-disable @typescript-eslint/naming-convention */ +import { + clone, + internal, + LDApplication, + LDContext, + LDDevice, + LDMultiKindContext, + LDSingleKindContext, + LDUser, + Platform, +} from '@launchdarkly/js-sdk-common'; + +import Configuration from '../configuration'; +import { getOrGenerateKey } from './getOrGenerateKey'; + +const { isLegacyUser, isSingleKind } = internal; + +const toMulti = (c: LDSingleKindContext) => { + const { kind, ...contextCommon } = c; + + return { + kind, + [c.kind]: contextCommon, + }; +}; + +const injectAutoEnv = async (context: LDContext, platform: Platform, config: Configuration) => { + // LDUser is not supported for auto env reporting + if (isLegacyUser(context)) { + return context as LDUser; + } + + let multi; + const cloned = clone(context); + if (isSingleKind(cloned)) { + multi = toMulti(cloned); + } + + const { crypto, info } = platform; + const { name, version, wrapperName, wrapperVersion } = info.sdkData(); + const { ld_application, ld_device } = info.platformData(); + + const ldApplication = clone(ld_application); + ldApplication.id = config.application?.id ?? ldApplication.id ?? name ?? wrapperName; + ldApplication.version = + config.application?.version ?? ldApplication.version ?? version ?? wrapperVersion; + + const hasher = crypto.createHash('sha256'); + hasher.update(ldApplication.id!); + ldApplication.key = hasher.digest('base64'); + + const ldDevice = clone(ld_device); + if (ld_device && !ld_device.key) { + ldDevice.key = await getOrGenerateKey('ld_device', platform); + } + + return { ...multi, ld_application, ld_device } as LDMultiKindContext; +}; + +export default injectAutoEnv; From c3883a36bfccb2a475f8b7134dec9ed2a1c68dbc Mon Sep 17 00:00:00 2001 From: Yusinto Ngadiman Date: Fri, 19 Jan 2024 15:04:06 -0800 Subject: [PATCH 19/44] chore: Refactored internal inject functions. --- .../sdk-client/src/utils/injectAutoEnv.ts | 43 +++++++++++-------- 1 file changed, 26 insertions(+), 17 deletions(-) diff --git a/packages/shared/sdk-client/src/utils/injectAutoEnv.ts b/packages/shared/sdk-client/src/utils/injectAutoEnv.ts index bf0c62599..9548134d4 100644 --- a/packages/shared/sdk-client/src/utils/injectAutoEnv.ts +++ b/packages/shared/sdk-client/src/utils/injectAutoEnv.ts @@ -25,21 +25,9 @@ const toMulti = (c: LDSingleKindContext) => { }; }; -const injectAutoEnv = async (context: LDContext, platform: Platform, config: Configuration) => { - // LDUser is not supported for auto env reporting - if (isLegacyUser(context)) { - return context as LDUser; - } - - let multi; - const cloned = clone(context); - if (isSingleKind(cloned)) { - multi = toMulti(cloned); - } - - const { crypto, info } = platform; +const injectApplication = ({ crypto, info }: Platform, config: Configuration) => { const { name, version, wrapperName, wrapperVersion } = info.sdkData(); - const { ld_application, ld_device } = info.platformData(); + const { ld_application } = info.platformData(); const ldApplication = clone(ld_application); ldApplication.id = config.application?.id ?? ldApplication.id ?? name ?? wrapperName; @@ -50,12 +38,33 @@ const injectAutoEnv = async (context: LDContext, platform: Platform, config: Con hasher.update(ldApplication.id!); ldApplication.key = hasher.digest('base64'); - const ldDevice = clone(ld_device); - if (ld_device && !ld_device.key) { + return ldApplication; +}; + +const injectDevice = async (platform: Platform) => { + const ldDevice = clone(platform.info.platformData().ld_device); + if (ldDevice && !ldDevice.key) { ldDevice.key = await getOrGenerateKey('ld_device', platform); } + return ldDevice; +}; - return { ...multi, ld_application, ld_device } as LDMultiKindContext; +const injectAutoEnv = async (context: LDContext, platform: Platform, config: Configuration) => { + // LDUser is not supported for auto env reporting + if (isLegacyUser(context)) { + return context as LDUser; + } + + let multi; + if (isSingleKind(context)) { + multi = toMulti(context); + } + + return { + ...multi, + ld_application: injectApplication(platform, config), + ld_device: await injectDevice(platform), + } as LDMultiKindContext; }; export default injectAutoEnv; From cf67b77e69d41056ab31e12afc4f243bcd38032b Mon Sep 17 00:00:00 2001 From: Yusinto Ngadiman Date: Sun, 21 Jan 2024 17:26:31 -0800 Subject: [PATCH 20/44] chore: Improved mocks so crypto is more easily testable. --- packages/shared/common/src/utils/clone.ts | 4 + packages/shared/mocks/src/clientContext.ts | 2 +- packages/shared/mocks/src/crypto.ts | 28 + packages/shared/mocks/src/hasher.ts | 25 - packages/shared/mocks/src/index.ts | 9 +- packages/shared/mocks/src/mockFetch.ts | 2 +- packages/shared/mocks/src/platform.ts | 39 +- packages/shared/mocks/src/setupMocks.ts | 7 + .../sdk-client/jest-setupFilesAfterEnv.ts | 4 + .../sdk-client/src/LDClientImpl.test.ts | 40 +- .../shared/sdk-client/src/LDClientImpl.ts | 3 +- .../sdk-client/src/utils/ensureKey.test.ts | 3 +- packages/shared/sdk-client/src/utils/index.ts | 3 +- .../sdk-client/src/utils/injectAutoEnv.ts | 44 +- .../__tests__/evaluation/Bucketer.test.ts | 212 ++--- .../evaluation/Evaluator.bucketing.test.ts | 12 +- .../evaluation/Evaluator.clause.test.ts | 791 +++++++++--------- .../evaluation/Evaluator.rules.test.ts | 309 +++---- .../__tests__/evaluation/Evaluator.test.ts | 264 +++--- .../sdk-server/jest-setupFilesAfterEnv.ts | 3 + packages/shared/sdk-server/jest.config.js | 1 + packages/shared/sdk-server/tsconfig.json | 2 +- 22 files changed, 959 insertions(+), 848 deletions(-) create mode 100644 packages/shared/mocks/src/crypto.ts delete mode 100644 packages/shared/mocks/src/hasher.ts create mode 100644 packages/shared/mocks/src/setupMocks.ts create mode 100644 packages/shared/sdk-server/jest-setupFilesAfterEnv.ts diff --git a/packages/shared/common/src/utils/clone.ts b/packages/shared/common/src/utils/clone.ts index 14e19d8e3..a52cbac15 100644 --- a/packages/shared/common/src/utils/clone.ts +++ b/packages/shared/common/src/utils/clone.ts @@ -1,3 +1,7 @@ export default function clone(obj: any) { + if (obj === undefined || obj === null) { + return obj; + } + return JSON.parse(JSON.stringify(obj)) as T; } diff --git a/packages/shared/mocks/src/clientContext.ts b/packages/shared/mocks/src/clientContext.ts index 90baaefe7..dca6f19a1 100644 --- a/packages/shared/mocks/src/clientContext.ts +++ b/packages/shared/mocks/src/clientContext.ts @@ -1,6 +1,6 @@ import type { ClientContext } from '@common'; -import basicPlatform from './platform'; +import { basicPlatform } from './platform'; const clientContext: ClientContext = { basicConfiguration: { diff --git a/packages/shared/mocks/src/crypto.ts b/packages/shared/mocks/src/crypto.ts new file mode 100644 index 000000000..032303fbb --- /dev/null +++ b/packages/shared/mocks/src/crypto.ts @@ -0,0 +1,28 @@ +import type { Crypto, Hasher } from '@common'; + +// HACK: inject hasher so we can easily test calls to update & digest +export type CryptoWithHash = Crypto & { hasher: Hasher }; +// eslint-disable-next-line import/no-mutable-exports +export let crypto: CryptoWithHash; + +export const setupCrypto = () => { + let counter = 0; + const hasher: Hasher = { + update: jest.fn(), + digest: jest.fn(() => '1234567890123456'), + }; + + crypto = { + hasher, + createHash: jest.fn(() => hasher), + createHmac: jest.fn(), + randomUUID: jest.fn(() => { + counter += 1; + // Will provide a unique value for tests. + // Very much not a UUID of course. + return `${counter}`; + }), + }; + + return crypto; +}; diff --git a/packages/shared/mocks/src/hasher.ts b/packages/shared/mocks/src/hasher.ts deleted file mode 100644 index 999f8fbee..000000000 --- a/packages/shared/mocks/src/hasher.ts +++ /dev/null @@ -1,25 +0,0 @@ -import type { Crypto, Hasher, Hmac } from '@common'; - -export const hasher: Hasher = { - update: jest.fn(), - digest: jest.fn(() => '1234567890123456'), -}; - -let counter = 0; - -export const crypto: Crypto = { - createHash(algorithm: string): Hasher { - expect(algorithm).toEqual('sha1'); - return hasher; - }, - createHmac(algorithm: string, key: string): Hmac { - // Not used for this test. - throw new Error(`Function not implemented.${algorithm}${key}`); - }, - randomUUID: jest.fn(() => { - counter += 1; - // Will provide a unique value for tests. - // Very much not a UUID of course. - return `${counter}`; - }), -}; diff --git a/packages/shared/mocks/src/index.ts b/packages/shared/mocks/src/index.ts index beceb6f47..58114d5a5 100644 --- a/packages/shared/mocks/src/index.ts +++ b/packages/shared/mocks/src/index.ts @@ -1,19 +1,20 @@ import clientContext from './clientContext'; import ContextDeduplicator from './contextDeduplicator'; -import { crypto, hasher } from './hasher'; +import { type CryptoWithHash } from './crypto'; import logger from './logger'; import mockFetch from './mockFetch'; -import basicPlatform from './platform'; +import { basicPlatform } from './platform'; +import setupMocks from './setupMocks'; import { MockStreamingProcessor, setupMockStreamingProcessor } from './streamingProcessor'; export { basicPlatform, clientContext, mockFetch, - crypto, logger, - hasher, ContextDeduplicator, MockStreamingProcessor, setupMockStreamingProcessor, + setupMocks, + CryptoWithHash, }; diff --git a/packages/shared/mocks/src/mockFetch.ts b/packages/shared/mocks/src/mockFetch.ts index 0ae07f804..aa0013f14 100644 --- a/packages/shared/mocks/src/mockFetch.ts +++ b/packages/shared/mocks/src/mockFetch.ts @@ -1,6 +1,6 @@ import { Response } from '@common'; -import basicPlatform from './platform'; +import { basicPlatform } from './platform'; const createMockResponse = (remoteJson: any, statusCode: number) => { const response: Response = { diff --git a/packages/shared/mocks/src/platform.ts b/packages/shared/mocks/src/platform.ts index ed08a80c9..6263d34c5 100644 --- a/packages/shared/mocks/src/platform.ts +++ b/packages/shared/mocks/src/platform.ts @@ -1,6 +1,6 @@ import type { Encoding, Info, Platform, PlatformData, Requests, SdkData, Storage } from '@common'; -import { crypto } from './hasher'; +import { setupCrypto } from './crypto'; const encoding: Encoding = { btoa: (s: string) => Buffer.from(s).toString('base64'), @@ -10,14 +10,27 @@ const info: Info = { platformData(): PlatformData { return { os: { - name: 'An OS', - version: '1.0.1', - arch: 'An Arch', + name: 'iOS', + version: '17.17', + arch: 'ARM64', }, name: 'The SDK Name', additional: { nodeVersion: '42', }, + ld_application: { + key: '', + envAttributesVersion: '1.0', + id: 'com.testapp.ld', + name: 'LDApplication.TestApp', + version: '1.1.1', + }, + ld_device: { + key: '', + envAttributesVersion: '1.0', + os: { name: 'ios', version: '17', family: 'apple' }, + manufacturer: 'apple', + }, }; }, sdkData(): SdkData { @@ -42,12 +55,14 @@ const storage: Storage = { clear: jest.fn(), }; -const basicPlatform: Platform = { - encoding, - info, - crypto, - requests, - storage, +// eslint-disable-next-line import/no-mutable-exports +export let basicPlatform: Platform; +export const setupBasicPlatform = () => { + basicPlatform = { + encoding, + info, + crypto: setupCrypto(), + requests, + storage, + }; }; - -export default basicPlatform; diff --git a/packages/shared/mocks/src/setupMocks.ts b/packages/shared/mocks/src/setupMocks.ts new file mode 100644 index 000000000..928e38849 --- /dev/null +++ b/packages/shared/mocks/src/setupMocks.ts @@ -0,0 +1,7 @@ +import { setupBasicPlatform } from './platform'; + +export default function setupMocks() { + beforeEach(() => { + setupBasicPlatform(); + }); +} diff --git a/packages/shared/sdk-client/jest-setupFilesAfterEnv.ts b/packages/shared/sdk-client/jest-setupFilesAfterEnv.ts index 7b0828bfa..291ba9eca 100644 --- a/packages/shared/sdk-client/jest-setupFilesAfterEnv.ts +++ b/packages/shared/sdk-client/jest-setupFilesAfterEnv.ts @@ -1 +1,5 @@ import '@testing-library/jest-dom'; + +import { setupMocks } from '@launchdarkly/private-js-mocks'; + +setupMocks(); diff --git a/packages/shared/sdk-client/src/LDClientImpl.test.ts b/packages/shared/sdk-client/src/LDClientImpl.test.ts index 57524bf9c..93c66550c 100644 --- a/packages/shared/sdk-client/src/LDClientImpl.test.ts +++ b/packages/shared/sdk-client/src/LDClientImpl.test.ts @@ -19,9 +19,23 @@ jest.mock('@launchdarkly/js-sdk-common', () => { }; }); -const { crypto } = basicPlatform; const testSdkKey = 'test-sdk-key'; const context: LDContext = { kind: 'org', key: 'Testy Pizza' }; +const autoEnv = { + ld_application: { + key: 'digested1', + envAttributesVersion: '1.0', + id: 'com.testapp.ld', + name: 'LDApplication.TestApp', + version: '1.1.1', + }, + ld_device: { + key: 'random1', + envAttributesVersion: '1.0', + manufacturer: 'apple', + os: { family: 'apple', name: 'iOS', version: '17.17' }, + }, +}; let ldc: LDClientImpl; let defaultPutResponse: Flags; @@ -29,7 +43,8 @@ describe('sdk-client object', () => { beforeEach(() => { defaultPutResponse = clone(mockResponseJson); setupMockStreamingProcessor(false, defaultPutResponse); - crypto.randomUUID.mockReturnValueOnce('random1'); + basicPlatform.crypto.randomUUID.mockReturnValue('random1'); + basicPlatform.crypto.hasher.digest.mockReturnValue('digested1'); ldc = new LDClientImpl(testSdkKey, basicPlatform, { logger, sendEvents: false }); jest @@ -129,7 +144,11 @@ describe('sdk-client object', () => { const c = ldc.getContext(); const all = ldc.allFlags(); - expect(carContext).toEqual(c); + expect(c).toEqual({ + kind: 'multi', + car: { key: 'mazda-cx7' }, + ...autoEnv, + }); expect(all).toMatchObject({ 'dev-test-flag': false, }); @@ -143,18 +162,21 @@ describe('sdk-client object', () => { const c = ldc.getContext(); const all = ldc.allFlags(); - expect(c!.key).toEqual('random1'); + expect(c).toEqual({ + kind: 'multi', + car: { anonymous: true, key: 'random1' }, + ...autoEnv, + }); expect(all).toMatchObject({ 'dev-test-flag': false, }); }); test('identify error invalid context', async () => { - // @ts-ignore - const carContext: LDContext = { kind: 'car', key: undefined }; + const carContext: LDContext = { kind: 'car', key: '' }; - await expect(ldc.identify(carContext)).rejects.toThrowError(/no key/); - expect(logger.error).toBeCalledTimes(1); + await expect(ldc.identify(carContext)).rejects.toThrow(/no key/); + expect(logger.error).toHaveBeenCalledTimes(1); expect(ldc.getContext()).toBeUndefined(); }); @@ -166,7 +188,7 @@ describe('sdk-client object', () => { code: 401, message: 'test-error', }); - expect(logger.error).toBeCalledTimes(1); + expect(logger.error).toHaveBeenCalledTimes(1); expect(ldc.getContext()).toBeUndefined(); }); diff --git a/packages/shared/sdk-client/src/LDClientImpl.ts b/packages/shared/sdk-client/src/LDClientImpl.ts index d1d2ce77f..5f800b6c6 100644 --- a/packages/shared/sdk-client/src/LDClientImpl.ts +++ b/packages/shared/sdk-client/src/LDClientImpl.ts @@ -24,8 +24,7 @@ import createDiagnosticsManager from './diagnostics/createDiagnosticsManager'; import createEventProcessor from './events/createEventProcessor'; import EventFactory from './events/EventFactory'; import { DeleteFlag, Flags, PatchFlag } from './types'; -import { calculateFlagChanges, ensureKey } from './utils'; -import injectAutoEnv from './utils/injectAutoEnv'; +import { calculateFlagChanges, ensureKey, injectAutoEnv } from './utils'; const { createErrorEvaluationDetail, createSuccessEvaluationDetail, ClientMessages, ErrorKinds } = internal; diff --git a/packages/shared/sdk-client/src/utils/ensureKey.test.ts b/packages/shared/sdk-client/src/utils/ensureKey.test.ts index 141bd5a51..5990061a8 100644 --- a/packages/shared/sdk-client/src/utils/ensureKey.test.ts +++ b/packages/shared/sdk-client/src/utils/ensureKey.test.ts @@ -6,7 +6,8 @@ import type { } from '@launchdarkly/js-sdk-common'; import { basicPlatform } from '@launchdarkly/private-js-mocks'; -import ensureKey, { addNamespace, getOrGenerateKey } from './ensureKey'; +import ensureKey from './ensureKey'; +import { addNamespace, getOrGenerateKey } from './getOrGenerateKey'; const { crypto, storage } = basicPlatform; describe('ensureKey', () => { diff --git a/packages/shared/sdk-client/src/utils/index.ts b/packages/shared/sdk-client/src/utils/index.ts index f203d0985..9f0614ad0 100644 --- a/packages/shared/sdk-client/src/utils/index.ts +++ b/packages/shared/sdk-client/src/utils/index.ts @@ -1,4 +1,5 @@ import calculateFlagChanges from './calculateFlagChanges'; import ensureKey from './ensureKey'; +import { injectAutoEnv } from './injectAutoEnv'; -export { calculateFlagChanges, ensureKey }; +export { calculateFlagChanges, ensureKey, injectAutoEnv }; diff --git a/packages/shared/sdk-client/src/utils/injectAutoEnv.ts b/packages/shared/sdk-client/src/utils/injectAutoEnv.ts index 9548134d4..36b244226 100644 --- a/packages/shared/sdk-client/src/utils/injectAutoEnv.ts +++ b/packages/shared/sdk-client/src/utils/injectAutoEnv.ts @@ -16,16 +16,24 @@ import { getOrGenerateKey } from './getOrGenerateKey'; const { isLegacyUser, isSingleKind } = internal; -const toMulti = (c: LDSingleKindContext) => { +export const toMulti = (c: LDSingleKindContext) => { const { kind, ...contextCommon } = c; return { - kind, - [c.kind]: contextCommon, + kind: 'multi', + [kind]: contextCommon, }; }; -const injectApplication = ({ crypto, info }: Platform, config: Configuration) => { +/** + * Clones the LDApplication object and populates the key, id and version fields. + * + * @param crypto + * @param info + * @param config + * @return An LDApplication object with populated key, id and version. + */ +export const injectApplication = ({ crypto, info }: Platform, config: Configuration) => { const { name, version, wrapperName, wrapperVersion } = info.sdkData(); const { ld_application } = info.platformData(); @@ -41,15 +49,29 @@ const injectApplication = ({ crypto, info }: Platform, config: Configuration) => return ldApplication; }; -const injectDevice = async (platform: Platform) => { - const ldDevice = clone(platform.info.platformData().ld_device); - if (ldDevice && !ldDevice.key) { - ldDevice.key = await getOrGenerateKey('ld_device', platform); - } +/** + * Clones the LDDevice object and populates the key field. + * + * @param platform + * @return An LDDevice object with populated key. + */ +export const injectDevice = async (platform: Platform) => { + const { ld_device, os } = platform.info.platformData(); + const ldDevice = clone(ld_device); + + // TODO: check for empty string + ldDevice.os.name = os?.name ?? ldDevice.os.name; + ldDevice.os.version = os?.version ?? ldDevice.os.version; + ldDevice.key = await getOrGenerateKey('ld_device', platform); + return ldDevice; }; -const injectAutoEnv = async (context: LDContext, platform: Platform, config: Configuration) => { +export const injectAutoEnv = async ( + context: LDContext, + platform: Platform, + config: Configuration, +) => { // LDUser is not supported for auto env reporting if (isLegacyUser(context)) { return context as LDUser; @@ -66,5 +88,3 @@ const injectAutoEnv = async (context: LDContext, platform: Platform, config: Con ld_device: await injectDevice(platform), } as LDMultiKindContext; }; - -export default injectAutoEnv; diff --git a/packages/shared/sdk-server/__tests__/evaluation/Bucketer.test.ts b/packages/shared/sdk-server/__tests__/evaluation/Bucketer.test.ts index c2bc82276..58088e186 100644 --- a/packages/shared/sdk-server/__tests__/evaluation/Bucketer.test.ts +++ b/packages/shared/sdk-server/__tests__/evaluation/Bucketer.test.ts @@ -3,123 +3,131 @@ // should contain a consistency test. // Testing here can only validate we are providing correct inputs to the hashing algorithm. import { AttributeReference, Context, LDContext } from '@launchdarkly/js-sdk-common'; -import * as mocks from '@launchdarkly/private-js-mocks'; +import { basicPlatform, type CryptoWithHash } from '@launchdarkly/private-js-mocks'; import Bucketer from '../../src/evaluation/Bucketer'; -describe.each< - [ - context: LDContext, - key: string, - attr: string, - salt: string, - kindForRollout: string | undefined, - seed: number | undefined, - expected: string, - ] ->([ - [{ key: 'is-key' }, 'flag-key', 'key', 'salty', undefined, undefined, 'flag-key.salty.is-key'], - // No specified kind, and user, are equivalent. - [{ key: 'is-key' }, 'flag-key', 'key', 'salty', 'user', undefined, 'flag-key.salty.is-key'], - [{ key: 'is-key' }, 'flag-key', 'key', 'salty', undefined, undefined, 'flag-key.salty.is-key'], +describe('Bucketer.test', () => { + let crypto: CryptoWithHash; - [{ key: 'is-key' }, 'flag-key', 'key', 'salty', undefined, 82, '82.is-key'], - [ - { key: 'is-key', kind: 'org' }, - 'flag-key', - 'key', - 'salty', - 'org', - undefined, - 'flag-key.salty.is-key', - ], - [ - { key: 'is-key', kind: 'org', integer: 17 }, - 'flag-key', - 'integer', - 'salty', - 'org', - undefined, - 'flag-key.salty.17', - ], - [ - { kind: 'multi', user: { key: 'user-key' }, org: { key: 'org-key' } }, - 'flag-key', - 'key', - 'salty', - undefined, - undefined, - 'flag-key.salty.user-key', - ], - [ - { kind: 'multi', user: { key: 'user-key' }, org: { key: 'org-key' } }, - 'flag-key', - 'key', - 'salty', - 'org', - undefined, - 'flag-key.salty.org-key', - ], -])('given bucketing parameters', (context, key, attr, salt, kindForRollout, seed, expected) => { - it('hashes the correct string', () => { - const validatedContext = Context.fromLDContext(context); - const attrRef = new AttributeReference(attr); - - const bucketer = new Bucketer(mocks.crypto); - const [bucket, hadContext] = bucketer.bucket( - validatedContext!, - key, - attrRef, - salt, - kindForRollout, - seed, - ); - - // The mocks.hasher always returns the same value. This just checks that it converts it to a number - // in the expected way. - expect(bucket).toBeCloseTo(0.07111111110140983, 5); - expect(hadContext).toBeTruthy(); - expect(mocks.hasher.update).toHaveBeenCalledWith(expected); - expect(mocks.hasher.digest).toHaveBeenCalledWith('hex'); + beforeEach(() => { + crypto = basicPlatform.crypto; }); afterEach(() => { - jest.clearAllMocks(); + jest.resetAllMocks(); }); -}); -describe.each([ - ['org', 'object'], - ['org', 'array'], - ['org', 'null'], - ['bad', 'key'], -])('when given a non string or integer reference', (kind, attr) => { - it('buckets to 0 when given bad data', () => { - const validatedContext = Context.fromLDContext({ - key: 'context-key', - kind, - object: {}, - array: [], - null: null, - }); - const attrRef = new AttributeReference(attr); + describe.each< + [ + context: LDContext, + key: string, + attr: string, + salt: string, + kindForRollout: string | undefined, + seed: number | undefined, + expected: string, + ] + >([ + [{ key: 'is-key' }, 'flag-key', 'key', 'salty', undefined, undefined, 'flag-key.salty.is-key'], + // No specified kind, and user, are equivalent. + [{ key: 'is-key' }, 'flag-key', 'key', 'salty', 'user', undefined, 'flag-key.salty.is-key'], + [{ key: 'is-key' }, 'flag-key', 'key', 'salty', undefined, undefined, 'flag-key.salty.is-key'], - const bucketer = new Bucketer(mocks.crypto); - const [bucket, hadContext] = bucketer.bucket( - validatedContext!, + [{ key: 'is-key' }, 'flag-key', 'key', 'salty', undefined, 82, '82.is-key'], + [ + { key: 'is-key', kind: 'org' }, + 'flag-key', 'key', - attrRef, 'salty', 'org', undefined, - ); - expect(bucket).toEqual(0); - expect(hadContext).toEqual(kind === 'org'); - expect(mocks.hasher.update).toBeCalledTimes(0); - expect(mocks.hasher.digest).toBeCalledTimes(0); + 'flag-key.salty.is-key', + ], + [ + { key: 'is-key', kind: 'org', integer: 17 }, + 'flag-key', + 'integer', + 'salty', + 'org', + undefined, + 'flag-key.salty.17', + ], + [ + { kind: 'multi', user: { key: 'user-key' }, org: { key: 'org-key' } }, + 'flag-key', + 'key', + 'salty', + undefined, + undefined, + 'flag-key.salty.user-key', + ], + [ + { kind: 'multi', user: { key: 'user-key' }, org: { key: 'org-key' } }, + 'flag-key', + 'key', + 'salty', + 'org', + undefined, + 'flag-key.salty.org-key', + ], + ])('given bucketing parameters', (context, key, attr, salt, kindForRollout, seed, expected) => { + it('hashes the correct string', () => { + const validatedContext = Context.fromLDContext(context); + const attrRef = new AttributeReference(attr); + + const bucketer = new Bucketer(crypto); + const [bucket, hadContext] = bucketer.bucket( + validatedContext!, + key, + attrRef, + salt, + kindForRollout, + seed, + ); + + // The mocks.hasher always returns the same value. This just checks that it converts it to a number + // in the expected way. + expect(bucket).toBeCloseTo(0.07111111110140983, 5); + expect(hadContext).toBeTruthy(); + expect(crypto.hasher.update).toHaveBeenCalledWith(expected); + expect(crypto.hasher.digest).toHaveBeenCalledWith('hex'); + }); + + afterEach(() => { + jest.resetAllMocks(); + }); }); - afterEach(() => { - jest.clearAllMocks(); + describe.each([ + ['org', 'object'], + ['org', 'array'], + ['org', 'null'], + ['bad', 'key'], + ])('when given a non string or integer reference', (kind, attr) => { + it('buckets to 0 when given bad data', () => { + const validatedContext = Context.fromLDContext({ + key: 'context-key', + kind, + object: {}, + array: [], + null: null, + }); + const attrRef = new AttributeReference(attr); + + const bucketer = new Bucketer(crypto); + const [bucket, hadContext] = bucketer.bucket( + validatedContext!, + 'key', + attrRef, + 'salty', + 'org', + undefined, + ); + expect(bucket).toEqual(0); + expect(hadContext).toEqual(kind === 'org'); + expect(crypto.hasher.update).toBeCalledTimes(0); + expect(crypto.hasher.digest).toBeCalledTimes(0); + }); }); }); diff --git a/packages/shared/sdk-server/__tests__/evaluation/Evaluator.bucketing.test.ts b/packages/shared/sdk-server/__tests__/evaluation/Evaluator.bucketing.test.ts index 51acff48d..6ec82a86c 100644 --- a/packages/shared/sdk-server/__tests__/evaluation/Evaluator.bucketing.test.ts +++ b/packages/shared/sdk-server/__tests__/evaluation/Evaluator.bucketing.test.ts @@ -6,9 +6,17 @@ import { Rollout } from '../../src/evaluation/data/Rollout'; import Evaluator from '../../src/evaluation/Evaluator'; import noQueries from './mocks/noQueries'; -const evaluator = new Evaluator(mocks.basicPlatform, noQueries); - describe('given a flag with a rollout', () => { + let evaluator: Evaluator; + + beforeEach(() => { + evaluator = new Evaluator(mocks.basicPlatform, noQueries); + }); + + afterEach(() => { + jest.resetAllMocks(); + }); + const seed = 61; const flagKey = 'flagkey'; const salt = 'salt'; diff --git a/packages/shared/sdk-server/__tests__/evaluation/Evaluator.clause.test.ts b/packages/shared/sdk-server/__tests__/evaluation/Evaluator.clause.test.ts index 9a2e0c9aa..c5fa9463e 100644 --- a/packages/shared/sdk-server/__tests__/evaluation/Evaluator.clause.test.ts +++ b/packages/shared/sdk-server/__tests__/evaluation/Evaluator.clause.test.ts @@ -12,460 +12,465 @@ import { } from './flags'; import noQueries from './mocks/noQueries'; -const evaluator = new Evaluator(mocks.basicPlatform, noQueries); - -// Either a legacy user, or context with equivalent user. -describe('given user clauses and contexts', () => { - it.each([ - { key: 'x', name: 'Bob' }, - { kind: 'user', key: 'x', name: 'Bob' }, - { kind: 'multi', user: { key: 'x', name: 'Bob' } }, - ])('can match built-in attribute', async (user) => { - const clause: Clause = { - attribute: 'name', - op: 'in', - values: ['Bob'], - attributeReference: new AttributeReference('name'), - }; - const flag = makeBooleanFlagWithOneClause(clause); - const context = Context.fromLDContext(user); - const res = await evaluator.evaluate(flag, context!); - expect(res.detail.value).toBe(true); +describe('Evaluator.clause', () => { + let evaluator: Evaluator; + beforeEach(() => { + evaluator = new Evaluator(mocks.basicPlatform, noQueries); }); - it.each([ - { key: 'x', name: 'Bob', custom: { legs: 4 } }, - { - kind: 'user', - key: 'x', - name: 'Bob', - legs: 4, - }, - { kind: 'multi', user: { key: 'x', name: 'Bob', legs: 4 } }, - ])('can match custom attribute', async (user) => { - const clause: Clause = { - attribute: 'legs', - op: 'in', - values: [4], - attributeReference: new AttributeReference('legs'), - }; - const flag = makeBooleanFlagWithOneClause(clause); - const context = Context.fromLDContext(user); - const res = await evaluator.evaluate(flag, context!); - expect(res.detail.value).toBe(true); - }); + // Either a legacy user, or context with equivalent user. + describe('given user clauses and contexts', () => { + it.each([ + { key: 'x', name: 'Bob' }, + { kind: 'user', key: 'x', name: 'Bob' }, + { kind: 'multi', user: { key: 'x', name: 'Bob' } }, + ])('can match built-in attribute', async (user) => { + const clause: Clause = { + attribute: 'name', + op: 'in', + values: ['Bob'], + attributeReference: new AttributeReference('name'), + }; + const flag = makeBooleanFlagWithOneClause(clause); + const context = Context.fromLDContext(user); + const res = await evaluator.evaluate(flag, context!); + expect(res.detail.value).toBe(true); + }); - it.each<[LDContext, string]>([ - [{ key: 'x', name: 'Bob', custom: { '//': 4 } }, '//'], - [ - { - kind: 'user', - key: 'x', - name: 'Bob', - '//': 4, - }, - '//', - ], - [{ kind: 'multi', user: { key: 'x', name: 'Bob', '//': 4 } }, '//'], - [{ key: 'x', name: 'Bob', custom: { '/~~': 4 } }, '/~~'], - [ + it.each([ + { key: 'x', name: 'Bob', custom: { legs: 4 } }, { kind: 'user', key: 'x', name: 'Bob', - '/~~': 4, + legs: 4, }, - '/~~', - ], - [{ kind: 'multi', user: { key: 'x', name: 'Bob', '/~~': 4 } }, '/~~'], - ])( - 'can match attributes which would have be invalid references, but are valid literals', - async (user, attribute) => { + { kind: 'multi', user: { key: 'x', name: 'Bob', legs: 4 } }, + ])('can match custom attribute', async (user) => { const clause: Clause = { - attribute, + attribute: 'legs', op: 'in', values: [4], - attributeReference: new AttributeReference(attribute, true), + attributeReference: new AttributeReference('legs'), }; const flag = makeBooleanFlagWithOneClause(clause); const context = Context.fromLDContext(user); const res = await evaluator.evaluate(flag, context!); expect(res.detail.value).toBe(true); - }, - ); + }); - it.each([ - { key: 'x', name: 'Bob' }, - { kind: 'user', key: 'x', name: 'Bob' }, - { kind: 'multi', user: { key: 'x', name: 'Bob' } }, - ])('does not match missing attribute', async (user) => { - const clause: Clause = { - attribute: 'legs', - op: 'in', - values: [4], - attributeReference: new AttributeReference('legs'), - }; - const flag = makeBooleanFlagWithOneClause(clause); - const context = Context.fromLDContext(user); - const res = await evaluator.evaluate(flag, context!); - expect(res.detail.value).toBe(false); - }); + it.each<[LDContext, string]>([ + [{ key: 'x', name: 'Bob', custom: { '//': 4 } }, '//'], + [ + { + kind: 'user', + key: 'x', + name: 'Bob', + '//': 4, + }, + '//', + ], + [{ kind: 'multi', user: { key: 'x', name: 'Bob', '//': 4 } }, '//'], + [{ key: 'x', name: 'Bob', custom: { '/~~': 4 } }, '/~~'], + [ + { + kind: 'user', + key: 'x', + name: 'Bob', + '/~~': 4, + }, + '/~~', + ], + [{ kind: 'multi', user: { key: 'x', name: 'Bob', '/~~': 4 } }, '/~~'], + ])( + 'can match attributes which would have be invalid references, but are valid literals', + async (user, attribute) => { + const clause: Clause = { + attribute, + op: 'in', + values: [4], + attributeReference: new AttributeReference(attribute, true), + }; + const flag = makeBooleanFlagWithOneClause(clause); + const context = Context.fromLDContext(user); + const res = await evaluator.evaluate(flag, context!); + expect(res.detail.value).toBe(true); + }, + ); - it.each([ - { key: 'x', name: 'Bob' }, - { kind: 'user', key: 'x', name: 'Bob' }, - { kind: 'multi', user: { key: 'x', name: 'Bob' } }, - ])('can have a negated clause', async (user) => { - const clause: Clause = { - attribute: 'name', - op: 'in', - values: ['Bob'], - negate: true, - attributeReference: new AttributeReference('name'), - }; - const flag = makeBooleanFlagWithOneClause(clause); - const context = Context.fromLDContext(user); - const res = await evaluator.evaluate(flag, context!); - expect(res.detail.value).toBe(false); - }); + it.each([ + { key: 'x', name: 'Bob' }, + { kind: 'user', key: 'x', name: 'Bob' }, + { kind: 'multi', user: { key: 'x', name: 'Bob' } }, + ])('does not match missing attribute', async (user) => { + const clause: Clause = { + attribute: 'legs', + op: 'in', + values: [4], + attributeReference: new AttributeReference('legs'), + }; + const flag = makeBooleanFlagWithOneClause(clause); + const context = Context.fromLDContext(user); + const res = await evaluator.evaluate(flag, context!); + expect(res.detail.value).toBe(false); + }); - // An equivalent test existed in the previous suite. I see no reason that the - // current code could encounter such a situation, but this will help ensure - // it never does. - it('does not overflow the call stack when evaluating a huge number of clauses', async () => { - const user = { key: 'user' }; - const clauseCount = 5000; - const flag: Flag = { - key: 'flag', - targets: [], - on: true, - variations: [false, true], - fallthrough: { variation: 0 }, - version: 1, - }; - // Note, for this test to be meaningful, the clauses must all match the user, since we - // stop evaluating clauses on the first non-match. - const clause = makeClauseThatMatchesUser(user); - const clauses = []; - for (let i = 0; i < clauseCount; i += 1) { - clauses.push(clause); - } - const rule: FlagRule = { id: '1234', clauses, variation: 1 }; - flag.rules = [rule]; - const context = Context.fromLDContext(user); - const res = await evaluator.evaluate(flag, context!); - expect(res.detail.value).toBe(true); - }); + it.each([ + { key: 'x', name: 'Bob' }, + { kind: 'user', key: 'x', name: 'Bob' }, + { kind: 'multi', user: { key: 'x', name: 'Bob' } }, + ])('can have a negated clause', async (user) => { + const clause: Clause = { + attribute: 'name', + op: 'in', + values: ['Bob'], + negate: true, + attributeReference: new AttributeReference('name'), + }; + const flag = makeBooleanFlagWithOneClause(clause); + const context = Context.fromLDContext(user); + const res = await evaluator.evaluate(flag, context!); + expect(res.detail.value).toBe(false); + }); - it('matches kind of implicit user', async () => { - const user = { key: 'x', name: 'Bob' }; - const clause: Clause = { - attribute: 'kind', - op: 'in', - values: ['user'], - attributeReference: new AttributeReference('kind'), - }; - const flag = makeBooleanFlagWithOneClause(clause); - const context = Context.fromLDContext(user); - const res = await evaluator.evaluate(flag, context!); - expect(res.detail.value).toBe(true); - }); + // An equivalent test existed in the previous suite. I see no reason that the + // current code could encounter such a situation, but this will help ensure + // it never does. + it('does not overflow the call stack when evaluating a huge number of clauses', async () => { + const user = { key: 'user' }; + const clauseCount = 5000; + const flag: Flag = { + key: 'flag', + targets: [], + on: true, + variations: [false, true], + fallthrough: { variation: 0 }, + version: 1, + }; + // Note, for this test to be meaningful, the clauses must all match the user, since we + // stop evaluating clauses on the first non-match. + const clause = makeClauseThatMatchesUser(user); + const clauses = []; + for (let i = 0; i < clauseCount; i += 1) { + clauses.push(clause); + } + const rule: FlagRule = { id: '1234', clauses, variation: 1 }; + flag.rules = [rule]; + const context = Context.fromLDContext(user); + const res = await evaluator.evaluate(flag, context!); + expect(res.detail.value).toBe(true); + }); - it('implicit user kind does not match rules for non-user kinds', async () => { - const user = { key: 'x', name: 'Bob' }; - const clause: Clause = { - attribute: 'key', - op: 'in', - values: ['userkey'], - contextKind: 'org', - attributeReference: new AttributeReference('key'), - }; - const flag = makeBooleanFlagWithOneClause(clause); - const context = Context.fromLDContext(user); - const res = await evaluator.evaluate(flag, context!); - expect(res.detail.value).toBe(false); - }); -}); + it('matches kind of implicit user', async () => { + const user = { key: 'x', name: 'Bob' }; + const clause: Clause = { + attribute: 'kind', + op: 'in', + values: ['user'], + attributeReference: new AttributeReference('kind'), + }; + const flag = makeBooleanFlagWithOneClause(clause); + const context = Context.fromLDContext(user); + const res = await evaluator.evaluate(flag, context!); + expect(res.detail.value).toBe(true); + }); -describe('given non-user single-kind contexts', () => { - it('does not match implicit user clauses to non-user contexts', async () => { - const clause: Clause = { - attribute: 'name', - op: 'in', - values: ['Bob'], - attributeReference: new AttributeReference('name'), - }; - const flag = makeBooleanFlagWithOneClause(clause); - const context = Context.fromLDContext({ kind: 'org', name: 'Bob', key: 'bobkey' }); - const res = await evaluator.evaluate(flag, context!); - expect(res.detail.value).toBe(false); + it('implicit user kind does not match rules for non-user kinds', async () => { + const user = { key: 'x', name: 'Bob' }; + const clause: Clause = { + attribute: 'key', + op: 'in', + values: ['userkey'], + contextKind: 'org', + attributeReference: new AttributeReference('key'), + }; + const flag = makeBooleanFlagWithOneClause(clause); + const context = Context.fromLDContext(user); + const res = await evaluator.evaluate(flag, context!); + expect(res.detail.value).toBe(false); + }); }); - it('cannot use an object attribute for a match.', async () => { - const clause: Clause = { - attribute: 'complex', - op: 'in', - values: [{ thing: true }], - contextKind: 'org', - attributeReference: new AttributeReference('complex'), - }; - const flag = makeBooleanFlagWithOneClause(clause); - const context = Context.fromLDContext({ - kind: 'org', - name: 'Bob', - key: 'bobkey', - complex: { thing: true }, + describe('given non-user single-kind contexts', () => { + it('does not match implicit user clauses to non-user contexts', async () => { + const clause: Clause = { + attribute: 'name', + op: 'in', + values: ['Bob'], + attributeReference: new AttributeReference('name'), + }; + const flag = makeBooleanFlagWithOneClause(clause); + const context = Context.fromLDContext({ kind: 'org', name: 'Bob', key: 'bobkey' }); + const res = await evaluator.evaluate(flag, context!); + expect(res.detail.value).toBe(false); }); - const res = await evaluator.evaluate(flag, context!); - expect(res.detail.value).toBe(false); - }); - it('does match clauses for the correct context kind', async () => { - const clause: Clause = { - attribute: 'name', - op: 'in', - values: ['Bob'], - contextKind: 'org', - attributeReference: new AttributeReference('name'), - }; - const flag = makeBooleanFlagWithOneClause(clause); - const context = Context.fromLDContext({ kind: 'org', name: 'Bob', key: 'bobkey' }); - const res = await evaluator.evaluate(flag, context!); - expect(res.detail.value).toBe(true); - }); + it('cannot use an object attribute for a match.', async () => { + const clause: Clause = { + attribute: 'complex', + op: 'in', + values: [{ thing: true }], + contextKind: 'org', + attributeReference: new AttributeReference('complex'), + }; + const flag = makeBooleanFlagWithOneClause(clause); + const context = Context.fromLDContext({ + kind: 'org', + name: 'Bob', + key: 'bobkey', + complex: { thing: true }, + }); + const res = await evaluator.evaluate(flag, context!); + expect(res.detail.value).toBe(false); + }); - it('matches clauses for the kind attribute', async () => { - // The context kind here should not matter, but the 'kind' attribute should. - const clause: Clause = { - attribute: 'kind', - op: 'in', - values: ['org'], - contextKind: 'potato', - attributeReference: new AttributeReference('kind'), - }; - const flag = makeBooleanFlagWithOneClause(clause); - const context = Context.fromLDContext({ kind: 'org', name: 'Bob', key: 'bobkey' }); - const res = await evaluator.evaluate(flag, context!); - expect(res.detail.value).toBe(true); - }); + it('does match clauses for the correct context kind', async () => { + const clause: Clause = { + attribute: 'name', + op: 'in', + values: ['Bob'], + contextKind: 'org', + attributeReference: new AttributeReference('name'), + }; + const flag = makeBooleanFlagWithOneClause(clause); + const context = Context.fromLDContext({ kind: 'org', name: 'Bob', key: 'bobkey' }); + const res = await evaluator.evaluate(flag, context!); + expect(res.detail.value).toBe(true); + }); - it('does not match clauses for the kind attribute if the kind does not match', async () => { - // The context kind here should not matter, but the 'kind' attribute should. - const clause: Clause = { - attribute: 'kind', - op: 'in', - values: ['org'], - contextKind: 'potato', - attributeReference: new AttributeReference('kind'), - }; - const flag = makeBooleanFlagWithOneClause(clause); - const context = Context.fromLDContext({ kind: 'party', name: 'Bob', key: 'bobkey' }); - const res = await evaluator.evaluate(flag, context!); - expect(res.detail.value).toBe(false); + it('matches clauses for the kind attribute', async () => { + // The context kind here should not matter, but the 'kind' attribute should. + const clause: Clause = { + attribute: 'kind', + op: 'in', + values: ['org'], + contextKind: 'potato', + attributeReference: new AttributeReference('kind'), + }; + const flag = makeBooleanFlagWithOneClause(clause); + const context = Context.fromLDContext({ kind: 'org', name: 'Bob', key: 'bobkey' }); + const res = await evaluator.evaluate(flag, context!); + expect(res.detail.value).toBe(true); + }); + + it('does not match clauses for the kind attribute if the kind does not match', async () => { + // The context kind here should not matter, but the 'kind' attribute should. + const clause: Clause = { + attribute: 'kind', + op: 'in', + values: ['org'], + contextKind: 'potato', + attributeReference: new AttributeReference('kind'), + }; + const flag = makeBooleanFlagWithOneClause(clause); + const context = Context.fromLDContext({ kind: 'party', name: 'Bob', key: 'bobkey' }); + const res = await evaluator.evaluate(flag, context!); + expect(res.detail.value).toBe(false); + }); }); -}); -describe('given multi-kind contexts', () => { - it('does match clauses correctly with multiple contexts', async () => { - const clause1: Clause = { - attribute: 'region', - op: 'in', - values: ['north'], - contextKind: 'park', - attributeReference: new AttributeReference('region'), - }; - const clause2: Clause = { - attribute: 'count', - op: 'in', - values: [5], - contextKind: 'party', - attributeReference: new AttributeReference('count'), - }; + describe('given multi-kind contexts', () => { + it('does match clauses correctly with multiple contexts', async () => { + const clause1: Clause = { + attribute: 'region', + op: 'in', + values: ['north'], + contextKind: 'park', + attributeReference: new AttributeReference('region'), + }; + const clause2: Clause = { + attribute: 'count', + op: 'in', + values: [5], + contextKind: 'party', + attributeReference: new AttributeReference('count'), + }; - const context = Context.fromLDContext({ - kind: 'multi', - park: { - key: 'park', - region: 'north', - }, - party: { - key: 'party', - count: 5, - }, + const context = Context.fromLDContext({ + kind: 'multi', + park: { + key: 'park', + region: 'north', + }, + party: { + key: 'party', + count: 5, + }, + }); + + const flag = makeBooleanFlagWithRules([ + { id: '1234', clauses: [clause1, clause2], variation: 1 }, + ]); + const res = await evaluator.evaluate(flag, context!); + expect(res.detail.value).toBe(true); }); - const flag = makeBooleanFlagWithRules([ - { id: '1234', clauses: [clause1, clause2], variation: 1 }, - ]); - const res = await evaluator.evaluate(flag, context!); - expect(res.detail.value).toBe(true); - }); - - it('does not match the values from the wrong contexts', async () => { - const clause1: Clause = { - attribute: 'region', - op: 'in', - values: ['north'], - contextKind: 'park', - attributeReference: new AttributeReference('region'), - }; - const clause2: Clause = { - attribute: 'count', - op: 'in', - values: [5], - contextKind: 'party', - attributeReference: new AttributeReference('count'), - }; + it('does not match the values from the wrong contexts', async () => { + const clause1: Clause = { + attribute: 'region', + op: 'in', + values: ['north'], + contextKind: 'park', + attributeReference: new AttributeReference('region'), + }; + const clause2: Clause = { + attribute: 'count', + op: 'in', + values: [5], + contextKind: 'party', + attributeReference: new AttributeReference('count'), + }; - const context = Context.fromLDContext({ - kind: 'multi', - park: { - key: 'park', - count: 5, - }, - party: { - key: 'party', - region: 'north', - }, + const context = Context.fromLDContext({ + kind: 'multi', + park: { + key: 'park', + count: 5, + }, + party: { + key: 'party', + region: 'north', + }, + }); + + const flag = makeBooleanFlagWithRules([ + { id: '1234', clauses: [clause1, clause2], variation: 1 }, + ]); + const res = await evaluator.evaluate(flag, context!); + expect(res.detail.value).toBe(false); }); - const flag = makeBooleanFlagWithRules([ - { id: '1234', clauses: [clause1, clause2], variation: 1 }, - ]); - const res = await evaluator.evaluate(flag, context!); - expect(res.detail.value).toBe(false); - }); + it('can check for the presence of contexts', async () => { + const clause: Clause = { + attribute: 'kind', + op: 'in', + values: ['party'], + attributeReference: new AttributeReference('kind'), + }; - it('can check for the presence of contexts', async () => { - const clause: Clause = { - attribute: 'kind', - op: 'in', - values: ['party'], - attributeReference: new AttributeReference('kind'), - }; + const context = Context.fromLDContext({ + kind: 'multi', + park: { + key: 'park', + count: 5, + }, + party: { + key: 'party', + region: 'north', + }, + }); - const context = Context.fromLDContext({ - kind: 'multi', - park: { - key: 'park', - count: 5, - }, - party: { - key: 'party', - region: 'north', - }, + const flag = makeBooleanFlagWithOneClause(clause); + const res = await evaluator.evaluate(flag, context!); + expect(res.detail.value).toBe(true); }); - const flag = makeBooleanFlagWithOneClause(clause); - const res = await evaluator.evaluate(flag, context!); - expect(res.detail.value).toBe(true); + it('does not match if the kind is not in the context', async () => { + const clause: Clause = { + attribute: 'kind', + op: 'in', + values: ['zoo'], + attributeReference: new AttributeReference('kind'), + }; + + const context = Context.fromLDContext({ + kind: 'multi', + park: { + key: 'park', + count: 5, + }, + party: { + key: 'party', + region: 'north', + }, + }); + + const flag = makeBooleanFlagWithOneClause(clause); + const res = await evaluator.evaluate(flag, context!); + expect(res.detail.value).toBe(false); + }); }); - it('does not match if the kind is not in the context', async () => { + it('handles clauses with malformed attribute references', async () => { const clause: Clause = { - attribute: 'kind', + attribute: '//region', op: 'in', - values: ['zoo'], - attributeReference: new AttributeReference('kind'), + values: ['north'], + contextKind: 'park', + attributeReference: new AttributeReference('//region'), }; const context = Context.fromLDContext({ kind: 'multi', park: { key: 'park', - count: 5, + region: 'north', }, party: { key: 'party', - region: 'north', + count: 5, }, }); - const flag = makeBooleanFlagWithOneClause(clause); + const flag = makeBooleanFlagWithRules([{ id: '1234', clauses: [clause], variation: 1 }]); const res = await evaluator.evaluate(flag, context!); - expect(res.detail.value).toBe(false); - }); -}); - -it('handles clauses with malformed attribute references', async () => { - const clause: Clause = { - attribute: '//region', - op: 'in', - values: ['north'], - contextKind: 'park', - attributeReference: new AttributeReference('//region'), - }; - - const context = Context.fromLDContext({ - kind: 'multi', - park: { - key: 'park', - region: 'north', - }, - party: { - key: 'party', - count: 5, - }, + expect(res.detail.reason).toEqual({ kind: 'ERROR', errorKind: 'MALFORMED_FLAG' }); + expect(res.detail.value).toBe(null); }); - const flag = makeBooleanFlagWithRules([{ id: '1234', clauses: [clause], variation: 1 }]); - const res = await evaluator.evaluate(flag, context!); - expect(res.detail.reason).toEqual({ kind: 'ERROR', errorKind: 'MALFORMED_FLAG' }); - expect(res.detail.value).toBe(null); -}); + describe.each([ + ['lessThan', 99, 99.0001], + ['lessThanOrEqual', 99, 99.0001], + ['greaterThan', 99.0001, 99], + ['greaterThanOrEqual', 99.0001, 99], -describe.each([ - ['lessThan', 99, 99.0001], - ['lessThanOrEqual', 99, 99.0001], - ['greaterThan', 99.0001, 99], - ['greaterThanOrEqual', 99.0001, 99], - - // string comparisons - ['startsWith', 'xyz', 'x'], - ['endsWith', 'xyz', 'z'], - ['contains', 'xyz', 'y'], - - // regex - ['matches', 'hello world', 'hello.*rld'], - - // dates - ['before', 0, 1], - ['after', '1970-01-01T00:00:02.500Z', 1000], - - // semver - ['semVerLessThan', '2.0.0', '2.0.1'], - ['semVerGreaterThan', '2.0.1', '2.0.0'], -])( - 'executes operations with the clause value and context value correctly', - (operator, contextValue, clauseValue) => { - const clause: Clause = { - attribute: 'value', - // @ts-ignore - op: operator, - values: [clauseValue], - contextKind: 'potato', - attributeReference: new AttributeReference('value'), - }; + // string comparisons + ['startsWith', 'xyz', 'x'], + ['endsWith', 'xyz', 'z'], + ['contains', 'xyz', 'y'], - const context = Context.fromLDContext({ - kind: 'potato', - key: 'potato', - value: contextValue, - }); + // regex + ['matches', 'hello world', 'hello.*rld'], - const contextWArray = Context.fromLDContext({ - kind: 'potato', - key: 'potato', - value: [contextValue], - }); + // dates + ['before', 0, 1], + ['after', '1970-01-01T00:00:02.500Z', 1000], - it(`Operator ${operator} with ${contextValue} and ${clauseValue} should be true`, async () => { - const flag = makeBooleanFlagWithOneClause(clause); - const res = await evaluator.evaluate(flag, context); - expect(res.detail.value).toBe(true); + // semver + ['semVerLessThan', '2.0.0', '2.0.1'], + ['semVerGreaterThan', '2.0.1', '2.0.0'], + ])( + 'executes operations with the clause value and context value correctly', + (operator, contextValue, clauseValue) => { + const clause: Clause = { + attribute: 'value', + // @ts-ignore + op: operator, + values: [clauseValue], + contextKind: 'potato', + attributeReference: new AttributeReference('value'), + }; - const res2 = await evaluator.evaluate(flag, contextWArray); - expect(res2.detail.value).toBe(true); - }); - }, -); + const context = Context.fromLDContext({ + kind: 'potato', + key: 'potato', + value: contextValue, + }); + + const contextWArray = Context.fromLDContext({ + kind: 'potato', + key: 'potato', + value: [contextValue], + }); + + it(`Operator ${operator} with ${contextValue} and ${clauseValue} should be true`, async () => { + const flag = makeBooleanFlagWithOneClause(clause); + const res = await evaluator.evaluate(flag, context); + expect(res.detail.value).toBe(true); + + const res2 = await evaluator.evaluate(flag, contextWArray); + expect(res2.detail.value).toBe(true); + }); + }, + ); +}); diff --git a/packages/shared/sdk-server/__tests__/evaluation/Evaluator.rules.test.ts b/packages/shared/sdk-server/__tests__/evaluation/Evaluator.rules.test.ts index 60de3b117..853d4051a 100644 --- a/packages/shared/sdk-server/__tests__/evaluation/Evaluator.rules.test.ts +++ b/packages/shared/sdk-server/__tests__/evaluation/Evaluator.rules.test.ts @@ -18,166 +18,171 @@ const basicUser: LDContext = { key: 'userkey' }; const basicSingleKindUser: LDContext = { kind: 'user', key: 'userkey' }; const basicMultiKindUser: LDContext = { kind: 'multi', user: { key: 'userkey' } }; -const evaluator = new Evaluator(mocks.basicPlatform, noQueries); +describe('Evaluator.rules', () => { + let evaluator: Evaluator; + beforeEach(() => { + evaluator = new Evaluator(mocks.basicPlatform, noQueries); + }); -describe('when evaluating user equivalent contexts', () => { - const matchClause = makeClauseThatMatchesUser(basicUser); - const noMatchClause = makeClauseThatDoesNotMatchUser(basicUser); + describe('when evaluating user equivalent contexts', () => { + const matchClause = makeClauseThatMatchesUser(basicUser); + const noMatchClause = makeClauseThatDoesNotMatchUser(basicUser); - it.each([basicUser, basicSingleKindUser, basicMultiKindUser])( - 'matches user from rules', - async (userToTest) => { - const rule0: FlagRule = { - id: 'id0', - clauses: [noMatchClause], - variation: 1, - }; - const rule1: FlagRule = { - id: 'id1', - clauses: [matchClause], - variation: 2, - }; - const flag = makeFlagWithRules([rule0, rule1]); - const res = await evaluator.evaluate(flag, Context.fromLDContext(userToTest)); - expect(res.detail).toMatchObject({ - value: 'c', - variationIndex: 2, - reason: { kind: 'RULE_MATCH', ruleIndex: 1, ruleId: 'id1' }, - }); - }, - ); + it.each([basicUser, basicSingleKindUser, basicMultiKindUser])( + 'matches user from rules', + async (userToTest) => { + const rule0: FlagRule = { + id: 'id0', + clauses: [noMatchClause], + variation: 1, + }; + const rule1: FlagRule = { + id: 'id1', + clauses: [matchClause], + variation: 2, + }; + const flag = makeFlagWithRules([rule0, rule1]); + const res = await evaluator.evaluate(flag, Context.fromLDContext(userToTest)); + expect(res.detail).toMatchObject({ + value: 'c', + variationIndex: 2, + reason: { kind: 'RULE_MATCH', ruleIndex: 1, ruleId: 'id1' }, + }); + }, + ); - it.each([basicUser, basicSingleKindUser, basicMultiKindUser])( - 'returns error if rule variation is too high', - async (userToTest) => { - const rule: FlagRule = { id: 'id', clauses: [matchClause], variation: 99 }; - const flag = makeFlagWithRules([rule]); - const res = await evaluator.evaluate(flag, Context.fromLDContext(userToTest)); - expect(res.isError).toBeTruthy(); - expect(res.message).toEqual('Invalid variation index in flag'); - expect(res.detail).toMatchObject({ - value: null, - variationIndex: null, - reason: { kind: 'ERROR', errorKind: 'MALFORMED_FLAG' }, - }); - }, - ); + it.each([basicUser, basicSingleKindUser, basicMultiKindUser])( + 'returns error if rule variation is too high', + async (userToTest) => { + const rule: FlagRule = { id: 'id', clauses: [matchClause], variation: 99 }; + const flag = makeFlagWithRules([rule]); + const res = await evaluator.evaluate(flag, Context.fromLDContext(userToTest)); + expect(res.isError).toBeTruthy(); + expect(res.message).toEqual('Invalid variation index in flag'); + expect(res.detail).toMatchObject({ + value: null, + variationIndex: null, + reason: { kind: 'ERROR', errorKind: 'MALFORMED_FLAG' }, + }); + }, + ); - it.each([basicUser, basicSingleKindUser, basicMultiKindUser])( - 'returns error if rule variation is negative', - async (userToTest) => { - const rule: FlagRule = { id: 'id', clauses: [matchClause], variation: -1 }; - const flag = makeFlagWithRules([rule]); - const res = await evaluator.evaluate(flag, Context.fromLDContext(userToTest)); - expect(res.isError).toBeTruthy(); - expect(res.message).toEqual('Invalid variation index in flag'); - expect(res.detail).toMatchObject({ - value: null, - variationIndex: null, - reason: { kind: 'ERROR', errorKind: 'MALFORMED_FLAG' }, - }); - }, - ); + it.each([basicUser, basicSingleKindUser, basicMultiKindUser])( + 'returns error if rule variation is negative', + async (userToTest) => { + const rule: FlagRule = { id: 'id', clauses: [matchClause], variation: -1 }; + const flag = makeFlagWithRules([rule]); + const res = await evaluator.evaluate(flag, Context.fromLDContext(userToTest)); + expect(res.isError).toBeTruthy(); + expect(res.message).toEqual('Invalid variation index in flag'); + expect(res.detail).toMatchObject({ + value: null, + variationIndex: null, + reason: { kind: 'ERROR', errorKind: 'MALFORMED_FLAG' }, + }); + }, + ); - it.each([basicUser, basicSingleKindUser, basicMultiKindUser])( - 'returns error if rule has no variation or rollout', - async (userToTest) => { - const rule: FlagRule = { id: 'id', clauses: [matchClause] }; - const flag = makeFlagWithRules([rule]); - const res = await evaluator.evaluate(flag, Context.fromLDContext(userToTest)); - expect(res.isError).toBeTruthy(); - expect(res.message).toEqual('Variation/rollout object with no variation or rollout'); - expect(res.detail).toMatchObject({ - value: null, - variationIndex: null, - reason: { kind: 'ERROR', errorKind: 'MALFORMED_FLAG' }, - }); - }, - ); + it.each([basicUser, basicSingleKindUser, basicMultiKindUser])( + 'returns error if rule has no variation or rollout', + async (userToTest) => { + const rule: FlagRule = { id: 'id', clauses: [matchClause] }; + const flag = makeFlagWithRules([rule]); + const res = await evaluator.evaluate(flag, Context.fromLDContext(userToTest)); + expect(res.isError).toBeTruthy(); + expect(res.message).toEqual('Variation/rollout object with no variation or rollout'); + expect(res.detail).toMatchObject({ + value: null, + variationIndex: null, + reason: { kind: 'ERROR', errorKind: 'MALFORMED_FLAG' }, + }); + }, + ); - it.each([basicUser, basicSingleKindUser, basicMultiKindUser])( - 'returns error if rule has rollout with no variations', - async (userToTest) => { - const rule: FlagRule = { id: 'id', clauses: [matchClause], rollout: { variations: [] } }; - const flag = makeFlagWithRules([rule]); - const res = await evaluator.evaluate(flag, Context.fromLDContext(userToTest)); - expect(res.isError).toBeTruthy(); - expect(res.message).toEqual('Variation/rollout object with no variation or rollout'); - expect(res.detail).toMatchObject({ - value: null, - variationIndex: null, - reason: { kind: 'ERROR', errorKind: 'MALFORMED_FLAG' }, - }); - }, - ); + it.each([basicUser, basicSingleKindUser, basicMultiKindUser])( + 'returns error if rule has rollout with no variations', + async (userToTest) => { + const rule: FlagRule = { id: 'id', clauses: [matchClause], rollout: { variations: [] } }; + const flag = makeFlagWithRules([rule]); + const res = await evaluator.evaluate(flag, Context.fromLDContext(userToTest)); + expect(res.isError).toBeTruthy(); + expect(res.message).toEqual('Variation/rollout object with no variation or rollout'); + expect(res.detail).toMatchObject({ + value: null, + variationIndex: null, + reason: { kind: 'ERROR', errorKind: 'MALFORMED_FLAG' }, + }); + }, + ); - it.each([basicUser, basicSingleKindUser, basicMultiKindUser])( - 'does not overflow the call stack when evaluating a huge number of rules', - async (userToTest) => { - const ruleCount = 5000; - const flag: Flag = { - key: 'flag', - targets: [], - on: true, - variations: [false, true], - fallthrough: { variation: 0 }, - version: 1, - }; - // Note, for this test to be meaningful, the rules must *not* match the user, since we - // stop evaluating rules on the first match. - const rules: FlagRule[] = []; - for (let i = 0; i < ruleCount; i += 1) { - rules.push({ id: '1234', clauses: [noMatchClause], variation: 1 }); - } - flag.rules = rules; - const res = await evaluator.evaluate(flag, Context.fromLDContext(userToTest)); - expect(res.isError).toBeFalsy(); - expect(res.detail.value).toEqual(false); - }, - ); -}); + it.each([basicUser, basicSingleKindUser, basicMultiKindUser])( + 'does not overflow the call stack when evaluating a huge number of rules', + async (userToTest) => { + const ruleCount = 5000; + const flag: Flag = { + key: 'flag', + targets: [], + on: true, + variations: [false, true], + fallthrough: { variation: 0 }, + version: 1, + }; + // Note, for this test to be meaningful, the rules must *not* match the user, since we + // stop evaluating rules on the first match. + const rules: FlagRule[] = []; + for (let i = 0; i < ruleCount; i += 1) { + rules.push({ id: '1234', clauses: [noMatchClause], variation: 1 }); + } + flag.rules = rules; + const res = await evaluator.evaluate(flag, Context.fromLDContext(userToTest)); + expect(res.isError).toBeFalsy(); + expect(res.detail.value).toEqual(false); + }, + ); + }); -describe('when evaluating non-user contexts', () => { - const targetKey = 'targetKey'; - const targetContextKind = 'org'; - const matchClause: Clause = { - attribute: 'key', - op: 'in', - values: [targetKey], - contextKind: targetContextKind, - attributeReference: new AttributeReference('key'), - }; - const noMatchClause: Clause = { - attribute: 'key', - op: 'in', - values: [`not-${targetKey}`], - contextKind: targetContextKind, - attributeReference: new AttributeReference('key'), - }; + describe('when evaluating non-user contexts', () => { + const targetKey = 'targetKey'; + const targetContextKind = 'org'; + const matchClause: Clause = { + attribute: 'key', + op: 'in', + values: [targetKey], + contextKind: targetContextKind, + attributeReference: new AttributeReference('key'), + }; + const noMatchClause: Clause = { + attribute: 'key', + op: 'in', + values: [`not-${targetKey}`], + contextKind: targetContextKind, + attributeReference: new AttributeReference('key'), + }; - const singleKindContext: LDContext = { - kind: targetContextKind, - key: targetKey, - }; - const multiKindContext: LDContext = { - kind: 'multi', - }; - multiKindContext[targetContextKind] = { - key: targetKey, - }; + const singleKindContext: LDContext = { + kind: targetContextKind, + key: targetKey, + }; + const multiKindContext: LDContext = { + kind: 'multi', + }; + multiKindContext[targetContextKind] = { + key: targetKey, + }; - it.each([singleKindContext, multiKindContext])( - 'matches user from rules', - async (contextToTest) => { - const rule0: FlagRule = { id: 'id0', clauses: [noMatchClause], variation: 1 }; - const rule1: FlagRule = { id: 'id1', clauses: [matchClause], variation: 2 }; - const flag = makeFlagWithRules([rule0, rule1]); - const res = await evaluator.evaluate(flag, Context.fromLDContext(contextToTest)); - expect(res.detail).toMatchObject({ - value: 'c', - variationIndex: 2, - reason: { kind: 'RULE_MATCH', ruleIndex: 1, ruleId: 'id1' }, - }); - }, - ); + it.each([singleKindContext, multiKindContext])( + 'matches user from rules', + async (contextToTest) => { + const rule0: FlagRule = { id: 'id0', clauses: [noMatchClause], variation: 1 }; + const rule1: FlagRule = { id: 'id1', clauses: [matchClause], variation: 2 }; + const flag = makeFlagWithRules([rule0, rule1]); + const res = await evaluator.evaluate(flag, Context.fromLDContext(contextToTest)); + expect(res.detail).toMatchObject({ + value: 'c', + variationIndex: 2, + reason: { kind: 'RULE_MATCH', ruleIndex: 1, ruleId: 'id1' }, + }); + }, + ); + }); }); diff --git a/packages/shared/sdk-server/__tests__/evaluation/Evaluator.test.ts b/packages/shared/sdk-server/__tests__/evaluation/Evaluator.test.ts index a51cd35e2..9c12dccc6 100644 --- a/packages/shared/sdk-server/__tests__/evaluation/Evaluator.test.ts +++ b/packages/shared/sdk-server/__tests__/evaluation/Evaluator.test.ts @@ -15,139 +15,143 @@ const offBaseFlag = { variations: ['zero', 'one', 'two'], }; -describe.each<[Flag, LDContext, EvalResult | undefined]>([ - [ - { - ...offBaseFlag, - }, - { key: 'user-key' }, - EvalResult.forSuccess(null, Reasons.Off, undefined), - ], - [ - { - ...offBaseFlag, - offVariation: 2, - }, - { key: 'user-key' }, - EvalResult.forSuccess('two', Reasons.Off, 2), - ], -])('Given off flags and an evaluator', (flag, context, expected) => { - const evaluator = new Evaluator(mocks.basicPlatform, noQueries); +describe('Evaluator.test', () => { + let evaluator: Evaluator; + beforeEach(() => { + evaluator = new Evaluator(mocks.basicPlatform, noQueries); + }); - it(`produces the expected evaluation result for context: ${context.key} ${ - // @ts-ignore - context.kind - } targets: ${flag.targets?.map( - (t) => `${t.values}, ${t.variation}`, - )} context targets: ${flag.contextTargets?.map( - (t) => `${t.contextKind}: ${t.values}, ${t.variation}`, - )}`, async () => { - const result = await evaluator.evaluate(flag, Context.fromLDContext(context)); - expect(result?.isError).toEqual(expected?.isError); - expect(result?.detail).toStrictEqual(expected?.detail); - expect(result?.message).toEqual(expected?.message); + describe.each<[Flag, LDContext, EvalResult | undefined]>([ + [ + { + ...offBaseFlag, + }, + { key: 'user-key' }, + EvalResult.forSuccess(null, Reasons.Off, undefined), + ], + [ + { + ...offBaseFlag, + offVariation: 2, + }, + { key: 'user-key' }, + EvalResult.forSuccess('two', Reasons.Off, 2), + ], + ])('Given off flags and an evaluator', (flag, context, expected) => { + it(`produces the expected evaluation result for context: ${context.key} ${ + // @ts-ignore + context.kind + } targets: ${flag.targets?.map( + (t) => `${t.values}, ${t.variation}`, + )} context targets: ${flag.contextTargets?.map( + (t) => `${t.contextKind}: ${t.values}, ${t.variation}`, + )}`, async () => { + const result = await evaluator.evaluate(flag, Context.fromLDContext(context)); + expect(result?.isError).toEqual(expected?.isError); + expect(result?.detail).toStrictEqual(expected?.detail); + expect(result?.message).toEqual(expected?.message); + }); }); -}); -const targetBaseFlag = { - key: 'feature0', - version: 1, - on: true, - fallthrough: { variation: 1 }, - variations: ['zero', 'one', 'two'], -}; + const targetBaseFlag = { + key: 'feature0', + version: 1, + on: true, + fallthrough: { variation: 1 }, + variations: ['zero', 'one', 'two'], + }; -describe.each<[Flag, LDContext, EvalResult | undefined]>([ - [ - { - ...targetBaseFlag, - targets: [ - { - values: ['user-key'], - variation: 0, - }, - ], - }, - { key: 'user-key' }, - EvalResult.forSuccess('zero', Reasons.TargetMatch, 0), - ], - [ - { - ...targetBaseFlag, - targets: [ - { - values: ['user-key'], - variation: 0, - }, - { - values: ['user-key2'], - variation: 2, - }, - ], - }, - { key: 'user-key2' }, - EvalResult.forSuccess('two', Reasons.TargetMatch, 2), - ], - [ - { - ...targetBaseFlag, - targets: [ - { - values: ['user-key'], - variation: 0, - }, - { - values: ['user-key2'], - variation: 2, - }, - ], - contextTargets: [ - { - values: [], - variation: 2, - }, - ], - }, - { key: 'user-key2' }, - EvalResult.forSuccess('two', Reasons.TargetMatch, 2), - ], - [ - { - ...targetBaseFlag, - targets: [ - { - values: ['user-key'], - variation: 0, - }, - { - values: ['user-key2'], - variation: 2, - }, - ], - contextTargets: [ - { - contextKind: 'org', - values: ['org-key'], - variation: 1, - }, - ], - }, - { kind: 'org', key: 'org-key' }, - EvalResult.forSuccess('one', Reasons.TargetMatch, 1), - ], -])('given flag configurations with different targets that match', (flag, context, expected) => { - const evaluator = new Evaluator(mocks.basicPlatform, noQueries); - it(`produces the expected evaluation result for context: ${context.key} ${ - // @ts-ignore - context.kind - } targets: ${flag.targets?.map( - (t) => `${t.values}, ${t.variation}`, - )} context targets: ${flag.contextTargets?.map( - (t) => `${t.contextKind}: ${t.values}, ${t.variation}`, - )}`, async () => { - const result = await evaluator.evaluate(flag, Context.fromLDContext(context)); - expect(result?.isError).toEqual(expected?.isError); - expect(result?.detail).toStrictEqual(expected?.detail); - expect(result?.message).toEqual(expected?.message); + describe.each<[Flag, LDContext, EvalResult | undefined]>([ + [ + { + ...targetBaseFlag, + targets: [ + { + values: ['user-key'], + variation: 0, + }, + ], + }, + { key: 'user-key' }, + EvalResult.forSuccess('zero', Reasons.TargetMatch, 0), + ], + [ + { + ...targetBaseFlag, + targets: [ + { + values: ['user-key'], + variation: 0, + }, + { + values: ['user-key2'], + variation: 2, + }, + ], + }, + { key: 'user-key2' }, + EvalResult.forSuccess('two', Reasons.TargetMatch, 2), + ], + [ + { + ...targetBaseFlag, + targets: [ + { + values: ['user-key'], + variation: 0, + }, + { + values: ['user-key2'], + variation: 2, + }, + ], + contextTargets: [ + { + values: [], + variation: 2, + }, + ], + }, + { key: 'user-key2' }, + EvalResult.forSuccess('two', Reasons.TargetMatch, 2), + ], + [ + { + ...targetBaseFlag, + targets: [ + { + values: ['user-key'], + variation: 0, + }, + { + values: ['user-key2'], + variation: 2, + }, + ], + contextTargets: [ + { + contextKind: 'org', + values: ['org-key'], + variation: 1, + }, + ], + }, + { kind: 'org', key: 'org-key' }, + EvalResult.forSuccess('one', Reasons.TargetMatch, 1), + ], + ])('given flag configurations with different targets that match', (flag, context, expected) => { + it(`produces the expected evaluation result for context: ${context.key} ${ + // @ts-ignore + context.kind + } targets: ${flag.targets?.map( + (t) => `${t.values}, ${t.variation}`, + )} context targets: ${flag.contextTargets?.map( + (t) => `${t.contextKind}: ${t.values}, ${t.variation}`, + )}`, async () => { + const result = await evaluator.evaluate(flag, Context.fromLDContext(context)); + expect(result?.isError).toEqual(expected?.isError); + expect(result?.detail).toStrictEqual(expected?.detail); + expect(result?.message).toEqual(expected?.message); + }); }); }); diff --git a/packages/shared/sdk-server/jest-setupFilesAfterEnv.ts b/packages/shared/sdk-server/jest-setupFilesAfterEnv.ts new file mode 100644 index 000000000..7f23f47df --- /dev/null +++ b/packages/shared/sdk-server/jest-setupFilesAfterEnv.ts @@ -0,0 +1,3 @@ +import { setupMocks } from '@launchdarkly/private-js-mocks'; + +setupMocks(); diff --git a/packages/shared/sdk-server/jest.config.js b/packages/shared/sdk-server/jest.config.js index 6753062cc..68e3f99f9 100644 --- a/packages/shared/sdk-server/jest.config.js +++ b/packages/shared/sdk-server/jest.config.js @@ -4,4 +4,5 @@ module.exports = { testEnvironment: 'node', moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'json', 'node'], collectCoverageFrom: ['src/**/*.ts'], + setupFilesAfterEnv: ['./jest-setupFilesAfterEnv.ts'], }; diff --git a/packages/shared/sdk-server/tsconfig.json b/packages/shared/sdk-server/tsconfig.json index 1a394bfbf..83caa8a43 100644 --- a/packages/shared/sdk-server/tsconfig.json +++ b/packages/shared/sdk-server/tsconfig.json @@ -14,5 +14,5 @@ "declarationMap": true, // enables importers to jump to source "stripInternal": true }, - "exclude": ["**/*.test.ts", "dist", "node_modules", "__tests__"] + "exclude": ["**/*.test.ts", "dist", "node_modules", "__tests__", "jest-setupFilesAfterEnv.ts"] } From 990cd5d680c7234e4afcd41c7b1bf0b8577f179b Mon Sep 17 00:00:00 2001 From: Yusinto Ngadiman Date: Sun, 21 Jan 2024 17:38:23 -0800 Subject: [PATCH 21/44] chore: Replace null coaslescence with falsy checks for empty strings. --- packages/shared/sdk-client/src/utils/injectAutoEnv.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/shared/sdk-client/src/utils/injectAutoEnv.ts b/packages/shared/sdk-client/src/utils/injectAutoEnv.ts index 36b244226..6ce4bdebf 100644 --- a/packages/shared/sdk-client/src/utils/injectAutoEnv.ts +++ b/packages/shared/sdk-client/src/utils/injectAutoEnv.ts @@ -38,9 +38,9 @@ export const injectApplication = ({ crypto, info }: Platform, config: Configurat const { ld_application } = info.platformData(); const ldApplication = clone(ld_application); - ldApplication.id = config.application?.id ?? ldApplication.id ?? name ?? wrapperName; + ldApplication.id = config.application?.id || ldApplication.id || name || wrapperName; ldApplication.version = - config.application?.version ?? ldApplication.version ?? version ?? wrapperVersion; + config.application?.version || ldApplication.version || version || wrapperVersion; const hasher = crypto.createHash('sha256'); hasher.update(ldApplication.id!); @@ -60,8 +60,8 @@ export const injectDevice = async (platform: Platform) => { const ldDevice = clone(ld_device); // TODO: check for empty string - ldDevice.os.name = os?.name ?? ldDevice.os.name; - ldDevice.os.version = os?.version ?? ldDevice.os.version; + ldDevice.os.name = os?.name || ldDevice.os.name; + ldDevice.os.version = os?.version || ldDevice.os.version; ldDevice.key = await getOrGenerateKey('ld_device', platform); return ldDevice; From ac612b6d6930462949d0de9bff595d8958c327f0 Mon Sep 17 00:00:00 2001 From: Yusinto Ngadiman Date: Mon, 22 Jan 2024 10:52:23 -0800 Subject: [PATCH 22/44] chore: Removed CryptoWithHash. Added hasher to exports. --- packages/shared/mocks/src/crypto.ts | 13 ++++--------- packages/shared/mocks/src/index.ts | 4 ++-- .../sdk-server-edge/jest-setupFilesAfterEnv.ts | 3 +++ packages/shared/sdk-server-edge/jest.config.json | 3 ++- .../__tests__/evaluation/Bucketer.test.ts | 14 +++++++------- 5 files changed, 18 insertions(+), 19 deletions(-) create mode 100644 packages/shared/sdk-server-edge/jest-setupFilesAfterEnv.ts diff --git a/packages/shared/mocks/src/crypto.ts b/packages/shared/mocks/src/crypto.ts index 032303fbb..22e800adc 100644 --- a/packages/shared/mocks/src/crypto.ts +++ b/packages/shared/mocks/src/crypto.ts @@ -1,19 +1,16 @@ -import type { Crypto, Hasher } from '@common'; +import type { Hasher } from '@common'; -// HACK: inject hasher so we can easily test calls to update & digest -export type CryptoWithHash = Crypto & { hasher: Hasher }; // eslint-disable-next-line import/no-mutable-exports -export let crypto: CryptoWithHash; +export let hasher: Hasher; export const setupCrypto = () => { let counter = 0; - const hasher: Hasher = { + hasher = { update: jest.fn(), digest: jest.fn(() => '1234567890123456'), }; - crypto = { - hasher, + return { createHash: jest.fn(() => hasher), createHmac: jest.fn(), randomUUID: jest.fn(() => { @@ -23,6 +20,4 @@ export const setupCrypto = () => { return `${counter}`; }), }; - - return crypto; }; diff --git a/packages/shared/mocks/src/index.ts b/packages/shared/mocks/src/index.ts index 58114d5a5..215c1ee65 100644 --- a/packages/shared/mocks/src/index.ts +++ b/packages/shared/mocks/src/index.ts @@ -1,6 +1,6 @@ import clientContext from './clientContext'; import ContextDeduplicator from './contextDeduplicator'; -import { type CryptoWithHash } from './crypto'; +import { hasher } from './crypto'; import logger from './logger'; import mockFetch from './mockFetch'; import { basicPlatform } from './platform'; @@ -10,11 +10,11 @@ import { MockStreamingProcessor, setupMockStreamingProcessor } from './streaming export { basicPlatform, clientContext, + hasher, mockFetch, logger, ContextDeduplicator, MockStreamingProcessor, setupMockStreamingProcessor, setupMocks, - CryptoWithHash, }; diff --git a/packages/shared/sdk-server-edge/jest-setupFilesAfterEnv.ts b/packages/shared/sdk-server-edge/jest-setupFilesAfterEnv.ts new file mode 100644 index 000000000..7f23f47df --- /dev/null +++ b/packages/shared/sdk-server-edge/jest-setupFilesAfterEnv.ts @@ -0,0 +1,3 @@ +import { setupMocks } from '@launchdarkly/private-js-mocks'; + +setupMocks(); diff --git a/packages/shared/sdk-server-edge/jest.config.json b/packages/shared/sdk-server-edge/jest.config.json index 617480774..205d3c3ab 100644 --- a/packages/shared/sdk-server-edge/jest.config.json +++ b/packages/shared/sdk-server-edge/jest.config.json @@ -5,5 +5,6 @@ "modulePathIgnorePatterns": ["dist"], "testEnvironment": "node", "moduleFileExtensions": ["ts", "tsx", "js", "jsx", "json", "node"], - "collectCoverageFrom": ["src/**/*.ts"] + "collectCoverageFrom": ["src/**/*.ts"], + "setupFilesAfterEnv": ["./jest-setupFilesAfterEnv.ts"] } diff --git a/packages/shared/sdk-server/__tests__/evaluation/Bucketer.test.ts b/packages/shared/sdk-server/__tests__/evaluation/Bucketer.test.ts index 58088e186..9e5413f03 100644 --- a/packages/shared/sdk-server/__tests__/evaluation/Bucketer.test.ts +++ b/packages/shared/sdk-server/__tests__/evaluation/Bucketer.test.ts @@ -2,13 +2,13 @@ // We cannot fully validate bucketing in the common tests. Platform implementations // should contain a consistency test. // Testing here can only validate we are providing correct inputs to the hashing algorithm. -import { AttributeReference, Context, LDContext } from '@launchdarkly/js-sdk-common'; -import { basicPlatform, type CryptoWithHash } from '@launchdarkly/private-js-mocks'; +import { AttributeReference, Context, Crypto, LDContext } from '@launchdarkly/js-sdk-common'; +import { basicPlatform, hasher } from '@launchdarkly/private-js-mocks'; import Bucketer from '../../src/evaluation/Bucketer'; describe('Bucketer.test', () => { - let crypto: CryptoWithHash; + let crypto: Crypto; beforeEach(() => { crypto = basicPlatform.crypto; @@ -90,8 +90,8 @@ describe('Bucketer.test', () => { // in the expected way. expect(bucket).toBeCloseTo(0.07111111110140983, 5); expect(hadContext).toBeTruthy(); - expect(crypto.hasher.update).toHaveBeenCalledWith(expected); - expect(crypto.hasher.digest).toHaveBeenCalledWith('hex'); + expect(hasher.update).toHaveBeenCalledWith(expected); + expect(hasher.digest).toHaveBeenCalledWith('hex'); }); afterEach(() => { @@ -126,8 +126,8 @@ describe('Bucketer.test', () => { ); expect(bucket).toEqual(0); expect(hadContext).toEqual(kind === 'org'); - expect(crypto.hasher.update).toBeCalledTimes(0); - expect(crypto.hasher.digest).toBeCalledTimes(0); + expect(hasher.update).toBeCalledTimes(0); + expect(hasher.digest).toBeCalledTimes(0); }); }); }); From 06cf961d49cc5a74a31b75bbd07b32f292e5ef66 Mon Sep 17 00:00:00 2001 From: Yusinto Ngadiman Date: Mon, 22 Jan 2024 12:12:15 -0800 Subject: [PATCH 23/44] chore: Improve mocks setup. Added readme instructions for mocks. Removed redundant jest setup files. --- packages/shared/mocks/README.md | 78 ++++++++++++++++++- packages/shared/mocks/package.json | 6 ++ packages/shared/mocks/src/index.ts | 2 - packages/shared/mocks/src/setupMocks.ts | 8 +- .../sdk-client/jest-setupFilesAfterEnv.ts | 5 -- packages/shared/sdk-client/jest.config.json | 2 +- .../jest-setupFilesAfterEnv.ts | 3 - .../shared/sdk-server-edge/jest.config.json | 2 +- .../sdk-server/jest-setupFilesAfterEnv.ts | 3 - packages/shared/sdk-server/jest.config.js | 2 +- 10 files changed, 88 insertions(+), 23 deletions(-) delete mode 100644 packages/shared/sdk-client/jest-setupFilesAfterEnv.ts delete mode 100644 packages/shared/sdk-server-edge/jest-setupFilesAfterEnv.ts delete mode 100644 packages/shared/sdk-server/jest-setupFilesAfterEnv.ts diff --git a/packages/shared/mocks/README.md b/packages/shared/mocks/README.md index 730bb4fee..a304c0a06 100644 --- a/packages/shared/mocks/README.md +++ b/packages/shared/mocks/README.md @@ -2,9 +2,83 @@ [![Actions Status][mocks-ci-badge]][mocks-ci] -**Internal use only.** +> [!CAUTION] +> Internal use only. +> This project contains JavaScript mocks that are consumed in unit tests in client-side and server-side JavaScript SDKs. -This project contains JavaScript mocks that are consumed in unit tests in client-side and server-side JavaScript SDKs. +## Installation + +This package is not published publicly. To use it internally, add the following line to your project's package.json +devDependencies. yarn workspace has been setup to recognize this package so this dependency should automatically work: + +```bash + "devDependencies": { + "@launchdarkly/private-js-mocks": "0.0.1", + ... +``` + +Then in your jest config add `@launchdarkly/private-js-mocks/setup` to setupFilesAfterEnv: + +```js +// jest.config.js or jest.config.json +module.exports = { + setupFilesAfterEnv: ['@launchdarkly/private-js-mocks/setup'], + ... +} +``` + +## Usage + +> [!IMPORTANT] +> basicPlatform must be used inside a test because it's setup before each test. + +- `basicPlatform`: a concrete but basic implementation of [Platform](https://github.com/launchdarkly/js-core/blob/main/packages/shared/common/src/api/platform/Platform.ts). This is setup beforeEach so it must be used inside a test. + +- `hasher`: a Hasher object returned by `Crypto.createHash`. All functions in this object are jest mocks. This is exported + separately as a top level export because `Crypto` does not expose this publicly and we want to respect that. + +## Example + +```tsx +import { basicPlatform, hasher } from '@launchdarkly/private-js-mocks'; + +// DOES NOT WORK: crypto is undefined because basicPlatform must be inside a test +// because it's setup by the package in beforeEach. +// const { crypto } = basicPlatform; // DON'T DO THIS + +describe('button', () => { + let crypto: Crypto; + + beforeEach(() => { + // WORKS: basicPlatform has been setup by the package + crypto = basicPlatform.crypto; // DO THIS + }); + + afterEach(() => { + jest.resetAllMocks(); + }); + + it('hashes the correct string', () => { + // arrange + const bucketer = new Bucketer(crypto); + + // act + const [bucket, hadContext] = bucketer.bucket(); + + // assert + expect(crypto.createHash).toHaveBeenCalled(); + + // GOTCHA: hasher is a separte import from crypto to respect + // the public Crypto interface. + expect(hasher.update).toHaveBeenCalledWith(expected); + expect(hasher.digest).toHaveBeenCalledWith('hex'); + }); +}); +``` + +## Developing this package + +If you make changes to this package, you'll need to run `yarn build` in the `mocks` directory for changes to take effect. ## Contributing diff --git a/packages/shared/mocks/package.json b/packages/shared/mocks/package.json index aa8ceb63f..4981ad1a6 100644 --- a/packages/shared/mocks/package.json +++ b/packages/shared/mocks/package.json @@ -5,6 +5,12 @@ "type": "commonjs", "main": "./dist/index.js", "types": "./dist/index.d.ts", + "exports": { + ".": "./dist/index.js", + "./setup": { + "default": "./dist/setupMocks.js" + } + }, "homepage": "https://github.com/launchdarkly/js-core/tree/main/packages/shared/common", "repository": { "type": "git", diff --git a/packages/shared/mocks/src/index.ts b/packages/shared/mocks/src/index.ts index 215c1ee65..a78d38c41 100644 --- a/packages/shared/mocks/src/index.ts +++ b/packages/shared/mocks/src/index.ts @@ -4,7 +4,6 @@ import { hasher } from './crypto'; import logger from './logger'; import mockFetch from './mockFetch'; import { basicPlatform } from './platform'; -import setupMocks from './setupMocks'; import { MockStreamingProcessor, setupMockStreamingProcessor } from './streamingProcessor'; export { @@ -16,5 +15,4 @@ export { ContextDeduplicator, MockStreamingProcessor, setupMockStreamingProcessor, - setupMocks, }; diff --git a/packages/shared/mocks/src/setupMocks.ts b/packages/shared/mocks/src/setupMocks.ts index 928e38849..fcfc3f4c2 100644 --- a/packages/shared/mocks/src/setupMocks.ts +++ b/packages/shared/mocks/src/setupMocks.ts @@ -1,7 +1,5 @@ import { setupBasicPlatform } from './platform'; -export default function setupMocks() { - beforeEach(() => { - setupBasicPlatform(); - }); -} +beforeEach(() => { + setupBasicPlatform(); +}); diff --git a/packages/shared/sdk-client/jest-setupFilesAfterEnv.ts b/packages/shared/sdk-client/jest-setupFilesAfterEnv.ts deleted file mode 100644 index 291ba9eca..000000000 --- a/packages/shared/sdk-client/jest-setupFilesAfterEnv.ts +++ /dev/null @@ -1,5 +0,0 @@ -import '@testing-library/jest-dom'; - -import { setupMocks } from '@launchdarkly/private-js-mocks'; - -setupMocks(); diff --git a/packages/shared/sdk-client/jest.config.json b/packages/shared/sdk-client/jest.config.json index 65ddc27dd..c9d0d3b3d 100644 --- a/packages/shared/sdk-client/jest.config.json +++ b/packages/shared/sdk-client/jest.config.json @@ -6,5 +6,5 @@ "testEnvironment": "jsdom", "moduleFileExtensions": ["ts", "tsx", "js", "jsx", "json", "node"], "collectCoverageFrom": ["src/**/*.ts"], - "setupFilesAfterEnv": ["./jest-setupFilesAfterEnv.ts"] + "setupFilesAfterEnv": ["@launchdarkly/private-js-mocks/setup"] } diff --git a/packages/shared/sdk-server-edge/jest-setupFilesAfterEnv.ts b/packages/shared/sdk-server-edge/jest-setupFilesAfterEnv.ts deleted file mode 100644 index 7f23f47df..000000000 --- a/packages/shared/sdk-server-edge/jest-setupFilesAfterEnv.ts +++ /dev/null @@ -1,3 +0,0 @@ -import { setupMocks } from '@launchdarkly/private-js-mocks'; - -setupMocks(); diff --git a/packages/shared/sdk-server-edge/jest.config.json b/packages/shared/sdk-server-edge/jest.config.json index 205d3c3ab..ccaa70d72 100644 --- a/packages/shared/sdk-server-edge/jest.config.json +++ b/packages/shared/sdk-server-edge/jest.config.json @@ -6,5 +6,5 @@ "testEnvironment": "node", "moduleFileExtensions": ["ts", "tsx", "js", "jsx", "json", "node"], "collectCoverageFrom": ["src/**/*.ts"], - "setupFilesAfterEnv": ["./jest-setupFilesAfterEnv.ts"] + "setupFilesAfterEnv": ["@launchdarkly/private-js-mocks/setup"] } diff --git a/packages/shared/sdk-server/jest-setupFilesAfterEnv.ts b/packages/shared/sdk-server/jest-setupFilesAfterEnv.ts deleted file mode 100644 index 7f23f47df..000000000 --- a/packages/shared/sdk-server/jest-setupFilesAfterEnv.ts +++ /dev/null @@ -1,3 +0,0 @@ -import { setupMocks } from '@launchdarkly/private-js-mocks'; - -setupMocks(); diff --git a/packages/shared/sdk-server/jest.config.js b/packages/shared/sdk-server/jest.config.js index 68e3f99f9..bcd6a8d01 100644 --- a/packages/shared/sdk-server/jest.config.js +++ b/packages/shared/sdk-server/jest.config.js @@ -4,5 +4,5 @@ module.exports = { testEnvironment: 'node', moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'json', 'node'], collectCoverageFrom: ['src/**/*.ts'], - setupFilesAfterEnv: ['./jest-setupFilesAfterEnv.ts'], + setupFilesAfterEnv: ['@launchdarkly/private-js-mocks/setup'], }; From d39b47fd54e0c7cc1b2e009764a9eba6f53a252e Mon Sep 17 00:00:00 2001 From: Yusinto Ngadiman Date: Mon, 22 Jan 2024 12:15:01 -0800 Subject: [PATCH 24/44] chore: Fixed eslint require complaint. --- packages/sdk/react-native/jestSetupFile.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/sdk/react-native/jestSetupFile.ts b/packages/sdk/react-native/jestSetupFile.ts index 4b0720331..03f14b96f 100644 --- a/packages/sdk/react-native/jestSetupFile.ts +++ b/packages/sdk/react-native/jestSetupFile.ts @@ -1,5 +1,5 @@ jest.mock('@react-native-async-storage/async-storage', () => - require('@react-native-async-storage/async-storage/jest/async-storage-mock'), + jest.requireActual('@react-native-async-storage/async-storage/jest/async-storage-mock'), ); jest.mock('react-native', () => { From 89cddc2a9f01496c98445b0d1e144ec138100b9a Mon Sep 17 00:00:00 2001 From: Yusinto Ngadiman Date: Mon, 22 Jan 2024 12:22:50 -0800 Subject: [PATCH 25/44] chore: Replace deprecated toBeCalledTimes with toHaveBeenCalledTimes. Removed redundant exclusion for setupFilesAfterEnv. --- .../sdk-server/__tests__/evaluation/Bucketer.test.ts | 8 ++------ packages/shared/sdk-server/tsconfig.json | 2 +- 2 files changed, 3 insertions(+), 7 deletions(-) diff --git a/packages/shared/sdk-server/__tests__/evaluation/Bucketer.test.ts b/packages/shared/sdk-server/__tests__/evaluation/Bucketer.test.ts index 9e5413f03..8928868f1 100644 --- a/packages/shared/sdk-server/__tests__/evaluation/Bucketer.test.ts +++ b/packages/shared/sdk-server/__tests__/evaluation/Bucketer.test.ts @@ -93,10 +93,6 @@ describe('Bucketer.test', () => { expect(hasher.update).toHaveBeenCalledWith(expected); expect(hasher.digest).toHaveBeenCalledWith('hex'); }); - - afterEach(() => { - jest.resetAllMocks(); - }); }); describe.each([ @@ -126,8 +122,8 @@ describe('Bucketer.test', () => { ); expect(bucket).toEqual(0); expect(hadContext).toEqual(kind === 'org'); - expect(hasher.update).toBeCalledTimes(0); - expect(hasher.digest).toBeCalledTimes(0); + expect(hasher.update).toHaveBeenCalledTimes(0); + expect(hasher.digest).toHaveBeenCalledTimes(0); }); }); }); diff --git a/packages/shared/sdk-server/tsconfig.json b/packages/shared/sdk-server/tsconfig.json index 83caa8a43..1a394bfbf 100644 --- a/packages/shared/sdk-server/tsconfig.json +++ b/packages/shared/sdk-server/tsconfig.json @@ -14,5 +14,5 @@ "declarationMap": true, // enables importers to jump to source "stripInternal": true }, - "exclude": ["**/*.test.ts", "dist", "node_modules", "__tests__", "jest-setupFilesAfterEnv.ts"] + "exclude": ["**/*.test.ts", "dist", "node_modules", "__tests__"] } From 00090a70a1f15cdfca136d6bfe51e91e4e828513 Mon Sep 17 00:00:00 2001 From: Yusinto Ngadiman Date: Mon, 22 Jan 2024 12:37:53 -0800 Subject: [PATCH 26/44] chore: minor fixes related to previous mocks api changes. --- packages/shared/sdk-client/src/LDClientImpl.test.ts | 9 +++++++-- .../shared/sdk-client/src/utils/ensureKey.test.ts | 13 ++++++++++--- 2 files changed, 17 insertions(+), 5 deletions(-) diff --git a/packages/shared/sdk-client/src/LDClientImpl.test.ts b/packages/shared/sdk-client/src/LDClientImpl.test.ts index 93c66550c..d0fe98c55 100644 --- a/packages/shared/sdk-client/src/LDClientImpl.test.ts +++ b/packages/shared/sdk-client/src/LDClientImpl.test.ts @@ -1,5 +1,10 @@ import { clone, LDContext } from '@launchdarkly/js-sdk-common'; -import { basicPlatform, logger, setupMockStreamingProcessor } from '@launchdarkly/private-js-mocks'; +import { + basicPlatform, + hasher, + logger, + setupMockStreamingProcessor, +} from '@launchdarkly/private-js-mocks'; import * as mockResponseJson from './evaluation/mockResponse.json'; import LDClientImpl from './LDClientImpl'; @@ -44,7 +49,7 @@ describe('sdk-client object', () => { defaultPutResponse = clone(mockResponseJson); setupMockStreamingProcessor(false, defaultPutResponse); basicPlatform.crypto.randomUUID.mockReturnValue('random1'); - basicPlatform.crypto.hasher.digest.mockReturnValue('digested1'); + hasher.digest.mockReturnValue('digested1'); ldc = new LDClientImpl(testSdkKey, basicPlatform, { logger, sendEvents: false }); jest diff --git a/packages/shared/sdk-client/src/utils/ensureKey.test.ts b/packages/shared/sdk-client/src/utils/ensureKey.test.ts index 5990061a8..73f5d7881 100644 --- a/packages/shared/sdk-client/src/utils/ensureKey.test.ts +++ b/packages/shared/sdk-client/src/utils/ensureKey.test.ts @@ -1,18 +1,25 @@ import type { + Crypto, LDContext, LDContextCommon, LDMultiKindContext, LDUser, + Storage, } from '@launchdarkly/js-sdk-common'; import { basicPlatform } from '@launchdarkly/private-js-mocks'; import ensureKey from './ensureKey'; import { addNamespace, getOrGenerateKey } from './getOrGenerateKey'; -const { crypto, storage } = basicPlatform; describe('ensureKey', () => { + let crypto: Crypto; + let storage: Storage; + beforeEach(() => { - crypto.randomUUID.mockReturnValueOnce('random1').mockReturnValueOnce('random2'); + crypto = basicPlatform.crypto; + storage = basicPlatform.storage; + + (crypto.randomUUID as jest.Mock).mockReturnValueOnce('random1').mockReturnValueOnce('random2'); }); afterEach(() => { @@ -34,7 +41,7 @@ describe('ensureKey', () => { }); test('getOrGenerateKey existing key', async () => { - storage.get.mockImplementation((nsKind: string) => + (storage.get as jest.Mock).mockImplementation((nsKind: string) => nsKind === 'LaunchDarkly_AnonKeys_org' ? 'random1' : undefined, ); From 419f7932ffdb2ac7c71ee712b9d6e4597d4c6ee5 Mon Sep 17 00:00:00 2001 From: Yusinto Ngadiman Date: Mon, 22 Jan 2024 13:05:59 -0800 Subject: [PATCH 27/44] chore: Added autoEnvAttributes config option. Fixed unit tests. --- .../sdk-client/src/LDClientImpl.storage.test.ts | 6 +++++- packages/shared/sdk-client/src/LDClientImpl.ts | 8 ++++---- packages/shared/sdk-client/src/api/LDOptions.ts | 13 ++++++++++++- .../sdk-client/src/configuration/Configuration.ts | 1 + .../sdk-client/src/configuration/validators.ts | 1 + .../sdk-client/src/evaluation/fetchFlags.test.ts | 9 +++++---- 6 files changed, 28 insertions(+), 10 deletions(-) diff --git a/packages/shared/sdk-client/src/LDClientImpl.storage.test.ts b/packages/shared/sdk-client/src/LDClientImpl.storage.test.ts index 13b295d3b..ad151dd29 100644 --- a/packages/shared/sdk-client/src/LDClientImpl.storage.test.ts +++ b/packages/shared/sdk-client/src/LDClientImpl.storage.test.ts @@ -76,7 +76,11 @@ describe('sdk-client storage', () => { .spyOn(LDClientImpl.prototype as any, 'createStreamUriPath') .mockReturnValue('/stream/path'); - ldc = new LDClientImpl(testSdkKey, basicPlatform, { logger, sendEvents: false }); + ldc = new LDClientImpl(testSdkKey, basicPlatform, { + autoEnvAttributes: false, + logger, + sendEvents: false, + }); // @ts-ignore emitter = ldc.emitter; diff --git a/packages/shared/sdk-client/src/LDClientImpl.ts b/packages/shared/sdk-client/src/LDClientImpl.ts index 5f800b6c6..a694579ca 100644 --- a/packages/shared/sdk-client/src/LDClientImpl.ts +++ b/packages/shared/sdk-client/src/LDClientImpl.ts @@ -74,9 +74,6 @@ export default class LDClientImpl implements LDClient { this.diagnosticsManager, ); this.emitter = new LDEmitter(); - - // TODO: add logic to process auto env attributes correctly - // this.autoEnv = platform.info.platformData().autoEnv; } allFlags(): LDFlagSet { @@ -236,7 +233,10 @@ export default class LDClientImpl implements LDClient { // TODO: implement secure mode async identify(pristineContext: LDContext, _hash?: string): Promise { let context = await ensureKey(pristineContext, this.platform); - context = await injectAutoEnv(context, this.platform, this.config); + + if (this.config.autoEnvAttributes) { + context = await injectAutoEnv(context, this.platform, this.config); + } const checkedContext = Context.fromLDContext(context); if (!checkedContext.valid) { diff --git a/packages/shared/sdk-client/src/api/LDOptions.ts b/packages/shared/sdk-client/src/api/LDOptions.ts index 06484397c..9d85323e8 100644 --- a/packages/shared/sdk-client/src/api/LDOptions.ts +++ b/packages/shared/sdk-client/src/api/LDOptions.ts @@ -24,7 +24,6 @@ export interface LDOptions { * Example: `authentication-service` */ id?: string; - /** * A unique identifier representing the version of the application where the LaunchDarkly SDK is running. * @@ -36,6 +35,18 @@ export interface LDOptions { version?: string; }; + /** + * Enable / disable Auto environment attributes. When enabled, the SDK will automatically + * provide data about the mobile environment where the application is running. This data makes it simpler to target + * your mobile customers based on application name or version, or on device characteristics including manufacturer, + * model, operating system, locale, and so on. We recommend enabling this when you configure the SDK. To learn more, + * read [Automatic environment attributes](https://docs.launchdarkly.com/sdk/features/environment-attributes). + * for more documentation. + * + * The default is true. + */ + autoEnvAttributes?: boolean; + /** * The base uri for the LaunchDarkly server. * diff --git a/packages/shared/sdk-client/src/configuration/Configuration.ts b/packages/shared/sdk-client/src/configuration/Configuration.ts index e4f25940e..e50f37f5a 100644 --- a/packages/shared/sdk-client/src/configuration/Configuration.ts +++ b/packages/shared/sdk-client/src/configuration/Configuration.ts @@ -28,6 +28,7 @@ export default class Configuration { public readonly flushInterval = 2; public readonly streamInitialReconnectDelay = 1; + public readonly autoEnvAttributes = true; public readonly allAttributesPrivate = false; public readonly diagnosticOptOut = false; public readonly withReasons = false; diff --git a/packages/shared/sdk-client/src/configuration/validators.ts b/packages/shared/sdk-client/src/configuration/validators.ts index a48f9c5cf..9688e99c8 100644 --- a/packages/shared/sdk-client/src/configuration/validators.ts +++ b/packages/shared/sdk-client/src/configuration/validators.ts @@ -25,6 +25,7 @@ const validators: Record = { flushInterval: TypeValidators.numberWithMin(2), streamInitialReconnectDelay: TypeValidators.numberWithMin(0), + autoEnvAttributes: TypeValidators.Boolean, allAttributesPrivate: TypeValidators.Boolean, diagnosticOptOut: TypeValidators.Boolean, withReasons: TypeValidators.Boolean, diff --git a/packages/shared/sdk-client/src/evaluation/fetchFlags.test.ts b/packages/shared/sdk-client/src/evaluation/fetchFlags.test.ts index 2f6bbc9a9..2b3c3df8b 100644 --- a/packages/shared/sdk-client/src/evaluation/fetchFlags.test.ts +++ b/packages/shared/sdk-client/src/evaluation/fetchFlags.test.ts @@ -16,9 +16,10 @@ describe('fetchFeatures', () => { }; let config: Configuration; - const platformFetch = basicPlatform.requests.fetch as jest.Mock; + let platformFetch: jest.Mock; beforeEach(() => { + platformFetch = basicPlatform.requests.fetch as jest.Mock; mockFetch(mockResponse); config = new Configuration(); }); @@ -30,7 +31,7 @@ describe('fetchFeatures', () => { test('get', async () => { const json = await fetchFlags(sdkKey, context, config, basicPlatform); - expect(platformFetch).toBeCalledWith( + expect(platformFetch).toHaveBeenCalledWith( 'https://sdk.launchdarkly.com/sdk/evalx/testSdkKey1/contexts/eyJraW5kIjoidXNlciIsImtleSI6InRlc3QtdXNlci1rZXktMSJ9', { method: 'GET', @@ -45,7 +46,7 @@ describe('fetchFeatures', () => { config = new Configuration({ withReasons: true }); const json = await fetchFlags(sdkKey, context, config, basicPlatform); - expect(platformFetch).toBeCalledWith( + expect(platformFetch).toHaveBeenCalledWith( 'https://sdk.launchdarkly.com/sdk/evalx/testSdkKey1/contexts/eyJraW5kIjoidXNlciIsImtleSI6InRlc3QtdXNlci1rZXktMSJ9?withReasons=true', { method: 'GET', @@ -59,7 +60,7 @@ describe('fetchFeatures', () => { config = new Configuration({ hash: 'test-hash', withReasons: false }); const json = await fetchFlags(sdkKey, context, config, basicPlatform); - expect(platformFetch).toBeCalledWith( + expect(platformFetch).toHaveBeenCalledWith( 'https://sdk.launchdarkly.com/sdk/evalx/testSdkKey1/contexts/eyJraW5kIjoidXNlciIsImtleSI6InRlc3QtdXNlci1rZXktMSJ9?h=test-hash', { method: 'GET', From 3f5b0f2fa7ded39eeffa5bef9fdb075ceb14d26e Mon Sep 17 00:00:00 2001 From: Yusinto Ngadiman Date: Mon, 22 Jan 2024 15:08:20 -0800 Subject: [PATCH 28/44] chore: Added more tests. --- .../src/configuration/Configuration.test.ts | 1 + .../sdk-client/src/utils/ensureKey.test.ts | 4 +- .../src/utils/getOrGenerateKey.test.ts | 38 ++++++++++++ .../sdk-client/src/utils/getOrGenerateKey.ts | 6 +- .../src/utils/injectAutoEnv.test.ts | 61 +++++++++++++++++++ 5 files changed, 105 insertions(+), 5 deletions(-) create mode 100644 packages/shared/sdk-client/src/utils/getOrGenerateKey.test.ts create mode 100644 packages/shared/sdk-client/src/utils/injectAutoEnv.test.ts diff --git a/packages/shared/sdk-client/src/configuration/Configuration.test.ts b/packages/shared/sdk-client/src/configuration/Configuration.test.ts index 6a569b296..3305f6395 100644 --- a/packages/shared/sdk-client/src/configuration/Configuration.test.ts +++ b/packages/shared/sdk-client/src/configuration/Configuration.test.ts @@ -12,6 +12,7 @@ describe('Configuration', () => { expect(config).toMatchObject({ allAttributesPrivate: false, + autoEnvAttributes: true, baseUri: 'https://sdk.launchdarkly.com', capacity: 100, diagnosticOptOut: false, diff --git a/packages/shared/sdk-client/src/utils/ensureKey.test.ts b/packages/shared/sdk-client/src/utils/ensureKey.test.ts index 73f5d7881..bee38193f 100644 --- a/packages/shared/sdk-client/src/utils/ensureKey.test.ts +++ b/packages/shared/sdk-client/src/utils/ensureKey.test.ts @@ -41,8 +41,8 @@ describe('ensureKey', () => { }); test('getOrGenerateKey existing key', async () => { - (storage.get as jest.Mock).mockImplementation((nsKind: string) => - nsKind === 'LaunchDarkly_AnonKeys_org' ? 'random1' : undefined, + (storage.get as jest.Mock).mockImplementation((namespacedKind: string) => + namespacedKind === 'LaunchDarkly_AnonKeys_org' ? 'random1' : undefined, ); const key = await getOrGenerateKey('org', basicPlatform); diff --git a/packages/shared/sdk-client/src/utils/getOrGenerateKey.test.ts b/packages/shared/sdk-client/src/utils/getOrGenerateKey.test.ts new file mode 100644 index 000000000..7b6e587a9 --- /dev/null +++ b/packages/shared/sdk-client/src/utils/getOrGenerateKey.test.ts @@ -0,0 +1,38 @@ +import { Crypto, Storage } from '@launchdarkly/js-sdk-common'; +import { basicPlatform } from '@launchdarkly/private-js-mocks'; + +import { getOrGenerateKey } from './getOrGenerateKey'; + +describe('getOrGenerateKey', () => { + let crypto: Crypto; + let storage: Storage; + + beforeEach(() => { + crypto = basicPlatform.crypto; + storage = basicPlatform.storage; + + (crypto.randomUUID as jest.Mock).mockResolvedValue('test-org-key-1'); + }); + + afterEach(() => { + jest.resetAllMocks(); + }); + + test('key does not exist in cache so it must be generated', async () => { + (storage.get as jest.Mock).mockResolvedValue(undefined); + const k = await getOrGenerateKey('org', basicPlatform); + + expect(crypto.randomUUID).toHaveBeenCalled(); + expect(storage.set).toHaveBeenCalled(); + expect(k).toEqual('test-org-key-1'); + }); + + test('key exists in cache so not generated', async () => { + (storage.get as jest.Mock).mockResolvedValue('test-org-key-2'); + const k = await getOrGenerateKey('org', basicPlatform); + + expect(crypto.randomUUID).not.toHaveBeenCalled(); + expect(storage.set).not.toHaveBeenCalled(); + expect(k).toEqual('test-org-key-2'); + }); +}); diff --git a/packages/shared/sdk-client/src/utils/getOrGenerateKey.ts b/packages/shared/sdk-client/src/utils/getOrGenerateKey.ts index ee037197b..e4d3c4211 100644 --- a/packages/shared/sdk-client/src/utils/getOrGenerateKey.ts +++ b/packages/shared/sdk-client/src/utils/getOrGenerateKey.ts @@ -3,12 +3,12 @@ import { Platform } from '@launchdarkly/js-sdk-common'; export const addNamespace = (s: string) => `LaunchDarkly_AnonKeys_${s}`; export const getOrGenerateKey = async (kind: string, { crypto, storage }: Platform) => { - const nsKind = addNamespace(kind); - let contextKey = await storage?.get(nsKind); + const namespacedKind = addNamespace(kind); + let contextKey = await storage?.get(namespacedKind); if (!contextKey) { contextKey = crypto.randomUUID(); - await storage?.set(nsKind, contextKey); + await storage?.set(namespacedKind, contextKey); } return contextKey; diff --git a/packages/shared/sdk-client/src/utils/injectAutoEnv.test.ts b/packages/shared/sdk-client/src/utils/injectAutoEnv.test.ts new file mode 100644 index 000000000..a681fbead --- /dev/null +++ b/packages/shared/sdk-client/src/utils/injectAutoEnv.test.ts @@ -0,0 +1,61 @@ +import { Info } from '@launchdarkly/js-sdk-common'; +import { basicPlatform } from '@launchdarkly/private-js-mocks'; + +import Configuration from '../configuration'; +import { injectApplication, injectDevice, toMulti } from './injectAutoEnv'; + +describe('injectAutoEnv', () => { + let crypto: Crypto; + let info: Info; + + beforeEach(() => { + crypto = basicPlatform.crypto; + info = basicPlatform.info; + + (crypto.randomUUID as jest.Mock).mockResolvedValue('test-org-key-1'); + }); + + afterEach(() => { + jest.resetAllMocks(); + }); + + test('toMulti', () => { + const singleContext = { kind: 'user', key: 'test-user-key-1', name: 'bob' }; + const multi = toMulti(singleContext); + + expect(multi).toEqual({ kind: 'multi', user: { key: 'test-user-key-1', name: 'bob' } }); + }); + + test.todo('injectApplication uses user config application id'); + test.todo('injectApplication uses sdkData name'); + test.todo('injectApplication uses sdkData wrapperName'); + test('injectApplication uses auto env ld_application id, name and version', () => { + const config = new Configuration(); + const ldApplication = injectApplication(basicPlatform, config); + + expect(ldApplication).toEqual({ + envAttributesVersion: '1.0', + id: 'com.testapp.ld', + key: '1234567890123456', + name: 'LDApplication.TestApp', + version: '1.1.1', + }); + }); + + test.todo('injectDevice uses platformData os name and version'); + test('injectDevice use auto env os name and version', async () => { + const config = new Configuration(); + const ldDevice = await injectDevice(basicPlatform); + + expect(ldDevice).toEqual({ + envAttributesVersion: '1.0', + key: 'test-org-key-1', + manufacturer: 'apple', + os: { + family: 'apple', + name: 'iOS', + version: '17.17', + }, + }); + }); +}); From 000687b0ee3d4bf1e88453ec12af0851f041e21a Mon Sep 17 00:00:00 2001 From: Yusinto Ngadiman Date: Mon, 22 Jan 2024 17:19:09 -0800 Subject: [PATCH 29/44] chore: Added more auto env tests and fixed related bugs. Changed mock info to jest mocks so they can be reset. --- packages/shared/mocks/src/platform.ts | 22 +-- .../src/utils/injectAutoEnv.test.ts | 181 +++++++++++++++++- .../sdk-client/src/utils/injectAutoEnv.ts | 15 +- 3 files changed, 190 insertions(+), 28 deletions(-) diff --git a/packages/shared/mocks/src/platform.ts b/packages/shared/mocks/src/platform.ts index 6263d34c5..759bf5f1a 100644 --- a/packages/shared/mocks/src/platform.ts +++ b/packages/shared/mocks/src/platform.ts @@ -6,9 +6,9 @@ const encoding: Encoding = { btoa: (s: string) => Buffer.from(s).toString('base64'), }; -const info: Info = { - platformData(): PlatformData { - return { +const setupInfo = () => ({ + platformData: jest.fn( + (): PlatformData => ({ os: { name: 'iOS', version: '17.17', @@ -31,18 +31,18 @@ const info: Info = { os: { name: 'ios', version: '17', family: 'apple' }, manufacturer: 'apple', }, - }; - }, - sdkData(): SdkData { - return { + }), + ), + sdkData: jest.fn( + (): SdkData => ({ name: 'An SDK', version: '2.0.2', userAgentBase: 'TestUserAgent', wrapperName: 'Rapper', wrapperVersion: '1.2.3', - }; - }, -}; + }), + ), +}); const requests: Requests = { fetch: jest.fn(), @@ -60,7 +60,7 @@ export let basicPlatform: Platform; export const setupBasicPlatform = () => { basicPlatform = { encoding, - info, + info: setupInfo(), crypto: setupCrypto(), requests, storage, diff --git a/packages/shared/sdk-client/src/utils/injectAutoEnv.test.ts b/packages/shared/sdk-client/src/utils/injectAutoEnv.test.ts index a681fbead..95b18a4a5 100644 --- a/packages/shared/sdk-client/src/utils/injectAutoEnv.test.ts +++ b/packages/shared/sdk-client/src/utils/injectAutoEnv.test.ts @@ -1,8 +1,8 @@ -import { Info } from '@launchdarkly/js-sdk-common'; +import { Info, type LDContext, LDUser } from '@launchdarkly/js-sdk-common'; import { basicPlatform } from '@launchdarkly/private-js-mocks'; import Configuration from '../configuration'; -import { injectApplication, injectDevice, toMulti } from './injectAutoEnv'; +import { injectApplication, injectAutoEnv, injectDevice, toMulti } from './injectAutoEnv'; describe('injectAutoEnv', () => { let crypto: Crypto; @@ -12,7 +12,7 @@ describe('injectAutoEnv', () => { crypto = basicPlatform.crypto; info = basicPlatform.info; - (crypto.randomUUID as jest.Mock).mockResolvedValue('test-org-key-1'); + (crypto.randomUUID as jest.Mock).mockResolvedValue('test-device-key-1'); }); afterEach(() => { @@ -26,10 +26,85 @@ describe('injectAutoEnv', () => { expect(multi).toEqual({ kind: 'multi', user: { key: 'test-user-key-1', name: 'bob' } }); }); - test.todo('injectApplication uses user config application id'); - test.todo('injectApplication uses sdkData name'); - test.todo('injectApplication uses sdkData wrapperName'); - test('injectApplication uses auto env ld_application id, name and version', () => { + test('LDUser is unsupported', async () => { + const config = new Configuration(); + // const context = { kind: 'user', key: 'test-user-key-1', name: 'bob' }; + const user: LDUser = { key: 'legacy-user-key', name: 'bob' }; + const result = await injectAutoEnv(user, basicPlatform, config); + + expect(result).toEqual(user); + }); + + test('single kind should be converted to multi', async () => { + const config = new Configuration(); + const context = { kind: 'user', key: 'test-user-key-1', name: 'bob' }; + + const result = await injectAutoEnv(context, basicPlatform, config); + + expect(result).toEqual({ + kind: 'multi', + ld_application: { + envAttributesVersion: '1.0', + id: 'com.testapp.ld', + key: '1234567890123456', + name: 'LDApplication.TestApp', + version: '1.1.1', + }, + ld_device: { + envAttributesVersion: '1.0', + key: 'test-device-key-1', + manufacturer: 'apple', + os: { family: 'apple', name: 'iOS', version: '17.17' }, + }, + user: { key: 'test-user-key-1', name: 'bob' }, + }); + }); + + test('multi kind', async () => { + const config = new Configuration(); + const context: LDContext = { + kind: 'multi', + user: { key: 'test-user-key-1', name: 'bob' }, + org: { key: 'test-org-key-1', name: 'Best company' }, + }; + const result = await injectAutoEnv(context, basicPlatform, config); + + expect(result).toEqual({ + kind: 'multi', + user: { key: 'test-user-key-1', name: 'bob' }, + org: { key: 'test-org-key-1', name: 'Best company' }, + ld_application: { + envAttributesVersion: '1.0', + id: 'com.testapp.ld', + key: '1234567890123456', + name: 'LDApplication.TestApp', + version: '1.1.1', + }, + ld_device: { + envAttributesVersion: '1.0', + key: 'test-device-key-1', + manufacturer: 'apple', + os: { family: 'apple', name: 'iOS', version: '17.17' }, + }, + }); + }); + + test('injectApplication with config application id, version', () => { + const config = new Configuration({ + application: { id: 'com.from-config.ld', version: '2.2.2' }, + }); + const ldApplication = injectApplication(basicPlatform, config); + + expect(ldApplication).toEqual({ + envAttributesVersion: '1.0', + id: 'com.from-config.ld', + key: '1234567890123456', + name: 'LDApplication.TestApp', + version: '2.2.2', + }); + }); + + test('injectApplication with auto env application id, name, version', () => { const config = new Configuration(); const ldApplication = injectApplication(basicPlatform, config); @@ -42,14 +117,62 @@ describe('injectAutoEnv', () => { }); }); - test.todo('injectDevice uses platformData os name and version'); - test('injectDevice use auto env os name and version', async () => { + test('injectApplication with sdk data name, version', () => { + const platformData = basicPlatform.info.platformData(); + delete platformData.ld_application; + delete platformData.ld_device; + basicPlatform.info.platformData = jest.fn().mockReturnValueOnce(platformData); + basicPlatform.info.sdkData = jest.fn().mockReturnValueOnce({ + name: 'Name from sdk data', + version: '3.3.3', + userAgentBase: 'TestUserAgent', + wrapperName: 'Rapper', + wrapperVersion: '9.9.9', + }); + + const config = new Configuration(); + const ldApplication = injectApplication(basicPlatform, config); + + expect(ldApplication).toEqual({ + envAttributesVersion: '1.0', + id: 'Name from sdk data', + key: '1234567890123456', + name: 'Name from sdk data', + version: '3.3.3', + }); + }); + + test('injectApplication with sdkData wrapperName, wrapperVersion', () => { + const platformData = basicPlatform.info.platformData(); + delete platformData.ld_application; + delete platformData.ld_device; + basicPlatform.info.platformData = jest.fn().mockReturnValueOnce(platformData); + basicPlatform.info.sdkData = jest.fn().mockReturnValueOnce({ + name: '', + version: '', + userAgentBase: 'TestUserAgent', + wrapperName: 'Rapper', + wrapperVersion: '9.9.9', + }); + const config = new Configuration(); + const ldApplication = injectApplication(basicPlatform, config); + + expect(ldApplication).toEqual({ + envAttributesVersion: '1.0', + id: 'Rapper', + key: '1234567890123456', + name: 'Rapper', + version: '9.9.9', + }); + }); + + test('injectDevice with platformData os name, version', async () => { const ldDevice = await injectDevice(basicPlatform); expect(ldDevice).toEqual({ envAttributesVersion: '1.0', - key: 'test-org-key-1', + key: 'test-device-key-1', manufacturer: 'apple', os: { family: 'apple', @@ -58,4 +181,42 @@ describe('injectAutoEnv', () => { }, }); }); + + test('injectDevice with auto env os name, version', async () => { + const platformData = basicPlatform.info.platformData(); + delete platformData.os; + basicPlatform.info.platformData = jest.fn().mockReturnValueOnce(platformData); + + const ldDevice = await injectDevice(basicPlatform); + + expect(ldDevice).toEqual({ + envAttributesVersion: '1.0', + key: 'test-device-key-1', + manufacturer: 'apple', + os: { + family: 'apple', + name: 'ios', + version: '17', + }, + }); + }); + + test('injectDevice with auto env os name, version when platform data are empty strings', async () => { + const platformData = basicPlatform.info.platformData(); + platformData.os = { name: '', version: '' }; + basicPlatform.info.platformData = jest.fn().mockReturnValueOnce(platformData); + + const ldDevice = await injectDevice(basicPlatform); + + expect(ldDevice).toEqual({ + envAttributesVersion: '1.0', + key: 'test-device-key-1', + manufacturer: 'apple', + os: { + family: 'apple', + name: 'ios', + version: '17', + }, + }); + }); }); diff --git a/packages/shared/sdk-client/src/utils/injectAutoEnv.ts b/packages/shared/sdk-client/src/utils/injectAutoEnv.ts index 6ce4bdebf..9f952c474 100644 --- a/packages/shared/sdk-client/src/utils/injectAutoEnv.ts +++ b/packages/shared/sdk-client/src/utils/injectAutoEnv.ts @@ -15,6 +15,7 @@ import Configuration from '../configuration'; import { getOrGenerateKey } from './getOrGenerateKey'; const { isLegacyUser, isSingleKind } = internal; +const defaultAutoEnvSchemaVersion = '1.0'; export const toMulti = (c: LDSingleKindContext) => { const { kind, ...contextCommon } = c; @@ -37,14 +38,17 @@ export const injectApplication = ({ crypto, info }: Platform, config: Configurat const { name, version, wrapperName, wrapperVersion } = info.sdkData(); const { ld_application } = info.platformData(); - const ldApplication = clone(ld_application); - ldApplication.id = config.application?.id || ldApplication.id || name || wrapperName; + const ldApplication = clone(ld_application) ?? {}; + ldApplication.id = config.application?.id || ldApplication?.id || name || wrapperName; + ldApplication.name = ldApplication?.name || name || wrapperName; ldApplication.version = config.application?.version || ldApplication.version || version || wrapperVersion; const hasher = crypto.createHash('sha256'); hasher.update(ldApplication.id!); ldApplication.key = hasher.digest('base64'); + ldApplication.envAttributesVersion = + ldApplication.envAttributesVersion || defaultAutoEnvSchemaVersion; return ldApplication; }; @@ -59,10 +63,10 @@ export const injectDevice = async (platform: Platform) => { const { ld_device, os } = platform.info.platformData(); const ldDevice = clone(ld_device); - // TODO: check for empty string ldDevice.os.name = os?.name || ldDevice.os.name; ldDevice.os.version = os?.version || ldDevice.os.version; ldDevice.key = await getOrGenerateKey('ld_device', platform); + ldDevice.envAttributesVersion = ldDevice.envAttributesVersion || defaultAutoEnvSchemaVersion; return ldDevice; }; @@ -77,10 +81,7 @@ export const injectAutoEnv = async ( return context as LDUser; } - let multi; - if (isSingleKind(context)) { - multi = toMulti(context); - } + const multi = isSingleKind(context) ? toMulti(context) : context; return { ...multi, From ac337068a71f71c627dc0de83e07274c06c29e7f Mon Sep 17 00:00:00 2001 From: Yusinto Ngadiman Date: Mon, 22 Jan 2024 17:27:05 -0800 Subject: [PATCH 30/44] chore: replace the word inject with add to avoid confusion with DI. --- .../shared/sdk-client/src/LDClientImpl.ts | 4 +- ...jectAutoEnv.test.ts => addAutoEnv.test.ts} | 58 +++++++++---------- .../utils/{injectAutoEnv.ts => addAutoEnv.ts} | 14 ++--- packages/shared/sdk-client/src/utils/index.ts | 4 +- 4 files changed, 38 insertions(+), 42 deletions(-) rename packages/shared/sdk-client/src/utils/{injectAutoEnv.test.ts => addAutoEnv.test.ts} (70%) rename packages/shared/sdk-client/src/utils/{injectAutoEnv.ts => addAutoEnv.ts} (86%) diff --git a/packages/shared/sdk-client/src/LDClientImpl.ts b/packages/shared/sdk-client/src/LDClientImpl.ts index a694579ca..3c06ac079 100644 --- a/packages/shared/sdk-client/src/LDClientImpl.ts +++ b/packages/shared/sdk-client/src/LDClientImpl.ts @@ -24,7 +24,7 @@ import createDiagnosticsManager from './diagnostics/createDiagnosticsManager'; import createEventProcessor from './events/createEventProcessor'; import EventFactory from './events/EventFactory'; import { DeleteFlag, Flags, PatchFlag } from './types'; -import { calculateFlagChanges, ensureKey, injectAutoEnv } from './utils'; +import { addAutoEnv, calculateFlagChanges, ensureKey } from './utils'; const { createErrorEvaluationDetail, createSuccessEvaluationDetail, ClientMessages, ErrorKinds } = internal; @@ -235,7 +235,7 @@ export default class LDClientImpl implements LDClient { let context = await ensureKey(pristineContext, this.platform); if (this.config.autoEnvAttributes) { - context = await injectAutoEnv(context, this.platform, this.config); + context = await addAutoEnv(context, this.platform, this.config); } const checkedContext = Context.fromLDContext(context); diff --git a/packages/shared/sdk-client/src/utils/injectAutoEnv.test.ts b/packages/shared/sdk-client/src/utils/addAutoEnv.test.ts similarity index 70% rename from packages/shared/sdk-client/src/utils/injectAutoEnv.test.ts rename to packages/shared/sdk-client/src/utils/addAutoEnv.test.ts index 95b18a4a5..23393737e 100644 --- a/packages/shared/sdk-client/src/utils/injectAutoEnv.test.ts +++ b/packages/shared/sdk-client/src/utils/addAutoEnv.test.ts @@ -2,9 +2,9 @@ import { Info, type LDContext, LDUser } from '@launchdarkly/js-sdk-common'; import { basicPlatform } from '@launchdarkly/private-js-mocks'; import Configuration from '../configuration'; -import { injectApplication, injectAutoEnv, injectDevice, toMulti } from './injectAutoEnv'; +import { addApplicationInfo, addAutoEnv, addDeviceInfo, toMulti } from './addAutoEnv'; -describe('injectAutoEnv', () => { +describe('addAutoEnv', () => { let crypto: Crypto; let info: Info; @@ -30,7 +30,7 @@ describe('injectAutoEnv', () => { const config = new Configuration(); // const context = { kind: 'user', key: 'test-user-key-1', name: 'bob' }; const user: LDUser = { key: 'legacy-user-key', name: 'bob' }; - const result = await injectAutoEnv(user, basicPlatform, config); + const result = await addAutoEnv(user, basicPlatform, config); expect(result).toEqual(user); }); @@ -39,7 +39,7 @@ describe('injectAutoEnv', () => { const config = new Configuration(); const context = { kind: 'user', key: 'test-user-key-1', name: 'bob' }; - const result = await injectAutoEnv(context, basicPlatform, config); + const result = await addAutoEnv(context, basicPlatform, config); expect(result).toEqual({ kind: 'multi', @@ -67,7 +67,7 @@ describe('injectAutoEnv', () => { user: { key: 'test-user-key-1', name: 'bob' }, org: { key: 'test-org-key-1', name: 'Best company' }, }; - const result = await injectAutoEnv(context, basicPlatform, config); + const result = await addAutoEnv(context, basicPlatform, config); expect(result).toEqual({ kind: 'multi', @@ -89,11 +89,11 @@ describe('injectAutoEnv', () => { }); }); - test('injectApplication with config application id, version', () => { + test('addApplicationInfo with config application id, version', () => { const config = new Configuration({ application: { id: 'com.from-config.ld', version: '2.2.2' }, }); - const ldApplication = injectApplication(basicPlatform, config); + const ldApplication = addApplicationInfo(basicPlatform, config); expect(ldApplication).toEqual({ envAttributesVersion: '1.0', @@ -104,9 +104,9 @@ describe('injectAutoEnv', () => { }); }); - test('injectApplication with auto env application id, name, version', () => { + test('addApplicationInfo with auto env application id, name, version', () => { const config = new Configuration(); - const ldApplication = injectApplication(basicPlatform, config); + const ldApplication = addApplicationInfo(basicPlatform, config); expect(ldApplication).toEqual({ envAttributesVersion: '1.0', @@ -117,12 +117,12 @@ describe('injectAutoEnv', () => { }); }); - test('injectApplication with sdk data name, version', () => { - const platformData = basicPlatform.info.platformData(); + test('addApplicationInfo with sdk data name, version', () => { + const platformData = info.platformData(); delete platformData.ld_application; delete platformData.ld_device; - basicPlatform.info.platformData = jest.fn().mockReturnValueOnce(platformData); - basicPlatform.info.sdkData = jest.fn().mockReturnValueOnce({ + info.platformData = jest.fn().mockReturnValueOnce(platformData); + info.sdkData = jest.fn().mockReturnValueOnce({ name: 'Name from sdk data', version: '3.3.3', userAgentBase: 'TestUserAgent', @@ -131,7 +131,7 @@ describe('injectAutoEnv', () => { }); const config = new Configuration(); - const ldApplication = injectApplication(basicPlatform, config); + const ldApplication = addApplicationInfo(basicPlatform, config); expect(ldApplication).toEqual({ envAttributesVersion: '1.0', @@ -142,12 +142,12 @@ describe('injectAutoEnv', () => { }); }); - test('injectApplication with sdkData wrapperName, wrapperVersion', () => { - const platformData = basicPlatform.info.platformData(); + test('addApplicationInfo with sdkData wrapperName, wrapperVersion', () => { + const platformData = info.platformData(); delete platformData.ld_application; delete platformData.ld_device; - basicPlatform.info.platformData = jest.fn().mockReturnValueOnce(platformData); - basicPlatform.info.sdkData = jest.fn().mockReturnValueOnce({ + info.platformData = jest.fn().mockReturnValueOnce(platformData); + info.sdkData = jest.fn().mockReturnValueOnce({ name: '', version: '', userAgentBase: 'TestUserAgent', @@ -156,7 +156,7 @@ describe('injectAutoEnv', () => { }); const config = new Configuration(); - const ldApplication = injectApplication(basicPlatform, config); + const ldApplication = addApplicationInfo(basicPlatform, config); expect(ldApplication).toEqual({ envAttributesVersion: '1.0', @@ -167,8 +167,8 @@ describe('injectAutoEnv', () => { }); }); - test('injectDevice with platformData os name, version', async () => { - const ldDevice = await injectDevice(basicPlatform); + test('addDeviceInfo with platformData os name, version', async () => { + const ldDevice = await addDeviceInfo(basicPlatform); expect(ldDevice).toEqual({ envAttributesVersion: '1.0', @@ -182,12 +182,12 @@ describe('injectAutoEnv', () => { }); }); - test('injectDevice with auto env os name, version', async () => { - const platformData = basicPlatform.info.platformData(); + test('addDeviceInfo with auto env os name, version', async () => { + const platformData = info.platformData(); delete platformData.os; - basicPlatform.info.platformData = jest.fn().mockReturnValueOnce(platformData); + info.platformData = jest.fn().mockReturnValueOnce(platformData); - const ldDevice = await injectDevice(basicPlatform); + const ldDevice = await addDeviceInfo(basicPlatform); expect(ldDevice).toEqual({ envAttributesVersion: '1.0', @@ -201,12 +201,12 @@ describe('injectAutoEnv', () => { }); }); - test('injectDevice with auto env os name, version when platform data are empty strings', async () => { - const platformData = basicPlatform.info.platformData(); + test('addDeviceInfo with auto env os name, version when platform data are empty strings', async () => { + const platformData = info.platformData(); platformData.os = { name: '', version: '' }; - basicPlatform.info.platformData = jest.fn().mockReturnValueOnce(platformData); + info.platformData = jest.fn().mockReturnValueOnce(platformData); - const ldDevice = await injectDevice(basicPlatform); + const ldDevice = await addDeviceInfo(basicPlatform); expect(ldDevice).toEqual({ envAttributesVersion: '1.0', diff --git a/packages/shared/sdk-client/src/utils/injectAutoEnv.ts b/packages/shared/sdk-client/src/utils/addAutoEnv.ts similarity index 86% rename from packages/shared/sdk-client/src/utils/injectAutoEnv.ts rename to packages/shared/sdk-client/src/utils/addAutoEnv.ts index 9f952c474..65d1e128b 100644 --- a/packages/shared/sdk-client/src/utils/injectAutoEnv.ts +++ b/packages/shared/sdk-client/src/utils/addAutoEnv.ts @@ -34,7 +34,7 @@ export const toMulti = (c: LDSingleKindContext) => { * @param config * @return An LDApplication object with populated key, id and version. */ -export const injectApplication = ({ crypto, info }: Platform, config: Configuration) => { +export const addApplicationInfo = ({ crypto, info }: Platform, config: Configuration) => { const { name, version, wrapperName, wrapperVersion } = info.sdkData(); const { ld_application } = info.platformData(); @@ -59,7 +59,7 @@ export const injectApplication = ({ crypto, info }: Platform, config: Configurat * @param platform * @return An LDDevice object with populated key. */ -export const injectDevice = async (platform: Platform) => { +export const addDeviceInfo = async (platform: Platform) => { const { ld_device, os } = platform.info.platformData(); const ldDevice = clone(ld_device); @@ -71,11 +71,7 @@ export const injectDevice = async (platform: Platform) => { return ldDevice; }; -export const injectAutoEnv = async ( - context: LDContext, - platform: Platform, - config: Configuration, -) => { +export const addAutoEnv = async (context: LDContext, platform: Platform, config: Configuration) => { // LDUser is not supported for auto env reporting if (isLegacyUser(context)) { return context as LDUser; @@ -85,7 +81,7 @@ export const injectAutoEnv = async ( return { ...multi, - ld_application: injectApplication(platform, config), - ld_device: await injectDevice(platform), + ld_application: addApplicationInfo(platform, config), + ld_device: await addDeviceInfo(platform), } as LDMultiKindContext; }; diff --git a/packages/shared/sdk-client/src/utils/index.ts b/packages/shared/sdk-client/src/utils/index.ts index 9f0614ad0..c16f6e1b1 100644 --- a/packages/shared/sdk-client/src/utils/index.ts +++ b/packages/shared/sdk-client/src/utils/index.ts @@ -1,5 +1,5 @@ +import { addAutoEnv } from './addAutoEnv'; import calculateFlagChanges from './calculateFlagChanges'; import ensureKey from './ensureKey'; -import { injectAutoEnv } from './injectAutoEnv'; -export { calculateFlagChanges, ensureKey, injectAutoEnv }; +export { calculateFlagChanges, ensureKey, addAutoEnv }; From b9bd289a91b9503a8dc9e3a1b81d45ff74cc268d Mon Sep 17 00:00:00 2001 From: Yusinto Ngadiman Date: Mon, 22 Jan 2024 17:36:48 -0800 Subject: [PATCH 31/44] chore: More auto env tests. --- .../src/LDClientImpl.storage.test.ts | 47 +++++++++++++++++++ .../sdk-client/src/LDClientImpl.test.ts | 19 ++++++++ 2 files changed, 66 insertions(+) diff --git a/packages/shared/sdk-client/src/LDClientImpl.storage.test.ts b/packages/shared/sdk-client/src/LDClientImpl.storage.test.ts index ad151dd29..563db1cad 100644 --- a/packages/shared/sdk-client/src/LDClientImpl.storage.test.ts +++ b/packages/shared/sdk-client/src/LDClientImpl.storage.test.ts @@ -5,6 +5,7 @@ import LDEmitter from './api/LDEmitter'; import * as mockResponseJson from './evaluation/mockResponse.json'; import LDClientImpl from './LDClientImpl'; import { DeleteFlag, Flag, Flags, PatchFlag } from './types'; +import { toMulti } from './utils/addAutoEnv'; jest.mock('@launchdarkly/js-sdk-common', () => { const actual = jest.requireActual('@launchdarkly/js-sdk-common'); @@ -119,6 +120,52 @@ describe('sdk-client storage', () => { }); }); + test('initialize from storage succeeds with auto env', async () => { + ldc = new LDClientImpl(testSdkKey, basicPlatform, { + logger, + sendEvents: false, + }); + // @ts-ignore + emitter = ldc.emitter; + jest.spyOn(emitter as LDEmitter, 'emit'); + + const allFlags = await identifyGetAllFlags(true, defaultPutResponse); + + expect(basicPlatform.storage.get).toHaveBeenLastCalledWith( + expect.stringMatching(/org:Testy Pizza$/), + ); + + // 'change' should not have been emitted + expect(emitter.emit).toHaveBeenCalledTimes(3); + expect(emitter.emit).toHaveBeenNthCalledWith( + 1, + 'identifying', + expect.objectContaining(toMulti(context)), + ); + expect(emitter.emit).toHaveBeenNthCalledWith( + 2, + 'change', + expect.objectContaining(toMulti(context)), + defaultFlagKeys, + ); + expect(emitter.emit).toHaveBeenNthCalledWith( + 3, + 'error', + expect.objectContaining(toMulti(context)), + expect.objectContaining({ message: 'test-error' }), + ); + expect(allFlags).toEqual({ + 'dev-test-flag': true, + 'easter-i-tunes-special': false, + 'easter-specials': 'no specials', + fdsafdsafdsafdsa: true, + 'log-level': 'warn', + 'moonshot-demo': true, + test1: 's1', + 'this-is-a-test': true, + }); + }); + test('no storage, cold start from streamer', async () => { // fake previously cached flags even though there's no storage for this context // @ts-ignore diff --git a/packages/shared/sdk-client/src/LDClientImpl.test.ts b/packages/shared/sdk-client/src/LDClientImpl.test.ts index d0fe98c55..4097497ef 100644 --- a/packages/shared/sdk-client/src/LDClientImpl.test.ts +++ b/packages/shared/sdk-client/src/LDClientImpl.test.ts @@ -159,6 +159,25 @@ describe('sdk-client object', () => { }); }); + test('identify success without auto env', async () => { + defaultPutResponse['dev-test-flag'].value = false; + const carContext: LDContext = { kind: 'car', key: 'mazda-cx7' }; + ldc = new LDClientImpl(testSdkKey, basicPlatform, { + autoEnvAttributes: false, + logger, + sendEvents: false, + }); + + await ldc.identify(carContext); + const c = ldc.getContext(); + const all = ldc.allFlags(); + + expect(c).toEqual(carContext); + expect(all).toMatchObject({ + 'dev-test-flag': false, + }); + }); + test('identify anonymous', async () => { defaultPutResponse['dev-test-flag'].value = false; const carContext: LDContext = { kind: 'car', anonymous: true, key: '' }; From 1a7163718754730b782408636e382d2131c6b2a1 Mon Sep 17 00:00:00 2001 From: Yusinto Ngadiman Date: Tue, 23 Jan 2024 13:47:31 -0800 Subject: [PATCH 32/44] chore: improve platform.crypto error messages. --- packages/sdk/react-native/src/platform/index.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/sdk/react-native/src/platform/index.ts b/packages/sdk/react-native/src/platform/index.ts index 2320da907..47aa888ce 100644 --- a/packages/sdk/react-native/src/platform/index.ts +++ b/packages/sdk/react-native/src/platform/index.ts @@ -71,11 +71,11 @@ class PlatformInfo implements Info { class PlatformCrypto implements Crypto { createHash(_algorithm: string): Hasher { - throw new Error('not implemented'); + throw new Error('createHash not implemented'); } createHmac(_algorithm: string, _key: string): Hmac { - throw new Error('not implemented'); + throw new Error('createHmac not implemented'); } randomUUID(): string { From 5a78540494701f4ebe30e873b2c3cc0bef8540b8 Mon Sep 17 00:00:00 2001 From: Yusinto Ngadiman Date: Wed, 24 Jan 2024 10:29:00 -0800 Subject: [PATCH 33/44] chore: Added third party sha library. Implemented rn crypto. Created new dir for external libs. --- .eslintrc.js | 2 +- .../src/fromExternal/js-sha256/LICENSE | 22 + .../src/fromExternal/js-sha256/index.d.ts | 149 ++++ .../src/fromExternal/js-sha256/index.js | 667 ++++++++++++++++++ .../react-native-sse/EventSource.ts | 0 .../react-native-sse/LICENSE | 0 .../react-native-sse/index.ts | 0 .../react-native-sse/types.ts | 0 .../src/platform/crypto/PlatformHasher.ts | 33 + .../react-native/src/platform/crypto/index.ts | 40 ++ .../react-native/src/platform/crypto/types.ts | 2 + .../sdk/react-native/src/platform/index.ts | 22 +- .../sdk/react-native/src/polyfills/btoa.ts | 6 +- .../sdk/react-native/src/polyfills/index.ts | 6 +- .../sdk/react-native/src/polyfills/uuid.ts | 16 - 15 files changed, 925 insertions(+), 40 deletions(-) create mode 100644 packages/sdk/react-native/src/fromExternal/js-sha256/LICENSE create mode 100644 packages/sdk/react-native/src/fromExternal/js-sha256/index.d.ts create mode 100644 packages/sdk/react-native/src/fromExternal/js-sha256/index.js rename packages/sdk/react-native/src/{ => fromExternal}/react-native-sse/EventSource.ts (100%) rename packages/sdk/react-native/src/{ => fromExternal}/react-native-sse/LICENSE (100%) rename packages/sdk/react-native/src/{ => fromExternal}/react-native-sse/index.ts (100%) rename packages/sdk/react-native/src/{ => fromExternal}/react-native-sse/types.ts (100%) create mode 100644 packages/sdk/react-native/src/platform/crypto/PlatformHasher.ts create mode 100644 packages/sdk/react-native/src/platform/crypto/index.ts create mode 100644 packages/sdk/react-native/src/platform/crypto/types.ts delete mode 100644 packages/sdk/react-native/src/polyfills/uuid.ts diff --git a/.eslintrc.js b/.eslintrc.js index 4a0af77b6..dbc1e1a4d 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -8,7 +8,7 @@ module.exports = { project: './tsconfig.eslint.json', }, plugins: ['@typescript-eslint', 'prettier'], - ignorePatterns: ['**/dist/**', '**/vercel/examples/**'], + ignorePatterns: ['**/dist/**', '**/vercel/examples/**', '**/fromExternal/**'], rules: { '@typescript-eslint/lines-between-class-members': 'off', '@typescript-eslint/no-unused-vars': [ diff --git a/packages/sdk/react-native/src/fromExternal/js-sha256/LICENSE b/packages/sdk/react-native/src/fromExternal/js-sha256/LICENSE new file mode 100644 index 000000000..d06f6d49a --- /dev/null +++ b/packages/sdk/react-native/src/fromExternal/js-sha256/LICENSE @@ -0,0 +1,22 @@ +Copyright (c) 2014-2023 Chen, Yi-Cyuan + +MIT License + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/packages/sdk/react-native/src/fromExternal/js-sha256/index.d.ts b/packages/sdk/react-native/src/fromExternal/js-sha256/index.d.ts new file mode 100644 index 000000000..1ac455ecb --- /dev/null +++ b/packages/sdk/react-native/src/fromExternal/js-sha256/index.d.ts @@ -0,0 +1,149 @@ +type Message = string | number[] | ArrayBuffer | Uint8Array; + +interface Hasher { + /** + * Update hash + * + * @param message The message you want to hash. + */ + update(message: Message): Hasher; + + /** + * Return hash in hex string. + */ + hex(): string; + + /** + * Return hash in hex string. + */ + toString(): string; + + /** + * Return hash in ArrayBuffer. + */ + arrayBuffer(): ArrayBuffer; + + /** + * Return hash in integer array. + */ + digest(): number[]; + + /** + * Return hash in integer array. + */ + array(): number[]; +} + +interface Hmac { + /** + * Computes a Hash-based message authentication code (HMAC) using a secret key + * + * @param secretKey The Secret Key + * @param message The message you want to hash. + */ + (secretKey: Message, message: Message): string; + + /** + * Create a hash object using a secret key. + * + * @param secretKey The Secret Key + */ + create(secretKey: Message): Hasher; + + /** + * Create a hash object and hash message using a secret key + * + * @param secretKey The Secret Key + * @param message The message you want to hash. + */ + update(secretKey: Message, message: Message): Hasher; + + /** + * Return hash in hex string. + * + * @param secretKey The Secret Key + * @param message The message you want to hash. + */ + hex(secretKey: Message, message: Message): string; + + /** + * Return hash in ArrayBuffer. + * + * @param secretKey The Secret Key + * @param message The message you want to hash. + */ + arrayBuffer(secretKey: Message, message: Message): ArrayBuffer; + + /** + * Return hash in integer array. + * + * @param secretKey The Secret Key + * @param message The message you want to hash. + */ + digest(secretKey: Message, message: Message): number[]; + + /** + * Return hash in integer array. + * + * @param secretKey The Secret Key + * @param message The message you want to hash. + */ + array(secretKey: Message, message: Message): number[]; +} + +interface Hash { + /** + * Hash and return hex string. + * + * @param message The message you want to hash. + */ + (message: Message): string; + + /** + * Create a hash object. + */ + create(): Hasher; + + /** + * Create a hash object and hash message. + * + * @param message The message you want to hash. + */ + update(message: Message): Hasher; + + /** + * Return hash in hex string. + * + * @param message The message you want to hash. + */ + hex(message: Message): string; + + /** + * Return hash in ArrayBuffer. + * + * @param message The message you want to hash. + */ + arrayBuffer(message: Message): ArrayBuffer; + + /** + * Return hash in integer array. + * + * @param message The message you want to hash. + */ + digest(message: Message): number[]; + + /** + * Return hash in integer array. + * + * @param message The message you want to hash. + */ + array(message: Message): number[]; + + /** + * HMAC interface + */ + hmac: Hmac; +} + +export var sha256: Hash; +export var sha224: Hash; diff --git a/packages/sdk/react-native/src/fromExternal/js-sha256/index.js b/packages/sdk/react-native/src/fromExternal/js-sha256/index.js new file mode 100644 index 000000000..5fdaa08ed --- /dev/null +++ b/packages/sdk/react-native/src/fromExternal/js-sha256/index.js @@ -0,0 +1,667 @@ +/** + * [js-sha256]{@link https://github.com/emn178/js-sha256} + * + * @version 0.10.1 + * @author Chen, Yi-Cyuan [emn178@gmail.com] + * @copyright Chen, Yi-Cyuan 2014-2023 + * @license MIT + */ +/*jslint bitwise: true */ +(function () { + 'use strict'; + + var ERROR = 'input is invalid type'; + var WINDOW = typeof window === 'object'; + var root = WINDOW ? window : {}; + if (root.JS_SHA256_NO_WINDOW) { + WINDOW = false; + } + var WEB_WORKER = !WINDOW && typeof self === 'object'; + var NODE_JS = + !root.JS_SHA256_NO_NODE_JS && + typeof process === 'object' && + process.versions && + process.versions.node; + if (NODE_JS) { + root = global; + } else if (WEB_WORKER) { + root = self; + } + var COMMON_JS = !root.JS_SHA256_NO_COMMON_JS && typeof module === 'object' && module.exports; + var AMD = typeof define === 'function' && define.amd; + var ARRAY_BUFFER = !root.JS_SHA256_NO_ARRAY_BUFFER && typeof ArrayBuffer !== 'undefined'; + var HEX_CHARS = '0123456789abcdef'.split(''); + var EXTRA = [-2147483648, 8388608, 32768, 128]; + var SHIFT = [24, 16, 8, 0]; + var K = [ + 0x428a2f98, 0x71374491, 0xb5c0fbcf, 0xe9b5dba5, 0x3956c25b, 0x59f111f1, 0x923f82a4, 0xab1c5ed5, + 0xd807aa98, 0x12835b01, 0x243185be, 0x550c7dc3, 0x72be5d74, 0x80deb1fe, 0x9bdc06a7, 0xc19bf174, + 0xe49b69c1, 0xefbe4786, 0x0fc19dc6, 0x240ca1cc, 0x2de92c6f, 0x4a7484aa, 0x5cb0a9dc, 0x76f988da, + 0x983e5152, 0xa831c66d, 0xb00327c8, 0xbf597fc7, 0xc6e00bf3, 0xd5a79147, 0x06ca6351, 0x14292967, + 0x27b70a85, 0x2e1b2138, 0x4d2c6dfc, 0x53380d13, 0x650a7354, 0x766a0abb, 0x81c2c92e, 0x92722c85, + 0xa2bfe8a1, 0xa81a664b, 0xc24b8b70, 0xc76c51a3, 0xd192e819, 0xd6990624, 0xf40e3585, 0x106aa070, + 0x19a4c116, 0x1e376c08, 0x2748774c, 0x34b0bcb5, 0x391c0cb3, 0x4ed8aa4a, 0x5b9cca4f, 0x682e6ff3, + 0x748f82ee, 0x78a5636f, 0x84c87814, 0x8cc70208, 0x90befffa, 0xa4506ceb, 0xbef9a3f7, 0xc67178f2, + ]; + var OUTPUT_TYPES = ['hex', 'array', 'digest', 'arrayBuffer']; + + var blocks = []; + + if (root.JS_SHA256_NO_NODE_JS || !Array.isArray) { + Array.isArray = function (obj) { + return Object.prototype.toString.call(obj) === '[object Array]'; + }; + } + + if (ARRAY_BUFFER && (root.JS_SHA256_NO_ARRAY_BUFFER_IS_VIEW || !ArrayBuffer.isView)) { + ArrayBuffer.isView = function (obj) { + return typeof obj === 'object' && obj.buffer && obj.buffer.constructor === ArrayBuffer; + }; + } + + var createOutputMethod = function (outputType, is224) { + return function (message) { + return new Sha256(is224, true).update(message)[outputType](); + }; + }; + + var createMethod = function (is224) { + var method = createOutputMethod('hex', is224); + if (NODE_JS) { + method = nodeWrap(method, is224); + } + method.create = function () { + return new Sha256(is224); + }; + method.update = function (message) { + return method.create().update(message); + }; + for (var i = 0; i < OUTPUT_TYPES.length; ++i) { + var type = OUTPUT_TYPES[i]; + method[type] = createOutputMethod(type, is224); + } + return method; + }; + + var nodeWrap = function (method, is224) { + var crypto = require('crypto'); + var Buffer = require('buffer').Buffer; + var algorithm = is224 ? 'sha224' : 'sha256'; + var bufferFrom; + if (Buffer.from && !root.JS_SHA256_NO_BUFFER_FROM) { + bufferFrom = Buffer.from; + } else { + bufferFrom = function (message) { + return new Buffer(message); + }; + } + var nodeMethod = function (message) { + if (typeof message === 'string') { + return crypto.createHash(algorithm).update(message, 'utf8').digest('hex'); + } else { + if (message === null || message === undefined) { + throw new Error(ERROR); + } else if (message.constructor === ArrayBuffer) { + message = new Uint8Array(message); + } + } + if (Array.isArray(message) || ArrayBuffer.isView(message) || message.constructor === Buffer) { + return crypto.createHash(algorithm).update(bufferFrom(message)).digest('hex'); + } else { + return method(message); + } + }; + return nodeMethod; + }; + + var createHmacOutputMethod = function (outputType, is224) { + return function (key, message) { + return new HmacSha256(key, is224, true).update(message)[outputType](); + }; + }; + + var createHmacMethod = function (is224) { + var method = createHmacOutputMethod('hex', is224); + method.create = function (key) { + return new HmacSha256(key, is224); + }; + method.update = function (key, message) { + return method.create(key).update(message); + }; + for (var i = 0; i < OUTPUT_TYPES.length; ++i) { + var type = OUTPUT_TYPES[i]; + method[type] = createHmacOutputMethod(type, is224); + } + return method; + }; + + function Sha256(is224, sharedMemory) { + if (sharedMemory) { + blocks[0] = + blocks[16] = + blocks[1] = + blocks[2] = + blocks[3] = + blocks[4] = + blocks[5] = + blocks[6] = + blocks[7] = + blocks[8] = + blocks[9] = + blocks[10] = + blocks[11] = + blocks[12] = + blocks[13] = + blocks[14] = + blocks[15] = + 0; + this.blocks = blocks; + } else { + this.blocks = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]; + } + + if (is224) { + this.h0 = 0xc1059ed8; + this.h1 = 0x367cd507; + this.h2 = 0x3070dd17; + this.h3 = 0xf70e5939; + this.h4 = 0xffc00b31; + this.h5 = 0x68581511; + this.h6 = 0x64f98fa7; + this.h7 = 0xbefa4fa4; + } else { + // 256 + this.h0 = 0x6a09e667; + this.h1 = 0xbb67ae85; + this.h2 = 0x3c6ef372; + this.h3 = 0xa54ff53a; + this.h4 = 0x510e527f; + this.h5 = 0x9b05688c; + this.h6 = 0x1f83d9ab; + this.h7 = 0x5be0cd19; + } + + this.block = this.start = this.bytes = this.hBytes = 0; + this.finalized = this.hashed = false; + this.first = true; + this.is224 = is224; + } + + Sha256.prototype.update = function (message) { + if (this.finalized) { + return; + } + var notString, + type = typeof message; + if (type !== 'string') { + if (type === 'object') { + if (message === null) { + throw new Error(ERROR); + } else if (ARRAY_BUFFER && message.constructor === ArrayBuffer) { + message = new Uint8Array(message); + } else if (!Array.isArray(message)) { + if (!ARRAY_BUFFER || !ArrayBuffer.isView(message)) { + throw new Error(ERROR); + } + } + } else { + throw new Error(ERROR); + } + notString = true; + } + var code, + index = 0, + i, + length = message.length, + blocks = this.blocks; + + while (index < length) { + if (this.hashed) { + this.hashed = false; + blocks[0] = this.block; + blocks[16] = + blocks[1] = + blocks[2] = + blocks[3] = + blocks[4] = + blocks[5] = + blocks[6] = + blocks[7] = + blocks[8] = + blocks[9] = + blocks[10] = + blocks[11] = + blocks[12] = + blocks[13] = + blocks[14] = + blocks[15] = + 0; + } + + if (notString) { + for (i = this.start; index < length && i < 64; ++index) { + blocks[i >> 2] |= message[index] << SHIFT[i++ & 3]; + } + } else { + for (i = this.start; index < length && i < 64; ++index) { + code = message.charCodeAt(index); + if (code < 0x80) { + blocks[i >> 2] |= code << SHIFT[i++ & 3]; + } else if (code < 0x800) { + blocks[i >> 2] |= (0xc0 | (code >> 6)) << SHIFT[i++ & 3]; + blocks[i >> 2] |= (0x80 | (code & 0x3f)) << SHIFT[i++ & 3]; + } else if (code < 0xd800 || code >= 0xe000) { + blocks[i >> 2] |= (0xe0 | (code >> 12)) << SHIFT[i++ & 3]; + blocks[i >> 2] |= (0x80 | ((code >> 6) & 0x3f)) << SHIFT[i++ & 3]; + blocks[i >> 2] |= (0x80 | (code & 0x3f)) << SHIFT[i++ & 3]; + } else { + code = 0x10000 + (((code & 0x3ff) << 10) | (message.charCodeAt(++index) & 0x3ff)); + blocks[i >> 2] |= (0xf0 | (code >> 18)) << SHIFT[i++ & 3]; + blocks[i >> 2] |= (0x80 | ((code >> 12) & 0x3f)) << SHIFT[i++ & 3]; + blocks[i >> 2] |= (0x80 | ((code >> 6) & 0x3f)) << SHIFT[i++ & 3]; + blocks[i >> 2] |= (0x80 | (code & 0x3f)) << SHIFT[i++ & 3]; + } + } + } + + this.lastByteIndex = i; + this.bytes += i - this.start; + if (i >= 64) { + this.block = blocks[16]; + this.start = i - 64; + this.hash(); + this.hashed = true; + } else { + this.start = i; + } + } + if (this.bytes > 4294967295) { + this.hBytes += (this.bytes / 4294967296) << 0; + this.bytes = this.bytes % 4294967296; + } + return this; + }; + + Sha256.prototype.finalize = function () { + if (this.finalized) { + return; + } + this.finalized = true; + var blocks = this.blocks, + i = this.lastByteIndex; + blocks[16] = this.block; + blocks[i >> 2] |= EXTRA[i & 3]; + this.block = blocks[16]; + if (i >= 56) { + if (!this.hashed) { + this.hash(); + } + blocks[0] = this.block; + blocks[16] = + blocks[1] = + blocks[2] = + blocks[3] = + blocks[4] = + blocks[5] = + blocks[6] = + blocks[7] = + blocks[8] = + blocks[9] = + blocks[10] = + blocks[11] = + blocks[12] = + blocks[13] = + blocks[14] = + blocks[15] = + 0; + } + blocks[14] = (this.hBytes << 3) | (this.bytes >>> 29); + blocks[15] = this.bytes << 3; + this.hash(); + }; + + Sha256.prototype.hash = function () { + var a = this.h0, + b = this.h1, + c = this.h2, + d = this.h3, + e = this.h4, + f = this.h5, + g = this.h6, + h = this.h7, + blocks = this.blocks, + j, + s0, + s1, + maj, + t1, + t2, + ch, + ab, + da, + cd, + bc; + + for (j = 16; j < 64; ++j) { + // rightrotate + t1 = blocks[j - 15]; + s0 = ((t1 >>> 7) | (t1 << 25)) ^ ((t1 >>> 18) | (t1 << 14)) ^ (t1 >>> 3); + t1 = blocks[j - 2]; + s1 = ((t1 >>> 17) | (t1 << 15)) ^ ((t1 >>> 19) | (t1 << 13)) ^ (t1 >>> 10); + blocks[j] = (blocks[j - 16] + s0 + blocks[j - 7] + s1) << 0; + } + + bc = b & c; + for (j = 0; j < 64; j += 4) { + if (this.first) { + if (this.is224) { + ab = 300032; + t1 = blocks[0] - 1413257819; + h = (t1 - 150054599) << 0; + d = (t1 + 24177077) << 0; + } else { + ab = 704751109; + t1 = blocks[0] - 210244248; + h = (t1 - 1521486534) << 0; + d = (t1 + 143694565) << 0; + } + this.first = false; + } else { + s0 = ((a >>> 2) | (a << 30)) ^ ((a >>> 13) | (a << 19)) ^ ((a >>> 22) | (a << 10)); + s1 = ((e >>> 6) | (e << 26)) ^ ((e >>> 11) | (e << 21)) ^ ((e >>> 25) | (e << 7)); + ab = a & b; + maj = ab ^ (a & c) ^ bc; + ch = (e & f) ^ (~e & g); + t1 = h + s1 + ch + K[j] + blocks[j]; + t2 = s0 + maj; + h = (d + t1) << 0; + d = (t1 + t2) << 0; + } + s0 = ((d >>> 2) | (d << 30)) ^ ((d >>> 13) | (d << 19)) ^ ((d >>> 22) | (d << 10)); + s1 = ((h >>> 6) | (h << 26)) ^ ((h >>> 11) | (h << 21)) ^ ((h >>> 25) | (h << 7)); + da = d & a; + maj = da ^ (d & b) ^ ab; + ch = (h & e) ^ (~h & f); + t1 = g + s1 + ch + K[j + 1] + blocks[j + 1]; + t2 = s0 + maj; + g = (c + t1) << 0; + c = (t1 + t2) << 0; + s0 = ((c >>> 2) | (c << 30)) ^ ((c >>> 13) | (c << 19)) ^ ((c >>> 22) | (c << 10)); + s1 = ((g >>> 6) | (g << 26)) ^ ((g >>> 11) | (g << 21)) ^ ((g >>> 25) | (g << 7)); + cd = c & d; + maj = cd ^ (c & a) ^ da; + ch = (g & h) ^ (~g & e); + t1 = f + s1 + ch + K[j + 2] + blocks[j + 2]; + t2 = s0 + maj; + f = (b + t1) << 0; + b = (t1 + t2) << 0; + s0 = ((b >>> 2) | (b << 30)) ^ ((b >>> 13) | (b << 19)) ^ ((b >>> 22) | (b << 10)); + s1 = ((f >>> 6) | (f << 26)) ^ ((f >>> 11) | (f << 21)) ^ ((f >>> 25) | (f << 7)); + bc = b & c; + maj = bc ^ (b & d) ^ cd; + ch = (f & g) ^ (~f & h); + t1 = e + s1 + ch + K[j + 3] + blocks[j + 3]; + t2 = s0 + maj; + e = (a + t1) << 0; + a = (t1 + t2) << 0; + this.chromeBugWorkAround = true; + } + + this.h0 = (this.h0 + a) << 0; + this.h1 = (this.h1 + b) << 0; + this.h2 = (this.h2 + c) << 0; + this.h3 = (this.h3 + d) << 0; + this.h4 = (this.h4 + e) << 0; + this.h5 = (this.h5 + f) << 0; + this.h6 = (this.h6 + g) << 0; + this.h7 = (this.h7 + h) << 0; + }; + + Sha256.prototype.hex = function () { + this.finalize(); + + var h0 = this.h0, + h1 = this.h1, + h2 = this.h2, + h3 = this.h3, + h4 = this.h4, + h5 = this.h5, + h6 = this.h6, + h7 = this.h7; + + var hex = + HEX_CHARS[(h0 >> 28) & 0x0f] + + HEX_CHARS[(h0 >> 24) & 0x0f] + + HEX_CHARS[(h0 >> 20) & 0x0f] + + HEX_CHARS[(h0 >> 16) & 0x0f] + + HEX_CHARS[(h0 >> 12) & 0x0f] + + HEX_CHARS[(h0 >> 8) & 0x0f] + + HEX_CHARS[(h0 >> 4) & 0x0f] + + HEX_CHARS[h0 & 0x0f] + + HEX_CHARS[(h1 >> 28) & 0x0f] + + HEX_CHARS[(h1 >> 24) & 0x0f] + + HEX_CHARS[(h1 >> 20) & 0x0f] + + HEX_CHARS[(h1 >> 16) & 0x0f] + + HEX_CHARS[(h1 >> 12) & 0x0f] + + HEX_CHARS[(h1 >> 8) & 0x0f] + + HEX_CHARS[(h1 >> 4) & 0x0f] + + HEX_CHARS[h1 & 0x0f] + + HEX_CHARS[(h2 >> 28) & 0x0f] + + HEX_CHARS[(h2 >> 24) & 0x0f] + + HEX_CHARS[(h2 >> 20) & 0x0f] + + HEX_CHARS[(h2 >> 16) & 0x0f] + + HEX_CHARS[(h2 >> 12) & 0x0f] + + HEX_CHARS[(h2 >> 8) & 0x0f] + + HEX_CHARS[(h2 >> 4) & 0x0f] + + HEX_CHARS[h2 & 0x0f] + + HEX_CHARS[(h3 >> 28) & 0x0f] + + HEX_CHARS[(h3 >> 24) & 0x0f] + + HEX_CHARS[(h3 >> 20) & 0x0f] + + HEX_CHARS[(h3 >> 16) & 0x0f] + + HEX_CHARS[(h3 >> 12) & 0x0f] + + HEX_CHARS[(h3 >> 8) & 0x0f] + + HEX_CHARS[(h3 >> 4) & 0x0f] + + HEX_CHARS[h3 & 0x0f] + + HEX_CHARS[(h4 >> 28) & 0x0f] + + HEX_CHARS[(h4 >> 24) & 0x0f] + + HEX_CHARS[(h4 >> 20) & 0x0f] + + HEX_CHARS[(h4 >> 16) & 0x0f] + + HEX_CHARS[(h4 >> 12) & 0x0f] + + HEX_CHARS[(h4 >> 8) & 0x0f] + + HEX_CHARS[(h4 >> 4) & 0x0f] + + HEX_CHARS[h4 & 0x0f] + + HEX_CHARS[(h5 >> 28) & 0x0f] + + HEX_CHARS[(h5 >> 24) & 0x0f] + + HEX_CHARS[(h5 >> 20) & 0x0f] + + HEX_CHARS[(h5 >> 16) & 0x0f] + + HEX_CHARS[(h5 >> 12) & 0x0f] + + HEX_CHARS[(h5 >> 8) & 0x0f] + + HEX_CHARS[(h5 >> 4) & 0x0f] + + HEX_CHARS[h5 & 0x0f] + + HEX_CHARS[(h6 >> 28) & 0x0f] + + HEX_CHARS[(h6 >> 24) & 0x0f] + + HEX_CHARS[(h6 >> 20) & 0x0f] + + HEX_CHARS[(h6 >> 16) & 0x0f] + + HEX_CHARS[(h6 >> 12) & 0x0f] + + HEX_CHARS[(h6 >> 8) & 0x0f] + + HEX_CHARS[(h6 >> 4) & 0x0f] + + HEX_CHARS[h6 & 0x0f]; + if (!this.is224) { + hex += + HEX_CHARS[(h7 >> 28) & 0x0f] + + HEX_CHARS[(h7 >> 24) & 0x0f] + + HEX_CHARS[(h7 >> 20) & 0x0f] + + HEX_CHARS[(h7 >> 16) & 0x0f] + + HEX_CHARS[(h7 >> 12) & 0x0f] + + HEX_CHARS[(h7 >> 8) & 0x0f] + + HEX_CHARS[(h7 >> 4) & 0x0f] + + HEX_CHARS[h7 & 0x0f]; + } + return hex; + }; + + Sha256.prototype.toString = Sha256.prototype.hex; + + Sha256.prototype.digest = function () { + this.finalize(); + + var h0 = this.h0, + h1 = this.h1, + h2 = this.h2, + h3 = this.h3, + h4 = this.h4, + h5 = this.h5, + h6 = this.h6, + h7 = this.h7; + + var arr = [ + (h0 >> 24) & 0xff, + (h0 >> 16) & 0xff, + (h0 >> 8) & 0xff, + h0 & 0xff, + (h1 >> 24) & 0xff, + (h1 >> 16) & 0xff, + (h1 >> 8) & 0xff, + h1 & 0xff, + (h2 >> 24) & 0xff, + (h2 >> 16) & 0xff, + (h2 >> 8) & 0xff, + h2 & 0xff, + (h3 >> 24) & 0xff, + (h3 >> 16) & 0xff, + (h3 >> 8) & 0xff, + h3 & 0xff, + (h4 >> 24) & 0xff, + (h4 >> 16) & 0xff, + (h4 >> 8) & 0xff, + h4 & 0xff, + (h5 >> 24) & 0xff, + (h5 >> 16) & 0xff, + (h5 >> 8) & 0xff, + h5 & 0xff, + (h6 >> 24) & 0xff, + (h6 >> 16) & 0xff, + (h6 >> 8) & 0xff, + h6 & 0xff, + ]; + if (!this.is224) { + arr.push((h7 >> 24) & 0xff, (h7 >> 16) & 0xff, (h7 >> 8) & 0xff, h7 & 0xff); + } + return arr; + }; + + Sha256.prototype.array = Sha256.prototype.digest; + + Sha256.prototype.arrayBuffer = function () { + this.finalize(); + + var buffer = new ArrayBuffer(this.is224 ? 28 : 32); + var dataView = new DataView(buffer); + dataView.setUint32(0, this.h0); + dataView.setUint32(4, this.h1); + dataView.setUint32(8, this.h2); + dataView.setUint32(12, this.h3); + dataView.setUint32(16, this.h4); + dataView.setUint32(20, this.h5); + dataView.setUint32(24, this.h6); + if (!this.is224) { + dataView.setUint32(28, this.h7); + } + return buffer; + }; + + function HmacSha256(key, is224, sharedMemory) { + var i, + type = typeof key; + if (type === 'string') { + var bytes = [], + length = key.length, + index = 0, + code; + for (i = 0; i < length; ++i) { + code = key.charCodeAt(i); + if (code < 0x80) { + bytes[index++] = code; + } else if (code < 0x800) { + bytes[index++] = 0xc0 | (code >> 6); + bytes[index++] = 0x80 | (code & 0x3f); + } else if (code < 0xd800 || code >= 0xe000) { + bytes[index++] = 0xe0 | (code >> 12); + bytes[index++] = 0x80 | ((code >> 6) & 0x3f); + bytes[index++] = 0x80 | (code & 0x3f); + } else { + code = 0x10000 + (((code & 0x3ff) << 10) | (key.charCodeAt(++i) & 0x3ff)); + bytes[index++] = 0xf0 | (code >> 18); + bytes[index++] = 0x80 | ((code >> 12) & 0x3f); + bytes[index++] = 0x80 | ((code >> 6) & 0x3f); + bytes[index++] = 0x80 | (code & 0x3f); + } + } + key = bytes; + } else { + if (type === 'object') { + if (key === null) { + throw new Error(ERROR); + } else if (ARRAY_BUFFER && key.constructor === ArrayBuffer) { + key = new Uint8Array(key); + } else if (!Array.isArray(key)) { + if (!ARRAY_BUFFER || !ArrayBuffer.isView(key)) { + throw new Error(ERROR); + } + } + } else { + throw new Error(ERROR); + } + } + + if (key.length > 64) { + key = new Sha256(is224, true).update(key).array(); + } + + var oKeyPad = [], + iKeyPad = []; + for (i = 0; i < 64; ++i) { + var b = key[i] || 0; + oKeyPad[i] = 0x5c ^ b; + iKeyPad[i] = 0x36 ^ b; + } + + Sha256.call(this, is224, sharedMemory); + + this.update(iKeyPad); + this.oKeyPad = oKeyPad; + this.inner = true; + this.sharedMemory = sharedMemory; + } + HmacSha256.prototype = new Sha256(); + + HmacSha256.prototype.finalize = function () { + Sha256.prototype.finalize.call(this); + if (this.inner) { + this.inner = false; + var innerHash = this.array(); + Sha256.call(this, this.is224, this.sharedMemory); + this.update(this.oKeyPad); + this.update(innerHash); + Sha256.prototype.finalize.call(this); + } + }; + + var exports = createMethod(); + exports.sha256 = exports; + exports.sha224 = createMethod(true); + exports.sha256.hmac = createHmacMethod(); + exports.sha224.hmac = createHmacMethod(true); + + if (COMMON_JS) { + module.exports = exports; + } else { + root.sha256 = exports.sha256; + root.sha224 = exports.sha224; + if (AMD) { + define(function () { + return exports; + }); + } + } +})(); diff --git a/packages/sdk/react-native/src/react-native-sse/EventSource.ts b/packages/sdk/react-native/src/fromExternal/react-native-sse/EventSource.ts similarity index 100% rename from packages/sdk/react-native/src/react-native-sse/EventSource.ts rename to packages/sdk/react-native/src/fromExternal/react-native-sse/EventSource.ts diff --git a/packages/sdk/react-native/src/react-native-sse/LICENSE b/packages/sdk/react-native/src/fromExternal/react-native-sse/LICENSE similarity index 100% rename from packages/sdk/react-native/src/react-native-sse/LICENSE rename to packages/sdk/react-native/src/fromExternal/react-native-sse/LICENSE diff --git a/packages/sdk/react-native/src/react-native-sse/index.ts b/packages/sdk/react-native/src/fromExternal/react-native-sse/index.ts similarity index 100% rename from packages/sdk/react-native/src/react-native-sse/index.ts rename to packages/sdk/react-native/src/fromExternal/react-native-sse/index.ts diff --git a/packages/sdk/react-native/src/react-native-sse/types.ts b/packages/sdk/react-native/src/fromExternal/react-native-sse/types.ts similarity index 100% rename from packages/sdk/react-native/src/react-native-sse/types.ts rename to packages/sdk/react-native/src/fromExternal/react-native-sse/types.ts diff --git a/packages/sdk/react-native/src/platform/crypto/PlatformHasher.ts b/packages/sdk/react-native/src/platform/crypto/PlatformHasher.ts new file mode 100644 index 000000000..6432bca3c --- /dev/null +++ b/packages/sdk/react-native/src/platform/crypto/PlatformHasher.ts @@ -0,0 +1,33 @@ +import { Hasher as LDHasher } from '@launchdarkly/js-client-sdk-common'; + +import { Hasher, sha256 } from '../../fromExternal/js-sha256'; +import { base64FromByteArray } from '../../polyfills'; +import { SupportedHashAlgorithm, SupportedOutputEncoding } from './types'; + +export default class PlatformHasher implements LDHasher { + private hasher: Hasher; + + constructor(algorithm: SupportedHashAlgorithm, hmacKey?: string) { + if (algorithm === 'sha256') { + this.hasher = hmacKey ? sha256.hmac.create(hmacKey) : sha256.create(); + } + + throw new Error('Unsupported hash algorithm. Only sha256 is supported.'); + } + + digest(encoding: SupportedOutputEncoding): string { + switch (encoding) { + case 'base64': + return base64FromByteArray(new Uint8Array(this.hasher.arrayBuffer())); + case 'hex': + return this.hasher.hex(); + default: + throw new Error('unsupported output encoding. Only base64 and hex are supported.'); + } + } + + update(data: string): this { + this.hasher.update(data); + return this; + } +} diff --git a/packages/sdk/react-native/src/platform/crypto/index.ts b/packages/sdk/react-native/src/platform/crypto/index.ts new file mode 100644 index 000000000..a4bfdd4dd --- /dev/null +++ b/packages/sdk/react-native/src/platform/crypto/index.ts @@ -0,0 +1,40 @@ +import type { Crypto, Hmac } from '@launchdarkly/js-client-sdk-common'; + +import PlatformHasher from './PlatformHasher'; +import { SupportedHashAlgorithm } from './types'; + +/* eslint-disable no-bitwise */ +/** + * To avoid dependencies on uuid, this is good enough for now. + * Ripped from the react-native repo: + * https://github.com/facebook/react-native/blob/main/packages/react-native/Libraries/Blob/BlobManager.js#L27 + * + * Based on the rfc4122-compliant solution posted at + * http://stackoverflow.com/questions/105034 + */ +function uuidv4(): string { + return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, (c) => { + const r = (Math.random() * 16) | 0; + const v = c === 'x' ? r : (r & 0x3) | 0x8; + return v.toString(16); + }); +} + +/** + * Uses crypto-js as substitute to node:crypto because the latter + * is not yet supported in some runtimes. + * https://cryptojs.gitbook.io/docs/ + */ +export default class PlatformCrypto implements Crypto { + createHash(algorithm: SupportedHashAlgorithm): PlatformHasher { + return new PlatformHasher(algorithm); + } + + createHmac(algorithm: SupportedHashAlgorithm, key: string): Hmac { + return new PlatformHasher(algorithm, key); + } + + randomUUID(): string { + return uuidv4(); + } +} diff --git a/packages/sdk/react-native/src/platform/crypto/types.ts b/packages/sdk/react-native/src/platform/crypto/types.ts new file mode 100644 index 000000000..446d5f6df --- /dev/null +++ b/packages/sdk/react-native/src/platform/crypto/types.ts @@ -0,0 +1,2 @@ +export type SupportedHashAlgorithm = 'sha256'; +export type SupportedOutputEncoding = 'base64' | 'hex'; diff --git a/packages/sdk/react-native/src/platform/index.ts b/packages/sdk/react-native/src/platform/index.ts index 47aa888ce..6e9e7a81c 100644 --- a/packages/sdk/react-native/src/platform/index.ts +++ b/packages/sdk/react-native/src/platform/index.ts @@ -1,12 +1,9 @@ /* eslint-disable max-classes-per-file */ import type { - Crypto, Encoding, EventName, EventSource, EventSourceInitDict, - Hasher, - Hmac, Info, LDLogger, Options, @@ -19,10 +16,11 @@ import type { } from '@launchdarkly/js-client-sdk-common'; import { name, version } from '../../package.json'; -import { btoa, uuidv4 } from '../polyfills'; -import RNEventSource from '../react-native-sse'; +import RNEventSource from '../fromExternal/react-native-sse'; +import { btoa } from '../polyfills'; import { ldApplication, ldDevice } from './autoEnv'; import AsyncStorage from './ConditionalAsyncStorage'; +import PlatformCrypto from './crypto'; class PlatformRequests implements Requests { createEventSource(url: string, eventSourceInitDict: EventSourceInitDict): EventSource { @@ -69,20 +67,6 @@ class PlatformInfo implements Info { } } -class PlatformCrypto implements Crypto { - createHash(_algorithm: string): Hasher { - throw new Error('createHash not implemented'); - } - - createHmac(_algorithm: string, _key: string): Hmac { - throw new Error('createHmac not implemented'); - } - - randomUUID(): string { - return uuidv4(); - } -} - class PlatformStorage implements Storage { constructor(private readonly logger: LDLogger) {} async clear(key: string): Promise { diff --git a/packages/sdk/react-native/src/polyfills/btoa.ts b/packages/sdk/react-native/src/polyfills/btoa.ts index 944ce41c9..d9b29bfac 100644 --- a/packages/sdk/react-native/src/polyfills/btoa.ts +++ b/packages/sdk/react-native/src/polyfills/btoa.ts @@ -8,6 +8,10 @@ function convertToByteArray(s: string) { return Uint8Array.from(b); } -export default function btoa(s: string) { +export function btoa(s: string) { return fromByteArray(convertToByteArray(s)); } + +export function base64FromByteArray(a: Uint8Array) { + return fromByteArray(a); +} diff --git a/packages/sdk/react-native/src/polyfills/index.ts b/packages/sdk/react-native/src/polyfills/index.ts index bf2bac134..03e1b11fe 100644 --- a/packages/sdk/react-native/src/polyfills/index.ts +++ b/packages/sdk/react-native/src/polyfills/index.ts @@ -1,8 +1,8 @@ import EventTarget from 'event-target-shim'; -import btoa from './btoa'; +import { type Hasher, sha256 } from '../fromExternal/js-sha256'; +import { base64FromByteArray, btoa } from './btoa'; import CustomEvent from './CustomEvent'; -import uuidv4 from './uuid'; function setupPolyfill() { Object.assign(global, { @@ -10,4 +10,4 @@ function setupPolyfill() { CustomEvent, }); } -export { btoa, setupPolyfill, uuidv4 }; +export { base64FromByteArray, btoa, type Hasher, setupPolyfill, sha256 }; diff --git a/packages/sdk/react-native/src/polyfills/uuid.ts b/packages/sdk/react-native/src/polyfills/uuid.ts deleted file mode 100644 index 142a322bc..000000000 --- a/packages/sdk/react-native/src/polyfills/uuid.ts +++ /dev/null @@ -1,16 +0,0 @@ -/* eslint-disable no-bitwise */ -/** - * To avoid dependencies on uuid, this is good enough for now. - * Ripped from the react-native repo: - * https://github.com/facebook/react-native/blob/main/packages/react-native/Libraries/Blob/BlobManager.js#L27 - * - * Based on the rfc4122-compliant solution posted at - * http://stackoverflow.com/questions/105034 - */ -export default function uuidv4(): string { - return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, (c) => { - const r = (Math.random() * 16) | 0; - const v = c === 'x' ? r : (r & 0x3) | 0x8; - return v.toString(16); - }); -} From 8a9298f70b154db3369717fc7d304ce428782ce5 Mon Sep 17 00:00:00 2001 From: Yusinto Ngadiman Date: Wed, 24 Jan 2024 10:46:14 -0800 Subject: [PATCH 34/44] chore: fix typescript build to ignore jest files and include js fromExternal. --- packages/sdk/react-native/tsconfig.json | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/packages/sdk/react-native/tsconfig.json b/packages/sdk/react-native/tsconfig.json index a79d5ca77..275a512e7 100644 --- a/packages/sdk/react-native/tsconfig.json +++ b/packages/sdk/react-native/tsconfig.json @@ -18,7 +18,16 @@ "strict": true, "stripInternal": true, "target": "ES2017", - "types": ["node", "jest"] + "types": ["node", "jest"], + "allowJs": true }, - "exclude": ["**/*.test.ts*", "dist", "node_modules", "__tests__", "example"] + "exclude": [ + "jest.config.ts", + "jestSetupFile.ts", + "**/*.test.ts*", + "dist", + "node_modules", + "__tests__", + "example" + ] } From cf1b4c8a55027cdf84843a03b013ce5ce1091489 Mon Sep 17 00:00:00 2001 From: Yusinto Ngadiman Date: Wed, 24 Jan 2024 10:47:13 -0800 Subject: [PATCH 35/44] fix: PlatformHasher incorrectly throws error after construction. Removed all node references in external hasher. --- .../src/fromExternal/js-sha256/index.js | 34 ------------------- .../src/platform/crypto/PlatformHasher.ts | 12 ++++--- 2 files changed, 8 insertions(+), 38 deletions(-) diff --git a/packages/sdk/react-native/src/fromExternal/js-sha256/index.js b/packages/sdk/react-native/src/fromExternal/js-sha256/index.js index 5fdaa08ed..6edd54555 100644 --- a/packages/sdk/react-native/src/fromExternal/js-sha256/index.js +++ b/packages/sdk/react-native/src/fromExternal/js-sha256/index.js @@ -67,9 +67,6 @@ var createMethod = function (is224) { var method = createOutputMethod('hex', is224); - if (NODE_JS) { - method = nodeWrap(method, is224); - } method.create = function () { return new Sha256(is224); }; @@ -83,37 +80,6 @@ return method; }; - var nodeWrap = function (method, is224) { - var crypto = require('crypto'); - var Buffer = require('buffer').Buffer; - var algorithm = is224 ? 'sha224' : 'sha256'; - var bufferFrom; - if (Buffer.from && !root.JS_SHA256_NO_BUFFER_FROM) { - bufferFrom = Buffer.from; - } else { - bufferFrom = function (message) { - return new Buffer(message); - }; - } - var nodeMethod = function (message) { - if (typeof message === 'string') { - return crypto.createHash(algorithm).update(message, 'utf8').digest('hex'); - } else { - if (message === null || message === undefined) { - throw new Error(ERROR); - } else if (message.constructor === ArrayBuffer) { - message = new Uint8Array(message); - } - } - if (Array.isArray(message) || ArrayBuffer.isView(message) || message.constructor === Buffer) { - return crypto.createHash(algorithm).update(bufferFrom(message)).digest('hex'); - } else { - return method(message); - } - }; - return nodeMethod; - }; - var createHmacOutputMethod = function (outputType, is224) { return function (key, message) { return new HmacSha256(key, is224, true).update(message)[outputType](); diff --git a/packages/sdk/react-native/src/platform/crypto/PlatformHasher.ts b/packages/sdk/react-native/src/platform/crypto/PlatformHasher.ts index 6432bca3c..f2b9d7489 100644 --- a/packages/sdk/react-native/src/platform/crypto/PlatformHasher.ts +++ b/packages/sdk/react-native/src/platform/crypto/PlatformHasher.ts @@ -1,3 +1,5 @@ +import { algo as CryptoAlgo } from 'crypto-js'; + import { Hasher as LDHasher } from '@launchdarkly/js-client-sdk-common'; import { Hasher, sha256 } from '../../fromExternal/js-sha256'; @@ -8,11 +10,13 @@ export default class PlatformHasher implements LDHasher { private hasher: Hasher; constructor(algorithm: SupportedHashAlgorithm, hmacKey?: string) { - if (algorithm === 'sha256') { - this.hasher = hmacKey ? sha256.hmac.create(hmacKey) : sha256.create(); + switch (algorithm) { + case 'sha256': + this.hasher = hmacKey ? sha256.hmac.create(hmacKey) : sha256.create(); + break; + default: + throw new Error(`Unsupported hash algorithm: ${algorithm}. Only sha256 is supported.`); } - - throw new Error('Unsupported hash algorithm. Only sha256 is supported.'); } digest(encoding: SupportedOutputEncoding): string { From 5ce5e45c7f367c5c90ff1a12241b7af332ff3a7b Mon Sep 17 00:00:00 2001 From: Yusinto Ngadiman Date: Wed, 24 Jan 2024 11:48:34 -0800 Subject: [PATCH 36/44] chore: Added hasher tests. Improve unsupported encoding error message. --- .../platform/crypto/PlatformHasher.test.ts | 61 +++++++++++++++++++ .../src/platform/crypto/PlatformHasher.ts | 6 +- 2 files changed, 64 insertions(+), 3 deletions(-) create mode 100644 packages/sdk/react-native/src/platform/crypto/PlatformHasher.test.ts diff --git a/packages/sdk/react-native/src/platform/crypto/PlatformHasher.test.ts b/packages/sdk/react-native/src/platform/crypto/PlatformHasher.test.ts new file mode 100644 index 000000000..c2108bfd0 --- /dev/null +++ b/packages/sdk/react-native/src/platform/crypto/PlatformHasher.test.ts @@ -0,0 +1,61 @@ +import PlatformHasher from './PlatformHasher'; + +/** + * The links below are different from js-sha256 and are useful to verify the + * correctness of hash and encoding output: + * https://www.liavaag.org/English/SHA-Generator/ + * https://www.liavaag.org/English/SHA-Generator/HMAC/ + */ +describe('PlatformHasher', () => { + test('sha256 produces correct base64 output', () => { + const h = new PlatformHasher('sha256'); + + h.update('test-app-id'); + const output = h.digest('base64'); + + expect(output).toEqual('XVm6ZNk6ejx6+IVtL7zfwYwRQ2/ck9+y7FaN32EcudQ='); + }); + + test('sha256 produces correct hex output', () => { + const h = new PlatformHasher('sha256'); + + h.update('test-app-id'); + const output = h.digest('hex'); + + expect(output).toEqual('5d59ba64d93a7a3c7af8856d2fbcdfc18c11436fdc93dfb2ec568ddf611cb9d4'); + }); + + test('unsupported hash algorithm', () => { + expect(() => { + // @ts-ignore + new PlatformHasher('sha1'); + }).toThrow(/unsupported/i); + }); + + test('unsupported output algorithm', () => { + expect(() => { + const h = new PlatformHasher('sha256'); + h.update('test-app-id'); + // @ts-ignore + const output = h.digest('base122'); + }).toThrow(/unsupported/i); + }); + + test('hmac produces correct base64 output', () => { + const h = new PlatformHasher('sha256', 'hmac-key'); + + h.update('test-app-id'); + const output = h.digest('base64'); + + expect(output).toEqual('tB+++rKY29eF480Oe3ekuWk4AbXV2E8cTgk+UEB9xfA='); + }); + + test('hmac produces correct hex output', () => { + const h = new PlatformHasher('sha256', 'hmac-key'); + + h.update('test-app-id'); + const output = h.digest('hex'); + + expect(output).toEqual('b41fbefab298dbd785e3cd0e7b77a4b9693801b5d5d84f1c4e093e50407dc5f0'); + }); +}); diff --git a/packages/sdk/react-native/src/platform/crypto/PlatformHasher.ts b/packages/sdk/react-native/src/platform/crypto/PlatformHasher.ts index f2b9d7489..081af2624 100644 --- a/packages/sdk/react-native/src/platform/crypto/PlatformHasher.ts +++ b/packages/sdk/react-native/src/platform/crypto/PlatformHasher.ts @@ -1,5 +1,3 @@ -import { algo as CryptoAlgo } from 'crypto-js'; - import { Hasher as LDHasher } from '@launchdarkly/js-client-sdk-common'; import { Hasher, sha256 } from '../../fromExternal/js-sha256'; @@ -26,7 +24,9 @@ export default class PlatformHasher implements LDHasher { case 'hex': return this.hasher.hex(); default: - throw new Error('unsupported output encoding. Only base64 and hex are supported.'); + throw new Error( + `unsupported output encoding: ${encoding}. Only base64 and hex are supported.`, + ); } } From e71e131df9b363a497b2c41c36a42e6c9e82986d Mon Sep 17 00:00:00 2001 From: Yusinto Ngadiman Date: Wed, 24 Jan 2024 12:57:26 -0800 Subject: [PATCH 37/44] Update README.md --- packages/shared/mocks/README.md | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/packages/shared/mocks/README.md b/packages/shared/mocks/README.md index a304c0a06..b4105f3d5 100644 --- a/packages/shared/mocks/README.md +++ b/packages/shared/mocks/README.md @@ -44,13 +44,18 @@ import { basicPlatform, hasher } from '@launchdarkly/private-js-mocks'; // DOES NOT WORK: crypto is undefined because basicPlatform must be inside a test // because it's setup by the package in beforeEach. -// const { crypto } = basicPlatform; // DON'T DO THIS +const { crypto } = basicPlatform; // DON'T DO THIS describe('button', () => { + // DOES NOT WORK: again must be inside an actual test. At the test suite, + // level, beforeEach has not been run. + const { crypto } = basicPlatform; // DON'T DO THIS + + // DO THIS let crypto: Crypto; beforeEach(() => { - // WORKS: basicPlatform has been setup by the package + // WORKS: basicPlatform has been setup by the package. crypto = basicPlatform.crypto; // DO THIS }); From b440dc3fc94e6977b16d2eb766f533f82044c291 Mon Sep 17 00:00:00 2001 From: Yusinto Ngadiman Date: Wed, 24 Jan 2024 13:04:55 -0800 Subject: [PATCH 38/44] chore: Fixed linting errors. --- .../react-native/src/platform/crypto/PlatformHasher.test.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/packages/sdk/react-native/src/platform/crypto/PlatformHasher.test.ts b/packages/sdk/react-native/src/platform/crypto/PlatformHasher.test.ts index c2108bfd0..38135983c 100644 --- a/packages/sdk/react-native/src/platform/crypto/PlatformHasher.test.ts +++ b/packages/sdk/react-native/src/platform/crypto/PlatformHasher.test.ts @@ -28,7 +28,8 @@ describe('PlatformHasher', () => { test('unsupported hash algorithm', () => { expect(() => { // @ts-ignore - new PlatformHasher('sha1'); + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const h = new PlatformHasher('sha1'); }).toThrow(/unsupported/i); }); @@ -37,7 +38,7 @@ describe('PlatformHasher', () => { const h = new PlatformHasher('sha256'); h.update('test-app-id'); // @ts-ignore - const output = h.digest('base122'); + h.digest('base122'); }).toThrow(/unsupported/i); }); From 07abdeaff92621df6e9ef72bc5e41859644b0834 Mon Sep 17 00:00:00 2001 From: Yusinto Ngadiman Date: Wed, 24 Jan 2024 14:36:54 -0800 Subject: [PATCH 39/44] chore: Run setup mocks for common. --- packages/shared/common/jest.config.js | 1 + .../src/internal/diagnostics/DiagnosticsManager.test.ts | 6 +++--- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/packages/shared/common/jest.config.js b/packages/shared/common/jest.config.js index 6753062cc..bcd6a8d01 100644 --- a/packages/shared/common/jest.config.js +++ b/packages/shared/common/jest.config.js @@ -4,4 +4,5 @@ module.exports = { testEnvironment: 'node', moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'json', 'node'], collectCoverageFrom: ['src/**/*.ts'], + setupFilesAfterEnv: ['@launchdarkly/private-js-mocks/setup'], }; diff --git a/packages/shared/common/src/internal/diagnostics/DiagnosticsManager.test.ts b/packages/shared/common/src/internal/diagnostics/DiagnosticsManager.test.ts index c275484aa..6c69b1cb7 100644 --- a/packages/shared/common/src/internal/diagnostics/DiagnosticsManager.test.ts +++ b/packages/shared/common/src/internal/diagnostics/DiagnosticsManager.test.ts @@ -58,9 +58,9 @@ describe('given a diagnostics manager', () => { const { platform } = manager.createInitEvent(); expect(platform).toEqual({ name: 'The SDK Name', - osName: 'An OS', - osVersion: '1.0.1', - osArch: 'An Arch', + osName: 'iOS', + osVersion: '17.17', + osArch: 'ARM64', nodeVersion: '42', }); }); From b8a6f7bfe2a99b88bb838d08a3a3f511fc7e49e5 Mon Sep 17 00:00:00 2001 From: Yusinto Ngadiman Date: Wed, 24 Jan 2024 14:48:45 -0800 Subject: [PATCH 40/44] chore: Fix common tests. --- .../stream/StreamingProcessor.test.ts | 15 ++++++---- packages/shared/mocks/src/clientContext.ts | 30 ++++++++++--------- packages/shared/mocks/src/index.ts | 2 +- packages/shared/mocks/src/setupMocks.ts | 7 +++++ 4 files changed, 33 insertions(+), 21 deletions(-) diff --git a/packages/shared/common/src/internal/stream/StreamingProcessor.test.ts b/packages/shared/common/src/internal/stream/StreamingProcessor.test.ts index cf2e397eb..9f53f99b9 100644 --- a/packages/shared/common/src/internal/stream/StreamingProcessor.test.ts +++ b/packages/shared/common/src/internal/stream/StreamingProcessor.test.ts @@ -1,18 +1,15 @@ import { basicPlatform, clientContext, logger } from '@launchdarkly/private-js-mocks'; -import { EventName, ProcessStreamResponse } from '../../api'; +import { EventName, Info, ProcessStreamResponse } from '../../api'; import { LDStreamProcessor } from '../../api/subsystem'; import { LDStreamingError } from '../../errors'; +import { ApplicationTags, ServiceEndpoints } from '../../options'; import { defaultHeaders } from '../../utils'; import { DiagnosticsManager } from '../diagnostics'; import StreamingProcessor from './StreamingProcessor'; const dateNowString = '2023-08-10'; const sdkKey = 'my-sdk-key'; -const { - basicConfiguration: { serviceEndpoints, tags }, - platform: { info }, -} = clientContext; const event = { data: { flags: { @@ -33,6 +30,9 @@ const createMockEventSource = (streamUri: string = '', options: any = {}) => ({ }); describe('given a stream processor with mock event source', () => { + let serviceEndpoints: ServiceEndpoints; + let tags: ApplicationTags; + let info: Info; let streamingProcessor: LDStreamProcessor; let diagnosticsManager: DiagnosticsManager; let listeners: Map; @@ -53,8 +53,11 @@ describe('given a stream processor with mock event source', () => { beforeEach(() => { mockErrorHandler = jest.fn(); + ({ + basicConfiguration: { serviceEndpoints, tags }, + platform: { info }, + } = clientContext); clientContext.basicConfiguration.logger = logger; - basicPlatform.requests = { createEventSource: jest.fn((streamUri: string, options: any) => { mockEventSource = createMockEventSource(streamUri, options); diff --git a/packages/shared/mocks/src/clientContext.ts b/packages/shared/mocks/src/clientContext.ts index dca6f19a1..75cf2e649 100644 --- a/packages/shared/mocks/src/clientContext.ts +++ b/packages/shared/mocks/src/clientContext.ts @@ -2,19 +2,21 @@ import type { ClientContext } from '@common'; import { basicPlatform } from './platform'; -const clientContext: ClientContext = { - basicConfiguration: { - sdkKey: 'testSdkKey', - serviceEndpoints: { - events: '', - polling: '', - streaming: 'https://mockstream.ld.com', - diagnosticEventPath: '/diagnostic', - analyticsEventPath: '/bulk', - includeAuthorizationHeader: true, +// eslint-disable-next-line import/no-mutable-exports +export let clientContext: ClientContext; +export const setupClientContext = () => { + clientContext = { + basicConfiguration: { + sdkKey: 'testSdkKey', + serviceEndpoints: { + events: '', + polling: '', + streaming: 'https://mockstream.ld.com', + diagnosticEventPath: '/diagnostic', + analyticsEventPath: '/bulk', + includeAuthorizationHeader: true, + }, }, - }, - platform: basicPlatform, + platform: basicPlatform, + }; }; - -export default clientContext; diff --git a/packages/shared/mocks/src/index.ts b/packages/shared/mocks/src/index.ts index a78d38c41..8c6927ba7 100644 --- a/packages/shared/mocks/src/index.ts +++ b/packages/shared/mocks/src/index.ts @@ -1,4 +1,4 @@ -import clientContext from './clientContext'; +import { clientContext } from './clientContext'; import ContextDeduplicator from './contextDeduplicator'; import { hasher } from './crypto'; import logger from './logger'; diff --git a/packages/shared/mocks/src/setupMocks.ts b/packages/shared/mocks/src/setupMocks.ts index fcfc3f4c2..9a663f5fc 100644 --- a/packages/shared/mocks/src/setupMocks.ts +++ b/packages/shared/mocks/src/setupMocks.ts @@ -1,5 +1,12 @@ +import { setupClientContext } from './clientContext'; import { setupBasicPlatform } from './platform'; +beforeAll(() => { + setupBasicPlatform(); + setupClientContext(); +}); + beforeEach(() => { setupBasicPlatform(); + setupClientContext(); }); From 8e6a25eddf04692226e40f9e3eee16c544192880 Mon Sep 17 00:00:00 2001 From: Yusinto Ngadiman Date: Wed, 24 Jan 2024 15:05:31 -0800 Subject: [PATCH 41/44] chore: Improve mocks readme. Improve destructuring syntax. --- packages/shared/mocks/README.md | 35 +++++++++++++++---- .../sdk-client/src/utils/addAutoEnv.test.ts | 4 +-- 2 files changed, 30 insertions(+), 9 deletions(-) diff --git a/packages/shared/mocks/README.md b/packages/shared/mocks/README.md index b4105f3d5..8c502a112 100644 --- a/packages/shared/mocks/README.md +++ b/packages/shared/mocks/README.md @@ -30,33 +30,51 @@ module.exports = { ## Usage > [!IMPORTANT] -> basicPlatform must be used inside a test because it's setup before each test. +> basicPlatform and clientContext must be used inside a test because it's setup before each test. - `basicPlatform`: a concrete but basic implementation of [Platform](https://github.com/launchdarkly/js-core/blob/main/packages/shared/common/src/api/platform/Platform.ts). This is setup beforeEach so it must be used inside a test. +- `clientContext`: ClientContext object including `basicPlatform` above. This is setup beforeEach so it must be used inside a test as well. + - `hasher`: a Hasher object returned by `Crypto.createHash`. All functions in this object are jest mocks. This is exported separately as a top level export because `Crypto` does not expose this publicly and we want to respect that. ## Example ```tsx -import { basicPlatform, hasher } from '@launchdarkly/private-js-mocks'; +import { basicPlatform, clientContext, hasher } from '@launchdarkly/private-js-mocks'; // DOES NOT WORK: crypto is undefined because basicPlatform must be inside a test // because it's setup by the package in beforeEach. -const { crypto } = basicPlatform; // DON'T DO THIS +const { crypto } = basicPlatform; // DON'T DO THIS HERE + +// DOES NOT WORK: clientContext must be used inside a test. Otherwise all properties +// of it will be undefined. +const { + basicConfiguration: { serviceEndpoints, tags }, + platform: { info }, +} = clientContext; // DON'T DO THIS HERE describe('button', () => { // DOES NOT WORK: again must be inside an actual test. At the test suite, // level, beforeEach has not been run. - const { crypto } = basicPlatform; // DON'T DO THIS + const { crypto } = basicPlatform; // DON'T DO THIS HERE // DO THIS let crypto: Crypto; + let info: Info; + let serviceEndpoints: ServiceEndpoints; + let tags: ApplicationTags; beforeEach(() => { - // WORKS: basicPlatform has been setup by the package. - crypto = basicPlatform.crypto; // DO THIS + // WORKS: basicPlatform and clientContext have been setup by the package. + ({ crypto, info } = basicPlatform); + + // WORKS + ({ + basicConfiguration: { serviceEndpoints, tags }, + platform: { info }, + } = clientContext); }); afterEach(() => { @@ -71,8 +89,13 @@ describe('button', () => { const [bucket, hadContext] = bucketer.bucket(); // assert + // WORKS expect(crypto.createHash).toHaveBeenCalled(); + // WORKS: alternatively you can just use the full path to access the properties + // of basicPlatform + expect(basicPlatform.crypto.createHash).toHaveBeenCalled(); + // GOTCHA: hasher is a separte import from crypto to respect // the public Crypto interface. expect(hasher.update).toHaveBeenCalledWith(expected); diff --git a/packages/shared/sdk-client/src/utils/addAutoEnv.test.ts b/packages/shared/sdk-client/src/utils/addAutoEnv.test.ts index 23393737e..d1ea1ac43 100644 --- a/packages/shared/sdk-client/src/utils/addAutoEnv.test.ts +++ b/packages/shared/sdk-client/src/utils/addAutoEnv.test.ts @@ -9,9 +9,7 @@ describe('addAutoEnv', () => { let info: Info; beforeEach(() => { - crypto = basicPlatform.crypto; - info = basicPlatform.info; - + ({ crypto, info } = basicPlatform); (crypto.randomUUID as jest.Mock).mockResolvedValue('test-device-key-1'); }); From 28983fd394cd6e334930746e5fcbc6713e02db6b Mon Sep 17 00:00:00 2001 From: Yusinto Ngadiman Date: Wed, 24 Jan 2024 15:18:40 -0800 Subject: [PATCH 42/44] chore: Add jest to example app for detox to work. --- .../sdk/react-native/example/package.json | 1 + packages/sdk/react-native/example/yarn.lock | 1235 ++++++++++++++++- 2 files changed, 1197 insertions(+), 39 deletions(-) diff --git a/packages/sdk/react-native/example/package.json b/packages/sdk/react-native/example/package.json index 717389b22..5e6af48c4 100644 --- a/packages/sdk/react-native/example/package.json +++ b/packages/sdk/react-native/example/package.json @@ -38,6 +38,7 @@ "@types/react": "~18.2.14", "@types/react-native-dotenv": "^0.2.1", "detox": "^20.14.7", + "jest": "^29.7.0", "ts-jest": "^29.1.1", "typescript": "^5.2.2" } diff --git a/packages/sdk/react-native/example/yarn.lock b/packages/sdk/react-native/example/yarn.lock index caafa8c67..80c8d4a39 100644 --- a/packages/sdk/react-native/example/yarn.lock +++ b/packages/sdk/react-native/example/yarn.lock @@ -46,6 +46,16 @@ __metadata: languageName: node linkType: hard +"@babel/code-frame@npm:^7.23.5": + version: 7.23.5 + resolution: "@babel/code-frame@npm:7.23.5" + dependencies: + "@babel/highlight": ^7.23.4 + chalk: ^2.4.2 + checksum: d90981fdf56a2824a9b14d19a4c0e8db93633fd488c772624b4e83e0ceac6039a27cd298a247c3214faa952bf803ba23696172ae7e7235f3b97f43ba278c569a + languageName: node + linkType: hard + "@babel/compat-data@npm:^7.20.5, @babel/compat-data@npm:^7.22.6, @babel/compat-data@npm:^7.22.9, @babel/compat-data@npm:^7.23.3": version: 7.23.3 resolution: "@babel/compat-data@npm:7.23.3" @@ -53,6 +63,36 @@ __metadata: languageName: node linkType: hard +"@babel/compat-data@npm:^7.23.5": + version: 7.23.5 + resolution: "@babel/compat-data@npm:7.23.5" + checksum: 06ce244cda5763295a0ea924728c09bae57d35713b675175227278896946f922a63edf803c322f855a3878323d48d0255a2a3023409d2a123483c8a69ebb4744 + languageName: node + linkType: hard + +"@babel/core@npm:^7.11.6, @babel/core@npm:^7.12.3": + version: 7.23.7 + resolution: "@babel/core@npm:7.23.7" + dependencies: + "@ampproject/remapping": ^2.2.0 + "@babel/code-frame": ^7.23.5 + "@babel/generator": ^7.23.6 + "@babel/helper-compilation-targets": ^7.23.6 + "@babel/helper-module-transforms": ^7.23.3 + "@babel/helpers": ^7.23.7 + "@babel/parser": ^7.23.6 + "@babel/template": ^7.22.15 + "@babel/traverse": ^7.23.7 + "@babel/types": ^7.23.6 + convert-source-map: ^2.0.0 + debug: ^4.1.0 + gensync: ^1.0.0-beta.2 + json5: ^2.2.3 + semver: ^6.3.1 + checksum: 32d5bf73372a47429afaae9adb0af39e47bcea6a831c4b5dcbb4791380cda6949cb8cb1a2fea8b60bb1ebe189209c80e333903df1fa8e9dcb04798c0ce5bf59e + languageName: node + linkType: hard + "@babel/core@npm:^7.13.16, @babel/core@npm:^7.20.0": version: 7.23.3 resolution: "@babel/core@npm:7.23.3" @@ -88,6 +128,18 @@ __metadata: languageName: node linkType: hard +"@babel/generator@npm:^7.23.6, @babel/generator@npm:^7.7.2": + version: 7.23.6 + resolution: "@babel/generator@npm:7.23.6" + dependencies: + "@babel/types": ^7.23.6 + "@jridgewell/gen-mapping": ^0.3.2 + "@jridgewell/trace-mapping": ^0.3.17 + jsesc: ^2.5.1 + checksum: 1a1a1c4eac210f174cd108d479464d053930a812798e09fee069377de39a893422df5b5b146199ead7239ae6d3a04697b45fc9ac6e38e0f6b76374390f91fc6c + languageName: node + linkType: hard + "@babel/helper-annotate-as-pure@npm:^7.22.5": version: 7.22.5 resolution: "@babel/helper-annotate-as-pure@npm:7.22.5" @@ -119,6 +171,19 @@ __metadata: languageName: node linkType: hard +"@babel/helper-compilation-targets@npm:^7.23.6": + version: 7.23.6 + resolution: "@babel/helper-compilation-targets@npm:7.23.6" + dependencies: + "@babel/compat-data": ^7.23.5 + "@babel/helper-validator-option": ^7.23.5 + browserslist: ^4.22.2 + lru-cache: ^5.1.1 + semver: ^6.3.1 + checksum: c630b98d4527ac8fe2c58d9a06e785dfb2b73ec71b7c4f2ddf90f814b5f75b547f3c015f110a010fd31f76e3864daaf09f3adcd2f6acdbfb18a8de3a48717590 + languageName: node + linkType: hard + "@babel/helper-create-class-features-plugin@npm:^7.18.6, @babel/helper-create-class-features-plugin@npm:^7.22.15": version: 7.22.15 resolution: "@babel/helper-create-class-features-plugin@npm:7.22.15" @@ -315,6 +380,13 @@ __metadata: languageName: node linkType: hard +"@babel/helper-validator-option@npm:^7.23.5": + version: 7.23.5 + resolution: "@babel/helper-validator-option@npm:7.23.5" + checksum: 537cde2330a8aede223552510e8a13e9c1c8798afee3757995a7d4acae564124fe2bf7e7c3d90d62d3657434a74340a274b3b3b1c6f17e9a2be1f48af29cb09e + languageName: node + linkType: hard + "@babel/helper-wrap-function@npm:^7.22.20": version: 7.22.20 resolution: "@babel/helper-wrap-function@npm:7.22.20" @@ -337,6 +409,17 @@ __metadata: languageName: node linkType: hard +"@babel/helpers@npm:^7.23.7": + version: 7.23.8 + resolution: "@babel/helpers@npm:7.23.8" + dependencies: + "@babel/template": ^7.22.15 + "@babel/traverse": ^7.23.7 + "@babel/types": ^7.23.6 + checksum: 8b522d527921f8df45a983dc7b8e790c021250addf81ba7900ba016e165442a527348f6f877aa55e1debb3eef9e860a334b4e8d834e6c9b438ed61a63d9a7ad4 + languageName: node + linkType: hard + "@babel/highlight@npm:^7.10.4, @babel/highlight@npm:^7.23.4": version: 7.23.4 resolution: "@babel/highlight@npm:7.23.4" @@ -348,6 +431,15 @@ __metadata: languageName: node linkType: hard +"@babel/parser@npm:^7.1.0, @babel/parser@npm:^7.14.7, @babel/parser@npm:^7.20.7, @babel/parser@npm:^7.23.6": + version: 7.23.6 + resolution: "@babel/parser@npm:7.23.6" + bin: + parser: ./bin/babel-parser.js + checksum: 140801c43731a6c41fd193f5c02bc71fd647a0360ca616b23d2db8be4b9739b9f951a03fc7c2db4f9b9214f4b27c1074db0f18bc3fa653783082d5af7c8860d5 + languageName: node + linkType: hard + "@babel/parser@npm:^7.13.16, @babel/parser@npm:^7.20.0, @babel/parser@npm:^7.22.15, @babel/parser@npm:^7.23.3, @babel/parser@npm:^7.23.4": version: 7.23.4 resolution: "@babel/parser@npm:7.23.4" @@ -542,7 +634,18 @@ __metadata: languageName: node linkType: hard -"@babel/plugin-syntax-class-properties@npm:^7.0.0, @babel/plugin-syntax-class-properties@npm:^7.12.13": +"@babel/plugin-syntax-bigint@npm:^7.8.3": + version: 7.8.3 + resolution: "@babel/plugin-syntax-bigint@npm:7.8.3" + dependencies: + "@babel/helper-plugin-utils": ^7.8.0 + peerDependencies: + "@babel/core": ^7.0.0-0 + checksum: 3a10849d83e47aec50f367a9e56a6b22d662ddce643334b087f9828f4c3dd73bdc5909aaeabe123fed78515767f9ca43498a0e621c438d1cd2802d7fae3c9648 + languageName: node + linkType: hard + +"@babel/plugin-syntax-class-properties@npm:^7.0.0, @babel/plugin-syntax-class-properties@npm:^7.12.13, @babel/plugin-syntax-class-properties@npm:^7.8.3": version: 7.12.13 resolution: "@babel/plugin-syntax-class-properties@npm:7.12.13" dependencies: @@ -641,7 +744,7 @@ __metadata: languageName: node linkType: hard -"@babel/plugin-syntax-import-meta@npm:^7.10.4": +"@babel/plugin-syntax-import-meta@npm:^7.10.4, @babel/plugin-syntax-import-meta@npm:^7.8.3": version: 7.10.4 resolution: "@babel/plugin-syntax-import-meta@npm:7.10.4" dependencies: @@ -663,7 +766,7 @@ __metadata: languageName: node linkType: hard -"@babel/plugin-syntax-jsx@npm:^7.0.0, @babel/plugin-syntax-jsx@npm:^7.23.3": +"@babel/plugin-syntax-jsx@npm:^7.0.0, @babel/plugin-syntax-jsx@npm:^7.23.3, @babel/plugin-syntax-jsx@npm:^7.7.2": version: 7.23.3 resolution: "@babel/plugin-syntax-jsx@npm:7.23.3" dependencies: @@ -674,7 +777,7 @@ __metadata: languageName: node linkType: hard -"@babel/plugin-syntax-logical-assignment-operators@npm:^7.10.4": +"@babel/plugin-syntax-logical-assignment-operators@npm:^7.10.4, @babel/plugin-syntax-logical-assignment-operators@npm:^7.8.3": version: 7.10.4 resolution: "@babel/plugin-syntax-logical-assignment-operators@npm:7.10.4" dependencies: @@ -696,7 +799,7 @@ __metadata: languageName: node linkType: hard -"@babel/plugin-syntax-numeric-separator@npm:^7.10.4": +"@babel/plugin-syntax-numeric-separator@npm:^7.10.4, @babel/plugin-syntax-numeric-separator@npm:^7.8.3": version: 7.10.4 resolution: "@babel/plugin-syntax-numeric-separator@npm:7.10.4" dependencies: @@ -751,7 +854,7 @@ __metadata: languageName: node linkType: hard -"@babel/plugin-syntax-top-level-await@npm:^7.14.5": +"@babel/plugin-syntax-top-level-await@npm:^7.14.5, @babel/plugin-syntax-top-level-await@npm:^7.8.3": version: 7.14.5 resolution: "@babel/plugin-syntax-top-level-await@npm:7.14.5" dependencies: @@ -762,7 +865,7 @@ __metadata: languageName: node linkType: hard -"@babel/plugin-syntax-typescript@npm:^7.23.3": +"@babel/plugin-syntax-typescript@npm:^7.23.3, @babel/plugin-syntax-typescript@npm:^7.7.2": version: 7.23.3 resolution: "@babel/plugin-syntax-typescript@npm:7.23.3" dependencies: @@ -1617,7 +1720,7 @@ __metadata: languageName: node linkType: hard -"@babel/template@npm:^7.0.0, @babel/template@npm:^7.22.15": +"@babel/template@npm:^7.0.0, @babel/template@npm:^7.22.15, @babel/template@npm:^7.3.3": version: 7.22.15 resolution: "@babel/template@npm:7.22.15" dependencies: @@ -1646,6 +1749,35 @@ __metadata: languageName: node linkType: hard +"@babel/traverse@npm:^7.23.7": + version: 7.23.7 + resolution: "@babel/traverse@npm:7.23.7" + dependencies: + "@babel/code-frame": ^7.23.5 + "@babel/generator": ^7.23.6 + "@babel/helper-environment-visitor": ^7.22.20 + "@babel/helper-function-name": ^7.23.0 + "@babel/helper-hoist-variables": ^7.22.5 + "@babel/helper-split-export-declaration": ^7.22.6 + "@babel/parser": ^7.23.6 + "@babel/types": ^7.23.6 + debug: ^4.3.1 + globals: ^11.1.0 + checksum: d4a7afb922361f710efc97b1e25ec343fab8b2a4ddc81ca84f9a153f22d4482112cba8f263774be8d297918b6c4767c7a98988ab4e53ac73686c986711dd002e + languageName: node + linkType: hard + +"@babel/types@npm:^7.0.0, @babel/types@npm:^7.20.7, @babel/types@npm:^7.23.6, @babel/types@npm:^7.3.3": + version: 7.23.6 + resolution: "@babel/types@npm:7.23.6" + dependencies: + "@babel/helper-string-parser": ^7.23.4 + "@babel/helper-validator-identifier": ^7.22.20 + to-fast-properties: ^2.0.0 + checksum: 68187dbec0d637f79bc96263ac95ec8b06d424396678e7e225492be866414ce28ebc918a75354d4c28659be6efe30020b4f0f6df81cc418a2d30645b690a8de0 + languageName: node + linkType: hard + "@babel/types@npm:^7.20.0, @babel/types@npm:^7.22.15, @babel/types@npm:^7.22.19, @babel/types@npm:^7.22.5, @babel/types@npm:^7.23.0, @babel/types@npm:^7.23.3, @babel/types@npm:^7.23.4, @babel/types@npm:^7.4.4, @babel/types@npm:^7.8.3": version: 7.23.4 resolution: "@babel/types@npm:7.23.4" @@ -1657,6 +1789,13 @@ __metadata: languageName: node linkType: hard +"@bcoe/v8-coverage@npm:^0.2.3": + version: 0.2.3 + resolution: "@bcoe/v8-coverage@npm:0.2.3" + checksum: 850f9305536d0f2bd13e9e0881cb5f02e4f93fad1189f7b2d4bebf694e3206924eadee1068130d43c11b750efcc9405f88a8e42ef098b6d75239c0f047de1a27 + languageName: node + linkType: hard + "@expo/bunyan@npm:4.0.0, @expo/bunyan@npm:^4.0.0": version: 4.0.0 resolution: "@expo/bunyan@npm:4.0.0" @@ -2085,6 +2224,81 @@ __metadata: languageName: node linkType: hard +"@istanbuljs/load-nyc-config@npm:^1.0.0": + version: 1.1.0 + resolution: "@istanbuljs/load-nyc-config@npm:1.1.0" + dependencies: + camelcase: ^5.3.1 + find-up: ^4.1.0 + get-package-type: ^0.1.0 + js-yaml: ^3.13.1 + resolve-from: ^5.0.0 + checksum: d578da5e2e804d5c93228450a1380e1a3c691de4953acc162f387b717258512a3e07b83510a936d9fab03eac90817473917e24f5d16297af3867f59328d58568 + languageName: node + linkType: hard + +"@istanbuljs/schema@npm:^0.1.2": + version: 0.1.3 + resolution: "@istanbuljs/schema@npm:0.1.3" + checksum: 5282759d961d61350f33d9118d16bcaed914ebf8061a52f4fa474b2cb08720c9c81d165e13b82f2e5a8a212cc5af482f0c6fc1ac27b9e067e5394c9a6ed186c9 + languageName: node + linkType: hard + +"@jest/console@npm:^29.7.0": + version: 29.7.0 + resolution: "@jest/console@npm:29.7.0" + dependencies: + "@jest/types": ^29.6.3 + "@types/node": "*" + chalk: ^4.0.0 + jest-message-util: ^29.7.0 + jest-util: ^29.7.0 + slash: ^3.0.0 + checksum: 0e3624e32c5a8e7361e889db70b170876401b7d70f509a2538c31d5cd50deb0c1ae4b92dc63fe18a0902e0a48c590c21d53787a0df41a52b34fa7cab96c384d6 + languageName: node + linkType: hard + +"@jest/core@npm:^29.7.0": + version: 29.7.0 + resolution: "@jest/core@npm:29.7.0" + dependencies: + "@jest/console": ^29.7.0 + "@jest/reporters": ^29.7.0 + "@jest/test-result": ^29.7.0 + "@jest/transform": ^29.7.0 + "@jest/types": ^29.6.3 + "@types/node": "*" + ansi-escapes: ^4.2.1 + chalk: ^4.0.0 + ci-info: ^3.2.0 + exit: ^0.1.2 + graceful-fs: ^4.2.9 + jest-changed-files: ^29.7.0 + jest-config: ^29.7.0 + jest-haste-map: ^29.7.0 + jest-message-util: ^29.7.0 + jest-regex-util: ^29.6.3 + jest-resolve: ^29.7.0 + jest-resolve-dependencies: ^29.7.0 + jest-runner: ^29.7.0 + jest-runtime: ^29.7.0 + jest-snapshot: ^29.7.0 + jest-util: ^29.7.0 + jest-validate: ^29.7.0 + jest-watcher: ^29.7.0 + micromatch: ^4.0.4 + pretty-format: ^29.7.0 + slash: ^3.0.0 + strip-ansi: ^6.0.0 + peerDependencies: + node-notifier: ^8.0.1 || ^9.0.0 || ^10.0.0 + peerDependenciesMeta: + node-notifier: + optional: true + checksum: af759c9781cfc914553320446ce4e47775ae42779e73621c438feb1e4231a5d4862f84b1d8565926f2d1aab29b3ec3dcfdc84db28608bdf5f29867124ebcfc0d + languageName: node + linkType: hard + "@jest/create-cache-key-function@npm:^29.2.1": version: 29.7.0 resolution: "@jest/create-cache-key-function@npm:29.7.0" @@ -2115,6 +2329,16 @@ __metadata: languageName: node linkType: hard +"@jest/expect@npm:^29.7.0": + version: 29.7.0 + resolution: "@jest/expect@npm:29.7.0" + dependencies: + expect: ^29.7.0 + jest-snapshot: ^29.7.0 + checksum: a01cb85fd9401bab3370618f4b9013b90c93536562222d920e702a0b575d239d74cecfe98010aaec7ad464f67cf534a353d92d181646a4b792acaa7e912ae55e + languageName: node + linkType: hard + "@jest/fake-timers@npm:^29.7.0": version: 29.7.0 resolution: "@jest/fake-timers@npm:29.7.0" @@ -2129,6 +2353,55 @@ __metadata: languageName: node linkType: hard +"@jest/globals@npm:^29.7.0": + version: 29.7.0 + resolution: "@jest/globals@npm:29.7.0" + dependencies: + "@jest/environment": ^29.7.0 + "@jest/expect": ^29.7.0 + "@jest/types": ^29.6.3 + jest-mock: ^29.7.0 + checksum: 97dbb9459135693ad3a422e65ca1c250f03d82b2a77f6207e7fa0edd2c9d2015fbe4346f3dc9ebff1678b9d8da74754d4d440b7837497f8927059c0642a22123 + languageName: node + linkType: hard + +"@jest/reporters@npm:^29.7.0": + version: 29.7.0 + resolution: "@jest/reporters@npm:29.7.0" + dependencies: + "@bcoe/v8-coverage": ^0.2.3 + "@jest/console": ^29.7.0 + "@jest/test-result": ^29.7.0 + "@jest/transform": ^29.7.0 + "@jest/types": ^29.6.3 + "@jridgewell/trace-mapping": ^0.3.18 + "@types/node": "*" + chalk: ^4.0.0 + collect-v8-coverage: ^1.0.0 + exit: ^0.1.2 + glob: ^7.1.3 + graceful-fs: ^4.2.9 + istanbul-lib-coverage: ^3.0.0 + istanbul-lib-instrument: ^6.0.0 + istanbul-lib-report: ^3.0.0 + istanbul-lib-source-maps: ^4.0.0 + istanbul-reports: ^3.1.3 + jest-message-util: ^29.7.0 + jest-util: ^29.7.0 + jest-worker: ^29.7.0 + slash: ^3.0.0 + string-length: ^4.0.1 + strip-ansi: ^6.0.0 + v8-to-istanbul: ^9.0.1 + peerDependencies: + node-notifier: ^8.0.1 || ^9.0.0 || ^10.0.0 + peerDependenciesMeta: + node-notifier: + optional: true + checksum: 7eadabd62cc344f629024b8a268ecc8367dba756152b761bdcb7b7e570a3864fc51b2a9810cd310d85e0a0173ef002ba4528d5ea0329fbf66ee2a3ada9c40455 + languageName: node + linkType: hard + "@jest/schemas@npm:^29.6.3": version: 29.6.3 resolution: "@jest/schemas@npm:29.6.3" @@ -2138,6 +2411,64 @@ __metadata: languageName: node linkType: hard +"@jest/source-map@npm:^29.6.3": + version: 29.6.3 + resolution: "@jest/source-map@npm:29.6.3" + dependencies: + "@jridgewell/trace-mapping": ^0.3.18 + callsites: ^3.0.0 + graceful-fs: ^4.2.9 + checksum: bcc5a8697d471396c0003b0bfa09722c3cd879ad697eb9c431e6164e2ea7008238a01a07193dfe3cbb48b1d258eb7251f6efcea36f64e1ebc464ea3c03ae2deb + languageName: node + linkType: hard + +"@jest/test-result@npm:^29.7.0": + version: 29.7.0 + resolution: "@jest/test-result@npm:29.7.0" + dependencies: + "@jest/console": ^29.7.0 + "@jest/types": ^29.6.3 + "@types/istanbul-lib-coverage": ^2.0.0 + collect-v8-coverage: ^1.0.0 + checksum: 67b6317d526e335212e5da0e768e3b8ab8a53df110361b80761353ad23b6aea4432b7c5665bdeb87658ea373b90fb1afe02ed3611ef6c858c7fba377505057fa + languageName: node + linkType: hard + +"@jest/test-sequencer@npm:^29.7.0": + version: 29.7.0 + resolution: "@jest/test-sequencer@npm:29.7.0" + dependencies: + "@jest/test-result": ^29.7.0 + graceful-fs: ^4.2.9 + jest-haste-map: ^29.7.0 + slash: ^3.0.0 + checksum: 73f43599017946be85c0b6357993b038f875b796e2f0950487a82f4ebcb115fa12131932dd9904026b4ad8be131fe6e28bd8d0aa93b1563705185f9804bff8bd + languageName: node + linkType: hard + +"@jest/transform@npm:^29.7.0": + version: 29.7.0 + resolution: "@jest/transform@npm:29.7.0" + dependencies: + "@babel/core": ^7.11.6 + "@jest/types": ^29.6.3 + "@jridgewell/trace-mapping": ^0.3.18 + babel-plugin-istanbul: ^6.1.1 + chalk: ^4.0.0 + convert-source-map: ^2.0.0 + fast-json-stable-stringify: ^2.1.0 + graceful-fs: ^4.2.9 + jest-haste-map: ^29.7.0 + jest-regex-util: ^29.6.3 + jest-util: ^29.7.0 + micromatch: ^4.0.4 + pirates: ^4.0.4 + slash: ^3.0.0 + write-file-atomic: ^4.0.2 + checksum: 0f8ac9f413903b3cb6d240102db848f2a354f63971ab885833799a9964999dd51c388162106a807f810071f864302cdd8e3f0c241c29ce02d85a36f18f3f40ab + languageName: node + linkType: hard + "@jest/types@npm:^26.6.2": version: 26.6.2 resolution: "@jest/types@npm:26.6.2" @@ -2220,6 +2551,16 @@ __metadata: languageName: node linkType: hard +"@jridgewell/trace-mapping@npm:^0.3.12, @jridgewell/trace-mapping@npm:^0.3.18": + version: 0.3.22 + resolution: "@jridgewell/trace-mapping@npm:0.3.22" + dependencies: + "@jridgewell/resolve-uri": ^3.1.0 + "@jridgewell/sourcemap-codec": ^1.4.14 + checksum: ac7dd2cfe0b479aa1b81776d40d789243131cc792dc8b6b6a028c70fcd6171958ae1a71bf67b618ffe3c0c3feead9870c095ee46a5e30319410d92976b28f498 + languageName: node + linkType: hard + "@jridgewell/trace-mapping@npm:^0.3.17, @jridgewell/trace-mapping@npm:^0.3.9": version: 0.3.20 resolution: "@jridgewell/trace-mapping@npm:0.3.20" @@ -2633,6 +2974,47 @@ __metadata: languageName: node linkType: hard +"@types/babel__core@npm:^7.1.14": + version: 7.20.5 + resolution: "@types/babel__core@npm:7.20.5" + dependencies: + "@babel/parser": ^7.20.7 + "@babel/types": ^7.20.7 + "@types/babel__generator": "*" + "@types/babel__template": "*" + "@types/babel__traverse": "*" + checksum: a3226f7930b635ee7a5e72c8d51a357e799d19cbf9d445710fa39ab13804f79ab1a54b72ea7d8e504659c7dfc50675db974b526142c754398d7413aa4bc30845 + languageName: node + linkType: hard + +"@types/babel__generator@npm:*": + version: 7.6.8 + resolution: "@types/babel__generator@npm:7.6.8" + dependencies: + "@babel/types": ^7.0.0 + checksum: 5b332ea336a2efffbdeedb92b6781949b73498606ddd4205462f7d96dafd45ff3618770b41de04c4881e333dd84388bfb8afbdf6f2764cbd98be550d85c6bb48 + languageName: node + linkType: hard + +"@types/babel__template@npm:*": + version: 7.4.4 + resolution: "@types/babel__template@npm:7.4.4" + dependencies: + "@babel/parser": ^7.1.0 + "@babel/types": ^7.0.0 + checksum: d7a02d2a9b67e822694d8e6a7ddb8f2b71a1d6962dfd266554d2513eefbb205b33ca71a0d163b1caea3981ccf849211f9964d8bd0727124d18ace45aa6c9ae29 + languageName: node + linkType: hard + +"@types/babel__traverse@npm:*, @types/babel__traverse@npm:^7.0.6": + version: 7.20.5 + resolution: "@types/babel__traverse@npm:7.20.5" + dependencies: + "@babel/types": ^7.20.7 + checksum: 608e0ab4fc31cd47011d98942e6241b34d461608c0c0e153377c5fd822c436c475f1ded76a56bfa76a1adf8d9266b727bbf9bfac90c4cb152c97f30dadc5b7e8 + languageName: node + linkType: hard + "@types/detox@npm:^18.1.0": version: 18.1.0 resolution: "@types/detox@npm:18.1.0" @@ -2642,7 +3024,16 @@ __metadata: languageName: node linkType: hard -"@types/istanbul-lib-coverage@npm:*, @types/istanbul-lib-coverage@npm:^2.0.0": +"@types/graceful-fs@npm:^4.1.3": + version: 4.1.9 + resolution: "@types/graceful-fs@npm:4.1.9" + dependencies: + "@types/node": "*" + checksum: 79d746a8f053954bba36bd3d94a90c78de995d126289d656fb3271dd9f1229d33f678da04d10bce6be440494a5a73438e2e363e92802d16b8315b051036c5256 + languageName: node + linkType: hard + +"@types/istanbul-lib-coverage@npm:*, @types/istanbul-lib-coverage@npm:^2.0.0, @types/istanbul-lib-coverage@npm:^2.0.1": version: 2.0.6 resolution: "@types/istanbul-lib-coverage@npm:2.0.6" checksum: 3feac423fd3e5449485afac999dcfcb3d44a37c830af898b689fadc65d26526460bedb889db278e0d4d815a670331796494d073a10ee6e3a6526301fe7415778 @@ -3092,6 +3483,48 @@ __metadata: languageName: node linkType: hard +"babel-jest@npm:^29.7.0": + version: 29.7.0 + resolution: "babel-jest@npm:29.7.0" + dependencies: + "@jest/transform": ^29.7.0 + "@types/babel__core": ^7.1.14 + babel-plugin-istanbul: ^6.1.1 + babel-preset-jest: ^29.6.3 + chalk: ^4.0.0 + graceful-fs: ^4.2.9 + slash: ^3.0.0 + peerDependencies: + "@babel/core": ^7.8.0 + checksum: ee6f8e0495afee07cac5e4ee167be705c711a8cc8a737e05a587a131fdae2b3c8f9aa55dfd4d9c03009ac2d27f2de63d8ba96d3e8460da4d00e8af19ef9a83f7 + languageName: node + linkType: hard + +"babel-plugin-istanbul@npm:^6.1.1": + version: 6.1.1 + resolution: "babel-plugin-istanbul@npm:6.1.1" + dependencies: + "@babel/helper-plugin-utils": ^7.0.0 + "@istanbuljs/load-nyc-config": ^1.0.0 + "@istanbuljs/schema": ^0.1.2 + istanbul-lib-instrument: ^5.0.4 + test-exclude: ^6.0.0 + checksum: cb4fd95738219f232f0aece1116628cccff16db891713c4ccb501cddbbf9272951a5df81f2f2658dfdf4b3e7b236a9d5cbcf04d5d8c07dd5077297339598061a + languageName: node + linkType: hard + +"babel-plugin-jest-hoist@npm:^29.6.3": + version: 29.6.3 + resolution: "babel-plugin-jest-hoist@npm:29.6.3" + dependencies: + "@babel/template": ^7.3.3 + "@babel/types": ^7.3.3 + "@types/babel__core": ^7.1.14 + "@types/babel__traverse": ^7.0.6 + checksum: 51250f22815a7318f17214a9d44650ba89551e6d4f47a2dc259128428324b52f5a73979d010cefd921fd5a720d8c1d55ad74ff601cd94c7bd44d5f6292fde2d1 + languageName: node + linkType: hard + "babel-plugin-module-resolver@npm:^5.0.0": version: 5.0.0 resolution: "babel-plugin-module-resolver@npm:5.0.0" @@ -3164,6 +3597,28 @@ __metadata: languageName: node linkType: hard +"babel-preset-current-node-syntax@npm:^1.0.0": + version: 1.0.1 + resolution: "babel-preset-current-node-syntax@npm:1.0.1" + dependencies: + "@babel/plugin-syntax-async-generators": ^7.8.4 + "@babel/plugin-syntax-bigint": ^7.8.3 + "@babel/plugin-syntax-class-properties": ^7.8.3 + "@babel/plugin-syntax-import-meta": ^7.8.3 + "@babel/plugin-syntax-json-strings": ^7.8.3 + "@babel/plugin-syntax-logical-assignment-operators": ^7.8.3 + "@babel/plugin-syntax-nullish-coalescing-operator": ^7.8.3 + "@babel/plugin-syntax-numeric-separator": ^7.8.3 + "@babel/plugin-syntax-object-rest-spread": ^7.8.3 + "@babel/plugin-syntax-optional-catch-binding": ^7.8.3 + "@babel/plugin-syntax-optional-chaining": ^7.8.3 + "@babel/plugin-syntax-top-level-await": ^7.8.3 + peerDependencies: + "@babel/core": ^7.0.0 + checksum: d118c2742498c5492c095bc8541f4076b253e705b5f1ad9a2e7d302d81a84866f0070346662355c8e25fc02caa28dc2da8d69bcd67794a0d60c4d6fab6913cc8 + languageName: node + linkType: hard + "babel-preset-expo@npm:~9.5.2": version: 9.5.2 resolution: "babel-preset-expo@npm:9.5.2" @@ -3217,6 +3672,18 @@ __metadata: languageName: node linkType: hard +"babel-preset-jest@npm:^29.6.3": + version: 29.6.3 + resolution: "babel-preset-jest@npm:29.6.3" + dependencies: + babel-plugin-jest-hoist: ^29.6.3 + babel-preset-current-node-syntax: ^1.0.0 + peerDependencies: + "@babel/core": ^7.0.0 + checksum: aa4ff2a8a728d9d698ed521e3461a109a1e66202b13d3494e41eea30729a5e7cc03b3a2d56c594423a135429c37bf63a9fa8b0b9ce275298be3095a88c69f6fb + languageName: node + linkType: hard + "balanced-match@npm:^1.0.0": version: 1.0.2 resolution: "balanced-match@npm:1.0.2" @@ -3359,6 +3826,20 @@ __metadata: languageName: node linkType: hard +"browserslist@npm:^4.22.2": + version: 4.22.2 + resolution: "browserslist@npm:4.22.2" + dependencies: + caniuse-lite: ^1.0.30001565 + electron-to-chromium: ^1.4.601 + node-releases: ^2.0.14 + update-browserslist-db: ^1.0.13 + bin: + browserslist: cli.js + checksum: 33ddfcd9145220099a7a1ac533cecfe5b7548ffeb29b313e1b57be6459000a1f8fa67e781cf4abee97268ac594d44134fcc4a6b2b4750ceddc9796e3a22076d9 + languageName: node + linkType: hard + "bs-logger@npm:0.x": version: 0.2.6 resolution: "bs-logger@npm:0.2.6" @@ -3606,7 +4087,14 @@ __metadata: languageName: node linkType: hard -"camelcase@npm:^5.0.0": +"callsites@npm:^3.0.0": + version: 3.1.0 + resolution: "callsites@npm:3.1.0" + checksum: 072d17b6abb459c2ba96598918b55868af677154bec7e73d222ef95a8fdb9bbf7dae96a8421085cdad8cd190d86653b5b6dc55a4484f2e5b2e27d5e0c3fc15b3 + languageName: node + linkType: hard + +"camelcase@npm:^5.0.0, camelcase@npm:^5.3.1": version: 5.3.1 resolution: "camelcase@npm:5.3.1" checksum: e6effce26b9404e3c0f301498184f243811c30dfe6d0b9051863bd8e4034d09c8c2923794f280d6827e5aa055f6c434115ff97864a16a963366fb35fd673024b @@ -3627,6 +4115,13 @@ __metadata: languageName: node linkType: hard +"caniuse-lite@npm:^1.0.30001565": + version: 1.0.30001579 + resolution: "caniuse-lite@npm:1.0.30001579" + checksum: 7539dcff74d2243a30c428393dc690c87fa34d7da36434731853e9bcfe783757763b2971f5cc878e25242a93e184e53f167d11bd74955af956579f7af71cc764 + languageName: node + linkType: hard + "chalk@npm:^2.0.1, chalk@npm:^2.4.2": version: 2.4.2 resolution: "chalk@npm:2.4.2" @@ -3648,6 +4143,13 @@ __metadata: languageName: node linkType: hard +"char-regex@npm:^1.0.2": + version: 1.0.2 + resolution: "char-regex@npm:1.0.2" + checksum: b563e4b6039b15213114626621e7a3d12f31008bdce20f9c741d69987f62aeaace7ec30f6018890ad77b2e9b4d95324c9f5acfca58a9441e3b1dcdd1e2525d17 + languageName: node + linkType: hard + "charenc@npm:0.0.2, charenc@npm:~0.0.1": version: 0.0.2 resolution: "charenc@npm:0.0.2" @@ -3687,6 +4189,13 @@ __metadata: languageName: node linkType: hard +"cjs-module-lexer@npm:^1.0.0": + version: 1.2.3 + resolution: "cjs-module-lexer@npm:1.2.3" + checksum: 5ea3cb867a9bb609b6d476cd86590d105f3cfd6514db38ff71f63992ab40939c2feb68967faa15a6d2b1f90daa6416b79ea2de486e9e2485a6f8b66a21b4fb0a + languageName: node + linkType: hard + "clean-stack@npm:^2.0.0": version: 2.2.0 resolution: "clean-stack@npm:2.2.0" @@ -3766,6 +4275,20 @@ __metadata: languageName: node linkType: hard +"co@npm:^4.6.0": + version: 4.6.0 + resolution: "co@npm:4.6.0" + checksum: 5210d9223010eb95b29df06a91116f2cf7c8e0748a9013ed853b53f362ea0e822f1e5bb054fb3cefc645239a4cf966af1f6133a3b43f40d591f3b68ed6cf0510 + languageName: node + linkType: hard + +"collect-v8-coverage@npm:^1.0.0": + version: 1.0.2 + resolution: "collect-v8-coverage@npm:1.0.2" + checksum: c10f41c39ab84629d16f9f6137bc8a63d332244383fc368caf2d2052b5e04c20cd1fd70f66fcf4e2422b84c8226598b776d39d5f2d2a51867cc1ed5d1982b4da + languageName: node + linkType: hard + "color-convert@npm:^1.9.0": version: 1.9.3 resolution: "color-convert@npm:1.9.3" @@ -3962,6 +4485,23 @@ __metadata: languageName: node linkType: hard +"create-jest@npm:^29.7.0": + version: 29.7.0 + resolution: "create-jest@npm:29.7.0" + dependencies: + "@jest/types": ^29.6.3 + chalk: ^4.0.0 + exit: ^0.1.2 + graceful-fs: ^4.2.9 + jest-config: ^29.7.0 + jest-util: ^29.7.0 + prompts: ^2.0.1 + bin: + create-jest: bin/create-jest.js + checksum: 1427d49458adcd88547ef6fa39041e1fe9033a661293aa8d2c3aa1b4967cb5bf4f0c00436c7a61816558f28ba2ba81a94d5c962e8022ea9a883978fc8e1f2945 + languageName: node + linkType: hard + "cross-fetch@npm:^3.1.5": version: 3.1.8 resolution: "cross-fetch@npm:3.1.8" @@ -4091,6 +4631,18 @@ __metadata: languageName: node linkType: hard +"dedent@npm:^1.0.0": + version: 1.5.1 + resolution: "dedent@npm:1.5.1" + peerDependencies: + babel-plugin-macros: ^3.1.0 + peerDependenciesMeta: + babel-plugin-macros: + optional: true + checksum: c3c300a14edf1bdf5a873f9e4b22e839d62490bc5c8d6169c1f15858a1a76733d06a9a56930e963d677a2ceeca4b6b0894cc5ea2f501aa382ca5b92af3413c2a + languageName: node + linkType: hard + "deep-extend@npm:^0.6.0": version: 0.6.0 resolution: "deep-extend@npm:0.6.0" @@ -4098,7 +4650,7 @@ __metadata: languageName: node linkType: hard -"deepmerge@npm:^4.3.0": +"deepmerge@npm:^4.2.2, deepmerge@npm:^4.3.0": version: 4.3.1 resolution: "deepmerge@npm:4.3.1" checksum: 2024c6a980a1b7128084170c4cf56b0fd58a63f2da1660dcfe977415f27b17dbe5888668b59d0b063753f3220719d5e400b7f113609489c90160bb9a5518d052 @@ -4206,6 +4758,13 @@ __metadata: languageName: node linkType: hard +"detect-newline@npm:^3.0.0": + version: 3.1.0 + resolution: "detect-newline@npm:3.1.0" + checksum: ae6cd429c41ad01b164c59ea36f264a2c479598e61cba7c99da24175a7ab80ddf066420f2bec9a1c57a6bead411b4655ff15ad7d281c000a89791f48cbe939e7 + languageName: node + linkType: hard + "detox@npm:*, detox@npm:^20.14.7": version: 20.14.7 resolution: "detox@npm:20.14.7" @@ -4340,6 +4899,20 @@ __metadata: languageName: node linkType: hard +"electron-to-chromium@npm:^1.4.601": + version: 1.4.645 + resolution: "electron-to-chromium@npm:1.4.645" + checksum: ac7d23b8123f09e2343016216b1a8f297ccfb4ae9dccefe3716023344cda8a81656916d40a87039fa3d448cac31c2c4147c6b913b22178a3a00d0221a8019513 + languageName: node + linkType: hard + +"emittery@npm:^0.13.1": + version: 0.13.1 + resolution: "emittery@npm:0.13.1" + checksum: 2b089ab6306f38feaabf4f6f02792f9ec85fc054fda79f44f6790e61bbf6bc4e1616afb9b232e0c5ec5289a8a452f79bfa6d905a6fd64e94b49981f0934001c6 + languageName: node + linkType: hard + "emoji-regex@npm:^8.0.0": version: 8.0.0 resolution: "emoji-regex@npm:8.0.0" @@ -4556,7 +5129,14 @@ __metadata: languageName: node linkType: hard -"expect@npm:^29.0.0": +"exit@npm:^0.1.2": + version: 0.1.2 + resolution: "exit@npm:0.1.2" + checksum: abc407f07a875c3961e4781dfcb743b58d6c93de9ab263f4f8c9d23bb6da5f9b7764fc773f86b43dd88030444d5ab8abcb611cb680fba8ca075362b77114bba3 + languageName: node + linkType: hard + +"expect@npm:^29.0.0, expect@npm:^29.7.0": version: 29.7.0 resolution: "expect@npm:29.7.0" dependencies: @@ -4737,7 +5317,7 @@ __metadata: languageName: node linkType: hard -"fast-json-stable-stringify@npm:2.x": +"fast-json-stable-stringify@npm:2.x, fast-json-stable-stringify@npm:^2.1.0": version: 2.1.0 resolution: "fast-json-stable-stringify@npm:2.1.0" checksum: b191531e36c607977e5b1c47811158733c34ccb3bfde92c44798929e9b4154884378536d26ad90dfecd32e1ffc09c545d23535ad91b3161a27ddbb8ebe0cbecb @@ -4865,7 +5445,7 @@ __metadata: languageName: node linkType: hard -"find-up@npm:^4.1.0": +"find-up@npm:^4.0.0, find-up@npm:^4.1.0": version: 4.1.0 resolution: "find-up@npm:4.1.0" dependencies: @@ -5096,6 +5676,13 @@ __metadata: languageName: node linkType: hard +"get-package-type@npm:^0.1.0": + version: 0.1.0 + resolution: "get-package-type@npm:0.1.0" + checksum: bba0811116d11e56d702682ddef7c73ba3481f114590e705fc549f4d868972263896af313c57a25c076e3c0d567e11d919a64ba1b30c879be985fc9d44f96148 + languageName: node + linkType: hard + "get-port@npm:^3.2.0": version: 3.2.0 resolution: "get-port@npm:3.2.0" @@ -5339,6 +5926,13 @@ __metadata: languageName: node linkType: hard +"html-escaper@npm:^2.0.0": + version: 2.0.2 + resolution: "html-escaper@npm:2.0.2" + checksum: d2df2da3ad40ca9ee3a39c5cc6475ef67c8f83c234475f24d8e9ce0dc80a2c82df8e1d6fa78ddd1e9022a586ea1bd247a615e80a5cd9273d90111ddda7d9e974 + languageName: node + linkType: hard + "http-cache-semantics@npm:^4.1.1": version: 4.1.1 resolution: "http-cache-semantics@npm:4.1.1" @@ -5449,6 +6043,18 @@ __metadata: languageName: node linkType: hard +"import-local@npm:^3.0.2": + version: 3.1.0 + resolution: "import-local@npm:3.1.0" + dependencies: + pkg-dir: ^4.2.0 + resolve-cwd: ^3.0.0 + bin: + import-local-fixture: fixtures/cli.js + checksum: bfcdb63b5e3c0e245e347f3107564035b128a414c4da1172a20dc67db2504e05ede4ac2eee1252359f78b0bfd7b19ef180aec427c2fce6493ae782d73a04cddd + languageName: node + linkType: hard + "imurmurhash@npm:^0.1.4": version: 0.1.4 resolution: "imurmurhash@npm:0.1.4" @@ -5608,6 +6214,13 @@ __metadata: languageName: node linkType: hard +"is-generator-fn@npm:^2.0.0": + version: 2.1.0 + resolution: "is-generator-fn@npm:2.1.0" + checksum: a6ad5492cf9d1746f73b6744e0c43c0020510b59d56ddcb78a91cbc173f09b5e6beff53d75c9c5a29feb618bfef2bf458e025ecf3a57ad2268e2fb2569f56215 + languageName: node + linkType: hard + "is-glob@npm:^2.0.0": version: 2.0.1 resolution: "is-glob@npm:2.0.1" @@ -5753,23 +6366,191 @@ __metadata: languageName: node linkType: hard -"isobject@npm:^3.0.1": - version: 3.0.1 - resolution: "isobject@npm:3.0.1" - checksum: db85c4c970ce30693676487cca0e61da2ca34e8d4967c2e1309143ff910c207133a969f9e4ddb2dc6aba670aabce4e0e307146c310350b298e74a31f7d464703 +"isobject@npm:^3.0.1": + version: 3.0.1 + resolution: "isobject@npm:3.0.1" + checksum: db85c4c970ce30693676487cca0e61da2ca34e8d4967c2e1309143ff910c207133a969f9e4ddb2dc6aba670aabce4e0e307146c310350b298e74a31f7d464703 + languageName: node + linkType: hard + +"istanbul-lib-coverage@npm:^3.0.0, istanbul-lib-coverage@npm:^3.2.0": + version: 3.2.2 + resolution: "istanbul-lib-coverage@npm:3.2.2" + checksum: 2367407a8d13982d8f7a859a35e7f8dd5d8f75aae4bb5484ede3a9ea1b426dc245aff28b976a2af48ee759fdd9be374ce2bd2669b644f31e76c5f46a2e29a831 + languageName: node + linkType: hard + +"istanbul-lib-instrument@npm:^5.0.4": + version: 5.2.1 + resolution: "istanbul-lib-instrument@npm:5.2.1" + dependencies: + "@babel/core": ^7.12.3 + "@babel/parser": ^7.14.7 + "@istanbuljs/schema": ^0.1.2 + istanbul-lib-coverage: ^3.2.0 + semver: ^6.3.0 + checksum: bf16f1803ba5e51b28bbd49ed955a736488381e09375d830e42ddeb403855b2006f850711d95ad726f2ba3f1ae8e7366de7e51d2b9ac67dc4d80191ef7ddf272 + languageName: node + linkType: hard + +"istanbul-lib-instrument@npm:^6.0.0": + version: 6.0.1 + resolution: "istanbul-lib-instrument@npm:6.0.1" + dependencies: + "@babel/core": ^7.12.3 + "@babel/parser": ^7.14.7 + "@istanbuljs/schema": ^0.1.2 + istanbul-lib-coverage: ^3.2.0 + semver: ^7.5.4 + checksum: fb23472e739cfc9b027cefcd7d551d5e7ca7ff2817ae5150fab99fe42786a7f7b56a29a2aa8309c37092e18297b8003f9c274f50ca4360949094d17fbac81472 + languageName: node + linkType: hard + +"istanbul-lib-report@npm:^3.0.0": + version: 3.0.1 + resolution: "istanbul-lib-report@npm:3.0.1" + dependencies: + istanbul-lib-coverage: ^3.0.0 + make-dir: ^4.0.0 + supports-color: ^7.1.0 + checksum: fd17a1b879e7faf9bb1dc8f80b2a16e9f5b7b8498fe6ed580a618c34df0bfe53d2abd35bf8a0a00e628fb7405462576427c7df20bbe4148d19c14b431c974b21 + languageName: node + linkType: hard + +"istanbul-lib-source-maps@npm:^4.0.0": + version: 4.0.1 + resolution: "istanbul-lib-source-maps@npm:4.0.1" + dependencies: + debug: ^4.1.1 + istanbul-lib-coverage: ^3.0.0 + source-map: ^0.6.1 + checksum: 21ad3df45db4b81852b662b8d4161f6446cd250c1ddc70ef96a585e2e85c26ed7cd9c2a396a71533cfb981d1a645508bc9618cae431e55d01a0628e7dec62ef2 + languageName: node + linkType: hard + +"istanbul-reports@npm:^3.1.3": + version: 3.1.6 + resolution: "istanbul-reports@npm:3.1.6" + dependencies: + html-escaper: ^2.0.0 + istanbul-lib-report: ^3.0.0 + checksum: 44c4c0582f287f02341e9720997f9e82c071627e1e862895745d5f52ec72c9b9f38e1d12370015d2a71dcead794f34c7732aaef3fab80a24bc617a21c3d911d6 + languageName: node + linkType: hard + +"jackspeak@npm:^2.3.5": + version: 2.3.6 + resolution: "jackspeak@npm:2.3.6" + dependencies: + "@isaacs/cliui": ^8.0.2 + "@pkgjs/parseargs": ^0.11.0 + dependenciesMeta: + "@pkgjs/parseargs": + optional: true + checksum: 57d43ad11eadc98cdfe7496612f6bbb5255ea69fe51ea431162db302c2a11011642f50cfad57288bd0aea78384a0612b16e131944ad8ecd09d619041c8531b54 + languageName: node + linkType: hard + +"jest-changed-files@npm:^29.7.0": + version: 29.7.0 + resolution: "jest-changed-files@npm:29.7.0" + dependencies: + execa: ^5.0.0 + jest-util: ^29.7.0 + p-limit: ^3.1.0 + checksum: 963e203893c396c5dfc75e00a49426688efea7361b0f0e040035809cecd2d46b3c01c02be2d9e8d38b1138357d2de7719ea5b5be21f66c10f2e9685a5a73bb99 + languageName: node + linkType: hard + +"jest-circus@npm:^29.7.0": + version: 29.7.0 + resolution: "jest-circus@npm:29.7.0" + dependencies: + "@jest/environment": ^29.7.0 + "@jest/expect": ^29.7.0 + "@jest/test-result": ^29.7.0 + "@jest/types": ^29.6.3 + "@types/node": "*" + chalk: ^4.0.0 + co: ^4.6.0 + dedent: ^1.0.0 + is-generator-fn: ^2.0.0 + jest-each: ^29.7.0 + jest-matcher-utils: ^29.7.0 + jest-message-util: ^29.7.0 + jest-runtime: ^29.7.0 + jest-snapshot: ^29.7.0 + jest-util: ^29.7.0 + p-limit: ^3.1.0 + pretty-format: ^29.7.0 + pure-rand: ^6.0.0 + slash: ^3.0.0 + stack-utils: ^2.0.3 + checksum: 349437148924a5a109c9b8aad6d393a9591b4dac1918fc97d81b7fc515bc905af9918495055071404af1fab4e48e4b04ac3593477b1d5dcf48c4e71b527c70a7 + languageName: node + linkType: hard + +"jest-cli@npm:^29.7.0": + version: 29.7.0 + resolution: "jest-cli@npm:29.7.0" + dependencies: + "@jest/core": ^29.7.0 + "@jest/test-result": ^29.7.0 + "@jest/types": ^29.6.3 + chalk: ^4.0.0 + create-jest: ^29.7.0 + exit: ^0.1.2 + import-local: ^3.0.2 + jest-config: ^29.7.0 + jest-util: ^29.7.0 + jest-validate: ^29.7.0 + yargs: ^17.3.1 + peerDependencies: + node-notifier: ^8.0.1 || ^9.0.0 || ^10.0.0 + peerDependenciesMeta: + node-notifier: + optional: true + bin: + jest: bin/jest.js + checksum: 664901277a3f5007ea4870632ed6e7889db9da35b2434e7cb488443e6bf5513889b344b7fddf15112135495b9875892b156faeb2d7391ddb9e2a849dcb7b6c36 languageName: node linkType: hard -"jackspeak@npm:^2.3.5": - version: 2.3.6 - resolution: "jackspeak@npm:2.3.6" +"jest-config@npm:^29.7.0": + version: 29.7.0 + resolution: "jest-config@npm:29.7.0" dependencies: - "@isaacs/cliui": ^8.0.2 - "@pkgjs/parseargs": ^0.11.0 - dependenciesMeta: - "@pkgjs/parseargs": + "@babel/core": ^7.11.6 + "@jest/test-sequencer": ^29.7.0 + "@jest/types": ^29.6.3 + babel-jest: ^29.7.0 + chalk: ^4.0.0 + ci-info: ^3.2.0 + deepmerge: ^4.2.2 + glob: ^7.1.3 + graceful-fs: ^4.2.9 + jest-circus: ^29.7.0 + jest-environment-node: ^29.7.0 + jest-get-type: ^29.6.3 + jest-regex-util: ^29.6.3 + jest-resolve: ^29.7.0 + jest-runner: ^29.7.0 + jest-util: ^29.7.0 + jest-validate: ^29.7.0 + micromatch: ^4.0.4 + parse-json: ^5.2.0 + pretty-format: ^29.7.0 + slash: ^3.0.0 + strip-json-comments: ^3.1.1 + peerDependencies: + "@types/node": "*" + ts-node: ">=9.0.0" + peerDependenciesMeta: + "@types/node": optional: true - checksum: 57d43ad11eadc98cdfe7496612f6bbb5255ea69fe51ea431162db302c2a11011642f50cfad57288bd0aea78384a0612b16e131944ad8ecd09d619041c8531b54 + ts-node: + optional: true + checksum: 4cabf8f894c180cac80b7df1038912a3fc88f96f2622de33832f4b3314f83e22b08fb751da570c0ab2b7988f21604bdabade95e3c0c041068ac578c085cf7dff languageName: node linkType: hard @@ -5785,6 +6566,28 @@ __metadata: languageName: node linkType: hard +"jest-docblock@npm:^29.7.0": + version: 29.7.0 + resolution: "jest-docblock@npm:29.7.0" + dependencies: + detect-newline: ^3.0.0 + checksum: 66390c3e9451f8d96c5da62f577a1dad701180cfa9b071c5025acab2f94d7a3efc2515cfa1654ebe707213241541ce9c5530232cdc8017c91ed64eea1bd3b192 + languageName: node + linkType: hard + +"jest-each@npm:^29.7.0": + version: 29.7.0 + resolution: "jest-each@npm:29.7.0" + dependencies: + "@jest/types": ^29.6.3 + chalk: ^4.0.0 + jest-get-type: ^29.6.3 + jest-util: ^29.7.0 + pretty-format: ^29.7.0 + checksum: e88f99f0184000fc8813f2a0aa79e29deeb63700a3b9b7928b8a418d7d93cd24933608591dbbdea732b473eb2021c72991b5cc51a17966842841c6e28e6f691c + languageName: node + linkType: hard + "jest-environment-emit@npm:^1.0.5": version: 1.0.5 resolution: "jest-environment-emit@npm:1.0.5" @@ -5818,7 +6621,7 @@ __metadata: languageName: node linkType: hard -"jest-environment-node@npm:^29.2.1": +"jest-environment-node@npm:^29.2.1, jest-environment-node@npm:^29.7.0": version: 29.7.0 resolution: "jest-environment-node@npm:29.7.0" dependencies: @@ -5839,6 +6642,39 @@ __metadata: languageName: node linkType: hard +"jest-haste-map@npm:^29.7.0": + version: 29.7.0 + resolution: "jest-haste-map@npm:29.7.0" + dependencies: + "@jest/types": ^29.6.3 + "@types/graceful-fs": ^4.1.3 + "@types/node": "*" + anymatch: ^3.0.3 + fb-watchman: ^2.0.0 + fsevents: ^2.3.2 + graceful-fs: ^4.2.9 + jest-regex-util: ^29.6.3 + jest-util: ^29.7.0 + jest-worker: ^29.7.0 + micromatch: ^4.0.4 + walker: ^1.0.8 + dependenciesMeta: + fsevents: + optional: true + checksum: c2c8f2d3e792a963940fbdfa563ce14ef9e14d4d86da645b96d3cd346b8d35c5ce0b992ee08593939b5f718cf0a1f5a90011a056548a1dbf58397d4356786f01 + languageName: node + linkType: hard + +"jest-leak-detector@npm:^29.7.0": + version: 29.7.0 + resolution: "jest-leak-detector@npm:29.7.0" + dependencies: + jest-get-type: ^29.6.3 + pretty-format: ^29.7.0 + checksum: e3950e3ddd71e1d0c22924c51a300a1c2db6cf69ec1e51f95ccf424bcc070f78664813bef7aed4b16b96dfbdeea53fe358f8aeaaea84346ae15c3735758f1605 + languageName: node + linkType: hard + "jest-matcher-utils@npm:^29.7.0": version: 29.7.0 resolution: "jest-matcher-utils@npm:29.7.0" @@ -5879,6 +6715,18 @@ __metadata: languageName: node linkType: hard +"jest-pnp-resolver@npm:^1.2.2": + version: 1.2.3 + resolution: "jest-pnp-resolver@npm:1.2.3" + peerDependencies: + jest-resolve: "*" + peerDependenciesMeta: + jest-resolve: + optional: true + checksum: db1a8ab2cb97ca19c01b1cfa9a9c8c69a143fde833c14df1fab0766f411b1148ff0df878adea09007ac6a2085ec116ba9a996a6ad104b1e58c20adbf88eed9b2 + languageName: node + linkType: hard + "jest-regex-util@npm:^27.0.6": version: 27.5.1 resolution: "jest-regex-util@npm:27.5.1" @@ -5886,6 +6734,127 @@ __metadata: languageName: node linkType: hard +"jest-regex-util@npm:^29.6.3": + version: 29.6.3 + resolution: "jest-regex-util@npm:29.6.3" + checksum: 0518beeb9bf1228261695e54f0feaad3606df26a19764bc19541e0fc6e2a3737191904607fb72f3f2ce85d9c16b28df79b7b1ec9443aa08c3ef0e9efda6f8f2a + languageName: node + linkType: hard + +"jest-resolve-dependencies@npm:^29.7.0": + version: 29.7.0 + resolution: "jest-resolve-dependencies@npm:29.7.0" + dependencies: + jest-regex-util: ^29.6.3 + jest-snapshot: ^29.7.0 + checksum: aeb75d8150aaae60ca2bb345a0d198f23496494677cd6aefa26fc005faf354061f073982175daaf32b4b9d86b26ca928586344516e3e6969aa614cb13b883984 + languageName: node + linkType: hard + +"jest-resolve@npm:^29.7.0": + version: 29.7.0 + resolution: "jest-resolve@npm:29.7.0" + dependencies: + chalk: ^4.0.0 + graceful-fs: ^4.2.9 + jest-haste-map: ^29.7.0 + jest-pnp-resolver: ^1.2.2 + jest-util: ^29.7.0 + jest-validate: ^29.7.0 + resolve: ^1.20.0 + resolve.exports: ^2.0.0 + slash: ^3.0.0 + checksum: 0ca218e10731aa17920526ec39deaec59ab9b966237905ffc4545444481112cd422f01581230eceb7e82d86f44a543d520a71391ec66e1b4ef1a578bd5c73487 + languageName: node + linkType: hard + +"jest-runner@npm:^29.7.0": + version: 29.7.0 + resolution: "jest-runner@npm:29.7.0" + dependencies: + "@jest/console": ^29.7.0 + "@jest/environment": ^29.7.0 + "@jest/test-result": ^29.7.0 + "@jest/transform": ^29.7.0 + "@jest/types": ^29.6.3 + "@types/node": "*" + chalk: ^4.0.0 + emittery: ^0.13.1 + graceful-fs: ^4.2.9 + jest-docblock: ^29.7.0 + jest-environment-node: ^29.7.0 + jest-haste-map: ^29.7.0 + jest-leak-detector: ^29.7.0 + jest-message-util: ^29.7.0 + jest-resolve: ^29.7.0 + jest-runtime: ^29.7.0 + jest-util: ^29.7.0 + jest-watcher: ^29.7.0 + jest-worker: ^29.7.0 + p-limit: ^3.1.0 + source-map-support: 0.5.13 + checksum: f0405778ea64812bf9b5c50b598850d94ccf95d7ba21f090c64827b41decd680ee19fcbb494007cdd7f5d0d8906bfc9eceddd8fa583e753e736ecd462d4682fb + languageName: node + linkType: hard + +"jest-runtime@npm:^29.7.0": + version: 29.7.0 + resolution: "jest-runtime@npm:29.7.0" + dependencies: + "@jest/environment": ^29.7.0 + "@jest/fake-timers": ^29.7.0 + "@jest/globals": ^29.7.0 + "@jest/source-map": ^29.6.3 + "@jest/test-result": ^29.7.0 + "@jest/transform": ^29.7.0 + "@jest/types": ^29.6.3 + "@types/node": "*" + chalk: ^4.0.0 + cjs-module-lexer: ^1.0.0 + collect-v8-coverage: ^1.0.0 + glob: ^7.1.3 + graceful-fs: ^4.2.9 + jest-haste-map: ^29.7.0 + jest-message-util: ^29.7.0 + jest-mock: ^29.7.0 + jest-regex-util: ^29.6.3 + jest-resolve: ^29.7.0 + jest-snapshot: ^29.7.0 + jest-util: ^29.7.0 + slash: ^3.0.0 + strip-bom: ^4.0.0 + checksum: d19f113d013e80691e07047f68e1e3448ef024ff2c6b586ce4f90cd7d4c62a2cd1d460110491019719f3c59bfebe16f0e201ed005ef9f80e2cf798c374eed54e + languageName: node + linkType: hard + +"jest-snapshot@npm:^29.7.0": + version: 29.7.0 + resolution: "jest-snapshot@npm:29.7.0" + dependencies: + "@babel/core": ^7.11.6 + "@babel/generator": ^7.7.2 + "@babel/plugin-syntax-jsx": ^7.7.2 + "@babel/plugin-syntax-typescript": ^7.7.2 + "@babel/types": ^7.3.3 + "@jest/expect-utils": ^29.7.0 + "@jest/transform": ^29.7.0 + "@jest/types": ^29.6.3 + babel-preset-current-node-syntax: ^1.0.0 + chalk: ^4.0.0 + expect: ^29.7.0 + graceful-fs: ^4.2.9 + jest-diff: ^29.7.0 + jest-get-type: ^29.6.3 + jest-matcher-utils: ^29.7.0 + jest-message-util: ^29.7.0 + jest-util: ^29.7.0 + natural-compare: ^1.4.0 + pretty-format: ^29.7.0 + semver: ^7.5.3 + checksum: 86821c3ad0b6899521ce75ee1ae7b01b17e6dfeff9166f2cf17f012e0c5d8c798f30f9e4f8f7f5bed01ea7b55a6bc159f5eda778311162cbfa48785447c237ad + languageName: node + linkType: hard + "jest-util@npm:^27.2.0": version: 27.5.1 resolution: "jest-util@npm:27.5.1" @@ -5914,7 +6883,7 @@ __metadata: languageName: node linkType: hard -"jest-validate@npm:^29.2.1": +"jest-validate@npm:^29.2.1, jest-validate@npm:^29.7.0": version: 29.7.0 resolution: "jest-validate@npm:29.7.0" dependencies: @@ -5928,6 +6897,22 @@ __metadata: languageName: node linkType: hard +"jest-watcher@npm:^29.7.0": + version: 29.7.0 + resolution: "jest-watcher@npm:29.7.0" + dependencies: + "@jest/test-result": ^29.7.0 + "@jest/types": ^29.6.3 + "@types/node": "*" + ansi-escapes: ^4.2.1 + chalk: ^4.0.0 + emittery: ^0.13.1 + jest-util: ^29.7.0 + string-length: ^4.0.1 + checksum: 67e6e7fe695416deff96b93a14a561a6db69389a0667e9489f24485bb85e5b54e12f3b2ba511ec0b777eca1e727235b073e3ebcdd473d68888650489f88df92f + languageName: node + linkType: hard + "jest-worker@npm:^27.2.0": version: 27.5.1 resolution: "jest-worker@npm:27.5.1" @@ -5939,6 +6924,37 @@ __metadata: languageName: node linkType: hard +"jest-worker@npm:^29.7.0": + version: 29.7.0 + resolution: "jest-worker@npm:29.7.0" + dependencies: + "@types/node": "*" + jest-util: ^29.7.0 + merge-stream: ^2.0.0 + supports-color: ^8.0.0 + checksum: 30fff60af49675273644d408b650fc2eb4b5dcafc5a0a455f238322a8f9d8a98d847baca9d51ff197b6747f54c7901daa2287799230b856a0f48287d131f8c13 + languageName: node + linkType: hard + +"jest@npm:^29.7.0": + version: 29.7.0 + resolution: "jest@npm:29.7.0" + dependencies: + "@jest/core": ^29.7.0 + "@jest/types": ^29.6.3 + import-local: ^3.0.2 + jest-cli: ^29.7.0 + peerDependencies: + node-notifier: ^8.0.1 || ^9.0.0 || ^10.0.0 + peerDependenciesMeta: + node-notifier: + optional: true + bin: + jest: bin/jest.js + checksum: 17ca8d67504a7dbb1998cf3c3077ec9031ba3eb512da8d71cb91bcabb2b8995c4e4b292b740cb9bf1cbff5ce3e110b3f7c777b0cefb6f41ab05445f248d0ee0b + languageName: node + linkType: hard + "jimp-compact@npm:0.16.1": version: 0.16.1 resolution: "jimp-compact@npm:0.16.1" @@ -6089,6 +7105,13 @@ __metadata: languageName: node linkType: hard +"json-parse-even-better-errors@npm:^2.3.0": + version: 2.3.1 + resolution: "json-parse-even-better-errors@npm:2.3.1" + checksum: 798ed4cf3354a2d9ccd78e86d2169515a0097a5c133337807cdf7f1fc32e1391d207ccfc276518cc1d7d8d4db93288b8a50ba4293d212ad1336e52a8ec0a941f + languageName: node + linkType: hard + "json-schema-deref-sync@npm:^0.13.0": version: 0.13.0 resolution: "json-schema-deref-sync@npm:0.13.0" @@ -6415,6 +7438,15 @@ __metadata: languageName: node linkType: hard +"make-dir@npm:^4.0.0": + version: 4.0.0 + resolution: "make-dir@npm:4.0.0" + dependencies: + semver: ^7.5.3 + checksum: bf0731a2dd3aab4db6f3de1585cea0b746bb73eb5a02e3d8d72757e376e64e6ada190b1eddcde5b2f24a81b688a9897efd5018737d05e02e2a671dda9cff8a8a + languageName: node + linkType: hard + "make-error@npm:1.x": version: 1.3.6 resolution: "make-error@npm:1.3.6" @@ -7138,6 +8170,13 @@ __metadata: languageName: node linkType: hard +"natural-compare@npm:^1.4.0": + version: 1.4.0 + resolution: "natural-compare@npm:1.4.0" + checksum: 23ad088b08f898fc9b53011d7bb78ec48e79de7627e01ab5518e806033861bef68d5b0cd0e2205c2f36690ac9571ff6bcb05eb777ced2eeda8d4ac5b44592c3d + languageName: node + linkType: hard + "ncp@npm:~2.0.0": version: 2.0.0 resolution: "ncp@npm:2.0.0" @@ -7264,6 +8303,13 @@ __metadata: languageName: node linkType: hard +"node-releases@npm:^2.0.14": + version: 2.0.14 + resolution: "node-releases@npm:2.0.14" + checksum: 59443a2f77acac854c42d321bf1b43dea0aef55cd544c6a686e9816a697300458d4e82239e2d794ea05f7bbbc8a94500332e2d3ac3f11f52e4b16cbe638b3c41 + languageName: node + linkType: hard + "node-stream-zip@npm:^1.9.1": version: 1.15.0 resolution: "node-stream-zip@npm:1.15.0" @@ -7497,7 +8543,7 @@ __metadata: languageName: node linkType: hard -"p-limit@npm:^3.0.2": +"p-limit@npm:^3.0.2, p-limit@npm:^3.1.0": version: 3.1.0 resolution: "p-limit@npm:3.1.0" dependencies: @@ -7559,6 +8605,18 @@ __metadata: languageName: node linkType: hard +"parse-json@npm:^5.2.0": + version: 5.2.0 + resolution: "parse-json@npm:5.2.0" + dependencies: + "@babel/code-frame": ^7.0.0 + error-ex: ^1.3.1 + json-parse-even-better-errors: ^2.3.0 + lines-and-columns: ^1.1.6 + checksum: 62085b17d64da57f40f6afc2ac1f4d95def18c4323577e1eced571db75d9ab59b297d1d10582920f84b15985cbfc6b6d450ccbf317644cfa176f3ed982ad87e2 + languageName: node + linkType: hard + "parse-png@npm:^2.1.0": version: 2.1.0 resolution: "parse-png@npm:2.1.0" @@ -7672,7 +8730,7 @@ __metadata: languageName: node linkType: hard -"pirates@npm:^4.0.1, pirates@npm:^4.0.5": +"pirates@npm:^4.0.1, pirates@npm:^4.0.4, pirates@npm:^4.0.5": version: 4.0.6 resolution: "pirates@npm:4.0.6" checksum: 46a65fefaf19c6f57460388a5af9ab81e3d7fd0e7bc44ca59d753cb5c4d0df97c6c6e583674869762101836d68675f027d60f841c105d72734df9dfca97cbcc6 @@ -7688,6 +8746,15 @@ __metadata: languageName: node linkType: hard +"pkg-dir@npm:^4.2.0": + version: 4.2.0 + resolution: "pkg-dir@npm:4.2.0" + dependencies: + find-up: ^4.0.0 + checksum: 9863e3f35132bf99ae1636d31ff1e1e3501251d480336edb1c211133c8d58906bed80f154a1d723652df1fda91e01c7442c2eeaf9dc83157c7ae89087e43c8d6 + languageName: node + linkType: hard + "pkg-up@npm:^3.1.0": version: 3.1.0 resolution: "pkg-up@npm:3.1.0" @@ -7819,7 +8886,7 @@ __metadata: languageName: node linkType: hard -"prompts@npm:^2.3.2, prompts@npm:^2.4.0": +"prompts@npm:^2.0.1, prompts@npm:^2.3.2, prompts@npm:^2.4.0": version: 2.4.2 resolution: "prompts@npm:2.4.2" dependencies: @@ -7875,6 +8942,13 @@ __metadata: languageName: node linkType: hard +"pure-rand@npm:^6.0.0": + version: 6.0.4 + resolution: "pure-rand@npm:6.0.4" + checksum: e1c4e69f8bf7303e5252756d67c3c7551385cd34d94a1f511fe099727ccbab74c898c03a06d4c4a24a89b51858781057b83ebbfe740d984240cdc04fead36068 + languageName: node + linkType: hard + "qrcode-terminal@npm:0.11.0": version: 0.11.0 resolution: "qrcode-terminal@npm:0.11.0" @@ -8006,6 +9080,7 @@ __metadata: expo: ~49.0.16 expo-splash-screen: ~0.20.5 expo-status-bar: ~1.7.1 + jest: ^29.7.0 react: 18.2.0 react-native: 0.72.6 react-native-dotenv: ^3.4.9 @@ -8252,6 +9327,15 @@ __metadata: languageName: node linkType: hard +"resolve-cwd@npm:^3.0.0": + version: 3.0.0 + resolution: "resolve-cwd@npm:3.0.0" + dependencies: + resolve-from: ^5.0.0 + checksum: 546e0816012d65778e580ad62b29e975a642989108d9a3c5beabfb2304192fa3c9f9146fbdfe213563c6ff51975ae41bac1d3c6e047dd9572c94863a057b4d81 + languageName: node + linkType: hard + "resolve-from@npm:^3.0.0": version: 3.0.0 resolution: "resolve-from@npm:3.0.0" @@ -8266,7 +9350,14 @@ __metadata: languageName: node linkType: hard -"resolve@npm:^1.14.2, resolve@npm:^1.22.1": +"resolve.exports@npm:^2.0.0": + version: 2.0.2 + resolution: "resolve.exports@npm:2.0.2" + checksum: 1c7778ca1b86a94f8ab4055d196c7d87d1874b96df4d7c3e67bbf793140f0717fd506dcafd62785b079cd6086b9264424ad634fb904409764c3509c3df1653f2 + languageName: node + linkType: hard + +"resolve@npm:^1.14.2, resolve@npm:^1.20.0, resolve@npm:^1.22.1": version: 1.22.8 resolution: "resolve@npm:1.22.8" dependencies: @@ -8288,7 +9379,7 @@ __metadata: languageName: node linkType: hard -"resolve@patch:resolve@^1.14.2#~builtin, resolve@patch:resolve@^1.22.1#~builtin": +"resolve@patch:resolve@^1.14.2#~builtin, resolve@patch:resolve@^1.20.0#~builtin, resolve@patch:resolve@^1.22.1#~builtin": version: 1.22.8 resolution: "resolve@patch:resolve@npm%3A1.22.8#~builtin::version=1.22.8&hash=c3c19d" dependencies: @@ -8479,7 +9570,7 @@ __metadata: languageName: node linkType: hard -"semver@npm:^6.3.1": +"semver@npm:^6.3.0, semver@npm:^6.3.1": version: 6.3.1 resolution: "semver@npm:6.3.1" bin: @@ -8488,7 +9579,7 @@ __metadata: languageName: node linkType: hard -"semver@npm:^7.0.0, semver@npm:^7.3.5, semver@npm:^7.5.2, semver@npm:^7.5.3": +"semver@npm:^7.0.0, semver@npm:^7.3.5, semver@npm:^7.5.2, semver@npm:^7.5.3, semver@npm:^7.5.4": version: 7.5.4 resolution: "semver@npm:7.5.4" dependencies: @@ -8649,7 +9740,7 @@ __metadata: languageName: node linkType: hard -"signal-exit@npm:^3.0.0, signal-exit@npm:^3.0.2, signal-exit@npm:^3.0.3": +"signal-exit@npm:^3.0.0, signal-exit@npm:^3.0.2, signal-exit@npm:^3.0.3, signal-exit@npm:^3.0.7": version: 3.0.7 resolution: "signal-exit@npm:3.0.7" checksum: a2f098f247adc367dffc27845853e9959b9e88b01cb301658cfe4194352d8d2bb32e18467c786a7fe15f1d44b233ea35633d076d5e737870b7139949d1ab6318 @@ -8741,6 +9832,16 @@ __metadata: languageName: node linkType: hard +"source-map-support@npm:0.5.13": + version: 0.5.13 + resolution: "source-map-support@npm:0.5.13" + dependencies: + buffer-from: ^1.0.0 + source-map: ^0.6.0 + checksum: 933550047b6c1a2328599a21d8b7666507427c0f5ef5eaadd56b5da0fd9505e239053c66fe181bf1df469a3b7af9d775778eee283cbb7ae16b902ddc09e93a97 + languageName: node + linkType: hard + "source-map-support@npm:^0.5.16, source-map-support@npm:~0.5.20": version: 0.5.21 resolution: "source-map-support@npm:0.5.21" @@ -8758,7 +9859,7 @@ __metadata: languageName: node linkType: hard -"source-map@npm:^0.6.0, source-map@npm:~0.6.1": +"source-map@npm:^0.6.0, source-map@npm:^0.6.1, source-map@npm:~0.6.1": version: 0.6.1 resolution: "source-map@npm:0.6.1" checksum: 59ce8640cf3f3124f64ac289012c2b8bd377c238e316fb323ea22fbfe83da07d81e000071d7242cad7a23cd91c7de98e4df8830ec3f133cb6133a5f6e9f67bc2 @@ -8868,6 +9969,16 @@ __metadata: languageName: node linkType: hard +"string-length@npm:^4.0.1": + version: 4.0.2 + resolution: "string-length@npm:4.0.2" + dependencies: + char-regex: ^1.0.2 + strip-ansi: ^6.0.0 + checksum: ce85533ef5113fcb7e522bcf9e62cb33871aa99b3729cec5595f4447f660b0cefd542ca6df4150c97a677d58b0cb727a3fe09ac1de94071d05526c73579bf505 + languageName: node + linkType: hard + "string-width-cjs@npm:string-width@^4.2.0, string-width@npm:^4.1.0, string-width@npm:^4.2.0, string-width@npm:^4.2.3": version: 4.2.3 resolution: "string-width@npm:4.2.3" @@ -8935,6 +10046,13 @@ __metadata: languageName: node linkType: hard +"strip-bom@npm:^4.0.0": + version: 4.0.0 + resolution: "strip-bom@npm:4.0.0" + checksum: 9dbcfbaf503c57c06af15fe2c8176fb1bf3af5ff65003851a102749f875a6dbe0ab3b30115eccf6e805e9d756830d3e40ec508b62b3f1ddf3761a20ebe29d3f3 + languageName: node + linkType: hard + "strip-eof@npm:^1.0.0": version: 1.0.0 resolution: "strip-eof@npm:1.0.0" @@ -8949,6 +10067,13 @@ __metadata: languageName: node linkType: hard +"strip-json-comments@npm:^3.1.1": + version: 3.1.1 + resolution: "strip-json-comments@npm:3.1.1" + checksum: 492f73e27268f9b1c122733f28ecb0e7e8d8a531a6662efbd08e22cccb3f9475e90a1b82cab06a392f6afae6d2de636f977e231296400d0ec5304ba70f166443 + languageName: node + linkType: hard + "strip-json-comments@npm:~2.0.1": version: 2.0.1 resolution: "strip-json-comments@npm:2.0.1" @@ -9157,6 +10282,17 @@ __metadata: languageName: node linkType: hard +"test-exclude@npm:^6.0.0": + version: 6.0.0 + resolution: "test-exclude@npm:6.0.0" + dependencies: + "@istanbuljs/schema": ^0.1.2 + glob: ^7.1.4 + minimatch: ^3.0.4 + checksum: 3b34a3d77165a2cb82b34014b3aba93b1c4637a5011807557dc2f3da826c59975a5ccad765721c4648b39817e3472789f9b0fa98fc854c5c1c7a1e632aacdc28 + languageName: node + linkType: hard + "text-table@npm:^0.2.0": version: 0.2.0 resolution: "text-table@npm:0.2.0" @@ -9639,6 +10775,17 @@ __metadata: languageName: node linkType: hard +"v8-to-istanbul@npm:^9.0.1": + version: 9.2.0 + resolution: "v8-to-istanbul@npm:9.2.0" + dependencies: + "@jridgewell/trace-mapping": ^0.3.12 + "@types/istanbul-lib-coverage": ^2.0.1 + convert-source-map: ^2.0.0 + checksum: 31ef98c6a31b1dab6be024cf914f235408cd4c0dc56a5c744a5eea1a9e019ba279e1b6f90d695b78c3186feed391ed492380ccf095009e2eb91f3d058f0b4491 + languageName: node + linkType: hard + "valid-url@npm:~1.0.9": version: 1.0.9 resolution: "valid-url@npm:1.0.9" @@ -9669,7 +10816,7 @@ __metadata: languageName: node linkType: hard -"walker@npm:^1.0.7": +"walker@npm:^1.0.7, walker@npm:^1.0.8": version: 1.0.8 resolution: "walker@npm:1.0.8" dependencies: @@ -9816,6 +10963,16 @@ __metadata: languageName: node linkType: hard +"write-file-atomic@npm:^4.0.2": + version: 4.0.2 + resolution: "write-file-atomic@npm:4.0.2" + dependencies: + imurmurhash: ^0.1.4 + signal-exit: ^3.0.7 + checksum: 5da60bd4eeeb935eec97ead3df6e28e5917a6bd317478e4a85a5285e8480b8ed96032bbcc6ecd07b236142a24f3ca871c924ec4a6575e623ec1b11bf8c1c253c + languageName: node + linkType: hard + "ws@npm:^6.2.2": version: 6.2.2 resolution: "ws@npm:6.2.2" @@ -9993,7 +11150,7 @@ __metadata: languageName: node linkType: hard -"yargs@npm:^17.0.0, yargs@npm:^17.6.2": +"yargs@npm:^17.0.0, yargs@npm:^17.3.1, yargs@npm:^17.6.2": version: 17.7.2 resolution: "yargs@npm:17.7.2" dependencies: From 6a4604fc93e1d9bb55d818a2266b72454f359c80 Mon Sep 17 00:00:00 2001 From: Yusinto Ngadiman Date: Wed, 24 Jan 2024 15:57:26 -0800 Subject: [PATCH 43/44] Update autoEnv.ts --- packages/sdk/react-native/src/platform/autoEnv.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/sdk/react-native/src/platform/autoEnv.ts b/packages/sdk/react-native/src/platform/autoEnv.ts index bbbb5991c..728841020 100644 --- a/packages/sdk/react-native/src/platform/autoEnv.ts +++ b/packages/sdk/react-native/src/platform/autoEnv.ts @@ -5,6 +5,7 @@ import type { LDApplication, LDDevice } from '@launchdarkly/js-sdk-common'; import locale from './locale'; export const ldApplication: LDApplication = { + // key is populated by client common sdk key: '', envAttributesVersion: '1.0', @@ -17,6 +18,7 @@ export const ldApplication: LDApplication = { }; export const ldDevice: LDDevice = { + // key is populated by client common sdk key: '', envAttributesVersion: '1.0', manufacturer: Platform.select({ From 8e2382ee1c69b9af5caf900fd6976d2241b4e264 Mon Sep 17 00:00:00 2001 From: Yusinto Ngadiman Date: Wed, 24 Jan 2024 16:07:26 -0800 Subject: [PATCH 44/44] chore: Revert test data to look less real. --- .../diagnostics/DiagnosticsManager.test.ts | 6 ++-- packages/shared/mocks/src/platform.ts | 10 +++--- .../sdk-client/src/LDClientImpl.test.ts | 4 +-- .../sdk-client/src/utils/addAutoEnv.test.ts | 32 ++++++------------- 4 files changed, 20 insertions(+), 32 deletions(-) diff --git a/packages/shared/common/src/internal/diagnostics/DiagnosticsManager.test.ts b/packages/shared/common/src/internal/diagnostics/DiagnosticsManager.test.ts index 6c69b1cb7..c275484aa 100644 --- a/packages/shared/common/src/internal/diagnostics/DiagnosticsManager.test.ts +++ b/packages/shared/common/src/internal/diagnostics/DiagnosticsManager.test.ts @@ -58,9 +58,9 @@ describe('given a diagnostics manager', () => { const { platform } = manager.createInitEvent(); expect(platform).toEqual({ name: 'The SDK Name', - osName: 'iOS', - osVersion: '17.17', - osArch: 'ARM64', + osName: 'An OS', + osVersion: '1.0.1', + osArch: 'An Arch', nodeVersion: '42', }); }); diff --git a/packages/shared/mocks/src/platform.ts b/packages/shared/mocks/src/platform.ts index 759bf5f1a..24bcc0016 100644 --- a/packages/shared/mocks/src/platform.ts +++ b/packages/shared/mocks/src/platform.ts @@ -10,9 +10,9 @@ const setupInfo = () => ({ platformData: jest.fn( (): PlatformData => ({ os: { - name: 'iOS', - version: '17.17', - arch: 'ARM64', + name: 'An OS', + version: '1.0.1', + arch: 'An Arch', }, name: 'The SDK Name', additional: { @@ -28,8 +28,8 @@ const setupInfo = () => ({ ld_device: { key: '', envAttributesVersion: '1.0', - os: { name: 'ios', version: '17', family: 'apple' }, - manufacturer: 'apple', + os: { name: 'Another OS', version: '99', family: 'orange' }, + manufacturer: 'coconut', }, }), ), diff --git a/packages/shared/sdk-client/src/LDClientImpl.test.ts b/packages/shared/sdk-client/src/LDClientImpl.test.ts index 4097497ef..1859997ce 100644 --- a/packages/shared/sdk-client/src/LDClientImpl.test.ts +++ b/packages/shared/sdk-client/src/LDClientImpl.test.ts @@ -37,8 +37,8 @@ const autoEnv = { ld_device: { key: 'random1', envAttributesVersion: '1.0', - manufacturer: 'apple', - os: { family: 'apple', name: 'iOS', version: '17.17' }, + manufacturer: 'coconut', + os: { name: 'An OS', version: '1.0.1', family: 'orange' }, }, }; let ldc: LDClientImpl; diff --git a/packages/shared/sdk-client/src/utils/addAutoEnv.test.ts b/packages/shared/sdk-client/src/utils/addAutoEnv.test.ts index d1ea1ac43..fec9407dc 100644 --- a/packages/shared/sdk-client/src/utils/addAutoEnv.test.ts +++ b/packages/shared/sdk-client/src/utils/addAutoEnv.test.ts @@ -51,8 +51,8 @@ describe('addAutoEnv', () => { ld_device: { envAttributesVersion: '1.0', key: 'test-device-key-1', - manufacturer: 'apple', - os: { family: 'apple', name: 'iOS', version: '17.17' }, + manufacturer: 'coconut', + os: { name: 'An OS', version: '1.0.1', family: 'orange' }, }, user: { key: 'test-user-key-1', name: 'bob' }, }); @@ -81,8 +81,8 @@ describe('addAutoEnv', () => { ld_device: { envAttributesVersion: '1.0', key: 'test-device-key-1', - manufacturer: 'apple', - os: { family: 'apple', name: 'iOS', version: '17.17' }, + manufacturer: 'coconut', + os: { name: 'An OS', version: '1.0.1', family: 'orange' }, }, }); }); @@ -171,12 +171,8 @@ describe('addAutoEnv', () => { expect(ldDevice).toEqual({ envAttributesVersion: '1.0', key: 'test-device-key-1', - manufacturer: 'apple', - os: { - family: 'apple', - name: 'iOS', - version: '17.17', - }, + manufacturer: 'coconut', + os: { name: 'An OS', version: '1.0.1', family: 'orange' }, }); }); @@ -190,12 +186,8 @@ describe('addAutoEnv', () => { expect(ldDevice).toEqual({ envAttributesVersion: '1.0', key: 'test-device-key-1', - manufacturer: 'apple', - os: { - family: 'apple', - name: 'ios', - version: '17', - }, + manufacturer: 'coconut', + os: { name: 'Another OS', version: '99', family: 'orange' }, }); }); @@ -209,12 +201,8 @@ describe('addAutoEnv', () => { expect(ldDevice).toEqual({ envAttributesVersion: '1.0', key: 'test-device-key-1', - manufacturer: 'apple', - os: { - family: 'apple', - name: 'ios', - version: '17', - }, + manufacturer: 'coconut', + os: { name: 'Another OS', version: '99', family: 'orange' }, }); }); });