587 lines
18 KiB
TypeScript
587 lines
18 KiB
TypeScript
import { FlagNotFoundError, InMemoryProvider, ProviderEvents, StandardResolutionReasons, TypeMismatchError } from '../src';
|
|
import { VariantFoundError } from '../src/provider/in-memory-provider/variant-not-found-error';
|
|
|
|
describe('in-memory provider', () => {
|
|
describe('boolean flags', () => {
|
|
const provider = new InMemoryProvider({});
|
|
it('resolves to default variant with reason static', async () => {
|
|
const booleanFlagSpec = {
|
|
'a-boolean-flag': {
|
|
variants: {
|
|
on: true,
|
|
off: false,
|
|
},
|
|
disabled: false,
|
|
defaultVariant: 'on',
|
|
},
|
|
};
|
|
provider.putConfiguration(booleanFlagSpec);
|
|
|
|
const resolution = await provider.resolveBooleanEvaluation('a-boolean-flag', true);
|
|
|
|
expect(resolution).toEqual({ value: true, reason: StandardResolutionReasons.STATIC, variant: 'on' });
|
|
});
|
|
|
|
it('throws FlagNotFound if flag does not exist', async () => {
|
|
const booleanFlagSpec = {
|
|
'a-boolean-flag': {
|
|
variants: {
|
|
on: true,
|
|
off: false,
|
|
},
|
|
disabled: false,
|
|
defaultVariant: 'on',
|
|
},
|
|
};
|
|
provider.putConfiguration(booleanFlagSpec);
|
|
|
|
await expect(provider.resolveBooleanEvaluation('another-boolean-flag', false)).rejects.toThrow();
|
|
});
|
|
|
|
it('resolves to default value with reason disabled if flag is disabled', async () => {
|
|
const booleanFlagDisabledSpec = {
|
|
'a-boolean-flag': {
|
|
variants: {
|
|
on: true,
|
|
off: false,
|
|
},
|
|
disabled: true,
|
|
defaultVariant: 'on',
|
|
},
|
|
};
|
|
provider.putConfiguration(booleanFlagDisabledSpec);
|
|
|
|
const resolution = await provider.resolveBooleanEvaluation('a-boolean-flag', false);
|
|
|
|
expect(resolution).toEqual({ value: false, reason: StandardResolutionReasons.DISABLED });
|
|
});
|
|
|
|
it('throws VariantFoundError if variant does not exist', async () => {
|
|
const booleanFlagDisabledSpec = {
|
|
'a-boolean-flag': {
|
|
variants: {
|
|
on: true,
|
|
off: false,
|
|
},
|
|
disabled: false,
|
|
defaultVariant: 'dummy',
|
|
},
|
|
};
|
|
provider.putConfiguration(booleanFlagDisabledSpec);
|
|
|
|
await expect(provider.resolveBooleanEvaluation('a-boolean-flag', false)).rejects.toThrow(VariantFoundError);
|
|
});
|
|
|
|
it('throws TypeMismatchError if variant type does not match with accessors', async () => {
|
|
const booleanFlagSpec = {
|
|
'a-boolean-flag': {
|
|
variants: {
|
|
on: 'yes',
|
|
off: 'no',
|
|
},
|
|
disabled: false,
|
|
defaultVariant: 'on',
|
|
},
|
|
};
|
|
provider.putConfiguration(booleanFlagSpec);
|
|
|
|
await expect(provider.resolveBooleanEvaluation('a-boolean-flag', false)).rejects.toThrow(TypeMismatchError);
|
|
});
|
|
|
|
it('resolves to variant value with reason target match if context is provided and flag spec has context evaluator', async () => {
|
|
const booleanFlagCtxSpec = {
|
|
'a-boolean-flag': {
|
|
variants: {
|
|
on: true,
|
|
off: false,
|
|
},
|
|
disabled: false,
|
|
defaultVariant: 'on',
|
|
contextEvaluator: () => 'off',
|
|
},
|
|
};
|
|
provider.putConfiguration(booleanFlagCtxSpec);
|
|
const dummyContext = {};
|
|
|
|
const resolution = await provider.resolveBooleanEvaluation('a-boolean-flag', true, dummyContext);
|
|
|
|
expect(resolution).toEqual({ value: false, reason: StandardResolutionReasons.TARGETING_MATCH, variant: 'off' });
|
|
});
|
|
});
|
|
|
|
describe('string flags', () => {
|
|
const provider = new InMemoryProvider({});
|
|
const itsDefault = "it's deafault";
|
|
const itsOn = "it's on";
|
|
const itsOff = "it's off";
|
|
it('resolves to default variant with reason static', async () => {
|
|
const stringFlagSpec = {
|
|
'a-string-flag': {
|
|
variants: {
|
|
on: itsOn,
|
|
off: itsOff,
|
|
},
|
|
disabled: false,
|
|
defaultVariant: 'on',
|
|
},
|
|
};
|
|
provider.putConfiguration(stringFlagSpec);
|
|
|
|
const resolution = await provider.resolveStringEvaluation('a-string-flag', itsDefault);
|
|
|
|
expect(resolution).toEqual({ value: itsOn, reason: StandardResolutionReasons.STATIC, variant: 'on' });
|
|
});
|
|
|
|
it('throws FlagNotFound if flag does not exist', async () => {
|
|
const StringFlagSpec = {
|
|
'a-string-flag': {
|
|
variants: {
|
|
on: true,
|
|
off: false,
|
|
},
|
|
disabled: false,
|
|
defaultVariant: 'on',
|
|
},
|
|
};
|
|
|
|
provider.putConfiguration(StringFlagSpec);
|
|
|
|
await expect(provider.resolveStringEvaluation('another-string-flag', itsDefault)).rejects.toThrow(
|
|
FlagNotFoundError,
|
|
);
|
|
});
|
|
|
|
it('resolves to default value with reason disabled if flag is disabled', async () => {
|
|
const StringFlagDisabledSpec = {
|
|
'a-string-flag': {
|
|
variants: {
|
|
on: itsOn,
|
|
off: itsOff,
|
|
},
|
|
disabled: true,
|
|
defaultVariant: 'on',
|
|
},
|
|
};
|
|
provider.putConfiguration(StringFlagDisabledSpec);
|
|
|
|
const resolution = await provider.resolveStringEvaluation('a-string-flag', itsDefault);
|
|
|
|
expect(resolution).toEqual({ value: itsDefault, reason: StandardResolutionReasons.DISABLED });
|
|
});
|
|
|
|
it('throws VariantFoundError if variant does not exist', async () => {
|
|
const StringFlagSpec = {
|
|
'a-string-flag': {
|
|
variants: {
|
|
on: itsOn,
|
|
off: itsOff,
|
|
},
|
|
disabled: false,
|
|
defaultVariant: 'dummy',
|
|
},
|
|
};
|
|
provider.putConfiguration(StringFlagSpec);
|
|
|
|
await expect(provider.resolveStringEvaluation('a-string-flag', itsDefault)).rejects.toThrow(VariantFoundError);
|
|
});
|
|
|
|
it('throws TypeMismatchError if variant does not match with accessor method type', async () => {
|
|
const StringFlagSpec = {
|
|
'a-string-flag': {
|
|
variants: {
|
|
on: true,
|
|
off: false,
|
|
},
|
|
disabled: false,
|
|
defaultVariant: 'on',
|
|
},
|
|
};
|
|
|
|
provider.putConfiguration(StringFlagSpec);
|
|
|
|
await expect(provider.resolveStringEvaluation('a-string-flag', itsDefault)).rejects.toThrow(TypeMismatchError);
|
|
});
|
|
|
|
it('resolves to variant value with reason target match if context is provided and flag spec has context evaluator', async () => {
|
|
const StringFlagCtxSpec = {
|
|
'a-string-flag': {
|
|
variants: {
|
|
on: itsOn,
|
|
off: itsOff,
|
|
},
|
|
disabled: false,
|
|
defaultVariant: 'on',
|
|
contextEvaluator: () => 'off',
|
|
},
|
|
};
|
|
provider.putConfiguration(StringFlagCtxSpec);
|
|
const dummyContext = {};
|
|
|
|
const resolution = await provider.resolveStringEvaluation('a-string-flag', itsDefault, dummyContext);
|
|
|
|
expect(resolution).toEqual({ value: itsOff, reason: StandardResolutionReasons.TARGETING_MATCH, variant: 'off' });
|
|
});
|
|
});
|
|
|
|
describe('number flags', () => {
|
|
const provider = new InMemoryProvider({});
|
|
const defaultNumber = 42;
|
|
const onNumber = -528;
|
|
const offNumber = 0;
|
|
it('resolves to default variant with reason static', async () => {
|
|
const numberFlagSpec = {
|
|
'a-number-flag': {
|
|
variants: {
|
|
on: onNumber,
|
|
off: offNumber,
|
|
},
|
|
disabled: false,
|
|
defaultVariant: 'on',
|
|
},
|
|
};
|
|
provider.putConfiguration(numberFlagSpec);
|
|
|
|
const resolution = await provider.resolveNumberEvaluation('a-number-flag', defaultNumber);
|
|
|
|
expect(resolution).toEqual({ value: onNumber, reason: StandardResolutionReasons.STATIC, variant: 'on' });
|
|
});
|
|
|
|
it('throws FlagNotFound if flag does not exist', async () => {
|
|
const numberFlagSpec = {
|
|
'a-number-flag': {
|
|
variants: {
|
|
on: onNumber,
|
|
off: offNumber,
|
|
},
|
|
disabled: false,
|
|
defaultVariant: 'dummy',
|
|
},
|
|
};
|
|
provider.putConfiguration(numberFlagSpec);
|
|
|
|
await expect(provider.resolveNumberEvaluation('another-number-flag', defaultNumber)).rejects.toThrow(
|
|
FlagNotFoundError,
|
|
);
|
|
});
|
|
|
|
it('resolves to default value with reason disabled if flag is disabled', async () => {
|
|
const numberFlagDisabledSpec = {
|
|
'a-number-flag': {
|
|
variants: {
|
|
on: onNumber,
|
|
off: offNumber,
|
|
},
|
|
disabled: true,
|
|
defaultVariant: 'on',
|
|
},
|
|
};
|
|
provider.putConfiguration(numberFlagDisabledSpec);
|
|
|
|
const resolution = await provider.resolveNumberEvaluation('a-number-flag', defaultNumber);
|
|
|
|
expect(resolution).toEqual({ value: defaultNumber, reason: StandardResolutionReasons.DISABLED });
|
|
});
|
|
|
|
it('throws VariantNotFoundError if variant does not exist', async () => {
|
|
const numberFlagSpec = {
|
|
'a-number-flag': {
|
|
variants: {
|
|
on: onNumber,
|
|
off: offNumber,
|
|
},
|
|
disabled: false,
|
|
defaultVariant: 'dummy',
|
|
},
|
|
};
|
|
provider.putConfiguration(numberFlagSpec);
|
|
|
|
await expect(provider.resolveNumberEvaluation('a-number-flag', defaultNumber)).rejects.toThrow(VariantFoundError);
|
|
});
|
|
|
|
it('throws TypeMismatchError if variant does not match with accessor method type', async () => {
|
|
const numberFlagSpec = {
|
|
'a-number-flag': {
|
|
variants: {
|
|
on: true,
|
|
off: false,
|
|
},
|
|
disabled: false,
|
|
defaultVariant: 'on',
|
|
},
|
|
};
|
|
|
|
provider.putConfiguration(numberFlagSpec);
|
|
|
|
await expect(provider.resolveNumberEvaluation('a-number-flag', defaultNumber)).rejects.toThrow(TypeMismatchError);
|
|
});
|
|
|
|
it('resolves to variant value with reason target match if context is provided and flag spec has context evaluator', async () => {
|
|
const numberFlagCtxSpec = {
|
|
'a-number-flag': {
|
|
variants: {
|
|
on: onNumber,
|
|
off: offNumber,
|
|
},
|
|
disabled: false,
|
|
defaultVariant: 'on',
|
|
contextEvaluator: () => 'off',
|
|
},
|
|
};
|
|
provider.putConfiguration(numberFlagCtxSpec);
|
|
const dummyContext = {};
|
|
|
|
const resolution = await provider.resolveNumberEvaluation('a-number-flag', defaultNumber, dummyContext);
|
|
|
|
expect(resolution).toEqual({
|
|
value: offNumber,
|
|
reason: StandardResolutionReasons.TARGETING_MATCH,
|
|
variant: 'off',
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('Object flags', () => {
|
|
const provider = new InMemoryProvider({});
|
|
const defaultObject = { someKey: 'default' };
|
|
const onObject = { someKey: 'on' };
|
|
const offObject = { someKey: 'off' };
|
|
it('resolves to default variant with reason static', async () => {
|
|
const ObjectFlagSpec = {
|
|
'a-object-flag': {
|
|
variants: {
|
|
on: onObject,
|
|
off: offObject,
|
|
},
|
|
disabled: false,
|
|
defaultVariant: 'on',
|
|
},
|
|
};
|
|
provider.putConfiguration(ObjectFlagSpec);
|
|
|
|
const resolution = await provider.resolveObjectEvaluation('a-object-flag', defaultObject);
|
|
|
|
expect(resolution).toEqual({ value: onObject, reason: StandardResolutionReasons.STATIC, variant: 'on' });
|
|
});
|
|
|
|
it('throws FlagNotFound if flag does not exist', async () => {
|
|
const ObjectFlagSpec = {
|
|
'a-Object-flag': {
|
|
variants: {
|
|
on: onObject,
|
|
off: offObject,
|
|
},
|
|
disabled: false,
|
|
defaultVariant: 'dummy',
|
|
},
|
|
};
|
|
provider.putConfiguration(ObjectFlagSpec);
|
|
|
|
await expect(provider.resolveObjectEvaluation('another-number-flag', defaultObject)).rejects.toThrow(
|
|
FlagNotFoundError,
|
|
);
|
|
});
|
|
|
|
it('resolves to default value with reason disabled if flag is disabled', async () => {
|
|
const ObjectFlagDisabledSpec = {
|
|
'a-object-flag': {
|
|
variants: {
|
|
on: onObject,
|
|
off: offObject,
|
|
},
|
|
disabled: true,
|
|
defaultVariant: 'on',
|
|
},
|
|
};
|
|
provider.putConfiguration(ObjectFlagDisabledSpec);
|
|
|
|
const resolution = await provider.resolveObjectEvaluation('a-object-flag', defaultObject);
|
|
|
|
expect(resolution).toEqual({ value: defaultObject, reason: StandardResolutionReasons.DISABLED });
|
|
});
|
|
|
|
it('throws VariantFoundError if variant does not exist', async () => {
|
|
const ObjectFlagSpec = {
|
|
'a-Object-flag': {
|
|
variants: {
|
|
on: onObject,
|
|
off: offObject,
|
|
},
|
|
disabled: false,
|
|
defaultVariant: 'dummy',
|
|
},
|
|
};
|
|
provider.putConfiguration(ObjectFlagSpec);
|
|
|
|
await expect(provider.resolveObjectEvaluation('a-Object-flag', defaultObject)).rejects.toThrow(VariantFoundError);
|
|
});
|
|
|
|
it('throws TypeMismatchError if variant does not match with accessor method type', async () => {
|
|
const ObjectFlagSpec = {
|
|
'a-object-flag': {
|
|
variants: {
|
|
on: true,
|
|
off: false,
|
|
},
|
|
disabled: false,
|
|
defaultVariant: 'on',
|
|
},
|
|
};
|
|
|
|
provider.putConfiguration(ObjectFlagSpec);
|
|
|
|
await expect(provider.resolveObjectEvaluation('a-object-flag', defaultObject)).rejects.toThrow(TypeMismatchError);
|
|
});
|
|
|
|
it('resolves to variant value with reason target match if context is provided and flag spec has context evaluator', async () => {
|
|
const ObjectFlagCtxSpec = {
|
|
'a-object-flag': {
|
|
variants: {
|
|
on: onObject,
|
|
off: offObject,
|
|
},
|
|
disabled: false,
|
|
defaultVariant: 'on',
|
|
contextEvaluator: () => 'off',
|
|
},
|
|
};
|
|
provider.putConfiguration(ObjectFlagCtxSpec);
|
|
const dummyContext = {};
|
|
|
|
const resolution = await provider.resolveObjectEvaluation('a-object-flag', defaultObject, dummyContext);
|
|
|
|
expect(resolution).toEqual({
|
|
value: offObject,
|
|
reason: StandardResolutionReasons.TARGETING_MATCH,
|
|
variant: 'off',
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('events', () => {
|
|
it('emits provider changed event if a new value is added', (done) => {
|
|
const flagsSpec = {
|
|
'some-flag': {
|
|
variants: {
|
|
on: 'initial-value',
|
|
},
|
|
defaultVariant: 'on',
|
|
disabled: false,
|
|
},
|
|
};
|
|
const provider = new InMemoryProvider(flagsSpec);
|
|
|
|
provider.events.addHandler(ProviderEvents.ConfigurationChanged, (details) => {
|
|
expect(details?.flagsChanged).toEqual(['some-other-flag']);
|
|
done();
|
|
});
|
|
|
|
const newFlag = {
|
|
'some-other-flag': {
|
|
variants: {
|
|
off: 'some-other-value',
|
|
},
|
|
defaultVariant: 'off',
|
|
disabled: false,
|
|
},
|
|
};
|
|
|
|
const newflagsSpec = { ...flagsSpec, ...newFlag };
|
|
provider.putConfiguration(newflagsSpec);
|
|
});
|
|
|
|
it('emits provider changed event if an existing value is changed', (done) => {
|
|
const flagsSpec = {
|
|
'some-flag': {
|
|
variants: {
|
|
on: 'initial-value',
|
|
},
|
|
defaultVariant: 'on',
|
|
disabled: false,
|
|
},
|
|
};
|
|
const provider = new InMemoryProvider(flagsSpec);
|
|
|
|
provider.events.addHandler(ProviderEvents.ConfigurationChanged, (details) => {
|
|
expect(details?.flagsChanged).toEqual(['some-flag']);
|
|
done();
|
|
});
|
|
|
|
const newFlagSpec = {
|
|
'some-flag': {
|
|
variants: {
|
|
off: 'some-other-value',
|
|
},
|
|
defaultVariant: 'off',
|
|
disabled: false,
|
|
},
|
|
};
|
|
provider.putConfiguration(newFlagSpec);
|
|
});
|
|
});
|
|
|
|
describe('Flags configuration', () => {
|
|
it('reflects changes in flag configuration', async () => {
|
|
const provider = new InMemoryProvider({
|
|
'some-flag': {
|
|
variants: {
|
|
on: 'initial-value',
|
|
},
|
|
defaultVariant: 'on',
|
|
disabled: false,
|
|
},
|
|
});
|
|
|
|
const firstResolution = await provider.resolveStringEvaluation('some-flag', 'deafaultFirstResolution');
|
|
|
|
expect(firstResolution).toEqual({
|
|
value: 'initial-value',
|
|
reason: StandardResolutionReasons.STATIC,
|
|
variant: 'on',
|
|
});
|
|
|
|
provider.putConfiguration({
|
|
'some-flag': {
|
|
variants: {
|
|
on: 'new-value',
|
|
},
|
|
defaultVariant: 'on',
|
|
disabled: false,
|
|
},
|
|
});
|
|
|
|
const secondResolution = await provider.resolveStringEvaluation('some-flag', 'defaultSecondResolution');
|
|
|
|
expect(secondResolution).toEqual({ value: 'new-value', reason: StandardResolutionReasons.STATIC, variant: 'on' });
|
|
});
|
|
|
|
it('does not let you change values with the configuration passed by reference', async () => {
|
|
const flagsSpec = {
|
|
'some-flag': {
|
|
variants: {
|
|
on: 'initial-value',
|
|
},
|
|
defaultVariant: 'on',
|
|
disabled: false,
|
|
},
|
|
};
|
|
|
|
const substituteSpec = {
|
|
variants: {
|
|
on: 'some-other-value',
|
|
},
|
|
defaultVariant: 'on',
|
|
disabled: false,
|
|
};
|
|
|
|
const provider = new InMemoryProvider(flagsSpec);
|
|
|
|
// I passed configuration by reference, so maybe I can mess
|
|
// with it behind the providers back!
|
|
flagsSpec['some-flag'] = substituteSpec;
|
|
|
|
const resolution = await provider.resolveStringEvaluation('some-flag', 'default value');
|
|
expect(resolution).toEqual({ value: 'initial-value', reason: StandardResolutionReasons.STATIC, variant: 'on' });
|
|
});
|
|
});
|
|
});
|