js-sdk/packages/server/test/hooks.spec.ts

1082 lines
32 KiB
TypeScript

import type {
Provider,
ResolutionDetails,
Client,
FlagValueType,
EvaluationContext,
Hook} from '../src';
import {
OpenFeature,
StandardResolutionReasons,
ErrorCode,
} from '../src';
const BOOLEAN_VALUE = true;
const BOOLEAN_VARIANT = `${BOOLEAN_VALUE}`;
const REASON = 'mocked-value';
// a mock provider with some jest spies
const MOCK_PROVIDER: Provider = {
metadata: {
name: 'mock-hooks-success',
},
resolveBooleanEvaluation: jest.fn((): Promise<ResolutionDetails<boolean>> => {
return Promise.resolve({
value: BOOLEAN_VALUE,
variant: BOOLEAN_VARIANT,
reason: REASON,
});
}),
} as unknown as Provider;
// a mock provider with some jest spies
const MOCK_ERROR_PROVIDER: Provider = {
metadata: {
name: 'mock-hooks-error',
},
resolveBooleanEvaluation: jest.fn((): Promise<ResolutionDetails<boolean>> => {
return Promise.reject({
reason: StandardResolutionReasons.ERROR,
errorCode: ErrorCode.GENERAL,
});
}),
} as unknown as Provider;
describe('Hooks', () => {
// set timeouts short for this suite.
jest.setTimeout(1000);
let client: Client;
const FLAG_KEY = 'my-flag';
afterEach(async () => {
await OpenFeature.clearProviders();
jest.clearAllMocks();
});
beforeEach(() => {
OpenFeature.setProvider(MOCK_PROVIDER);
client = OpenFeature.getClient();
});
describe('Requirement 4.1.1, 4.1.2', () => {
it('must provide flagKey, flagType, evaluationContext, defaultValue, client metadata and provider metadata', (done) => {
client.getBooleanValue(FLAG_KEY, false, undefined, {
hooks: [
{
before: (hookContext) => {
try {
expect(hookContext.flagKey).toEqual(FLAG_KEY);
expect(hookContext.flagValueType).toBeDefined();
expect(hookContext.context).toBeDefined();
expect(hookContext.defaultValue).toBeDefined();
expect(hookContext.providerMetadata).toBeDefined();
expect(hookContext.clientMetadata).toBeDefined();
done();
} catch (err) {
done(err);
}
},
},
],
});
});
it('client metadata and provider metadata must match the client and provider used to resolve the flag', (done) => {
const provider: Provider = {
metadata: {
name: 'mock-my-domain-provider',
},
resolveBooleanEvaluation: jest.fn((): Promise<ResolutionDetails<boolean>> => {
return Promise.resolve({
value: BOOLEAN_VALUE,
variant: BOOLEAN_VARIANT,
reason: REASON,
});
}),
} as unknown as Provider;
OpenFeature.setProvider('my-domain', provider);
const client = OpenFeature.getClient('my-domain');
client.getBooleanValue(FLAG_KEY, false, undefined, {
hooks: [
{
before: (hookContext) => {
try {
expect(hookContext.providerMetadata).toEqual(provider.metadata);
expect(hookContext.clientMetadata).toEqual(client.metadata);
done();
} catch (err) {
done(err);
}
},
},
],
});
});
});
describe('Requirement 4.1.3', () => {
it('flagKey, flagType, defaultValue must be immutable', (done) => {
client.getBooleanValue(FLAG_KEY, false, undefined, {
hooks: [
{
before: (hookContext) => {
try {
// cast this to allow us to attempt to overwrite, to verify runtime immutability.
const hookContextCasted = hookContext as {
flagKey: string;
flagValueType: FlagValueType;
defaultValue: boolean;
};
hookContextCasted.flagKey = 'not allowed';
hookContextCasted.flagValueType = 'object';
hookContextCasted.defaultValue = true;
done(new Error('Expected error, hookContext should be immutable'));
} catch (err) {
done();
}
},
},
],
});
});
});
describe('Requirement 4.1.4', () => {
describe('before', () => {
it('evaluationContext must be mutable', (done) => {
client.getBooleanValue(FLAG_KEY, false, undefined, {
hooks: [
{
before: (hookContext) => {
try {
// evaluation context is mutable in before, so this should work.
hookContext.context.newBeforeProp = 'new!';
expect(hookContext.context.newBeforeProp).toBeTruthy();
done();
} catch (err) {
done(err);
}
},
},
],
});
});
});
describe('after', () => {
it('evaluationContext must be immutable', (done) => {
client.getBooleanValue(FLAG_KEY, false, undefined, {
hooks: [
{
after: (hookContext) => {
try {
// evaluation context is immutable (frozen) in all but the "before" stage
// cast this to allow us to attempt to overwrite, to verify runtime immutability.
const evaluationContextCasted = hookContext.context as EvaluationContext;
evaluationContextCasted.newAfterProp = true;
done(new Error('Expected error, hookContext should be immutable'));
} catch (err) {
done();
}
},
},
],
});
});
});
});
describe('4.3.2', () => {
it('"before" must run before flag resolution', async () => {
await client.getBooleanValue(FLAG_KEY, false, undefined, {
hooks: [
{
before: () => {
// add a prop to the context.
return { beforeRan: true };
},
},
],
});
expect(MOCK_PROVIDER.resolveBooleanEvaluation).toHaveBeenCalledWith(
expect.anything(),
expect.anything(),
// ensure property was added by the time the flag resolution occurred.
expect.objectContaining({
beforeRan: true,
}),
expect.anything(),
);
});
});
describe('Requirement 4.3.3', () => {
it('EvaluationContext must be passed to next "before" hook', (done) => {
client.getBooleanValue(FLAG_KEY, false, undefined, {
hooks: [
{
before: () => {
// add a prop to the context.
return { beforeRan: true };
},
},
{
before: (hookContext) => {
// ensure added prop exists in next hook
try {
expect(hookContext.context.beforeRan).toBeTruthy();
done();
} catch (err) {
done(err);
}
return { beforeRan: true };
},
},
],
});
});
});
describe('Requirement 4.3.4', () => {
it('When before hooks have finished executing, any resulting evaluation context MUST be merged with the existing evaluation context in the following order: before-hook (highest precedence), invocation, client, api (lowest precedence).', async () => {
const globalProp434 = 'globalProp';
const globalPropToOverwrite434 = 'globalPropToOverwrite';
const clientProp434 = 'clientProp';
const clientPropToOverwrite434 = 'clientPropToOverwrite';
const invocationProp434 = 'invocationProp';
const invocationPropToOverwrite434 = 'invocationPropToOverwrite';
const syncHookProp434 = 'syncHookProp';
const asyncHookProp434 = 'asyncHookProp';
OpenFeature.setContext({
[globalProp434]: true,
[globalPropToOverwrite434]: false,
});
const clientContext = {
[clientProp434]: true,
[clientPropToOverwrite434]: false,
[globalPropToOverwrite434]: true,
};
const invocationContext = {
[invocationProp434]: true,
[invocationPropToOverwrite434]: false,
[clientPropToOverwrite434]: true,
};
const syncHookContext = {
[invocationPropToOverwrite434]: true,
[syncHookProp434]: true,
};
const asyncHookContext = {
[asyncHookProp434]: true,
};
const localClient = OpenFeature.getClient('merge-test', 'test', clientContext);
const syncVoidHook: Hook = {
before: () => {
// synchronous hook that doesn't modify context
},
};
const syncContextHook: Hook = {
before: () => {
return syncHookContext;
},
};
const asyncVoidHook: Hook = {
before: async () => {
// asynchronous hook that doesn't modify context
await Promise.resolve();
},
};
const asyncContextHook: Hook = {
before: () => {
return Promise.resolve(asyncHookContext);
},
};
await localClient.getBooleanValue(FLAG_KEY, false, invocationContext, {
hooks: [syncVoidHook, syncContextHook, asyncVoidHook, asyncContextHook],
});
expect(MOCK_PROVIDER.resolveBooleanEvaluation).toHaveBeenCalledWith(
expect.anything(),
expect.anything(),
// ensure correct properties were maintained/overwritten
expect.objectContaining({
[globalProp434]: true,
[globalPropToOverwrite434]: true,
[clientProp434]: true,
[clientPropToOverwrite434]: true,
[invocationProp434]: true,
[invocationPropToOverwrite434]: true,
[syncHookProp434]: true,
[asyncHookProp434]: true,
}),
expect.anything(),
);
});
});
describe('Requirement 4.3.5', () => {
it('"after" must run after flag evaluation', (done) => {
client.getBooleanValue(FLAG_KEY, false, undefined, {
hooks: [
{
after: () => {
try {
// expect provider was called by the time "after" hook runs.
expect(MOCK_PROVIDER.resolveBooleanEvaluation).toHaveBeenCalled();
done();
} catch (err) {
done(err);
}
},
},
],
});
});
});
describe('"error" stage', () => {
beforeEach(() => {
OpenFeature.setProvider(MOCK_ERROR_PROVIDER);
});
describe('Requirement 4.3.6', () => {
it('"error" must run if any errors occur', (done) => {
client.getBooleanValue(FLAG_KEY, false, undefined, {
hooks: [
{
error: () => {
try {
// expect provider was by called the time "error" hook runs.
expect(MOCK_ERROR_PROVIDER.resolveBooleanEvaluation).toHaveBeenCalled();
done();
} catch (err) {
done(err);
}
},
},
],
});
});
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,
}),
);
});
});
});
describe('"finally" stage', () => {
describe('Requirement 4.3.7', () => {
it('"finally" must run after "after" stage', (done) => {
OpenFeature.setProvider(MOCK_PROVIDER);
const afterAndFinallyHook: Hook = {
// mock "after"
after: jest.fn(() => {
return;
}),
finally: () => {
try {
// assert mock was called
expect(afterAndFinallyHook.after).toHaveBeenCalled();
done();
} catch (err) {
done(err);
}
},
};
client.getBooleanValue(FLAG_KEY, false, undefined, {
hooks: [afterAndFinallyHook],
});
});
it('"finally" must run after "error" stage', (done) => {
OpenFeature.setProvider(MOCK_ERROR_PROVIDER);
const errorAndFinallyHook: Hook = {
error: jest.fn(() => {
return;
}),
finally: () => {
try {
// assert mock is called
expect(errorAndFinallyHook.error).toHaveBeenCalled();
done();
} catch (err) {
done(err);
}
},
};
client.getBooleanValue(FLAG_KEY, false, undefined, {
hooks: [errorAndFinallyHook],
});
});
});
describe('Requirement 4.3.8', () => {
it('"evaluation details" passed to the "finally" stage matches the evaluation details returned to the application author', async () => {
OpenFeature.setProvider(MOCK_PROVIDER);
let evaluationDetailsHooks;
const evaluationDetails = await client.getBooleanDetails(
FLAG_KEY,
false,
{},
{
hooks: [
{
finally: (_, details) => {
evaluationDetailsHooks = details;
},
},
],
},
);
expect(evaluationDetailsHooks).toEqual(evaluationDetails);
});
});
});
describe('Requirement 4.4.2', () => {
it('"before" must run hook in order global, client, invocation, provider', (done) => {
const globalBeforeHook: Hook = {
before: jest.fn(() => {
try {
// provider, nor client, nor invocation should have run at this point.
expect(provider.hooks?.[0].before).not.toHaveBeenCalled();
expect(clientBeforeHook.before).not.toHaveBeenCalled();
expect(invocationBeforeHook.before).not.toHaveBeenCalled();
} catch (err) {
done(err);
}
}),
};
const clientBeforeHook: Hook = {
before: jest.fn(() => {
try {
// global should have run and, but not invocation or provider
expect(globalBeforeHook.before).toHaveBeenCalled();
expect(invocationBeforeHook.before).not.toHaveBeenCalled();
expect(provider.hooks?.[0].before).not.toHaveBeenCalled();
} catch (err) {
done(err);
}
}),
};
const invocationBeforeHook: Hook = {
before: jest.fn(() => {
try {
// global and client should have been called, but not provider
expect(globalBeforeHook.before).toHaveBeenCalled();
expect(clientBeforeHook.before).toHaveBeenCalled();
expect(provider.hooks?.[0].before).not.toHaveBeenCalled();
} catch (err) {
done(err);
}
}),
};
const provider: Provider = {
metadata: {
name: 'mock-hooks-before-with-hooks',
},
hooks: [
{
before: jest.fn(() => {
try {
// invocation, nor client, nor global should have run at this point.
expect(globalBeforeHook.before).toHaveBeenCalled();
expect(clientBeforeHook.before).toHaveBeenCalled();
expect(invocationBeforeHook.before).toHaveBeenCalled();
done();
} catch (err) {
done(err);
}
}),
},
],
resolveBooleanEvaluation: jest.fn((): Promise<ResolutionDetails<boolean>> => {
return Promise.resolve({
value: BOOLEAN_VALUE,
variant: BOOLEAN_VARIANT,
reason: REASON,
});
}),
} as unknown as Provider;
OpenFeature.setProvider(provider);
OpenFeature.clearHooks();
client.clearHooks();
OpenFeature.addHooks(globalBeforeHook);
client.addHooks(clientBeforeHook);
client.getBooleanValue(FLAG_KEY, false, undefined, {
hooks: [invocationBeforeHook],
});
});
it('"after" must run hook in order provider, invocation, client, global', (done) => {
const invocationAfterHook: Hook = {
after: jest.fn(() => {
try {
// provider should have run.
expect(provider.hooks?.[0].after).toHaveBeenCalled();
// neither client, nor global should have run at this point.
expect(clientAfterHook.after).not.toHaveBeenCalled();
expect(globalAfterHook.after).not.toHaveBeenCalled();
} catch (err) {
done(err);
}
}),
};
const clientAfterHook: Hook = {
after: jest.fn(() => {
try {
// provider and invocation should have run, but not global
expect(provider.hooks?.[0].after).toHaveBeenCalled();
expect(invocationAfterHook.after).toHaveBeenCalled();
expect(globalAfterHook.after).not.toHaveBeenCalled();
} catch (err) {
done(err);
}
}),
};
const globalAfterHook: Hook = {
after: jest.fn(() => {
try {
// all hooks should have been called by now
expect(provider.hooks?.[0].after).toHaveBeenCalled();
expect(invocationAfterHook.after).toHaveBeenCalled();
expect(clientAfterHook.after).toHaveBeenCalled();
done();
} catch (err) {
done(err);
}
}),
};
const provider: Provider = {
metadata: {
name: 'mock-hooks-after-with-hooks',
},
hooks: [
{
after: jest.fn(() => {
try {
// not invocation, nor client, nor global should have run at this point.
expect(globalAfterHook.after).not.toHaveBeenCalled();
expect(clientAfterHook.after).not.toHaveBeenCalled();
expect(invocationAfterHook.after).not.toHaveBeenCalled();
} catch (err) {
done(err);
}
}),
},
],
resolveBooleanEvaluation: jest.fn((): Promise<ResolutionDetails<boolean>> => {
return Promise.resolve({
value: BOOLEAN_VALUE,
variant: BOOLEAN_VARIANT,
reason: REASON,
});
}),
} as unknown as Provider;
OpenFeature.setProvider(provider);
OpenFeature.clearHooks();
client.clearHooks();
OpenFeature.addHooks(globalAfterHook);
client.addHooks(clientAfterHook);
client.getBooleanValue(FLAG_KEY, false, undefined, {
hooks: [invocationAfterHook],
});
});
it('"error" must run hook in order provider, invocation, client, global', (done) => {
const invocationErrorHook: Hook = {
error: jest.fn(() => {
try {
// provider should have run.
expect(errorProviderWithHooks.hooks?.[0].error).toHaveBeenCalled();
// neither client, nor global should have run at this point.
expect(clientErrorHook.error).not.toHaveBeenCalled();
expect(globalErrorHook.error).not.toHaveBeenCalled();
} catch (err) {
done(err);
}
}),
};
const clientErrorHook: Hook = {
error: jest.fn(() => {
try {
// provider and invocation should have run, but not global
expect(errorProviderWithHooks.hooks?.[0].error).toHaveBeenCalled();
expect(invocationErrorHook.error).toHaveBeenCalled();
expect(globalErrorHook.error).not.toHaveBeenCalled();
} catch (err) {
done(err);
}
}),
};
const globalErrorHook: Hook = {
error: jest.fn(() => {
try {
// all hooks should have been called by now
expect(errorProviderWithHooks.hooks?.[0].error).toHaveBeenCalled();
expect(invocationErrorHook.error).toHaveBeenCalled();
expect(clientErrorHook.error).toHaveBeenCalled();
done();
} catch (err) {
done(err);
}
}),
};
const errorProviderWithHooks = {
metadata: {
name: 'mock-hooks-error-with-hooks',
},
hooks: [
{
error: jest.fn(() => {
try {
// not invocation, nor client, nor global should have run at this point.
expect(invocationErrorHook.error).not.toHaveBeenCalled();
expect(clientErrorHook.error).not.toHaveBeenCalled();
expect(globalErrorHook.error).not.toHaveBeenCalled();
} catch (err) {
done(err);
}
}),
},
],
resolveBooleanEvaluation: jest.fn((): Promise<ResolutionDetails<boolean>> => {
return Promise.reject({
reason: StandardResolutionReasons.ERROR,
errorCode: ErrorCode.INVALID_CONTEXT,
});
}),
} as unknown as Provider;
OpenFeature.setProvider(errorProviderWithHooks);
OpenFeature.clearHooks();
client.clearHooks();
OpenFeature.addHooks(globalErrorHook);
client.addHooks(clientErrorHook);
client.getBooleanValue(FLAG_KEY, false, undefined, {
hooks: [invocationErrorHook],
});
});
it('"finally" must run hook in order provider, invocation, client, global', (done) => {
const clientFinallyHook: Hook = {
finally: jest.fn(() => {
try {
// provider and invocation should have run, but not global
expect(errorProviderWithHooks.hooks?.[0].finally).toHaveBeenCalled();
expect(invocationFinallyHook.finally).toHaveBeenCalled();
expect(globalFinallyHook.finally).not.toHaveBeenCalled();
} catch (err) {
done(err);
}
}),
};
const globalFinallyHook: Hook = {
finally: jest.fn(() => {
try {
// all hooks should have been called by now
expect(errorProviderWithHooks.hooks?.[0].finally).toHaveBeenCalled();
expect(invocationFinallyHook.finally).toHaveBeenCalled();
expect(clientFinallyHook.finally).toHaveBeenCalled();
done();
} catch (err) {
done(err);
}
}),
};
const invocationFinallyHook: Hook = {
finally: jest.fn(() => {
try {
// provider hooks should have run.
expect(errorProviderWithHooks.hooks?.[0].finally).toHaveBeenCalled();
// neither client, nor global should have run at this point.
expect(clientFinallyHook.finally).not.toHaveBeenCalled();
expect(globalFinallyHook.finally).not.toHaveBeenCalled();
} catch (err) {
done(err);
}
}),
};
const errorProviderWithHooks = {
metadata: {
name: 'mock-hooks-finally-with-hooks',
},
hooks: [
{
finally: jest.fn(() => {
try {
// not invocation, nor client, nor global should have run at this point.
expect(invocationFinallyHook.finally).not.toHaveBeenCalled();
expect(clientFinallyHook.finally).not.toHaveBeenCalled();
expect(globalFinallyHook.finally).not.toHaveBeenCalled();
} catch (err) {
done(err);
}
}),
},
],
resolveBooleanEvaluation: jest.fn((): Promise<ResolutionDetails<boolean>> => {
return Promise.reject({
reason: StandardResolutionReasons.ERROR,
errorCode: ErrorCode.PROVIDER_NOT_READY,
});
}),
} as unknown as Provider;
OpenFeature.setProvider(errorProviderWithHooks);
OpenFeature.clearHooks();
client.clearHooks();
OpenFeature.addHooks(globalFinallyHook);
client.addHooks(clientFinallyHook);
client.getBooleanValue(FLAG_KEY, false, undefined, {
hooks: [invocationFinallyHook],
});
});
});
describe('Requirement 4.4.3', () => {
it('all "finally" hooks must execute, despite errors', (done) => {
OpenFeature.setProvider(MOCK_PROVIDER);
OpenFeature.clearHooks();
client.clearHooks();
const firstFinallyHook: Hook = {
finally: jest.fn(() => {
throw new Error('expected');
}),
};
const secondFinallyHook: Hook = {
finally: () => {
try {
expect(firstFinallyHook.finally).toHaveBeenCalled();
done();
} catch (err) {
done(err);
}
},
};
client.getBooleanValue(FLAG_KEY, false, undefined, {
hooks: [secondFinallyHook, firstFinallyHook], // remember error hooks run in reverse order
});
});
});
describe('Requirement 4.4.4', () => {
it('all "error" hooks must execute, despite errors', (done) => {
OpenFeature.setProvider(MOCK_ERROR_PROVIDER);
OpenFeature.clearHooks();
client.clearHooks();
const firstErrorHook: Hook = {
error: jest.fn(() => {
throw new Error('expected');
}),
};
const secondErrorHook: Hook = {
error: () => {
try {
expect(firstErrorHook.error).toHaveBeenCalled();
done();
} catch (err) {
done(err);
}
},
};
client.getBooleanValue(FLAG_KEY, false, undefined, {
hooks: [secondErrorHook, firstErrorHook], // remember error hooks run in reverse order
});
});
});
describe('Requirement 4.4.5', () => {
it('"before" must trigger error hook', (done) => {
OpenFeature.setProvider(MOCK_PROVIDER);
OpenFeature.clearHooks();
client.clearHooks();
const beforeAndErrorHook: Hook = {
before: jest.fn(() => {
throw new Error('Fake error');
}),
error: jest.fn(() => {
try {
expect(beforeAndErrorHook.before).toHaveBeenCalled();
done();
} catch (err) {
done(err);
}
}),
};
client.getBooleanValue(FLAG_KEY, false, undefined, {
hooks: [beforeAndErrorHook],
});
});
it('"after" must trigger error hook', (done) => {
OpenFeature.setProvider(MOCK_PROVIDER);
OpenFeature.clearHooks();
client.clearHooks();
const afterAndErrorHook: Hook = {
after: jest.fn(() => {
throw new Error('Fake error');
}),
error: jest.fn(() => {
try {
expect(afterAndErrorHook.after).toHaveBeenCalled();
done();
} catch (err) {
done(err);
}
}),
};
client.getBooleanValue(FLAG_KEY, false, undefined, {
hooks: [afterAndErrorHook],
});
});
});
describe('Requirement 4.4.6', () => {
it('remaining before/after hooks must not run after error', (done) => {
OpenFeature.setProvider(MOCK_PROVIDER);
OpenFeature.clearHooks();
client.clearHooks();
const clientBeforeHook: Hook = {
before: jest.fn(() => {
throw new Error('Fake error!');
}),
};
const beforeAndErrorHook: Hook = {
error: jest.fn(() => {
try {
// our subsequent "before" hook should not have run.
expect(beforeAndErrorHook.before).not.toHaveBeenCalled();
done();
} catch (err) {
done(err);
}
}),
before: jest.fn(() => {
done(new Error('Should not have run!'));
}),
};
client.getBooleanValue(FLAG_KEY, false, undefined, {
hooks: [clientBeforeHook, beforeAndErrorHook],
});
});
});
describe('Requirement 4.5.1, 4.5.2, 4.5.3', () => {
it('HookHints should be passed to each hook', (done) => {
OpenFeature.setProvider(MOCK_PROVIDER);
OpenFeature.clearHooks();
client.clearHooks();
client.getBooleanValue(FLAG_KEY, false, undefined, {
hooks: [
{
before: (_, hookHints) => {
try {
expect(hookHints?.hint).toBeTruthy();
} catch (err) {
done(err);
}
},
after: (_hookContext, _evaluationDetails, hookHints) => {
try {
expect(hookHints?.hint).toBeTruthy();
} catch (err) {
done(err);
}
},
finally: (_, _evaluationDetails, hookHints) => {
try {
expect(hookHints?.hint).toBeTruthy();
done();
} catch (err) {
done(err);
}
},
},
],
hookHints: {
hint: true,
},
});
});
});
describe('Requirement 5.4', () => {
it('HookHints should be immutable', (done) => {
OpenFeature.setProvider(MOCK_PROVIDER);
OpenFeature.clearHooks();
client.clearHooks();
client.getBooleanValue(FLAG_KEY, false, undefined, {
hooks: [
{
before: (_, hookHints) => {
try {
expect(hookHints?.hint).toBeTruthy();
} catch (err) {
done(err);
}
try {
// cast this so we can attempt to modify it.
(hookHints as { hint: boolean }).hint = false;
done(new Error('Expected error, "hookHints" to be immutable.'));
} catch (err) {
// expect an error since we are modifying a frozen object.
done();
}
},
},
],
hookHints: {
hint: true,
},
});
});
});
describe('async hooks', () => {
describe('before, after, finally', () => {
it('should be awaited, run in order', (done) => {
OpenFeature.setProvider(MOCK_PROVIDER);
const asyncBeforeAfterFinally: Hook = {
before: jest.fn(() => {
return new Promise<EvaluationContext>((resolve) =>
setTimeout(() => {
resolve({ beforeRan: true });
}, 100),
);
}),
after: jest.fn((hookContext) => {
try {
expect(asyncBeforeAfterFinally.before).toHaveBeenCalled();
expect(hookContext.context.beforeRan).toBeTruthy();
return Promise.resolve();
} catch (err) {
done(err);
}
}),
finally: () => {
try {
expect(asyncBeforeAfterFinally.after).toHaveBeenCalled();
done();
} catch (err) {
done(err);
}
},
};
client.getBooleanValue(FLAG_KEY, false, undefined, {
hooks: [asyncBeforeAfterFinally],
});
});
});
describe('before, error, finally', () => {
it('should be awaited, run in order', (done) => {
OpenFeature.setProvider(MOCK_PROVIDER);
const asyncBeforeErrorFinally = {
before: jest.fn(() => {
return new Promise<EvaluationContext>((resolve, reject) =>
setTimeout(() => {
reject();
}, 100),
);
}),
error: jest.fn(() => {
try {
expect(asyncBeforeErrorFinally.before).toHaveBeenCalled();
} catch (err) {
done(err);
}
return Promise.resolve();
}),
finally: () => {
try {
expect(asyncBeforeErrorFinally.error).toHaveBeenCalled();
done();
} catch (err) {
done(err);
}
},
};
client.getBooleanValue(FLAG_KEY, false, undefined, {
hooks: [asyncBeforeErrorFinally],
});
});
});
});
});