mirror of
https://github.com/Unleash/unleash.git
synced 2025-05-12 01:17:04 +02:00
feat: API prevents you from deleting segments in crs (#5308)
This PR hooks up the changes introduced in #5301 to the API and puts them behind a feature flag. A new test has been added and the test setup has been slightly tweaked to allow this test. When the flag is enabled, the API will now not let you delete a segment that's used in any active CRs.
This commit is contained in:
parent
4d1f76e61b
commit
ece5a634bf
@ -2,7 +2,7 @@ import { IUser } from 'lib/server-impl';
|
|||||||
import dbInit, { ITestDb } from '../../../test/e2e/helpers/database-init';
|
import dbInit, { ITestDb } from '../../../test/e2e/helpers/database-init';
|
||||||
import getLogger from '../../../test/fixtures/no-logger';
|
import getLogger from '../../../test/fixtures/no-logger';
|
||||||
import { IChangeRequestSegmentUsageReadModel } from './change-request-segment-usage-read-model';
|
import { IChangeRequestSegmentUsageReadModel } from './change-request-segment-usage-read-model';
|
||||||
import { createChangeRequestSegmentUsageModel } from './createChangeRequestSegmentUsageReadModel';
|
import { createChangeRequestSegmentUsageReadModel } from './createChangeRequestSegmentUsageReadModel';
|
||||||
import { randomId } from '../../../lib/util';
|
import { randomId } from '../../../lib/util';
|
||||||
|
|
||||||
let db: ITestDb;
|
let db: ITestDb;
|
||||||
@ -20,7 +20,7 @@ beforeAll(async () => {
|
|||||||
username: 'cr-creator',
|
username: 'cr-creator',
|
||||||
});
|
});
|
||||||
|
|
||||||
readModel = createChangeRequestSegmentUsageModel(db.rawDatabase);
|
readModel = createChangeRequestSegmentUsageReadModel(db.rawDatabase);
|
||||||
|
|
||||||
await db.stores.featureToggleStore.create('default', {
|
await db.stores.featureToggleStore.create('default', {
|
||||||
name: FLAG_NAME,
|
name: FLAG_NAME,
|
@ -3,13 +3,13 @@ import { ChangeRequestSegmentUsageReadModel } from './sql-change-request-segment
|
|||||||
import { FakeChangeRequestSegmentUsageReadModel } from './fake-change-request-segment-usage-read-model';
|
import { FakeChangeRequestSegmentUsageReadModel } from './fake-change-request-segment-usage-read-model';
|
||||||
import { IChangeRequestSegmentUsageReadModel } from './change-request-segment-usage-read-model';
|
import { IChangeRequestSegmentUsageReadModel } from './change-request-segment-usage-read-model';
|
||||||
|
|
||||||
export const createChangeRequestSegmentUsageModel = (
|
export const createChangeRequestSegmentUsageReadModel = (
|
||||||
db: Db,
|
db: Db,
|
||||||
): IChangeRequestSegmentUsageReadModel => {
|
): IChangeRequestSegmentUsageReadModel => {
|
||||||
return new ChangeRequestSegmentUsageReadModel(db);
|
return new ChangeRequestSegmentUsageReadModel(db);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const createFakeChangeRequestAccessService =
|
export const createFakeChangeRequestSegmentUsageReadModel =
|
||||||
(): IChangeRequestSegmentUsageReadModel => {
|
(): IChangeRequestSegmentUsageReadModel => {
|
||||||
return new FakeChangeRequestSegmentUsageReadModel();
|
return new FakeChangeRequestSegmentUsageReadModel();
|
||||||
};
|
};
|
@ -11,6 +11,10 @@ import {
|
|||||||
createChangeRequestAccessReadModel,
|
createChangeRequestAccessReadModel,
|
||||||
createFakeChangeRequestAccessService,
|
createFakeChangeRequestAccessService,
|
||||||
} from '../change-request-access-service/createChangeRequestAccessReadModel';
|
} from '../change-request-access-service/createChangeRequestAccessReadModel';
|
||||||
|
import {
|
||||||
|
createChangeRequestSegmentUsageReadModel,
|
||||||
|
createFakeChangeRequestSegmentUsageReadModel,
|
||||||
|
} from '../change-request-segment-usage-service/createChangeRequestSegmentUsageReadModel';
|
||||||
import {
|
import {
|
||||||
createFakePrivateProjectChecker,
|
createFakePrivateProjectChecker,
|
||||||
createPrivateProjectChecker,
|
createPrivateProjectChecker,
|
||||||
@ -40,6 +44,10 @@ export const createSegmentService = (
|
|||||||
db,
|
db,
|
||||||
config,
|
config,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const changeRequestSegmentUsageReadModel =
|
||||||
|
createChangeRequestSegmentUsageReadModel(db);
|
||||||
|
|
||||||
const privateProjectChecker = createPrivateProjectChecker(db, config);
|
const privateProjectChecker = createPrivateProjectChecker(db, config);
|
||||||
|
|
||||||
const eventService = new EventService(
|
const eventService = new EventService(
|
||||||
@ -53,6 +61,7 @@ export const createSegmentService = (
|
|||||||
return new SegmentService(
|
return new SegmentService(
|
||||||
{ segmentStore, featureStrategiesStore },
|
{ segmentStore, featureStrategiesStore },
|
||||||
changeRequestAccessReadModel,
|
changeRequestAccessReadModel,
|
||||||
|
changeRequestSegmentUsageReadModel,
|
||||||
config,
|
config,
|
||||||
eventService,
|
eventService,
|
||||||
privateProjectChecker,
|
privateProjectChecker,
|
||||||
@ -66,6 +75,8 @@ export const createFakeSegmentService = (
|
|||||||
const segmentStore = new FakeSegmentStore();
|
const segmentStore = new FakeSegmentStore();
|
||||||
const featureStrategiesStore = new FakeFeatureStrategiesStore();
|
const featureStrategiesStore = new FakeFeatureStrategiesStore();
|
||||||
const changeRequestAccessReadModel = createFakeChangeRequestAccessService();
|
const changeRequestAccessReadModel = createFakeChangeRequestAccessService();
|
||||||
|
const changeRequestSegmentUsageReadModel =
|
||||||
|
createFakeChangeRequestSegmentUsageReadModel();
|
||||||
|
|
||||||
const privateProjectChecker = createFakePrivateProjectChecker();
|
const privateProjectChecker = createFakePrivateProjectChecker();
|
||||||
|
|
||||||
@ -80,6 +91,7 @@ export const createFakeSegmentService = (
|
|||||||
return new SegmentService(
|
return new SegmentService(
|
||||||
{ segmentStore, featureStrategiesStore },
|
{ segmentStore, featureStrategiesStore },
|
||||||
changeRequestAccessReadModel,
|
changeRequestAccessReadModel,
|
||||||
|
changeRequestSegmentUsageReadModel,
|
||||||
config,
|
config,
|
||||||
eventService,
|
eventService,
|
||||||
privateProjectChecker,
|
privateProjectChecker,
|
||||||
|
@ -373,9 +373,16 @@ export class SegmentsController extends Controller {
|
|||||||
res: Response,
|
res: Response,
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
const id = Number(req.params.id);
|
const id = Number(req.params.id);
|
||||||
const strategies = await this.segmentService.getAllStrategies(id);
|
|
||||||
|
|
||||||
if (strategies.length > 0) {
|
let segmentIsInUse = false;
|
||||||
|
if (this.flagResolver.isEnabled('detectSegmentUsageInChangeRequests')) {
|
||||||
|
segmentIsInUse = await this.segmentService.isInUse(id);
|
||||||
|
} else {
|
||||||
|
const strategies = await this.segmentService.getAllStrategies(id);
|
||||||
|
segmentIsInUse = strategies.length > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (segmentIsInUse) {
|
||||||
res.status(409).send();
|
res.status(409).send();
|
||||||
} else {
|
} else {
|
||||||
await this.segmentService.delete(id, req.user);
|
await this.segmentService.delete(id, req.user);
|
||||||
|
@ -60,4 +60,6 @@ export interface ISegmentService {
|
|||||||
sourceStrategyId: string,
|
sourceStrategyId: string,
|
||||||
targetStrategyId: string,
|
targetStrategyId: string,
|
||||||
): Promise<void>;
|
): Promise<void>;
|
||||||
|
|
||||||
|
isInUse(id: number): Promise<boolean>;
|
||||||
}
|
}
|
||||||
|
@ -63,6 +63,10 @@ import {
|
|||||||
createChangeRequestAccessReadModel,
|
createChangeRequestAccessReadModel,
|
||||||
createFakeChangeRequestAccessService,
|
createFakeChangeRequestAccessService,
|
||||||
} from '../features/change-request-access-service/createChangeRequestAccessReadModel';
|
} from '../features/change-request-access-service/createChangeRequestAccessReadModel';
|
||||||
|
import {
|
||||||
|
createChangeRequestSegmentUsageReadModel,
|
||||||
|
createFakeChangeRequestSegmentUsageReadModel,
|
||||||
|
} from '../features/change-request-segment-usage-service/createChangeRequestSegmentUsageReadModel';
|
||||||
import ConfigurationRevisionService from '../features/feature-toggle/configuration-revision-service';
|
import ConfigurationRevisionService from '../features/feature-toggle/configuration-revision-service';
|
||||||
import {
|
import {
|
||||||
createFakeProjectService,
|
createFakeProjectService,
|
||||||
@ -322,9 +326,15 @@ export const createServices = (
|
|||||||
const changeRequestAccessReadModel = db
|
const changeRequestAccessReadModel = db
|
||||||
? createChangeRequestAccessReadModel(db, config)
|
? createChangeRequestAccessReadModel(db, config)
|
||||||
: createFakeChangeRequestAccessService();
|
: createFakeChangeRequestAccessService();
|
||||||
|
|
||||||
|
const changeRequestSegmentUsageReadModel = db
|
||||||
|
? createChangeRequestSegmentUsageReadModel(db)
|
||||||
|
: createFakeChangeRequestSegmentUsageReadModel();
|
||||||
|
|
||||||
const segmentService = new SegmentService(
|
const segmentService = new SegmentService(
|
||||||
stores,
|
stores,
|
||||||
changeRequestAccessReadModel,
|
changeRequestAccessReadModel,
|
||||||
|
changeRequestSegmentUsageReadModel,
|
||||||
config,
|
config,
|
||||||
eventService,
|
eventService,
|
||||||
privateProjectChecker,
|
privateProjectChecker,
|
||||||
|
@ -23,6 +23,7 @@ import { PermissionError } from '../error';
|
|||||||
import { IChangeRequestAccessReadModel } from '../features/change-request-access-service/change-request-access-read-model';
|
import { IChangeRequestAccessReadModel } from '../features/change-request-access-service/change-request-access-read-model';
|
||||||
import { IPrivateProjectChecker } from '../features/private-project/privateProjectCheckerType';
|
import { IPrivateProjectChecker } from '../features/private-project/privateProjectCheckerType';
|
||||||
import EventService from './event-service';
|
import EventService from './event-service';
|
||||||
|
import { IChangeRequestSegmentUsageReadModel } from 'lib/features/change-request-segment-usage-service/change-request-segment-usage-read-model';
|
||||||
|
|
||||||
export class SegmentService implements ISegmentService {
|
export class SegmentService implements ISegmentService {
|
||||||
private logger: Logger;
|
private logger: Logger;
|
||||||
@ -33,6 +34,8 @@ export class SegmentService implements ISegmentService {
|
|||||||
|
|
||||||
private changeRequestAccessReadModel: IChangeRequestAccessReadModel;
|
private changeRequestAccessReadModel: IChangeRequestAccessReadModel;
|
||||||
|
|
||||||
|
private changeRequestSegmentUsageReadModel: IChangeRequestSegmentUsageReadModel;
|
||||||
|
|
||||||
private config: IUnleashConfig;
|
private config: IUnleashConfig;
|
||||||
|
|
||||||
private flagResolver: IFlagResolver;
|
private flagResolver: IFlagResolver;
|
||||||
@ -47,6 +50,7 @@ export class SegmentService implements ISegmentService {
|
|||||||
featureStrategiesStore,
|
featureStrategiesStore,
|
||||||
}: Pick<IUnleashStores, 'segmentStore' | 'featureStrategiesStore'>,
|
}: Pick<IUnleashStores, 'segmentStore' | 'featureStrategiesStore'>,
|
||||||
changeRequestAccessReadModel: IChangeRequestAccessReadModel,
|
changeRequestAccessReadModel: IChangeRequestAccessReadModel,
|
||||||
|
changeRequestSegmentUsageReadModel: IChangeRequestSegmentUsageReadModel,
|
||||||
config: IUnleashConfig,
|
config: IUnleashConfig,
|
||||||
eventService: EventService,
|
eventService: EventService,
|
||||||
privateProjectChecker: IPrivateProjectChecker,
|
privateProjectChecker: IPrivateProjectChecker,
|
||||||
@ -55,6 +59,8 @@ export class SegmentService implements ISegmentService {
|
|||||||
this.featureStrategiesStore = featureStrategiesStore;
|
this.featureStrategiesStore = featureStrategiesStore;
|
||||||
this.eventService = eventService;
|
this.eventService = eventService;
|
||||||
this.changeRequestAccessReadModel = changeRequestAccessReadModel;
|
this.changeRequestAccessReadModel = changeRequestAccessReadModel;
|
||||||
|
this.changeRequestSegmentUsageReadModel =
|
||||||
|
changeRequestSegmentUsageReadModel;
|
||||||
this.privateProjectChecker = privateProjectChecker;
|
this.privateProjectChecker = privateProjectChecker;
|
||||||
this.logger = config.getLogger('services/segment-service.ts');
|
this.logger = config.getLogger('services/segment-service.ts');
|
||||||
this.flagResolver = config.flagResolver;
|
this.flagResolver = config.flagResolver;
|
||||||
@ -108,6 +114,17 @@ export class SegmentService implements ISegmentService {
|
|||||||
return strategies;
|
return strategies;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async isInUse(id: number): Promise<boolean> {
|
||||||
|
const strategies = await this.getAllStrategies(id);
|
||||||
|
if (strategies.length > 0) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return await this.changeRequestSegmentUsageReadModel.isSegmentUsedInActiveChangeRequests(
|
||||||
|
id,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
async create(
|
async create(
|
||||||
data: unknown,
|
data: unknown,
|
||||||
user: Partial<Pick<User, 'username' | 'email'>>,
|
user: Partial<Pick<User, 'username' | 'email'>>,
|
||||||
|
@ -108,14 +108,18 @@ const validateSegment = (
|
|||||||
|
|
||||||
beforeAll(async () => {
|
beforeAll(async () => {
|
||||||
db = await dbInit('segments_api_serial', getLogger);
|
db = await dbInit('segments_api_serial', getLogger);
|
||||||
app = await setupAppWithCustomConfig(db.stores, {
|
app = await setupAppWithCustomConfig(
|
||||||
experimental: {
|
db.stores,
|
||||||
flags: {
|
{
|
||||||
strictSchemaValidation: true,
|
experimental: {
|
||||||
anonymiseEventLog: true,
|
flags: {
|
||||||
|
anonymiseEventLog: true,
|
||||||
|
detectSegmentUsageInChangeRequests: true,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
db.rawDatabase,
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
afterAll(async () => {
|
afterAll(async () => {
|
||||||
@ -230,6 +234,57 @@ test('should not delete segments used by strategies', async () => {
|
|||||||
expect((await fetchSegments()).length).toEqual(1);
|
expect((await fetchSegments()).length).toEqual(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('should not delete segments used by strategies in CRs', async () => {
|
||||||
|
await createSegment({ name: 'a', constraints: [] });
|
||||||
|
const toggle = mockFeatureToggle();
|
||||||
|
await createFeatureToggle(app, toggle);
|
||||||
|
const [segment] = await fetchSegments();
|
||||||
|
|
||||||
|
const CR_ID = 54321;
|
||||||
|
|
||||||
|
const user = await db.stores.userStore.insert({
|
||||||
|
username: 'test',
|
||||||
|
});
|
||||||
|
|
||||||
|
await db.rawDatabase.table('change_requests').insert({
|
||||||
|
id: CR_ID,
|
||||||
|
environment: 'default',
|
||||||
|
state: 'In Review',
|
||||||
|
project: 'default',
|
||||||
|
created_by: user.id,
|
||||||
|
created_at: '2023-01-01 00:00:00',
|
||||||
|
min_approvals: 1,
|
||||||
|
title: 'My change request',
|
||||||
|
});
|
||||||
|
|
||||||
|
await db.rawDatabase.table('change_request_events').insert({
|
||||||
|
feature: toggle.name,
|
||||||
|
action: 'addStrategy',
|
||||||
|
payload: {
|
||||||
|
name: 'flexibleRollout',
|
||||||
|
title: '',
|
||||||
|
disabled: false,
|
||||||
|
segments: [segment.id],
|
||||||
|
variants: [],
|
||||||
|
parameters: {
|
||||||
|
groupId: toggle.name,
|
||||||
|
rollout: '100',
|
||||||
|
stickiness: 'default',
|
||||||
|
},
|
||||||
|
constraints: [],
|
||||||
|
},
|
||||||
|
created_at: '2023-01-01 00:01:00',
|
||||||
|
change_request_id: CR_ID,
|
||||||
|
created_by: user.id,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect((await fetchSegments()).length).toEqual(1);
|
||||||
|
|
||||||
|
await app.request.delete(`${SEGMENTS_BASE_PATH}/${segment.id}`).expect(409);
|
||||||
|
|
||||||
|
expect((await fetchSegments()).length).toEqual(1);
|
||||||
|
});
|
||||||
|
|
||||||
test('should list strategies by segment', async () => {
|
test('should list strategies by segment', async () => {
|
||||||
await createSegment({ name: 'S1', constraints: [] });
|
await createSegment({ name: 'S1', constraints: [] });
|
||||||
await createSegment({ name: 'S2', constraints: [] });
|
await createSegment({ name: 'S2', constraints: [] });
|
||||||
|
Loading…
Reference in New Issue
Block a user