1
0
mirror of https://github.com/Unleash/unleash.git synced 2025-01-25 00:07:47 +01:00

feat: add all feature variants to the playground payload (#1835)

* Chore: extract variant creation arbitrary.

* Feat: add "variants" as a required property on playground response

* Wip: fix up some schema generation

* Fix remaining openapi schemas

* Fix: add missing variants property

* Feat: test for variants

* Feat: add `variants` property to playground response

* Chore: update openapi snapshot
This commit is contained in:
Thomas Heartman 2022-07-20 08:54:34 +02:00 committed by GitHub
parent 7a7b86d440
commit 012da8469f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 207 additions and 84 deletions

View File

@ -1,5 +1,5 @@
import fc, { Arbitrary } from 'fast-check';
import { urlFriendlyString } from '../../../test/arbitraries.test';
import { urlFriendlyString, variants } from '../../../test/arbitraries.test';
import { validateSchema } from '../validate';
import {
playgroundFeatureSchema,
@ -7,42 +7,67 @@ import {
} from './playground-feature-schema';
export const generate = (): Arbitrary<PlaygroundFeatureSchema> =>
fc.boolean().chain((isEnabled) =>
fc.record({
isEnabled: fc.constant(isEnabled),
projectId: urlFriendlyString(),
name: urlFriendlyString(),
variant: fc.record(
{
name: urlFriendlyString(),
enabled: fc.constant(isEnabled),
payload: 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(),
}),
),
},
{ requiredKeys: ['name', 'enabled'] },
),
}),
);
fc
.tuple(
fc.boolean(),
variants(),
fc.nat(),
fc.record({
projectId: urlFriendlyString(),
name: urlFriendlyString(),
}),
)
.map(([isEnabled, generatedVariants, activeVariantIndex, feature]) => {
// the active variant is the disabled variant if the feature is
// disabled or has no variants.
let activeVariant = { name: 'disabled', enabled: false } as {
name: string;
enabled: boolean;
payload?: {
type: 'string' | 'json' | 'csv';
value: string;
};
};
if (generatedVariants.length && isEnabled) {
const targetVariant =
generatedVariants[
activeVariantIndex % generatedVariants.length
];
const targetPayload = targetVariant.payload
? (targetVariant.payload as {
type: 'string' | 'json' | 'csv';
value: string;
})
: undefined;
activeVariant = {
enabled: isEnabled,
name: targetVariant.name,
payload: targetPayload,
};
}
return {
...feature,
isEnabled,
variants: generatedVariants,
variant: activeVariant,
};
});
test('playgroundFeatureSchema', () =>
fc.assert(
fc.property(
generate(),
(data: PlaygroundFeatureSchema) =>
validateSchema(playgroundFeatureSchema.$id, data) === undefined,
fc.context(),
(data: PlaygroundFeatureSchema, ctx) => {
const results = validateSchema(
playgroundFeatureSchema.$id,
data,
);
ctx.log(JSON.stringify(results));
return results === undefined;
},
),
));

View File

@ -1,4 +1,6 @@
import { FromSchema } from 'json-schema-to-ts';
import { variantSchema } from './variant-schema';
import { overrideSchema } from './override-schema';
export const playgroundFeatureSchema = {
$id: '#/components/schemas/playgroundFeatureSchema',
@ -6,7 +8,7 @@ export const playgroundFeatureSchema = {
'A simplified feature toggle model intended for the Unleash playground.',
type: 'object',
additionalProperties: false,
required: ['name', 'projectId', 'isEnabled', 'variant'],
required: ['name', 'projectId', 'isEnabled', 'variant', 'variants'],
properties: {
name: { type: 'string', examples: ['my-feature'] },
projectId: { type: 'string', examples: ['my-project'] },
@ -34,8 +36,9 @@ export const playgroundFeatureSchema = {
nullable: true,
examples: ['green'],
},
variants: { type: 'array', items: { $ref: variantSchema.$id } },
},
components: { schemas: {} },
components: { schemas: { variantSchema, overrideSchema } },
} as const;
export type PlaygroundFeatureSchema = FromSchema<

View File

@ -5,12 +5,14 @@ import {
} from '../../../lib/openapi/spec/playground-response-schema';
import { validateSchema } from '../validate';
import { generate as generateInput } from './playground-request-schema.test';
import { generate as generateToggles } from './playground-feature-schema.test';
import { generate as generateFeature } from './playground-feature-schema.test';
const generate = (): Arbitrary<PlaygroundResponseSchema> =>
fc.record({
input: generateInput(),
features: fc.array(generateToggles()),
features: fc.uniqueArray(generateFeature(), {
selector: (feature) => feature.name,
}),
});
test('playgroundResponseSchema', () =>

View File

@ -2,6 +2,8 @@ import { FromSchema } from 'json-schema-to-ts';
import { sdkContextSchema } from './sdk-context-schema';
import { playgroundRequestSchema } from './playground-request-schema';
import { playgroundFeatureSchema } from './playground-feature-schema';
import { variantSchema } from './variant-schema';
import { overrideSchema } from './override-schema';
export const playgroundResponseSchema = {
$id: '#/components/schemas/playgroundResponseSchema',
@ -15,16 +17,16 @@ export const playgroundResponseSchema = {
},
features: {
type: 'array',
items: {
$ref: playgroundFeatureSchema.$id,
},
items: { $ref: playgroundFeatureSchema.$id },
},
},
components: {
schemas: {
sdkContextSchema,
playgroundRequestSchema,
playgroundFeatureSchema,
playgroundRequestSchema,
sdkContextSchema,
variantSchema,
overrideSchema,
},
},
} as const;

View File

@ -36,6 +36,11 @@ export class PlaygroundService {
if (!head) {
return [];
} else {
const variantsMap = toggles.reduce((acc, feature) => {
acc[feature.name] = feature.variants;
return acc;
}, {});
const client = await offlineUnleashClient(
[head, ...rest],
context,
@ -60,6 +65,7 @@ export class PlaygroundService {
),
variant: client.getVariant(feature.name, clientContext),
name: feature.name,
variants: variantsMap[feature.name] || [],
};
}),
);

