828 lines
24 KiB
TypeScript
828 lines
24 KiB
TypeScript
import type { Provider, ResolutionDetails, Client, FlagValueType, EvaluationContext, Hook } from '../src';
|
|
import { GeneralError, 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((): ResolutionDetails<boolean> => {
|
|
return {
|
|
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((): ResolutionDetails<boolean> => {
|
|
throw new GeneralError();
|
|
}),
|
|
} 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(async () => {
|
|
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, {
|
|
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, {
|
|
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, {
|
|
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('after', () => {
|
|
it('evaluationContext must be immutable', (done) => {
|
|
client.getBooleanValue(FLAG_KEY, false, {
|
|
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('Requirement 4.3.5', () => {
|
|
it('"after" must run after flag evaluation', (done) => {
|
|
client.getBooleanValue(FLAG_KEY, false, {
|
|
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, {
|
|
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', () => {
|
|
(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,
|
|
}),
|
|
);
|
|
});
|
|
});
|
|
});
|
|
|
|
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, {
|
|
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, {
|
|
hooks: [errorAndFinallyHook],
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('Requirement 4.3.8', () => {
|
|
it('"evaluation details" passed to the "finally" stage matches the evaluation details returned to the application author', () => {
|
|
OpenFeature.setProvider(MOCK_PROVIDER);
|
|
let evaluationDetailsHooks;
|
|
|
|
const evaluationDetails = 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((): ResolutionDetails<boolean> => {
|
|
return {
|
|
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, {
|
|
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((): ResolutionDetails<boolean> => {
|
|
return {
|
|
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, {
|
|
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((): ResolutionDetails<boolean> => {
|
|
throw new GeneralError();
|
|
}),
|
|
} as unknown as Provider;
|
|
|
|
OpenFeature.setProvider(errorProviderWithHooks);
|
|
OpenFeature.clearHooks();
|
|
client.clearHooks();
|
|
|
|
OpenFeature.addHooks(globalErrorHook);
|
|
client.addHooks(clientErrorHook);
|
|
|
|
client.getBooleanValue(FLAG_KEY, false, {
|
|
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((): ResolutionDetails<boolean> => {
|
|
throw new GeneralError();
|
|
}),
|
|
} as unknown as Provider;
|
|
OpenFeature.setProvider(errorProviderWithHooks);
|
|
OpenFeature.clearHooks();
|
|
client.clearHooks();
|
|
|
|
OpenFeature.addHooks(globalFinallyHook);
|
|
client.addHooks(clientFinallyHook);
|
|
|
|
client.getBooleanValue(FLAG_KEY, false, {
|
|
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, {
|
|
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, {
|
|
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, {
|
|
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, {
|
|
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, {
|
|
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, {
|
|
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, {
|
|
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,
|
|
},
|
|
});
|
|
});
|
|
});
|
|
});
|