1
0
mirror of https://github.com/Unleash/unleash.git synced 2024-12-22 19:07:54 +01:00

feat: disallow clone toggle on change request enabled (#3383)

This commit is contained in:
Mateusz Kwasniewski 2023-03-27 13:21:50 +02:00 committed by GitHub
parent 663f26b712
commit 2caab45801
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 81 additions and 14 deletions

View File

@ -18,6 +18,7 @@ import { getTogglePath } from 'utils/routePathHelpers';
import useFeatureApi from 'hooks/api/actions/useFeatureApi/useFeatureApi';
import { useFeature } from 'hooks/api/getters/useFeature/useFeature';
import { useRequiredPathParam } from 'hooks/useRequiredPathParam';
import { useChangeRequestsEnabled } from '../../../hooks/useChangeRequestsEnabled';
const StyledPage = styled(Paper)(({ theme }) => ({
overflow: 'visible',
@ -65,6 +66,8 @@ export const CopyFeatureToggle = () => {
const projectId = useRequiredPathParam('projectId');
const { feature } = useFeature(projectId, featureId);
const navigate = useNavigate();
const { isChangeRequestConfiguredInAnyEnv } =
useChangeRequestsEnabled(projectId);
const setValue: ChangeEventHandler<HTMLInputElement> = event => {
const value = trim(event.target.value);
@ -152,7 +155,12 @@ export const CopyFeatureToggle = () => {
label="Replace groupId"
/>
<Button type="submit" color="primary" variant="contained">
<Button
type="submit"
color="primary"
variant="contained"
disabled={isChangeRequestConfiguredInAnyEnv()}
>
<FileCopy />
&nbsp;&nbsp;&nbsp; Create from copy
</Button>

View File

@ -10,4 +10,5 @@ export interface IChangeRequestAccessReadModel {
project: string,
environment: string,
): Promise<boolean>;
isChangeRequestsEnabledForProject(project: string): Promise<boolean>;
}

View File

@ -19,4 +19,8 @@ export class FakeChangeRequestAccessReadModel
public async isChangeRequestsEnabled(): Promise<boolean> {
return this.isChangeRequestEnabled;
}
public async isChangeRequestsEnabledForProject(): Promise<boolean> {
return this.isChangeRequestEnabled;
}
}

View File

@ -49,4 +49,18 @@ export class ChangeRequestAccessReadModel
const { present } = result.rows[0];
return present;
}
public async isChangeRequestsEnabledForProject(
project: string,
): Promise<boolean> {
const result = await this.db.raw(
`SELECT EXISTS(SELECT 1
FROM change_request_settings
WHERE project = ?
) AS present`,
[project],
);
const { present } = result.rows[0];
return present;
}
}

View File

@ -882,6 +882,15 @@ class FeatureToggleService {
replaceGroupId: boolean = true, // eslint-disable-line
userName: string,
): Promise<FeatureToggle> {
const changeRequestEnabled =
await this.changeRequestAccessReadModel.isChangeRequestsEnabledForProject(
projectId,
);
if (changeRequestEnabled) {
throw new NoAccessError(
`Cloning not allowed. Project ${projectId} has change requests enabled.`,
);
}
this.logger.info(
`${userName} clones feature toggle ${featureName} to ${newFeatureName}`,
);
@ -1754,7 +1763,7 @@ class FeatureToggleService {
project: string,
environment: string,
featureName: string,
user: User,
user?: User,
) {
const hasEnvironment =
await this.featureEnvironmentStore.featureHasEnvironment(

View File

@ -83,14 +83,18 @@ const updateStrategy = async (
beforeAll(async () => {
db = await dbInit('feature_strategy_api_serial', getLogger);
app = await setupAppWithCustomConfig(db.stores, {
experimental: {
flags: {
strictSchemaValidation: true,
bulkOperations: true,
app = await setupAppWithCustomConfig(
db.stores,
{
experimental: {
flags: {
strictSchemaValidation: true,
bulkOperations: true,
},
},
},
});
db.rawDatabase,
);
});
afterEach(async () => {

View File

@ -29,6 +29,8 @@ const mockConstraints = (): IConstraint[] => {
}));
};
const irrelevantDate = new Date();
beforeAll(async () => {
const config = createTestConfig();
db = await dbInit(
@ -54,9 +56,14 @@ beforeAll(async () => {
});
afterAll(async () => {
await db.rawDatabase('change_request_settings').del();
await db.destroy();
});
beforeEach(async () => {
await db.rawDatabase('change_request_settings').del();
});
test('Should create feature toggle strategy configuration', async () => {
const projectId = 'default';
const username = 'feature-toggle';
@ -263,7 +270,7 @@ test('adding and removing an environment preserves variants when variants per en
const toggle = await service.getFeature({
featureName,
projectId: null,
projectId: undefined,
environmentVariants: false,
});
expect(toggle.variants).toHaveLength(1);
@ -327,6 +334,26 @@ test('cloning a feature toggle copies variant environments correctly', async ()
expect(newEnv.variants).toHaveLength(1);
});
test('cloning a feature toggle not allowed for change requests enabled', async () => {
await db.rawDatabase('change_request_settings').insert({
project: 'default',
environment: 'default',
});
await expect(
service.cloneFeatureToggle(
'newToggleName',
'default',
'clonedToggleName',
true,
'test-user',
),
).rejects.toEqual(
new NoAccessError(
`Cloning not allowed. Project default has change requests enabled.`,
),
);
});
test('Cloning a feature toggle also clones segments correctly', async () => {
const featureName = 'ToggleWithSegments';
const clonedFeatureName = 'AWholeNewFeatureToggle';
@ -372,7 +399,7 @@ test('Cloning a feature toggle also clones segments correctly', async () => {
let feature = await service.getFeature({ featureName: clonedFeatureName });
expect(
feature.environments.find((x) => x.name === 'default').strategies[0]
feature.environments.find((x) => x.name === 'default')?.strategies[0]
.segments,
).toHaveLength(1);
});
@ -425,14 +452,14 @@ test('If change requests are enabled, cannot change variants without going via C
'default',
[newVariant],
{
createdAt: undefined,
createdAt: irrelevantDate,
email: '',
id: 0,
imageUrl: '',
loginAttempts: 0,
name: '',
permissions: [],
seenAt: undefined,
seenAt: irrelevantDate,
username: '',
generateImageUrl(): string {
return '';
@ -532,14 +559,14 @@ test('If CRs are protected for any environment in the project stops bulk update
[enabledEnv.name, disabledEnv.name],
newVariants,
{
createdAt: undefined,
createdAt: irrelevantDate,
email: '',
id: 0,
imageUrl: '',
loginAttempts: 0,
name: '',
permissions: [],
seenAt: undefined,
seenAt: irrelevantDate,
username: '',
generateImageUrl(): string {
return '';