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.