From cc3336417a6dd1ece56a201149d688706e723851 Mon Sep 17 00:00:00 2001 From: Garrett Beatty Date: Tue, 12 Nov 2024 14:51:27 -0500 Subject: [PATCH 1/5] Update platform query logic to support deprecated (#882) --- .../changes/721a3463-3ad9-413d-b2b6-11efb7575fac.json | 11 +++++++++++ .../CDK/CDKBootstrapTemplate.yaml | 8 ++++++-- .../Data/AWSResourceQueryer.cs | 2 +- 3 files changed, 18 insertions(+), 3 deletions(-) create mode 100644 .autover/changes/721a3463-3ad9-413d-b2b6-11efb7575fac.json diff --git a/.autover/changes/721a3463-3ad9-413d-b2b6-11efb7575fac.json b/.autover/changes/721a3463-3ad9-413d-b2b6-11efb7575fac.json new file mode 100644 index 00000000..dd6c6fea --- /dev/null +++ b/.autover/changes/721a3463-3ad9-413d-b2b6-11efb7575fac.json @@ -0,0 +1,11 @@ +{ + "Projects": [ + { + "Name": "AWS.Deploy.CLI", + "Type": "Patch", + "ChangelogMessages": [ + "Update beanstalk platform resolution logic to additionally use 'Deprecated' versions in order to continue supporting .NET 6." + ] + } + ] +} \ No newline at end of file diff --git a/src/AWS.Deploy.Orchestration/CDK/CDKBootstrapTemplate.yaml b/src/AWS.Deploy.Orchestration/CDK/CDKBootstrapTemplate.yaml index 205243a2..f5a5db52 100644 --- a/src/AWS.Deploy.Orchestration/CDK/CDKBootstrapTemplate.yaml +++ b/src/AWS.Deploy.Orchestration/CDK/CDKBootstrapTemplate.yaml @@ -203,7 +203,11 @@ Resources: - Id: CleanupOldVersions Status: Enabled NoncurrentVersionExpiration: - NoncurrentDays: 365 + NoncurrentDays: 30 + - Id: AbortIncompleteMultipartUploads + Status: Enabled + AbortIncompleteMultipartUpload: + DaysAfterInitiation: 1 UpdateReplacePolicy: Delete DeletionPolicy: Delete StagingBucketPolicy: @@ -611,7 +615,7 @@ Resources: Type: String Name: Fn::Sub: /cdk-bootstrap/${Qualifier}/version - Value: "23" + Value: "25" Outputs: BucketName: Description: The name of the S3 bucket owned by the CDK toolkit stack diff --git a/src/AWS.Deploy.Orchestration/Data/AWSResourceQueryer.cs b/src/AWS.Deploy.Orchestration/Data/AWSResourceQueryer.cs index ea9fdf6d..7445cb4f 100644 --- a/src/AWS.Deploy.Orchestration/Data/AWSResourceQueryer.cs +++ b/src/AWS.Deploy.Orchestration/Data/AWSResourceQueryer.cs @@ -574,7 +574,7 @@ public async Task> GetElasticBeanstalkPlatformArns(string? if (string.IsNullOrEmpty(version.PlatformCategory) || string.IsNullOrEmpty(version.PlatformBranchLifecycleState)) continue; - if (!version.PlatformBranchLifecycleState.Equals("Supported")) + if (!(version.PlatformBranchLifecycleState.Equals("Supported") || version.PlatformBranchLifecycleState.Equals("Deprecated"))) continue; platformVersions.Add(version); From e810ffea3065828a48cb6a9e9ff1abc6bcc89179 Mon Sep 17 00:00:00 2001 From: Garrett Beatty Date: Tue, 12 Nov 2024 15:36:09 -0500 Subject: [PATCH 2/5] Update logic to read the region when reading non default profiles (#880) --- .../ebd7f49a-72cd-4c5b-83f4-2790a2560e94.json | 11 + src/AWS.Deploy.CLI/AWSCredentialsFactory.cs | 17 + src/AWS.Deploy.CLI/AWSUtilities.cs | 53 +- src/AWS.Deploy.CLI/Commands/CommandFactory.cs | 12 +- .../CredentialProfileStoreChainFactory.cs | 17 + .../CustomServiceCollectionExtension.cs | 3 + src/AWS.Deploy.CLI/IAWSCredentialsFactory.cs | 18 + .../ICredentialProfileStoreChainFactory.cs | 18 + .../ISharedCredentialsFileFactory.cs | 18 + .../SharedCredentialsFileFactory.cs | 17 + .../AWSCredentialsFactoryTests.cs | 25 + .../AWSUtilitiesTests.cs | 277 +++++++++ .../Commands/CommandFactoryTest.cs | 576 ++++++++++++++++++ ...CredentialProfileStoreChainFactoryTests.cs | 25 + .../SharedCredentialsFileFactoryTests.cs | 25 + 15 files changed, 1092 insertions(+), 20 deletions(-) create mode 100644 .autover/changes/ebd7f49a-72cd-4c5b-83f4-2790a2560e94.json create mode 100644 src/AWS.Deploy.CLI/AWSCredentialsFactory.cs create mode 100644 src/AWS.Deploy.CLI/CredentialProfileStoreChainFactory.cs create mode 100644 src/AWS.Deploy.CLI/IAWSCredentialsFactory.cs create mode 100644 src/AWS.Deploy.CLI/ICredentialProfileStoreChainFactory.cs create mode 100644 src/AWS.Deploy.CLI/ISharedCredentialsFileFactory.cs create mode 100644 src/AWS.Deploy.CLI/SharedCredentialsFileFactory.cs create mode 100644 test/AWS.Deploy.CLI.UnitTests/AWSCredentialsFactoryTests.cs create mode 100644 test/AWS.Deploy.CLI.UnitTests/AWSUtilitiesTests.cs create mode 100644 test/AWS.Deploy.CLI.UnitTests/Commands/CommandFactoryTest.cs create mode 100644 test/AWS.Deploy.CLI.UnitTests/CredentialProfileStoreChainFactoryTests.cs create mode 100644 test/AWS.Deploy.CLI.UnitTests/SharedCredentialsFileFactoryTests.cs diff --git a/.autover/changes/ebd7f49a-72cd-4c5b-83f4-2790a2560e94.json b/.autover/changes/ebd7f49a-72cd-4c5b-83f4-2790a2560e94.json new file mode 100644 index 00000000..df1f60a8 --- /dev/null +++ b/.autover/changes/ebd7f49a-72cd-4c5b-83f4-2790a2560e94.json @@ -0,0 +1,11 @@ +{ + "Projects": [ + { + "Name": "AWS.Deploy.CLI", + "Type": "Minor", + "ChangelogMessages": [ + "Read region value for non default profiles" + ] + } + ] +} \ No newline at end of file diff --git a/src/AWS.Deploy.CLI/AWSCredentialsFactory.cs b/src/AWS.Deploy.CLI/AWSCredentialsFactory.cs new file mode 100644 index 00000000..d9ac3522 --- /dev/null +++ b/src/AWS.Deploy.CLI/AWSCredentialsFactory.cs @@ -0,0 +1,17 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +using Amazon.Runtime; + +namespace AWS.Deploy.CLI +{ + /// + public class AWSCredentialsFactory : IAWSCredentialsFactory + { + /// + public AWSCredentials Create() + { + return FallbackCredentialsFactory.GetCredentials(); + } + } +} diff --git a/src/AWS.Deploy.CLI/AWSUtilities.cs b/src/AWS.Deploy.CLI/AWSUtilities.cs index 9a337657..997773e3 100644 --- a/src/AWS.Deploy.CLI/AWSUtilities.cs +++ b/src/AWS.Deploy.CLI/AWSUtilities.cs @@ -6,7 +6,6 @@ using System.Linq; using System.Threading.Tasks; using Amazon.Runtime; -using Amazon.Runtime.CredentialManagement; using AWS.Deploy.CLI.Utilities; using AWS.Deploy.Common; using AWS.Deploy.Common.IO; @@ -17,7 +16,7 @@ namespace AWS.Deploy.CLI { public interface IAWSUtilities { - Task ResolveAWSCredentials(string? profileName); + Task> ResolveAWSCredentials(string? profileName); string ResolveAWSRegion(string? region, string? lastRegionUsed = null); } @@ -28,27 +27,43 @@ public class AWSUtilities : IAWSUtilities private readonly IDirectoryManager _directoryManager; private readonly IOptionSettingHandler _optionSettingHandler; private readonly IServiceProvider _serviceProvider; + private readonly ICredentialProfileStoreChainFactory _credentialChainFactory; + private readonly ISharedCredentialsFileFactory _sharedCredentialsFileFactory; + private readonly IAWSCredentialsFactory _awsCredentialsFactory; public AWSUtilities( IServiceProvider serviceProvider, IToolInteractiveService toolInteractiveService, IConsoleUtilities consoleUtilities, IDirectoryManager directoryManager, - IOptionSettingHandler optionSettingHandler) + IOptionSettingHandler optionSettingHandler, + ICredentialProfileStoreChainFactory credentialChainFactory, + ISharedCredentialsFileFactory sharedCredentialsFileFactory, + IAWSCredentialsFactory awsCredentialsFactory) { _serviceProvider = serviceProvider; _toolInteractiveService = toolInteractiveService; _consoleUtilities = consoleUtilities; _directoryManager = directoryManager; _optionSettingHandler = optionSettingHandler; + _credentialChainFactory = credentialChainFactory; + _sharedCredentialsFileFactory = sharedCredentialsFileFactory; + _awsCredentialsFactory = awsCredentialsFactory; } - public async Task ResolveAWSCredentials(string? profileName) + + /// + /// At a high level there are 2 possible return values for this function: + /// 1. In this case, both the credentials and region were able to be read from the profile. + /// 2. : In this case, the region was not able to be read from the profile, so we return null for it. The null case will be handled later on by . + /// + public async Task> ResolveAWSCredentials(string? profileName) { - async Task Resolve() + async Task> Resolve() { - var chain = new CredentialProfileStoreChain(); + var chain = _credentialChainFactory.Create(); + // Use provided profile to read credentials if (!string.IsNullOrEmpty(profileName)) { if (chain.TryGetAWSCredentials(profileName, out var profileCredentials) && @@ -56,7 +71,10 @@ async Task Resolve() (profileCredentials is AssumeRoleAWSCredentials || await CanLoadCredentials(profileCredentials))) { _toolInteractiveService.WriteLine($"Configuring AWS Credentials from Profile {profileName}."); - return profileCredentials; + chain.TryGetProfile(profileName, out var profile); + // Return the credentials since they must be found at this point. + // For region, we try to read it from the profile. If it's not found in the profile, then return null and region selection will be handled later on by ResolveAWSRegion. + return Tuple.Create(profileCredentials, profile.Region?.SystemName); } else { @@ -65,14 +83,17 @@ async Task Resolve() } } + // Use default credentials try { - var fallbackCredentials = FallbackCredentialsFactory.GetCredentials(); + var fallbackCredentials = _awsCredentialsFactory.Create(); if (await CanLoadCredentials(fallbackCredentials)) { + // Always return the credentials since they must be found at this point. + // For region, we return null here, since it will read from default region in ResolveAWSRegion _toolInteractiveService.WriteLine("Configuring AWS Credentials using AWS SDK credential search."); - return fallbackCredentials; + return Tuple.Create(fallbackCredentials, null); } } catch (AmazonServiceException ex) @@ -82,7 +103,8 @@ async Task Resolve() _toolInteractiveService.WriteDebugLine(ex.PrettyPrint()); } - var sharedCredentials = new SharedCredentialsFile(); + // Use Shared Credentials + var sharedCredentials = _sharedCredentialsFileFactory.Create(); if (sharedCredentials.ListProfileNames().Count == 0) { throw new NoAWSCredentialsFoundException(DeployToolErrorCode.UnableToResolveAWSCredentials, "Unable to resolve AWS credentials to access AWS."); @@ -93,21 +115,24 @@ async Task Resolve() if (chain.TryGetAWSCredentials(selectedProfileName, out var selectedProfileCredentials) && (await CanLoadCredentials(selectedProfileCredentials))) { - return selectedProfileCredentials; + // Return the credentials since they must be found at this point. + // For region, we try to read it from the profile. If it's not found in the profile, then return null and region selection will be handled later on by ResolveAWSRegion. + chain.TryGetProfile(selectedProfileName, out var profile); + return Tuple.Create(selectedProfileCredentials, profile.Region?.SystemName); } throw new NoAWSCredentialsFoundException(DeployToolErrorCode.UnableToCreateAWSCredentials, $"Unable to create AWS credentials for profile {selectedProfileName}."); } - var credentials = await Resolve(); + var credentialsAndRegion = await Resolve(); - if (credentials is AssumeRoleAWSCredentials assumeRoleAWSCredentials) + if (credentialsAndRegion.Item1 is AssumeRoleAWSCredentials assumeRoleAWSCredentials) { var assumeOptions = assumeRoleAWSCredentials.Options; assumeOptions.MfaTokenCodeCallback = ActivatorUtilities.CreateInstance(_serviceProvider, assumeOptions).Execute; } - return credentials; + return credentialsAndRegion; } private async Task CanLoadCredentials(AWSCredentials credentials) diff --git a/src/AWS.Deploy.CLI/Commands/CommandFactory.cs b/src/AWS.Deploy.CLI/Commands/CommandFactory.cs index 2240d8a1..5e30341e 100644 --- a/src/AWS.Deploy.CLI/Commands/CommandFactory.cs +++ b/src/AWS.Deploy.CLI/Commands/CommandFactory.cs @@ -209,8 +209,8 @@ private Command BuildDeployCommand() deploymentSettings = await _deploymentSettingsHandler.ReadSettings(applyPath); } - var awsCredentials = await _awsUtilities.ResolveAWSCredentials(input.Profile ?? deploymentSettings?.AWSProfile); - var awsRegion = _awsUtilities.ResolveAWSRegion(input.Region ?? deploymentSettings?.AWSRegion); + var (awsCredentials, regionFromProfile) = await _awsUtilities.ResolveAWSCredentials(input.Profile ?? deploymentSettings?.AWSProfile); + var awsRegion = _awsUtilities.ResolveAWSRegion(input.Region ?? deploymentSettings?.AWSRegion ?? regionFromProfile); _commandLineWrapper.RegisterAWSContext(awsCredentials, awsRegion); _awsClientFactory.RegisterAWSContext(awsCredentials, awsRegion); @@ -318,8 +318,8 @@ private Command BuildDeleteCommand() _toolInteractiveService.Diagnostics = input.Diagnostics; _toolInteractiveService.DisableInteractive = input.Silent; - var awsCredentials = await _awsUtilities.ResolveAWSCredentials(input.Profile); - var awsRegion = _awsUtilities.ResolveAWSRegion(input.Region); + var (awsCredentials, regionFromProfile) = await _awsUtilities.ResolveAWSCredentials(input.Profile); + var awsRegion = _awsUtilities.ResolveAWSRegion(input.Region ?? regionFromProfile); _awsClientFactory.ConfigureAWSOptions(awsOption => { @@ -401,8 +401,8 @@ private Command BuildListCommand() { _toolInteractiveService.Diagnostics = input.Diagnostics; - var awsCredentials = await _awsUtilities.ResolveAWSCredentials(input.Profile); - var awsRegion = _awsUtilities.ResolveAWSRegion(input.Region); + var (awsCredentials, regionFromProfile) = await _awsUtilities.ResolveAWSCredentials(input.Profile); + var awsRegion = _awsUtilities.ResolveAWSRegion(input.Region ?? regionFromProfile); _awsClientFactory.ConfigureAWSOptions(awsOptions => { diff --git a/src/AWS.Deploy.CLI/CredentialProfileStoreChainFactory.cs b/src/AWS.Deploy.CLI/CredentialProfileStoreChainFactory.cs new file mode 100644 index 00000000..4cf75c40 --- /dev/null +++ b/src/AWS.Deploy.CLI/CredentialProfileStoreChainFactory.cs @@ -0,0 +1,17 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +using Amazon.Runtime.CredentialManagement; + +namespace AWS.Deploy.CLI +{ + /// + public class CredentialProfileStoreChainFactory : ICredentialProfileStoreChainFactory + { + /// + public CredentialProfileStoreChain Create() + { + return new CredentialProfileStoreChain(); + } + } +} diff --git a/src/AWS.Deploy.CLI/Extensions/CustomServiceCollectionExtension.cs b/src/AWS.Deploy.CLI/Extensions/CustomServiceCollectionExtension.cs index fd3f7797..f19a7bea 100644 --- a/src/AWS.Deploy.CLI/Extensions/CustomServiceCollectionExtension.cs +++ b/src/AWS.Deploy.CLI/Extensions/CustomServiceCollectionExtension.cs @@ -71,6 +71,9 @@ public static void AddCustomServices(this IServiceCollection serviceCollection, serviceCollection.TryAdd(new ServiceDescriptor(typeof(IEnvironmentVariableManager), typeof(EnvironmentVariableManager), lifetime)); serviceCollection.TryAdd(new ServiceDescriptor(typeof(IDeployToolWorkspaceMetadata), typeof(DeployToolWorkspaceMetadata), lifetime)); serviceCollection.TryAdd(new ServiceDescriptor(typeof(IDeploymentSettingsHandler), typeof(DeploymentSettingsHandler), lifetime)); + serviceCollection.TryAdd(new ServiceDescriptor(typeof(ICredentialProfileStoreChainFactory), typeof(CredentialProfileStoreChainFactory), lifetime)); + serviceCollection.TryAdd(new ServiceDescriptor(typeof(ISharedCredentialsFileFactory), typeof(SharedCredentialsFileFactory), lifetime)); + serviceCollection.TryAdd(new ServiceDescriptor(typeof(IAWSCredentialsFactory), typeof(AWSCredentialsFactory), lifetime)); var packageJsonTemplate = typeof(PackageJsonGenerator).Assembly.ReadEmbeddedFile(PackageJsonGenerator.TemplateIdentifier); serviceCollection.TryAdd(new ServiceDescriptor(typeof(IPackageJsonGenerator), (serviceProvider) => new PackageJsonGenerator(packageJsonTemplate), lifetime)); diff --git a/src/AWS.Deploy.CLI/IAWSCredentialsFactory.cs b/src/AWS.Deploy.CLI/IAWSCredentialsFactory.cs new file mode 100644 index 00000000..2d2c9ed4 --- /dev/null +++ b/src/AWS.Deploy.CLI/IAWSCredentialsFactory.cs @@ -0,0 +1,18 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +using Amazon.Runtime; + +namespace AWS.Deploy.CLI +{ + /// + /// Represents a factory for creating + /// + public interface IAWSCredentialsFactory + { + /// + /// Creates + /// + AWSCredentials Create(); + } +} diff --git a/src/AWS.Deploy.CLI/ICredentialProfileStoreChainFactory.cs b/src/AWS.Deploy.CLI/ICredentialProfileStoreChainFactory.cs new file mode 100644 index 00000000..4cb1c7d6 --- /dev/null +++ b/src/AWS.Deploy.CLI/ICredentialProfileStoreChainFactory.cs @@ -0,0 +1,18 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +using Amazon.Runtime.CredentialManagement; + +namespace AWS.Deploy.CLI +{ + /// + /// Represents a factory for creating + /// + public interface ICredentialProfileStoreChainFactory + { + /// + /// Creates a + /// + CredentialProfileStoreChain Create(); + } +} diff --git a/src/AWS.Deploy.CLI/ISharedCredentialsFileFactory.cs b/src/AWS.Deploy.CLI/ISharedCredentialsFileFactory.cs new file mode 100644 index 00000000..b8152907 --- /dev/null +++ b/src/AWS.Deploy.CLI/ISharedCredentialsFileFactory.cs @@ -0,0 +1,18 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +using Amazon.Runtime.CredentialManagement; + +namespace AWS.Deploy.CLI +{ + /// + /// Represents a factory for creating + /// + public interface ISharedCredentialsFileFactory + { + /// + /// Creates + /// + SharedCredentialsFile Create(); + } +} diff --git a/src/AWS.Deploy.CLI/SharedCredentialsFileFactory.cs b/src/AWS.Deploy.CLI/SharedCredentialsFileFactory.cs new file mode 100644 index 00000000..eaf96f82 --- /dev/null +++ b/src/AWS.Deploy.CLI/SharedCredentialsFileFactory.cs @@ -0,0 +1,17 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +using Amazon.Runtime.CredentialManagement; + +namespace AWS.Deploy.CLI +{ + /// + public class SharedCredentialsFileFactory : ISharedCredentialsFileFactory + { + /// + public SharedCredentialsFile Create() + { + return new SharedCredentialsFile(); + } + } +} diff --git a/test/AWS.Deploy.CLI.UnitTests/AWSCredentialsFactoryTests.cs b/test/AWS.Deploy.CLI.UnitTests/AWSCredentialsFactoryTests.cs new file mode 100644 index 00000000..a9cf1665 --- /dev/null +++ b/test/AWS.Deploy.CLI.UnitTests/AWSCredentialsFactoryTests.cs @@ -0,0 +1,25 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +using Amazon.Runtime; +using Xunit; + +namespace AWS.Deploy.CLI.UnitTests +{ + public class AWSCredentialsFactoryTests + { + [Fact] + public void Create_ReturnsAWSCredentialsInstance() + { + // Arrange + var factory = new AWSCredentialsFactory(); + + // Act + var result = factory.Create(); + + // Assert + Assert.NotNull(result); + Assert.IsAssignableFrom(result); + } + } +} diff --git a/test/AWS.Deploy.CLI.UnitTests/AWSUtilitiesTests.cs b/test/AWS.Deploy.CLI.UnitTests/AWSUtilitiesTests.cs new file mode 100644 index 00000000..ab616059 --- /dev/null +++ b/test/AWS.Deploy.CLI.UnitTests/AWSUtilitiesTests.cs @@ -0,0 +1,277 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +using System; +using System.Collections.Generic; +using System.IO; +using System.Text; +using System.Threading.Tasks; +using Amazon; +using Amazon.Runtime; +using Amazon.Runtime.CredentialManagement; +using AWS.Deploy.CLI.Common.UnitTests.IO; +using AWS.Deploy.Common.IO; +using AWS.Deploy.Common.Recipes; +using Moq; +using Xunit; + +namespace AWS.Deploy.CLI.UnitTests +{ + public class AWSUtilitiesTests : IDisposable + { + private readonly IDirectoryManager _directoryManager; + private readonly IOptionSettingHandler _optionSettingHandler; + private readonly Mock _mockToolInteractiveService; + private readonly Mock _mockConsoleUtilities; + private readonly Mock _mockServiceProvider; + private readonly Mock _mockCredentialChainFactory; + private readonly Mock _mockSharedCredentialsFileFactory; + private readonly Mock _mockAWSCredentialsFactory; + private CredentialProfileStoreChain _credentialProfileStoreChain; + + private readonly string _tempCredentialsFile; + private SharedCredentialsFile _sharedCredentialsFile; + + public AWSUtilitiesTests() + { + _directoryManager = new TestDirectoryManager(); + _mockToolInteractiveService = new Mock(); + _mockConsoleUtilities = new Mock(); + _mockServiceProvider = new Mock(); + _optionSettingHandler = new Mock().Object; + _mockCredentialChainFactory = new Mock(); + _mockSharedCredentialsFileFactory = new Mock(); + _mockAWSCredentialsFactory = new Mock(); + + _credentialProfileStoreChain = new CredentialProfileStoreChain(); + + _mockCredentialChainFactory + .Setup(f => f.Create()) + .Returns(_credentialProfileStoreChain); + + // Create a temporary credentials file + _tempCredentialsFile = Path.GetTempFileName(); + Environment.SetEnvironmentVariable("AWS_SHARED_CREDENTIALS_FILE", _tempCredentialsFile); + + // Create a real SharedCredentialsFile instance + _sharedCredentialsFile = new SharedCredentialsFile(_tempCredentialsFile); + + _mockSharedCredentialsFileFactory + .Setup(f => f.Create()) + .Returns(_sharedCredentialsFile); + } + + private AWSUtilities CreateAWSUtilities() + { + return new AWSUtilities( + _mockServiceProvider.Object, + _mockToolInteractiveService.Object, + _mockConsoleUtilities.Object, + _directoryManager, + _optionSettingHandler, + _mockCredentialChainFactory.Object, + _mockSharedCredentialsFileFactory.Object, + _mockAWSCredentialsFactory.Object + ); + } + + public void Dispose() + { + // Clean up the temporary file + if (File.Exists(_tempCredentialsFile)) + { + File.Delete(_tempCredentialsFile); + } + Environment.SetEnvironmentVariable("AWS_SHARED_CREDENTIALS_FILE", null); + } + + private void SetupCredentialsFile(params string[] profileNames) + { + var contents = new StringBuilder(); + foreach (var profileName in profileNames) + { + contents.AppendLine($"[{profileName}]"); + contents.AppendLine("aws_access_key_id = 123"); + contents.AppendLine("aws_secret_access_key = abc"); + contents.AppendLine(); + } + File.WriteAllText(_tempCredentialsFile, contents.ToString()); + + // Re-create SharedCredentialsFile to pick up the changes + _sharedCredentialsFile = new SharedCredentialsFile(_tempCredentialsFile); + _mockSharedCredentialsFileFactory + .Setup(f => f.Create()) + .Returns(_sharedCredentialsFile); + } + + + [Fact] + public async Task ResolveAWSCredentials_WithValidProfileName_ReturnsCredentials() + { + // Arrange + var awsUtilities = CreateAWSUtilities(); + var profileName = "valid-profile"; + var options = new CredentialProfileOptions + { + AccessKey = "abc", + SecretKey = "123" + }; + var mockProfile = new CredentialProfile(profileName, options) + { + Region = RegionEndpoint.USEast1 + }; + + var store = new CredentialProfileStoreChain(); + store.RegisterProfile(mockProfile); + _credentialProfileStoreChain = store; + + _mockCredentialChainFactory + .Setup(f => f.Create()) + .Returns(_credentialProfileStoreChain); + + // Act + var result = await awsUtilities.ResolveAWSCredentials(profileName); + + // Assert + Assert.NotNull(result); + Assert.NotNull(result.Item1); + Assert.Equal("us-east-1", result.Item2); + } + + [Fact] + public async Task ResolveAWSCredentials_WithValidProfileNameAndNullRegion_ReturnsCredentialsAndNullRegion() + { + // Arrange + var awsUtilities = CreateAWSUtilities(); + var profileName = "valid-profile-no-region"; + var options = new CredentialProfileOptions + { + AccessKey = "123", + SecretKey = "abc" + }; + var mockProfile = new CredentialProfile(profileName, options) + { + Region = null + }; + + var store = new CredentialProfileStoreChain(); + store.RegisterProfile(mockProfile); + _credentialProfileStoreChain = store; + + _mockCredentialChainFactory + .Setup(f => f.Create()) + .Returns(_credentialProfileStoreChain); + + // Act + var result = await awsUtilities.ResolveAWSCredentials(profileName); + + // Assert + Assert.NotNull(result); + Assert.NotNull(result.Item1); + Assert.Null(result.Item2); + } + + [Fact] + public async Task ResolveAWSCredentials_WithInvalidProfileName_ThrowsException() + { + // Arrange + var awsUtilities = CreateAWSUtilities(); + var profileName = "invalid-profile"; + + var store = new CredentialProfileStoreChain(); + _credentialProfileStoreChain = store; + + _mockCredentialChainFactory + .Setup(f => f.Create()) + .Returns(_credentialProfileStoreChain); + + // Act & Assert + await Assert.ThrowsAsync(() => awsUtilities.ResolveAWSCredentials(profileName)); + } + + [Fact] + public async Task ResolveAWSCredentials_WithNullProfileName_UsesFallbackCredentials() + { + // Arrange + var awsUtilities = CreateAWSUtilities(); + var mockAWSCredentials = new Mock().Object; + + _mockAWSCredentialsFactory + .Setup(f => f.Create()) + .Returns(mockAWSCredentials); + + // Act + var result = await awsUtilities.ResolveAWSCredentials(null); + + // Assert + Assert.NotNull(result); + Assert.Equal(mockAWSCredentials, result.Item1); + Assert.Null(result.Item2); + } + + [Fact] + public async Task ResolveAWSCredentials_WithNullProfileNameAndFallbackException_PromptsUserToChooseProfile() + { + // Arrange + var awsUtilities = CreateAWSUtilities(); + var profileNames = new List { "profile1", "profile2" }; + var selectedProfileName = "profile1"; + + SetupCredentialsFile(profileNames.ToArray()); + + _mockAWSCredentialsFactory + .Setup(f => f.Create()) + .Throws(new AmazonServiceException("No credentials found")); + + _mockConsoleUtilities + .Setup(c => c.AskUserToChoose(It.IsAny>(), It.IsAny(), It.IsAny(), It.IsAny())) + .Returns(selectedProfileName); + + var store = new CredentialProfileStoreChain(_tempCredentialsFile); + _credentialProfileStoreChain = store; + + _mockCredentialChainFactory + .Setup(f => f.Create()) + .Returns(_credentialProfileStoreChain); + + // Act + var result = await awsUtilities.ResolveAWSCredentials(null); + + // Assert + Assert.NotNull(result); + Assert.NotNull(result.Item1); + Assert.Null(result.Item2); // Region will be null as we didn't set it in the file + _mockConsoleUtilities.Verify(c => c.AskUserToChoose(It.IsAny>(), It.IsAny(), It.IsAny(), It.IsAny()), Times.Once); + } + + [Fact] + public async Task ResolveAWSCredentials_WithNoProfiles_ThrowsNoAWSCredentialsFoundException() + { + // Arrange + var awsUtilities = CreateAWSUtilities(); + + SetupCredentialsFile(); // Empty file, no profiles + + _mockAWSCredentialsFactory + .Setup(f => f.Create()) + .Throws(new AmazonServiceException("No credentials found")); + + // Act & Assert + await Assert.ThrowsAsync(() => awsUtilities.ResolveAWSCredentials(null)); + } + + [Fact] + public void ResolveAWSRegion_WithNonNullRegion_ReturnsRegion() + { + // Arrange + var awsUtilities = CreateAWSUtilities(); + var region = "us-west-2"; + + // Act + var result = awsUtilities.ResolveAWSRegion(region); + + // Assert + Assert.Equal(region, result); + } + } +} diff --git a/test/AWS.Deploy.CLI.UnitTests/Commands/CommandFactoryTest.cs b/test/AWS.Deploy.CLI.UnitTests/Commands/CommandFactoryTest.cs new file mode 100644 index 00000000..35af3992 --- /dev/null +++ b/test/AWS.Deploy.CLI.UnitTests/Commands/CommandFactoryTest.cs @@ -0,0 +1,576 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +using System; +using System.CommandLine; +using System.CommandLine.Invocation; +using System.Linq; +using Amazon.Runtime; +using System.Threading.Tasks; +using AWS.Deploy.CLI.Commands; +using AWS.Deploy.CLI.Commands.CommandHandlerInput; +using AWS.Deploy.CLI.Commands.TypeHints; +using AWS.Deploy.CLI.Utilities; +using AWS.Deploy.Common; +using AWS.Deploy.Common.Data; +using AWS.Deploy.Common.DeploymentManifest; +using AWS.Deploy.Common.IO; +using AWS.Deploy.Common.Recipes; +using AWS.Deploy.Common.Recipes.Validation; +using AWS.Deploy.Orchestration; +using AWS.Deploy.Orchestration.CDK; +using AWS.Deploy.Orchestration.DisplayedResources; +using AWS.Deploy.Orchestration.LocalUserSettings; +using AWS.Deploy.Orchestration.ServiceHandlers; +using AWS.Deploy.Orchestration.Utilities; +using Moq; +using Xunit; +using Amazon.SecurityToken.Model; +using System.Collections.Generic; +using Amazon.Extensions.NETCore.Setup; + +namespace AWS.Deploy.CLI.UnitTests +{ + public class CommandFactoryTests + { + private readonly Mock _mockServiceProvider; + private readonly Mock _mockToolInteractiveService; + private readonly Mock _mockOrchestratorInteractiveService; + private readonly Mock _mockCdkManager; + private readonly Mock _mockSystemCapabilityEvaluator; + private readonly Mock _mockCloudApplicationNameGenerator; + private readonly Mock _mockAwsUtilities; + private readonly Mock _mockAwsClientFactory; + private readonly Mock _mockAwsResourceQueryer; + private readonly Mock _mockProjectParserUtility; + private readonly Mock _mockCommandLineWrapper; + private readonly Mock _mockCdkProjectHandler; + private readonly Mock _mockDeploymentBundleHandler; + private readonly Mock _mockCloudFormationTemplateReader; + private readonly Mock _mockDeployedApplicationQueryer; + private readonly Mock _mockTypeHintCommandFactory; + private readonly Mock _mockDisplayedResourceHandler; + private readonly Mock _mockConsoleUtilities; + private readonly Mock _mockDirectoryManager; + private readonly Mock _mockFileManager; + private readonly Mock _mockDeploymentManifestEngine; + private readonly Mock _mockLocalUserSettingsEngine; + private readonly Mock _mockCdkVersionDetector; + private readonly Mock _mockAwsServiceHandler; + private readonly Mock _mockOptionSettingHandler; + private readonly Mock _mockValidatorFactory; + private readonly Mock _mockRecipeHandler; + private readonly Mock _mockDeployToolWorkspaceMetadata; + private readonly Mock _mockDeploymentSettingsHandler; + + public CommandFactoryTests() + { + _mockServiceProvider = new Mock(); + _mockToolInteractiveService = new Mock(); + _mockOrchestratorInteractiveService = new Mock(); + _mockCdkManager = new Mock(); + _mockSystemCapabilityEvaluator = new Mock(); + _mockCloudApplicationNameGenerator = new Mock(); + _mockAwsUtilities = new Mock(); + _mockAwsClientFactory = new Mock(); + _mockAwsResourceQueryer = new Mock(); + _mockProjectParserUtility = new Mock(); + _mockCommandLineWrapper = new Mock(); + _mockCdkProjectHandler = new Mock(); + _mockDeploymentBundleHandler = new Mock(); + _mockCloudFormationTemplateReader = new Mock(); + _mockDeployedApplicationQueryer = new Mock(); + _mockTypeHintCommandFactory = new Mock(); + _mockDisplayedResourceHandler = new Mock(); + _mockConsoleUtilities = new Mock(); + _mockDirectoryManager = new Mock(); + _mockFileManager = new Mock(); + _mockDeploymentManifestEngine = new Mock(); + _mockLocalUserSettingsEngine = new Mock(); + _mockCdkVersionDetector = new Mock(); + _mockAwsServiceHandler = new Mock(); + _mockOptionSettingHandler = new Mock(); + _mockValidatorFactory = new Mock(); + _mockRecipeHandler = new Mock(); + _mockDeployToolWorkspaceMetadata = new Mock(); + _mockDeploymentSettingsHandler = new Mock(); + } + + private CommandFactory CreateCommandFactory() + { + return new CommandFactory( + _mockServiceProvider.Object, + _mockToolInteractiveService.Object, + _mockOrchestratorInteractiveService.Object, + _mockCdkManager.Object, + _mockSystemCapabilityEvaluator.Object, + _mockCloudApplicationNameGenerator.Object, + _mockAwsUtilities.Object, + _mockAwsClientFactory.Object, + _mockAwsResourceQueryer.Object, + _mockProjectParserUtility.Object, + _mockCommandLineWrapper.Object, + _mockCdkProjectHandler.Object, + _mockDeploymentBundleHandler.Object, + _mockCloudFormationTemplateReader.Object, + _mockDeployedApplicationQueryer.Object, + _mockTypeHintCommandFactory.Object, + _mockDisplayedResourceHandler.Object, + _mockConsoleUtilities.Object, + _mockDirectoryManager.Object, + _mockFileManager.Object, + _mockDeploymentManifestEngine.Object, + _mockLocalUserSettingsEngine.Object, + _mockCdkVersionDetector.Object, + _mockAwsServiceHandler.Object, + _mockOptionSettingHandler.Object, + _mockValidatorFactory.Object, + _mockRecipeHandler.Object, + _mockDeployToolWorkspaceMetadata.Object, + _mockDeploymentSettingsHandler.Object + ); + } + + [Fact] + public void BuildRootCommand_ReturnsRootCommandWithExpectedSubcommands() + { + // Arrange + var commandFactory = CreateCommandFactory(); + + // Act + var rootCommand = commandFactory.BuildRootCommand(); + + // Assert + Assert.NotNull(rootCommand); + Assert.Equal("dotnet-aws", rootCommand.Name); + Assert.Contains(rootCommand.Options, o => o.Name == "version"); + Assert.Contains(rootCommand.Children, c => c.Name == "deploy"); + Assert.Contains(rootCommand.Children, c => c.Name == "list-deployments"); + Assert.Contains(rootCommand.Children, c => c.Name == "delete-deployment"); + Assert.Contains(rootCommand.Children, c => c.Name == "deployment-project"); + Assert.Contains(rootCommand.Children, c => c.Name == "server-mode"); + } + + [Fact] + public void BuildRootCommand_DeployCommandHasExpectedOptions() + { + // Arrange + var commandFactory = CreateCommandFactory(); + + // Act + var rootCommand = commandFactory.BuildRootCommand(); + var deployCommand = rootCommand.Children.First(c => c.Name == "deploy") as Command; + + // Assert + Assert.NotNull(deployCommand); + Assert.Contains(deployCommand.Options, o => o.Name == "profile"); + Assert.Contains(deployCommand.Options, o => o.Name == "region"); + Assert.Contains(deployCommand.Options, o => o.Name == "project-path"); + Assert.Contains(deployCommand.Options, o => o.Name == "application-name"); + Assert.Contains(deployCommand.Options, o => o.Name == "apply"); + Assert.Contains(deployCommand.Options, o => o.Name == "diagnostics"); + Assert.Contains(deployCommand.Options, o => o.Name == "silent"); + Assert.Contains(deployCommand.Options, o => o.Name == "deployment-project"); + Assert.Contains(deployCommand.Options, o => o.Name == "save-settings"); + Assert.Contains(deployCommand.Options, o => o.Name == "save-all-settings"); + } + + // Add more tests for other commands and their options... + + [Fact] + public void BuildRootCommand_ServerModeCommandHasExpectedOptions() + { + // Arrange + var commandFactory = CreateCommandFactory(); + + // Act + var rootCommand = commandFactory.BuildRootCommand(); + var serverModeCommand = rootCommand.Children.First(c => c.Name == "server-mode") as Command; + + // Assert + Assert.NotNull(serverModeCommand); + Assert.Contains(serverModeCommand.Options, o => o.Name == "port"); + Assert.Contains(serverModeCommand.Options, o => o.Name == "parent-pid"); + Assert.Contains(serverModeCommand.Options, o => o.Name == "unsecure-mode"); + Assert.Contains(serverModeCommand.Options, o => o.Name == "diagnostics"); + } + + [Fact] + public async Task DeployCommand_UsesRegionFromCLIWhenProvided() + { + // Arrange + var commandFactory = CreateCommandFactory(); + var mockCredentials = new Mock(); + var testProfile = "test-profile"; + var regionFromCLI = "us-west-2"; + var regionFromProfile = "us-east-1"; + var testProjectPath = "/path/to/project"; + + // Mock ProjectDefinition + var mockProjectDefinition = new ProjectDefinition( + new System.Xml.XmlDocument(), + testProjectPath, + testProjectPath, + "123"); + + _mockProjectParserUtility + .Setup(x => x.Parse(It.IsAny())) + .ReturnsAsync(mockProjectDefinition); + + _mockAwsUtilities + .Setup(x => x.ResolveAWSCredentials(It.IsAny())) + .Returns(Task.FromResult((Tuple.Create(mockCredentials.Object, regionFromProfile)))); + + _mockAwsUtilities + .Setup(x => x.ResolveAWSRegion(It.IsAny(), It.IsAny())) + .Returns(regionFromCLI); + + // Create a mock DeploymentSettings + var mockDeploymentSettings = new DeploymentSettings + { + AWSProfile = "deployment-profile", + AWSRegion = "deployment-region" + }; + + _mockDeploymentSettingsHandler + .Setup(x => x.ReadSettings(It.IsAny())) + .ReturnsAsync(mockDeploymentSettings); + + _mockAwsResourceQueryer + .Setup(x => x.GetCallerIdentity(It.IsAny())) + .ReturnsAsync(new GetCallerIdentityResponse { Account = "123456789012" }); + + + // Act + var result = await InvokeDeployCommandHandler(new DeployCommandHandlerInput + { + Profile = testProfile, + Region = regionFromCLI, + Apply = "some-settings-file.json", + ProjectPath = testProjectPath + }); + + // Assert + _mockProjectParserUtility.Verify(x => x.Parse(testProjectPath), Times.Once); + _mockAwsUtilities.Verify(x => x.ResolveAWSCredentials(testProfile), Times.Once); + _mockAwsUtilities.Verify(x => x.ResolveAWSRegion(regionFromCLI, null), Times.Once); + } + + [Fact] + public async Task DeployCommand_UsesRegionFromProfileWhenCLIRegionNotProvided() + { + // Arrange + var commandFactory = CreateCommandFactory(); + var mockCredentials = new Mock(); + var testProfile = "test-profile"; + var regionFromProfile = "us-east-1"; + var testProjectPath = "/path/to/project"; + + // Mock ProjectDefinition + var mockProjectDefinition = new ProjectDefinition( + new System.Xml.XmlDocument(), + testProjectPath, + testProjectPath, + "123"); + + _mockProjectParserUtility + .Setup(x => x.Parse(It.IsAny())) + .ReturnsAsync(mockProjectDefinition); + + _mockAwsUtilities + .Setup(x => x.ResolveAWSCredentials(It.IsAny())) + .Returns(Task.FromResult((Tuple.Create(mockCredentials.Object, regionFromProfile)))); + + _mockAwsUtilities + .Setup(x => x.ResolveAWSRegion(It.IsAny(), It.IsAny())) + .Returns((string r, string f) => r); + + // Create a mock DeploymentSettings + var mockDeploymentSettings = new DeploymentSettings + { + AWSProfile = "deployment-profile", + AWSRegion = null // Ensure this is null to test our scenario + }; + + _mockDeploymentSettingsHandler + .Setup(x => x.ReadSettings(It.IsAny())) + .ReturnsAsync(mockDeploymentSettings); + + _mockAwsResourceQueryer + .Setup(x => x.GetCallerIdentity(It.IsAny())) + .ReturnsAsync(new GetCallerIdentityResponse { Account = "123456789012" }); + + // Act + var result = await InvokeDeployCommandHandler(new DeployCommandHandlerInput + { + Profile = testProfile, + Region = null, // Not providing a region via CLI + Apply = "some-settings-file.json", + ProjectPath = testProjectPath + }); + + // Assert + _mockProjectParserUtility.Verify(x => x.Parse(testProjectPath), Times.Once); + _mockAwsUtilities.Verify(x => x.ResolveAWSCredentials(testProfile), Times.Once); + _mockAwsUtilities.Verify(x => x.ResolveAWSRegion(regionFromProfile, null), Times.Once); + + // Verify that the region from the profile is used + _mockAwsResourceQueryer.Verify(x => x.GetCallerIdentity(regionFromProfile), Times.Once); + } + + [Fact] + public void BuildRootCommand_DeleteCommandHasExpectedOptions() + { + // Arrange + var commandFactory = CreateCommandFactory(); + + // Act + var rootCommand = commandFactory.BuildRootCommand(); + var deleteCommand = rootCommand.Children.First(c => c.Name == "delete-deployment") as Command; + + // Assert + Assert.NotNull(deleteCommand); + Assert.Contains(deleteCommand.Options, o => o.Name == "profile"); + Assert.Contains(deleteCommand.Options, o => o.Name == "region"); + Assert.Contains(deleteCommand.Options, o => o.Name == "project-path"); + Assert.Contains(deleteCommand.Options, o => o.Name == "diagnostics"); + Assert.Contains(deleteCommand.Options, o => o.Name == "silent"); + + // Verify that the delete command has a deployment-name argument + Assert.Contains(deleteCommand.Arguments, a => a.Name == "deployment-name"); + } + + [Fact] + public void BuildRootCommand_ListCommandHasExpectedOptions() + { + // Arrange + var commandFactory = CreateCommandFactory(); + + // Act + var rootCommand = commandFactory.BuildRootCommand(); + var listCommand = rootCommand.Children.First(c => c.Name == "list-deployments") as Command; + + // Assert + Assert.NotNull(listCommand); + Assert.Contains(listCommand.Options, o => o.Name == "profile"); + Assert.Contains(listCommand.Options, o => o.Name == "region"); + Assert.Contains(listCommand.Options, o => o.Name == "diagnostics"); + } + + [Fact] + public async Task DeleteCommand_UsesRegionFromCLIWhenProvided() + { + // Arrange + var commandFactory = CreateCommandFactory(); + var mockCredentials = new Mock(); + var testProfile = "test-profile"; + var regionFromCLI = "us-west-2"; + var regionFromProfile = "us-east-1"; + var deploymentName = "test-deployment"; + + _mockAwsUtilities + .Setup(x => x.ResolveAWSCredentials(It.IsAny())) + .Returns(Task.FromResult((Tuple.Create(mockCredentials.Object, regionFromProfile)))); + + _mockAwsUtilities + .Setup(x => x.ResolveAWSRegion(It.IsAny(), It.IsAny())) + .Returns(regionFromCLI); + + _mockAwsClientFactory + .Setup(x => x.ConfigureAWSOptions(It.IsAny>())) + .Callback>(action => + { + var options = new AWSOptions(); + action(options); + Assert.Equal(regionFromCLI, options.Region.SystemName); + }); + + // Act + var result = await InvokeDeleteCommandHandler(new DeleteCommandHandlerInput + { + Profile = testProfile, + Region = regionFromCLI, + DeploymentName = deploymentName + }); + + // Assert + _mockAwsUtilities.Verify(x => x.ResolveAWSCredentials(testProfile), Times.Once); + _mockAwsUtilities.Verify(x => x.ResolveAWSRegion(regionFromCLI, null), Times.Once); + _mockAwsClientFactory.Verify(x => x.ConfigureAWSOptions(It.IsAny>()), Times.Once); + } + + [Fact] + public async Task DeleteCommand_UsesRegionFromProfileWhenCLIRegionNotProvided() + { + // Arrange + var commandFactory = CreateCommandFactory(); + var mockCredentials = new Mock(); + var testProfile = "test-profile"; + var regionFromProfile = "us-east-1"; + var deploymentName = "test-deployment"; + + _mockAwsUtilities + .Setup(x => x.ResolveAWSCredentials(It.IsAny())) + .Returns(Task.FromResult((Tuple.Create(mockCredentials.Object, regionFromProfile)))); + + _mockAwsUtilities + .Setup(x => x.ResolveAWSRegion(It.IsAny(), It.IsAny())) + .Returns((string r, string f) => f); + + _mockAwsClientFactory + .Setup(x => x.ConfigureAWSOptions(It.IsAny>())) + .Callback>(action => + { + var options = new AWSOptions(); + action(options); + Assert.Equal(regionFromProfile, options.Region.SystemName); + }); + + // Act + var result = await InvokeDeleteCommandHandler(new DeleteCommandHandlerInput + { + Profile = testProfile, + Region = null, // Not providing a region via CLI + DeploymentName = deploymentName + }); + + // Assert + _mockAwsUtilities.Verify(x => x.ResolveAWSCredentials(testProfile), Times.Once); + _mockAwsUtilities.Verify(x => x.ResolveAWSRegion(regionFromProfile, null), Times.Once); + _mockAwsClientFactory.Verify(x => x.ConfigureAWSOptions(It.IsAny>()), Times.Once); + } + + [Fact] + public async Task ListCommand_UsesRegionFromCLIWhenProvided() + { + // Arrange + var commandFactory = CreateCommandFactory(); + var mockCredentials = new Mock(); + var testProfile = "test-profile"; + var regionFromCLI = "us-west-2"; + var regionFromProfile = "us-east-1"; + + _mockAwsUtilities + .Setup(x => x.ResolveAWSCredentials(It.IsAny())) + .Returns(Task.FromResult((Tuple.Create(mockCredentials.Object, regionFromProfile)))); + + _mockAwsUtilities + .Setup(x => x.ResolveAWSRegion(It.IsAny(), It.IsAny())) + .Returns(regionFromCLI); + + _mockAwsClientFactory + .Setup(x => x.ConfigureAWSOptions(It.IsAny>())) + .Callback>(action => + { + var options = new AWSOptions(); + action(options); + Assert.Equal(regionFromCLI, options.Region.SystemName); + }); + + // Act + var result = await InvokeListCommandHandler(new ListCommandHandlerInput + { + Profile = testProfile, + Region = regionFromCLI + }); + + // Assert + _mockAwsUtilities.Verify(x => x.ResolveAWSCredentials(testProfile), Times.Once); + _mockAwsUtilities.Verify(x => x.ResolveAWSRegion(regionFromCLI, null), Times.Once); + _mockAwsClientFactory.Verify(x => x.ConfigureAWSOptions(It.IsAny>()), Times.Once); + } + + [Fact] + public async Task ListCommand_UsesRegionFromProfileWhenCLIRegionNotProvided() + { + // Arrange + var commandFactory = CreateCommandFactory(); + var mockCredentials = new Mock(); + var testProfile = "test-profile"; + var regionFromProfile = "us-east-1"; + + _mockAwsUtilities + .Setup(x => x.ResolveAWSCredentials(It.IsAny())) + .Returns(Task.FromResult((Tuple.Create(mockCredentials.Object, regionFromProfile)))); + + _mockAwsUtilities + .Setup(x => x.ResolveAWSRegion(It.IsAny(), It.IsAny())) + .Returns((string r, string f) => f); + + _mockAwsClientFactory + .Setup(x => x.ConfigureAWSOptions(It.IsAny>())) + .Callback>(action => + { + var options = new AWSOptions(); + action(options); + Assert.Equal(regionFromProfile, options.Region.SystemName); + }); + + // Act + var result = await InvokeListCommandHandler(new ListCommandHandlerInput + { + Profile = testProfile, + Region = null // Not providing a region via CLI + }); + + // Assert + _mockAwsUtilities.Verify(x => x.ResolveAWSCredentials(testProfile), Times.Once); + _mockAwsUtilities.Verify(x => x.ResolveAWSRegion(regionFromProfile, null), Times.Once); + _mockAwsClientFactory.Verify(x => x.ConfigureAWSOptions(It.IsAny>()), Times.Once); + } + + private async Task InvokeDeployCommandHandler(DeployCommandHandlerInput input) + { + var args = new List { "deploy" }; + + if (!string.IsNullOrEmpty(input.Profile)) + args.AddRange(new[] { "--profile", input.Profile }); + + if (!string.IsNullOrEmpty(input.Region)) + args.AddRange(new[] { "--region", input.Region }); + + if (!string.IsNullOrEmpty(input.Apply)) + args.AddRange(new[] { "--apply", input.Apply }); + + if (!string.IsNullOrEmpty(input.ProjectPath)) + args.AddRange(new[] { "--project-path", input.ProjectPath }); + + var rootCommand = CreateCommandFactory().BuildRootCommand(); + return await rootCommand.InvokeAsync(args.ToArray()); + } + + + private async Task InvokeDeleteCommandHandler(DeleteCommandHandlerInput input) + { + var args = new List { "delete-deployment" }; + + if (!string.IsNullOrEmpty(input.Profile)) + args.AddRange(new[] { "--profile", input.Profile }); + + if (!string.IsNullOrEmpty(input.Region)) + args.AddRange(new[] { "--region", input.Region }); + + if (!string.IsNullOrEmpty(input.DeploymentName)) + args.Add(input.DeploymentName); + + var rootCommand = CreateCommandFactory().BuildRootCommand(); + return await rootCommand.InvokeAsync(args.ToArray()); + } + + private async Task InvokeListCommandHandler(ListCommandHandlerInput input) + { + var args = new List { "list-deployments" }; + + if (!string.IsNullOrEmpty(input.Profile)) + args.AddRange(new[] { "--profile", input.Profile }); + + if (!string.IsNullOrEmpty(input.Region)) + args.AddRange(new[] { "--region", input.Region }); + + var rootCommand = CreateCommandFactory().BuildRootCommand(); + return await rootCommand.InvokeAsync(args.ToArray()); + } + + + } +} diff --git a/test/AWS.Deploy.CLI.UnitTests/CredentialProfileStoreChainFactoryTests.cs b/test/AWS.Deploy.CLI.UnitTests/CredentialProfileStoreChainFactoryTests.cs new file mode 100644 index 00000000..8da6f39a --- /dev/null +++ b/test/AWS.Deploy.CLI.UnitTests/CredentialProfileStoreChainFactoryTests.cs @@ -0,0 +1,25 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +using Amazon.Runtime.CredentialManagement; +using Xunit; + +namespace AWS.Deploy.CLI.UnitTests +{ + public class CredentialProfileStoreChainFactoryTests + { + [Fact] + public void Create_ReturnsCredentialProfileStoreChainInstance() + { + // Arrange + var factory = new CredentialProfileStoreChainFactory(); + + // Act + var result = factory.Create(); + + // Assert + Assert.NotNull(result); + Assert.IsType(result); + } + } +} diff --git a/test/AWS.Deploy.CLI.UnitTests/SharedCredentialsFileFactoryTests.cs b/test/AWS.Deploy.CLI.UnitTests/SharedCredentialsFileFactoryTests.cs new file mode 100644 index 00000000..5d8db76a --- /dev/null +++ b/test/AWS.Deploy.CLI.UnitTests/SharedCredentialsFileFactoryTests.cs @@ -0,0 +1,25 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +using Amazon.Runtime.CredentialManagement; +using Xunit; + +namespace AWS.Deploy.CLI.UnitTests +{ + public class SharedCredentialsFileFactoryTests + { + [Fact] + public void Create_ReturnsSharedCredentialsFileInstance() + { + // Arrange + var factory = new SharedCredentialsFileFactory(); + + // Act + var result = factory.Create(); + + // Assert + Assert.NotNull(result); + Assert.IsType(result); + } + } +} From 4efd115f4c644c5f130aa9d616cea51878714ff4 Mon Sep 17 00:00:00 2001 From: Garrett Beatty Date: Thu, 14 Nov 2024 15:02:18 -0500 Subject: [PATCH 3/5] Update Microsoft.AspNetCore.SignalR.Client version to fix System.Text.Json vulnerability (#883) --- .../changes/27d0ce13-f148-4129-a977-1ae2fa1b36e4.json | 11 +++++++++++ .../AWS.Deploy.ServerMode.Client.csproj | 2 +- 2 files changed, 12 insertions(+), 1 deletion(-) create mode 100644 .autover/changes/27d0ce13-f148-4129-a977-1ae2fa1b36e4.json diff --git a/.autover/changes/27d0ce13-f148-4129-a977-1ae2fa1b36e4.json b/.autover/changes/27d0ce13-f148-4129-a977-1ae2fa1b36e4.json new file mode 100644 index 00000000..dede0871 --- /dev/null +++ b/.autover/changes/27d0ce13-f148-4129-a977-1ae2fa1b36e4.json @@ -0,0 +1,11 @@ +{ + "Projects": [ + { + "Name": "AWS.Deploy.ServerMode.Client", + "Type": "Patch", + "ChangelogMessages": [ + "Update Microsoft.AspNetCore.SignalR.Client version to fix System.Text.Json vulnerability" + ] + } + ] +} \ No newline at end of file diff --git a/src/AWS.Deploy.ServerMode.Client/AWS.Deploy.ServerMode.Client.csproj b/src/AWS.Deploy.ServerMode.Client/AWS.Deploy.ServerMode.Client.csproj index 1c627968..2b65ad2f 100644 --- a/src/AWS.Deploy.ServerMode.Client/AWS.Deploy.ServerMode.Client.csproj +++ b/src/AWS.Deploy.ServerMode.Client/AWS.Deploy.ServerMode.Client.csproj @@ -17,7 +17,7 @@ - + From ce5b185845b6ba11eda0d7969d9d2ebafe9a9e4e Mon Sep 17 00:00:00 2001 From: aws-sdk-dotnet-automation Date: Fri, 15 Nov 2024 17:43:59 +0000 Subject: [PATCH 4/5] release_2024-11-15 --- src/AWS.Deploy.CLI/AWS.Deploy.CLI.csproj | 2 +- .../AWS.Deploy.Recipes.CDK.Common.csproj | 2 +- .../AWS.Deploy.ServerMode.Client.csproj | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/AWS.Deploy.CLI/AWS.Deploy.CLI.csproj b/src/AWS.Deploy.CLI/AWS.Deploy.CLI.csproj index 4b2fbd2e..87622b61 100644 --- a/src/AWS.Deploy.CLI/AWS.Deploy.CLI.csproj +++ b/src/AWS.Deploy.CLI/AWS.Deploy.CLI.csproj @@ -18,7 +18,7 @@ $(NoWarn);1570;1591;ASP0000 Major README.md - 1.27.0 + 1.28.0 diff --git a/src/AWS.Deploy.Recipes.CDK.Common/AWS.Deploy.Recipes.CDK.Common.csproj b/src/AWS.Deploy.Recipes.CDK.Common/AWS.Deploy.Recipes.CDK.Common.csproj index c6289774..94304d28 100644 --- a/src/AWS.Deploy.Recipes.CDK.Common/AWS.Deploy.Recipes.CDK.Common.csproj +++ b/src/AWS.Deploy.Recipes.CDK.Common/AWS.Deploy.Recipes.CDK.Common.csproj @@ -10,7 +10,7 @@ icon.png https://github.com/aws/aws-dotnet-deploy README.md - 1.27.0 + 1.28.0 diff --git a/src/AWS.Deploy.ServerMode.Client/AWS.Deploy.ServerMode.Client.csproj b/src/AWS.Deploy.ServerMode.Client/AWS.Deploy.ServerMode.Client.csproj index 2b65ad2f..b2730930 100644 --- a/src/AWS.Deploy.ServerMode.Client/AWS.Deploy.ServerMode.Client.csproj +++ b/src/AWS.Deploy.ServerMode.Client/AWS.Deploy.ServerMode.Client.csproj @@ -12,7 +12,7 @@ https://github.com/aws/aws-dotnet-deploy true ..\..\public.snk - 1.27.0 + 1.28.0 From a1ebf10587f873e07e358cebc26e0455e9e0a584 Mon Sep 17 00:00:00 2001 From: aws-sdk-dotnet-automation Date: Fri, 15 Nov 2024 17:44:00 +0000 Subject: [PATCH 5/5] Updated changelog --- .../changes/27d0ce13-f148-4129-a977-1ae2fa1b36e4.json | 11 ----------- .../changes/721a3463-3ad9-413d-b2b6-11efb7575fac.json | 11 ----------- .../changes/ebd7f49a-72cd-4c5b-83f4-2790a2560e94.json | 11 ----------- CHANGELOG.md | 9 +++++++++ 4 files changed, 9 insertions(+), 33 deletions(-) delete mode 100644 .autover/changes/27d0ce13-f148-4129-a977-1ae2fa1b36e4.json delete mode 100644 .autover/changes/721a3463-3ad9-413d-b2b6-11efb7575fac.json delete mode 100644 .autover/changes/ebd7f49a-72cd-4c5b-83f4-2790a2560e94.json diff --git a/.autover/changes/27d0ce13-f148-4129-a977-1ae2fa1b36e4.json b/.autover/changes/27d0ce13-f148-4129-a977-1ae2fa1b36e4.json deleted file mode 100644 index dede0871..00000000 --- a/.autover/changes/27d0ce13-f148-4129-a977-1ae2fa1b36e4.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "Projects": [ - { - "Name": "AWS.Deploy.ServerMode.Client", - "Type": "Patch", - "ChangelogMessages": [ - "Update Microsoft.AspNetCore.SignalR.Client version to fix System.Text.Json vulnerability" - ] - } - ] -} \ No newline at end of file diff --git a/.autover/changes/721a3463-3ad9-413d-b2b6-11efb7575fac.json b/.autover/changes/721a3463-3ad9-413d-b2b6-11efb7575fac.json deleted file mode 100644 index dd6c6fea..00000000 --- a/.autover/changes/721a3463-3ad9-413d-b2b6-11efb7575fac.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "Projects": [ - { - "Name": "AWS.Deploy.CLI", - "Type": "Patch", - "ChangelogMessages": [ - "Update beanstalk platform resolution logic to additionally use 'Deprecated' versions in order to continue supporting .NET 6." - ] - } - ] -} \ No newline at end of file diff --git a/.autover/changes/ebd7f49a-72cd-4c5b-83f4-2790a2560e94.json b/.autover/changes/ebd7f49a-72cd-4c5b-83f4-2790a2560e94.json deleted file mode 100644 index df1f60a8..00000000 --- a/.autover/changes/ebd7f49a-72cd-4c5b-83f4-2790a2560e94.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "Projects": [ - { - "Name": "AWS.Deploy.CLI", - "Type": "Minor", - "ChangelogMessages": [ - "Read region value for non default profiles" - ] - } - ] -} \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index 65950e13..b61ded42 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,12 @@ +## Release 2024-11-15 + +### AWS.Deploy.ServerMode.Client (1.28.0) +* Update Microsoft.AspNetCore.SignalR.Client version to fix System.Text.Json vulnerability +### AWS.Deploy.CLI (1.28.0) +* Update beanstalk platform resolution logic to additionally use 'Deprecated' versions in order to continue supporting .NET 6. +* Read region value for non default profiles +### AWS.Deploy.Recipes.CDK.Common (1.28.0) + ## Release 2024-10-24 ### AWS.Deploy.CLI (1.27.0)