From 89bea0d5329f4fb2c72a8908767bfac6b2182fc8 Mon Sep 17 00:00:00 2001 From: andreas-unleash Date: Thu, 25 Jan 2024 10:53:43 +0200 Subject: [PATCH] fix: clone variants (featureEnv and strategy) when cloning an env (#6026) Fixes 2 bugs - Strategy variants - Feature env variants not being cloned when cloning an environment Closes # [SR-350](https://linear.app/unleash/issue/SR-350/cloning-environment-does-not-clone-variants-or-strategy-variants) Manual test verifies the fix Screenshot 2024-01-24 at 16 48 28 Screenshot 2024-01-24 at 16 48 10 --------- Signed-off-by: andreas-unleash --- src/lib/db/feature-environment-store.ts | 10 +- .../feature-environment-store.e2e.test.ts | 101 +++++++++++++++++- 2 files changed, 107 insertions(+), 4 deletions(-) diff --git a/src/lib/db/feature-environment-store.ts b/src/lib/db/feature-environment-store.ts index b8938aec12..139e08707e 100644 --- a/src/lib/db/feature-environment-store.ts +++ b/src/lib/db/feature-environment-store.ts @@ -152,7 +152,7 @@ export class FeatureEnvironmentStore implements IFeatureEnvironmentStore { await this.db('feature_environments') .insert({ feature_name: featureName, environment, enabled }) .onConflict(['environment', 'feature_name']) - .merge('enabled'); + .merge(['enabled']); } // TODO: move to project store. @@ -366,8 +366,11 @@ export class FeatureEnvironmentStore implements IFeatureEnvironmentStore { projects: string[], ): Promise { await this.db.raw( - `INSERT INTO ${T.featureEnvs} ( - SELECT distinct ? AS environment, feature_name, enabled FROM ${T.featureEnvs} INNER JOIN ${T.features} ON ${T.featureEnvs}.feature_name = ${T.features}.name WHERE environment = ? AND project = ANY(?))`, + `INSERT INTO ${T.featureEnvs} (environment, feature_name, enabled, variants) + SELECT DISTINCT ? AS environemnt, fe.feature_name, fe.enabled, fe.variants + FROM ${T.featureEnvs} AS fe + INNER JOIN ${T.features} AS f ON fe.feature_name = f.name + WHERE fe.environment = ? AND f.project = ANY(?)`, [destinationEnvironment, sourceEnvironment, projects], ); } @@ -441,6 +444,7 @@ export class FeatureEnvironmentStore implements IFeatureEnvironmentStore { parameters: JSON.stringify(featureStrategy.parameters), constraints: JSON.stringify(featureStrategy.constraints), sort_order: featureStrategy.sort_order, + variants: JSON.stringify(featureStrategy.variants), }; }, ); diff --git a/src/test/e2e/stores/feature-environment-store.e2e.test.ts b/src/test/e2e/stores/feature-environment-store.e2e.test.ts index dd96717240..694c999416 100644 --- a/src/test/e2e/stores/feature-environment-store.e2e.test.ts +++ b/src/test/e2e/stores/feature-environment-store.e2e.test.ts @@ -1,4 +1,4 @@ -import { IUnleashStores } from '../../../lib/types'; +import { IFeatureStrategiesStore, IUnleashStores } from '../../../lib/types'; import dbInit, { ITestDb } from '../helpers/database-init'; import getLogger from '../../fixtures/no-logger'; import { IFeatureEnvironmentStore } from '../../../lib/types/stores/feature-environment-store'; @@ -10,6 +10,7 @@ let stores: IUnleashStores; let featureEnvironmentStore: IFeatureEnvironmentStore; let featureStore: IFeatureToggleStore; let environmentStore: IEnvironmentStore; +let featureStrategiesStore: IFeatureStrategiesStore; beforeAll(async () => { db = await dbInit('feature_environment_store_serial', getLogger); @@ -17,6 +18,7 @@ beforeAll(async () => { featureEnvironmentStore = stores.featureEnvironmentStore; environmentStore = stores.environmentStore; featureStore = stores.featureToggleStore; + featureStrategiesStore = stores.featureStrategiesStore; }); afterAll(async () => { @@ -74,3 +76,100 @@ test('Setting enabled to not existing value returns 1', async () => { ); expect(changed).toBe(1); }); + +test('Copying features also copies variants', async () => { + const envName = 'copy-env'; + const featureName = 'copy-env-toggle-feature'; + await environmentStore.create({ + name: envName, + enabled: true, + type: 'test', + }); + await featureStore.create('default', { + name: featureName, + createdByUserId: 9999, + }); + await featureEnvironmentStore.connectProject(envName, 'default'); + await featureEnvironmentStore.connectFeatures(envName, 'default'); + + const variant = { + name: 'a', + weight: 1, + stickiness: 'default', + weightType: 'fix' as any, + }; + await featureEnvironmentStore.setVariantsToFeatureEnvironments( + featureName, + [envName], + [variant], + ); + + await environmentStore.create({ + name: 'clone', + enabled: true, + type: 'test', + }); + await featureEnvironmentStore.connectProject('clone', 'default'); + + await featureEnvironmentStore.copyEnvironmentFeaturesByProjects( + envName, + 'clone', + ['default'], + ); + + const cloned = await featureEnvironmentStore.get({ + featureName: featureName, + environment: 'clone', + }); + expect(cloned.variants).toMatchObject([variant]); +}); + +test('Copying strategies also copies strategy variants', async () => { + const envName = 'copy-strategy'; + const featureName = 'copy-env-strategy-feature'; + await environmentStore.create({ + name: envName, + enabled: true, + type: 'test', + }); + await featureStore.create('default', { + name: featureName, + createdByUserId: 9999, + }); + await featureEnvironmentStore.connectProject(envName, 'default'); + await featureEnvironmentStore.connectFeatures(envName, 'default'); + + const strategyVariant = { + name: 'a', + weight: 1, + stickiness: 'default', + weightType: 'fix' as any, + }; + await featureStrategiesStore.createStrategyFeatureEnv({ + environment: envName, + projectId: 'default', + featureName, + strategyName: 'default', + variants: [strategyVariant], + parameters: {}, + constraints: [], + }); + + await environmentStore.create({ + name: 'clone-2', + enabled: true, + type: 'test', + }); + await featureEnvironmentStore.connectProject('clone-2', 'default'); + + await featureEnvironmentStore.cloneStrategies(envName, 'clone-2'); + + const clonedStrategy = + await featureStrategiesStore.getStrategiesForFeatureEnv( + 'default', + featureName, + 'clone-2', + ); + expect(clonedStrategy.length).toBe(1); + expect(clonedStrategy[0].variants).toMatchObject([strategyVariant]); +});