From c3b064adfc24f72f45842e3e92bd029334907a04 Mon Sep 17 00:00:00 2001 From: sighphyre Date: Fri, 11 Mar 2022 11:16:58 +0200 Subject: [PATCH 1/5] feat: Add environment variable to set override enabled environments --- src/lib/create-config.test.ts | 44 +++++++++++++++ src/lib/create-config.ts | 11 ++++ src/lib/db/environment-store.ts | 16 ++++++ src/lib/server-impl.ts | 6 +++ src/lib/services/environment-service.ts | 22 ++++++++ src/lib/types/option.ts | 1 + src/lib/types/stores/environment-store.ts | 2 + .../e2e/services/environment-service.test.ts | 53 +++++++++++++++++++ src/test/fixtures/fake-environment-store.ts | 14 +++++ 9 files changed, 169 insertions(+) diff --git a/src/lib/create-config.test.ts b/src/lib/create-config.test.ts index 92eba02babc..be9c0044a2f 100644 --- a/src/lib/create-config.test.ts +++ b/src/lib/create-config.test.ts @@ -224,3 +224,47 @@ test('should handle cases where no env var specified for tokens', async () => { expect(config.authentication.initApiTokens).toHaveLength(1); }); + +test('should load environment overrides from env var', async () => { + process.env.ENABLED_ENVIRONMENTS = 'default,production'; + + const config = createConfig({ + db: { + host: 'localhost', + port: 4242, + user: 'unleash', + password: 'password', + database: 'unleash_db', + }, + server: { + port: 4242, + }, + authentication: { + initApiTokens: [], + }, + }); + + expect(config.environmentEnableOverrides).toHaveLength(2); + expect(config.environmentEnableOverrides).toContain('production'); + delete process.env.ENABLED_ENVIRONMENTS; +}); + +test('should yield an empty list when no environment overrides are specified', async () => { + const config = createConfig({ + db: { + host: 'localhost', + port: 4242, + user: 'unleash', + password: 'password', + database: 'unleash_db', + }, + server: { + port: 4242, + }, + authentication: { + initApiTokens: [], + }, + }); + + expect(config.environmentEnableOverrides).toStrictEqual([]); +}); diff --git a/src/lib/create-config.ts b/src/lib/create-config.ts index 91c6d1223e7..9ab90b57c6b 100644 --- a/src/lib/create-config.ts +++ b/src/lib/create-config.ts @@ -217,6 +217,14 @@ const loadInitApiTokens = () => { ]; }; +const loadEnvironmentEnableOverrides = () => { + const environmentsString = process.env.ENABLED_ENVIRONMENTS; + if (environmentsString) { + return environmentsString.split(','); + } + return []; +}; + export function createConfig(options: IUnleashOptions): IUnleashConfig { let extraDbOptions = {}; @@ -275,6 +283,8 @@ export function createConfig(options: IUnleashOptions): IUnleashConfig { { initApiTokens: initApiTokens }, ]); + const environmentEnableOverrides = loadEnvironmentEnableOverrides(); + const importSetting: IImportOption = mergeAll([ defaultImport, options.import, @@ -323,6 +333,7 @@ export function createConfig(options: IUnleashOptions): IUnleashConfig { eventHook: options.eventHook, enterpriseVersion: options.enterpriseVersion, eventBus: new EventEmitter(), + environmentEnableOverrides, }; } diff --git a/src/lib/db/environment-store.ts b/src/lib/db/environment-store.ts index ca000b1e860..d118ae57b8e 100644 --- a/src/lib/db/environment-store.ts +++ b/src/lib/db/environment-store.ts @@ -163,6 +163,22 @@ export default class EnvironmentStore implements IEnvironmentStore { return mapRow(row[0]); } + async disableAllExcept(environments: string[]): Promise { + await this.db(TABLE) + .update({ + enabled: false, + }) + .whereNotIn('name', environments); + } + + async enable(environments: string[]): Promise { + await this.db(TABLE) + .update({ + enabled: true, + }) + .whereIn('name', environments); + } + async delete(name: string): Promise { await this.db(TABLE).where({ name, protected: false }).del(); } diff --git a/src/lib/server-impl.ts b/src/lib/server-impl.ts index af038507a46..1c2904588fc 100644 --- a/src/lib/server-impl.ts +++ b/src/lib/server-impl.ts @@ -87,6 +87,12 @@ async function createApp( }); } + if (config.environmentEnableOverrides?.length > 0) { + await services.environmentService.overrideEnabledProjects( + config.environmentEnableOverrides, + ); + } + return new Promise((resolve, reject) => { if (startApp) { const server = stoppable( diff --git a/src/lib/services/environment-service.ts b/src/lib/services/environment-service.ts index 48c9ce91da3..02405a01a8b 100644 --- a/src/lib/services/environment-service.ts +++ b/src/lib/services/environment-service.ts @@ -94,6 +94,28 @@ export default class EnvironmentService { } } + async overrideEnabledProjects( + environmentsToEnable: string[], + ): Promise { + if (environmentsToEnable.length === 0) { + return Promise.resolve(); + } + + const environmentsExist = await Promise.all( + environmentsToEnable.map((env) => + this.environmentStore.exists(env), + ), + ); + if (!environmentsExist.every((exists) => exists)) { + this.logger.error( + "Found environment enabled overrides but some of the specified environments don't exist, no overrides will be executed", + ); + return; + } + await this.environmentStore.disableAllExcept(environmentsToEnable); + await this.environmentStore.enable(environmentsToEnable); + } + async removeEnvironmentFromProject( environment: string, projectId: string, diff --git a/src/lib/types/option.ts b/src/lib/types/option.ts index f60413cd4af..61212860e0b 100644 --- a/src/lib/types/option.ts +++ b/src/lib/types/option.ts @@ -158,4 +158,5 @@ export interface IUnleashConfig { enterpriseVersion?: string; eventBus: EventEmitter; disableLegacyFeaturesApi?: boolean; + environmentEnableOverrides?: string[]; } diff --git a/src/lib/types/stores/environment-store.ts b/src/lib/types/stores/environment-store.ts index c6fe2600f2c..45e21d72edb 100644 --- a/src/lib/types/stores/environment-store.ts +++ b/src/lib/types/stores/environment-store.ts @@ -16,4 +16,6 @@ export interface IEnvironmentStore extends Store { updateSortOrder(id: string, value: number): Promise; importEnvironments(environments: IEnvironment[]): Promise; delete(name: string): Promise; + disableAllExcept(environments: string[]): Promise; + enable(environments: string[]): Promise; } diff --git a/src/test/e2e/services/environment-service.test.ts b/src/test/e2e/services/environment-service.test.ts index 8cb953b7336..73823cce4b9 100644 --- a/src/test/e2e/services/environment-service.test.ts +++ b/src/test/e2e/services/environment-service.test.ts @@ -136,3 +136,56 @@ test('Trying to get an environment that does not exist throws NotFoundError', as new NotFoundError(`Could not find environment with name: ${envName}`), ); }); + +test('Setting an override disables all other envs', async () => { + const enabledEnvName = 'should-get-enabled'; + const disabledEnvName = 'should-get-disabled'; + await db.stores.environmentStore.create({ + name: disabledEnvName, + type: 'production', + }); + + await db.stores.environmentStore.create({ + name: enabledEnvName, + type: 'production', + }); + + //Set these to the wrong state so we can assert that overriding them flips + await service.toggleEnvironment(disabledEnvName, true); + await service.toggleEnvironment(enabledEnvName, false); + + await service.overrideEnabledProjects([enabledEnvName]); + + const environments = await service.getAll(); + const targetedEnvironment = environments.find( + (env) => env.name == enabledEnvName, + ); + + const allOtherEnvironments = environments + .filter((x) => x.name != enabledEnvName) + .map((env) => env.enabled); + + console.log(allOtherEnvironments); + expect(targetedEnvironment.enabled).toBe(true); + expect(allOtherEnvironments.every((x) => x === false)).toBe(true); +}); + +test('Passing an empty override does nothing', async () => { + const enabledEnvName = 'should-be-enabled'; + + await db.stores.environmentStore.create({ + name: enabledEnvName, + type: 'production', + }); + + await service.toggleEnvironment(enabledEnvName, true); + + await service.overrideEnabledProjects([]); + + const environments = await service.getAll(); + const targetedEnvironment = environments.find( + (env) => env.name == enabledEnvName, + ); + + expect(targetedEnvironment.enabled).toBe(true); +}); diff --git a/src/test/fixtures/fake-environment-store.ts b/src/test/fixtures/fake-environment-store.ts index c7aaad31270..35847aba578 100644 --- a/src/test/fixtures/fake-environment-store.ts +++ b/src/test/fixtures/fake-environment-store.ts @@ -10,6 +10,20 @@ export default class FakeEnvironmentStore implements IEnvironmentStore { environments: IEnvironment[] = []; + disableAllExcept(environments: string[]): Promise { + for (let env of this.environments) { + if (!environments.includes(env.name)) env.enabled = false; + } + return Promise.resolve(); + } + + enable(environments: string[]): Promise { + for (let env of this.environments) { + if (environments.includes(env.name)) env.enabled = true; + } + return Promise.resolve(); + } + async getAll(): Promise { return this.environments; } From 8410a8e3ac3228ec1ea20ed98748ecb9fa46cc94 Mon Sep 17 00:00:00 2001 From: sighphyre Date: Fri, 11 Mar 2022 15:52:13 +0200 Subject: [PATCH 2/5] feat: enabled environments override now also moves projects and toggles to new environments --- src/lib/db/environment-store.ts | 14 ++- src/lib/db/project-store.ts | 22 +++++ src/lib/services/environment-service.ts | 90 +++++++++++++++---- src/lib/types/stores/environment-store.ts | 4 +- src/lib/types/stores/project-store.ts | 4 + .../e2e/services/environment-service.test.ts | 47 +++++++++- src/test/fixtures/fake-environment-store.ts | 10 ++- src/test/fixtures/fake-project-store.ts | 14 ++- 8 files changed, 174 insertions(+), 31 deletions(-) diff --git a/src/lib/db/environment-store.ts b/src/lib/db/environment-store.ts index d118ae57b8e..f8250ec7035 100644 --- a/src/lib/db/environment-store.ts +++ b/src/lib/db/environment-store.ts @@ -163,20 +163,26 @@ export default class EnvironmentStore implements IEnvironmentStore { return mapRow(row[0]); } - async disableAllExcept(environments: string[]): Promise { + async disable(environments: IEnvironment[]): Promise { await this.db(TABLE) .update({ enabled: false, }) - .whereNotIn('name', environments); + .whereIn( + 'name', + environments.map((env) => env.name), + ); } - async enable(environments: string[]): Promise { + async enable(environments: IEnvironment[]): Promise { await this.db(TABLE) .update({ enabled: true, }) - .whereIn('name', environments); + .whereIn( + 'name', + environments.map((env) => env.name), + ); } async delete(name: string): Promise { diff --git a/src/lib/db/project-store.ts b/src/lib/db/project-store.ts index 2c08a845d5b..8b10a12ad1f 100644 --- a/src/lib/db/project-store.ts +++ b/src/lib/db/project-store.ts @@ -24,6 +24,11 @@ const COLUMNS = [ ]; const TABLE = 'projects'; +export interface IEnvironmentProjectLink { + environmentName: string; + projectId: string; +} + class ProjectStore implements IProjectStore { private db: Knex; @@ -197,6 +202,15 @@ class ProjectStore implements IProjectStore { } } + async getProjectLinksForEnvironments( + environments: string[], + ): Promise { + let rows = await this.db('project_environments') + .select(['project_id', 'environment_name']) + .whereIn('environment_name', environments); + return rows.map(this.mapLinkRow); + } + async deleteEnvironmentForProject( id: string, environment: string, @@ -251,6 +265,14 @@ class ProjectStore implements IProjectStore { .then((res) => Number(res[0].count)); } + // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types + mapLinkRow(row): IEnvironmentProjectLink { + return { + environmentName: row.environment_name, + projectId: row.project_id, + }; + } + // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types mapRow(row): IProject { if (!row) { diff --git a/src/lib/services/environment-service.ts b/src/lib/services/environment-service.ts index 02405a01a8b..891eca0a074 100644 --- a/src/lib/services/environment-service.ts +++ b/src/lib/services/environment-service.ts @@ -95,25 +95,87 @@ export default class EnvironmentService { } async overrideEnabledProjects( - environmentsToEnable: string[], + environmentNamesToEnable: string[], ): Promise { - if (environmentsToEnable.length === 0) { + if (environmentNamesToEnable.length === 0) { return Promise.resolve(); } - const environmentsExist = await Promise.all( - environmentsToEnable.map((env) => - this.environmentStore.exists(env), - ), + const allEnvironments = await this.environmentStore.getAll(); + const existingEnvironmentsToEnable = allEnvironments.filter((env) => + environmentNamesToEnable.includes(env.name), ); - if (!environmentsExist.every((exists) => exists)) { - this.logger.error( + + if ( + existingEnvironmentsToEnable.length !== + environmentNamesToEnable.length + ) { + this.logger.warn( "Found environment enabled overrides but some of the specified environments don't exist, no overrides will be executed", ); - return; + return Promise.resolve(); } - await this.environmentStore.disableAllExcept(environmentsToEnable); - await this.environmentStore.enable(environmentsToEnable); + + const environmentsNotAlreadyEnabled = + existingEnvironmentsToEnable.filter((env) => env.enabled == false); + const environmentsToDisable = allEnvironments.filter((env) => { + return ( + !environmentNamesToEnable.includes(env.name) && + env.enabled == true + ); + }); + + await this.environmentStore.disable(environmentsToDisable); + await this.environmentStore.enable(environmentsNotAlreadyEnabled); + + await this.remapProjectsLinks( + environmentsToDisable, + environmentsNotAlreadyEnabled, + ); + } + + private async remapProjectsLinks( + toDisable: IEnvironment[], + toEnable: IEnvironment[], + ) { + const projectLinks = + await this.projectStore.getProjectLinksForEnvironments( + toDisable.map((env) => env.name), + ); + + const unlinkTasks = projectLinks.map((link) => { + return this.forceRemoveEnvironmentFromProject( + link.environmentName, + link.projectId, + ); + }); + await Promise.all(unlinkTasks.flat()); + + const uniqueProjects = [ + ...new Set(projectLinks.map((link) => link.projectId)), + ]; + + let linkTasks = uniqueProjects.map((project) => { + return toEnable.map((enabledEnv) => { + return this.addEnvironmentToProject(enabledEnv.name, project); + }); + }); + + await Promise.all(linkTasks.flat()); + } + + async forceRemoveEnvironmentFromProject( + environment: string, + projectId: string, + ): Promise { + await this.featureEnvironmentStore.disconnectFeatures( + environment, + projectId, + ); + await this.featureEnvironmentStore.disconnectProject( + environment, + projectId, + ); } async removeEnvironmentFromProject( @@ -125,11 +187,7 @@ export default class EnvironmentService { ); if (projectEnvs.length > 1) { - await this.featureEnvironmentStore.disconnectFeatures( - environment, - projectId, - ); - await this.featureEnvironmentStore.disconnectProject( + await this.forceRemoveEnvironmentFromProject( environment, projectId, ); diff --git a/src/lib/types/stores/environment-store.ts b/src/lib/types/stores/environment-store.ts index 45e21d72edb..4c1f4c48b75 100644 --- a/src/lib/types/stores/environment-store.ts +++ b/src/lib/types/stores/environment-store.ts @@ -16,6 +16,6 @@ export interface IEnvironmentStore extends Store { updateSortOrder(id: string, value: number): Promise; importEnvironments(environments: IEnvironment[]): Promise; delete(name: string): Promise; - disableAllExcept(environments: string[]): Promise; - enable(environments: string[]): Promise; + disable(environments: IEnvironment[]): Promise; + enable(environments: IEnvironment[]): Promise; } diff --git a/src/lib/types/stores/project-store.ts b/src/lib/types/stores/project-store.ts index 2e4c41d8b5f..aca4caa9507 100644 --- a/src/lib/types/stores/project-store.ts +++ b/src/lib/types/stores/project-store.ts @@ -1,3 +1,4 @@ +import { IEnvironmentProjectLink } from 'lib/db/project-store'; import { IProject, IProjectWithCount } from '../model'; import { Store } from './store'; @@ -35,4 +36,7 @@ export interface IProjectStore extends Store { getProjectsWithCounts(query?: IProjectQuery): Promise; count(): Promise; getAll(query?: IProjectQuery): Promise; + getProjectLinksForEnvironments( + environments: string[], + ): Promise; } diff --git a/src/test/e2e/services/environment-service.test.ts b/src/test/e2e/services/environment-service.test.ts index 73823cce4b9..e2510559be0 100644 --- a/src/test/e2e/services/environment-service.test.ts +++ b/src/test/e2e/services/environment-service.test.ts @@ -150,7 +150,7 @@ test('Setting an override disables all other envs', async () => { type: 'production', }); - //Set these to the wrong state so we can assert that overriding them flips + //Set these to the wrong state so we can assert that overriding them flips their state await service.toggleEnvironment(disabledEnvName, true); await service.toggleEnvironment(enabledEnvName, false); @@ -165,7 +165,6 @@ test('Setting an override disables all other envs', async () => { .filter((x) => x.name != enabledEnvName) .map((env) => env.enabled); - console.log(allOtherEnvironments); expect(targetedEnvironment.enabled).toBe(true); expect(allOtherEnvironments.every((x) => x === false)).toBe(true); }); @@ -189,3 +188,47 @@ test('Passing an empty override does nothing', async () => { expect(targetedEnvironment.enabled).toBe(true); }); + +test('When given overrides should remap projects to override environments', async () => { + const enabledEnvName = 'enabled'; + const ignoredEnvName = 'ignored'; + const disabledEnvName = 'disabled'; + const toggleName = 'test-toggle'; + + await db.stores.environmentStore.create({ + name: enabledEnvName, + type: 'production', + }); + + await db.stores.environmentStore.create({ + name: ignoredEnvName, + type: 'production', + }); + + await db.stores.environmentStore.create({ + name: disabledEnvName, + type: 'production', + }); + + await service.toggleEnvironment(disabledEnvName, true); + await service.toggleEnvironment(ignoredEnvName, true); + await service.toggleEnvironment(enabledEnvName, false); + + await stores.featureToggleStore.create('default', { + name: toggleName, + type: 'release', + description: '', + stale: false, + }); + + await service.addEnvironmentToProject(disabledEnvName, 'default'); + + await service.overrideEnabledProjects([enabledEnvName]); + + const projects = await stores.projectStore.getEnvironmentsForProject( + 'default', + ); + + expect(projects).toContain('enabled'); + expect(projects).not.toContain('default'); +}); diff --git a/src/test/fixtures/fake-environment-store.ts b/src/test/fixtures/fake-environment-store.ts index 35847aba578..0df598952ae 100644 --- a/src/test/fixtures/fake-environment-store.ts +++ b/src/test/fixtures/fake-environment-store.ts @@ -10,16 +10,18 @@ export default class FakeEnvironmentStore implements IEnvironmentStore { environments: IEnvironment[] = []; - disableAllExcept(environments: string[]): Promise { + disable(environments: IEnvironment[]): Promise { for (let env of this.environments) { - if (!environments.includes(env.name)) env.enabled = false; + if (environments.map((e) => e.name).includes(env.name)) + env.enabled = false; } return Promise.resolve(); } - enable(environments: string[]): Promise { + enable(environments: IEnvironment[]): Promise { for (let env of this.environments) { - if (environments.includes(env.name)) env.enabled = true; + if (environments.map((e) => e.name).includes(env.name)) + env.enabled = true; } return Promise.resolve(); } diff --git a/src/test/fixtures/fake-project-store.ts b/src/test/fixtures/fake-project-store.ts index 2494b508e2b..9ae835fbfd4 100644 --- a/src/test/fixtures/fake-project-store.ts +++ b/src/test/fixtures/fake-project-store.ts @@ -5,15 +5,23 @@ import { } from '../../lib/types/stores/project-store'; import { IProject, IProjectWithCount } from '../../lib/types/model'; import NotFoundError from '../../lib/error/notfound-error'; +import { IEnvironmentProjectLink } from 'lib/db/project-store'; export default class FakeProjectStore implements IProjectStore { + projects: IProject[] = []; + + projectEnvironment: Map> = new Map(); + getEnvironmentsForProject(): Promise { throw new Error('Method not implemented.'); } - projects: IProject[] = []; - - projectEnvironment: Map> = new Map(); + getProjectLinksForEnvironments( + // eslint-disable-next-line @typescript-eslint/no-unused-vars + environments: string[], + ): Promise { + throw new Error('Method not implemented.'); + } async addEnvironmentToProject( // eslint-disable-next-line @typescript-eslint/no-unused-vars From bf95a40527b937a4145595eb6d7d86434f06df0f Mon Sep 17 00:00:00 2001 From: sighphyre Date: Mon, 14 Mar 2022 09:48:54 +0200 Subject: [PATCH 3/5] docs: describe details of enabled_environments in unleash config docs --- website/docs/deploy/configuring-unleash.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/website/docs/deploy/configuring-unleash.md b/website/docs/deploy/configuring-unleash.md index 700811097d5..678ba51d533 100644 --- a/website/docs/deploy/configuring-unleash.md +++ b/website/docs/deploy/configuring-unleash.md @@ -131,6 +131,10 @@ unleash.start(unleashOptions); - **versionCheck** - the object deciding where to check for latest version - `url` - The url to check version (Defaults to `https://version.unleash.run`) - Overridable with (`UNLEASH_VERSION_URL`) - `enable` - Whether version checking is enabled (defaults to true) - Overridable with (`CHECK_VERSION`) (if anything other than `true`, does not check) +- **environmentEnableOverrides** - A list of environment names to force enable at startup. This is feature should be + used with caution. When passed a list, this will enable each environment in that list and disable all other environments. You can't use this to disable all environments, passing an empty list will do nothing. If one of the given environments is not already enabled on startup then it will also enable projects and toggles for that environment. Note that if one of the passed environments doesn't already exist this will do nothing aside from log a warning. + + You can also set the environment variable `ENABLED_ENVIRONMENTS` to a comma delimited string of environment names to override environments. ### Disabling Auto-Start {#disabling-auto-start} From 91fa0ed296f84d26d75d4c93c4edeee172af50f5 Mon Sep 17 00:00:00 2001 From: sighphyre Date: Mon, 14 Mar 2022 10:01:05 +0200 Subject: [PATCH 4/5] fix: update config tests for enabled environments --- src/lib/__snapshots__/create-config.test.ts.snap | 1 + 1 file changed, 1 insertion(+) diff --git a/src/lib/__snapshots__/create-config.test.ts.snap b/src/lib/__snapshots__/create-config.test.ts.snap index c9536182987..609b84065bd 100644 --- a/src/lib/__snapshots__/create-config.test.ts.snap +++ b/src/lib/__snapshots__/create-config.test.ts.snap @@ -41,6 +41,7 @@ Object { }, "enableOAS": false, "enterpriseVersion": undefined, + "environmentEnableOverrides": Array [], "eventBus": EventEmitter { "_events": Object {}, "_eventsCount": 0, From ba07080aada3ae638ff659a9d4b4fc5dcb2e0730 Mon Sep 17 00:00:00 2001 From: sighphyre Date: Wed, 16 Mar 2022 14:29:11 +0200 Subject: [PATCH 5/5] chore: extend tests for enabled environments --- .../e2e/services/environment-service.test.ts | 35 +++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/src/test/e2e/services/environment-service.test.ts b/src/test/e2e/services/environment-service.test.ts index e2510559be0..ac21c74df59 100644 --- a/src/test/e2e/services/environment-service.test.ts +++ b/src/test/e2e/services/environment-service.test.ts @@ -232,3 +232,38 @@ test('When given overrides should remap projects to override environments', asyn expect(projects).toContain('enabled'); expect(projects).not.toContain('default'); }); + +test('Override works correctly when enabling default and disabling prod and dev', async () => { + const defaultEnvironment = 'default'; + const prodEnvironment = 'production'; + const devEnvironment = 'development'; + + await db.stores.environmentStore.create({ + name: prodEnvironment, + type: 'production', + }); + + await db.stores.environmentStore.create({ + name: devEnvironment, + type: 'development', + }); + await service.toggleEnvironment(prodEnvironment, true); + await service.toggleEnvironment(devEnvironment, true); + + await service.overrideEnabledProjects([defaultEnvironment]); + + const environments = await service.getAll(); + const targetedEnvironment = environments.find( + (env) => env.name == defaultEnvironment, + ); + + const allOtherEnvironments = environments + .filter((x) => x.name != defaultEnvironment) + .map((env) => env.enabled); + const envNames = environments.map((x) => x.name); + + expect(envNames).toContain('production'); + expect(envNames).toContain('development'); + expect(targetedEnvironment.enabled).toBe(true); + expect(allOtherEnvironments.every((x) => x === false)).toBe(true); +});