1
0
mirror of https://github.com/Unleash/unleash.git synced 2025-01-25 00:07:47 +01:00

fixes 2-456: Preserve all data from strategy import (#2720)

## What
Previously when importing strategies we've used the same data type we've
used for creating strategies (the minimal, a name, an optional
description, optional parameters and an optional editable column). This
meant that exporting strategies and then importing them would reactivate
deprecated strategies. This PR changes to allow the import to preserve
all the data coming in the export file.

## Tests
Added four new tests, two new unit tests using our fake stores and two
new e2e tests. Interestingly the ones in the fake store ran green before
this change as well, probably because we just insert the parsed json
object in the fake store, whereas the real store actually converts the
object from camelCasing to the postgresql snake_casing standard.

## Discussion points:
### Mismatch between fake and real stores
This is inevitable since storing things in javascript arrays vs saving
in a real database will have some differences, but this again shows the
value of our e2e tests.

### Invariants
Should we see if we can add some invariants to our import/export so that
we can write some proptests for it? One candidate is commutativity of
import/export. On a fresh database importing and then exporting should
yield the same file that was imported provided all flags are turned on.
Candidate for Q1 improvement of import/export.
This commit is contained in:
Christopher Kolstad 2022-12-21 13:33:41 +01:00 committed by GitHub
parent be045dc13a
commit 5b66346e56
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 127 additions and 6 deletions

View File

@ -6,6 +6,7 @@ import {
IEditableStrategy,
IMinimalStrategyRow,
IStrategy,
IStrategyImport,
IStrategyStore,
} from '../types/stores/strategy-store';
@ -156,9 +157,16 @@ export default class StrategyStore implements IStrategyStore {
await this.db(TABLE).where({ name }).del();
}
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
async importStrategy(data): Promise<void> {
const rowData = this.eventDataToRow(data);
async importStrategy(data: IStrategyImport): Promise<void> {
const rowData = {
name: data.name,
description: data.description,
deprecated: data.deprecated || false,
parameters: JSON.stringify(data.parameters || []),
built_in: data.builtIn ? 1 : 0,
sort_order: data.sortOrder || 9999,
display_name: data.displayName,
};
await this.db(TABLE).insert(rowData).onConflict(['name']).merge();
}

View File

@ -12,7 +12,7 @@ import {
PROJECT_IMPORT,
} from '../types/events';
import { GLOBAL_ENV } from '../types/environment';
import variantsExportV3 from '../../test/examples/variantsexport_v3.json';
const oldExportExample = require('./state-service-export-v1.json');
function getSetup() {
@ -813,3 +813,55 @@ test('Import v1 and exporting v2 should work', async () => {
).toBeTruthy();
expect(exported.featureStrategies).toHaveLength(strategiesCount);
});
test('Importing states with deprecated strategies should keep their deprecated state', async () => {
const { stateService, stores } = getSetup();
const deprecatedStrategyExample = {
version: 4,
features: [],
strategies: [
{
name: 'deprecatedstrat',
description: 'This should be deprecated when imported',
deprecated: true,
parameters: [],
builtIn: false,
sortOrder: 9999,
displayName: 'Deprecated strategy',
},
],
featureStrategies: [],
};
await stateService.import({
data: deprecatedStrategyExample,
userName: 'strategy-importer',
dropBeforeImport: true,
keepExisting: false,
});
const deprecatedStrategy = await stores.strategyStore.get(
'deprecatedstrat',
);
expect(deprecatedStrategy.deprecated).toBe(true);
});
test('Exporting a deprecated strategy and then importing it should keep correct state', async () => {
const { stateService, stores } = getSetup();
await stateService.import({
data: variantsExportV3,
keepExisting: false,
dropBeforeImport: true,
userName: 'strategy importer',
});
const rolloutRandom = await stores.strategyStore.get(
'gradualRolloutRandom',
);
expect(rolloutRandom.deprecated).toBe(true);
const rolloutSessionId = await stores.strategyStore.get(
'gradualRolloutSessionId',
);
expect(rolloutSessionId.deprecated).toBe(true);
const rolloutUserId = await stores.strategyStore.get(
'gradualRolloutUserId',
);
expect(rolloutUserId.deprecated).toBe(true);
});

View File

@ -23,6 +23,16 @@ export interface IMinimalStrategy {
parameters?: any[];
}
export interface IStrategyImport {
name: string;
description?: string;
deprecated?: boolean;
parameters?: object[];
builtIn?: boolean;
sortOrder?: number;
displayName?: string;
}
export interface IMinimalStrategyRow {
name: string;
description?: string;
@ -36,7 +46,7 @@ export interface IStrategyStore extends Store<IStrategy, string> {
updateStrategy(update: IMinimalStrategy): Promise<void>;
deprecateStrategy({ name }: Pick<IStrategy, 'name'>): Promise<void>;
reactivateStrategy({ name }: Pick<IStrategy, 'name'>): Promise<void>;
importStrategy(data: IMinimalStrategy): Promise<void>;
importStrategy(data: IStrategyImport): Promise<void>;
dropCustomStrategies(): Promise<void>;
count(): Promise<number>;
}

View File

@ -162,3 +162,53 @@ test('Should import variants in new format (per environment)', async () => {
let featureEnvironments = await stores.featureEnvironmentStore.getAll();
expect(featureEnvironments).toHaveLength(6); // 3 environments, 2 features === 6 rows
});
test('Importing states with deprecated strategies should keep their deprecated state', async () => {
const deprecatedStrategyExample = {
version: 4,
features: [],
strategies: [
{
name: 'deprecatedstrat',
description: 'This should be deprecated when imported',
deprecated: true,
parameters: [],
builtIn: false,
sortOrder: 9999,
displayName: 'Deprecated strategy',
},
],
featureStrategies: [],
};
await stateService.import({
data: deprecatedStrategyExample,
userName: 'strategy-importer',
dropBeforeImport: true,
keepExisting: false,
});
const deprecatedStrategy = await stores.strategyStore.get(
'deprecatedstrat',
);
expect(deprecatedStrategy.deprecated).toBe(true);
});
test('Exporting a deprecated strategy and then importing it should keep correct state', async () => {
await stateService.import({
data: oldFormat,
keepExisting: false,
dropBeforeImport: true,
userName: 'strategy importer',
});
const rolloutRandom = await stores.strategyStore.get(
'gradualRolloutRandom',
);
expect(rolloutRandom.deprecated).toBe(true);
const rolloutSessionId = await stores.strategyStore.get(
'gradualRolloutSessionId',
);
expect(rolloutSessionId.deprecated).toBe(true);
const rolloutUserId = await stores.strategyStore.get(
'gradualRolloutUserId',
);
expect(rolloutUserId.deprecated).toBe(true);
});

View File

@ -2,6 +2,7 @@ import {
IEditableStrategy,
IMinimalStrategy,
IStrategy,
IStrategyImport,
IStrategyStore,
} from '../../lib/types/stores/strategy-store';
import NotFoundError from '../../lib/error/notfound-error';
@ -104,7 +105,7 @@ export default class FakeStrategiesStore implements IStrategyStore {
throw new NotFoundError(`Could not find strategy with name: ${name}`);
}
async importStrategy(data: IMinimalStrategy): Promise<void> {
async importStrategy(data: IStrategyImport): Promise<void> {
return this.createStrategy(data);
}