diff --git a/packages/aws-cdk/lib/commands/flag-operations.ts b/packages/aws-cdk/lib/commands/flag-operations.ts index 334be5740..d2128afd9 100644 --- a/packages/aws-cdk/lib/commands/flag-operations.ts +++ b/packages/aws-cdk/lib/commands/flag-operations.ts @@ -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; } @@ -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; diff --git a/packages/aws-cdk/test/commands/flag-operations.test.ts b/packages/aws-cdk/test/commands/flag-operations.test.ts index 01c4f1fc6..a8e4c8b83 100644 --- a/packages/aws-cdk/test/commands/flag-operations.test.ts +++ b/packages/aws-cdk/test/commands/flag-operations.test.ts @@ -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, @@ -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', () => {