UNPKG

unleash-server

Version:

Unleash is an enterprise ready feature toggles service. It provides different strategies for handling feature toggles.

411 lines • 20.2 kB
"use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); const fast_check_1 = __importDefault(require("fast-check")); const arbitraries_test_1 = require("../../../arbitraries.test"); const playground_request_schema_test_1 = require("../../../../lib/openapi/spec/playground-request-schema.test"); const database_init_1 = __importDefault(require("../../helpers/database-init")); const test_helper_1 = require("../../helpers/test-helper"); const model_1 = require("../../../../lib/types/model"); const no_logger_1 = __importDefault(require("../../../fixtures/no-logger")); const api_token_1 = require("../../../../lib/types/models/api-token"); let app; let db; let token; beforeAll(async () => { db = await (0, database_init_1.default)('playground_api_serial', no_logger_1.default); app = await (0, test_helper_1.setupAppWithAuth)(db.stores); const { apiTokenService } = app.services; token = await apiTokenService.createApiTokenWithProjects({ type: api_token_1.ApiTokenType.ADMIN, username: 'tester', environment: api_token_1.ALL, projects: [api_token_1.ALL], }); }); afterAll(async () => { await app.destroy(); await db.destroy(); }); const reset = (database) => async () => { await database.stores.featureToggleStore.deleteAll(); await database.stores.featureStrategiesStore.deleteAll(); await database.stores.environmentStore.deleteAll(); }; const toArray = (x) => [x]; const testParams = { interruptAfterTimeLimit: 4000, markInterruptAsFailure: false, // When set to false, timeout during initial cases will not be considered as a failure }; const playgroundRequest = async (testApp, secret, request) => { const { body, } = await testApp.request .post('/api/admin/playground') .set('Authorization', secret) .send(request) .expect(200); return body; }; describe('Playground API E2E', () => { // utility function for seeding the database before runs const seedDatabase = async (database, features, environment) => { // create environment if necessary await database.stores.environmentStore .create({ name: environment, type: 'development', enabled: true, }) .catch(() => { // purposefully left empty: env creation may fail if the // env already exists, and because of the async nature // of things, this is the easiest way to make it work. }); return Promise.all(features.map(async (feature) => { // create feature const toggle = await database.stores.featureToggleStore.create(feature.project, { ...feature, createdAt: undefined, variants: null, }); // enable/disable the feature in environment await database.stores.featureEnvironmentStore.addEnvironmentToFeature(feature.name, environment, feature.enabled); await database.stores.featureToggleStore.saveVariants(feature.project, feature.name, [ ...(feature.variants ?? []).map((variant) => ({ ...variant, weightType: model_1.WeightType.VARIABLE, stickiness: 'default', })), ]); // assign strategies await Promise.all((feature.strategies || []).map((strategy) => database.stores.featureStrategiesStore.createStrategyFeatureEnv({ parameters: {}, constraints: [], ...strategy, featureName: feature.name, environment, strategyName: strategy.name, projectId: feature.project, }))); return toggle; })); }; test('Returned features should be a subset of the provided toggles', async () => { await fast_check_1.default.assert(fast_check_1.default .asyncProperty((0, arbitraries_test_1.clientFeatures)({ minLength: 1 }), (0, playground_request_schema_test_1.generate)(), async (features, request) => { // seed the database await seedDatabase(db, features, request.environment); const body = await playgroundRequest(app, token.secret, request); // the returned list should always be a subset of the provided list expect(features.map((feature) => feature.name)).toEqual(expect.arrayContaining(body.features.map((feature) => feature.name))); }) .afterEach(reset(db)), testParams); }); test('should filter the list according to the input parameters', async () => { await fast_check_1.default.assert(fast_check_1.default .asyncProperty((0, playground_request_schema_test_1.generate)(), (0, arbitraries_test_1.clientFeatures)({ minLength: 1 }), async (request, features) => { await seedDatabase(db, features, request.environment); // get a subset of projects that exist among the features const [projects] = fast_check_1.default.sample(fast_check_1.default.oneof(fast_check_1.default.constant(api_token_1.ALL), fast_check_1.default.uniqueArray(fast_check_1.default.constantFrom(...features.map((feature) => feature.project))))); request.projects = projects; // create a list of features that can be filtered // pass in args that should filter the list const body = await playgroundRequest(app, token.secret, request); switch (projects) { case api_token_1.ALL: // no features have been filtered out return body.features.length === features.length; case []: // no feature should be without a project return body.features.length === 0; default: // every feature should be in one of the prescribed projects return body.features.every((feature) => projects.includes(feature.projectId)); } }) .afterEach(reset(db)), testParams); }); test('should map project and name correctly', async () => { // note: we're not testing `isEnabled` and `variant` here, because // that's the SDK's responsibility and it's tested elsewhere. await fast_check_1.default.assert(fast_check_1.default .asyncProperty((0, arbitraries_test_1.clientFeatures)(), fast_check_1.default.context(), async (features, ctx) => { await seedDatabase(db, features, 'default'); const body = await playgroundRequest(app, token.secret, { projects: api_token_1.ALL, environment: 'default', context: { appName: 'playground-test', }, }); const createDict = (xs) => xs.reduce((acc, next) => ({ ...acc, [next.name]: next }), {}); const mappedToggles = createDict(body.features); if (features.length !== body.features.length) { ctx.log(`I expected the number of mapped toggles (${body.features.length}) to be the same as the number of created toggles (${features.length}), but that was not the case.`); return false; } return features.every((feature) => { const mapped = mappedToggles[feature.name]; expect(mapped).toBeTruthy(); return (feature.name === mapped.name && feature.project === mapped.projectId); }); }) .afterEach(reset(db)), testParams); }); test('isEnabledInCurrentEnvironment should always match feature.enabled', async () => { await fast_check_1.default.assert(fast_check_1.default .asyncProperty((0, arbitraries_test_1.clientFeatures)(), fast_check_1.default.context(), async (features, ctx) => { await seedDatabase(db, features, 'default'); const body = await playgroundRequest(app, token.secret, { projects: api_token_1.ALL, environment: 'default', context: { appName: 'playground-test', }, }); const createDict = (xs) => xs.reduce((acc, next) => ({ ...acc, [next.name]: next }), {}); const mappedToggles = createDict(body.features); ctx.log(JSON.stringify(features)); ctx.log(JSON.stringify(mappedToggles)); return features.every((feature) => feature.enabled === mappedToggles[feature.name] .isEnabledInCurrentEnvironment); }) .afterEach(reset(db)), testParams); }); describe('context application', () => { it('applies appName constraints correctly', async () => { const appNames = ['A', 'B', 'C']; // Choose one of the app names at random const appName = () => fast_check_1.default.constantFrom(...appNames); // generate a list of features that are active only for a specific // app name (constraints). Each feature will be constrained to a // random appName from the list above. const constrainedFeatures = () => fast_check_1.default.uniqueArray(fast_check_1.default .tuple((0, arbitraries_test_1.clientFeature)(), fast_check_1.default.record({ name: fast_check_1.default.constant('default'), constraints: fast_check_1.default .record({ values: appName().map(toArray), inverted: fast_check_1.default.constant(false), operator: fast_check_1.default.constant('IN'), contextName: fast_check_1.default.constant('appName'), caseInsensitive: fast_check_1.default.boolean(), }) .map(toArray), })) .map(([feature, strategy]) => ({ ...feature, enabled: true, strategies: [strategy], })), { selector: (feature) => feature.name }); await fast_check_1.default.assert(fast_check_1.default .asyncProperty(fast_check_1.default .tuple(appName(), (0, playground_request_schema_test_1.generate)()) .map(([generatedAppName, req]) => ({ ...req, // generate a context that has appName set to // one of the above values context: { appName: generatedAppName, environment: 'default', }, })), constrainedFeatures(), async (req, features) => { await seedDatabase(db, features, req.environment); const body = await playgroundRequest(app, token.secret, req); const shouldBeEnabled = features.reduce((acc, next) => ({ ...acc, [next.name]: next.strategies[0].constraints[0] .values[0] === req.context.appName, }), {}); return body.features.every((feature) => feature.isEnabled === shouldBeEnabled[feature.name]); }) .afterEach(reset(db)), { ...testParams, examples: [], }); }); it('applies dynamic context fields correctly', async () => { const contextValue = () => fast_check_1.default.oneof(fast_check_1.default.record({ name: fast_check_1.default.constant('remoteAddress'), value: fast_check_1.default.ipV4(), operator: fast_check_1.default.constant('IN'), }), fast_check_1.default.record({ name: fast_check_1.default.constant('sessionId'), value: fast_check_1.default.uuid(), operator: fast_check_1.default.constant('IN'), }), fast_check_1.default.record({ name: fast_check_1.default.constant('userId'), value: fast_check_1.default.emailAddress(), operator: fast_check_1.default.constant('IN'), })); const constrainedFeatures = () => fast_check_1.default.uniqueArray(fast_check_1.default .tuple((0, arbitraries_test_1.clientFeature)(), contextValue().map((context) => ({ name: 'default', constraints: [ { values: [context.value], inverted: false, operator: context.operator, contextName: context.name, caseInsensitive: false, }, ], }))) .map(([feature, strategy]) => ({ ...feature, enabled: true, strategies: [strategy], })), { selector: (feature) => feature.name }); await fast_check_1.default.assert(fast_check_1.default .asyncProperty(fast_check_1.default .tuple(contextValue(), (0, playground_request_schema_test_1.generate)()) .map(([generatedContextValue, req]) => ({ ...req, // generate a context that has a dynamic context field set to // one of the above values context: { ...req.context, [generatedContextValue.name]: generatedContextValue.value, }, })), constrainedFeatures(), async (req, features) => { await seedDatabase(db, features, 'default'); const body = await playgroundRequest(app, token.secret, req); const contextField = Object.values(req.context)[0]; const shouldBeEnabled = features.reduce((acc, next) => ({ ...acc, [next.name]: next.strategies[0].constraints[0] .values[0] === contextField, }), {}); return body.features.every((feature) => feature.isEnabled === shouldBeEnabled[feature.name]); }) .afterEach(reset(db)), testParams); }); it('applies custom context fields correctly', async () => { const environment = 'default'; const contextValue = () => fast_check_1.default.record({ name: fast_check_1.default.constantFrom('Context field A', 'Context field B'), value: fast_check_1.default.constantFrom('Context value 1', 'Context value 2'), }); const constrainedFeatures = () => fast_check_1.default.uniqueArray(fast_check_1.default .tuple((0, arbitraries_test_1.clientFeature)(), contextValue().map((context) => ({ name: 'default', constraints: [ { values: [context.value], inverted: false, operator: 'IN', contextName: context.name, caseInsensitive: false, }, ], }))) .map(([feature, strategy]) => ({ ...feature, enabled: true, strategies: [strategy], })), { selector: (feature) => feature.name }); // generate a constraint to be used for the context and a request // that contains that constraint value. const constraintAndRequest = () => fast_check_1.default .tuple(contextValue(), fast_check_1.default.constantFrom('top', 'nested'), (0, playground_request_schema_test_1.generate)()) .map(([generatedContextValue, placement, req]) => { const request = placement === 'top' ? { ...req, environment, context: { ...req.context, [generatedContextValue.name]: generatedContextValue.value, }, } : { ...req, environment, context: { ...req.context, properties: { [generatedContextValue.name]: generatedContextValue.value, }, }, }; return { generatedContextValue, request, }; }); await fast_check_1.default.assert(fast_check_1.default .asyncProperty(constraintAndRequest(), constrainedFeatures(), fast_check_1.default.context(), async ({ generatedContextValue, request }, features, ctx) => { await seedDatabase(db, features, environment); const body = await playgroundRequest(app, token.secret, request); const shouldBeEnabled = features.reduce((acc, next) => { const constraint = next.strategies[0].constraints[0]; return { ...acc, [next.name]: constraint.contextName === generatedContextValue.name && constraint.values[0] === generatedContextValue.value, }; }, {}); ctx.log(`Got these ${JSON.stringify(body.features)} and I expect them to be enabled/disabled: ${JSON.stringify(shouldBeEnabled)}`); return body.features.every((feature) => feature.isEnabled === shouldBeEnabled[feature.name]); }) .afterEach(reset(db)), testParams); }); test('context is applied to variant checks', async () => { const environment = 'development'; const featureName = 'feature-name'; const customContextFieldName = 'customField'; const customContextValue = 'customValue'; const features = [ { project: 'any-project', strategies: [ { name: 'default', constraints: [ { contextName: customContextFieldName, operator: 'IN', values: [customContextValue], }, ], }, ], stale: false, enabled: true, name: featureName, type: 'experiment', variants: [ { name: 'a', weight: 1000, weightType: 'variable', stickiness: 'default', overrides: [], }, ], }, ]; await seedDatabase(db, features, environment); const request = { projects: api_token_1.ALL, environment, context: { appName: 'playground', [customContextFieldName]: customContextValue, }, }; const body = await playgroundRequest(app, token.secret, request); // when enabled, this toggle should have one of the variants expect(body.features[0].variant.name).toBe('a'); }); }); }); //# sourceMappingURL=playground.e2e.test.js.map