View File

@ -2,7 +2,7 @@ import fc, { Arbitrary } from 'fast-check';
import { ALL_OPERATORS } from '../lib/util/constants';
import { ClientFeatureSchema } from '../lib/openapi/spec/client-feature-schema';
import { WeightType } from '../lib/types/model';
import { IVariant, WeightType } from '../lib/types/model';
import { FeatureStrategySchema } from '../lib/openapi/spec/feature-strategy-schema';
import { ConstraintSchema } from 'lib/openapi/spec/constraint-schema';
@ -92,6 +92,49 @@ export const strategies = (): Arbitrary<FeatureStrategySchema[]> =>
),
);
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(
{
@ -111,32 +154,7 @@ export const clientFeature = (name?: string): Arbitrary<ClientFeatureSchema> =>
stale: fc.boolean(),
impressionData: fc.option(fc.boolean()),
strategies: strategies(),
variants: fc.array(
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'),
value: fc.json(),
}),
fc.record({
type: fc.constant('csv'),
value: fc
.array(fc.lorem())
.map((words) => words.join(',')),
}),
fc.record({
type: fc.constant('string'),
value: fc.string(),
}),
),
),
}),
),
variants: variants(),
},
{ requiredKeys: ['name', 'enabled', 'project', 'strategies'] },
);
@ -157,3 +175,13 @@ test('url-friendly strings are URL-friendly', () =>
/^[\w~.-]+$/.test(input),
),
));
test('variant payloads are either present or undefined; never null', () =>
fc.assert(
fc.property(
variant(),
(generatedVariant) =>
!!generatedVariant.payload ||
generatedVariant.payload === undefined,
),
));

View File

@ -1821,12 +1821,19 @@ Object {
],
"type": "object",
},
"variants": Object {
"items": Object {
"$ref": "#/components/schemas/variantSchema",
},
"type": "array",
},
},
"required": Array [
"name",
"projectId",
"isEnabled",
"variant",
"variants",
],
"type": "object",
},

View File

@ -7,9 +7,10 @@ import dbInit, { ITestDb } from '../helpers/database-init';
import { IUnleashStores } from '../../../lib/types/stores';
import FeatureToggleService from '../../../lib/services/feature-toggle-service';
import { SegmentService } from '../../../lib/services/segment-service';
import { WeightType } from '../../../lib/types/model';
import { FeatureToggleDTO, IVariant } from '../../../lib/types/model';
import { PlaygroundFeatureSchema } from '../../../lib/openapi/spec/playground-feature-schema';
import { offlineUnleashClient } from '../../../lib/util/offline-unleash-client';
import { ClientFeatureSchema } from '../../../lib/openapi/spec/client-feature-schema';
let stores: IUnleashStores;
let db: ITestDb;
@ -48,6 +49,15 @@ describe('the playground service (e2e)', () => {
enabled: boolean;
}) => name === 'disabled' && !enabled;
const toFeatureToggleDTO = (
feature: ClientFeatureSchema,
): FeatureToggleDTO => ({
...feature,
// the arbitrary generator takes care of this
variants: feature.variants as IVariant[] | undefined,
createdAt: undefined,
});
test('should return the same enabled toggles as the raw SDK correctly mapped', async () => {
await fc.assert(
fc
@ -59,20 +69,7 @@ describe('the playground service (e2e)', () => {
toggles.map((feature) =>
stores.featureToggleStore.create(
feature.project,
{
...feature,
createdAt: undefined,
variants: [
...(feature.variants ?? []).map(
(variant) => ({
...variant,
weightType:
WeightType.VARIABLE,
stickiness: 'default',
}),
),
],
},
toFeatureToggleDTO(feature),
),
),
);
@ -154,4 +151,57 @@ describe('the playground service (e2e)', () => {
testParams,
);
});
test('output toggles should have the same variants as input toggles', async () => {
await fc.assert(
fc
.asyncProperty(
clientFeatures({ minLength: 1 }),
generateContext(),
async (toggles, context) => {
await Promise.all(
toggles.map((feature) =>
stores.featureToggleStore.create(
feature.project,
toFeatureToggleDTO(feature),
),
),
);
const projects = '*';
const env = 'default';
const serviceToggles: PlaygroundFeatureSchema[] =
await service.evaluateQuery(projects, env, context);
const variantsMap = toggles.reduce(
(acc, feature) => ({
...acc,
[feature.name]: feature.variants,
}),
{},
);
serviceToggles.forEach((feature) => {
if (variantsMap[feature.name]) {
expect(feature.variants).toEqual(
expect.arrayContaining(
variantsMap[feature.name],
),
);
expect(variantsMap[feature.name]).toEqual(
expect.arrayContaining(feature.variants),
);
} else {
expect(feature.variants).toStrictEqual([]);
}
});
},
)
.afterEach(async () => {
await stores.featureToggleStore.deleteAll();
}),
testParams,
);
});
});