diff --git a/packages/client-http/src/client/Configuration.ts b/packages/client-http/src/client/Configuration.ts index 15950558..c0c451ed 100644 --- a/packages/client-http/src/client/Configuration.ts +++ b/packages/client-http/src/client/Configuration.ts @@ -6,6 +6,7 @@ export namespace Configuration { Match = 'RESOLVE_REASON_MATCH', NoSegmentMatch = 'RESOLVE_REASON_NO_SEGMENT_MATCH', NoTreatmentMatch = 'RESOLVE_REASON_NO_TREATMENT_MATCH', + TargetingKeyError = 'RESOLVE_REASON_TARGETING_KEY_ERROR', Archived = 'RESOLVE_REASON_FLAG_ARCHIVED', } diff --git a/packages/openfeature-server-provider/src/ConfidenceServerProvider.test.ts b/packages/openfeature-server-provider/src/ConfidenceServerProvider.test.ts index 714c09bb..aee18322 100644 --- a/packages/openfeature-server-provider/src/ConfidenceServerProvider.test.ts +++ b/packages/openfeature-server-provider/src/ConfidenceServerProvider.test.ts @@ -1,4 +1,11 @@ -import { ErrorCode, Logger, ProviderStatus } from '@openfeature/web-sdk'; +import { + FlagNotFoundError, + Logger, + ParseError, + ProviderStatus, + TypeMismatchError, + InvalidContextError, +} from '@openfeature/js-sdk'; import { ConfidenceClient, Configuration } from '@spotify-confidence/client-http'; import { ConfidenceServerProvider } from './ConfidenceServerProvider'; @@ -65,6 +72,13 @@ const dummyConfiguration: Configuration = { value: undefined, schema: 'undefined', }, + ['targeting-error-flag']: { + name: 'targeting-error-flag', + variant: '', + reason: Configuration.ResolveReason.TargetingKeyError, + value: { enabled: true }, + schema: { enabled: 'boolean' }, + }, }, resolveToken: 'before-each', context: {}, @@ -94,6 +108,12 @@ describe('ConfidenceServerProvider', () => { expect(resolveMock).toHaveBeenCalledTimes(2); }); + it('should throw invalid context error when the reason from confidence is targeting key error', async () => { + expect(() => + instanceUnderTest.resolveBooleanEvaluation('targeting-error-flag.enabled', false, {}, dummyConsole), + ).rejects.toThrow(InvalidContextError); + }); + describe('resolveBooleanEvaluation', () => { it('should resolve a boolean', async () => { expect(await instanceUnderTest.resolveBooleanEvaluation('testFlag.bool', false, {}, dummyConsole)).toEqual({ @@ -125,33 +145,21 @@ describe('ConfidenceServerProvider', () => { }); it('should return default if the flag is not found', async () => { - const actual = await instanceUnderTest.resolveBooleanEvaluation('notARealFlag.bool', false, {}, dummyConsole); - - expect(actual).toEqual({ - value: false, - errorCode: ErrorCode.FLAG_NOT_FOUND, - reason: 'ERROR', - }); + expect(() => + instanceUnderTest.resolveBooleanEvaluation('notARealFlag.bool', false, {}, dummyConsole), + ).rejects.toThrow(new FlagNotFoundError('Flag "notARealFlag" was not found')); }); it('should return default if the flag requested is the wrong type', async () => { - const actual = await instanceUnderTest.resolveBooleanEvaluation('testFlag.str', false, {}, dummyConsole); - - expect(actual).toEqual({ - value: false, - errorCode: ErrorCode.TYPE_MISMATCH, - reason: 'ERROR', - }); + expect(() => instanceUnderTest.resolveBooleanEvaluation('testFlag.str', false, {}, dummyConsole)).rejects.toThrow( + TypeMismatchError, + ); }); it('should return default if the value requested is not in the flag schema', async () => { - const actual = await instanceUnderTest.resolveBooleanEvaluation('testFlag.404', false, {}, dummyConsole); - - expect(actual).toEqual({ - value: false, - errorCode: ErrorCode.PARSE_ERROR, - reason: 'ERROR', - }); + expect(() => instanceUnderTest.resolveBooleanEvaluation('testFlag.404', false, {}, dummyConsole)).rejects.toThrow( + ParseError, + ); }); }); @@ -201,33 +209,21 @@ describe('ConfidenceServerProvider', () => { }); it('should return default if the flag is not found', async () => { - const actual = await instanceUnderTest.resolveNumberEvaluation('notARealFlag.int', 1, {}, dummyConsole); - - expect(actual).toEqual({ - value: 1, - errorCode: ErrorCode.FLAG_NOT_FOUND, - reason: 'ERROR', - }); + expect(() => instanceUnderTest.resolveNumberEvaluation('notARealFlag.int', 1, {}, dummyConsole)).rejects.toThrow( + FlagNotFoundError, + ); }); it('should return default if the flag requested is the wrong type', async () => { - const actual = await instanceUnderTest.resolveNumberEvaluation('testFlag.str', 1, {}, dummyConsole); - - expect(actual).toEqual({ - value: 1, - errorCode: ErrorCode.TYPE_MISMATCH, - reason: 'ERROR', - }); + expect(() => instanceUnderTest.resolveNumberEvaluation('testFlag.str', 1, {}, dummyConsole)).rejects.toThrow( + TypeMismatchError, + ); }); it('should return default if the value requested is not in the flag schema', async () => { - const actual = await instanceUnderTest.resolveNumberEvaluation('testFlag.404', 1, {}, dummyConsole); - - expect(actual).toEqual({ - value: 1, - errorCode: ErrorCode.PARSE_ERROR, - reason: 'ERROR', - }); + expect(() => instanceUnderTest.resolveNumberEvaluation('testFlag.404', 1, {}, dummyConsole)).rejects.toThrow( + ParseError, + ); }); }); @@ -255,43 +251,27 @@ describe('ConfidenceServerProvider', () => { }); it('should return default if the flag is not found', async () => { - const actual = await instanceUnderTest.resolveStringEvaluation('notARealFlag.str', 'default', {}, dummyConsole); - - expect(actual).toEqual({ - value: 'default', - errorCode: ErrorCode.FLAG_NOT_FOUND, - reason: 'ERROR', - }); + expect(() => + instanceUnderTest.resolveStringEvaluation('notARealFlag.str', 'default', {}, dummyConsole), + ).rejects.toThrow(new FlagNotFoundError('Flag "notARealFlag" was not found')); }); it('should return default if the flag requested is the wrong type', async () => { - const actual = await instanceUnderTest.resolveStringEvaluation('testFlag.int', 'default', {}, dummyConsole); - - expect(actual).toEqual({ - value: 'default', - errorCode: ErrorCode.TYPE_MISMATCH, - reason: 'ERROR', - }); + expect(() => + instanceUnderTest.resolveStringEvaluation('testFlag.int', 'default', {}, dummyConsole), + ).rejects.toThrow(TypeMismatchError); }); it('should return default if the flag requested is the wrong type from nested obj', async () => { - const actual = await instanceUnderTest.resolveStringEvaluation('testFlag.obj.int', 'default', {}, dummyConsole); - - expect(actual).toEqual({ - value: 'default', - errorCode: ErrorCode.TYPE_MISMATCH, - reason: 'ERROR', - }); + expect(() => + instanceUnderTest.resolveStringEvaluation('testFlag.obj.int', 'default', {}, dummyConsole), + ).rejects.toThrow(TypeMismatchError); }); it('should return default if the value requested is not in the flag schema', async () => { - const actual = await instanceUnderTest.resolveStringEvaluation('testFlag.404', 'default', {}, dummyConsole); - - expect(actual).toEqual({ - value: 'default', - errorCode: ErrorCode.PARSE_ERROR, - reason: 'ERROR', - }); + expect(() => + instanceUnderTest.resolveStringEvaluation('testFlag.404', 'default', {}, dummyConsole), + ).rejects.toThrow(ParseError); }); }); @@ -338,8 +318,8 @@ describe('ConfidenceServerProvider', () => { }); it('should resolve a full object with type mismatch default', async () => { - expect( - await instanceUnderTest.resolveObjectEvaluation( + expect(() => + instanceUnderTest.resolveObjectEvaluation( 'testFlag.obj', { testBool: false, @@ -347,43 +327,25 @@ describe('ConfidenceServerProvider', () => { {}, dummyConsole, ), - ).toEqual({ - errorCode: 'TYPE_MISMATCH', - reason: 'ERROR', - value: { - testBool: false, - }, - }); + ).rejects.toThrow(TypeMismatchError); }); it('should return default if the flag is not found', async () => { - const actual = await instanceUnderTest.resolveObjectEvaluation('notARealFlag.obj', {}, {}, dummyConsole); - - expect(actual).toEqual({ - value: {}, - errorCode: ErrorCode.FLAG_NOT_FOUND, - reason: 'ERROR', - }); + expect(() => instanceUnderTest.resolveObjectEvaluation('notARealFlag.obj', {}, {}, dummyConsole)).rejects.toThrow( + new FlagNotFoundError('Flag "notARealFlag" was not found'), + ); }); it('should return default if the flag requested is the wrong type', async () => { - const actual = await instanceUnderTest.resolveObjectEvaluation('testFlag.str', {}, {}, dummyConsole); - - expect(actual).toEqual({ - value: {}, - errorCode: ErrorCode.TYPE_MISMATCH, - reason: 'ERROR', - }); + expect(() => instanceUnderTest.resolveObjectEvaluation('testFlag.str', {}, {}, dummyConsole)).rejects.toThrow( + TypeMismatchError, + ); }); it('should return default if the value requested is not in the flag schema', async () => { - const actual = await instanceUnderTest.resolveObjectEvaluation('testFlag.404', {}, {}, dummyConsole); - - expect(actual).toEqual({ - value: {}, - errorCode: ErrorCode.PARSE_ERROR, - reason: 'ERROR', - }); + expect(() => instanceUnderTest.resolveObjectEvaluation('testFlag.404', {}, {}, dummyConsole)).rejects.toThrow( + ParseError, + ); }); }); }); diff --git a/packages/openfeature-server-provider/src/ConfidenceServerProvider.ts b/packages/openfeature-server-provider/src/ConfidenceServerProvider.ts index fdc3b2a2..f0eb9b56 100644 --- a/packages/openfeature-server-provider/src/ConfidenceServerProvider.ts +++ b/packages/openfeature-server-provider/src/ConfidenceServerProvider.ts @@ -1,13 +1,17 @@ import { ErrorCode, EvaluationContext, + FlagNotFoundError, + InvalidContextError, JsonValue, Logger, + ParseError, Provider, ProviderMetadata, ProviderStatus, ResolutionDetails, ResolutionReason, + TypeMismatchError, } from '@openfeature/js-sdk'; import { ConfidenceClient, ResolveContext, Configuration } from '@spotify-confidence/client-http'; @@ -38,7 +42,7 @@ export class ConfidenceServerProvider implements Provider { configuration: Configuration, flagKey: string, defaultValue: T, - _logger: Logger, + logger: Logger, ): ResolutionDetails { if (!configuration) { return { @@ -49,63 +53,47 @@ export class ConfidenceServerProvider implements Provider { } const [flagName, ...pathParts] = flagKey.split('.'); - try { - const flag = configuration.flags[flagName]; - - if (!flag) { - return { - errorCode: ErrorCode.FLAG_NOT_FOUND, - value: defaultValue, - reason: 'ERROR', - }; - } - - if (Configuration.ResolveReason.NoSegmentMatch === flag.reason) { - return { - value: defaultValue, - reason: 'DEFAULT', - }; - } - - let flagValue: Configuration.FlagValue; - try { - flagValue = Configuration.FlagValue.traverse(flag, pathParts.join('.')); - } catch (e) { - return { - errorCode: 'PARSE_ERROR' as ErrorCode, - value: defaultValue, - reason: 'ERROR', - }; - } - if (flagValue.value === null) { - return { - value: defaultValue, - reason: mapConfidenceReason(flag.reason), - }; - } - if (!Configuration.FlagValue.matches(flagValue, defaultValue)) { - return { - errorCode: 'TYPE_MISMATCH' as ErrorCode, - value: defaultValue, - reason: 'ERROR', - }; - } + const flag = configuration.flags[flagName]; + + if (!flag) { + logger.warn('Flag "%s" was not found', flagName); + throw new FlagNotFoundError(`Flag "${flagName}" was not found`); + } + + if (flag.reason === Configuration.ResolveReason.TargetingKeyError) { + throw new InvalidContextError(); + } + + if (Configuration.ResolveReason.NoSegmentMatch === flag.reason) { return { - value: flagValue.value as T, - reason: mapConfidenceReason(flag.reason), - variant: flag.variant, - flagMetadata: { - resolveToken: configuration.resolveToken || '', - }, - }; - } catch (e) { - return { - errorCode: ErrorCode.GENERAL, value: defaultValue, - reason: 'ERROR', + reason: 'DEFAULT', }; } + + let flagValue: Configuration.FlagValue; + try { + flagValue = Configuration.FlagValue.traverse(flag, pathParts.join('.')); + } catch (e) { + logger.warn('Value with path "%s" was not found in flag "%s"', pathParts.join('.'), flagName); + throw new ParseError(); + } + + if (!Configuration.FlagValue.matches(flagValue, defaultValue)) { + logger.warn('Value for "%s" is of incorrect type', flagKey); + throw new TypeMismatchError(); + } + + logger.info('Value for "%s" successfully evaluated', flagKey); + return { + value: flagValue.value === null ? defaultValue : (flagValue.value as T), + reason: mapConfidenceReason(flag.reason), + variant: flag.variant, + flagMetadata: { + resolveToken: configuration.resolveToken || '', + }, + }; } private async fetchFlag( diff --git a/packages/openfeature-web-provider/src/ConfidenceWebProvider.e2e.test.ts b/packages/openfeature-web-provider/src/ConfidenceWebProvider.e2e.test.ts index 404066c9..82d061a7 100644 --- a/packages/openfeature-web-provider/src/ConfidenceWebProvider.e2e.test.ts +++ b/packages/openfeature-web-provider/src/ConfidenceWebProvider.e2e.test.ts @@ -1,4 +1,4 @@ -import { OpenFeature, ProviderEvents } from '@openfeature/web-sdk'; +import { GeneralError, OpenFeature, ProviderEvents, ProviderStatus } from '@openfeature/web-sdk'; import axios from 'axios'; import { createConfidenceWebProvider } from './factory'; @@ -34,29 +34,58 @@ describe('ConfidenceHTTPProvider E2E tests', () => { return providerReadyPromise; }); - it('should return defaults after the timeout', async () => { - const confidenceProvider = createConfidenceWebProvider({ - fetchImplementation: global.fetch.bind(global), - region: 'eu', - clientSecret: 'RxDVTrXvc6op1XxiQ4OaR31dKbJ39aYV', - timeout: 0, + describe('timeout', () => { + it('should have error status and throw provider not ready when timeout hit', async () => { + const confidenceProvider = createConfidenceWebProvider({ + fetchImplementation: global.fetch.bind(global), + region: 'eu', + clientSecret: 'RxDVTrXvc6op1XxiQ4OaR31dKbJ39aYV', + timeout: 0, + }); + + await confidenceProvider.onContextChange!({}, { targetingKey: 'user-a' }); + + expect(confidenceProvider.status).toEqual(ProviderStatus.ERROR); + expect(() => + confidenceProvider.resolveBooleanEvaluation( + 'web-sdk-e2e-flag.bool', + true, + { targetingKey: 'user-a' }, + { + info: jest.fn(), + warn: jest.fn(), + error: jest.fn(), + debug: jest.fn(), + }, + ), + ).toThrow(new GeneralError('Provider not ready')); }); - await confidenceProvider.onContextChange!({}, { targetingKey: 'user-a' }); - - const flag = confidenceProvider.resolveStringEvaluation( - 'web-sdk-e2e-flag.str', - 'default', - { targetingKey: 'user-a' }, - { - info: jest.fn(), - warn: jest.fn(), - error: jest.fn(), - debug: jest.fn(), - }, - ); + it('should have ready status and return values when timeout not hit', async () => { + const confidenceProvider = createConfidenceWebProvider({ + fetchImplementation: global.fetch.bind(global), + region: 'eu', + clientSecret: 'RxDVTrXvc6op1XxiQ4OaR31dKbJ39aYV', + timeout: 1000, + }); - expect(flag.value).toEqual('default'); + await confidenceProvider.onContextChange!({}, { targetingKey: 'user-a' }); + + expect(confidenceProvider.status).toEqual(ProviderStatus.READY); + expect( + confidenceProvider.resolveBooleanEvaluation( + 'web-sdk-e2e-flag.bool', + true, + { targetingKey: 'user-a' }, + { + info: jest.fn(), + warn: jest.fn(), + error: jest.fn(), + debug: jest.fn(), + }, + ), + ).toBeTruthy(); + }); }); it('should resolve a boolean e2e', async () => { diff --git a/packages/openfeature-web-provider/src/ConfidenceWebProvider.test.ts b/packages/openfeature-web-provider/src/ConfidenceWebProvider.test.ts index ccd07959..0b5cbe8a 100644 --- a/packages/openfeature-web-provider/src/ConfidenceWebProvider.test.ts +++ b/packages/openfeature-web-provider/src/ConfidenceWebProvider.test.ts @@ -1,10 +1,14 @@ import { - ErrorCode, EvaluationContext, + FlagNotFoundError, + GeneralError, + InvalidContextError, Logger, OpenFeatureAPI, + ParseError, ProviderEvents, ProviderStatus, + TypeMismatchError, } from '@openfeature/web-sdk'; import { ConfidenceWebProvider } from './ConfidenceWebProvider'; import { ConfidenceClient, Configuration, ResolveContext } from '@spotify-confidence/client-http'; @@ -77,6 +81,13 @@ const dummyConfiguration: Configuration = { value: undefined, schema: 'undefined', }, + ['targeting-error-flag']: { + name: 'targeting-error-flag', + variant: '', + reason: Configuration.ResolveReason.TargetingKeyError, + value: { enabled: true }, + schema: { enabled: 'boolean' }, + }, }, resolveToken: 'before-each', context: dummyContext, @@ -213,7 +224,11 @@ describe('ConfidenceProvider', () => { describe('apply', () => { it('should apply when a flag has no segment match', async () => { await instanceUnderTest.initialize(dummyContext); - instanceUnderTest.resolveBooleanEvaluation('no-seg-flag.enabled', false, dummyEvaluationContext, dummyConsole); + try { + instanceUnderTest.resolveBooleanEvaluation('no-seg-flag.enabled', false, dummyEvaluationContext, dummyConsole); + } catch (e) { + expect(e).toBeDefined(); + } expect(mockApply).toHaveBeenCalledWith(dummyConfiguration.resolveToken, 'no-seg-flag'); }); @@ -226,6 +241,19 @@ describe('ConfidenceProvider', () => { }); }); + it('should throw invalid context error when the reason from confidence is targeting key error', async () => { + await instanceUnderTest.initialize(dummyContext); + + expect(() => + instanceUnderTest.resolveBooleanEvaluation( + 'targeting-error-flag.enabled', + false, + dummyEvaluationContext, + dummyConsole, + ), + ).toThrow(InvalidContextError); + }); + describe('resolveBooleanEvaluation', () => { it('should resolve a boolean', async () => { await instanceUnderTest.initialize(dummyContext); @@ -241,6 +269,7 @@ describe('ConfidenceProvider', () => { value: true, }); }); + it('should resolve default when accessing a flag with no segment match', async () => { await instanceUnderTest.initialize(dummyContext); @@ -267,67 +296,34 @@ describe('ConfidenceProvider', () => { }); }); - it('should return default if the provider is not ready', () => { - const actual = instanceUnderTest.resolveBooleanEvaluation( - 'testFlag.bool', - false, - dummyEvaluationContext, - dummyConsole, - ); - - expect(actual).toEqual({ - value: false, - errorCode: ErrorCode.PROVIDER_NOT_READY, - reason: 'ERROR', - }); + it('should throw if the provider is not ready', () => { + expect(() => + instanceUnderTest.resolveBooleanEvaluation('testFlag.bool', false, dummyEvaluationContext, dummyConsole), + ).toThrow(new GeneralError('Provider not ready')); }); - it('should return default if the flag is not found', async () => { + it('should throw if the flag is not found', async () => { await instanceUnderTest.initialize(dummyContext); - const actual = instanceUnderTest.resolveBooleanEvaluation( - 'notARealFlag.bool', - false, - dummyEvaluationContext, - dummyConsole, - ); - expect(actual).toEqual({ - value: false, - errorCode: ErrorCode.FLAG_NOT_FOUND, - reason: 'ERROR', - }); + expect(() => + instanceUnderTest.resolveBooleanEvaluation('notARealFlag.bool', false, dummyEvaluationContext, dummyConsole), + ).toThrow(new FlagNotFoundError(`Flag "notARealFlag" was not found`)); }); - it('should return default if the flag requested is the wrong type', async () => { + it('should throw if the flag requested is the wrong type', async () => { await instanceUnderTest.initialize(dummyContext); - const actual = instanceUnderTest.resolveBooleanEvaluation( - 'testFlag.str', - false, - dummyEvaluationContext, - dummyConsole, - ); - expect(actual).toEqual({ - value: false, - errorCode: ErrorCode.TYPE_MISMATCH, - reason: 'ERROR', - }); + expect(() => + instanceUnderTest.resolveBooleanEvaluation('testFlag.str', false, dummyEvaluationContext, dummyConsole), + ).toThrow(TypeMismatchError); }); - it('should return default if the value requested is not in the flag schema', async () => { + it('should throw if the value requested is not in the flag schema', async () => { await instanceUnderTest.initialize(dummyContext); - const actual = instanceUnderTest.resolveBooleanEvaluation( - 'testFlag.404', - false, - dummyEvaluationContext, - dummyConsole, - ); - expect(actual).toEqual({ - value: false, - errorCode: ErrorCode.PARSE_ERROR, - reason: 'ERROR', - }); + expect(() => + instanceUnderTest.resolveBooleanEvaluation('testFlag.404', false, dummyEvaluationContext, dummyConsole), + ).toThrow(ParseError); }); }); @@ -392,52 +388,34 @@ describe('ConfidenceProvider', () => { }); }); - it('should return default if the provider is not ready', () => { - const actual = instanceUnderTest.resolveNumberEvaluation('testFlag.int', 1, dummyEvaluationContext, dummyConsole); - - expect(actual).toEqual({ - value: 1, - errorCode: ErrorCode.PROVIDER_NOT_READY, - reason: 'ERROR', - }); + it('should throw if the provider is not ready', () => { + expect(() => + instanceUnderTest.resolveNumberEvaluation('testFlag.int', 1, dummyEvaluationContext, dummyConsole), + ).toThrow(new GeneralError('Provider not ready')); }); - it('should return default if the flag is not found', async () => { + it('should throw if the flag is not found', async () => { await instanceUnderTest.initialize(dummyContext); - const actual = instanceUnderTest.resolveNumberEvaluation( - 'notARealFlag.int', - 1, - dummyEvaluationContext, - dummyConsole, - ); - expect(actual).toEqual({ - value: 1, - errorCode: ErrorCode.FLAG_NOT_FOUND, - reason: 'ERROR', - }); + expect(() => + instanceUnderTest.resolveNumberEvaluation('notARealFlag.int', 1, dummyEvaluationContext, dummyConsole), + ).toThrow(FlagNotFoundError); }); - it('should return default if the flag requested is the wrong type', async () => { + it('should throw if the flag requested is the wrong type', async () => { await instanceUnderTest.initialize(dummyContext); - const actual = instanceUnderTest.resolveNumberEvaluation('testFlag.str', 1, dummyEvaluationContext, dummyConsole); - expect(actual).toEqual({ - value: 1, - errorCode: ErrorCode.TYPE_MISMATCH, - reason: 'ERROR', - }); + expect(() => + instanceUnderTest.resolveNumberEvaluation('testFlag.str', 1, dummyEvaluationContext, dummyConsole), + ).toThrow(TypeMismatchError); }); - it('should return default if the value requested is not in the flag schema', async () => { + it('should throw if the value requested is not in the flag schema', async () => { await instanceUnderTest.initialize(dummyContext); - const actual = instanceUnderTest.resolveNumberEvaluation('testFlag.404', 1, dummyEvaluationContext, dummyConsole); - expect(actual).toEqual({ - value: 1, - errorCode: ErrorCode.PARSE_ERROR, - reason: 'ERROR', - }); + expect(() => + instanceUnderTest.resolveNumberEvaluation('testFlag.404', 1, dummyEvaluationContext, dummyConsole), + ).toThrow(ParseError); }); }); @@ -472,83 +450,42 @@ describe('ConfidenceProvider', () => { }); }); - it('should return default if the provider is not ready', () => { - const actual = instanceUnderTest.resolveStringEvaluation( - 'testFlag.str', - 'default', - dummyEvaluationContext, - dummyConsole, - ); - - expect(actual).toEqual({ - value: 'default', - errorCode: ErrorCode.PROVIDER_NOT_READY, - reason: 'ERROR', - }); + it('should throw if the provider is not ready', () => { + expect(() => + instanceUnderTest.resolveStringEvaluation('testFlag.str', 'default', dummyEvaluationContext, dummyConsole), + ).toThrow(new GeneralError('Provider not ready')); }); - it('should return default if the flag is not found', async () => { + it('should throw if the flag is not found', async () => { await instanceUnderTest.initialize(dummyContext); - const actual = instanceUnderTest.resolveStringEvaluation( - 'notARealFlag.str', - 'default', - dummyEvaluationContext, - dummyConsole, - ); - expect(actual).toEqual({ - value: 'default', - errorCode: ErrorCode.FLAG_NOT_FOUND, - reason: 'ERROR', - }); + expect(() => + instanceUnderTest.resolveStringEvaluation('notARealFlag.str', 'default', dummyEvaluationContext, dummyConsole), + ).toThrow(FlagNotFoundError); }); - it('should return default if the flag requested is the wrong type', async () => { + it('should throw if the flag requested is the wrong type', async () => { await instanceUnderTest.initialize(dummyContext); - const actual = instanceUnderTest.resolveStringEvaluation( - 'testFlag.int', - 'default', - dummyEvaluationContext, - dummyConsole, - ); - expect(actual).toEqual({ - value: 'default', - errorCode: ErrorCode.TYPE_MISMATCH, - reason: 'ERROR', - }); + expect(() => + instanceUnderTest.resolveStringEvaluation('testFlag.int', 'default', dummyEvaluationContext, dummyConsole), + ).toThrow(TypeMismatchError); }); - it('should return default if the flag requested is the wrong type from nested obj', async () => { + it('should throw if the flag requested is the wrong type from nested obj', async () => { await instanceUnderTest.initialize(dummyContext); - const actual = instanceUnderTest.resolveStringEvaluation( - 'testFlag.obj.int', - 'default', - dummyEvaluationContext, - dummyConsole, - ); - expect(actual).toEqual({ - value: 'default', - errorCode: ErrorCode.TYPE_MISMATCH, - reason: 'ERROR', - }); + expect(() => + instanceUnderTest.resolveStringEvaluation('testFlag.obj.int', 'default', dummyEvaluationContext, dummyConsole), + ).toThrow(TypeMismatchError); }); - it('should return default if the value requested is not in the flag schema', async () => { + it('should throw if the value requested is not in the flag schema', async () => { await instanceUnderTest.initialize(dummyContext); - const actual = instanceUnderTest.resolveStringEvaluation( - 'testFlag.404', - 'default', - dummyEvaluationContext, - dummyConsole, - ); - expect(actual).toEqual({ - value: 'default', - errorCode: ErrorCode.PARSE_ERROR, - reason: 'ERROR', - }); + expect(() => + instanceUnderTest.resolveStringEvaluation('testFlag.404', 'default', dummyEvaluationContext, dummyConsole), + ).toThrow(ParseError); }); }); @@ -603,7 +540,7 @@ describe('ConfidenceProvider', () => { it('should resolve a full object with type mismatch default', async () => { await instanceUnderTest.initialize(dummyContext); - expect( + expect(() => instanceUnderTest.resolveObjectEvaluation( 'testFlag.obj', { @@ -612,76 +549,37 @@ describe('ConfidenceProvider', () => { dummyEvaluationContext, dummyConsole, ), - ).toEqual({ - errorCode: 'TYPE_MISMATCH', - reason: 'ERROR', - value: { - testBool: false, - }, - }); + ).toThrow(TypeMismatchError); }); - it('should return default if the provider is not ready', () => { - const actual = instanceUnderTest.resolveObjectEvaluation( - 'testFlag.obj', - {}, - dummyEvaluationContext, - dummyConsole, - ); - - expect(actual).toEqual({ - value: {}, - errorCode: ErrorCode.PROVIDER_NOT_READY, - reason: 'ERROR', - }); + it('should throw if the provider is not ready', () => { + expect(() => + instanceUnderTest.resolveObjectEvaluation('testFlag.obj', {}, dummyEvaluationContext, dummyConsole), + ).toThrow(new GeneralError('Provider not ready')); }); - it('should return default if the flag is not found', async () => { + it('should throw if the flag is not found', async () => { await instanceUnderTest.initialize(dummyContext); - const actual = instanceUnderTest.resolveObjectEvaluation( - 'notARealFlag.obj', - {}, - dummyEvaluationContext, - dummyConsole, - ); - expect(actual).toEqual({ - value: {}, - errorCode: ErrorCode.FLAG_NOT_FOUND, - reason: 'ERROR', - }); + expect(() => + instanceUnderTest.resolveObjectEvaluation('notARealFlag.obj', {}, dummyEvaluationContext, dummyConsole), + ).toThrow(FlagNotFoundError); }); - it('should return default if the flag requested is the wrong type', async () => { + it('should throw if the flag requested is the wrong type', async () => { await instanceUnderTest.initialize(dummyContext); - const actual = instanceUnderTest.resolveObjectEvaluation( - 'testFlag.str', - {}, - dummyEvaluationContext, - dummyConsole, - ); - expect(actual).toEqual({ - value: {}, - errorCode: ErrorCode.TYPE_MISMATCH, - reason: 'ERROR', - }); + expect(() => + instanceUnderTest.resolveObjectEvaluation('testFlag.str', {}, dummyEvaluationContext, dummyConsole), + ).toThrow(TypeMismatchError); }); - it('should return default if the value requested is not in the flag schema', async () => { + it('should throw if the value requested is not in the flag schema', async () => { await instanceUnderTest.initialize(dummyContext); - const actual = instanceUnderTest.resolveObjectEvaluation( - 'testFlag.404', - {}, - dummyEvaluationContext, - dummyConsole, - ); - expect(actual).toEqual({ - value: {}, - errorCode: ErrorCode.PARSE_ERROR, - reason: 'ERROR', - }); + expect(() => + instanceUnderTest.resolveObjectEvaluation('testFlag.404', {}, dummyEvaluationContext, dummyConsole), + ).toThrow(ParseError); }); }); }); diff --git a/packages/openfeature-web-provider/src/ConfidenceWebProvider.ts b/packages/openfeature-web-provider/src/ConfidenceWebProvider.ts index 7aeab382..c3af6bc5 100644 --- a/packages/openfeature-web-provider/src/ConfidenceWebProvider.ts +++ b/packages/openfeature-web-provider/src/ConfidenceWebProvider.ts @@ -1,15 +1,19 @@ import { - ErrorCode, EvaluationContext, + FlagNotFoundError, + GeneralError, + InvalidContextError, JsonValue, Logger, OpenFeatureEventEmitter, + ParseError, Provider, ProviderEvents, ProviderMetadata, ProviderStatus, ResolutionDetails, ResolutionReason, + TypeMismatchError, } from '@openfeature/web-sdk'; import equal from 'fast-deep-equal'; @@ -89,11 +93,7 @@ export class ConfidenceWebProvider implements Provider { ): ResolutionDetails { if (!this.configuration) { logger.warn('Provider not ready'); - return { - errorCode: ErrorCode.PROVIDER_NOT_READY, - value: defaultValue, - reason: 'ERROR', - }; + throw new GeneralError('Provider not ready'); } if (!equal(this.configuration.context, this.convertContext(context))) { @@ -105,72 +105,49 @@ export class ConfidenceWebProvider implements Provider { const [flagName, ...pathParts] = flagKey.split('.'); - try { - const flag = this.configuration.flags[flagName]; - - if (!flag) { - logger.warn('Flag "%s" was not found', flagName); - return { - errorCode: ErrorCode.FLAG_NOT_FOUND, - value: defaultValue, - reason: 'ERROR', - }; - } - - if (Configuration.ResolveReason.NoSegmentMatch === flag.reason) { - this.applyManager?.apply(this.configuration.resolveToken, flagName); - return { - value: defaultValue, - reason: 'DEFAULT', - }; - } - - let flagValue: Configuration.FlagValue; - try { - flagValue = Configuration.FlagValue.traverse(flag, pathParts.join('.')); - } catch (e) { - logger.warn('Value with path "%s" was not found in flag "%s"', pathParts.join('.'), flagName); - return { - errorCode: ErrorCode.PARSE_ERROR, - value: defaultValue, - reason: 'ERROR', - }; - } - - if (flagValue.value === null) { - return { - value: defaultValue, - reason: mapConfidenceReason(flag.reason), - }; - } - - if (!Configuration.FlagValue.matches(flagValue, defaultValue)) { - logger.warn('Value for "%s" is of incorrect type', flagKey); - return { - errorCode: ErrorCode.TYPE_MISMATCH, - value: defaultValue, - reason: 'ERROR', - }; - } + const flag = this.configuration.flags[flagName]; + if (!flag) { + logger.warn('Flag "%s" was not found', flagName); + throw new FlagNotFoundError(`Flag "${flagName}" was not found`); + } + + if (flag.reason === Configuration.ResolveReason.TargetingKeyError) { + throw new InvalidContextError(); + } + + if (Configuration.ResolveReason.NoSegmentMatch === flag.reason) { this.applyManager?.apply(this.configuration.resolveToken, flagName); - logger.info('Value for "%s" successfully evaluated', flagKey); return { - value: flagValue.value as T, - reason: mapConfidenceReason(flag.reason), - variant: flag.variant, - flagMetadata: { - resolveToken: this.configuration.resolveToken, - }, - }; - } catch (e: unknown) { - logger.warn('Error %o occurred in flag evaluation', e); - return { - errorCode: ErrorCode.GENERAL, value: defaultValue, - reason: 'ERROR', + reason: 'DEFAULT', }; } + + let flagValue: Configuration.FlagValue; + try { + flagValue = Configuration.FlagValue.traverse(flag, pathParts.join('.')); + } catch (e) { + logger.warn('Value with path "%s" was not found in flag "%s"', pathParts.join('.'), flagName); + throw new ParseError(); + } + + if (!Configuration.FlagValue.matches(flagValue, defaultValue)) { + logger.warn('Value for "%s" is of incorrect type', flagKey); + throw new TypeMismatchError(); + } + + logger.info('Value for "%s" successfully evaluated', flagKey); + + this.applyManager?.apply(this.configuration.resolveToken, flagName); + return { + value: flagValue.value === null ? defaultValue : (flagValue.value as T), + reason: mapConfidenceReason(flag.reason), + variant: flag.variant, + flagMetadata: { + resolveToken: this.configuration.resolveToken, + }, + }; } resolveBooleanEvaluation(