1
0
mirror of https://github.com/Unleash/unleash.git synced 2025-07-02 01:17:58 +02:00
unleash.unleash/src/lib/util/offline-unleash-client.test.ts
Thomas Heartman e55ad1a21e
feat(#1873/playground): Return detailed information on feature toggle evaluation (#1839)
* Feat: return reasons why a feature evaluated to true or false

Note: this is very rough and just straight ripped from the nodejs
client. It will need a lot of work, but is a good place to start

* Feat: add suggested shape for new payload

* Chore: minor cleanup

* Wip: make server compile again

* Remove unused schema ref

* Export new schemas

* Chore: fix some tests to use sub property

* Fix: fix some tests

* Refactor: rename some variables, uncomment some stuff

* Add segments type to bootstrap options

* Add segments capability to offline feature evaluator

* Fix function calls after turning params into an option abject

* Feat: test strategy order, etc

* Feat: add test to check that all strats are returned correctly

* Feat: allow you to include strategy ids in clients

* Wip: hook up segments in the offline client.

Note: compared to regular clients, they still fail

* Feat: add segments validation

* Fix: fix test case invariant.

* Chore: revert to returning only `boolean` from strategies.

This _should_ make it work with custom strategies too 🤞

* Feat: make more properties of the returned feature required

* Wip: add some comments and unfinished tests for edge cases

* Feat: add `isEnabledInCurrentEnvironment` prop

* Feat: consider more strategy failure cases

* Feat: test that isenabledinenvironment matches expectations

* Feat: add unknown strategies

* Fix: fix property access typo

* Feat: add unknown strategy for fallback purposes

* Feat: test edge case: all unknown strategies

* Feat: add custom strategy to arbitrary

* Feat: test that features can be true, even if not enabled in env

* Chore: add some comments

* Wip: fix sdk tests

* Remove comments, improve test logging

* Feat: add descriptions and examples to playground feature schema

* Switch `examples` for `example`

* Update schemas with descriptions and examples

* Fix: update snapshot

* Fix: openapi example

* Fix: merge issues

* Fix: fix issue where feature evaluation state was wrong

* Chore: update openapi spec

* Fix: fix broken offline client tests

* Refactor: move schemas into separate files

* Refactor: remove "reason" for incomplete evaluation.

The only instances where evaluation is incomplete is when we don't
know what the strategy is.

* Refactor: move unleash node client into test and dev dependencies

* Wip: further removal of stuff

* Chore: remove a bunch of code that we don't use

* Chore: remove comment

* Chore: remove unused code

* Fix: fix some prettier errors

* Type parameters in strategies to avoid `any`

* Fix: remove commented out code

* Feat: make `id` required on playground strategies

* Chore: remove redundant type

* Fix: remove redundant if and fix fallback evaluation

* Refactor: reduce nesting and remove duplication

* Fix: remove unused helper function

* Refactor: type `parameters` as `unknown`

* Chore: remove redundant comment

* Refactor: move constraint code into a separate file

* Refactor: rename `unleash` -> `feature-evaluator`

* Rename class `Unleash` -> `FeatureEvaluator`

* Refactor: remove this.ready and sync logic from feature evaluator

* Refactor: remove unused code, rename config type

* Refactor: remove event emission from the Unleash client

* Remove unlistened-for events in feature evaluator

* Refactor: make offline client synchronous; remove code

* Fix: update openapi snapshot after adding required strategy ids

* Feat: change `strategies` format.

This commit changes the format of a playground feature's `strategies`
properties from a list of strategies to an object with properties
`result` and `data`. It looks a bit like this:

```ts
type Strategies = {
  result: boolean | "unknown",
  data: Strategy[]
}
```

The reason is that this allows us to avoid the breaking change that
was previously suggested in the PR:

`feature.isEnabled` used to be a straight boolean. Then, when we found
out we couldn't necessarily evaluate all strategies (custom strats are
hard!) we changed it to `boolean | 'unevaluated'`. However, this is
confusing on a few levels as the playground results are no longer the
same as the SDK would be, nor are they strictly boolean anymore.

