1
0
mirror of https://github.com/Unleash/unleash.git synced 2024-12-28 00:06:53 +01:00

Fix/switch project endpoint (#923)

This commit is contained in:
Fredrik Strand Oseberg 2021-08-25 13:38:00 +02:00 committed by GitHub
parent bb47c19d4d
commit 856c7a358b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 253 additions and 59 deletions

View File

@ -17,7 +17,10 @@ import {
} from '../types/events'; } from '../types/events';
import { GLOBAL_ENV } from '../types/environment'; import { GLOBAL_ENV } from '../types/environment';
import NotFoundError from '../error/notfound-error'; import NotFoundError from '../error/notfound-error';
import { FeatureConfigurationClient, IFeatureStrategiesStore } from '../types/stores/feature-strategies-store'; import {
FeatureConfigurationClient,
IFeatureStrategiesStore,
} from '../types/stores/feature-strategies-store';
import { IFeatureTypeStore } from '../types/stores/feature-type-store'; import { IFeatureTypeStore } from '../types/stores/feature-type-store';
import { IEventStore } from '../types/stores/event-store'; import { IEventStore } from '../types/stores/event-store';
import { IEnvironmentStore } from '../types/stores/environment-store'; import { IEnvironmentStore } from '../types/stores/environment-store';
@ -95,16 +98,15 @@ class FeatureToggleServiceV2 {
environment: string = GLOBAL_ENV, environment: string = GLOBAL_ENV,
): Promise<IStrategyConfig> { ): Promise<IStrategyConfig> {
try { try {
const newFeatureStrategy = await this.featureStrategiesStore.createStrategyConfig( const newFeatureStrategy =
{ await this.featureStrategiesStore.createStrategyConfig({
strategyName: strategyConfig.name, strategyName: strategyConfig.name,
constraints: strategyConfig.constraints, constraints: strategyConfig.constraints,
parameters: strategyConfig.parameters, parameters: strategyConfig.parameters,
projectName, projectName,
featureName, featureName,
environment, environment,
}, });
);
return { return {
id: newFeatureStrategy.id, id: newFeatureStrategy.id,
name: newFeatureStrategy.strategyName, name: newFeatureStrategy.strategyName,
@ -150,7 +152,8 @@ class FeatureToggleServiceV2 {
featureName, featureName,
); );
if (hasEnv) { if (hasEnv) {
const featureStrategies = await this.featureStrategiesStore.getStrategiesForFeature( const featureStrategies =
await this.featureStrategiesStore.getStrategiesForFeature(
projectName, projectName,
featureName, featureName,
environment, environment,
@ -176,7 +179,10 @@ class FeatureToggleServiceV2 {
featureName: string, featureName: string,
archived: boolean = false, archived: boolean = false,
): Promise<FeatureToggleWithEnvironment> { ): Promise<FeatureToggleWithEnvironment> {
return this.featureStrategiesStore.getFeatureToggleAdmin(featureName, archived); return this.featureStrategiesStore.getFeatureToggleAdmin(
featureName,
archived,
);
} }
async getClientFeatures( async getClientFeatures(
@ -203,7 +209,10 @@ class FeatureToggleServiceV2 {
async getFeatureToggle( async getFeatureToggle(
featureName: string, featureName: string,
): Promise<FeatureToggleWithEnvironment> { ): Promise<FeatureToggleWithEnvironment> {
return this.featureStrategiesStore.getFeatureToggleAdmin(featureName, false); return this.featureStrategiesStore.getFeatureToggleAdmin(
featureName,
false,
);
} }
async createFeatureToggle( async createFeatureToggle(
@ -215,7 +224,9 @@ class FeatureToggleServiceV2 {
await this.validateName(value.name); await this.validateName(value.name);
const exists = await this.projectStore.hasProject(projectId); const exists = await this.projectStore.hasProject(projectId);
if (exists) { if (exists) {
const featureData = await featureMetadataSchema.validateAsync(value); const featureData = await featureMetadataSchema.validateAsync(
value,
);
const createdToggle = await this.featureToggleStore.createFeature( const createdToggle = await this.featureToggleStore.createFeature(
projectId, projectId,
featureData, featureData,
@ -250,7 +261,8 @@ class FeatureToggleServiceV2 {
projectId, projectId,
updatedFeature, updatedFeature,
); );
const tags = (await this.featureTagStore.getAllTagsForFeature( const tags =
(await this.featureTagStore.getAllTagsForFeature(
updatedFeature.name, updatedFeature.name,
)) || []; )) || [];
await this.eventStore.store({ await this.eventStore.store({
@ -296,11 +308,13 @@ class FeatureToggleServiceV2 {
environment: string, environment: string,
featureName: string, featureName: string,
): Promise<IFeatureEnvironmentInfo> { ): Promise<IFeatureEnvironmentInfo> {
const envMetadata = await this.featureEnvironmentStore.getEnvironmentMetaData( const envMetadata =
await this.featureEnvironmentStore.getEnvironmentMetaData(
environment, environment,
featureName, featureName,
); );
const strategies = await this.featureStrategiesStore.getStrategiesForFeature( const strategies =
await this.featureStrategiesStore.getStrategiesForFeature(
project, project,
featureName, featureName,
environment, environment,
@ -321,7 +335,10 @@ class FeatureToggleServiceV2 {
projectId, projectId,
environment, environment,
); );
await this.projectStore.deleteEnvironmentForProject(projectId, environment); await this.projectStore.deleteEnvironmentForProject(
projectId,
environment,
);
} }
/** Validations */ /** Validations */
@ -358,8 +375,9 @@ class FeatureToggleServiceV2 {
); );
feature.stale = isStale; feature.stale = isStale;
await this.featureToggleStore.updateFeature(feature.project, feature); await this.featureToggleStore.updateFeature(feature.project, feature);
const tags = (await this.featureTagStore.getAllTagsForFeature(featureName)) const tags =
|| []; (await this.featureTagStore.getAllTagsForFeature(featureName)) ||
[];
await this.eventStore.store({ await this.eventStore.store({
type: isStale ? FEATURE_STALE_ON : FEATURE_STALE_OFF, type: isStale ? FEATURE_STALE_ON : FEATURE_STALE_OFF,
@ -373,7 +391,8 @@ class FeatureToggleServiceV2 {
async archiveToggle(name: string, userName: string): Promise<void> { async archiveToggle(name: string, userName: string): Promise<void> {
await this.featureToggleStore.hasFeature(name); await this.featureToggleStore.hasFeature(name);
await this.featureToggleStore.archiveFeature(name); await this.featureToggleStore.archiveFeature(name);
const tags = (await this.featureTagStore.getAllTagsForFeature(name)) || []; const tags =
(await this.featureTagStore.getAllTagsForFeature(name)) || [];
await this.eventStore.store({ await this.eventStore.store({
type: FEATURE_ARCHIVED, type: FEATURE_ARCHIVED,
createdBy: userName, createdBy: userName,
@ -388,12 +407,14 @@ class FeatureToggleServiceV2 {
enabled: boolean, enabled: boolean,
userName: string, userName: string,
): Promise<FeatureToggle> { ): Promise<FeatureToggle> {
const hasEnvironment = await this.featureEnvironmentStore.featureHasEnvironment( const hasEnvironment =
await this.featureEnvironmentStore.featureHasEnvironment(
environment, environment,
featureName, featureName,
); );
if (hasEnvironment) { if (hasEnvironment) {
const newEnabled = await this.featureEnvironmentStore.toggleEnvironmentEnabledStatus( const newEnabled =
await this.featureEnvironmentStore.toggleEnvironmentEnabledStatus(
environment, environment,
featureName, featureName,
enabled, enabled,
@ -401,7 +422,8 @@ class FeatureToggleServiceV2 {
const feature = await this.featureToggleStore.getFeatureMetadata( const feature = await this.featureToggleStore.getFeatureMetadata(
featureName, featureName,
); );
const tags = (await this.featureTagStore.getAllTagsForFeature( const tags =
(await this.featureTagStore.getAllTagsForFeature(
featureName, featureName,
)) || []; )) || [];
await this.eventStore.store({ await this.eventStore.store({
@ -424,7 +446,8 @@ class FeatureToggleServiceV2 {
userName: string, userName: string,
): Promise<FeatureToggle> { ): Promise<FeatureToggle> {
await this.featureToggleStore.hasFeature(featureName); await this.featureToggleStore.hasFeature(featureName);
const isEnabled = await this.featureEnvironmentStore.isEnvironmentEnabled( const isEnabled =
await this.featureEnvironmentStore.isEnvironmentEnabled(
featureName, featureName,
environment, environment,
); );
@ -443,17 +466,19 @@ class FeatureToggleServiceV2 {
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
value: any, value: any,
userName: string, userName: string,
event?: string,
): Promise<any> { ): Promise<any> {
const feature = await this.featureToggleStore.getFeatureMetadata( const feature = await this.featureToggleStore.getFeatureMetadata(
featureName, featureName,
); );
feature[field] = value; feature[field] = value;
await this.featureToggleStore.updateFeature(feature.project, feature); await this.featureToggleStore.updateFeature(feature.project, feature);
const tags = (await this.featureTagStore.getAllTagsForFeature(featureName)) const tags =
|| []; (await this.featureTagStore.getAllTagsForFeature(featureName)) ||
[];
await this.eventStore.store({ await this.eventStore.store({
type: FEATURE_UPDATED, type: event || FEATURE_UPDATED,
createdBy: userName, createdBy: userName,
data: feature, data: feature,
tags, tags,
@ -478,7 +503,9 @@ class FeatureToggleServiceV2 {
async reviveToggle(featureName: string, userName: string): Promise<void> { async reviveToggle(featureName: string, userName: string): Promise<void> {
const data = await this.featureToggleStore.reviveFeature(featureName); const data = await this.featureToggleStore.reviveFeature(featureName);
const tags = await this.featureTagStore.getAllTagsForFeature(featureName); const tags = await this.featureTagStore.getAllTagsForFeature(
featureName,
);
await this.eventStore.store({ await this.eventStore.store({
type: FEATURE_REVIVED, type: FEATURE_REVIVED,
createdBy: userName, createdBy: userName,
@ -487,12 +514,16 @@ class FeatureToggleServiceV2 {
}); });
} }
async getMetadataForAllFeatures(archived: boolean): Promise<FeatureToggle[]> { async getMetadataForAllFeatures(
archived: boolean,
): Promise<FeatureToggle[]> {
return this.featureToggleStore.getFeatures(archived); return this.featureToggleStore.getFeatures(archived);
} }
async getProjectId(name: string): Promise<string> { async getProjectId(name: string): Promise<string> {
const { project } = await this.featureToggleStore.getFeatureMetadata(name); const { project } = await this.featureToggleStore.getFeatureMetadata(
name,
);
return project; return project;
} }
} }

View File

@ -6,6 +6,7 @@ import { nameType } from '../routes/util';
import schema from './project-schema'; import schema from './project-schema';
import NotFoundError from '../error/notfound-error'; import NotFoundError from '../error/notfound-error';
import { import {
FEATURE_PROJECT_CHANGE,
PROJECT_CREATED, PROJECT_CREATED,
PROJECT_DELETED, PROJECT_DELETED,
PROJECT_UPDATED, PROJECT_UPDATED,
@ -27,6 +28,8 @@ import { IProjectStore } from '../types/stores/project-store';
import { IRole } from '../types/stores/access-store'; import { IRole } from '../types/stores/access-store';
import { IEventStore } from '../types/stores/event-store'; import { IEventStore } from '../types/stores/event-store';
import FeatureToggleServiceV2 from './feature-toggle-service-v2'; import FeatureToggleServiceV2 from './feature-toggle-service-v2';
import { CREATE_FEATURE, UPDATE_FEATURE } from '../types/permissions';
import NoAccessError from '../error/no-access-error';
const getCreatedBy = (user: User) => user.email || user.username; const getCreatedBy = (user: User) => user.email || user.username;
@ -140,6 +143,45 @@ export default class ProjectService {
}); });
} }
async changeProject(
newProjectId: string,
featureName: string,
user: User,
currentProjectId: string,
): Promise<any> {
const feature = await this.featureToggleStore.get(featureName);
if (feature.project !== currentProjectId) {
throw new NoAccessError(UPDATE_FEATURE);
}
const project = await this.getProject(newProjectId);
if (!project) {
throw new NotFoundError(`Project ${newProjectId} not found`);
}
const authorized = await this.accessService.hasPermission(
user,
CREATE_FEATURE,
newProjectId,
);
if (!authorized) {
throw new NoAccessError(CREATE_FEATURE);
}
const updatedFeature = await this.featureToggleService.updateField(
featureName,
'project',
newProjectId,
user.username,
FEATURE_PROJECT_CHANGE,
);
return updatedFeature;
}
async deleteProject(id: string, user: User): Promise<void> { async deleteProject(id: string, user: User): Promise<void> {
if (id === DEFAULT_PROJECT) { if (id === DEFAULT_PROJECT) {
throw new InvalidOperationError( throw new InvalidOperationError(

View File

@ -2,6 +2,7 @@ export const APPLICATION_CREATED = 'application-created';
export const FEATURE_CREATED = 'feature-created'; export const FEATURE_CREATED = 'feature-created';
export const FEATURE_DELETED = 'feature-deleted'; export const FEATURE_DELETED = 'feature-deleted';
export const FEATURE_UPDATED = 'feature-updated'; export const FEATURE_UPDATED = 'feature-updated';
export const FEATURE_PROJECT_CHANGE = 'feature-project-change';
export const FEATURE_ARCHIVED = 'feature-archived'; export const FEATURE_ARCHIVED = 'feature-archived';
export const FEATURE_REVIVED = 'feature-revived'; export const FEATURE_REVIVED = 'feature-revived';
export const FEATURE_IMPORT = 'feature-import'; export const FEATURE_IMPORT = 'feature-import';

View File

@ -3,7 +3,11 @@ import getLogger from '../../fixtures/no-logger';
import FeatureToggleServiceV2 from '../../../lib/services/feature-toggle-service-v2'; import FeatureToggleServiceV2 from '../../../lib/services/feature-toggle-service-v2';
import ProjectService from '../../../lib/services/project-service'; import ProjectService from '../../../lib/services/project-service';
import { AccessService } from '../../../lib/services/access-service'; import { AccessService } from '../../../lib/services/access-service';
import { UPDATE_PROJECT } from '../../../lib/types/permissions'; import {
CREATE_FEATURE,
UPDATE_FEATURE,
UPDATE_PROJECT,
} from '../../../lib/types/permissions';
import NotFoundError from '../../../lib/error/notfound-error'; import NotFoundError from '../../../lib/error/notfound-error';
import { createTestConfig } from '../../config/test-config'; import { createTestConfig } from '../../config/test-config';
import { RoleName } from '../../../lib/types/model'; import { RoleName } from '../../../lib/types/model';
@ -390,3 +394,119 @@ test('should not remove user from the project', async () => {
new Error('A project must have at least one owner'), new Error('A project must have at least one owner'),
); );
}); });
test('should not change project if feature toggle project does not match current project id', async () => {
const project = {
id: 'test-change-project',
name: 'New project',
description: 'Blah',
};
const toggle = { name: 'test-toggle' };
await projectService.createProject(project, user);
await featureToggleService.createFeatureToggle(project.id, toggle, user);
try {
await projectService.changeProject(
'newProject',
toggle.name,
user,
'wrong-project-id',
);
} catch (err) {
expect(err.message).toBe(
`You need permission=${UPDATE_FEATURE} to perform this action`,
);
}
});
test('should return 404 if no project is found with the project id', async () => {
const project = {
id: 'test-change-project-2',
name: 'New project',
description: 'Blah',
};
const toggle = { name: 'test-toggle-2' };
await projectService.createProject(project, user);
await featureToggleService.createFeatureToggle(project.id, toggle, user);
try {
await projectService.changeProject(
'newProject',
toggle.name,
user,
project.id,
);
} catch (err) {
expect(err.message).toBe(`No project found`);
}
});
test('should fail if user is not authorized', async () => {
const project = {
id: 'test-change-project-3',
name: 'New project',
description: 'Blah',
};
const projectDestination = {
id: 'test-change-project-dest',
name: 'New project 2',
description: 'Blah',
};
const toggle = { name: 'test-toggle-3' };
const projectAdmin1 = await stores.userStore.insert({
name: 'test-change-project-creator',
email: 'admin-change-project@getunleash.io',
});
await projectService.createProject(project, user);
await projectService.createProject(projectDestination, projectAdmin1);
await featureToggleService.createFeatureToggle(project.id, toggle, user);
try {
await projectService.changeProject(
projectDestination.id,
toggle.name,
user,
project.id,
);
} catch (err) {
expect(err.message).toBe(
`You need permission=${CREATE_FEATURE} to perform this action`,
);
}
});
test('should change project when checks pass', async () => {
const project = {
id: 'test-change-project-4',
name: 'New project',
description: 'Blah',
};
const projectDestination = {
id: 'test-change-project-dest-2',
name: 'New project 2',
description: 'Blah',
};
const toggle = { name: 'test-toggle-4' };
await projectService.createProject(project, user);
await projectService.createProject(projectDestination, user);
await featureToggleService.createFeatureToggle(project.id, toggle, user);
const updatedFeature = await projectService.changeProject(
projectDestination.id,
toggle.name,
user,
project.id,
);
expect(updatedFeature.project).toBe(projectDestination.id);
});