unleash-server
Version:
Unleash is an enterprise ready feature toggles service. It provides different strategies for handling feature toggles.
411 lines • 20.2 kB
JavaScript
;
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