This change reverts the `isEnabled` functionality to what it was
before (so it's always a mirror of what the SDK would show).
The equivalent of `feature.isEnabled === 'unevaluated'` now becomes
`feature.isEnabled && strategy.result === 'unknown'`.

* Fix: Fold long string descriptions over multiple lines.

* Fix: update snapshot after adding line breaks to descriptions
2022-08-04 15:41:52 +02:00

397 lines
13 KiB
TypeScript

import {
ClientInitOptions,
mapFeaturesForBootstrap,
mapSegmentsForBootstrap,
offlineUnleashClient,
} from './offline-unleash-client';
import {
Unleash as UnleashClientNode,
InMemStorageProvider as InMemStorageProviderNode,
} from 'unleash-client';
import { once } from 'events';
import { playgroundStrategyEvaluation } from '../openapi/spec/playground-strategy-schema';
export const offlineUnleashClientNode = async ({
features,
context,
logError,
segments,
}: ClientInitOptions): Promise<UnleashClientNode> => {
const client = new UnleashClientNode({
...context,
appName: context.appName,
disableMetrics: true,
refreshInterval: 0,
url: 'not-needed',
storageProvider: new InMemStorageProviderNode(),
bootstrap: {
data: mapFeaturesForBootstrap(features),
segments: mapSegmentsForBootstrap(segments),
},
});
client.on('error', logError);
client.start();
await once(client, 'ready');
return client;
};
describe('offline client', () => {
it('considers enabled variants with a default strategy to be on', async () => {
const name = 'toggle-name';
const client = await offlineUnleashClient({
features: [
{
name,
enabled: true,
strategies: [{ name: 'default' }],
variants: [],
type: '',
stale: false,
},
],
context: { appName: 'other-app', environment: 'default' },
logError: console.log,
});
expect(client.isEnabled(name).result).toBeTruthy();
});
it('constrains on appName', async () => {
const enabledFeature = 'toggle-name';
const disabledFeature = 'other-toggle';
const appName = 'app-name';
const client = await offlineUnleashClient({
features: [
{
name: enabledFeature,
enabled: true,
strategies: [
{
name: 'default',
constraints: [
{
contextName: 'appName',
operator: 'IN',
values: [appName],
},
],
},
],
variants: [],
type: '',
stale: false,
},
{
name: disabledFeature,
enabled: true,
strategies: [
{
name: 'default',
constraints: [
{
contextName: 'appName',
operator: 'IN',
values: ['otherApp'],
},
],
},
],
variants: [],
type: '',
stale: false,
},
],
context: { appName, environment: 'default' },
logError: console.log,
});
expect(client.isEnabled(enabledFeature).result).toBeTruthy();
expect(client.isEnabled(disabledFeature).result).toBeFalsy();
});
it('considers disabled features with a default strategy to be enabled', async () => {
const name = 'toggle-name';
const context = { appName: 'client-test' };
const client = await offlineUnleashClient({
features: [
{
strategies: [
{
name: 'default',
},
],
stale: false,
enabled: false,
name,
type: 'experiment',
variants: [],
},
],
context,
logError: console.log,
});
const result = client.isEnabled(name, context);
expect(result.result).toBe(true);
});
it('considers disabled variants with a default strategy and variants to be on', async () => {
const name = 'toggle-name';
const client = await offlineUnleashClient({
features: [
{
strategies: [
{
name: 'default',
},
],
stale: false,
enabled: false,
name,
type: 'experiment',
variants: [
{
name: 'a',
weight: 500,
weightType: 'variable',
stickiness: 'default',
overrides: [],
},
{
name: 'b',
weight: 500,
weightType: 'variable',
stickiness: 'default',
overrides: [],
},
],
},
],
context: { appName: 'client-test' },
logError: console.log,
});
expect(client.isEnabled(name).result).toBe(true);
});
it("returns variant {name: 'disabled', enabled: false } if the toggle isn't enabled", async () => {
const name = 'toggle-name';
const client = await offlineUnleashClient({
features: [
{
strategies: [],
stale: false,
enabled: false,
name,
type: 'experiment',
variants: [
{
name: 'a',
weight: 500,
weightType: 'variable',
stickiness: 'default',
overrides: [],
},
{
name: 'b',
weight: 500,
weightType: 'variable',
stickiness: 'default',
overrides: [],
},
],
},
],
context: { appName: 'client-test' },
logError: console.log,
});
expect(client.isEnabled(name).result).toBeFalsy();
expect(client.getVariant(name).name).toEqual('disabled');
expect(client.getVariant(name).enabled).toBeFalsy();
});
it('returns the disabled variant if there are no variants', async () => {
const name = 'toggle-name';
const client = await offlineUnleashClient({
features: [
{
strategies: [
{
name: 'default',
constraints: [],
},
],
stale: false,
enabled: true,
name,
type: 'experiment',
variants: [],
},
],
context: { appName: 'client-test' },
logError: console.log,
});
expect(client.getVariant(name, {}).name).toEqual('disabled');
expect(client.getVariant(name, {}).enabled).toBeFalsy();
expect(client.isEnabled(name, {}).result).toBeTruthy();
});
it(`returns '${playgroundStrategyEvaluation.unknownResult}' if it can't evaluate a feature`, async () => {
const name = 'toggle-name';
const context = { appName: 'client-test' };
const client = await offlineUnleashClient({
features: [
{
strategies: [
{
name: 'unimplemented-custom-strategy',
constraints: [],
},
],
stale: false,
enabled: true,
name,
type: 'experiment',
variants: [],
},
],
context,
logError: console.log,
});
const result = client.isEnabled(name, context);
result.strategies.forEach((strategy) =>
expect(strategy.result.enabled).toEqual(
playgroundStrategyEvaluation.unknownResult,
),
);
expect(result.result).toEqual(
playgroundStrategyEvaluation.unknownResult,
);
});
it('returns strategies in the order they are provided', async () => {
const featureName = 'featureName';
const strategies = [
{
name: 'default',
constraints: [],
parameters: {},
},
{
name: 'default',
constraints: [
{
values: ['my-app-name'],
inverted: false,
operator: 'IN' as 'IN',
contextName: 'appName',
caseInsensitive: false,
},
],
parameters: {},
},
{
name: 'applicationHostname',
constraints: [],
parameters: {
hostNames: 'myhostname.com',
},
},
{
name: 'flexibleRollout',
constraints: [],
parameters: {
groupId: 'killer',
rollout: '34',
stickiness: 'userId',
},
},
{
name: 'userWithId',
constraints: [],
parameters: {
userIds: 'uoea,ueoa',
},
},
{
name: 'remoteAddress',
constraints: [],
parameters: {
IPs: '196.6.6.05',
},
},
];
const context = { appName: 'client-test' };
const client = await offlineUnleashClient({
features: [
{
strategies,
// impressionData: false,
enabled: true,
name: featureName,
// description: '',
// project: 'heartman-for-test',
stale: false,
type: 'kill-switch',
variants: [
{
name: 'a',
weight: 334,
weightType: 'variable',
stickiness: 'default',
overrides: [],
payload: {
type: 'json',
value: '{"hello": "world"}',
},
},
{
name: 'b',
weight: 333,
weightType: 'variable',
stickiness: 'default',
overrides: [],
payload: {
type: 'string',
value: 'ueoau',
},
},
{
name: 'c',
weight: 333,
weightType: 'variable',
stickiness: 'default',
payload: {
type: 'csv',
value: '1,2,3',
},
overrides: [],
},
],
},
],
context,
logError: console.log,
});
const evaluatedStrategies = client
.isEnabled(featureName, context)
.strategies.map((strategy) => strategy.name);
expect(evaluatedStrategies).toEqual(
strategies.map((strategy) => strategy.name),
);
});
});