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:
parent
7a7b86d440
commit
012da8469f
@ -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;
|
||||
},
|
||||
),
|
||||
));
|
||||
|
@ -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<
|
||||
|
@ -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', () =>
|
||||
|
@ -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;
|
||||
|
@ -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] || [],
|
||||
};
|
||||
}),
|
||||
);
|
||||
|
@ -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,
|
||||
),
|
||||
));
|
||||
|
@ -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",
|
||||
},
|
||||
|
@ -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,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
Loading…
Reference in New Issue
Block a user