Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion packages/aws-cdk/lib/commands/flag-operations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -128,7 +128,7 @@ export async function handleFlags(flagData: FeatureFlag[], ioHelper: IoHelper, o
}

if (options.unconfigured && options.FLAGNAME) {
await ioHelper.defaults.error('Error: Cannot use --unconfigured with a specific flag name. --unconfigured works on multiple flags.');
await ioHelper.defaults.error('Error: Cannot use --unconfigured with a specific flag name. --unconfigured works with multiple flags.');
return;
}

Expand All @@ -147,6 +147,11 @@ export async function handleFlags(flagData: FeatureFlag[], ioHelper: IoHelper, o
return;
}

if (options.set && !options.all && !options.unconfigured && !options.FLAGNAME) {
await ioHelper.defaults.error('Error: When using --set, you must specify either --all, --unconfigured, or provide a specific flag name.');
return;
}

if (options.FLAGNAME && !options.set && !options.value) {
await displayFlags(params);
return;
Expand Down
351 changes: 351 additions & 0 deletions packages/aws-cdk/test/commands/flag-operations.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -189,6 +189,68 @@ describe('displayFlags', () => {
expect(plainTextOutput).toContain('different-module');
});

test('sorts flags by module name and then by flag name within module', async () => {
// This test targets the sorting logic in displayFlagTable:
// if (a.module !== b.module) { return a.module.localeCompare(b.module); }
// return a.name.localeCompare(b.name);

const flagsForSortingTest: FeatureFlag[] = [
{
module: 'z-module',
name: '@aws-cdk/z:flagB',
recommendedValue: 'true',
userValue: undefined,
explanation: 'Flag B in Z module',
},
{
module: 'a-module',
name: '@aws-cdk/a:flagZ',
recommendedValue: 'true',
userValue: undefined,
explanation: 'Flag Z in A module',
},
{
module: 'a-module',
name: '@aws-cdk/a:flagA',
recommendedValue: 'true',
userValue: undefined,
explanation: 'Flag A in A module',
},
{
module: 'z-module',
name: '@aws-cdk/z:flagA',
recommendedValue: 'true',
userValue: undefined,
explanation: 'Flag A in Z module',
},
];

const params = {
flagData: flagsForSortingTest,
toolkit: mockToolkit,
ioHelper,
all: true,
};
await displayFlags(params);

const plainTextOutput = output();

// Verify that modules are sorted alphabetically (a-module before z-module)
const aModuleIndex = plainTextOutput.indexOf('Module: a-module');
const zModuleIndex = plainTextOutput.indexOf('Module: z-module');
expect(aModuleIndex).toBeLessThan(zModuleIndex);

// Verify that within a-module, flags are sorted alphabetically (flagA before flagZ)
const flagAIndex = plainTextOutput.indexOf('@aws-cdk/a:flagA');
const flagZIndex = plainTextOutput.indexOf('@aws-cdk/a:flagZ');
expect(flagAIndex).toBeLessThan(flagZIndex);

// Verify that within z-module, flags are sorted alphabetically (flagA before flagB)
const zFlagAIndex = plainTextOutput.indexOf('@aws-cdk/z:flagA');
const zFlagBIndex = plainTextOutput.indexOf('@aws-cdk/z:flagB');
expect(zFlagAIndex).toBeLessThan(zFlagBIndex);
});

test('does not display flag when unconfigured behavior is the same as recommended behavior', async () => {
const params = {
flagData: mockFlagsData,
Expand Down Expand Up @@ -491,6 +553,295 @@ describe('handleFlags', () => {
const plainTextOutput = output();
expect(plainTextOutput).toContain('The \'cdk flags\' command is not compatible with the AWS CDK library used by your application. Please upgrade to 2.212.0 or above.');
});

test('shows error when --set is used without required options', async () => {
const options: FlagsOptions = {
set: true,
};

await handleFlags(mockFlagsData, ioHelper, options, mockToolkit);

const plainTextOutput = output();
expect(plainTextOutput).toContain('Error: When using --set, you must specify either --all, --unconfigured, or provide a specific flag name.');
});

test('shows error when --set is used with --recommended but no target flags', async () => {
const options: FlagsOptions = {
set: true,
recommended: true,
};

await handleFlags(mockFlagsData, ioHelper, options, mockToolkit);

const plainTextOutput = output();
expect(plainTextOutput).toContain('Error: When using --set, you must specify either --all, --unconfigured, or provide a specific flag name.');
});

test('shows error when using both --all and a specific flag name', async () => {
const options: FlagsOptions = {
FLAGNAME: ['@aws-cdk/core:testFlag'],
all: true,
};

await handleFlags(mockFlagsData, ioHelper, options, mockToolkit);

const plainTextOutput = output();
expect(plainTextOutput).toContain('Error: Cannot use both --all and a specific flag name. Please use either --all to show all flags or specify a single flag name.');
});

test('shows error when using options without --set', async () => {
const options: FlagsOptions = {
value: 'true',
};

await handleFlags(mockFlagsData, ioHelper, options, mockToolkit);

const plainTextOutput = output();
expect(plainTextOutput).toContain('Error: This option can only be used with --set.');
});

test('shows error when using --value without a specific flag name', async () => {
const options: FlagsOptions = {
value: 'true',
set: true,
};

await handleFlags(mockFlagsData, ioHelper, options, mockToolkit);

const plainTextOutput = output();
expect(plainTextOutput).toContain('Error: --value requires a specific flag name. Please specify a flag name when providing a value.');
});

test('shows error when using both --recommended and --default', async () => {
const options: FlagsOptions = {
recommended: true,
default: true,
set: true,
all: true,
};

await handleFlags(mockFlagsData, ioHelper, options, mockToolkit);

const plainTextOutput = output();
expect(plainTextOutput).toContain('Error: Cannot use both --recommended and --default. Please choose one option.');
});

test('shows error when using both --unconfigured and --all', async () => {
const options: FlagsOptions = {
set: true,
unconfigured: true,
all: true,
};

await handleFlags(mockFlagsData, ioHelper, options, mockToolkit);

const plainTextOutput = output();
expect(plainTextOutput).toContain('Error: Cannot use both --unconfigured and --all. Please choose one option.');
});

test('shows error when using both --unconfigured and a specific flag name', async () => {
const options: FlagsOptions = {
set: true,
unconfigured: true,
FLAGNAME: ['@aws-cdk/core:testFlag'],
};

await handleFlags(mockFlagsData, ioHelper, options, mockToolkit);

const plainTextOutput = output();
expect(plainTextOutput).toContain('Error: Cannot use --unconfigured with a specific flag name. --unconfigured works with multiple flags.');
});

test('shows error when setting a flag without providing a value', async () => {
const options: FlagsOptions = {
set: true,
FLAGNAME: ['@aws-cdk/core:testFlag'],
};

await handleFlags(mockFlagsData, ioHelper, options, mockToolkit);

const plainTextOutput = output();
expect(plainTextOutput).toContain('Error: When setting a specific flag, you must provide a --value.');
});

test('shows error when using --set with --all without --recommended or --default', async () => {
const options: FlagsOptions = {
set: true,
all: true,
};

await handleFlags(mockFlagsData, ioHelper, options, mockToolkit);

const plainTextOutput = output();
expect(plainTextOutput).toContain('Error: When using --set with --all, you must specify either --recommended or --default.');
});

test('shows error when using --set with --unconfigured without --recommended or --default', async () => {
const options: FlagsOptions = {
set: true,
unconfigured: true,
};

await handleFlags(mockFlagsData, ioHelper, options, mockToolkit);

const plainTextOutput = output();
expect(plainTextOutput).toContain('Error: When using --set with --unconfigured, you must specify either --recommended or --default.');
});

test('shows error when trying to set a flag that does not exist', async () => {
const options: FlagsOptions = {
set: true,
FLAGNAME: ['@aws-cdk/core:nonExistentFlag'],
value: 'true',
};

await handleFlags(mockFlagsData, ioHelper, options, mockToolkit);

const plainTextOutput = output();
expect(plainTextOutput).toContain('Flag not found.');
});

test('calls setMultipleFlagsIfSupported when using --set with --unconfigured and --default', async () => {
const flagsWithUnconfiguredBehavior: FeatureFlag[] = [
{
module: 'aws-cdk-lib',
name: '@aws-cdk/core:flagWithV2True',
recommendedValue: 'false',
userValue: undefined,
explanation: 'Flag with unconfiguredBehavesLike.v2 = true',
unconfiguredBehavesLike: { v2: 'true' },
},
{
module: 'aws-cdk-lib',
name: '@aws-cdk/core:flagWithV2False',
recommendedValue: 'false',
userValue: undefined,
explanation: 'Flag with unconfiguredBehavesLike.v2 = false',
unconfiguredBehavesLike: { v2: 'false' },
},
];

const cdkJsonPath = await createCdkJsonFile({});

setupMockToolkitForPrototyping(mockToolkit);

const requestResponseSpy = jest.spyOn(ioHelper, 'requestResponse');
requestResponseSpy.mockResolvedValue(true);

const options: FlagsOptions = {
set: true,
unconfigured: true,
default: true,
};

await handleFlags(flagsWithUnconfiguredBehavior, ioHelper, options, mockToolkit);

// Verify that the prototyping process was called (indicating setMultipleFlagsIfSupported was executed)
expect(mockToolkit.fromCdkApp).toHaveBeenCalled();
expect(mockToolkit.synth).toHaveBeenCalled();
expect(mockToolkit.diff).toHaveBeenCalled();
expect(requestResponseSpy).toHaveBeenCalled();

// Verify that the flags were set to their default values based on unconfiguredBehavesLike.v2
const updatedContent = await fs.promises.readFile(cdkJsonPath, 'utf-8');
const updatedJson = JSON.parse(updatedContent);

expect(updatedJson.context['@aws-cdk/core:flagWithV2True']).toBe(true);
expect(updatedJson.context['@aws-cdk/core:flagWithV2False']).toBe(false);

await cleanupCdkJsonFile(cdkJsonPath);
requestResponseSpy.mockRestore();
});

test('handles boolean flag values correctly in toBooleanValue function', async () => {
const flagsWithBooleanRecommendedValues: FeatureFlag[] = [
{
module: 'aws-cdk-lib',
name: '@aws-cdk/core:booleanTrueFlag',
recommendedValue: true,
userValue: undefined,
explanation: 'Flag with boolean true recommended value',
},
{
module: 'aws-cdk-lib',
name: '@aws-cdk/core:booleanFalseFlag',
recommendedValue: false,
userValue: undefined,
explanation: 'Flag with boolean false recommended value',
},
];

const cdkJsonPath = await createCdkJsonFile({});

setupMockToolkitForPrototyping(mockToolkit);

const requestResponseSpy = jest.spyOn(ioHelper, 'requestResponse');
requestResponseSpy.mockResolvedValue(true);

const options: FlagsOptions = {
set: true,
all: true,
recommended: true,
};

await handleFlags(flagsWithBooleanRecommendedValues, ioHelper, options, mockToolkit);

// Verify that the flags were set correctly using boolean values
const updatedContent = await fs.promises.readFile(cdkJsonPath, 'utf-8');
const updatedJson = JSON.parse(updatedContent);

// These should be set to their boolean recommended values, testing the toBooleanValue boolean branch
expect(updatedJson.context['@aws-cdk/core:booleanTrueFlag']).toBe(true);
expect(updatedJson.context['@aws-cdk/core:booleanFalseFlag']).toBe(false);

await cleanupCdkJsonFile(cdkJsonPath);
requestResponseSpy.mockRestore();
});

test('shows error when flag is not found during prototypeChanges', async () => {
// This test targets the validation in prototypeChanges function:
// if (!flag) { await ioHelper.defaults.error(`Flag ${flagName} not found.`); return false; }

const cdkJsonPath = await createCdkJsonFile({});

setupMockToolkitForPrototyping(mockToolkit);

// Create a scenario where we try to set multiple flags but one doesn't exist
// We'll mock the internal flag lookup to simulate a missing flag during the prototyping process
const originalFind = Array.prototype.find;
let findCallCount = 0;

// Mock Array.find to return undefined for the second call (simulating missing flag in prototypeChanges)
Array.prototype.find = function(this: any[], callback: any) {
findCallCount++;
// First call is in handleFlags validation (should find the flag)
// Second call is in prototypeChanges (should not find the flag to trigger our test case)
if (findCallCount === 2) {
return undefined; // Simulate flag not found in prototypeChanges
}
return originalFind.call(this, callback);
};

const options: FlagsOptions = {
set: true,
all: true,
recommended: true,
};

await handleFlags(mockFlagsData, ioHelper, options, mockToolkit);

const plainTextOutput = output();
expect(plainTextOutput).toContain('Flag @aws-cdk/s3:anotherFlag not found.');

// Verify that prototyping was attempted but failed due to missing flag
expect(mockToolkit.fromCdkApp).toHaveBeenCalledTimes(1); // Only the initial call, not the modified one
expect(mockToolkit.diff).not.toHaveBeenCalled(); // Diff should not be called due to early return

// Restore original Array.find
Array.prototype.find = originalFind;

await cleanupCdkJsonFile(cdkJsonPath);
});
});

describe('modifyValues', () => {
Expand Down
Loading