1
0
mirror of https://github.com/Unleash/unleash.git synced 2024-11-01 19:07:38 +01:00
unleash.unleash/src/test/arbitraries.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

279 lines
9.5 KiB
TypeScript

import fc, { Arbitrary } from 'fast-check';
import { ALL_OPERATORS } from '../lib/util/constants';
import { ClientFeatureSchema } from '../lib/openapi/spec/client-feature-schema';
import { IVariant, WeightType } from '../lib/types/model';
import { FeatureStrategySchema } from '../lib/openapi/spec/feature-strategy-schema';
import { ConstraintSchema } from 'lib/openapi/spec/constraint-schema';
import { SegmentSchema } from 'lib/openapi/spec/segment-schema';
export const urlFriendlyString = (): Arbitrary<string> =>
fc
.array(
fc.oneof(
fc.integer({ min: 0x30, max: 0x39 }).map(String.fromCharCode), // numbers
fc.integer({ min: 0x41, max: 0x5a }).map(String.fromCharCode), // UPPERCASE LETTERS
fc.integer({ min: 0x61, max: 0x7a }).map(String.fromCharCode), // lowercase letters
fc.constantFrom('-', '_', '~', '.'), // rest
fc.lorem({ maxCount: 1 }), // random words for more 'realistic' names
),
{ minLength: 1 },
)
.map((arr) => arr.join(''));
export const commonISOTimestamp = (): Arbitrary<string> =>
fc
.date({
min: new Date('1900-01-01T00:00:00.000Z'),
max: new Date('9999-12-31T23:59:59.999Z'),
})
.map((timestamp) => timestamp.toISOString());
export const strategyConstraint = (): Arbitrary<ConstraintSchema> =>
fc.record({
contextName: urlFriendlyString(),
operator: fc.constantFrom(...ALL_OPERATORS),
caseInsensitive: fc.boolean(),
inverted: fc.boolean(),
values: fc.array(fc.string()),
value: fc.string(),
});
const strategyConstraints = (): Arbitrary<ConstraintSchema[]> =>
fc.array(strategyConstraint());
export const strategy = (
name: string,
parameters?: Arbitrary<Record<string, string>>,
): Arbitrary<FeatureStrategySchema> =>
parameters
? fc.record(
{
name: fc.constant(name),
id: fc.uuid(),
parameters,
segments: fc.uniqueArray(fc.integer({ min: 1 })),
constraints: strategyConstraints(),
},
{ requiredKeys: ['name', 'parameters', 'id'] },
)
: fc.record(
{
id: fc.uuid(),
name: fc.constant(name),
segments: fc.uniqueArray(fc.integer({ min: 1 })),
constraints: strategyConstraints(),
},
{ requiredKeys: ['name', 'id'] },
);
export const segment = (): Arbitrary<SegmentSchema> =>
fc.record({
id: fc.integer({ min: 1 }),
name: urlFriendlyString(),
constraints: strategyConstraints(),
});
export const strategies = (): Arbitrary<FeatureStrategySchema[]> =>
fc.uniqueArray(
fc.oneof(
strategy('default'),
strategy(
'flexibleRollout',
fc.record({
groupId: fc.lorem({ maxCount: 1 }),
rollout: fc.nat({ max: 100 }).map(String),
stickiness: fc.constantFrom(
'default',
'userId',
'sessionId',
),
}),
),
strategy(
'applicationHostname',
fc.record({
hostNames: fc
.uniqueArray(fc.domain())
.map((domains) => domains.join(',')),
}),
),
strategy(
'userWithId',
fc.record({
userIds: fc
.uniqueArray(fc.emailAddress())
.map((ids) => ids.join(',')),
}),
),
strategy(
'remoteAddress',
fc.record({
IPs: fc.uniqueArray(fc.ipV4()).map((ips) => ips.join(',')),
}),
),
strategy(
'custom-strategy',
fc.record({
customParam: fc
.uniqueArray(fc.lorem())
.map((words) => words.join(',')),
}),
),
),
{ selector: (generatedStrategy) => generatedStrategy.id },
);
export const variant = (): Arbitrary<IVariant> =>
fc.record(
{
name: urlFriendlyString(),
weight: fc.nat({ max: 1000 }),
weightType: fc.constant(WeightType.VARIABLE),
stickiness: fc.constant('default'),
payload: fc.option(
fc.oneof(
fc.record({
type: fc.constant('json' as 'json'),
value: fc.json(),
}),
fc.record({
type: fc.constant('csv' as 'csv'),
value: fc
.array(fc.lorem())
.map((words) => words.join(',')),
}),
fc.record({
type: fc.constant('string' as 'string'),
value: fc.string(),
}),
),
{ nil: undefined },
),
},
{ requiredKeys: ['name', 'weight', 'weightType', 'stickiness'] },
);
export const variants = (): Arbitrary<IVariant[]> =>
fc
.uniqueArray(variant(), {
maxLength: 1000,
selector: (variantInstance) => variantInstance.name,
})
.map((allVariants) =>
allVariants.map((variantInstance) => ({
...variantInstance,
weight: Math.round(1000 / allVariants.length),
})),
);
export const clientFeature = (name?: string): Arbitrary<ClientFeatureSchema> =>
fc.record(
{
name: name ? fc.constant(name) : urlFriendlyString(),
type: fc.constantFrom(
'release',
'kill-switch',
'experiment',
'operational',
'permission',
),
description: fc.lorem(),
project: urlFriendlyString(),
enabled: fc.boolean(),
createdAt: commonISOTimestamp(),
lastSeenAt: commonISOTimestamp(),
stale: fc.boolean(),
impressionData: fc.option(fc.boolean()),
strategies: strategies(),
variants: variants(),
},
{ requiredKeys: ['name', 'enabled', 'project', 'strategies'] },
);
export const clientFeatures = (constraints?: {
minLength?: number;
}): Arbitrary<ClientFeatureSchema[]> =>
fc.uniqueArray(clientFeature(), {
...constraints,
selector: (v) => v.name,
});
export const clientFeaturesAndSegments = (featureConstraints?: {
minLength?: number;
}): Arbitrary<{
features: ClientFeatureSchema[];
segments: SegmentSchema[];
}> => {
const segments = () =>
fc.uniqueArray(segment(), {
selector: (generatedSegment) => generatedSegment.id,
});
// create segments and make sure that all strategies reference segments that
// exist
return fc
.tuple(segments(), clientFeatures(featureConstraints))
.map(([generatedSegments, generatedFeatures]) => {
const renumberedSegments = generatedSegments.map(
(generatedSegment, index) => ({
...generatedSegment,
id: index + 1,
}),
);
const features: ClientFeatureSchema[] = generatedFeatures.map(
(feature) => ({
...feature,
...(feature.strategies && {
strategies: feature.strategies.map(
(generatedStrategy) => ({
...generatedStrategy,
...(generatedStrategy.segments && {
segments:
renumberedSegments.length > 0
? [
...new Set(
generatedStrategy.segments.map(
(generatedSegment) =>
(generatedSegment %
renumberedSegments.length) +
1,
),
),
]
: [],
}),
}),
),
}),
}),
);
return {
features,
segments: renumberedSegments,
};
});
};
// TEST ARBITRARIES
test('url-friendly strings are URL-friendly', () =>
fc.assert(
fc.property(urlFriendlyString(), (input: string) =>
/^[\w~.-]+$/.test(input),
),
));
test('variant payloads are either present or undefined; never null', () =>
fc.assert(
fc.property(
variant(),
(generatedVariant) =>
!!generatedVariant.payload ||
generatedVariant.payload === undefined,
),
));