Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: run error hook when provider returns reason error or error code #926

Merged
merged 6 commits into from
May 8, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions jest.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -175,13 +175,13 @@ export default {
displayName: 'react',
testEnvironment: 'jsdom',
preset: 'ts-jest',
testMatch: ['<rootDir>/packages/react/test/**/*.spec.ts*'],
testMatch: ['<rootDir>/packages/react/test/**/*.spec.{ts,tsx}'],
moduleNameMapper: {
'@openfeature/core': '<rootDir>/packages/shared/src',
'@openfeature/web-sdk': '<rootDir>/packages/client/src',
},
transform: {
'^.+\\.tsx$': [
'^.+\\.(ts|tsx)$': [
'ts-jest',
{
tsconfig: '<rootDir>/packages/react/test/tsconfig.json',
Expand Down
9 changes: 7 additions & 2 deletions packages/client/src/client/open-feature-client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,8 @@ import {
ResolutionDetails,
SafeLogger,
StandardResolutionReasons,
statusMatchesEvent
instantiateErrorByErrorCode,
statusMatchesEvent,
} from '@openfeature/core';
import { FlagEvaluationOptions } from '../evaluation';
import { ProviderEvents } from '../events';
Expand Down Expand Up @@ -208,7 +209,7 @@ export class OpenFeatureClient implements Client {

try {
this.beforeHooks(allHooks, hookContext, options);

// short circuit evaluation entirely if provider is in a bad state
if (this.providerStatus === ProviderStatus.NOT_READY) {
throw new ProviderNotReadyError('provider has not yet initialized');
Expand All @@ -225,6 +226,10 @@ export class OpenFeatureClient implements Client {
flagKey,
};

if (evaluationDetails.errorCode) {
throw instantiateErrorByErrorCode(evaluationDetails.errorCode);
}

this.afterHooks(allHooksReversed, hookContext, evaluationDetails, options);

return evaluationDetails;
Expand Down
23 changes: 23 additions & 0 deletions packages/client/test/hooks.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ import {
GeneralError,
OpenFeature,
Hook,
StandardResolutionReasons,
ErrorCode,
} from '../src';

const BOOLEAN_VALUE = true;
Expand Down Expand Up @@ -206,6 +208,27 @@ describe('Hooks', () => {
],
});
});

it('"error" must run if resolution details contains an error code', () => {
(MOCK_ERROR_PROVIDER.resolveBooleanEvaluation as jest.Mock).mockReturnValue({
value: BOOLEAN_VALUE,
errorCode: ErrorCode.FLAG_NOT_FOUND,
});

const mockErrorHook = jest.fn();

const details = client.getBooleanDetails(FLAG_KEY, false, {
hooks: [{ error: mockErrorHook }],
});

expect(mockErrorHook).toHaveBeenCalled();
expect(details).toEqual(
expect.objectContaining({
errorCode: ErrorCode.FLAG_NOT_FOUND,
reason: StandardResolutionReasons.ERROR,
}),
);
});
});
});

Expand Down
5 changes: 5 additions & 0 deletions packages/server/src/client/open-feature-client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import {
ResolutionDetails,
SafeLogger,
StandardResolutionReasons,
instantiateErrorByErrorCode,
statusMatchesEvent,
} from '@openfeature/core';
import { FlagEvaluationOptions } from '../evaluation';
Expand Down Expand Up @@ -278,6 +279,10 @@ export class OpenFeatureClient implements Client, ManageContext<OpenFeatureClien
flagKey,
};

if (evaluationDetails.errorCode) {
throw instantiateErrorByErrorCode(evaluationDetails.errorCode);
}

await this.afterHooks(allHooksReversed, hookContext, evaluationDetails, options);

return evaluationDetails;
Expand Down
47 changes: 38 additions & 9 deletions packages/server/test/hooks.spec.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,19 @@
import { OpenFeature, Provider, ResolutionDetails, Client, FlagValueType, EvaluationContext, Hook } from '../src';
import {
OpenFeature,
Provider,
ResolutionDetails,
Client,
FlagValueType,
EvaluationContext,
Hook,
StandardResolutionReasons,
ErrorCode,
} from '../src';

const BOOLEAN_VALUE = true;

const BOOLEAN_VARIANT = `${BOOLEAN_VALUE}`;
const REASON = 'mocked-value';
const ERROR_REASON = 'error';
const ERROR_CODE = 'MOCKED_ERROR';

