diff --git a/src/LocalStack.Client/Session.cs b/src/LocalStack.Client/Session.cs index b959ce6..f9772bb 100644 --- a/src/LocalStack.Client/Session.cs +++ b/src/LocalStack.Client/Session.cs @@ -63,6 +63,12 @@ public AmazonServiceClient CreateClientByImplementation( public AmazonServiceClient CreateClientByInterface(bool useServiceUrl = false) where TClient : IAmazonService { +#if NET10_0_OR_GREATER + if (_sessionReflection.GetType() == typeof(SessionReflection)) + { + return CreateClientByStaticFactory(useServiceUrl); + } +#endif Type serviceInterfaceType = typeof(TClient); return CreateClientByInterface(serviceInterfaceType, useServiceUrl); @@ -127,6 +133,48 @@ public AmazonServiceClient CreateClientByInterface( return client; } +#if NET10_0_OR_GREATER + private AmazonServiceClient CreateClientByStaticFactory(bool useServiceUrl) where TClient : IAmazonService + { + if (!useServiceUrl && string.IsNullOrWhiteSpace(_sessionOptions.RegionName)) + { + throw new MisconfiguredClientException($"{nameof(_sessionOptions.RegionName)} must be set if {nameof(useServiceUrl)} is false."); + } + + ClientConfig clientConfig = TClient.CreateDefaultClientConfig(); + string serviceId = clientConfig.ServiceId; + AwsServiceEndpoint awsServiceEndpoint = _config.GetAwsServiceEndpoint(serviceId) ?? + throw new NotSupportedClientException($"{serviceId} is not supported by this mock session."); + AWSCredentials awsCredentials = new SessionAWSCredentials(_sessionOptions.AwsAccessKeyId, _sessionOptions.AwsAccessKey, _sessionOptions.AwsSessionToken); + + clientConfig.UseHttp = !_config.GetConfigOptions().UseSsl; + S3ForcePathStyle.TrySet(clientConfig, true); + clientConfig.ProxyHost = awsServiceEndpoint.Host; + clientConfig.ProxyPort = awsServiceEndpoint.Port; + + if (useServiceUrl) + { + clientConfig.ServiceURL = awsServiceEndpoint.ServiceUrl.AbsoluteUri; + } + else if (!string.IsNullOrWhiteSpace(_sessionOptions.RegionName)) + { + clientConfig.RegionEndpoint = RegionEndpoint.GetBySystemName(_sessionOptions.RegionName); + } + + IAmazonService client; + try + { + client = TClient.CreateDefaultServiceClient(awsCredentials, clientConfig); + } + catch (Exception exception) + { + throw new TargetInvocationException(exception); + } + + return (AmazonServiceClient)client; + } +#endif + private static Type? FindClientType(Assembly assembly, string name) { #if NET8_0_OR_GREATER diff --git a/src/LocalStack.Client/Utils/S3ForcePathStyle.cs b/src/LocalStack.Client/Utils/S3ForcePathStyle.cs new file mode 100644 index 0000000..083d561 --- /dev/null +++ b/src/LocalStack.Client/Utils/S3ForcePathStyle.cs @@ -0,0 +1,22 @@ +#if NET10_0_OR_GREATER +using System.Runtime.CompilerServices; + +namespace LocalStack.Client.Utils; + +internal static class S3ForcePathStyle +{ + [UnsafeAccessor(UnsafeAccessorKind.Method, Name = "set_ForcePathStyle")] + private static extern void SetForcePathStyle([UnsafeAccessorType("Amazon.S3.AmazonS3Config, AWSSDK.S3")] object config, bool value); + + internal static bool TrySet(ClientConfig clientConfig, bool value) + { + if (string.Equals(clientConfig.ServiceId, "S3", StringComparison.Ordinal)) + { + SetForcePathStyle(clientConfig, value); + return true; + } + + return false; + } +} +#endif diff --git a/src/LocalStack.Client/Utils/SessionReflection.cs b/src/LocalStack.Client/Utils/SessionReflection.cs index 09fa78f..81f2060 100644 --- a/src/LocalStack.Client/Utils/SessionReflection.cs +++ b/src/LocalStack.Client/Utils/SessionReflection.cs @@ -124,6 +124,12 @@ public bool SetForcePathStyle(ClientConfig clientConfig, bool value = true) throw new ArgumentNullException(nameof(clientConfig)); } +#if NET10_0_OR_GREATER + if (S3ForcePathStyle.TrySet(clientConfig, value)) + { + return true; + } +#endif #if NET8_0_OR_GREATER if (GeneratedClientRegistry.TryGetConfig(clientConfig.GetType(), out GeneratedClientRegistry.ConfigEntry? entry)) { diff --git a/tests/LocalStack.Client.Integration.Tests/CreateClientByInterfaceTests.cs b/tests/LocalStack.Client.Integration.Tests/CreateClientByInterfaceTests.cs index fbfdaf4..44b0409 100644 --- a/tests/LocalStack.Client.Integration.Tests/CreateClientByInterfaceTests.cs +++ b/tests/LocalStack.Client.Integration.Tests/CreateClientByInterfaceTests.cs @@ -11,6 +11,18 @@ static CreateClientByInterfaceTests() { } + [Fact] + public void SetForcePathStyle_Should_Set_S3_Config_And_Return_False_For_Sqs_Config() + { + var reflection = new Utils.SessionReflection(); + var s3Config = new AmazonS3Config(); + + Assert.False(s3Config.ForcePathStyle); + Assert.True(reflection.SetForcePathStyle(s3Config)); + Assert.True(s3Config.ForcePathStyle); + Assert.False(reflection.SetForcePathStyle(new AmazonSQSConfig())); + } + [Fact] public void Should_Able_To_Create_AmazonAPIGatewayClient() { diff --git a/tests/LocalStack.Client.Tests/SessionTests/SessionStaticFactoryTests.cs b/tests/LocalStack.Client.Tests/SessionTests/SessionStaticFactoryTests.cs new file mode 100644 index 0000000..9187986 --- /dev/null +++ b/tests/LocalStack.Client.Tests/SessionTests/SessionStaticFactoryTests.cs @@ -0,0 +1,50 @@ +#if NET10_0_OR_GREATER +namespace LocalStack.Client.Tests.SessionTests; + +public class SessionStaticFactoryTests +{ + [Fact] + public void CreateClientByInterface_Should_Use_The_Static_Factory_When_The_Client_Name_Does_Not_Follow_The_Convention() + { + Session session = CreateSession(); + + using AmazonServiceClient client = session.CreateClientByInterface(); + + Assert.IsType(client); + Assert.True(client.Config.UseHttp); + Assert.Equal("localhost", client.Config.ProxyHost); + Assert.Equal(1234, client.Config.ProxyPort); + Assert.Equal(RegionEndpoint.EUWest2, client.Config.RegionEndpoint); + } + + [Fact] + public void CreateClientByInterface_Should_Wrap_Static_Factory_Failures_In_TargetInvocationException() + { + Session session = CreateSession(); + + TargetInvocationException exception = Assert.Throws(() => session.CreateClientByInterface()); + + Assert.Same(IMockThrowingFactoryAmazonService.Failure, exception.InnerException); + } + + [Fact] + public void CreateClientByInterface_Should_Pass_The_Configured_ClientConfig_To_The_Static_Factory() + { + Session session = CreateSession(); + FactoryCreatedMockClient.RecordedConfig = null; + + using AmazonServiceClient client = session.CreateClientByInterface(useServiceUrl: true); + + Assert.Same(FactoryCreatedMockClient.RecordedConfig, client.Config); + Assert.Equal("http://localhost:1234/", client.Config.ServiceURL); + } + + private static Session CreateSession() + { + var config = new Mock(MockBehavior.Strict); + config.Setup(value => value.GetAwsServiceEndpoint(MockServiceMetadata.MockServiceId)).Returns(new MockAwsServiceEndpoint()); + config.Setup(value => value.GetConfigOptions()).Returns(new ConfigOptions()); + return new Session(new SessionOptions(regionName: "eu-west-2"), config.Object, new SessionReflection()); + } +} +#endif diff --git a/tests/common/LocalStack.Tests.Common/Mocks/MockServiceClients/FactoryCreatedMockClient.cs b/tests/common/LocalStack.Tests.Common/Mocks/MockServiceClients/FactoryCreatedMockClient.cs new file mode 100644 index 0000000..1cc96eb --- /dev/null +++ b/tests/common/LocalStack.Tests.Common/Mocks/MockServiceClients/FactoryCreatedMockClient.cs @@ -0,0 +1,9 @@ +#if NET10_0_OR_GREATER +namespace LocalStack.Tests.Common.Mocks.MockServiceClients; + +public sealed class FactoryCreatedMockClient(AWSCredentials credentials, MockClientConfig clientConfig) + : MockAmazonServiceClient(credentials, clientConfig), IMockFactoryAmazonService +{ + public static ClientConfig? RecordedConfig { get; set; } +} +#endif diff --git a/tests/common/LocalStack.Tests.Common/Mocks/MockServiceClients/IMockAmazonService.cs b/tests/common/LocalStack.Tests.Common/Mocks/MockServiceClients/IMockAmazonService.cs index d5c2a9c..dd95a3e 100644 --- a/tests/common/LocalStack.Tests.Common/Mocks/MockServiceClients/IMockAmazonService.cs +++ b/tests/common/LocalStack.Tests.Common/Mocks/MockServiceClients/IMockAmazonService.cs @@ -8,8 +8,8 @@ public interface IMockAmazonService : IDisposable, IAmazonService static IAmazonService IAmazonService.CreateDefaultServiceClient(AWSCredentials awsCredentials, ClientConfig clientConfig) { - return new MockAmazonServiceClient(awsCredentials, MockClientConfig.CreateDefaultMockClientConfig()); + return new MockAmazonServiceClient(awsCredentials, (MockClientConfig)clientConfig); } #pragma warning restore CA1033 #endif -} \ No newline at end of file +} diff --git a/tests/common/LocalStack.Tests.Common/Mocks/MockServiceClients/IMockAmazonServiceWithServiceMetadata.cs b/tests/common/LocalStack.Tests.Common/Mocks/MockServiceClients/IMockAmazonServiceWithServiceMetadata.cs index c1dbf29..aa970a6 100644 --- a/tests/common/LocalStack.Tests.Common/Mocks/MockServiceClients/IMockAmazonServiceWithServiceMetadata.cs +++ b/tests/common/LocalStack.Tests.Common/Mocks/MockServiceClients/IMockAmazonServiceWithServiceMetadata.cs @@ -8,8 +8,8 @@ public interface IMockAmazonServiceWithServiceMetadata : IDisposable, IAmazonSer static IAmazonService IAmazonService.CreateDefaultServiceClient(AWSCredentials awsCredentials, ClientConfig clientConfig) { - return new MockAmazonServiceWithServiceMetadataClient(awsCredentials, MockClientConfig.CreateDefaultMockClientConfig()); + return new MockAmazonServiceWithServiceMetadataClient(awsCredentials, (MockClientConfig)clientConfig); } #pragma warning restore CA1033 #endif -} \ No newline at end of file +} diff --git a/tests/common/LocalStack.Tests.Common/Mocks/MockServiceClients/IMockFactoryAmazonService.cs b/tests/common/LocalStack.Tests.Common/Mocks/MockServiceClients/IMockFactoryAmazonService.cs new file mode 100644 index 0000000..f8481c5 --- /dev/null +++ b/tests/common/LocalStack.Tests.Common/Mocks/MockServiceClients/IMockFactoryAmazonService.cs @@ -0,0 +1,16 @@ +#if NET10_0_OR_GREATER +namespace LocalStack.Tests.Common.Mocks.MockServiceClients; + +public interface IMockFactoryAmazonService : IAmazonService +{ +#pragma warning disable CA1033 + static ClientConfig IAmazonService.CreateDefaultClientConfig() => MockClientConfig.CreateDefaultMockClientConfig(); + + static IAmazonService IAmazonService.CreateDefaultServiceClient(AWSCredentials awsCredentials, ClientConfig clientConfig) + { + FactoryCreatedMockClient.RecordedConfig = clientConfig; + return new FactoryCreatedMockClient(awsCredentials, (MockClientConfig)clientConfig); + } +#pragma warning restore CA1033 +} +#endif diff --git a/tests/common/LocalStack.Tests.Common/Mocks/MockServiceClients/IMockThrowingFactoryAmazonService.cs b/tests/common/LocalStack.Tests.Common/Mocks/MockServiceClients/IMockThrowingFactoryAmazonService.cs new file mode 100644 index 0000000..402f9f7 --- /dev/null +++ b/tests/common/LocalStack.Tests.Common/Mocks/MockServiceClients/IMockThrowingFactoryAmazonService.cs @@ -0,0 +1,14 @@ +#if NET10_0_OR_GREATER +namespace LocalStack.Tests.Common.Mocks.MockServiceClients; + +public interface IMockThrowingFactoryAmazonService : IAmazonService +{ + static InvalidOperationException Failure { get; } = new("fixture static factory failure"); + +#pragma warning disable CA1033 + static ClientConfig IAmazonService.CreateDefaultClientConfig() => MockClientConfig.CreateDefaultMockClientConfig(); + + static IAmazonService IAmazonService.CreateDefaultServiceClient(AWSCredentials awsCredentials, ClientConfig clientConfig) => throw Failure; +#pragma warning restore CA1033 +} +#endif diff --git a/tests/common/LocalStack.Tests.Common/Mocks/MockServiceClients/MockAmazonServiceClient.cs b/tests/common/LocalStack.Tests.Common/Mocks/MockServiceClients/MockAmazonServiceClient.cs index 5eeb1f7..5693c26 100644 --- a/tests/common/LocalStack.Tests.Common/Mocks/MockServiceClients/MockAmazonServiceClient.cs +++ b/tests/common/LocalStack.Tests.Common/Mocks/MockServiceClients/MockAmazonServiceClient.cs @@ -34,7 +34,7 @@ public static ClientConfig CreateDefaultClientConfig() public static IAmazonService CreateDefaultServiceClient(AWSCredentials awsCredentials, ClientConfig clientConfig) { - return new MockAmazonServiceClient(awsCredentials, MockClientConfig.CreateDefaultMockClientConfig()); + return new MockAmazonServiceClient(awsCredentials, (MockClientConfig)clientConfig); } #endif -} \ No newline at end of file +} diff --git a/tests/common/LocalStack.Tests.Common/Mocks/MockServiceClients/MockAmazonServiceWithServiceMetadataClient.cs b/tests/common/LocalStack.Tests.Common/Mocks/MockServiceClients/MockAmazonServiceWithServiceMetadataClient.cs index 05d93b0..2f17ca5 100644 --- a/tests/common/LocalStack.Tests.Common/Mocks/MockServiceClients/MockAmazonServiceWithServiceMetadataClient.cs +++ b/tests/common/LocalStack.Tests.Common/Mocks/MockServiceClients/MockAmazonServiceWithServiceMetadataClient.cs @@ -33,7 +33,7 @@ public static ClientConfig CreateDefaultClientConfig() public static IAmazonService CreateDefaultServiceClient(AWSCredentials awsCredentials, ClientConfig clientConfig) { - return new MockAmazonServiceWithServiceMetadataClient(awsCredentials, MockClientConfig.CreateDefaultMockClientConfig()); + return new MockAmazonServiceWithServiceMetadataClient(awsCredentials, (MockClientConfig)clientConfig); } #endif -} \ No newline at end of file +} diff --git a/tests/common/LocalStack.Tests.Common/Mocks/MockServiceClients/MockClientConfig.cs b/tests/common/LocalStack.Tests.Common/Mocks/MockServiceClients/MockClientConfig.cs index d19c23e..51b8c4b 100644 --- a/tests/common/LocalStack.Tests.Common/Mocks/MockServiceClients/MockClientConfig.cs +++ b/tests/common/LocalStack.Tests.Common/Mocks/MockServiceClients/MockClientConfig.cs @@ -10,6 +10,7 @@ public MockClientConfig() : this(new MockConfigurationProvider()) public MockClientConfig(IDefaultConfigurationProvider configurationProvider) : base(configurationProvider) { + ServiceId = MockServiceMetadata.MockServiceId; ServiceURL = "http://localhost"; } @@ -25,4 +26,4 @@ public override Endpoint DetermineServiceOperationEndpoint(ServiceOperationEndpo public override string RegionEndpointServiceName => "mock-service"; public static MockClientConfig CreateDefaultMockClientConfig() => new(new MockConfigurationProvider()); -} \ No newline at end of file +}