mirror of
				https://github.com/Unleash/unleash.git
				synced 2025-10-27 11:02:16 +01:00 
			
		
		
		
	feat: change project with feature dependencies (#4915)
This commit is contained in:
		
							parent
							
								
									1c4897da4d
								
							
						
					
					
						commit
						5141d9db67
					
				@ -0,0 +1,63 @@
 | 
			
		||||
import { screen, waitFor } from '@testing-library/react';
 | 
			
		||||
import { render } from 'utils/testRenderer';
 | 
			
		||||
import { testServerRoute, testServerSetup } from 'utils/testServer';
 | 
			
		||||
import FeatureSettingsProjectConfirm from './FeatureSettingsProjectConfirm';
 | 
			
		||||
import { IFeatureToggle } from 'interfaces/featureToggle';
 | 
			
		||||
import { UIProviderContainer } from '../../../../../providers/UIProvider/UIProviderContainer';
 | 
			
		||||
import { Route, Routes } from 'react-router-dom';
 | 
			
		||||
import React from 'react';
 | 
			
		||||
import userEvent from '@testing-library/user-event';
 | 
			
		||||
 | 
			
		||||
const server = testServerSetup();
 | 
			
		||||
 | 
			
		||||
const setupApi = () => {
 | 
			
		||||
    testServerRoute(server, '/api/admin/ui-config', {
 | 
			
		||||
        flags: {
 | 
			
		||||
            dependentFeatures: true,
 | 
			
		||||
        },
 | 
			
		||||
    });
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
test('Cannot change project for feature with dependencies', async () => {
 | 
			
		||||
    let closed = false;
 | 
			
		||||
    setupApi();
 | 
			
		||||
    render(
 | 
			
		||||
        <UIProviderContainer>
 | 
			
		||||
            <Routes>
 | 
			
		||||
                <Route
 | 
			
		||||
                    path={'projects/:projectId/features/:featureId/settings'}
 | 
			
		||||
                    element={
 | 
			
		||||
                        <FeatureSettingsProjectConfirm
 | 
			
		||||
                            projectId={'newProjectId'}
 | 
			
		||||
                            feature={
 | 
			
		||||
                                {
 | 
			
		||||
                                    environments: [],
 | 
			
		||||
                                    dependencies: [],
 | 
			
		||||
                                    children: ['child'],
 | 
			
		||||
                                } as unknown as IFeatureToggle
 | 
			
		||||
                            }
 | 
			
		||||
                            onClose={() => {
 | 
			
		||||
                                closed = true;
 | 
			
		||||
                            }}
 | 
			
		||||
                            onClick={() => {}}
 | 
			
		||||
                            open={true}
 | 
			
		||||
                            changeRequests={[]}
 | 
			
		||||
                        />
 | 
			
		||||
                    }
 | 
			
		||||
                />
 | 
			
		||||
            </Routes>
 | 
			
		||||
        </UIProviderContainer>,
 | 
			
		||||
        {
 | 
			
		||||
            route: 'projects/default/features/parent/settings',
 | 
			
		||||
        },
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    await screen.findByText('Please remove feature dependencies first.');
 | 
			
		||||
 | 
			
		||||
    const closeButton = await screen.findByText('Close');
 | 
			
		||||
    userEvent.click(closeButton);
 | 
			
		||||
 | 
			
		||||
    await waitFor(() => {
 | 
			
		||||
        expect(closed).toBe(true);
 | 
			
		||||
    });
 | 
			
		||||
});
 | 
			
		||||
@ -9,6 +9,7 @@ import { Link } from 'react-router-dom';
 | 
			
		||||
import { IChangeRequest } from 'component/changeRequest/changeRequest.types';
 | 
			
		||||
import { useRequiredPathParam } from 'hooks/useRequiredPathParam';
 | 
			
		||||
import { useChangeRequestsEnabled } from 'hooks/useChangeRequestsEnabled';
 | 
			
		||||
import { useUiFlag } from 'hooks/useUiFlag';
 | 
			
		||||
 | 
			
		||||
const StyledContainer = styled('div')(({ theme }) => ({
 | 
			
		||||
    display: 'grid',
 | 
			
		||||
@ -40,6 +41,7 @@ const FeatureSettingsProjectConfirm = ({
 | 
			
		||||
    feature,
 | 
			
		||||
    changeRequests,
 | 
			
		||||
}: IFeatureSettingsProjectConfirm) => {
 | 
			
		||||
    const dependentFeatures = useUiFlag('dependentFeatures');
 | 
			
		||||
    const currentProjectId = useRequiredPathParam('projectId');
 | 
			
		||||
    const { project } = useProject(projectId);
 | 
			
		||||
 | 
			
		||||
@ -58,10 +60,15 @@ const FeatureSettingsProjectConfirm = ({
 | 
			
		||||
        ? changeRequests.length > 0
 | 
			
		||||
        : false;
 | 
			
		||||
 | 
			
		||||
    const hasDependencies =
 | 
			
		||||
        dependentFeatures &&
 | 
			
		||||
        (feature.dependencies.length > 0 || feature.children.length > 0);
 | 
			
		||||
 | 
			
		||||
    return (
 | 
			
		||||
        <ConditionallyRender
 | 
			
		||||
            condition={
 | 
			
		||||
                hasSameEnvironments &&
 | 
			
		||||
                !hasDependencies &&
 | 
			
		||||
                !hasPendingChangeRequests &&
 | 
			
		||||
                !targetProjectHasChangeRequestsEnabled
 | 
			
		||||
            }
 | 
			
		||||
@ -98,6 +105,22 @@ const FeatureSettingsProjectConfirm = ({
 | 
			
		||||
                            Cannot proceed with the move
 | 
			
		||||
                        </StyledAlert>
 | 
			
		||||
 | 
			
		||||
                        <ConditionallyRender
 | 
			
		||||
                            condition={hasDependencies}
 | 
			
		||||
                            show={
 | 
			
		||||
                                <p>
 | 
			
		||||
                                    <span>
 | 
			
		||||
                                        The feature toggle must not have any
 | 
			
		||||
                                        dependencies.
 | 
			
		||||
                                    </span>{' '}
 | 
			
		||||
                                    <br />
 | 
			
		||||
                                    <span>
 | 
			
		||||
                                        Please remove feature dependencies
 | 
			
		||||
                                        first.
 | 
			
		||||
                                    </span>
 | 
			
		||||
                                </p>
 | 
			
		||||
                            }
 | 
			
		||||
                        />
 | 
			
		||||
                        <ConditionallyRender
 | 
			
		||||
                            condition={!hasSameEnvironments}
 | 
			
		||||
                            show={
 | 
			
		||||
 | 
			
		||||
@ -4,4 +4,5 @@ export interface IDependentFeaturesReadModel {
 | 
			
		||||
    getChildren(parents: string[]): Promise<string[]>;
 | 
			
		||||
    getParents(child: string): Promise<IDependency[]>;
 | 
			
		||||
    getParentOptions(child: string): Promise<string[]>;
 | 
			
		||||
    hasDependencies(feature: string): Promise<boolean>;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -49,4 +49,13 @@ export class DependentFeaturesReadModel implements IDependentFeaturesReadModel {
 | 
			
		||||
 | 
			
		||||
        return rows.map((item) => item.name);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    async hasDependencies(feature: string): Promise<boolean> {
 | 
			
		||||
        const parents = await this.db('dependent_features')
 | 
			
		||||
            .where('parent', feature)
 | 
			
		||||
            .orWhere('child', feature)
 | 
			
		||||
            .limit(1);
 | 
			
		||||
 | 
			
		||||
        return parents.length > 0;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -15,4 +15,8 @@ export class FakeDependentFeaturesReadModel
 | 
			
		||||
    getParentOptions(): Promise<string[]> {
 | 
			
		||||
        return Promise.resolve([]);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    hasDependencies(): Promise<boolean> {
 | 
			
		||||
        return Promise.resolve(false);
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -1732,6 +1732,13 @@ class FeatureToggleService {
 | 
			
		||||
                `Changing project not allowed. Project ${newProject} has change requests enabled.`,
 | 
			
		||||
            );
 | 
			
		||||
        }
 | 
			
		||||
        if (
 | 
			
		||||
            await this.dependentFeaturesReadModel.hasDependencies(featureName)
 | 
			
		||||
        ) {
 | 
			
		||||
            throw new ForbiddenError(
 | 
			
		||||
                'Changing project not allowed. Feature has dependencies.',
 | 
			
		||||
            );
 | 
			
		||||
        }
 | 
			
		||||
        const feature = await this.featureToggleStore.get(featureName);
 | 
			
		||||
        const oldProject = feature.project;
 | 
			
		||||
        feature.project = newProject;
 | 
			
		||||
 | 
			
		||||
@ -27,6 +27,7 @@ import supertest from 'supertest';
 | 
			
		||||
import { randomId } from '../../../../../lib/util/random-id';
 | 
			
		||||
import { DEFAULT_PROJECT } from '../../../../../lib/types';
 | 
			
		||||
import { FeatureStrategySchema, SetStrategySortOrderSchema } from 'lib/openapi';
 | 
			
		||||
import { ForbiddenError } from '../../../../../lib/error';
 | 
			
		||||
 | 
			
		||||
let app: IUnleashTest;
 | 
			
		||||
let db: ITestDb;
 | 
			
		||||
@ -241,6 +242,36 @@ test('should list dependencies and children', async () => {
 | 
			
		||||
    });
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
test('should not allow to change project with dependencies', async () => {
 | 
			
		||||
    const parent = uuidv4();
 | 
			
		||||
    const child = uuidv4();
 | 
			
		||||
    await app.createFeature(parent, 'default');
 | 
			
		||||
    await app.createFeature(child, 'default');
 | 
			
		||||
    await app.addDependency(child, parent);
 | 
			
		||||
    const user = new ApiUser({
 | 
			
		||||
        tokenName: 'project-changer',
 | 
			
		||||
        permissions: ['ADMIN'],
 | 
			
		||||
        project: '*',
 | 
			
		||||
        type: ApiTokenType.ADMIN,
 | 
			
		||||
        environment: '*',
 | 
			
		||||
        secret: 'a',
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    await expect(async () =>
 | 
			
		||||
        app.services.projectService.changeProject(
 | 
			
		||||
            'default',
 | 
			
		||||
            child,
 | 
			
		||||
            // @ts-ignore
 | 
			
		||||
            user,
 | 
			
		||||
            'default',
 | 
			
		||||
        ),
 | 
			
		||||
    ).rejects.toThrow(
 | 
			
		||||
        new ForbiddenError(
 | 
			
		||||
            'Changing project not allowed. Feature has dependencies.',
 | 
			
		||||
        ),
 | 
			
		||||
    );
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
test('Should not allow to archive/delete feature with children', async () => {
 | 
			
		||||
    const parent = uuidv4();
 | 
			
		||||
    const child = uuidv4();
 | 
			
		||||
 | 
			
		||||
		Loading…
	
		Reference in New Issue
	
	Block a user