1
0
mirror of https://github.com/Unleash/unleash.git synced 2024-12-22 19:07:54 +01:00

feat: Permissions update import (#3141)

This commit is contained in:
Jaanus Sellin 2023-02-17 12:58:55 +02:00 committed by GitHub
parent 350b55644a
commit 996fb1c104
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 542 additions and 131 deletions

View File

@ -0,0 +1,346 @@
import {
IUnleashTest,
setupAppWithAuth,
} from '../../test/e2e/helpers/test-helper';
import dbInit, { ITestDb } from '../../test/e2e/helpers/database-init';
import getLogger from '../../test/fixtures/no-logger';
import {
DEFAULT_PROJECT,
IEnvironmentStore,
IEventStore,
IFeatureToggleStore,
IProjectStore,
IUnleashStores,
RoleName,
} from '../types';
import { ImportTogglesSchema, VariantsSchema } from '../openapi';
import { IContextFieldDto } from '../types/stores/context-field-store';
import { AccessService } from '../services';
import { DEFAULT_ENV } from '../util';
let app: IUnleashTest;
let db: ITestDb;
let eventStore: IEventStore;
let environmentStore: IEnvironmentStore;
let projectStore: IProjectStore;
let toggleStore: IFeatureToggleStore;
let accessService: AccessService;
let adminRole;
let stores: IUnleashStores;
const regularUserName = 'import-user';
const adminUserName = 'admin-user';
const validateImport = (importPayload: ImportTogglesSchema, status = 200) =>
app.request
.post('/api/admin/features-batch/full-validate')
.send(importPayload)
.set('Content-Type', 'application/json')
.expect(status);
const createContextField = async (contextField: IContextFieldDto) => {
await app.request.post(`/api/admin/context`).send(contextField).expect(201);
};
const createFeature = async (featureName: string) => {
await app.request
.post(`/api/admin/projects/${DEFAULT_PROJECT}/features`)
.send({
name: featureName,
})
.set('Content-Type', 'application/json')
.expect(201);
};
const createFeatureToggleWithStrategy = async (featureName: string) => {
await createFeature(featureName);
return app.request
.post(
`/api/admin/projects/${DEFAULT_PROJECT}/features/${featureName}/environments/${DEFAULT_ENV}/strategies`,
)
.send({
name: 'default',
parameters: {
userId: 'string',
},
})
.expect(200);
};
const archiveFeature = async (featureName: string) => {
await app.request
.delete(
`/api/admin/projects/${DEFAULT_PROJECT}/features/${featureName}`,
)
.set('Content-Type', 'application/json')
.expect(202);
};
const createProject = async () => {
await db.stores.environmentStore.create({
name: DEFAULT_ENV,
type: 'production',
});
await db.stores.projectStore.create({
name: DEFAULT_PROJECT,
description: '',
id: DEFAULT_PROJECT,
});
};
const newFeature = 'new_feature';
const archivedFeature = 'archived_feature';
const existingFeature = 'existing_feature';
const variants: VariantsSchema = [
{
name: 'variantA',
weight: 500,
payload: {
type: 'string',
value: 'payloadA',
},
overrides: [],
stickiness: 'default',
weightType: 'variable',
},
{
name: 'variantB',
weight: 500,
payload: {
type: 'string',
value: 'payloadB',
},
overrides: [],
stickiness: 'default',
weightType: 'variable',
},
];
const feature1: ImportTogglesSchema['data']['features'][0] = {
project: 'old_project',
name: archivedFeature,
};
const feature2: ImportTogglesSchema['data']['features'][0] = {
project: 'old_project',
name: newFeature,
};
const feature3: ImportTogglesSchema['data']['features'][0] = {
project: 'old_project',
name: existingFeature,
};
const constraints: ImportTogglesSchema['data']['featureStrategies'][0]['constraints'] =
[
{
values: ['conduit'],
inverted: false,
operator: 'IN',
contextName: 'appName',
caseInsensitive: false,
},
];
const exportedStrategy: ImportTogglesSchema['data']['featureStrategies'][0] = {
featureName: newFeature,
id: '798cb25a-2abd-47bd-8a95-40ec13472309',
name: 'default',
parameters: {},
constraints,
};
const tags = [
{
featureName: newFeature,
tagType: 'simple',
tagValue: 'tag1',
},
{
featureName: newFeature,
tagType: 'simple',
tagValue: 'tag2',
},
{
featureName: newFeature,
tagType: 'special_tag',
tagValue: 'feature_tagged',
},
];
const tagTypes = [
{ name: 'bestt', description: 'test' },
{ name: 'special_tag', description: 'this is my special tag' },
{ name: 'special_tag', description: 'this is my special tag' }, // deliberate duplicate
];
const importPayload: ImportTogglesSchema = {
data: {
features: [feature1, feature2, feature3],
featureStrategies: [exportedStrategy],
featureEnvironments: [
{
enabled: true,
environment: 'irrelevant',
featureName: newFeature,
name: newFeature,
variants,
},
],
featureTags: tags,
tagTypes,
contextFields: [],
segments: [],
},
project: DEFAULT_PROJECT,
environment: DEFAULT_ENV,
};
const createUserEditorAccess = async (name, email) => {
const { userStore } = stores;
const user = await userStore.insert({ name, email });
return user;
};
const createUserAdminAccess = async (name, email) => {
const { userStore } = stores;
const user = await userStore.insert({ name, email });
await accessService.addUserToRole(user.id, adminRole.id, 'default');
return user;
};
const loginRegularUser = () =>
app.request
.post(`/auth/demo/login`)
.send({
email: `${regularUserName}@getunleash.io`,
})
.expect(200);
const loginAdminUser = () =>
app.request
.post(`/auth/demo/login`)
.send({
email: `${adminUserName}@getunleash.io`,
})
.expect(200);
beforeAll(async () => {
db = await dbInit('export_import_permissions_api_serial', getLogger);
stores = db.stores;
app = await setupAppWithAuth(
db.stores,
{
experimental: {
flags: {
featuresExportImport: true,
},
},
},
db.rawDatabase,
);
eventStore = db.stores.eventStore;
environmentStore = db.stores.environmentStore;
projectStore = db.stores.projectStore;
toggleStore = db.stores.featureToggleStore;
accessService = app.services.accessService;
const roles = await accessService.getRootRoles();
adminRole = roles.find((role) => role.name === RoleName.ADMIN);
await createUserEditorAccess(
regularUserName,
`${regularUserName}@getunleash.io`,
);
await createUserAdminAccess(
adminUserName,
`${adminUserName}@getunleash.io`,
);
});
beforeEach(async () => {
await eventStore.deleteAll();
await toggleStore.deleteAll();
await projectStore.deleteAll();
await environmentStore.deleteAll();
});
afterAll(async () => {
await app.destroy();
await db.destroy();
});
test('validate import data', async () => {
await loginAdminUser();
await createProject();
const contextField: IContextFieldDto = {
name: 'validate_context_field',
legalValues: [{ value: 'Value1' }],
};
const createdContextField: IContextFieldDto = {
name: 'created_context_field',
legalValues: [{ value: 'new_value' }],
};
await createFeature(archivedFeature);
await archiveFeature(archivedFeature);
await createFeatureToggleWithStrategy(existingFeature);
await createContextField(contextField);
const importPayloadWithContextFields: ImportTogglesSchema = {
...importPayload,
data: {
...importPayload.data,
featureStrategies: [{ name: 'customStrategy' }],
segments: [{ id: 1, name: 'customSegment' }],
contextFields: [
{
...contextField,
legalValues: [{ value: 'Value2' }],
},
createdContextField,
],
},
};
await loginRegularUser();
const { body } = await validateImport(importPayloadWithContextFields, 200);
expect(body).toMatchObject({
errors: [
{
message:
'We detected the following custom strategy in the import file that needs to be created first:',
affectedItems: ['customStrategy'],
},
{
message:
'We detected the following context fields that do not have matching legal values with the imported ones:',
affectedItems: [contextField.name],
},
],
warnings: [
{
message:
'The following features will not be imported as they are currently archived. To import them, please unarchive them first:',
affectedItems: [archivedFeature],
},
],
permissions: [
{
message:
'We detected you are missing the following permissions:',
affectedItems: [
'Create feature toggles',
'Update feature toggles',
'Update tag types',
'Create context fields',
'Create activation strategies',
'Delete activation strategies',
'Update variants on environment',
],
},
],
});
});

View File

@ -28,6 +28,7 @@ import {
UPDATE_FEATURE,
UPDATE_FEATURE_ENVIRONMENT_VARIANTS,
UPDATE_TAG_TYPE,
WithRequired,
} from '../types';
import {
ExportResultSchema,
@ -162,8 +163,8 @@ export default class ExportImportService {
const errors = this.compileErrors(
dto.project,
unsupportedStrategies,
unsupportedContextFields,
otherProjectFeatures,
unsupportedContextFields,
);
const warnings = this.compileWarnings(
usedCustomStrategies,
@ -224,24 +225,32 @@ export default class ExportImportService {
}
private async importStrategies(dto: ImportTogglesSchema, user: User) {
const hasFeatureName = (
featureStrategy: FeatureStrategySchema,
): featureStrategy is WithRequired<
FeatureStrategySchema,
'featureName'
> => Boolean(featureStrategy.featureName);
await Promise.all(
dto.data.featureStrategies?.map((featureStrategy) =>
this.featureToggleService.createStrategy(
{
name: featureStrategy.name,
constraints: featureStrategy.constraints,
parameters: featureStrategy.parameters,
segments: featureStrategy.segments,
sortOrder: featureStrategy.sortOrder,
},
{
featureName: featureStrategy.featureName,
environment: dto.environment,
projectId: dto.project,
},
extractUsernameFromUser(user),
dto.data.featureStrategies
?.filter(hasFeatureName)
.map((featureStrategy) =>
this.featureToggleService.createStrategy(
{
name: featureStrategy.name,
constraints: featureStrategy.constraints,
parameters: featureStrategy.parameters,
segments: featureStrategy.segments,
sortOrder: featureStrategy.sortOrder,
},
{
featureName: featureStrategy.featureName,
environment: dto.environment,
projectId: dto.project,
},
extractUsernameFromUser(user),
),
),
),
);
}
@ -268,7 +277,7 @@ export default class ExportImportService {
}
private async importContextFields(dto: ImportTogglesSchema, user: User) {
const newContextFields = await this.getNewContextFields(dto);
const newContextFields = (await this.getNewContextFields(dto)) || [];
await Promise.all(
newContextFields.map((contextField) =>
this.contextService.createContextField(
@ -297,9 +306,12 @@ export default class ExportImportService {
}
private async importToggleVariants(dto: ImportTogglesSchema, user: User) {
const featureEnvsWithVariants = dto.data.featureEnvironments?.filter(
(featureEnvironment) => featureEnvironment.variants?.length > 0,
);
const featureEnvsWithVariants =
dto.data.featureEnvironments?.filter(
(featureEnvironment) =>
Array.isArray(featureEnvironment.variants) &&
featureEnvironment.variants.length > 0,
) || [];
await Promise.all(
featureEnvsWithVariants.map((featureEnvironment) =>
this.featureToggleService.saveVariantsOnEnv(
@ -335,7 +347,10 @@ export default class ExportImportService {
const unsupportedContextFields = await this.getUnsupportedContextFields(
dto,
);
if (unsupportedContextFields.length > 0) {
if (
Array.isArray(unsupportedContextFields) &&
unsupportedContextFields.length > 0
) {
throw new BadDataError(
`Context fields with errors: ${unsupportedContextFields
.map((field) => field.name)
@ -387,9 +402,10 @@ export default class ExportImportService {
private async removeArchivedFeatures(dto: ImportTogglesSchema) {
const archivedFeatures = await this.getArchivedFeatures(dto);
const featureTags = dto.data.featureTags.filter(
(tag) => !archivedFeatures.includes(tag.featureName),
);
const featureTags =
dto.data.featureTags?.filter(
(tag) => !archivedFeatures.includes(tag.featureName),
) || [];
return {
...dto,
data: {
@ -397,12 +413,14 @@ export default class ExportImportService {
features: dto.data.features.filter(
(feature) => !archivedFeatures.includes(feature.name),
),
featureEnvironments: dto.data.featureEnvironments.filter(
featureEnvironments: dto.data.featureEnvironments?.filter(
(environment) =>
environment.featureName &&
!archivedFeatures.includes(environment.featureName),
),
featureStrategies: dto.data.featureStrategies.filter(
(strategy) =>
strategy.featureName &&
!archivedFeatures.includes(strategy.featureName),
),
featureTags,
@ -429,8 +447,8 @@ export default class ExportImportService {
private compileErrors(
projectName: string,
strategies: FeatureStrategySchema[],
contextFields: IContextFieldDto[],
otherProjectFeatures: string[],
contextFields?: IContextFieldDto[],
) {
const errors: ImportTogglesValidateItemSchema[] = [];
@ -441,7 +459,7 @@ export default class ExportImportService {
affectedItems: strategies.map((strategy) => strategy.name),
});
}
if (contextFields.length > 0) {
if (Array.isArray(contextFields) && contextFields.length > 0) {
errors.push({
message:
'We detected the following context fields that do not have matching legal values with the imported ones:',
@ -558,25 +576,52 @@ export default class ExportImportService {
dto: ImportTogglesSchema,
user: User,
): Promise<string[]> {
const requiredImportPermission = [
CREATE_FEATURE,
UPDATE_FEATURE,
DELETE_FEATURE_STRATEGY,
CREATE_FEATURE_STRATEGY,
UPDATE_FEATURE_ENVIRONMENT_VARIANTS,
];
const [newTagTypes, newContextFields] = await Promise.all([
this.getNewTagTypes(dto),
this.getNewContextFields(dto),
]);
const permissions = [...requiredImportPermission];
const permissions = [UPDATE_FEATURE];
if (newTagTypes.length > 0) {
permissions.push(UPDATE_TAG_TYPE);
}
if (newContextFields.length > 0) {
if (Array.isArray(newContextFields) && newContextFields.length > 0) {
permissions.push(CREATE_CONTEXT_FIELD);
}
const strategiesExistForFeatures =
await this.importTogglesStore.strategiesExistForFeatures(
dto.data.features.map((feature) => feature.name),
dto.environment,
);
if (strategiesExistForFeatures) {
permissions.push(DELETE_FEATURE_STRATEGY);
}
if (dto.data.featureStrategies.length > 0) {
permissions.push(CREATE_FEATURE_STRATEGY);
}
const featureEnvsWithVariants =
dto.data.featureEnvironments?.filter(
(featureEnvironment) =>
Array.isArray(featureEnvironment.variants) &&
featureEnvironment.variants.length > 0,
) || [];
if (featureEnvsWithVariants.length > 0) {
permissions.push(UPDATE_FEATURE_ENVIRONMENT_VARIANTS);
}
const existingFeatures =
await this.importTogglesStore.getExistingFeatures(
dto.data.features.map((feature) => feature.name),
);
if (existingFeatures.length < dto.data.features.length) {
permissions.push(CREATE_FEATURE);
}
const displayPermissions =
await this.importTogglesStore.getDisplayPermissions(permissions);
@ -603,9 +648,10 @@ export default class ExportImportService {
const existingTagTypes = (await this.tagTypeService.getAll()).map(
(tagType) => tagType.name,
);
const newTagTypes = dto.data.tagTypes?.filter(
(tagType) => !existingTagTypes.includes(tagType.name),
);
const newTagTypes =
dto.data.tagTypes?.filter(
(tagType) => !existingTagTypes.includes(tagType.name),
) || [];
const uniqueTagTypes = [
...new Map(newTagTypes.map((item) => [item.name, item])).values(),
];
@ -657,10 +703,10 @@ export default class ExportImportService {
const filteredContextFields = contextFields.filter(
(field) =>
featureEnvironments.some((featureEnv) =>
featureEnv.variants.some(
featureEnv.variants?.some(
(variant) =>
variant.stickiness === field.name ||
variant.overrides.some(
variant.overrides?.some(
(override) =>
override.contextName === field.name,
),
@ -677,7 +723,7 @@ export default class ExportImportService {
);
const filteredSegments = segments.filter((segment) =>
featureStrategies.some((strategy) =>
strategy.segments.includes(segment.id),
strategy.segments?.includes(segment.id),
),
);
const filteredTagTypes = tagTypes.filter((tagType) =>

View File

@ -1,10 +1,11 @@
import {
IUnleashTest,
setupAppWithCustomConfig,
} from '../../helpers/test-helper';
import dbInit, { ITestDb } from '../../helpers/database-init';
import getLogger from '../../../fixtures/no-logger';
} from '../../test/e2e/helpers/test-helper';
import dbInit, { ITestDb } from '../../test/e2e/helpers/database-init';
import getLogger from '../../test/fixtures/no-logger';
import {
DEFAULT_PROJECT,
FeatureToggleDTO,
IEnvironmentStore,
IEventStore,
@ -13,14 +14,15 @@ import {
ISegment,
IStrategyConfig,
IVariant,
} from 'lib/types';
import { DEFAULT_ENV } from '../../../../lib/util';
} from '../types';
import { DEFAULT_ENV } from '../util';
import {
ContextFieldSchema,
ImportTogglesSchema,
} from '../../../../lib/openapi';
import User from '../../../../lib/types/user';
import { IContextFieldDto } from '../../../../lib/types/stores/context-field-store';
VariantsSchema,
} from '../openapi';
import User from '../types/user';
import { IContextFieldDto } from '../types/stores/context-field-store';
let app: IUnleashTest;
let db: ITestDb;
@ -85,35 +87,30 @@ const createContext = async (context: ContextFieldSchema = defaultContext) => {
.expect(201);
};
const createVariants = async (
project: string,
feature: string,
environment: string,
variants: IVariant[],
) => {
const createVariants = async (feature: string, variants: IVariant[]) => {
await app.services.featureToggleService.saveVariantsOnEnv(
project,
DEFAULT_PROJECT,
feature,
environment,
DEFAULT_ENV,
variants,
new User({ id: 1 }),
);
};
const createProject = async (project: string, environment: string) => {
const createProject = async () => {
await db.stores.environmentStore.create({
name: environment,
name: DEFAULT_ENV,
type: 'production',
});
await db.stores.projectStore.create({
name: project,
name: DEFAULT_PROJECT,
description: '',
id: project,
id: DEFAULT_PROJECT,
});
await app.request
.post(`/api/admin/projects/${project}/environments`)
.post(`/api/admin/projects/${DEFAULT_PROJECT}/environments`)
.send({
environment,
environment: DEFAULT_ENV,
})
.expect(200);
};
@ -128,9 +125,9 @@ const createContextField = async (contextField: IContextFieldDto) => {
await app.request.post(`/api/admin/context`).send(contextField).expect(201);
};
const createFeature = async (featureName: string, project: string) => {
const createFeature = async (featureName: string) => {
await app.request
.post(`/api/admin/projects/${project}/features`)
.post(`/api/admin/projects/${DEFAULT_PROJECT}/features`)
.send({
name: featureName,
})
@ -138,9 +135,11 @@ const createFeature = async (featureName: string, project: string) => {
.expect(201);
};
const archiveFeature = async (featureName: string, project: string) => {
const archiveFeature = async (featureName: string) => {
await app.request
.delete(`/api/admin/projects/${project}/features/${featureName}`)
.delete(
`/api/admin/projects/${DEFAULT_PROJECT}/features/${featureName}`,
)
.set('Content-Type', 'application/json')
.expect(202);
};
@ -188,7 +187,7 @@ afterAll(async () => {
test('exports features', async () => {
const segmentName = 'my-segment';
await createProject('default', 'default');
await createProject();
const segment = await createSegment({ name: segmentName, constraints: [] });
const strategy = {
name: 'default',
@ -249,7 +248,7 @@ test('exports features', async () => {
});
test('should export custom context fields from strategies and variants', async () => {
await createProject('default', 'default');
await createProject();
const strategyContext = {
name: 'strategy-context',
legalValues: [
@ -301,7 +300,7 @@ test('should export custom context fields from strategies and variants', async (
};
await createContext(variantStickinessContext);
await createContext(variantOverridesContext);
await createVariants('default', 'first_feature', 'default', [
await createVariants('first_feature', [
{
name: 'irrelevant',
weight: 1000,
@ -351,7 +350,7 @@ test('should export custom context fields from strategies and variants', async (
test('should export tags', async () => {
const featureName = 'first_feature';
await createProject('default', 'default');
await createProject();
await createToggle(
{
name: featureName,
@ -390,7 +389,7 @@ test('should export tags', async () => {
});
test('returns no features, when no feature was requested', async () => {
await createProject('default', 'default');
await createProject();
await createToggle({
name: 'first_feature',
description: 'the #1 feature',
@ -424,34 +423,31 @@ const importToggles = (
.expect(expect);
const defaultFeature = 'first_feature';
const defaultProject = 'default';
const defaultEnvironment = 'defalt';
const variants: ImportTogglesSchema['data']['featureEnvironments'][0]['variants'] =
[
{
name: 'variantA',
weight: 500,
payload: {
type: 'string',
value: 'payloadA',
},
overrides: [],
stickiness: 'default',
weightType: 'variable',
const variants: VariantsSchema = [
{
name: 'variantA',
weight: 500,
payload: {
type: 'string',
value: 'payloadA',
},
{
name: 'variantB',
weight: 500,
payload: {
type: 'string',
value: 'payloadB',
},
overrides: [],
stickiness: 'default',
weightType: 'variable',
overrides: [],
stickiness: 'default',
weightType: 'variable',
},
{
name: 'variantB',
weight: 500,
payload: {
type: 'string',
value: 'payloadB',
},
];
overrides: [],
stickiness: 'default',
weightType: 'variable',
},
];
const exportedFeature: ImportTogglesSchema['data']['features'][0] = {
project: 'old_project',
name: 'first_feature',
@ -522,21 +518,17 @@ const defaultImportPayload: ImportTogglesSchema = {
contextFields: [],
segments: [],
},
project: defaultProject,
environment: defaultEnvironment,
project: DEFAULT_PROJECT,
environment: DEFAULT_ENV,
};
const getFeature = async (feature: string) =>
app.request.get(`/api/admin/features/${feature}`).expect(200);
const getFeatureEnvironment = (
project: string,
feature: string,
environment: string,
) =>
const getFeatureEnvironment = (feature: string) =>
app.request
.get(
`/api/admin/projects/${project}/features/${feature}/environments/${environment}`,
`/api/admin/projects/${DEFAULT_PROJECT}/features/${feature}/environments/${DEFAULT_ENV}`,
)
.expect(200);
@ -551,25 +543,23 @@ const validateImport = (importPayload: ImportTogglesSchema, status = 200) =>
.expect(status);
test('import features to existing project and environment', async () => {
await createProject(defaultProject, defaultEnvironment);
await createProject();
await importToggles(defaultImportPayload);
const { body: importedFeature } = await getFeature(defaultFeature);
expect(importedFeature).toMatchObject({
name: 'first_feature',
project: defaultProject,
project: DEFAULT_PROJECT,
variants,
});
const { body: importedFeatureEnvironment } = await getFeatureEnvironment(
defaultProject,
defaultFeature,
defaultEnvironment,
);
expect(importedFeatureEnvironment).toMatchObject({
name: defaultFeature,
environment: defaultEnvironment,
environment: DEFAULT_ENV,
enabled: true,
strategies: [
{
@ -589,26 +579,24 @@ test('import features to existing project and environment', async () => {
});
test('importing same JSON should work multiple times in a row', async () => {
await createProject(defaultProject, defaultEnvironment);
await createProject();
await importToggles(defaultImportPayload);
await importToggles(defaultImportPayload);
const { body: importedFeature } = await getFeature(defaultFeature);
expect(importedFeature).toMatchObject({
name: 'first_feature',
project: defaultProject,
project: DEFAULT_PROJECT,
variants,
});
const { body: importedFeatureEnvironment } = await getFeatureEnvironment(
defaultProject,
defaultFeature,
defaultEnvironment,
);
expect(importedFeatureEnvironment).toMatchObject({
name: defaultFeature,
environment: defaultEnvironment,
environment: DEFAULT_ENV,
enabled: true,
strategies: [
{
@ -623,7 +611,7 @@ test('importing same JSON should work multiple times in a row', async () => {
});
test('reject import with unknown context fields', async () => {
await createProject(defaultProject, defaultEnvironment);
await createProject();
const contextField = {
name: 'ContextField1',
legalValues: [{ value: 'Value1', description: '' }],
@ -654,12 +642,14 @@ test('reject import with unknown context fields', async () => {
});
test('reject import with unsupported strategies', async () => {
await createProject(defaultProject, defaultEnvironment);
await createProject();
const importPayloadWithContextFields: ImportTogglesSchema = {
...defaultImportPayload,
data: {
...defaultImportPayload.data,
featureStrategies: [{ name: 'customStrategy' }],
featureStrategies: [
{ name: 'customStrategy', featureName: 'featureName' },
],
},
};
@ -675,7 +665,7 @@ test('reject import with unsupported strategies', async () => {
});
test('validate import data', async () => {
await createProject(defaultProject, defaultEnvironment);
await createProject();
const contextField: IContextFieldDto = {
name: 'validate_context_field',
legalValues: [{ value: 'Value1' }],
@ -686,8 +676,8 @@ test('validate import data', async () => {
legalValues: [{ value: 'new_value' }],
};
await createFeature(defaultFeature, defaultProject);
await archiveFeature(defaultFeature, defaultProject);
await createFeature(defaultFeature);
await archiveFeature(defaultFeature);
await createContextField(contextField);
const importPayloadWithContextFields: ImportTogglesSchema = {
@ -733,7 +723,7 @@ test('validate import data', async () => {
});
test('should create new context', async () => {
await createProject(defaultProject, defaultEnvironment);
await createProject();
const context = {
name: 'create-new-context',
legalValues: [{ value: 'Value1' }],
@ -753,10 +743,10 @@ test('should create new context', async () => {
});
test('should not import archived features tags', async () => {
await createProject(defaultProject, defaultEnvironment);
await createProject();
await importToggles(defaultImportPayload);
await archiveFeature(defaultFeature, defaultProject);
await archiveFeature(defaultFeature);
await importToggles({
...defaultImportPayload,

View File

@ -13,7 +13,14 @@ export interface IImportTogglesStore {
deleteTagsForFeatures(tags: string[]): Promise<void>;
strategiesExistForFeatures(
featureNames: string[],
environment: string,
): Promise<boolean>;
getDisplayPermissions(
names: string[],
): Promise<{ name: string; displayName: string }[]>;
getExistingFeatures(featureNames: string[]): Promise<string[]>;
}

View File

@ -4,7 +4,7 @@ import { Knex } from 'knex';
const T = {
featureStrategies: 'feature_strategies',
features: 'features',
feature_tag: 'feature_tag',
featureTag: 'feature_tag',
};
export class ImportTogglesStore implements IImportTogglesStore {
private db: Knex;
@ -35,6 +35,20 @@ export class ImportTogglesStore implements IImportTogglesStore {
.del();
}
async strategiesExistForFeatures(
featureNames: string[],
environment: string,
): Promise<boolean> {
const result = await this.db.raw(
'SELECT EXISTS (SELECT 1 FROM feature_strategies WHERE environment = ? and feature_name in (' +
featureNames.map(() => '?').join(',') +
')) AS present',
[environment, ...featureNames],
);
const { present } = result.rows[0];
return present;
}
async getArchivedFeatures(featureNames: string[]): Promise<string[]> {
const rows = await this.db(T.features)
.select('name')
@ -43,6 +57,11 @@ export class ImportTogglesStore implements IImportTogglesStore {
return rows.map((row) => row.name);
}
async getExistingFeatures(featureNames: string[]): Promise<string[]> {
const rows = await this.db(T.features).whereIn('name', featureNames);
return rows.map((row) => row.name);
}
async getFeaturesInOtherProjects(
featureNames: string[],
project: string,
@ -54,7 +73,7 @@ export class ImportTogglesStore implements IImportTogglesStore {
return rows.map((row) => ({ name: row.name, project: row.project }));
}
async deleteTagsForFeatures(tags: string[]): Promise<void> {
return this.db(T.feature_tag).whereIn('feature_name', tags).del();
async deleteTagsForFeatures(features: string[]): Promise<void> {
return this.db(T.featureTag).whereIn('feature_name', features).del();
}
}

View File

@ -8,3 +8,5 @@ export type PartialDeep<T> = T extends object
// Mark one or more properties as optional.
export type PartialSome<T, K extends keyof T> = Pick<Partial<T>, K> &
Omit<T, K>;
export type WithRequired<T, K extends keyof T> = T & { [P in K]-?: T[P] };

View File

@ -70,8 +70,9 @@ export async function setupAppWithCustomConfig(
export async function setupAppWithAuth(
stores: IUnleashStores,
customOptions?: any,
db?: Db,
): Promise<IUnleashTest> {
return createApp(stores, IAuthType.DEMO, undefined, customOptions);
return createApp(stores, IAuthType.DEMO, undefined, customOptions, db);
}
export async function setupAppWithCustomAuth(

View File

@ -27,7 +27,7 @@
/* Strict Type-Checking Options */
// "strict": true, /* Enable all strict type-checking options. */
// "noImplicitAny": true, /* Raise error on expressions and declarations with an implied 'any' type. */
// "strictNullChecks": true, /* Enable strict null checks. */
// "strictNullChecks": true, /* Enable strict null checks. */
// "strictFunctionTypes": true, /* Enable strict checking of function types. */
// "strictBindCallApply": true, /* Enable strict 'bind', 'call', and 'apply' methods on functions. */
// "strictPropertyInitialization": true, /* Enable strict checking of property initialization in classes. */