From 4b7e1f4a81fa344038f50281dcefe8d2ab2c7736 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ivar=20Conradi=20=C3=98sthus?= Date: Fri, 24 Sep 2021 13:55:00 +0200 Subject: [PATCH] feat: rename :global: env to "default" (#947) Our testing and internal validation has proven that the :global: environment concept confuses people more than the problems it solves. We have thus decided to group all configuration that was created before the environment concept was introduced in to the "default environment. This would still make everything work as before in addition to introducing the env concept. Co-authored-by: Christopher Kolstad --- src/lib/db/environment-store.ts | 23 ++ src/lib/db/feature-toggle-client-store.ts | 15 +- src/lib/db/project-store.ts | 7 +- src/lib/routes/admin-api/feature.ts | 20 +- src/lib/routes/admin-api/state.ts | 1 + src/lib/schema/api-token-schema.ts | 7 +- src/lib/services/api-token-service.ts | 10 +- src/lib/services/feature-toggle-service-v2.ts | 18 +- src/lib/services/project-service.ts | 5 +- src/lib/services/state-schema.ts | 4 +- src/lib/services/state-service.ts | 80 ++++- src/lib/types/events.ts | 2 + src/lib/types/stores/environment-store.ts | 1 + src/lib/util/constants.ts | 1 + ...104218-rename-global-env-to-default-env.js | 33 +++ ...0210921105032-client-api-tokens-default.js | 20 ++ src/test/e2e/api/admin/api-token.e2e.test.ts | 39 +-- src/test/e2e/api/admin/environment.test.ts | 15 +- .../admin/project/environments.e2e.test.ts | 3 +- .../project/feature.strategy.e2e.test.ts | 12 +- src/test/e2e/api/admin/state.e2e.test.ts | 20 +- src/test/e2e/api/client/feature.e2e.test.ts | 6 +- .../client/feature.token.access.e2e.test.ts | 8 +- src/test/e2e/helpers/database-init.ts | 3 +- src/test/e2e/helpers/database.json | 4 +- .../services/api-token-service.e2e.test.ts | 11 +- .../e2e/services/environment-service.test.ts | 2 +- .../feature-toggle-service-v2.e2e.test.ts | 6 +- .../examples/exported-3175-enterprise.json | 127 +++++++- src/test/examples/exported405-enterprise.json | 199 ++++++++++++- ...xported412-enterprise-necessary-fixes.json | 26 +- src/test/examples/exported412-enterprise.json | 2 +- src/test/examples/exported412-version2.json | 275 ++++++++++++++++++ src/test/fixtures/fake-environment-store.ts | 5 + .../docs/api/admin/feature-toggles-api-v2.md | 2 +- 35 files changed, 900 insertions(+), 112 deletions(-) create mode 100644 src/migrations/20210920104218-rename-global-env-to-default-env.js create mode 100644 src/migrations/20210921105032-client-api-tokens-default.js create mode 100644 src/test/examples/exported412-version2.json diff --git a/src/lib/db/environment-store.ts b/src/lib/db/environment-store.ts index 2027636678..2aa45a83b4 100644 --- a/src/lib/db/environment-store.ts +++ b/src/lib/db/environment-store.ts @@ -39,6 +39,17 @@ function mapRow(row: IEnvironmentsTable): IEnvironment { }; } +function fieldToRow(env: IEnvironment): IEnvironmentsTable { + return { + name: env.name, + display_name: env.displayName, + type: env.type, + sort_order: env.sortOrder, + enabled: env.enabled, + protected: env.protected, + }; +} + const TABLE = 'environments'; export default class EnvironmentStore implements IEnvironmentStore { @@ -58,6 +69,18 @@ export default class EnvironmentStore implements IEnvironmentStore { }); } + async importEnvironments( + environments: IEnvironment[], + ): Promise { + const rows = await this.db(TABLE) + .insert(environments.map(fieldToRow)) + .returning(COLUMNS) + .onConflict('name') + .ignore(); + + return rows.map(mapRow); + } + async deleteAll(): Promise { await this.db(TABLE).del(); } diff --git a/src/lib/db/feature-toggle-client-store.ts b/src/lib/db/feature-toggle-client-store.ts index d6115f5186..fcb5e48740 100644 --- a/src/lib/db/feature-toggle-client-store.ts +++ b/src/lib/db/feature-toggle-client-store.ts @@ -9,6 +9,7 @@ import { IStrategyConfig, } from '../types/model'; import { IFeatureToggleClientStore } from '../types/stores/feature-toggle-client-store'; +import { DEFAULT_ENV } from '../util/constants'; export interface FeaturesTable { name: string; @@ -61,10 +62,7 @@ export default class FeatureToggleClientStore archived: boolean = false, isAdmin: boolean = true, ): Promise { - const environments = [':global:']; - if (featureQuery?.environment) { - environments.push(featureQuery.environment); - } + const environment = featureQuery?.environment || DEFAULT_ENV; const stopTimer = this.timer('getFeatureAdmin'); let query = this.db('features') .select( @@ -97,8 +95,9 @@ export default class FeatureToggleClientStore 'feature_environments.environment', ); }) - .whereIn('feature_environments.environment', environments) + .where('feature_environments.environment', environment) .where({ archived }); + if (featureQuery) { if (featureQuery.tag) { const tagQuery = this.db @@ -133,11 +132,7 @@ export default class FeatureToggleClientStore if (r.strategy_name) { feature.strategies.push(this.getAdminStrategy(r, isAdmin)); } - if (feature.enabled === undefined) { - feature.enabled = r.enabled; - } else { - feature.enabled = feature.enabled && r.enabled; - } + feature.enabled = r.enabled; feature.name = r.name; feature.description = r.description; feature.project = r.project; diff --git a/src/lib/db/project-store.ts b/src/lib/db/project-store.ts index 840d48abd3..adba0644c5 100644 --- a/src/lib/db/project-store.ts +++ b/src/lib/db/project-store.ts @@ -8,6 +8,7 @@ import { IProjectInsert, IProjectStore, } from '../types/stores/project-store'; +import { DEFAULT_ENV } from '../util/constants'; const COLUMNS = ['id', 'name', 'description', 'created_at', 'health']; const TABLE = 'projects'; @@ -99,16 +100,16 @@ class ProjectStore implements IProjectStore { .onConflict('id') .ignore(); if (rows.length > 0) { - await this.addGlobalEnvironment(rows); + await this.addDefaultEnvironment(rows); return rows.map(this.mapRow); } return []; } - async addGlobalEnvironment(projects: any[]): Promise { + async addDefaultEnvironment(projects: any[]): Promise { const environments = projects.map((p) => ({ project_id: p.id, - environment_name: ':global:', + environment_name: DEFAULT_ENV, })); await this.db('project_environments') .insert(environments) diff --git a/src/lib/routes/admin-api/feature.ts b/src/lib/routes/admin-api/feature.ts index 337832c082..0823fe629f 100644 --- a/src/lib/routes/admin-api/feature.ts +++ b/src/lib/routes/admin-api/feature.ts @@ -16,8 +16,8 @@ import FeatureToggleServiceV2 from '../../services/feature-toggle-service-v2'; import { featureSchema, querySchema } from '../../schema/feature-schema'; import { IFeatureToggleQuery } from '../../types/model'; import FeatureTagService from '../../services/feature-tag-service'; -import { GLOBAL_ENV } from '../../types/environment'; import { IAuthRequest } from '../unleash-types'; +import { DEFAULT_ENV } from '../../util/constants'; const version = 1; @@ -110,11 +110,11 @@ class FeatureController extends Controller { private async getLegacyFeatureToggle(name: string): Promise { const feature = await this.featureService2.getFeatureToggle(name); - const globalEnv = feature.environments.find( - (e) => e.name === GLOBAL_ENV, + const defaultEnv = feature.environments.find( + (e) => e.name === DEFAULT_ENV, ); - const strategies = globalEnv?.strategies || []; - const enabled = globalEnv?.enabled || false; + const strategies = defaultEnv?.strategies || []; + const enabled = defaultEnv?.enabled || false; delete feature.environments; return { ...feature, enabled, strategies }; @@ -181,7 +181,7 @@ class FeatureController extends Controller { await this.featureService2.updateEnabled( createdFeature.project, createdFeature.name, - GLOBAL_ENV, + DEFAULT_ENV, enabled, userName, ); @@ -228,7 +228,7 @@ class FeatureController extends Controller { await this.featureService2.updateEnabled( projectId, updatedFeature.name, - GLOBAL_ENV, + DEFAULT_ENV, updatedFeature.enabled, userName, ); @@ -246,7 +246,7 @@ class FeatureController extends Controller { const feature = await this.featureService2.toggle( projectId, featureName, - GLOBAL_ENV, + DEFAULT_ENV, userName, ); res.status(200).json(feature); @@ -259,7 +259,7 @@ class FeatureController extends Controller { const feature = await this.featureService2.updateEnabled( projectId, featureName, - GLOBAL_ENV, + DEFAULT_ENV, true, userName, ); @@ -273,7 +273,7 @@ class FeatureController extends Controller { const feature = await this.featureService2.updateEnabled( projectId, featureName, - GLOBAL_ENV, + DEFAULT_ENV, false, userName, ); diff --git a/src/lib/routes/admin-api/state.ts b/src/lib/routes/admin-api/state.ts index c7d82e1c47..ee010bd47c 100644 --- a/src/lib/routes/admin-api/state.ts +++ b/src/lib/routes/admin-api/state.ts @@ -36,6 +36,7 @@ class StateController extends Controller { this.logger = config.getLogger('/admin-api/state.ts'); this.stateService = stateService; this.fileupload('/import', upload.single('file'), this.import, ADMIN); + this.post('/import', this.import, ADMIN); this.get('/export', this.export, ADMIN); } diff --git a/src/lib/schema/api-token-schema.ts b/src/lib/schema/api-token-schema.ts index ed653b9286..be71638eb1 100644 --- a/src/lib/schema/api-token-schema.ts +++ b/src/lib/schema/api-token-schema.ts @@ -1,5 +1,6 @@ import joi from 'joi'; import { ALL, ApiTokenType } from '../types/models/api-token'; +import { DEFAULT_ENV } from '../util/constants'; export const createApiToken = joi .object() @@ -12,6 +13,10 @@ export const createApiToken = joi .valid(ApiTokenType.ADMIN, ApiTokenType.CLIENT), expiresAt: joi.date().optional(), project: joi.string().optional().default(ALL), - environment: joi.string().optional().default(ALL), + environment: joi.when('type', { + is: joi.string().valid(ApiTokenType.CLIENT), + then: joi.string().optional().default(DEFAULT_ENV), + otherwise: joi.string().optional().default(ALL), + }), }) .options({ stripUnknown: true, allowUnknown: false, abortEarly: false }); diff --git a/src/lib/services/api-token-service.ts b/src/lib/services/api-token-service.ts index 3f9ca0901b..6d69657b0f 100644 --- a/src/lib/services/api-token-service.ts +++ b/src/lib/services/api-token-service.ts @@ -83,7 +83,7 @@ export class ApiTokenService { return this.store.delete(secret); } - private validateAdminToken({ type, project, environment }) { + private validateNewApiToken({ type, project, environment }) { if (type === ApiTokenType.ADMIN && project !== ALL) { throw new BadDataError( 'Admin token cannot be scoped to single project', @@ -95,12 +95,18 @@ export class ApiTokenService { 'Admin token cannot be scoped to single environment', ); } + + if (type === ApiTokenType.CLIENT && environment === ALL) { + throw new BadDataError( + 'Client token cannot be scoped to all environments', + ); + } } public async createApiToken( newToken: Omit, ): Promise { - this.validateAdminToken(newToken); + this.validateNewApiToken(newToken); const secret = this.generateSecretKey(newToken); const createNewToken = { ...newToken, secret }; diff --git a/src/lib/services/feature-toggle-service-v2.ts b/src/lib/services/feature-toggle-service-v2.ts index 85ebca88e1..9aa66d8618 100644 --- a/src/lib/services/feature-toggle-service-v2.ts +++ b/src/lib/services/feature-toggle-service-v2.ts @@ -19,7 +19,6 @@ import { FEATURE_STRATEGY_UPDATE, FEATURE_UPDATED, } from '../types/events'; -import { GLOBAL_ENV } from '../types/environment'; import NotFoundError from '../error/notfound-error'; import { FeatureConfigurationClient, @@ -42,6 +41,7 @@ import { } from '../types/model'; import { IFeatureEnvironmentStore } from '../types/stores/feature-environment-store'; import { IFeatureToggleClientStore } from '../types/stores/feature-toggle-client-store'; +import { DEFAULT_ENV } from '../util/constants'; class FeatureToggleServiceV2 { private logger: Logger; @@ -96,7 +96,7 @@ class FeatureToggleServiceV2 { projectId: string, featureName: string, userName: string, - environment: string = GLOBAL_ENV, + environment: string = DEFAULT_ENV, ): Promise { try { const newFeatureStrategy = @@ -226,7 +226,7 @@ class FeatureToggleServiceV2 { id: string, userName: string, project: string = 'default', - environment: string = GLOBAL_ENV, + environment: string = DEFAULT_ENV, ): Promise { await this.featureStrategiesStore.delete(id); await this.eventStore.store({ @@ -243,7 +243,7 @@ class FeatureToggleServiceV2 { async getStrategiesForEnvironment( project: string, featureName: string, - environment: string = GLOBAL_ENV, + environment: string = DEFAULT_ENV, ): Promise { const hasEnv = await this.featureEnvironmentStore.featureHasEnvironment( environment, @@ -405,7 +405,7 @@ class FeatureToggleServiceV2 { async removeAllStrategiesForEnv( toggleName: string, - environment: string = GLOBAL_ENV, + environment: string = DEFAULT_ENV, ): Promise { await this.featureStrategiesStore.removeAllStrategiesForFeatureEnv( toggleName, @@ -605,11 +605,11 @@ class FeatureToggleServiceV2 { await this.featureStrategiesStore.getFeatureToggleWithEnvs( featureName, ); - const globalEnv = feature.environments.find( - (e) => e.name === GLOBAL_ENV, + const defaultEnv = feature.environments.find( + (e) => e.name === DEFAULT_ENV, ); - const strategies = globalEnv?.strategies || []; - const enabled = globalEnv?.enabled || false; + const strategies = defaultEnv?.strategies || []; + const enabled = defaultEnv?.enabled || false; return { ...feature, enabled, strategies }; } diff --git a/src/lib/services/project-service.ts b/src/lib/services/project-service.ts index 1ec6c8777d..7006c8100d 100644 --- a/src/lib/services/project-service.ts +++ b/src/lib/services/project-service.ts @@ -20,7 +20,6 @@ import { IUserWithRole, RoleName, } from '../types/model'; -import { GLOBAL_ENV } from '../types/environment'; import { IEnvironmentStore } from '../types/stores/environment-store'; import { IFeatureTypeStore } from '../types/stores/feature-type-store'; import { IFeatureToggleStore } from '../types/stores/feature-toggle-store'; @@ -31,6 +30,7 @@ import { IEventStore } from '../types/stores/event-store'; import FeatureToggleServiceV2 from './feature-toggle-service-v2'; import { CREATE_FEATURE, UPDATE_FEATURE } from '../types/permissions'; import NoAccessError from '../error/no-access-error'; +import { DEFAULT_ENV } from '../util/constants'; const getCreatedBy = (user: User) => user.email || user.username; @@ -123,7 +123,8 @@ export default class ProjectService { await this.store.create(data); - await this.featureEnvironmentStore.connectProject(GLOBAL_ENV, data.id); + // TODO: we should only connect to enabled environments + await this.featureEnvironmentStore.connectProject(DEFAULT_ENV, data.id); await this.accessService.createDefaultProjectRoles(user, data.id); diff --git a/src/lib/services/state-schema.ts b/src/lib/services/state-schema.ts index 2345d7aa93..0e7bd91758 100644 --- a/src/lib/services/state-schema.ts +++ b/src/lib/services/state-schema.ts @@ -13,7 +13,7 @@ export const featureStrategySchema = joi featureName: joi.string(), projectId: joi.string(), environment: joi.string(), - parameters: joi.object().optional(), + parameters: joi.object().optional().allow(null), constraints: joi.array().optional(), strategyName: joi.string(), }) @@ -26,7 +26,7 @@ export const featureEnvironmentsSchema = joi.object().keys({ }); export const environmentSchema = joi.object().keys({ - name: nameType.allow(':global:'), + name: nameType, displayName: joi.string().optional().allow(''), type: joi.string().required(), sortOrder: joi.number().optional(), diff --git a/src/lib/services/state-service.ts b/src/lib/services/state-service.ts index e1251b8b6a..e697fc04ef 100644 --- a/src/lib/services/state-service.ts +++ b/src/lib/services/state-service.ts @@ -1,11 +1,13 @@ import { stateSchema } from './state-schema'; import { + DROP_ENVIRONMENTS, DROP_FEATURE_TAGS, DROP_FEATURES, DROP_PROJECTS, DROP_STRATEGIES, DROP_TAG_TYPES, DROP_TAGS, + ENVIRONMENT_IMPORT, FEATURE_IMPORT, FEATURE_TAG_IMPORT, PROJECT_IMPORT, @@ -28,7 +30,6 @@ import { IProject, IStrategyConfig, } from '../types/model'; -import { GLOBAL_ENV } from '../types/environment'; import { Logger } from '../logger'; import { IFeatureTag, @@ -44,6 +45,8 @@ import { IFeatureStrategiesStore } from '../types/stores/feature-strategies-stor import { IEnvironmentStore } from '../types/stores/environment-store'; import { IFeatureEnvironmentStore } from '../types/stores/feature-environment-store'; import { IUnleashStores } from '../types/stores'; +import { DEFAULT_ENV } from '../util/constants'; +import { GLOBAL_ENV } from '../types/environment'; export interface IBackupOption { includeFeatureToggles: boolean; @@ -118,14 +121,47 @@ export default class StateService { ); } + // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types + replaceGlobalEnvWithDefaultEnv(data: any) { + data.environments?.forEach((e) => { + if (e.name === GLOBAL_ENV) { + e.name = DEFAULT_ENV; + } + }); + data.featureEnvironments?.forEach((fe) => { + if (fe.environment === GLOBAL_ENV) { + // eslint-disable-next-line no-param-reassign + fe.environment = DEFAULT_ENV; + } + }); + data.featureStrategies?.forEach((fs) => { + if (fs.environment === GLOBAL_ENV) { + // eslint-disable-next-line no-param-reassign + fs.environment = DEFAULT_ENV; + } + }); + } + async import({ data, userName = 'importUser', dropBeforeImport = false, keepExisting = true, }: IImportData): Promise { + if (data.version === 2) { + this.replaceGlobalEnvWithDefaultEnv(data); + } const importData = await stateSchema.validateAsync(data); + if (importData.environments) { + await this.importEnvironments({ + environments: data.environments, + userName, + dropBeforeImport, + keepExisting, + }); + } + if (importData.features) { let projectData; if (!importData.version || importData.version === 1) { @@ -245,14 +281,14 @@ export default class StateService { projectId: f.project, constraints: strategy.constraints || [], parameters: strategy.parameters || {}, - environment: GLOBAL_ENV, + environment: DEFAULT_ENV, strategyName: strategy.name, })), ); const newFeatures = features; const featureEnvironments = features.map((feature) => ({ featureName: feature.name, - environment: GLOBAL_ENV, + environment: DEFAULT_ENV, enabled: feature.enabled, })); return { @@ -340,6 +376,42 @@ export default class StateService { ); } + // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types + async importEnvironments({ + environments, + userName, + dropBeforeImport, + keepExisting, + }): Promise { + this.logger.info(`Import ${environments.length} projects`); + const oldEnvs = dropBeforeImport + ? [] + : await this.environmentStore.getAll(); + if (dropBeforeImport) { + this.logger.info('Dropping existing environments'); + await this.environmentStore.deleteAll(); + await this.eventStore.store({ + type: DROP_ENVIRONMENTS, + createdBy: userName, + data: { name: 'all-projects' }, + }); + } + const envsImport = environments.filter((env) => + keepExisting ? !oldEnvs.some((old) => old.name === env.name) : true, + ); + if (envsImport.length > 0) { + const importedEnvs = await this.environmentStore.importEnvironments( + envsImport, + ); + const importedEnvironmentEvents = importedEnvs.map((env) => ({ + type: ENVIRONMENT_IMPORT, + createdBy: userName, + data: env, + })); + await this.eventStore.batchStore(importedEnvironmentEvents); + } + } + // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types async importProjects({ projects, @@ -576,7 +648,7 @@ export default class StateService { environments, featureEnvironments, ]) => ({ - version: 2, + version: 3, features, strategies, projects, diff --git a/src/lib/types/events.ts b/src/lib/types/events.ts index 5facf26d35..b50a96efe6 100644 --- a/src/lib/types/events.ts +++ b/src/lib/types/events.ts @@ -48,3 +48,5 @@ export const DB_POOL_UPDATE = 'db-pool-update'; export const USER_CREATED = 'user-created'; export const USER_UPDATED = 'user-updated'; export const USER_DELETED = 'user-deleted'; +export const DROP_ENVIRONMENTS = 'drop-environments'; +export const ENVIRONMENT_IMPORT = 'environment-import'; diff --git a/src/lib/types/stores/environment-store.ts b/src/lib/types/stores/environment-store.ts index 9eb7dc6b12..cfc4ba609a 100644 --- a/src/lib/types/stores/environment-store.ts +++ b/src/lib/types/stores/environment-store.ts @@ -14,4 +14,5 @@ export interface IEnvironmentStore extends Store { value: string | number | boolean, ): Promise; updateSortOrder(id: string, value: number): Promise; + importEnvironments(environments: IEnvironment[]): Promise; } diff --git a/src/lib/util/constants.ts b/src/lib/util/constants.ts index a81e7ceb6b..6930597ae8 100644 --- a/src/lib/util/constants.ts +++ b/src/lib/util/constants.ts @@ -1,2 +1,3 @@ export const MILLISECONDS_IN_DAY = 86400000; export const MILLISECONDS_IN_ONE_HOUR = 3600000; +export const DEFAULT_ENV = 'default'; diff --git a/src/migrations/20210920104218-rename-global-env-to-default-env.js b/src/migrations/20210920104218-rename-global-env-to-default-env.js new file mode 100644 index 0000000000..a6b8839d5e --- /dev/null +++ b/src/migrations/20210920104218-rename-global-env-to-default-env.js @@ -0,0 +1,33 @@ +'use strict'; + +const up = function (db, cb) { + db.runSql( + ` + INSERT INTO environments(name, display_name, protected, sort_order) VALUES ('default', 'Default Environment', true, 1); + ALTER TABLE feature_strategies ALTER COLUMN environment SET DEFAULT 'default'; + ALTER TABLE feature_environments ALTER COLUMN environment SET DEFAULT 'default'; + UPDATE feature_strategies SET environment = 'default' WHERE environment = ':global:'; + UPDATE feature_environments SET environment = 'default' WHERE environment = ':global:'; + UPDATE project_environments SET environment_name = 'default' WHERE environment_name = ':global:'; + DELETE FROM environments WHERE name = ':global:'; + `, + cb, + ); +}; + +const down = function (db, cb) { + db.runSql( + ` + INSERT INTO environments(name, display_name, protected) VALUES (':global:', 'Across all environments', true); + ALTER TABLE feature_strategies ALTER COLUMN environment SET DEFAULT ':global:'; + ALTER TABLE feature_environments ALTER COLUMN environment SET DEFAULT ':global:'; + UPDATE feature_strategies SET environment = ':global:' WHERE environment = 'default'; + UPDATE feature_environments SET environment = ':global:' WHERE environment = 'default'; + UPDATE project_environments SET environment_name = ':global:' WHERE environment_name = 'default'; + DELETE FROM environments WHERE name = 'default'; + `, + cb, + ); +}; + +module.exports = { up, down }; diff --git a/src/migrations/20210921105032-client-api-tokens-default.js b/src/migrations/20210921105032-client-api-tokens-default.js new file mode 100644 index 0000000000..f97490ab8e --- /dev/null +++ b/src/migrations/20210921105032-client-api-tokens-default.js @@ -0,0 +1,20 @@ +'use strict'; + +exports.up = function (db, cb) { + db.runSql( + ` + UPDATE api_tokens SET environment = 'default' WHERE environment = ':global:'; + UPDATE api_tokens SET environment = 'default' WHERE type='client' AND environment is null; + `, + cb, + ); +}; + +exports.down = function (db, cb) { + db.runSql( + ` + UPDATE api_tokens SET environment = null WHERE type='client' AND environment = 'default'; + `, + cb, + ); +}; diff --git a/src/test/e2e/api/admin/api-token.e2e.test.ts b/src/test/e2e/api/admin/api-token.e2e.test.ts index 7d79431500..8bf0e28a91 100644 --- a/src/test/e2e/api/admin/api-token.e2e.test.ts +++ b/src/test/e2e/api/admin/api-token.e2e.test.ts @@ -2,6 +2,7 @@ import { setupApp } from '../../helpers/test-helper'; import dbInit from '../../helpers/database-init'; import getLogger from '../../../fixtures/no-logger'; import { ALL, ApiTokenType } from '../../../../lib/types/models/api-token'; +import { DEFAULT_ENV } from '../../../../lib/util/constants'; let db; let app; @@ -23,7 +24,6 @@ afterEach(async () => { }); test('returns empty list of tokens', async () => { - expect.assertions(1); return app.request .get('/api/admin/api-tokens') .expect('Content-Type', /json/) @@ -34,7 +34,6 @@ test('returns empty list of tokens', async () => { }); test('creates new client token', async () => { - expect.assertions(4); return app.request .post('/api/admin/api-tokens') .send({ @@ -52,7 +51,6 @@ test('creates new client token', async () => { }); test('creates new admin token', async () => { - expect.assertions(5); return app.request .post('/api/admin/api-tokens') .send({ @@ -64,6 +62,7 @@ test('creates new admin token', async () => { .expect((res) => { expect(res.body.username).toBe('default-admin'); expect(res.body.type).toBe('admin'); + expect(res.body.environment).toBe(ALL); expect(res.body.createdAt).toBeTruthy(); expect(res.body.expiresAt).toBeFalsy(); expect(res.body.secret.length > 16).toBe(true); @@ -71,7 +70,6 @@ test('creates new admin token', async () => { }); test('creates new ADMIN token should fix casing', async () => { - expect.assertions(5); return app.request .post('/api/admin/api-tokens') .send({ @@ -90,7 +88,6 @@ test('creates new ADMIN token should fix casing', async () => { }); test('creates new admin token with expiry', async () => { - expect.assertions(1); const expiresAt = new Date(); const expiresAtAsISOStr = JSON.parse(JSON.stringify(expiresAt)); return app.request @@ -108,8 +105,6 @@ test('creates new admin token with expiry', async () => { }); test('update admin token with expiry', async () => { - expect.assertions(2); - const tokenSecret = 'random-secret-update'; await db.stores.apiTokenStore.insert({ @@ -137,8 +132,6 @@ test('update admin token with expiry', async () => { }); test('creates a lot of client tokens', async () => { - expect.assertions(4); - const requests = []; for (let i = 0; i < 10; i++) { @@ -166,8 +159,6 @@ test('creates a lot of client tokens', async () => { }); test('removes api token', async () => { - expect.assertions(1); - const tokenSecret = 'random-secret'; await db.stores.apiTokenStore.insert({ @@ -202,7 +193,7 @@ test('creates new client token: project & environment defaults to "*"', async () .expect((res) => { expect(res.body.type).toBe('client'); expect(res.body.secret.length > 16).toBe(true); - expect(res.body.environment).toBe(ALL); + expect(res.body.environment).toBe(DEFAULT_ENV); expect(res.body.project).toBe(ALL); }); }); @@ -214,14 +205,14 @@ test('creates new client token with project & environment set', async () => { username: 'default-client', type: 'client', project: 'default', - environment: ':global:', + environment: DEFAULT_ENV, }) .set('Content-Type', 'application/json') .expect(201) .expect((res) => { expect(res.body.type).toBe('client'); expect(res.body.secret.length > 16).toBe(true); - expect(res.body.environment).toBe(':global:'); + expect(res.body.environment).toBe(DEFAULT_ENV); expect(res.body.project).toBe('default'); }); }); @@ -236,7 +227,7 @@ test('should prefix default token with "*:*."', async () => { .set('Content-Type', 'application/json') .expect(201) .expect((res) => { - expect(res.body.secret).toMatch(/\*:\*\..*/); + expect(res.body.secret).toMatch(/\*:default\..*/); }); }); @@ -247,12 +238,12 @@ test('should prefix token with "project:environment."', async () => { username: 'default-client', type: 'client', project: 'default', - environment: ':global:', + environment: DEFAULT_ENV, }) .set('Content-Type', 'application/json') .expect(201) .expect((res) => { - expect(res.body.secret).toMatch(/default::global:\..*/); + expect(res.body.secret).toMatch(/default:default\..*/); }); }); @@ -323,7 +314,19 @@ test('admin token only supports ALL environments', async () => { username: 'default-admin', type: 'admin', project: '*', - environment: ':global:', + environment: DEFAULT_ENV, + }) + .set('Content-Type', 'application/json') + .expect(400); +}); + +test('client tokens cannot span all environments', async () => { + return app.request + .post('/api/admin/api-tokens') + .send({ + username: 'default-client', + type: 'client', + environment: ALL, }) .set('Content-Type', 'application/json') .expect(400); diff --git a/src/test/e2e/api/admin/environment.test.ts b/src/test/e2e/api/admin/environment.test.ts index a901b7c9f6..8296208f2d 100644 --- a/src/test/e2e/api/admin/environment.test.ts +++ b/src/test/e2e/api/admin/environment.test.ts @@ -1,6 +1,7 @@ import dbInit, { ITestDb } from '../../helpers/database-init'; import getLogger from '../../../fixtures/no-logger'; import { IUnleashTest, setupApp } from '../../helpers/test-helper'; +import { DEFAULT_ENV } from '../../../../lib/util/constants'; let app: IUnleashTest; let db: ITestDb; @@ -23,8 +24,8 @@ test('Can list all existing environments', async () => { .expect((res) => { expect(res.body.version).toBe(1); expect(res.body.environments[0]).toStrictEqual({ - displayName: 'Across all environments', - name: ':global:', + displayName: 'Default Environment', + name: DEFAULT_ENV, enabled: true, sortOrder: 1, type: 'production', @@ -43,7 +44,7 @@ test('Can update sort order', async () => { await app.request .put('/api/admin/environments/sort-order') .send({ - ':global:': 2, + [DEFAULT_ENV]: 2, [envName]: 1, }) .expect(200); @@ -56,11 +57,11 @@ test('Can update sort order', async () => { const updatedSort = res.body.environments.find( (t) => t.name === envName, ); - const global = res.body.environments.find( - (t) => t.name === ':global:', + const defaultEnv = res.body.environments.find( + (t) => t.name === DEFAULT_ENV, ); expect(updatedSort.sortOrder).toBe(1); - expect(global.sortOrder).toBe(2); + expect(defaultEnv.sortOrder).toBe(2); }); }); @@ -70,7 +71,7 @@ test('Sort order will fail on wrong data format', async () => { await app.request .put('/api/admin/environments/sort-order') .send({ - ':global:': 'test', + [DEFAULT_ENV]: 'test', [envName]: 1, }) .expect(400); diff --git a/src/test/e2e/api/admin/project/environments.e2e.test.ts b/src/test/e2e/api/admin/project/environments.e2e.test.ts index d52a8bac8c..06907d29b8 100644 --- a/src/test/e2e/api/admin/project/environments.e2e.test.ts +++ b/src/test/e2e/api/admin/project/environments.e2e.test.ts @@ -1,6 +1,7 @@ import dbInit, { ITestDb } from '../../../helpers/database-init'; import { IUnleashTest, setupApp } from '../../../helpers/test-helper'; import getLogger from '../../../../fixtures/no-logger'; +import { DEFAULT_ENV } from '../../../../../lib/util/constants'; let app: IUnleashTest; let db: ITestDb; @@ -16,7 +17,7 @@ afterEach(async () => { ); await Promise.all( all - .filter((env) => env !== ':global:') + .filter((env) => env !== DEFAULT_ENV) .map(async (env) => db.stores.projectStore.deleteEnvironmentForProject( 'default', diff --git a/src/test/e2e/api/admin/project/feature.strategy.e2e.test.ts b/src/test/e2e/api/admin/project/feature.strategy.e2e.test.ts index f8234a6405..4b8552054d 100644 --- a/src/test/e2e/api/admin/project/feature.strategy.e2e.test.ts +++ b/src/test/e2e/api/admin/project/feature.strategy.e2e.test.ts @@ -1,7 +1,7 @@ import dbInit, { ITestDb } from '../../../helpers/database-init'; import { IUnleashTest, setupApp } from '../../../helpers/test-helper'; import getLogger from '../../../../fixtures/no-logger'; -import { GLOBAL_ENV } from '../../../../../lib/types/environment'; +import { DEFAULT_ENV } from '../../../../../lib/util/constants'; let app: IUnleashTest; let db: ITestDb; @@ -20,7 +20,7 @@ afterEach(async () => { ); await Promise.all( all - .filter((env) => env !== ':global:') + .filter((env) => env !== DEFAULT_ENV) .map(async (env) => db.stores.projectStore.deleteEnvironmentForProject( 'default', @@ -181,7 +181,7 @@ test('Project overview includes environment connected to feature', async () => { .get('/api/admin/projects/default') .expect(200) .expect((r) => { - expect(r.body.features[0].environments[0].name).toBe(':global:'); + expect(r.body.features[0].environments[0].name).toBe(DEFAULT_ENV); expect(r.body.features[0].environments[1].name).toBe( 'project-overview', ); @@ -540,8 +540,8 @@ test('Should archive feature toggle', async () => { expect(toggle).toBeDefined(); }); -test('Can add strategy to feature toggle to default env', async () => { - const envName = 'default'; +test('Can add strategy to feature toggle to a "some-env-2"', async () => { + const envName = 'some-env-2'; const featureName = 'feature.strategy.toggle'; // Create environment await db.stores.environmentStore.create({ @@ -644,7 +644,7 @@ test('Environments are returned in sortOrder', async () => { .expect(200) .expect((res) => { expect(res.body.environments).toHaveLength(3); - expect(res.body.environments[0].name).toBe(GLOBAL_ENV); + expect(res.body.environments[0].name).toBe(DEFAULT_ENV); expect(res.body.environments[1].name).toBe(sortedSecond); expect(res.body.environments[2].name).toBe(sortedLast); }); diff --git a/src/test/e2e/api/admin/state.e2e.test.ts b/src/test/e2e/api/admin/state.e2e.test.ts index e396d81153..fc8b450b46 100644 --- a/src/test/e2e/api/admin/state.e2e.test.ts +++ b/src/test/e2e/api/admin/state.e2e.test.ts @@ -1,7 +1,7 @@ import dbInit, { ITestDb } from '../../helpers/database-init'; import { IUnleashTest, setupApp } from '../../helpers/test-helper'; import getLogger from '../../../fixtures/no-logger'; -import { GLOBAL_ENV } from '../../../../lib/types/environment'; +import { DEFAULT_ENV } from '../../../../lib/util/constants'; const importData = require('../../../examples/import.json'); @@ -265,7 +265,7 @@ test('Roundtrip with strategies in multiple environments works', async () => { projectId, ); await app.services.environmentService.addEnvironmentToProject( - GLOBAL_ENV, + DEFAULT_ENV, projectId, ); await app.services.featureToggleServiceV2.createFeatureToggle( @@ -299,7 +299,7 @@ test('Roundtrip with strategies in multiple environments works', async () => { }, projectId, featureName, - GLOBAL_ENV, + DEFAULT_ENV, ); const data = await app.services.stateService.export({}); await app.services.stateService.import({ @@ -311,3 +311,17 @@ test('Roundtrip with strategies in multiple environments works', async () => { const f = await app.services.featureToggleServiceV2.getFeature(featureName); expect(f.environments).toHaveLength(2); }); + +test(`Importing version 2 replaces :global: environment with 'default'`, async () => { + await app.request + .post('/api/admin/state/import') + .attach('file', 'src/test/examples/exported412-version2.json') + .expect(202); + const env = await app.services.environmentService.get(DEFAULT_ENV); + expect(env).toBeTruthy(); + const feature = await app.services.featureToggleServiceV2.getFeatureToggle( + 'this-is-fun', + ); + expect(feature.environments).toHaveLength(1); + expect(feature.environments[0].name).toBe(DEFAULT_ENV); +}); diff --git a/src/test/e2e/api/client/feature.e2e.test.ts b/src/test/e2e/api/client/feature.e2e.test.ts index ada2a5ee72..e0e73f852d 100644 --- a/src/test/e2e/api/client/feature.e2e.test.ts +++ b/src/test/e2e/api/client/feature.e2e.test.ts @@ -1,6 +1,7 @@ import { IUnleashTest, setupApp } from '../../helpers/test-helper'; import dbInit, { ITestDb } from '../../helpers/database-init'; import getLogger from '../../../fixtures/no-logger'; +import { DEFAULT_ENV } from '../../../../lib/util/constants'; let app: IUnleashTest; let db: ITestDb; @@ -153,6 +154,7 @@ test('Can filter features by namePrefix', async () => { test('Can get strategies for specific environment', async () => { const featureName = 'test.feature.with.env'; + const env = DEFAULT_ENV; // Create feature toggle await app.request.post('/api/admin/projects/default/features').send({ @@ -163,7 +165,7 @@ test('Can get strategies for specific environment', async () => { // Add global strategy await app.request .post( - `/api/admin/projects/default/features/${featureName}/environments/:global:/strategies`, + `/api/admin/projects/default/features/${featureName}/environments/${env}/strategies`, ) .send({ name: 'default', @@ -198,7 +200,7 @@ test('Can get strategies for specific environment', async () => { .expect(200) .expect((res) => { expect(res.body.name).toBe(featureName); - expect(res.body.strategies).toHaveLength(2); + expect(res.body.strategies).toHaveLength(1); expect( res.body.strategies.find((s) => s.name === 'custom1'), ).toBeDefined(); diff --git a/src/test/e2e/api/client/feature.token.access.e2e.test.ts b/src/test/e2e/api/client/feature.token.access.e2e.test.ts index 26b1c9f833..0b394bc5a4 100644 --- a/src/test/e2e/api/client/feature.token.access.e2e.test.ts +++ b/src/test/e2e/api/client/feature.token.access.e2e.test.ts @@ -3,6 +3,7 @@ import dbInit, { ITestDb } from '../../helpers/database-init'; import getLogger from '../../../fixtures/no-logger'; import { ApiTokenService } from '../../../../lib/services/api-token-service'; import { ApiTokenType } from '../../../../lib/types/models/api-token'; +import { DEFAULT_ENV } from '../../../../lib/util/constants'; let app: IUnleashTest; let db: ITestDb; @@ -117,11 +118,11 @@ afterAll(async () => { await db.destroy(); }); -test('returns feature toggle with :global: config', async () => { +test('returns feature toggle with "default" config', async () => { const token = await apiTokenService.createApiToken({ type: ApiTokenType.CLIENT, username, - environment: ':global:', + environment: DEFAULT_ENV, project, }); await app.request @@ -157,7 +158,8 @@ test('returns feature toggle with testing environment config', async () => { const f2 = features.find((f) => f.name === feature2); expect(features).toHaveLength(2); - expect(f1.strategies).toHaveLength(2); + expect(f1.strategies).toHaveLength(1); + expect(f1.strategies[0].name).toBe('custom-testing'); expect(f2.strategies).toHaveLength(1); expect(query.project[0]).toBe(project); expect(query.environment).toBe(environment); diff --git a/src/test/e2e/helpers/database-init.ts b/src/test/e2e/helpers/database-init.ts index 16961fc5fb..64e1ab4aa2 100644 --- a/src/test/e2e/helpers/database-init.ts +++ b/src/test/e2e/helpers/database-init.ts @@ -10,6 +10,7 @@ import noLoggerProvider from '../../fixtures/no-logger'; import EnvironmentStore from '../../../lib/db/environment-store'; import { IUnleashStores } from '../../../lib/types'; import { IFeatureEnvironmentStore } from '../../../lib/types/stores/feature-environment-store'; +import { DEFAULT_ENV } from '../../../lib/util/constants'; // require('db-migrate-shared').log.silence(false); @@ -54,7 +55,7 @@ function createTagTypes(store) { } async function connectProject(store: IFeatureEnvironmentStore): Promise { - await store.connectProject(':global:', 'default'); + await store.connectProject(DEFAULT_ENV, 'default'); } async function createEnvironments(store: EnvironmentStore): Promise { diff --git a/src/test/e2e/helpers/database.json b/src/test/e2e/helpers/database.json index 32f18a10ab..f5d4b3d7e9 100644 --- a/src/test/e2e/helpers/database.json +++ b/src/test/e2e/helpers/database.json @@ -29,8 +29,8 @@ ], "environments": [ { - "name": ":global:", - "displayName": "Across all environments", + "name": "default", + "displayName": "Default Environment", "type": "production", "sortOrder": 1, "enabled": true, diff --git a/src/test/e2e/services/api-token-service.e2e.test.ts b/src/test/e2e/services/api-token-service.e2e.test.ts index 8c6e297e05..bf20c59186 100644 --- a/src/test/e2e/services/api-token-service.e2e.test.ts +++ b/src/test/e2e/services/api-token-service.e2e.test.ts @@ -3,6 +3,7 @@ import getLogger from '../../fixtures/no-logger'; import { ApiTokenService } from '../../../lib/services/api-token-service'; import { createTestConfig } from '../../config/test-config'; import { ApiTokenType, IApiToken } from '../../../lib/types/models/api-token'; +import { DEFAULT_ENV } from '../../../lib/util/constants'; let db; let stores; @@ -43,7 +44,7 @@ test('should create client token', async () => { username: 'default-client', type: ApiTokenType.CLIENT, project: '*', - environment: '*', + environment: DEFAULT_ENV, }); const allTokens = await apiTokenService.getAllTokens(); @@ -73,7 +74,7 @@ test('should set expiry of token', async () => { type: ApiTokenType.CLIENT, expiresAt: time, project: '*', - environment: '*', + environment: DEFAULT_ENV, }); const [token] = await apiTokenService.getAllTokens(); @@ -90,7 +91,7 @@ test('should update expiry of token', async () => { type: ApiTokenType.CLIENT, expiresAt: time, project: '*', - environment: '*', + environment: DEFAULT_ENV, }); await apiTokenService.updateExpiry(token.secret, newTime); @@ -109,7 +110,7 @@ test('should only return valid tokens', async () => { type: ApiTokenType.CLIENT, expiresAt: new Date('2021-01-01'), project: '*', - environment: '*', + environment: DEFAULT_ENV, }); const activeToken = await apiTokenService.createApiToken({ @@ -117,7 +118,7 @@ test('should only return valid tokens', async () => { type: ApiTokenType.CLIENT, expiresAt: tomorrow, project: '*', - environment: '*', + environment: DEFAULT_ENV, }); const tokens = await apiTokenService.getAllActiveTokens(); diff --git a/src/test/e2e/services/environment-service.test.ts b/src/test/e2e/services/environment-service.test.ts index 2817ced5f6..ac97dca4fd 100644 --- a/src/test/e2e/services/environment-service.test.ts +++ b/src/test/e2e/services/environment-service.test.ts @@ -38,7 +38,7 @@ test('Can get all', async () => { }); const environments = await service.getAll(); - expect(environments).toHaveLength(3); // the one we created plus ':global:' + expect(environments).toHaveLength(3); // the one we created plus 'default' }); test('Can connect environment to project', async () => { diff --git a/src/test/e2e/services/feature-toggle-service-v2.e2e.test.ts b/src/test/e2e/services/feature-toggle-service-v2.e2e.test.ts index 43b764417d..45a7e2bf08 100644 --- a/src/test/e2e/services/feature-toggle-service-v2.e2e.test.ts +++ b/src/test/e2e/services/feature-toggle-service-v2.e2e.test.ts @@ -4,7 +4,7 @@ import FeatureToggleServiceV2 from '../../../lib/services/feature-toggle-service import { IStrategyConfig } from '../../../lib/types/model'; import { createTestConfig } from '../../config/test-config'; import dbInit from '../helpers/database-init'; -import { GLOBAL_ENV } from '../../../lib/types/environment'; +import { DEFAULT_ENV } from '../../../lib/util/constants'; let stores; let db; @@ -80,7 +80,7 @@ test('Should be able to update existing strategy configuration', async () => { expect(createdConfig.name).toEqual('default'); const updatedConfig = await service.updateStrategy( createdConfig.id, - GLOBAL_ENV, + DEFAULT_ENV, projectId, username, { @@ -112,7 +112,7 @@ test('Should include legacy props in event log when updating strategy configurat await service.updateEnabled( 'default', featureName, - GLOBAL_ENV, + DEFAULT_ENV, true, userName, ); diff --git a/src/test/examples/exported-3175-enterprise.json b/src/test/examples/exported-3175-enterprise.json index b447711506..29adaca842 100644 --- a/src/test/examples/exported-3175-enterprise.json +++ b/src/test/examples/exported-3175-enterprise.json @@ -1 +1,126 @@ -{"version":1,"features":[{"name":"in-another-project","description":"","type":"release","project":"someother","enabled":true,"stale":false,"strategies":[{"name":"gradualRolloutRandom","parameters":{"percentage":"29"},"constraints":[{"contextName":"environment","operator":"IN","values":["dev"]},{"contextName":"environment","operator":"IN","values":["prod"]}]}],"variants":[],"createdAt":"2021-09-17T07:14:03.718Z","lastSeenAt":null},{"name":"this-is-fun","description":"","type":"release","project":"default","enabled":true,"stale":false,"strategies":[{"name":"gradualRolloutRandom","parameters":{"percentage":"100"}}],"variants":[],"createdAt":"2021-09-17T07:06:40.925Z","lastSeenAt":null},{"name":"version.three.seventeen","description":"","type":"operational","project":"default","enabled":true,"stale":false,"strategies":[{"name":"default","parameters":{}}],"variants":[],"createdAt":"2021-09-17T07:06:56.421Z","lastSeenAt":null},{"name":"with-constraints","description":"","type":"release","project":"default","enabled":true,"stale":false,"strategies":[{"name":"default","parameters":{},"constraints":[{"contextName":"userId","operator":"IN","values":["123456"]}]}],"variants":[],"createdAt":"2021-09-17T07:14:39.509Z","lastSeenAt":null}],"strategies":[],"projects":[{"id":"default","name":"Default","description":"Default project","createdAt":"2021-09-17T05:06:16.299Z"},{"id":"someother","name":"Some other project","description":"","createdAt":"2021-09-17T05:13:45.011Z"}],"tagTypes":[{"name":"simple","description":"Used to simplify filtering of features","icon":"#"}],"tags":[],"featureTags":[]} \ No newline at end of file +{ + "version": 1, + "features": [ + { + "name": "in-another-project", + "description": "", + "type": "release", + "project": "someother", + "enabled": true, + "stale": false, + "strategies": [ + { + "name": "gradualRolloutRandom", + "parameters": { + "percentage": "29" + }, + "constraints": [ + { + "contextName": "environment", + "operator": "IN", + "values": [ + "dev" + ] + }, + { + "contextName": "environment", + "operator": "IN", + "values": [ + "prod" + ] + } + ] + } + ], + "variants": [], + "createdAt": "2021-09-17T07:14:03.718Z", + "lastSeenAt": null + }, + { + "name": "this-is-fun", + "description": "", + "type": "release", + "project": "default", + "enabled": true, + "stale": false, + "strategies": [ + { + "name": "gradualRolloutRandom", + "parameters": { + "percentage": "100" + } + } + ], + "variants": [], + "createdAt": "2021-09-17T07:06:40.925Z", + "lastSeenAt": null + }, + { + "name": "version.three.seventeen", + "description": "", + "type": "operational", + "project": "default", + "enabled": true, + "stale": false, + "strategies": [ + { + "name": "default", + "parameters": {} + } + ], + "variants": [], + "createdAt": "2021-09-17T07:06:56.421Z", + "lastSeenAt": null + }, + { + "name": "with-constraints", + "description": "", + "type": "release", + "project": "default", + "enabled": true, + "stale": false, + "strategies": [ + { + "name": "default", + "parameters": {}, + "constraints": [ + { + "contextName": "userId", + "operator": "IN", + "values": [ + "123456" + ] + } + ] + } + ], + "variants": [], + "createdAt": "2021-09-17T07:14:39.509Z", + "lastSeenAt": null + } + ], + "strategies": [], + "projects": [ + { + "id": "default", + "name": "Default", + "description": "Default project", + "createdAt": "2021-09-17T05:06:16.299Z" + }, + { + "id": "someother", + "name": "Some other project", + "description": "", + "createdAt": "2021-09-17T05:13:45.011Z" + } + ], + "tagTypes": [ + { + "name": "simple", + "description": "Used to simplify filtering of features", + "icon": "#" + } + ], + "tags": [], + "featureTags": [] +} \ No newline at end of file diff --git a/src/test/examples/exported405-enterprise.json b/src/test/examples/exported405-enterprise.json index d2c0b5e30d..b43e29db81 100644 --- a/src/test/examples/exported405-enterprise.json +++ b/src/test/examples/exported405-enterprise.json @@ -1 +1,198 @@ -{"version":1,"features":[{"name":"another-toggle","description":"","type":"release","project":"someother","enabled":true,"stale":false,"strategies":[{"name":"userWithId","parameters":{"userIds":"12541,123"},"constraints":[]}],"variants":[],"createdAt":"2021-09-17T07:22:16.404Z","lastSeenAt":null},{"name":"in-another-project","description":"","type":"release","project":"someother","enabled":true,"stale":false,"strategies":[{"name":"gradualRolloutRandom","parameters":{"percentage":"29"},"constraints":[{"contextName":"environment","operator":"IN","values":["dev"]},{"contextName":"environment","operator":"IN","values":["prod"]}]}],"variants":[],"createdAt":"2021-09-17T07:14:03.718Z","lastSeenAt":null},{"name":"this-is-fun","description":"","type":"release","project":"default","enabled":true,"stale":false,"strategies":[{"name":"gradualRolloutRandom","parameters":{"percentage":"100"}}],"variants":[],"createdAt":"2021-09-17T07:06:40.925Z","lastSeenAt":null},{"name":"version.three.seventeen","description":"","type":"operational","project":"default","enabled":true,"stale":false,"strategies":[{"name":"default","parameters":{}}],"variants":[],"createdAt":"2021-09-17T07:06:56.421Z","lastSeenAt":null},{"name":"with-constraints","description":"","type":"release","project":"default","enabled":true,"stale":false,"strategies":[{"name":"default","parameters":{},"constraints":[{"contextName":"userId","operator":"IN","values":["123456"]}]}],"variants":[],"createdAt":"2021-09-17T07:14:39.509Z","lastSeenAt":null}],"strategies":[{"name":"gradualRolloutRandom","description":"Randomly activate the feature toggle. No stickiness.","parameters":[{"name":"percentage","type":"percentage","description":"","required":false}],"deprecated":true},{"name":"gradualRolloutSessionId","description":"Gradually activate feature toggle. Stickiness based on session id.","parameters":[{"name":"percentage","type":"percentage","description":"","required":false},{"name":"groupId","type":"string","description":"Used to define a activation groups, which allows you to correlate across feature toggles.","required":true}],"deprecated":true},{"name":"gradualRolloutUserId","description":"Gradually activate feature toggle for logged in users. Stickiness based on user id.","parameters":[{"name":"percentage","type":"percentage","description":"","required":false},{"name":"groupId","type":"string","description":"Used to define a activation groups, which allows you to correlate across feature toggles.","required":true}],"deprecated":true}],"projects":[{"id":"default","name":"Default","description":"Default project","createdAt":"2021-09-17T05:06:16.299Z"},{"id":"someother","name":"Some other project","description":"","createdAt":"2021-09-17T05:13:45.011Z"}],"tagTypes":[{"name":"simple","description":"Used to simplify filtering of features","icon":"#"}],"tags":[],"featureTags":[]} \ No newline at end of file +{ + "version": 1, + "features": [ + { + "name": "another-toggle", + "description": "", + "type": "release", + "project": "someother", + "enabled": true, + "stale": false, + "strategies": [ + { + "name": "userWithId", + "parameters": { + "userIds": "12541,123" + }, + "constraints": [] + } + ], + "variants": [], + "createdAt": "2021-09-17T07:22:16.404Z", + "lastSeenAt": null + }, + { + "name": "in-another-project", + "description": "", + "type": "release", + "project": "someother", + "enabled": true, + "stale": false, + "strategies": [ + { + "name": "gradualRolloutRandom", + "parameters": { + "percentage": "29" + }, + "constraints": [ + { + "contextName": "environment", + "operator": "IN", + "values": [ + "dev" + ] + }, + { + "contextName": "environment", + "operator": "IN", + "values": [ + "prod" + ] + } + ] + } + ], + "variants": [], + "createdAt": "2021-09-17T07:14:03.718Z", + "lastSeenAt": null + }, + { + "name": "this-is-fun", + "description": "", + "type": "release", + "project": "default", + "enabled": true, + "stale": false, + "strategies": [ + { + "name": "gradualRolloutRandom", + "parameters": { + "percentage": "100" + } + } + ], + "variants": [], + "createdAt": "2021-09-17T07:06:40.925Z", + "lastSeenAt": null + }, + { + "name": "version.three.seventeen", + "description": "", + "type": "operational", + "project": "default", + "enabled": true, + "stale": false, + "strategies": [ + { + "name": "default", + "parameters": {} + } + ], + "variants": [], + "createdAt": "2021-09-17T07:06:56.421Z", + "lastSeenAt": null + }, + { + "name": "with-constraints", + "description": "", + "type": "release", + "project": "default", + "enabled": true, + "stale": false, + "strategies": [ + { + "name": "default", + "parameters": {}, + "constraints": [ + { + "contextName": "userId", + "operator": "IN", + "values": [ + "123456" + ] + } + ] + } + ], + "variants": [], + "createdAt": "2021-09-17T07:14:39.509Z", + "lastSeenAt": null + } + ], + "strategies": [ + { + "name": "gradualRolloutRandom", + "description": "Randomly activate the feature toggle. No stickiness.", + "parameters": [ + { + "name": "percentage", + "type": "percentage", + "description": "", + "required": false + } + ], + "deprecated": true + }, + { + "name": "gradualRolloutSessionId", + "description": "Gradually activate feature toggle. Stickiness based on session id.", + "parameters": [ + { + "name": "percentage", + "type": "percentage", + "description": "", + "required": false + }, + { + "name": "groupId", + "type": "string", + "description": "Used to define a activation groups, which allows you to correlate across feature toggles.", + "required": true + } + ], + "deprecated": true + }, + { + "name": "gradualRolloutUserId", + "description": "Gradually activate feature toggle for logged in users. Stickiness based on user id.", + "parameters": [ + { + "name": "percentage", + "type": "percentage", + "description": "", + "required": false + }, + { + "name": "groupId", + "type": "string", + "description": "Used to define a activation groups, which allows you to correlate across feature toggles.", + "required": true + } + ], + "deprecated": true + } + ], + "projects": [ + { + "id": "default", + "name": "Default", + "description": "Default project", + "createdAt": "2021-09-17T05:06:16.299Z" + }, + { + "id": "someother", + "name": "Some other project", + "description": "", + "createdAt": "2021-09-17T05:13:45.011Z" + } + ], + "tagTypes": [ + { + "name": "simple", + "description": "Used to simplify filtering of features", + "icon": "#" + } + ], + "tags": [], + "featureTags": [] +} \ No newline at end of file diff --git a/src/test/examples/exported412-enterprise-necessary-fixes.json b/src/test/examples/exported412-enterprise-necessary-fixes.json index d1b1093d1c..0296d749f9 100644 --- a/src/test/examples/exported412-enterprise-necessary-fixes.json +++ b/src/test/examples/exported412-enterprise-necessary-fixes.json @@ -145,7 +145,7 @@ "id": "2ea91298-4565-4db2-8a23-50757001a076", "featureName": "this-is-fun", "projectId": "default", - "environment": ":global:", + "environment": "default", "strategyName": "gradualRolloutRandom", "parameters": { "percentage": "100" @@ -157,7 +157,7 @@ "id": "edaffaee-cf6e-473f-b137-ae15fb88ff53", "featureName": "version.three.seventeen", "projectId": "default", - "environment": ":global:", + "environment": "default", "strategyName": "default", "parameters": {}, "constraints": [], @@ -167,7 +167,7 @@ "id": "e6eaede4-027a-41a9-8e80-0e0fc0a5d7af", "featureName": "in-another-project", "projectId": "someother", - "environment": ":global:", + "environment": "default", "strategyName": "gradualRolloutRandom", "parameters": { "percentage": "29" @@ -194,7 +194,7 @@ "id": "da60e934-246c-4b3e-b314-f2fd1828dd51", "featureName": "with-constraints", "projectId": "default", - "environment": ":global:", + "environment": "default", "strategyName": "default", "parameters": {}, "constraints": [ @@ -212,7 +212,7 @@ "id": "162058f5-3600-4299-97df-d543a0301bdd", "featureName": "another-toggle", "projectId": "someother", - "environment": ":global:", + "environment": "default", "strategyName": "userWithId", "parameters": { "userIds": "12541,123" @@ -224,7 +224,7 @@ "id": "5630e0fb-ebc1-4313-b6df-06b0a563c7b4", "featureName": "toggle-created-in-4-1", "projectId": "default", - "environment": ":global:", + "environment": "default", "strategyName": "applicationHostname", "parameters": { "hostNames": "vg.no" @@ -235,7 +235,7 @@ ], "environments": [ { - "name": ":global:", + "name": "default", "displayName": "Across all environments", "type": "production" } @@ -244,32 +244,32 @@ { "enabled": true, "featureName": "this-is-fun", - "environment": ":global:" + "environment": "default" }, { "enabled": true, "featureName": "version.three.seventeen", - "environment": ":global:" + "environment": "default" }, { "enabled": true, "featureName": "in-another-project", - "environment": ":global:" + "environment": "default" }, { "enabled": true, "featureName": "with-constraints", - "environment": ":global:" + "environment": "default" }, { "enabled": true, "featureName": "another-toggle", - "environment": ":global:" + "environment": "default" }, { "enabled": true, "featureName": "toggle-created-in-4-1", - "environment": ":global:" + "environment": "default" } ] } diff --git a/src/test/examples/exported412-enterprise.json b/src/test/examples/exported412-enterprise.json index f14f23835d..f885bb5e66 100644 --- a/src/test/examples/exported412-enterprise.json +++ b/src/test/examples/exported412-enterprise.json @@ -1,4 +1,4 @@ -exported412-enterprise.json{ +{ "version": 2, "features": [ { diff --git a/src/test/examples/exported412-version2.json b/src/test/examples/exported412-version2.json new file mode 100644 index 0000000000..d1b1093d1c --- /dev/null +++ b/src/test/examples/exported412-version2.json @@ -0,0 +1,275 @@ +{ + "version": 2, + "features": [ + { + "name": "this-is-fun", + "description": "", + "type": "release", + "project": "default", + "stale": false, + "variants": [], + "createdAt": "2021-09-17T07:06:40.925Z", + "lastSeenAt": null + }, + { + "name": "version.three.seventeen", + "description": "", + "type": "operational", + "project": "default", + "stale": false, + "variants": [], + "createdAt": "2021-09-17T07:06:56.421Z", + "lastSeenAt": null + }, + { + "name": "in-another-project", + "description": "", + "type": "release", + "project": "someother", + "stale": false, + "variants": [], + "createdAt": "2021-09-17T07:14:03.718Z", + "lastSeenAt": null + }, + { + "name": "with-constraints", + "description": "", + "type": "release", + "project": "default", + "stale": false, + "variants": [], + "createdAt": "2021-09-17T07:14:39.509Z", + "lastSeenAt": null + }, + { + "name": "another-toggle", + "description": "", + "type": "release", + "project": "someother", + "stale": false, + "variants": [], + "createdAt": "2021-09-17T07:22:16.404Z", + "lastSeenAt": null + }, + { + "name": "toggle-created-in-4-1", + "description": "", + "type": "operational", + "project": "default", + "stale": false, + "variants": [], + "createdAt": "2021-09-17T07:24:13.897Z", + "lastSeenAt": null + } + ], + "strategies": [ + { + "name": "gradualRolloutRandom", + "description": "Randomly activate the feature toggle. No stickiness.", + "parameters": [ + { + "name": "percentage", + "type": "percentage", + "description": "", + "required": false + } + ], + "deprecated": true + }, + { + "name": "gradualRolloutSessionId", + "description": "Gradually activate feature toggle. Stickiness based on session id.", + "parameters": [ + { + "name": "percentage", + "type": "percentage", + "description": "", + "required": false + }, + { + "name": "groupId", + "type": "string", + "description": "Used to define a activation groups, which allows you to correlate across feature toggles.", + "required": true + } + ], + "deprecated": true + }, + { + "name": "gradualRolloutUserId", + "description": "Gradually activate feature toggle for logged in users. Stickiness based on user id.", + "parameters": [ + { + "name": "percentage", + "type": "percentage", + "description": "", + "required": false + }, + { + "name": "groupId", + "type": "string", + "description": "Used to define a activation groups, which allows you to correlate across feature toggles.", + "required": true + } + ], + "deprecated": true + } + ], + "projects": [ + { + "id": "default", + "name": "Default", + "description": "Default project", + "createdAt": "2021-09-17T05:06:16.299Z", + "health": 100 + }, + { + "id": "someother", + "name": "Some other project", + "description": "", + "createdAt": "2021-09-17T05:13:45.011Z", + "health": 100 + } + ], + "tagTypes": [ + { + "name": "simple", + "description": "Used to simplify filtering of features", + "icon": "#" + } + ], + "tags": [], + "featureTags": [], + "featureStrategies": [ + { + "id": "2ea91298-4565-4db2-8a23-50757001a076", + "featureName": "this-is-fun", + "projectId": "default", + "environment": ":global:", + "strategyName": "gradualRolloutRandom", + "parameters": { + "percentage": "100" + }, + "constraints": [], + "createdAt": "2021-09-17T07:23:39.374Z" + }, + { + "id": "edaffaee-cf6e-473f-b137-ae15fb88ff53", + "featureName": "version.three.seventeen", + "projectId": "default", + "environment": ":global:", + "strategyName": "default", + "parameters": {}, + "constraints": [], + "createdAt": "2021-09-17T07:23:39.374Z" + }, + { + "id": "e6eaede4-027a-41a9-8e80-0e0fc0a5d7af", + "featureName": "in-another-project", + "projectId": "someother", + "environment": ":global:", + "strategyName": "gradualRolloutRandom", + "parameters": { + "percentage": "29" + }, + "constraints": [ + { + "values": [ + "dev" + ], + "operator": "IN", + "contextName": "environment" + }, + { + "values": [ + "prod" + ], + "operator": "IN", + "contextName": "environment" + } + ], + "createdAt": "2021-09-17T07:23:39.374Z" + }, + { + "id": "da60e934-246c-4b3e-b314-f2fd1828dd51", + "featureName": "with-constraints", + "projectId": "default", + "environment": ":global:", + "strategyName": "default", + "parameters": {}, + "constraints": [ + { + "values": [ + "123456" + ], + "operator": "IN", + "contextName": "userId" + } + ], + "createdAt": "2021-09-17T07:23:39.374Z" + }, + { + "id": "162058f5-3600-4299-97df-d543a0301bdd", + "featureName": "another-toggle", + "projectId": "someother", + "environment": ":global:", + "strategyName": "userWithId", + "parameters": { + "userIds": "12541,123" + }, + "constraints": [], + "createdAt": "2021-09-17T07:23:39.374Z" + }, + { + "id": "5630e0fb-ebc1-4313-b6df-06b0a563c7b4", + "featureName": "toggle-created-in-4-1", + "projectId": "default", + "environment": ":global:", + "strategyName": "applicationHostname", + "parameters": { + "hostNames": "vg.no" + }, + "constraints": [], + "createdAt": "2021-09-17T07:24:13.904Z" + } + ], + "environments": [ + { + "name": ":global:", + "displayName": "Across all environments", + "type": "production" + } + ], + "featureEnvironments": [ + { + "enabled": true, + "featureName": "this-is-fun", + "environment": ":global:" + }, + { + "enabled": true, + "featureName": "version.three.seventeen", + "environment": ":global:" + }, + { + "enabled": true, + "featureName": "in-another-project", + "environment": ":global:" + }, + { + "enabled": true, + "featureName": "with-constraints", + "environment": ":global:" + }, + { + "enabled": true, + "featureName": "another-toggle", + "environment": ":global:" + }, + { + "enabled": true, + "featureName": "toggle-created-in-4-1", + "environment": ":global:" + } + ] +} diff --git a/src/test/fixtures/fake-environment-store.ts b/src/test/fixtures/fake-environment-store.ts index f5422fa20c..a939f5ec30 100644 --- a/src/test/fixtures/fake-environment-store.ts +++ b/src/test/fixtures/fake-environment-store.ts @@ -3,6 +3,11 @@ import NotFoundError from '../../lib/error/notfound-error'; import { IEnvironmentStore } from '../../lib/types/stores/environment-store'; export default class FakeEnvironmentStore implements IEnvironmentStore { + importEnvironments(envs: IEnvironment[]): Promise { + this.environments = envs; + return Promise.resolve(envs); + } + environments: IEnvironment[] = []; async getAll(): Promise { diff --git a/websitev2/docs/api/admin/feature-toggles-api-v2.md b/websitev2/docs/api/admin/feature-toggles-api-v2.md index 2ccf0e4e01..e90c0895ff 100644 --- a/websitev2/docs/api/admin/feature-toggles-api-v2.md +++ b/websitev2/docs/api/admin/feature-toggles-api-v2.md @@ -13,7 +13,7 @@ In this document we will guide you on how you can work with feature toggles and - A feature toggle can take different configuration, activation strategies, per environment. TODO: Need to explain the following in a bit more details: -- The _:global:: environment +- The _default_ environment > We will in this guide use [HTTPie](https://httpie.io) commands to show examples on how to interact with the API.