1
0
mirror of https://github.com/Unleash/unleash.git synced 2025-01-31 00:16:47 +01:00

feat: prevent adding dependency to archived or removed parent (#4987)

This commit is contained in:
Mateusz Kwasniewski 2023-10-11 16:21:57 +02:00 committed by GitHub
parent 7ea7c08654
commit 30e9fb87e9
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 99 additions and 12 deletions

View File

@ -14,6 +14,8 @@ import {
createChangeRequestAccessReadModel,
createFakeChangeRequestAccessService,
} from '../change-request-access-service/createChangeRequestAccessReadModel';
import { FeaturesReadModel } from '../feature-toggle/features-read-model';
import { FakeFeaturesReadModel } from '../feature-toggle/fake-features-read-model';
export const createDependentFeaturesService = (
db: Db,
@ -35,12 +37,14 @@ export const createDependentFeaturesService = (
db,
config,
);
return new DependentFeaturesService(
const featuresReadModel = new FeaturesReadModel(db);
return new DependentFeaturesService({
dependentFeaturesStore,
dependentFeaturesReadModel,
changeRequestAccessReadModel,
featuresReadModel,
eventService,
);
});
};
export const createFakeDependentFeaturesService = (
@ -58,11 +62,13 @@ export const createFakeDependentFeaturesService = (
const dependentFeaturesStore = new FakeDependentFeaturesStore();
const dependentFeaturesReadModel = new FakeDependentFeaturesReadModel();
const changeRequestAccessReadModel = createFakeChangeRequestAccessService();
const featuresReadModel = new FakeFeaturesReadModel();
return new DependentFeaturesService(
return new DependentFeaturesService({
dependentFeaturesStore,
dependentFeaturesReadModel,
changeRequestAccessReadModel,
featuresReadModel,
eventService,
);
});
};

View File

@ -8,6 +8,15 @@ import { User } from '../../server-impl';
import { SKIP_CHANGE_REQUEST } from '../../types';
import { IChangeRequestAccessReadModel } from '../change-request-access-service/change-request-access-read-model';
import { extractUsernameFromUser } from '../../util';
import { IFeaturesReadModel } from '../feature-toggle/features-read-model-type';
interface IDependentFeaturesServiceDeps {
dependentFeaturesStore: IDependentFeaturesStore;
dependentFeaturesReadModel: IDependentFeaturesReadModel;
changeRequestAccessReadModel: IChangeRequestAccessReadModel;
featuresReadModel: IFeaturesReadModel;
eventService: EventService;
}
export class DependentFeaturesService {
private dependentFeaturesStore: IDependentFeaturesStore;
@ -16,17 +25,21 @@ export class DependentFeaturesService {
private changeRequestAccessReadModel: IChangeRequestAccessReadModel;
private featuresReadModel: IFeaturesReadModel;
private eventService: EventService;
constructor(
dependentFeaturesStore: IDependentFeaturesStore,
dependentFeaturesReadModel: IDependentFeaturesReadModel,
changeRequestAccessReadModel: IChangeRequestAccessReadModel,
eventService: EventService,
) {
constructor({
featuresReadModel,
dependentFeaturesReadModel,
dependentFeaturesStore,
eventService,
changeRequestAccessReadModel,
}: IDependentFeaturesServiceDeps) {
this.dependentFeaturesStore = dependentFeaturesStore;
this.dependentFeaturesReadModel = dependentFeaturesReadModel;
this.changeRequestAccessReadModel = changeRequestAccessReadModel;
this.featuresReadModel = featuresReadModel;
this.eventService = eventService;
}
@ -77,15 +90,23 @@ export class DependentFeaturesService {
): Promise<void> {
const { enabled, feature: parent, variants } = dependentFeature;
const children = await this.dependentFeaturesReadModel.getChildren([
child,
const [children, parentExists] = await Promise.all([
this.dependentFeaturesReadModel.getChildren([child]),
this.featuresReadModel.featureExists(parent),
]);
if (children.length > 0) {
throw new InvalidOperationError(
'Transitive dependency detected. Cannot add a dependency to the feature that other features depend on.',
);
}
if (!parentExists) {
throw new InvalidOperationError(
`No active feature ${parent} exists`,
);
}
const featureDependency: FeatureDependency =
enabled === false
? {

View File

@ -136,3 +136,34 @@ test('should not allow to add a parent dependency to a feature that already has
403,
);
});
test('should not allow to add non-existent parent dependency', async () => {
const grandparent = uuidv4();
const parent = uuidv4();
const child = uuidv4();
await app.createFeature(child);
await addFeatureDependency(
child,
{
feature: parent,
},
403,
);
});
test('should not allow to add archived parent dependency', async () => {
const parent = uuidv4();
const child = uuidv4();
await app.createFeature(child);
await app.createFeature(parent);
await app.archiveFeature(parent);
await addFeatureDependency(
child,
{
feature: parent,
},
403,
);
});

View File

@ -0,0 +1,7 @@
import { IFeaturesReadModel } from './features-read-model-type';
export class FakeFeaturesReadModel implements IFeaturesReadModel {
featureExists(): Promise<boolean> {
return Promise.resolve(false);
}
}

View File

@ -0,0 +1,3 @@
export interface IFeaturesReadModel {
featureExists(parent: string): Promise<boolean>;
}

View File

@ -0,0 +1,19 @@
import { Db } from '../../db/db';
import { IFeaturesReadModel } from './features-read-model-type';
export class FeaturesReadModel implements IFeaturesReadModel {
private db: Db;
constructor(db: Db) {
this.db = db;
}
async featureExists(parent: string): Promise<boolean> {
const rows = await this.db('features')
.where('name', parent)
.andWhere('archived_at', null)
.select('name');
return rows.length > 0;
}
}