1
0
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:
Thomas Heartman 2023-11-09 12:09:39 +01:00 committed by GitHub
parent 4d1f76e61b
commit ece5a634bf
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 115 additions and 12 deletions

View File

@ -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,

View File

@ -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();
}; };

View File

@ -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,

View File

@ -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);

View File

@ -60,4 +60,6 @@ export interface ISegmentService {
sourceStrategyId: string, sourceStrategyId: string,
targetStrategyId: string, targetStrategyId: string,
): Promise<void>; ): Promise<void>;
isInUse(id: number): Promise<boolean>;
} }

View File

@ -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,

View File

@ -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'>>,

View File

@ -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: [] });