// a mock provider with some jest spies
const MOCK_PROVIDER: Provider = {
Expand All @@ -28,8 +36,8 @@ const MOCK_ERROR_PROVIDER: Provider = {
},
resolveBooleanEvaluation: jest.fn((): Promise<ResolutionDetails<boolean>> => {
return Promise.reject({
reason: ERROR_REASON,
errorCode: ERROR_CODE,
reason: StandardResolutionReasons.ERROR,
errorCode: ErrorCode.GENERAL,
});
}),
} as unknown as Provider;
Expand Down Expand Up @@ -357,6 +365,27 @@ describe('Hooks', () => {
],
});
});

it('"error" must run if resolution details contains an error code', async () => {
(MOCK_ERROR_PROVIDER.resolveBooleanEvaluation as jest.Mock).mockResolvedValueOnce({
value: BOOLEAN_VALUE,
errorCode: ErrorCode.FLAG_NOT_FOUND,
});

const mockErrorHook = jest.fn();

const details = await client.getBooleanDetails(FLAG_KEY, false, undefined, {
hooks: [{ error: mockErrorHook }],
});

expect(mockErrorHook).toHaveBeenCalled();
expect(details).toEqual(
expect.objectContaining({
errorCode: ErrorCode.FLAG_NOT_FOUND,
reason: StandardResolutionReasons.ERROR,
}),
);
});
});
});

Expand Down Expand Up @@ -636,8 +665,8 @@ describe('Hooks', () => {
],
resolveBooleanEvaluation: jest.fn((): Promise<ResolutionDetails<boolean>> => {
return Promise.reject({
reason: ERROR_REASON,
errorCode: ERROR_CODE,
reason: StandardResolutionReasons.ERROR,
errorCode: ErrorCode.INVALID_CONTEXT,
});
}),
} as unknown as Provider;
Expand Down Expand Up @@ -717,8 +746,8 @@ describe('Hooks', () => {
],
resolveBooleanEvaluation: jest.fn((): Promise<ResolutionDetails<boolean>> => {
return Promise.reject({
reason: ERROR_REASON,
errorCode: ERROR_CODE,
reason: StandardResolutionReasons.ERROR,
errorCode: ErrorCode.PROVIDER_NOT_READY,
});
}),
} as unknown as Provider;
Expand Down
54 changes: 45 additions & 9 deletions packages/shared/src/errors/index.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,45 @@
export * from './general-error';
export * from './flag-not-found-error';
export * from './parse-error';
export * from './type-mismatch-error';
export * from './targeting-key-missing-error';
export * from './invalid-context-error';
export * from './open-feature-error-abstract';
export * from './provider-not-ready-error';
export * from './provider-fatal-error';
import { ErrorCode } from '../evaluation';

import { FlagNotFoundError } from './flag-not-found-error';
import { GeneralError } from './general-error';
import { InvalidContextError } from './invalid-context-error';
import { OpenFeatureError } from './open-feature-error-abstract';
import { ParseError } from './parse-error';
import { ProviderFatalError } from './provider-fatal-error';
import { ProviderNotReadyError } from './provider-not-ready-error';
import { TargetingKeyMissingError } from './targeting-key-missing-error';
import { TypeMismatchError } from './type-mismatch-error';

const instantiateErrorByErrorCode = (errorCode: ErrorCode, message?: string): OpenFeatureError => {
switch (errorCode) {
case ErrorCode.FLAG_NOT_FOUND:
return new FlagNotFoundError(message);
case ErrorCode.PARSE_ERROR:
return new ParseError(message);
case ErrorCode.TYPE_MISMATCH:
return new TypeMismatchError(message);
case ErrorCode.TARGETING_KEY_MISSING:
return new TargetingKeyMissingError(message);
case ErrorCode.INVALID_CONTEXT:
return new InvalidContextError(message);
case ErrorCode.PROVIDER_NOT_READY:
return new ProviderNotReadyError(message);
case ErrorCode.PROVIDER_FATAL:
return new ProviderFatalError(message);
default:
return new GeneralError(message);
}
};

export {
FlagNotFoundError,
GeneralError,
InvalidContextError,
ParseError,
ProviderFatalError,
ProviderNotReadyError,
TargetingKeyMissingError,
TypeMismatchError,
OpenFeatureError,
instantiateErrorByErrorCode,
};
Loading