1
0
mirror of https://github.com/Unleash/unleash.git synced 2025-07-26 13:48:33 +02:00

fix/projectId cannot change for strategy configs (#1084)

This commit is contained in:
Ivar Conradi Østhus 2021-11-04 21:24:55 +01:00 committed by GitHub
parent ec60f4485c
commit 053956b45e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
21 changed files with 312 additions and 176 deletions

View File

@ -252,40 +252,6 @@ test('should lookup projectId from data', async () => {
); );
}); });
test('Need access to UPDATE_FEATURE on the project you change to', async () => {
const oldProjectId = 'some-project-34';
const newProjectId = 'some-project-35';
const featureName = 'some-feature-toggle';
const accessService = {
hasPermission: jest.fn().mockReturnValue(true),
};
featureToggleStore.getProjectId = jest.fn().mockReturnValue(oldProjectId);
const func = rbacMiddleware(config, { featureToggleStore }, accessService);
const cb = jest.fn();
const req: any = {
user: new User({ username: 'user', id: 1 }),
params: { featureName },
body: { featureName, project: newProjectId },
};
func(req, undefined, cb);
await req.checkRbac(perms.UPDATE_FEATURE);
expect(accessService.hasPermission).toHaveBeenCalledTimes(2);
expect(accessService.hasPermission).toHaveBeenNthCalledWith(
1,
req.user,
perms.UPDATE_FEATURE,
oldProjectId,
);
expect(accessService.hasPermission).toHaveBeenNthCalledWith(
2,
req.user,
perms.UPDATE_FEATURE,
newProjectId,
);
});
test('Does not double check permission if not changing project when updating toggle', async () => { test('Does not double check permission if not changing project when updating toggle', async () => {
const oldProjectId = 'some-project-34'; const oldProjectId = 'some-project-34';
const featureName = 'some-feature-toggle'; const featureName = 'some-feature-toggle';

View File

@ -1,9 +1,9 @@
/* eslint-disable @typescript-eslint/explicit-module-boundary-types */ /* eslint-disable @typescript-eslint/explicit-module-boundary-types */
import { import {
CREATE_FEATURE, CREATE_FEATURE,
UPDATE_FEATURE,
DELETE_FEATURE, DELETE_FEATURE,
ADMIN, ADMIN,
UPDATE_FEATURE,
} from '../types/permissions'; } from '../types/permissions';
import { IUnleashConfig } from '../types/option'; import { IUnleashConfig } from '../types/option';
import { IUnleashStores } from '../types/stores'; import { IUnleashStores } from '../types/stores';
@ -47,33 +47,10 @@ const rbacMiddleware = (
let { projectId } = params; let { projectId } = params;
// Temporary workaround to figure out projectId for feature toggle updates. // Temporary workaround to figure out projectId for feature toggle updates.
if (permission === DELETE_FEATURE) { // will be removed in Unleash v5.0
if ([DELETE_FEATURE, UPDATE_FEATURE].includes(permission)) {
const { featureName } = params; const { featureName } = params;
projectId = await featureToggleStore.getProjectId(featureName); projectId = await featureToggleStore.getProjectId(featureName);
} else if (permission === UPDATE_FEATURE) {
// if projectId of feature is different from project in body
// need to check that we have UPDATE_FEATURE access on both old and new project
// TODO: Look at this to make it smoother once we get around to looking at project
// Changing project of a toggle should most likely be a separate endpoint
const { featureName } = params;
projectId = await featureToggleStore.getProjectId(featureName);
const newProjectId = req.body
? req.body.project || projectId
: projectId;
if (newProjectId !== projectId) {
return (
accessService.hasPermission(
user,
permission,
projectId,
) &&
accessService.hasPermission(
user,
permission,
newProjectId,
)
);
}
} else if (permission === CREATE_FEATURE) { } else if (permission === CREATE_FEATURE) {
projectId = req.body.project || 'default'; projectId = req.body.project || 'default';
} }
@ -84,5 +61,4 @@ const rbacMiddleware = (
}; };
}; };
module.exports = rbacMiddleware;
export default rbacMiddleware; export default rbacMiddleware;

View File

@ -7,7 +7,7 @@ import Controller from '../controller';
import { extractUsername } from '../../util/extract-user'; import { extractUsername } from '../../util/extract-user';
import { DELETE_FEATURE, UPDATE_FEATURE } from '../../types/permissions'; import { DELETE_FEATURE, UPDATE_FEATURE } from '../../types/permissions';
import FeatureToggleServiceV2 from '../../services/feature-toggle-service-v2'; import FeatureToggleServiceV2 from '../../services/feature-toggle-service';
import { IAuthRequest } from '../unleash-types'; import { IAuthRequest } from '../unleash-types';
export default class ArchiveController extends Controller { export default class ArchiveController extends Controller {

View File

@ -11,7 +11,7 @@ import {
} from '../../types/permissions'; } from '../../types/permissions';
import { IUnleashConfig } from '../../types/option'; import { IUnleashConfig } from '../../types/option';
import { IUnleashServices } from '../../types/services'; import { IUnleashServices } from '../../types/services';
import FeatureToggleServiceV2 from '../../services/feature-toggle-service-v2'; import FeatureToggleServiceV2 from '../../services/feature-toggle-service';
import { featureSchema, querySchema } from '../../schema/feature-schema'; import { featureSchema, querySchema } from '../../schema/feature-schema';
import { IFeatureToggleQuery } from '../../types/model'; import { IFeatureToggleQuery } from '../../types/model';
import FeatureTagService from '../../services/feature-tag-service'; import FeatureTagService from '../../services/feature-tag-service';
@ -150,8 +150,11 @@ class FeatureController extends Controller {
toggle.strategies.map(async (s) => toggle.strategies.map(async (s) =>
this.service.createStrategy( this.service.createStrategy(
s, s,
createdFeature.project, {
createdFeature.name, projectId: createdFeature.project,
featureName: createdFeature.name,
environment: DEFAULT_ENV,
},
userName, userName,
), ),
), ),
@ -190,8 +193,7 @@ class FeatureController extends Controller {
updatedFeature.strategies.map(async (s) => updatedFeature.strategies.map(async (s) =>
this.service.createStrategy( this.service.createStrategy(
s, s,
projectId, { projectId, featureName, environment: DEFAULT_ENV },
featureName,
userName, userName,
), ),
), ),

View File

@ -3,7 +3,7 @@ import { applyPatch, Operation } from 'fast-json-patch';
import Controller from '../../controller'; import Controller from '../../controller';
import { IUnleashConfig } from '../../../types/option'; import { IUnleashConfig } from '../../../types/option';
import { IUnleashServices } from '../../../types/services'; import { IUnleashServices } from '../../../types/services';
import FeatureToggleServiceV2 from '../../../services/feature-toggle-service-v2'; import FeatureToggleServiceV2 from '../../../services/feature-toggle-service';
import { Logger } from '../../../logger'; import { Logger } from '../../../logger';
import { CREATE_FEATURE, UPDATE_FEATURE } from '../../../types/permissions'; import { CREATE_FEATURE, UPDATE_FEATURE } from '../../../types/permissions';
import { import {
@ -64,18 +64,20 @@ export default class ProjectFeaturesController extends Controller {
this.featureService = featureToggleServiceV2; this.featureService = featureToggleServiceV2;
this.logger = config.getLogger('/admin-api/project/features.ts'); this.logger = config.getLogger('/admin-api/project/features.ts');
// Environments
this.get(`${PATH_ENV}`, this.getEnvironment); this.get(`${PATH_ENV}`, this.getEnvironment);
this.post(`${PATH_ENV}/on`, this.toggleEnvironmentOn, UPDATE_FEATURE); this.post(`${PATH_ENV}/on`, this.toggleEnvironmentOn, UPDATE_FEATURE);
this.post(`${PATH_ENV}/off`, this.toggleEnvironmentOff, UPDATE_FEATURE); this.post(`${PATH_ENV}/off`, this.toggleEnvironmentOff, UPDATE_FEATURE);
// activation strategies
this.get(`${PATH_STRATEGIES}`, this.getStrategies); this.get(`${PATH_STRATEGIES}`, this.getStrategies);
this.post(`${PATH_STRATEGIES}`, this.addStrategy, UPDATE_FEATURE); this.post(`${PATH_STRATEGIES}`, this.addStrategy, UPDATE_FEATURE);
this.get(`${PATH_STRATEGY}`, this.getStrategy); this.get(`${PATH_STRATEGY}`, this.getStrategy);
this.put(`${PATH_STRATEGY}`, this.updateStrategy, UPDATE_FEATURE); this.put(`${PATH_STRATEGY}`, this.updateStrategy, UPDATE_FEATURE);
this.patch(`${PATH_STRATEGY}`, this.patchStrategy, UPDATE_FEATURE); this.patch(`${PATH_STRATEGY}`, this.patchStrategy, UPDATE_FEATURE);
this.delete(`${PATH_STRATEGY}`, this.deleteStrategy, UPDATE_FEATURE); this.delete(`${PATH_STRATEGY}`, this.deleteStrategy, UPDATE_FEATURE);
// feature toggles
this.get(PATH, this.getFeatures); this.get(PATH, this.getFeatures);
this.post(PATH, this.createFeature, CREATE_FEATURE); this.post(PATH, this.createFeature, CREATE_FEATURE);
@ -244,10 +246,8 @@ export default class ProjectFeaturesController extends Controller {
const userName = extractUsername(req); const userName = extractUsername(req);
const featureStrategy = await this.featureService.createStrategy( const featureStrategy = await this.featureService.createStrategy(
req.body, req.body,
projectId, { environment, projectId, featureName },
featureName,
userName, userName,
environment,
); );
res.status(200).json(featureStrategy); res.status(200).json(featureStrategy);
} }
@ -270,14 +270,13 @@ export default class ProjectFeaturesController extends Controller {
req: IAuthRequest<StrategyIdParams, any, StrategyUpdateBody, any>, req: IAuthRequest<StrategyIdParams, any, StrategyUpdateBody, any>,
res: Response, res: Response,
): Promise<void> { ): Promise<void> {
const { strategyId, environment, projectId } = req.params; const { strategyId, environment, projectId, featureName } = req.params;
const userName = extractUsername(req); const userName = extractUsername(req);
const updatedStrategy = await this.featureService.updateStrategy( const updatedStrategy = await this.featureService.updateStrategy(
strategyId, strategyId,
environment,
projectId,
userName,
req.body, req.body,
{ environment, projectId, featureName },
userName,
); );
res.status(200).json(updatedStrategy); res.status(200).json(updatedStrategy);
} }
@ -286,17 +285,16 @@ export default class ProjectFeaturesController extends Controller {
req: IAuthRequest<StrategyIdParams, any, Operation[], any>, req: IAuthRequest<StrategyIdParams, any, Operation[], any>,
res: Response, res: Response,
): Promise<void> { ): Promise<void> {
const { strategyId, projectId, environment } = req.params; const { strategyId, projectId, environment, featureName } = req.params;
const userName = extractUsername(req); const userName = extractUsername(req);
const patch = req.body; const patch = req.body;
const strategy = await this.featureService.getStrategy(strategyId); const strategy = await this.featureService.getStrategy(strategyId);
const { newDocument } = applyPatch(strategy, patch); const { newDocument } = applyPatch(strategy, patch);
const updatedStrategy = await this.featureService.updateStrategy( const updatedStrategy = await this.featureService.updateStrategy(
strategyId, strategyId,
environment,
projectId,
userName,
newDocument, newDocument,
{ environment, projectId, featureName },
userName,
); );
res.status(200).json(updatedStrategy); res.status(200).json(updatedStrategy);
} }
@ -323,10 +321,8 @@ export default class ProjectFeaturesController extends Controller {
this.logger.info(strategyId); this.logger.info(strategyId);
const strategy = await this.featureService.deleteStrategy( const strategy = await this.featureService.deleteStrategy(
strategyId, strategyId,
featureName, { environment, projectId, featureName },
userName, userName,
projectId,
environment,
); );
res.status(200).json(strategy); res.status(200).json(strategy);
} }
@ -340,7 +336,7 @@ export default class ProjectFeaturesController extends Controller {
>, >,
res: Response, res: Response,
): Promise<void> { ): Promise<void> {
const { strategyId, environment, projectId } = req.params; const { strategyId, environment, projectId, featureName } = req.params;
const userName = extractUsername(req); const userName = extractUsername(req);
const { name, value } = req.body; const { name, value } = req.body;
@ -349,9 +345,8 @@ export default class ProjectFeaturesController extends Controller {
strategyId, strategyId,
name, name,
value, value,
{ environment, projectId, featureName },
userName, userName,
projectId,
environment,
); );
res.status(200).json(updatedStrategy); res.status(200).json(updatedStrategy);
} }

View File

@ -3,7 +3,7 @@ import { Response } from 'express';
import Controller from '../controller'; import Controller from '../controller';
import { IUnleashServices } from '../../types/services'; import { IUnleashServices } from '../../types/services';
import { IUnleashConfig } from '../../types/option'; import { IUnleashConfig } from '../../types/option';
import FeatureToggleServiceV2 from '../../services/feature-toggle-service-v2'; import FeatureToggleServiceV2 from '../../services/feature-toggle-service';
import { Logger } from '../../logger'; import { Logger } from '../../logger';
import { querySchema } from '../../schema/feature-schema'; import { querySchema } from '../../schema/feature-schema';
import { IFeatureToggleQuery } from '../../types/model'; import { IFeatureToggleQuery } from '../../types/model';

View File

@ -48,6 +48,15 @@ import { IFeatureToggleClientStore } from '../types/stores/feature-toggle-client
import { DEFAULT_ENV } from '../util/constants'; import { DEFAULT_ENV } from '../util/constants';
import { applyPatch, deepClone, Operation } from 'fast-json-patch'; import { applyPatch, deepClone, Operation } from 'fast-json-patch';
interface IFeatureContext {
featureName: string;
projectId: string;
}
interface IFeatureStrategyContext extends IFeatureContext {
environment: string;
}
class FeatureToggleServiceV2 { class FeatureToggleServiceV2 {
private logger: Logger; private logger: Logger;
@ -96,6 +105,35 @@ class FeatureToggleServiceV2 {
this.featureEnvironmentStore = featureEnvironmentStore; this.featureEnvironmentStore = featureEnvironmentStore;
} }
async validateFeatureContext({
featureName,
projectId,
}: IFeatureContext): Promise<void> {
const id = await this.featureToggleStore.getProjectId(featureName);
if (id !== projectId) {
throw new InvalidOperationError(
'You can not change the projectId for an activation strategy.',
);
}
}
validateFeatureStrategyContext(
strategy: IFeatureStrategy,
{ featureName, projectId }: IFeatureStrategyContext,
): void {
if (strategy.projectId !== projectId) {
throw new InvalidOperationError(
'You can not change the projectId for an activation strategy.',
);
}
if (strategy.featureName !== featureName) {
throw new InvalidOperationError(
'You can not change the featureName for an activation strategy.',
);
}
}
async patchFeature( async patchFeature(
projectId: string, projectId: string,
featureName: string, featureName: string,
@ -126,11 +164,11 @@ class FeatureToggleServiceV2 {
async createStrategy( async createStrategy(
strategyConfig: Omit<IStrategyConfig, 'id'>, strategyConfig: Omit<IStrategyConfig, 'id'>,
projectId: string, context: IFeatureStrategyContext,
featureName: string,
userName: string, userName: string,
environment: string = DEFAULT_ENV,
): Promise<IStrategyConfig> { ): Promise<IStrategyConfig> {
const { featureName, projectId, environment } = context;
await this.validateFeatureContext(context);
try { try {
const newFeatureStrategy = const newFeatureStrategy =
await this.featureStrategiesStore.createStrategyFeatureEnv({ await this.featureStrategiesStore.createStrategyFeatureEnv({
@ -176,17 +214,20 @@ class FeatureToggleServiceV2 {
* } * }
* @param id * @param id
* @param updates * @param updates
* @param context - Which context does this strategy live in (projectId, featureName, environment)
* @param userName - Human readable id of the user performing the update
*/ */
// TODO: verify projectId is not changed from URL!
async updateStrategy( async updateStrategy(
id: string, id: string,
environment: string,
project: string,
userName: string,
updates: Partial<IFeatureStrategy>, updates: Partial<IFeatureStrategy>,
context: IFeatureStrategyContext,
userName: string,
): Promise<IStrategyConfig> { ): Promise<IStrategyConfig> {
const { projectId, environment } = context;
const existingStrategy = await this.featureStrategiesStore.get(id); const existingStrategy = await this.featureStrategiesStore.get(id);
this.validateFeatureStrategyContext(existingStrategy, context);
if (existingStrategy.id === id) { if (existingStrategy.id === id) {
const strategy = await this.featureStrategiesStore.updateStrategy( const strategy = await this.featureStrategiesStore.updateStrategy(
id, id,
@ -201,7 +242,7 @@ class FeatureToggleServiceV2 {
}; };
await this.eventStore.store({ await this.eventStore.store({
type: FEATURE_STRATEGY_UPDATE, type: FEATURE_STRATEGY_UPDATE,
project, project: projectId,
environment, environment,
createdBy: userName, createdBy: userName,
data, data,
@ -211,16 +252,18 @@ class FeatureToggleServiceV2 {
throw new NotFoundError(`Could not find strategy with id ${id}`); throw new NotFoundError(`Could not find strategy with id ${id}`);
} }
// TODO: verify projectId is not changed from URL!
async updateStrategyParameter( async updateStrategyParameter(
id: string, id: string,
name: string, name: string,
value: string | number, value: string | number,
context: IFeatureStrategyContext,
userName: string, userName: string,
project: string,
environment: string,
): Promise<IStrategyConfig> { ): Promise<IStrategyConfig> {
const { projectId, environment } = context;
const existingStrategy = await this.featureStrategiesStore.get(id); const existingStrategy = await this.featureStrategiesStore.get(id);
this.validateFeatureStrategyContext(existingStrategy, context);
if (existingStrategy.id === id) { if (existingStrategy.id === id) {
existingStrategy.parameters[name] = value; existingStrategy.parameters[name] = value;
const strategy = await this.featureStrategiesStore.updateStrategy( const strategy = await this.featureStrategiesStore.updateStrategy(
@ -235,7 +278,7 @@ class FeatureToggleServiceV2 {
}; };
await this.eventStore.store({ await this.eventStore.store({
type: FEATURE_STRATEGY_UPDATE, type: FEATURE_STRATEGY_UPDATE,
project, project: projectId,
environment, environment,
createdBy: userName, createdBy: userName,
data, data,
@ -251,22 +294,22 @@ class FeatureToggleServiceV2 {
* *
* } * }
* @param id - strategy id * @param id - strategy id
* @param featureName - Name of the feature the strategy belongs to * @param context - Which context does this strategy live in (projectId, featureName, environment)
* @param userName - Who's doing the change
* @param project - Which project does this feature toggle belong to
* @param environment - Which environment does this strategy belong to * @param environment - Which environment does this strategy belong to
*/ */
async deleteStrategy( async deleteStrategy(
id: string, id: string,
featureName: string, context: IFeatureStrategyContext,
userName: string, userName: string,
project: string = 'default',
environment: string = DEFAULT_ENV,
): Promise<void> { ): Promise<void> {
const existingStrategy = await this.featureStrategiesStore.get(id);
const { featureName, projectId, environment } = context;
this.validateFeatureStrategyContext(existingStrategy, context);
await this.featureStrategiesStore.delete(id); await this.featureStrategiesStore.delete(id);
await this.eventStore.store({ await this.eventStore.store({
type: FEATURE_STRATEGY_REMOVE, type: FEATURE_STRATEGY_REMOVE,
project, project: projectId,
environment, environment,
createdBy: userName, createdBy: userName,
data: { data: {
@ -274,6 +317,7 @@ class FeatureToggleServiceV2 {
name: featureName, name: featureName,
}, },
}); });
// If there are no strategies left for environment disable it // If there are no strategies left for environment disable it
await this.featureEnvironmentStore.disableEnvironmentIfNoStrategies( await this.featureEnvironmentStore.disableEnvironmentIfNoStrategies(
featureName, featureName,
@ -444,10 +488,12 @@ class FeatureToggleServiceV2 {
createStrategies.push( createStrategies.push(
this.createStrategy( this.createStrategy(
s, s,
projectId, {
newFeatureName, projectId,
featureName: newFeatureName,
environment: e.name,
},
userName, userName,
e.name,
), ),
); );
}), }),

View File

@ -23,7 +23,7 @@ import ResetTokenService from './reset-token-service';
import SettingService from './setting-service'; import SettingService from './setting-service';
import SessionService from './session-service'; import SessionService from './session-service';
import UserFeedbackService from './user-feedback-service'; import UserFeedbackService from './user-feedback-service';
import FeatureToggleServiceV2 from './feature-toggle-service-v2'; import FeatureToggleService from './feature-toggle-service';
import EnvironmentService from './environment-service'; import EnvironmentService from './environment-service';
import FeatureTagService from './feature-tag-service'; import FeatureTagService from './feature-tag-service';
import ProjectHealthService from './project-health-service'; import ProjectHealthService from './project-health-service';
@ -58,7 +58,7 @@ export const createServices = (
const versionService = new VersionService(stores, config); const versionService = new VersionService(stores, config);
const healthService = new HealthService(stores, config); const healthService = new HealthService(stores, config);
const userFeedbackService = new UserFeedbackService(stores, config); const userFeedbackService = new UserFeedbackService(stores, config);
const featureToggleServiceV2 = new FeatureToggleServiceV2(stores, config); const featureToggleServiceV2 = new FeatureToggleService(stores, config);
const environmentService = new EnvironmentService(stores, config); const environmentService = new EnvironmentService(stores, config);
const featureTagService = new FeatureTagService(stores, config); const featureTagService = new FeatureTagService(stores, config);
const projectHealthService = new ProjectHealthService( const projectHealthService = new ProjectHealthService(
@ -76,6 +76,7 @@ export const createServices = (
return { return {
accessService, accessService,
addonService, addonService,
featureToggleService: featureToggleServiceV2,
featureToggleServiceV2, featureToggleServiceV2,
featureTypeService, featureTypeService,
healthService, healthService,

View File

@ -11,7 +11,7 @@ import {
import { IFeatureToggleStore } from '../types/stores/feature-toggle-store'; import { IFeatureToggleStore } from '../types/stores/feature-toggle-store';
import { IFeatureTypeStore } from '../types/stores/feature-type-store'; import { IFeatureTypeStore } from '../types/stores/feature-type-store';
import { IProjectStore } from '../types/stores/project-store'; import { IProjectStore } from '../types/stores/project-store';
import FeatureToggleServiceV2 from './feature-toggle-service-v2'; import FeatureToggleServiceV2 from './feature-toggle-service';
import { hoursToMilliseconds } from 'date-fns'; import { hoursToMilliseconds } from 'date-fns';
import Timer = NodeJS.Timer; import Timer = NodeJS.Timer;

View File

@ -27,7 +27,7 @@ import { IFeatureEnvironmentStore } from '../types/stores/feature-environment-st
import { IProjectQuery, IProjectStore } from '../types/stores/project-store'; import { IProjectQuery, 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';
import { CREATE_FEATURE, UPDATE_FEATURE } from '../types/permissions'; import { CREATE_FEATURE, UPDATE_FEATURE } from '../types/permissions';
import NoAccessError from '../error/no-access-error'; import NoAccessError from '../error/no-access-error';
import IncompatibleProjectError from '../error/incompatible-project-error'; import IncompatibleProjectError from '../error/incompatible-project-error';

View File

@ -18,7 +18,7 @@ import HealthService from '../services/health-service';
import SettingService from '../services/setting-service'; import SettingService from '../services/setting-service';
import SessionService from '../services/session-service'; import SessionService from '../services/session-service';
import UserFeedbackService from '../services/user-feedback-service'; import UserFeedbackService from '../services/user-feedback-service';
import FeatureToggleServiceV2 from '../services/feature-toggle-service-v2'; import FeatureToggleService from '../services/feature-toggle-service';
import EnvironmentService from '../services/environment-service'; import EnvironmentService from '../services/environment-service';
import FeatureTagService from '../services/feature-tag-service'; import FeatureTagService from '../services/feature-tag-service';
import ProjectHealthService from '../services/project-health-service'; import ProjectHealthService from '../services/project-health-service';
@ -35,7 +35,8 @@ export interface IUnleashServices {
environmentService: EnvironmentService; environmentService: EnvironmentService;
eventService: EventService; eventService: EventService;
featureTagService: FeatureTagService; featureTagService: FeatureTagService;
featureToggleServiceV2: FeatureToggleServiceV2; featureToggleService: FeatureToggleService;
featureToggleServiceV2: FeatureToggleService; // deprecated
featureTypeService: FeatureTypeService; featureTypeService: FeatureTypeService;
healthService: HealthService; healthService: HealthService;
projectHealthService: ProjectHealthService; projectHealthService: ProjectHealthService;

View File

@ -98,8 +98,7 @@ test('returns three archived toggles', async () => {
}); });
}); });
test.skip('revives a feature by name', async () => { test('revives a feature by name', async () => {
expect.assertions(0);
return app.request return app.request
.post('/api/admin/archive/revive/featureArchivedX') .post('/api/admin/archive/revive/featureArchivedX')
.set('Content-Type', 'application/json') .set('Content-Type', 'application/json')

View File

@ -3,6 +3,7 @@ import { FeatureToggleDTO, IStrategyConfig } from 'lib/types/model';
import dbInit, { ITestDb } from '../../helpers/database-init'; import dbInit, { ITestDb } from '../../helpers/database-init';
import { IUnleashTest, setupApp } from '../../helpers/test-helper'; import { IUnleashTest, setupApp } from '../../helpers/test-helper';
import getLogger from '../../../fixtures/no-logger'; import getLogger from '../../../fixtures/no-logger';
import { DEFAULT_ENV } from '../../../../lib/util/constants';
let app: IUnleashTest; let app: IUnleashTest;
let db: ITestDb; let db: ITestDb;
@ -30,8 +31,7 @@ beforeAll(async () => {
); );
await app.services.featureToggleServiceV2.createStrategy( await app.services.featureToggleServiceV2.createStrategy(
strategy, strategy,
projectId, { projectId, featureName: toggle.name, environment: DEFAULT_ENV },
toggle.name,
username, username,
); );
}; };
@ -263,6 +263,24 @@ test('can change status of feature toggle that does exist', async () => {
.expect(200); .expect(200);
}); });
test('cannot change project for feature toggle', async () => {
await app.request
.put('/api/admin/features/featureY')
.send({
name: 'featureY',
enabled: true,
project: 'random', //will be ignored
strategies: [{ name: 'default' }],
})
.set('Content-Type', 'application/json')
.expect(200);
const { body } = await app.request
.get('/api/admin/features/featureY')
.expect(200);
expect(body.project).toBe('default');
});
test('can not toggle of feature that does not exist', async () => { test('can not toggle of feature that does not exist', async () => {
expect.assertions(0); expect.assertions(0);
return app.request return app.request
@ -285,8 +303,7 @@ test('can toggle a feature that does exist', async () => {
); );
await app.services.featureToggleServiceV2.createStrategy( await app.services.featureToggleServiceV2.createStrategy(
defaultStrategy, defaultStrategy,
'default', { projectId: 'default', featureName, environment: DEFAULT_ENV },
featureName,
username, username,
); );
return app.request return app.request

View File

@ -643,6 +643,145 @@ test('Can add strategy to feature toggle to a "some-env-2"', async () => {
}); });
}); });
test('Can update strategy on feature toggle', async () => {
const envName = 'default';
const featureName = 'feature.strategy.update.strat';
const projectPath = '/api/admin/projects/default';
const featurePath = `${projectPath}/features/${featureName}`;
// create feature toggle
await app.request
.post(`${projectPath}/features`)
.send({ name: featureName })
.expect(201);
// add strategy
const { body: strategy } = await app.request
.post(`${featurePath}/environments/${envName}/strategies`)
.send({
name: 'default',
parameters: {
userIds: '',
},
})
.expect(200);
// update strategy
await app.request
.put(`${featurePath}/environments/${envName}/strategies/${strategy.id}`)
.send({
name: 'default',
parameters: {
userIds: '1234',
},
})
.expect(200);
const { body } = await app.request.get(`${featurePath}`);
const defaultEnv = body.environments[0];
expect(body.environments).toHaveLength(1);
expect(defaultEnv.name).toBe(envName);
expect(defaultEnv.strategies).toHaveLength(1);
expect(defaultEnv.strategies[0].parameters).toStrictEqual({
userIds: '1234',
});
});
test('Can NOT delete strategy with wrong projectId', async () => {
const envName = 'default';
const featureName = 'feature.strategy.delete.strat.error';
const projectPath = '/api/admin/projects/default';
const featurePath = `${projectPath}/features/${featureName}`;
// create feature toggle
await app.request
.post(`${projectPath}/features`)
.send({ name: featureName })
.expect(201);
// add strategy
const { body: strategy } = await app.request
.post(`${featurePath}/environments/${envName}/strategies`)
.send({
name: 'default',
parameters: {
userIds: '',
},
})
.expect(200);
// delete strategy
await app.request
.delete(
`/api/admin/projects/wrongId/features/${featureName}/environments/${envName}/strategies/${strategy.id}`,
)
.expect(403);
});
test('add strategy cannot use wrong projectId', async () => {
const envName = 'default';
const featureName = 'feature.strategy.add.strat.wrong.projectId';
// create feature toggle
await app.request
.post('/api/admin/projects/default/features')
.send({ name: featureName })
.expect(201);
// add strategy
await app.request
.post(
`/api/admin/projects/invalidId/features/${featureName}/environments/${envName}/strategies`,
)
.send({
name: 'default',
parameters: {
userIds: '',
},
})
.expect(403);
});
test('update strategy on feature toggle cannot use wrong projectId', async () => {
const envName = 'default';
const featureName = 'feature.strategy.update.strat.wrong.projectId';
const projectPath = '/api/admin/projects/default';
const featurePath = `${projectPath}/features/${featureName}`;
// create feature toggle
await app.request
.post(`${projectPath}/features`)
.send({ name: featureName })
.expect(201);
// add strategy
const { body: strategy } = await app.request
.post(`${featurePath}/environments/${envName}/strategies`)
.send({
name: 'default',
parameters: {
userIds: '',
},
})
.expect(200);
// update strategy
await app.request
.put(
`/api/admin/projects/invalidId/features/${featureName}/environments/${envName}/strategies/${strategy.id}`,
)
.send({
name: 'default',
parameters: {
userIds: '1234',
},
})
.expect(403);
});
test('Environments are returned in sortOrder', async () => { test('Environments are returned in sortOrder', async () => {
const sortedSecond = 'sortedSecond'; const sortedSecond = 'sortedSecond';
const sortedLast = 'sortedLast'; const sortedLast = 'sortedLast';

View File

@ -136,11 +136,11 @@ test('import for 4.1.2 enterprise format fixed works', async () => {
test('Can roundtrip. I.e. export and then import', async () => { test('Can roundtrip. I.e. export and then import', async () => {
const projectId = 'export-project'; const projectId = 'export-project';
const environmentId = 'export-environment'; const environment = 'export-environment';
const userName = 'export-user'; const userName = 'export-user';
const featureName = 'export.feature'; const featureName = 'export.feature';
await db.stores.environmentStore.create({ await db.stores.environmentStore.create({
name: environmentId, name: environment,
type: 'test', type: 'test',
}); });
await db.stores.projectStore.create({ await db.stores.projectStore.create({
@ -149,7 +149,7 @@ test('Can roundtrip. I.e. export and then import', async () => {
description: 'Project for export', description: 'Project for export',
}); });
await app.services.environmentService.addEnvironmentToProject( await app.services.environmentService.addEnvironmentToProject(
environmentId, environment,
projectId, projectId,
); );
await app.services.featureToggleServiceV2.createFeatureToggle( await app.services.featureToggleServiceV2.createFeatureToggle(
@ -169,9 +169,8 @@ test('Can roundtrip. I.e. export and then import', async () => {
], ],
parameters: {}, parameters: {},
}, },
projectId, { projectId, featureName, environment },
featureName, userName,
environmentId,
); );
const data = await app.services.stateService.export({}); const data = await app.services.stateService.export({});
await app.services.stateService.import({ await app.services.stateService.import({
@ -184,11 +183,11 @@ test('Can roundtrip. I.e. export and then import', async () => {
test('Roundtrip with tags works', async () => { test('Roundtrip with tags works', async () => {
const projectId = 'tags-project'; const projectId = 'tags-project';
const environmentId = 'tags-environment'; const environment = 'tags-environment';
const userName = 'tags-user'; const userName = 'tags-user';
const featureName = 'tags.feature'; const featureName = 'tags.feature';
await db.stores.environmentStore.create({ await db.stores.environmentStore.create({
name: environmentId, name: environment,
type: 'test', type: 'test',
}); });
await db.stores.projectStore.create({ await db.stores.projectStore.create({
@ -197,7 +196,7 @@ test('Roundtrip with tags works', async () => {
description: 'Project for export', description: 'Project for export',
}); });
await app.services.environmentService.addEnvironmentToProject( await app.services.environmentService.addEnvironmentToProject(
environmentId, environment,
projectId, projectId,
); );
await app.services.featureToggleServiceV2.createFeatureToggle( await app.services.featureToggleServiceV2.createFeatureToggle(
@ -217,9 +216,12 @@ test('Roundtrip with tags works', async () => {
], ],
parameters: {}, parameters: {},
}, },
projectId, {
featureName, projectId,
environmentId, featureName,
environment,
},
userName,
); );
await app.services.featureTagService.addTag( await app.services.featureTagService.addTag(
featureName, featureName,
@ -245,11 +247,11 @@ test('Roundtrip with tags works', async () => {
test('Roundtrip with strategies in multiple environments works', async () => { test('Roundtrip with strategies in multiple environments works', async () => {
const projectId = 'multiple-environment-project'; const projectId = 'multiple-environment-project';
const environmentId = 'multiple-environment-environment'; const environment = 'multiple-environment-environment';
const userName = 'multiple-environment-user'; const userName = 'multiple-environment-user';
const featureName = 'multiple-environment.feature'; const featureName = 'multiple-environment.feature';
await db.stores.environmentStore.create({ await db.stores.environmentStore.create({
name: environmentId, name: environment,
type: 'test', type: 'test',
}); });
await db.stores.projectStore.create({ await db.stores.projectStore.create({
@ -258,7 +260,7 @@ test('Roundtrip with strategies in multiple environments works', async () => {
description: 'Project for export', description: 'Project for export',
}); });
await app.services.environmentService.addEnvironmentToProject( await app.services.environmentService.addEnvironmentToProject(
environmentId, environment,
projectId, projectId,
); );
await app.services.environmentService.addEnvironmentToProject( await app.services.environmentService.addEnvironmentToProject(
@ -282,9 +284,8 @@ test('Roundtrip with strategies in multiple environments works', async () => {
], ],
parameters: {}, parameters: {},
}, },
projectId, { projectId, featureName, environment },
featureName, userName,
environmentId,
); );
await app.services.featureToggleServiceV2.createStrategy( await app.services.featureToggleServiceV2.createStrategy(
{ {
@ -294,9 +295,8 @@ test('Roundtrip with strategies in multiple environments works', async () => {
], ],
parameters: {}, parameters: {},
}, },
projectId, { projectId, featureName, environment: DEFAULT_ENV },
featureName, userName,
DEFAULT_ENV,
); );
const data = await app.services.stateService.export({}); const data = await app.services.stateService.export({});
await app.services.stateService.import({ await app.services.stateService.import({

View File

@ -1,30 +1,32 @@
import { IUnleashTest, setupApp } from '../../helpers/test-helper'; import { IUnleashTest, setupApp } from '../../helpers/test-helper';
import dbInit, { ITestDb } from '../../helpers/database-init'; import dbInit, { ITestDb } from '../../helpers/database-init';
import getLogger from '../../../fixtures/no-logger'; import getLogger from '../../../fixtures/no-logger';
import { DEFAULT_ENV } from '../../../../lib/util/constants';
let app: IUnleashTest; let app: IUnleashTest;
let db: ITestDb; let db: ITestDb;
const featureName = 'feature.default.1'; const featureName = 'feature.default.1';
const username = 'test';
const projectId = 'default';
beforeAll(async () => { beforeAll(async () => {
db = await dbInit('feature_env_api_client', getLogger); db = await dbInit('feature_env_api_client', getLogger);
app = await setupApp(db.stores); app = await setupApp(db.stores);
await app.services.featureToggleServiceV2.createFeatureToggle( await app.services.featureToggleServiceV2.createFeatureToggle(
'default', projectId,
{ {
name: featureName, name: featureName,
description: 'the #1 feature', description: 'the #1 feature',
}, },
'test', username,
); );
await app.services.featureToggleServiceV2.createStrategy( await app.services.featureToggleServiceV2.createStrategy(
{ name: 'default', constraints: [], parameters: {} }, { name: 'default', constraints: [], parameters: {} },
'default', { projectId, featureName, environment: DEFAULT_ENV },
featureName, username,
'test',
); );
}); });

View File

@ -55,8 +55,7 @@ beforeAll(async () => {
constraints: [], constraints: [],
parameters: {}, parameters: {},
}, },
project, { projectId: project, featureName: feature1, environment: DEFAULT_ENV },
feature1,
username, username,
); );
await featureToggleServiceV2.createStrategy( await featureToggleServiceV2.createStrategy(
@ -65,10 +64,8 @@ beforeAll(async () => {
constraints: [], constraints: [],
parameters: {}, parameters: {},
}, },
project, { projectId: project, featureName: feature1, environment },
feature1,
username, username,
environment,
); );
// create feature 2 // create feature 2
@ -85,10 +82,8 @@ beforeAll(async () => {
constraints: [], constraints: [],
parameters: {}, parameters: {},
}, },
project, { projectId: project, featureName: feature2, environment },
feature2,
username, username,
environment,
); );
// create feature 3 // create feature 3
@ -105,10 +100,8 @@ beforeAll(async () => {
constraints: [], constraints: [],
parameters: {}, parameters: {},
}, },
project2, { projectId: project2, featureName: feature3, environment },
feature3,
username, username,
environment,
); );
}); });

View File

@ -1,4 +1,4 @@
import FeatureToggleServiceV2 from '../../../lib/services/feature-toggle-service-v2'; import FeatureToggleServiceV2 from '../../../lib/services/feature-toggle-service';
import { IStrategyConfig } from '../../../lib/types/model'; import { IStrategyConfig } from '../../../lib/types/model';
import { createTestConfig } from '../../config/test-config'; import { createTestConfig } from '../../config/test-config';
import dbInit from '../helpers/database-init'; import dbInit from '../helpers/database-init';
@ -41,8 +41,7 @@ test('Should create feature toggle strategy configuration', async () => {
const createdConfig = await service.createStrategy( const createdConfig = await service.createStrategy(
config, config,
projectId, { projectId, featureName: 'Demo', environment: DEFAULT_ENV },
'Demo',
username, username,
); );
@ -53,6 +52,7 @@ test('Should create feature toggle strategy configuration', async () => {
test('Should be able to update existing strategy configuration', async () => { test('Should be able to update existing strategy configuration', async () => {
const projectId = 'default'; const projectId = 'default';
const username = 'existing-strategy'; const username = 'existing-strategy';
const featureName = 'update-existing-strategy';
const config: Omit<IStrategyConfig, 'id'> = { const config: Omit<IStrategyConfig, 'id'> = {
name: 'default', name: 'default',
constraints: [], constraints: [],
@ -62,32 +62,33 @@ test('Should be able to update existing strategy configuration', async () => {
await service.createFeatureToggle( await service.createFeatureToggle(
projectId, projectId,
{ {
name: 'update-existing-strategy', name: featureName,
}, },
'test', 'test',
); );
const createdConfig = await service.createStrategy( const createdConfig = await service.createStrategy(
config, config,
'default', { projectId, featureName, environment: DEFAULT_ENV },
'update-existing-strategy',
username, username,
); );
expect(createdConfig.name).toEqual('default'); expect(createdConfig.name).toEqual('default');
const updatedConfig = await service.updateStrategy( const updatedConfig = await service.updateStrategy(
createdConfig.id, createdConfig.id,
DEFAULT_ENV,
projectId,
username,
{ {
parameters: { b2b: true }, parameters: { b2b: true },
}, },
{ projectId, featureName, environment: DEFAULT_ENV },
username,
); );
expect(createdConfig.id).toEqual(updatedConfig.id); expect(createdConfig.id).toEqual(updatedConfig.id);
expect(updatedConfig.parameters).toEqual({ b2b: true }); expect(updatedConfig.parameters).toEqual({ b2b: true });
}); });
test('Should be able to get strategy by id', async () => { test('Should be able to get strategy by id', async () => {
const featureName = 'get-strategy-by-id';
const projectId = 'default';
const userName = 'strategy'; const userName = 'strategy';
const config: Omit<IStrategyConfig, 'id'> = { const config: Omit<IStrategyConfig, 'id'> = {
name: 'default', name: 'default',
@ -95,19 +96,17 @@ test('Should be able to get strategy by id', async () => {
parameters: {}, parameters: {},
}; };
await service.createFeatureToggle( await service.createFeatureToggle(
'default', projectId,
{ {
name: 'get-strategy-by-id', name: featureName,
}, },
'test', userName,
); );
const createdConfig = await service.createStrategy( const createdConfig = await service.createStrategy(
config, config,
'default', { projectId, featureName, environment: DEFAULT_ENV },
'Demo',
userName, userName,
DEFAULT_ENV,
); );
const fetchedConfig = await service.getStrategy(createdConfig.id); const fetchedConfig = await service.getStrategy(createdConfig.id);
expect(fetchedConfig).toEqual(createdConfig); expect(fetchedConfig).toEqual(createdConfig);

View File

@ -1,6 +1,6 @@
import dbInit, { ITestDb } from '../helpers/database-init'; import dbInit, { ITestDb } from '../helpers/database-init';
import getLogger from '../../fixtures/no-logger'; import getLogger from '../../fixtures/no-logger';
import FeatureToggleServiceV2 from '../../../lib/services/feature-toggle-service-v2'; import FeatureToggleServiceV2 from '../../../lib/services/feature-toggle-service';
import { AccessService } from '../../../lib/services/access-service'; import { AccessService } from '../../../lib/services/access-service';
import ProjectService from '../../../lib/services/project-service'; import ProjectService from '../../../lib/services/project-service';
import ProjectHealthService from '../../../lib/services/project-health-service'; import ProjectHealthService from '../../../lib/services/project-health-service';

View File

@ -1,6 +1,6 @@
import dbInit, { ITestDb } from '../helpers/database-init'; import dbInit, { ITestDb } from '../helpers/database-init';
import getLogger from '../../fixtures/no-logger'; import getLogger from '../../fixtures/no-logger';
import FeatureToggleServiceV2 from '../../../lib/services/feature-toggle-service-v2'; import FeatureToggleServiceV2 from '../../../lib/services/feature-toggle-service';
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 { import {

View File

@ -6,6 +6,7 @@ let db;
let featureToggleStore; let featureToggleStore;
beforeAll(async () => { beforeAll(async () => {
getLogger.setMuteError(true);
db = await dbInit('feature_toggle_store_serial', getLogger); db = await dbInit('feature_toggle_store_serial', getLogger);
stores = db.stores; stores = db.stores;
featureToggleStore = stores.featureToggleStore; featureToggleStore = stores.featureToggleStore;
@ -23,7 +24,6 @@ test('should not crash for unknown toggle', async () => {
}); });
test('should not crash for undefined toggle name', async () => { test('should not crash for undefined toggle name', async () => {
getLogger.setMuteError(true);
const project = await featureToggleStore.getProjectId(undefined); const project = await featureToggleStore.getProjectId(undefined);
expect(project).toBe(undefined); expect(project).toBe(undefined);
}); });