1082 lines
32 KiB
TypeScript
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],
|
|
});
|
|
});
|
|
});
|
|
});
|
|
});
|