From 7c1b0c3e0d9bde37c2f9c7ab534666948f038787 Mon Sep 17 00:00:00 2001 From: Jesse Squire Date: Wed, 7 Feb 2024 09:27:19 -0500 Subject: [PATCH] [Service Bus] Emulator connection string support (#41750) * [Service Bus] Emulator connection string support The focus of these changes is to add support for the connection string format used by the emulator currently in development. This is intended to support internal testing on very early alpha builds. --- .../src/Amqp/AmqpClient.cs | 9 +- .../src/Amqp/AmqpConnectionScope.cs | 64 ++++++--- .../src/Core/TransportTypeExtensions.cs | 16 ++- .../src/Primitives/ServiceBusConnection.cs | 23 +++- .../ServiceBusConnectionStringProperties.cs | 63 ++++++++- .../tests/Amqp/AmqpClientTests.cs | 27 +++- .../tests/Diagnostics/EventSourceTests.cs | 3 +- .../Infrastructure/ServiceBusTestUtilities.cs | 3 +- ...rviceBusConnectionStringPropertiesTests.cs | 121 ++++++++++++++++-- .../Primitives/ServiceBusConnectionTests.cs | 89 ++++++++++++- .../tests/Processor/ProcessorTests.cs | 3 +- 11 files changed, 366 insertions(+), 55 deletions(-) diff --git a/sdk/servicebus/Azure.Messaging.ServiceBus/src/Amqp/AmqpClient.cs b/sdk/servicebus/Azure.Messaging.ServiceBus/src/Amqp/AmqpClient.cs index 358034ebb522e..b98efbebf52e1 100644 --- a/sdk/servicebus/Azure.Messaging.ServiceBus/src/Amqp/AmqpClient.cs +++ b/sdk/servicebus/Azure.Messaging.ServiceBus/src/Amqp/AmqpClient.cs @@ -79,6 +79,7 @@ internal class AmqpClient : TransportClient /// The fully qualified host name for the Service Bus namespace. This is likely to be similar to {yournamespace}.servicebus.windows.net. /// The Azure managed identity credential to use for authorization. Access controls may be specified by the Service Bus namespace or the requested Service Bus entity, depending on Azure configuration. /// A set of options to apply when configuring the client. + /// true if the client should secure the connection using TLS; otherwise, false. /// /// /// As an internal type, this class performs only basic sanity checks against its arguments. It @@ -92,7 +93,8 @@ internal class AmqpClient : TransportClient internal AmqpClient( string host, ServiceBusTokenCredential credential, - ServiceBusClientOptions options) + ServiceBusClientOptions options, + bool useTls) { Argument.AssertNotNullOrEmpty(host, nameof(host)); Argument.AssertNotNull(credential, nameof(credential)); @@ -102,14 +104,15 @@ internal AmqpClient( ServiceEndpoint = new UriBuilder { - Scheme = options.TransportType.GetUriScheme(), + Scheme = options.TransportType.GetUriScheme(useTls), Host = host }.Uri; ConnectionEndpoint = (options.CustomEndpointAddress == null) ? ServiceEndpoint : new UriBuilder { Scheme = ServiceEndpoint.Scheme, - Host = options.CustomEndpointAddress.Host + Host = options.CustomEndpointAddress.Host, + Port = options.CustomEndpointAddress.IsDefaultPort ? -1 : options.CustomEndpointAddress.Port }.Uri; Credential = credential; diff --git a/sdk/servicebus/Azure.Messaging.ServiceBus/src/Amqp/AmqpConnectionScope.cs b/sdk/servicebus/Azure.Messaging.ServiceBus/src/Amqp/AmqpConnectionScope.cs index 2f0cd50998b4f..398ae47ee7bdf 100644 --- a/sdk/servicebus/Azure.Messaging.ServiceBus/src/Amqp/AmqpConnectionScope.cs +++ b/sdk/servicebus/Azure.Messaging.ServiceBus/src/Amqp/AmqpConnectionScope.cs @@ -36,7 +36,10 @@ internal class AmqpConnectionScope : TransportConnectionScope private const string WebSocketsPathSuffix = "/$servicebus/websocket/"; /// The URI scheme to apply when using web sockets for service communication. - private const string WebSocketsUriScheme = "wss"; + private const string WebSocketsSecureUriScheme = "wss"; + + /// The URI scheme to apply when using web sockets for service communication. + private const string WebSocketsInsecureUriScheme = "ws"; /// The seed to use for initializing random number generated for a given thread-specific instance. private static int s_randomSeed = Environment.TickCount; @@ -461,13 +464,12 @@ protected virtual async Task CreateAndOpenConnectionAsync( TimeSpan timeout) { var serviceHostName = serviceEndpoint.Host; - var connectionHostName = connectionEndpoint.Host; AmqpSettings amqpSettings = CreateAmpqSettings(AmqpVersion); AmqpConnectionSettings connectionSetings = CreateAmqpConnectionSettings(serviceHostName, scopeIdentifier, _connectionIdleTimeoutMilliseconds); TransportSettings transportSettings = transportType.IsWebSocketTransport() - ? CreateTransportSettingsForWebSockets(connectionHostName, proxy) - : CreateTransportSettingsforTcp(connectionHostName, connectionEndpoint.Port); + ? CreateTransportSettingsForWebSockets(connectionEndpoint, proxy) + : CreateTransportSettingsforTcp(connectionEndpoint); // Create and open the connection, respecting the timeout constraint // that was received. @@ -1312,29 +1314,37 @@ private static AmqpSettings CreateAmpqSettings(Version amqpVersion) /// Creates the transport settings for use with TCP. /// /// - /// The host name of the Service Bus service endpoint. - /// The port to use for connecting to the endpoint. + /// The Event Hubs service endpoint to connect to. /// /// The settings to use for transport. - private static TransportSettings CreateTransportSettingsforTcp( - string hostName, - int port) + private static TransportSettings CreateTransportSettingsforTcp(Uri connectionEndpoint) { + var useTls = ShouldUseTls(connectionEndpoint.Scheme); + var port = connectionEndpoint.Port < 0 ? (useTls ? AmqpConstants.DefaultSecurePort : AmqpConstants.DefaultPort) : connectionEndpoint.Port; + // Allow the host to control the size of the transport buffers for sending and // receiving by setting the value to -1. This results in much improved throughput // across platforms, as different Linux distros have different needs to // maximize efficiency, with Window having its own needs as well. var tcpSettings = new TcpTransportSettings { - Host = hostName, - Port = port < 0 ? AmqpConstants.DefaultSecurePort : port, + Host = connectionEndpoint.Host, + Port = port, ReceiveBufferSize = -1, SendBufferSize = -1 }; - return new TlsTransportSettings(tcpSettings) + // If TLS is explicitly disabled, then use the TCP settings as-is. Otherwise, + // wrap them for TLS usage. + + return useTls switch { - TargetHost = hostName, + false => tcpSettings, + + _ => new TlsTransportSettings(tcpSettings) + { + TargetHost = connectionEndpoint.Host + } }; } @@ -1342,19 +1352,21 @@ private static TransportSettings CreateTransportSettingsforTcp( /// Creates the transport settings for use with web sockets. /// /// - /// The host name of the Service Bus service endpoint. + /// The Event Hubs service endpoint to connect to. /// The proxy to use for connecting to the endpoint. /// /// The settings to use for transport. private static TransportSettings CreateTransportSettingsForWebSockets( - string hostName, + Uri connectionEndpoint, IWebProxy proxy) { - var uriBuilder = new UriBuilder(hostName) + var useTls = ShouldUseTls(connectionEndpoint.Scheme); + + var uriBuilder = new UriBuilder(connectionEndpoint.Host) { Path = WebSocketsPathSuffix, - Scheme = WebSocketsUriScheme, - Port = -1 + Scheme = useTls ? WebSocketsSecureUriScheme : WebSocketsInsecureUriScheme, + Port = connectionEndpoint.Port < 0 ? -1 : connectionEndpoint.Port }; return new WebSocketTransportSettings @@ -1394,6 +1406,22 @@ private static AmqpConnectionSettings CreateAmqpConnectionSettings( return connectionSettings; } + /// + /// Determines if the specified URL scheme should use TLS when creating an AMQP connection. + /// + /// + /// The URL scheme to consider. + /// + /// true if the connection should use TLS; otherwise, false. + /// + private static bool ShouldUseTls(string urlScheme) => urlScheme switch + { + "ws" => false, + "amqp" => false, + "http" => false, + _ => true + }; + /// /// Validates the transport associated with the scope, throwing an argument exception /// if it is unknown in this context. diff --git a/sdk/servicebus/Azure.Messaging.ServiceBus/src/Core/TransportTypeExtensions.cs b/sdk/servicebus/Azure.Messaging.ServiceBus/src/Core/TransportTypeExtensions.cs index 9489b1c9eb72e..d881c50be3c72 100644 --- a/sdk/servicebus/Azure.Messaging.ServiceBus/src/Core/TransportTypeExtensions.cs +++ b/sdk/servicebus/Azure.Messaging.ServiceBus/src/Core/TransportTypeExtensions.cs @@ -12,24 +12,32 @@ namespace Azure.Messaging.ServiceBus.Core /// internal static class TransportTypeExtensions { - /// The URI scheme used for an AMQP-based connection. - private const string AmqpUriScheme = "amqps"; + /// The URI scheme used for a TLS-secured AMQP-based connection. + private const string AmqpTlsUriScheme = "amqps"; + + /// The URI scheme used for an insecure AMQP-based connection. + private const string AmqpInsecureUriScheme = "amqp"; /// /// Determines the URI scheme to be used for the given connection type. /// /// /// The instance that this method was invoked on. + /// true if the scheme should be for a TLS-secured connection; otherwise, false. /// /// The scheme that should be used for the given connection type when forming an associated URI. /// - public static string GetUriScheme(this ServiceBusTransportType instance) + public static string GetUriScheme(this ServiceBusTransportType instance, bool useTls = true) { switch (instance) { + case ServiceBusTransportType.AmqpTcp when useTls: + case ServiceBusTransportType.AmqpWebSockets when useTls: + return AmqpTlsUriScheme; + case ServiceBusTransportType.AmqpTcp: case ServiceBusTransportType.AmqpWebSockets: - return AmqpUriScheme; + return AmqpInsecureUriScheme; default: throw new ArgumentException(string.Format(CultureInfo.CurrentCulture, Resources.InvalidTransportType, instance.ToString(), nameof(instance))); diff --git a/sdk/servicebus/Azure.Messaging.ServiceBus/src/Primitives/ServiceBusConnection.cs b/sdk/servicebus/Azure.Messaging.ServiceBus/src/Primitives/ServiceBusConnection.cs index 8d92df07ddde5..ae0d1bef3c3c1 100644 --- a/sdk/servicebus/Azure.Messaging.ServiceBus/src/Primitives/ServiceBusConnection.cs +++ b/sdk/servicebus/Azure.Messaging.ServiceBus/src/Primitives/ServiceBusConnection.cs @@ -86,6 +86,17 @@ internal ServiceBusConnection( var connectionStringProperties = ServiceBusConnectionStringProperties.Parse(connectionString); ValidateConnectionStringProperties(connectionStringProperties, nameof(connectionString)); + // If the emulator is in use, then unset TLS and set the endpoint as a custom endpoint + // address, unless one was explicitly provided. + + var useTls = true; + + if (connectionStringProperties.UseDevelopmentEmulator) + { + useTls = false; + options.CustomEndpointAddress ??= connectionStringProperties.Endpoint; + } + FullyQualifiedNamespace = connectionStringProperties.Endpoint.Host; TransportType = options.TransportType; EntityPath = connectionStringProperties.EntityPath; @@ -108,7 +119,7 @@ internal ServiceBusConnection( var sharedCredential = new SharedAccessCredential(sharedAccessSignature); var tokenCredential = new ServiceBusTokenCredential(sharedCredential); #pragma warning disable CA2214 // Do not call overridable methods in constructors. This internal method is virtual for testing purposes. - InnerClient = CreateTransportClient(tokenCredential, options); + InnerClient = CreateTransportClient(tokenCredential, options, useTls); #pragma warning restore CA2214 // Do not call overridable methods in constructors } @@ -171,7 +182,7 @@ internal ServiceBusConnection( RetryOptions = options.RetryOptions; #pragma warning disable CA2214 // Do not call overridable methods in constructors. This internal method is virtual for testing purposes. - InnerClient = CreateTransportClient(tokenCredential, options); + InnerClient = CreateTransportClient(tokenCredential, options, useTls: true); #pragma warning restore CA2214 // Do not call overridable methods in constructors } @@ -263,7 +274,8 @@ internal virtual TransportRuleManager CreateTransportRuleManager( /// /// /// The Azure managed identity credential to use for authorization. - /// + /// The set of options to use for the client. + /// true if the client should secure the connection using TLS; otherwise, false. /// /// A client generalization specific to the specified protocol/transport to which operations may be delegated. /// @@ -277,13 +289,14 @@ internal virtual TransportRuleManager CreateTransportRuleManager( /// internal virtual TransportClient CreateTransportClient( ServiceBusTokenCredential credential, - ServiceBusClientOptions options) + ServiceBusClientOptions options, + bool useTls = true) { switch (TransportType) { case ServiceBusTransportType.AmqpTcp: case ServiceBusTransportType.AmqpWebSockets: - return new AmqpClient(FullyQualifiedNamespace, credential, options); + return new AmqpClient(FullyQualifiedNamespace, credential, options, useTls); default: throw new ArgumentException(string.Format(CultureInfo.CurrentCulture, Resources.InvalidTransportType, options.TransportType.ToString()), nameof(options)); diff --git a/sdk/servicebus/Azure.Messaging.ServiceBus/src/Primitives/ServiceBusConnectionStringProperties.cs b/sdk/servicebus/Azure.Messaging.ServiceBus/src/Primitives/ServiceBusConnectionStringProperties.cs index 6b1bf2846b15d..1b56fe418e66d 100644 --- a/sdk/servicebus/Azure.Messaging.ServiceBus/src/Primitives/ServiceBusConnectionStringProperties.cs +++ b/sdk/servicebus/Azure.Messaging.ServiceBus/src/Primitives/ServiceBusConnectionStringProperties.cs @@ -38,6 +38,9 @@ public class ServiceBusConnectionStringProperties /// The token that identifies the value of a shared access signature. private const string SharedAccessSignatureToken = "SharedAccessSignature"; + /// The token that identifies the intent to use a local emulator for development. + private const string DevelopmentEmulatorToken = "UseDevelopmentEmulator"; + /// The formatted protocol used by an Service Bus endpoint. private static readonly string ServiceBusEndpointScheme = $"{ ServiceBusEndpointSchemeName }{ Uri.SchemeDelimiter }"; @@ -85,6 +88,15 @@ public class ServiceBusConnectionStringProperties /// public string SharedAccessSignature { get; internal set; } + /// + /// Indicates whether or not the connection string indicates that the + /// local development emulator is being used. + /// + /// + /// true if the emulator is being used; otherwise, false. + /// + internal bool UseDevelopmentEmulator { get; set; } + /// /// Determines whether the specified is equal to this instance. /// @@ -202,6 +214,15 @@ internal string ToConnectionString() .Append(TokenValuePairDelimiter); } + if (UseDevelopmentEmulator) + { + builder + .Append(DevelopmentEmulatorToken) + .Append(TokenValueSeparator) + .Append("true") + .Append(TokenValuePairDelimiter); + } + return builder.ToString(); } @@ -278,10 +299,29 @@ public static ServiceBusConnectionStringProperties Parse(string connectionString if (string.Compare(EndpointToken, token, StringComparison.OrdinalIgnoreCase) == 0) { - var endpointBuilder = new UriBuilder(value) + // If this is an absolute URI, then it may have a custom port specified, which we + // want to preserve. If no scheme was specified, the URI is considered relative and + // the default port should be used. + + if (!Uri.TryCreate(value, UriKind.Absolute, out var endpointUri)) { - Scheme = ServiceBusEndpointScheme, - Port = -1 + endpointUri = null; + } + + var endpointBuilder = endpointUri switch + { + null => new UriBuilder(value) + { + Scheme = ServiceBusEndpointSchemeName, + Port = -1 + }, + + _ => new UriBuilder() + { + Scheme = ServiceBusEndpointSchemeName, + Host = endpointUri.Host, + Port = endpointUri.IsDefaultPort ? -1 : endpointUri.Port, + } }; if ((string.Compare(endpointBuilder.Scheme, ServiceBusEndpointSchemeName, StringComparison.OrdinalIgnoreCase) != 0) @@ -308,6 +348,16 @@ public static ServiceBusConnectionStringProperties Parse(string connectionString { parsedValues.SharedAccessSignature = value; } + else if (string.Compare(DevelopmentEmulatorToken, token, StringComparison.OrdinalIgnoreCase) == 0) + { + // Do not enforce a value for the development emulator token. If a valid boolean, use it. + // Otherwise, leave the default value of false. + + if (bool.TryParse(value, out var useEmulator)) + { + parsedValues.UseDevelopmentEmulator = useEmulator; + } + } } else if ((slice.Length != 1) || (slice[0] != TokenValuePairDelimiter)) { @@ -321,6 +371,13 @@ public static ServiceBusConnectionStringProperties Parse(string connectionString lastPosition = currentPosition; } + // Enforce that the development emulator can only be used for local development. + + if ((parsedValues.UseDevelopmentEmulator) && (!parsedValues.Endpoint.IsLoopback)) + { + throw new ArgumentException("The Service Bus emulator is only available locally. The endpoint must reference to the local host.", connectionString); + } + return parsedValues; } } diff --git a/sdk/servicebus/Azure.Messaging.ServiceBus/tests/Amqp/AmqpClientTests.cs b/sdk/servicebus/Azure.Messaging.ServiceBus/tests/Amqp/AmqpClientTests.cs index 75f8f885a3268..6832883dd4940 100644 --- a/sdk/servicebus/Azure.Messaging.ServiceBus/tests/Amqp/AmqpClientTests.cs +++ b/sdk/servicebus/Azure.Messaging.ServiceBus/tests/Amqp/AmqpClientTests.cs @@ -26,7 +26,7 @@ public void ConstructorInitializesTheEndpointsWithDefaults() var options = new ServiceBusClientOptions(); var endpoint = new Uri("http://fake.endpoint.com"); var token = new Mock(Mock.Of()); - var client = new AmqpClient(endpoint.Host, token.Object, options); + var client = new AmqpClient(endpoint.Host, token.Object, options, true); Assert.That(client.ConnectionEndpoint.Host, Is.EqualTo(endpoint.Host), "The connection endpoint should have used the namespace URI."); Assert.That(client.ServiceEndpoint.Host, Is.EqualTo(endpoint.Host), "The service endpoint should have used the namespace URI."); @@ -42,10 +42,33 @@ public void ConstructorInitializesTheEndpointsWithOptions() var options = new ServiceBusClientOptions() { CustomEndpointAddress = new Uri("http://fake.custom.com") }; var endpoint = new Uri("http://fake.endpoint.com"); var token = new Mock(Mock.Of()); - var client = new AmqpClient(endpoint.Host, token.Object, options); + var client = new AmqpClient(endpoint.Host, token.Object, options, true); Assert.That(client.ConnectionEndpoint.Host, Is.EqualTo(options.CustomEndpointAddress.Host), "The connection endpoint should have used the custom endpoint URI from the options."); Assert.That(client.ServiceEndpoint.Host, Is.EqualTo(endpoint.Host), "The service endpoint should have used the namespace URI."); } + + /// + /// Verifies functionality of the constructor. + /// + /// + [Test] + [TestCase(true, "amqps")] + [TestCase(false, "amqp")] + public void ConstructorRespectsTheUseTlsOption(bool useTls, + string expectedScheme) + { + var options = new ServiceBusClientOptions + { + CustomEndpointAddress = new Uri("sb://iam.custom.net"), + TransportType = ServiceBusTransportType.AmqpTcp + }; + + var credential = new Mock(Mock.Of()); + var client = new AmqpClient("my.endpoint.com", credential.Object, options, useTls); + + Assert.That(client.ConnectionEndpoint.Host, Is.EqualTo(options.CustomEndpointAddress.Host), "The connection endpoint should have used the custom endpoint URI from the options."); + Assert.That(client.ConnectionEndpoint.Scheme, Is.EqualTo(expectedScheme), "The connection endpoint scheme should reflect the TLS setting."); + } } } diff --git a/sdk/servicebus/Azure.Messaging.ServiceBus/tests/Diagnostics/EventSourceTests.cs b/sdk/servicebus/Azure.Messaging.ServiceBus/tests/Diagnostics/EventSourceTests.cs index 07aae9beb38d9..3d949f41fac5e 100644 --- a/sdk/servicebus/Azure.Messaging.ServiceBus/tests/Diagnostics/EventSourceTests.cs +++ b/sdk/servicebus/Azure.Messaging.ServiceBus/tests/Diagnostics/EventSourceTests.cs @@ -1307,7 +1307,8 @@ private Mock GetMockConnection() mockConnection.Setup( connection => connection.CreateTransportClient( It.IsAny(), - It.IsAny())) + It.IsAny(), + It.IsAny())) .Returns(Mock.Of()); return mockConnection; diff --git a/sdk/servicebus/Azure.Messaging.ServiceBus/tests/Infrastructure/ServiceBusTestUtilities.cs b/sdk/servicebus/Azure.Messaging.ServiceBus/tests/Infrastructure/ServiceBusTestUtilities.cs index 4841fb9f4f477..ba1c0c21454d5 100644 --- a/sdk/servicebus/Azure.Messaging.ServiceBus/tests/Infrastructure/ServiceBusTestUtilities.cs +++ b/sdk/servicebus/Azure.Messaging.ServiceBus/tests/Infrastructure/ServiceBusTestUtilities.cs @@ -129,7 +129,8 @@ internal static Mock CreateMockConnection() mockConnection .Setup(connection => connection.CreateTransportClient( It.IsAny(), - It.IsAny())) + It.IsAny(), + It.IsAny())) .Returns(Mock.Of()); return mockConnection; diff --git a/sdk/servicebus/Azure.Messaging.ServiceBus/tests/Primitives/ServiceBusConnectionStringPropertiesTests.cs b/sdk/servicebus/Azure.Messaging.ServiceBus/tests/Primitives/ServiceBusConnectionStringPropertiesTests.cs index 667f0e39fdd9b..ed3c3b410be2b 100644 --- a/sdk/servicebus/Azure.Messaging.ServiceBus/tests/Primitives/ServiceBusConnectionStringPropertiesTests.cs +++ b/sdk/servicebus/Azure.Messaging.ServiceBus/tests/Primitives/ServiceBusConnectionStringPropertiesTests.cs @@ -4,7 +4,6 @@ using System; using System.Collections.Generic; using System.Reflection; -using Azure.Messaging.ServiceBus; using NUnit.Framework; using NUnit.Framework.Constraints; @@ -22,7 +21,7 @@ public class ServiceBusConnectionStringPropertiesTests /// Provides the reordered token test cases for the tests. /// /// - public static IEnumerable ParseDoesNotforceTokenOrderingCases() + public static IEnumerable ParseDoesNotForceTokenOrderingCases() { var endpoint = "test.endpoint.com"; var eventHub = "some-path"; @@ -62,7 +61,7 @@ public static IEnumerable ParseCorrectlyParsesPartialConnectionStringC } /// - /// Provides the invalid properties argument cases for the tests. + /// Provides the invalid properties argument cases for the tests. /// /// public static IEnumerable ToConnectionStringValidatesPropertiesCases() @@ -183,7 +182,7 @@ public void ParseCorrectlyParsesAnEntityConnectionString() /// /// [Test] - [TestCaseSource(nameof(ParseDoesNotforceTokenOrderingCases))] + [TestCaseSource(nameof(ParseDoesNotForceTokenOrderingCases))] public void ParseCorrectlyParsesPartialConnectionStrings(string connectionString, string endpoint, string eventHub, @@ -290,7 +289,7 @@ public void ParseToleratesSpacesBetweenValues() /// /// [Test] - [TestCaseSource(nameof(ParseDoesNotforceTokenOrderingCases))] + [TestCaseSource(nameof(ParseDoesNotForceTokenOrderingCases))] public void ParseDoesNotForceTokenOrdering(string connectionString, string endpoint, string eventHub, @@ -350,7 +349,7 @@ public void ParseDoesAcceptsHostNamesAndUrisForTheEndpoint(string endpointValue) valueUri = new Uri($"fake://{ endpointValue }"); } - Assert.That(parsed.Endpoint.Port, Is.EqualTo(-1), "The default port should be used."); + Assert.That(parsed.Endpoint.Port, Is.EqualTo(valueUri.IsDefaultPort ? -1 : valueUri.Port), "The default port should be used."); Assert.That(parsed.Endpoint.Host, Does.Not.Contain(" "), "The host name should not contain any spaces."); Assert.That(parsed.Endpoint.Host, Does.Not.Contain(":"), "The host name should not contain any port separators (:)."); Assert.That(parsed.Endpoint.Host, Does.Not.Contain(valueUri.Port), "The host name should not contain the port."); @@ -384,13 +383,59 @@ public void ParseDoesNotAllowAnInvalidEndpointFormat(string endpointValue) [TestCase("Endpoint=value.com;SharedAccessKeyName=[value];SharedAccessKey=[value];EntityPath")] [TestCase("Endpoint;SharedAccessKeyName=;SharedAccessKey;EntityPath=")] [TestCase("Endpoint=;SharedAccessKeyName;SharedAccessKey;EntityPath=")] + [TestCase("Endpoint=value.com;SharedAccessKeyName=[value];SharedAccessKey=[value];UseDevelopmentEmulator")] public void ParseConsidersMissingValuesAsMalformed(string connectionString) { Assert.That(() => ServiceBusConnectionStringProperties.Parse(connectionString), Throws.InstanceOf()); } /// - /// Verifies functionality of the + /// Verifies functionality of the + /// method. + /// + /// + public void ParseDetectsDevelopmentEmulatorUse() + { + var connectionString = "Endpoint=localhost:1234;SharedAccessKeyName=[name];SharedAccessKey=[value];UseDevelopmentEmulator=true"; + var parsed = ServiceBusConnectionStringProperties.Parse(connectionString); + + Assert.That(parsed.Endpoint.IsLoopback, Is.True, "The endpoint should be a local address."); + Assert.That(parsed.UseDevelopmentEmulator, Is.True, "The development emulator flag should have been set."); + } + + /// + /// Verifies functionality of the + /// method. + /// + /// + public void ParseRespectsDevelopmentEmulatorValue() + { + var connectionString = "Endpoint=localhost:1234;SharedAccessKeyName=[name];SharedAccessKey=[value];UseDevelopmentEmulator=false"; + var parsed = ServiceBusConnectionStringProperties.Parse(connectionString); + + Assert.That(parsed.Endpoint.IsLoopback, Is.True, "The endpoint should be a local address."); + Assert.That(parsed.UseDevelopmentEmulator, Is.False, "The development emulator flag should have been unset."); + } + + /// + /// Verifies functionality of the + /// method. + /// + /// + [TestCase("1")] + [TestCase("Unset")] + [TestCase("|")] + public void ParseToleratesDevelopmentEmulatorInvalidValues(string emulatorValue) + { + var connectionString = $"Endpoint=sb://localhost:1234;SharedAccessKeyName=[name];SharedAccessKey=[value];UseDevelopmentEmulator={ emulatorValue }"; + var parsed = ServiceBusConnectionStringProperties.Parse(connectionString); + + Assert.That(parsed.Endpoint.IsLoopback, Is.True, "The endpoint should be a local address."); + Assert.That(parsed.UseDevelopmentEmulator, Is.False, $"The development emulator flag should have been unset because { emulatorValue } is not a boolean."); + } + + /// + /// Verifies functionality of the /// method. /// /// @@ -402,7 +447,33 @@ public void ToConnectionStringValidatesProperties(ServiceBusConnectionStringProp } /// - /// Verifies functionality of the + /// Verifies functionality of the + /// method. + /// + /// + [Test] + public void ToConnectionStringProducesTheConnectionStringForTheLocalEmulator() + { + var properties = new ServiceBusConnectionStringProperties + { + Endpoint = new Uri("sb://127.0.0.1"), + EntityPath = "QueueName", + SharedAccessKey = "FaKe#$1324@@", + SharedAccessKeyName = "RootSharedAccessManagementKey", + UseDevelopmentEmulator = true + }; + + var connectionString = properties.ToConnectionString(); + Assert.That(connectionString, Is.Not.Null, "The connection string should not be null."); + Assert.That(connectionString.Length, Is.GreaterThan(0), "The connection string should have content."); + + var parsed = ServiceBusConnectionStringProperties.Parse(connectionString); + Assert.That(parsed, Is.Not.Null, "The connection string should be parsable."); + Assert.That(PropertiesAreEquivalent(properties, parsed), Is.True, "The connection string should parse into the source properties."); + } + + /// + /// Verifies functionality of the /// method. /// /// @@ -426,7 +497,7 @@ public void ToConnectionStringProducesTheConnectionStringForSharedAccessSignatur } /// - /// Verifies functionality of the + /// Verifies functionality of the /// method. /// /// @@ -451,7 +522,7 @@ public void ToConnectionStringProducesTheConnectionStringForSharedKeys() } /// - /// Verifies functionality of the + /// Verifies functionality of the /// method. /// /// @@ -512,6 +583,32 @@ public void ToConnectionStringAllowsSharedAccessSignatureAuthorization() Assert.That(() => properties.ToConnectionString(), Throws.Nothing, "Validation should accept the shared access signature authorization."); } + /// + /// Verifies functionality of the + /// method. + /// + /// + [Test] + [TestCase("localhost", true)] + [TestCase("127.0.0.1", true)] + [TestCase("www.microsoft.com", false)] + [TestCase("fake.servicebus.windows.net", false)] + [TestCase("weirdname:8080", false)] + public void ValidateRequiresLocalEndpointForDevelopmentEmulator(string endpoint, + bool isValid) + { + var fakeConnection = $"Endpoint=sb://{ endpoint };SharedAccessSignature=[not_real];UseDevelopmentEmulator=true"; + + if (isValid) + { + Assert.That(() => ServiceBusConnectionStringProperties.Parse(fakeConnection), Throws.Nothing, "Validation should allow a local endpoint."); + } + else + { + Assert.That(() => ServiceBusConnectionStringProperties.Parse(fakeConnection), Throws.ArgumentException.And.Message.Contains("local"), "Parse should enforce that the endpoint is a local address."); + } + } + /// /// Compares two instances for /// structural equality. @@ -539,7 +636,9 @@ private static bool PropertiesAreEquivalent(ServiceBusConnectionStringProperties && string.Equals(first.EntityPath, second.EntityPath, StringComparison.OrdinalIgnoreCase) && string.Equals(first.SharedAccessSignature, second.SharedAccessSignature, StringComparison.OrdinalIgnoreCase) && string.Equals(first.SharedAccessKeyName, second.SharedAccessKeyName, StringComparison.OrdinalIgnoreCase) - && string.Equals(first.SharedAccessKey, second.SharedAccessKey, StringComparison.OrdinalIgnoreCase); + && string.Equals(first.SharedAccessKey, second.SharedAccessKey, StringComparison.OrdinalIgnoreCase) + && string.Equals(first.SharedAccessKey, second.SharedAccessKey, StringComparison.OrdinalIgnoreCase) + && (first.UseDevelopmentEmulator == second.UseDevelopmentEmulator); } /// diff --git a/sdk/servicebus/Azure.Messaging.ServiceBus/tests/Primitives/ServiceBusConnectionTests.cs b/sdk/servicebus/Azure.Messaging.ServiceBus/tests/Primitives/ServiceBusConnectionTests.cs index aa160d3f45c38..ca96c1346d60f 100644 --- a/sdk/servicebus/Azure.Messaging.ServiceBus/tests/Primitives/ServiceBusConnectionTests.cs +++ b/sdk/servicebus/Azure.Messaging.ServiceBus/tests/Primitives/ServiceBusConnectionTests.cs @@ -206,7 +206,7 @@ public void ConstructorWithExpandedArgumentsValidatesOptions() /// /// [Test] - public void ContructorWithConnectionStringCreatesTheTransportClient() + public void ConstructorWithConnectionStringCreatesTheTransportClient() { var connection = new ServiceBusConnection("Endpoint=sb://not-real.servicebus.windows.net/;SharedAccessKeyName=DummyKey;SharedAccessKey=[not_real]", new ServiceBusClientOptions()); Assert.That(connection.InnerClient, Is.Not.Null); @@ -218,7 +218,7 @@ public void ContructorWithConnectionStringCreatesTheTransportClient() /// /// [Test] - public void ContructorWithConnectionStringUsingSharedAccessSignatureCreatesTheCorrectTransportCredential() + public void ConstructorWithConnectionStringUsingSharedAccessSignatureCreatesTheCorrectTransportCredential() { var sasToken = new SharedAccessSignature("hub", "root", "abc1234").Value; var connection = new ObservableTransportClientMock($"Endpoint=sb://not-real.servicebus.windows.net/;EntityPath=fake;SharedAccessSignature={ sasToken }", new ServiceBusClientOptions()); @@ -233,7 +233,39 @@ public void ContructorWithConnectionStringUsingSharedAccessSignatureCreatesTheCo /// /// [Test] - public void ContructorWithTokenCredentialCreatesTheTransportClient() + public void ConstructorWithConnectionStringAndDevelopmentEmulatorDoesNotUseTls() + { + var endpoint = new Uri("sb://localhost:1234", UriKind.Absolute); + var fakeConnection = $"Endpoint={ endpoint };SharedAccessKeyName=DummyKey;SharedAccessKey=[not_real];EntityPath=ehName;UseDevelopmentEmulator=true"; + var connection = new ReadableTransportOptionsMock(fakeConnection); + + Assert.That(connection.UseTls.HasValue, Is.True, "The connection should have initialized the TLS flag."); + Assert.That(connection.UseTls.Value, Is.False, "The options should not use TLS for the development emulator."); + } + + /// + /// Verifies functionality of the + /// constructor. + /// + /// + [Test] + public void ConstructorWithConnectionStringAnNoDevelopmentEmulatorUsesTls() + { + var endpoint = new Uri("sb://localhost:1234", UriKind.Absolute); + var fakeConnection = $"Endpoint={ endpoint };SharedAccessKeyName=DummyKey;SharedAccessKey=[not_real];EntityPath=ehName"; + var connection = new ReadableTransportOptionsMock(fakeConnection); + + Assert.That(connection.UseTls.HasValue, Is.True, "The connection should have initialized the TLS flag."); + Assert.That(connection.UseTls.Value, Is.True, "The options should use TLS for communcating with the service."); + } + + /// + /// Verifies functionality of the + /// constructor. + /// + /// + [Test] + public void ConstructorWithTokenCredentialCreatesTheTransportClient() { var fullyQualifiedNamespace = "my.ServiceBus.com"; var keyName = "aWonderfulKey"; @@ -252,7 +284,7 @@ public void ContructorWithTokenCredentialCreatesTheTransportClient() /// /// [Test] - public void ContructorWithSharedKeyCredentialCreatesTheTransportClient() + public void ConstructorWithSharedKeyCredentialCreatesTheTransportClient() { var fullyQualifiedNamespace = "my.ServiceBus.com"; var keyName = "aWonderfulKey"; @@ -270,7 +302,7 @@ public void ContructorWithSharedKeyCredentialCreatesTheTransportClient() /// /// [Test] - public void ContructorWithSasCredentialCreatesTheTransportClient() + public void ConstructorWithSasCredentialCreatesTheTransportClient() { var fullyQualifiedNamespace = "my.ServiceBus.com"; var keyName = "aWonderfulKey"; @@ -430,6 +462,50 @@ public void CreateWithCredentialDisallowsUnknownCredentialTypes() Assert.That(() => ServiceBusConnection.CreateWithCredential("fqns", credential, options), Throws.InstanceOf()); } + /// + /// Allows for the options used by the client to be exposed for testing purposes. + /// + /// + internal class ReadableTransportOptionsMock : ServiceBusConnection + { + public ServiceBusClientOptions TransportClientOptions; + + public bool? UseTls; + + private ObservableTransportClient _transportClient; + + public ReadableTransportOptionsMock(string connectionString, + ServiceBusClientOptions clientOptions = default) : base(connectionString, clientOptions ?? new()) + { + } + + public ReadableTransportOptionsMock(string fullyQualifiedNamespace, + TokenCredential credential, + ServiceBusClientOptions clientOptions = default) : base(fullyQualifiedNamespace, credential, clientOptions ?? new()) + { + } + + public ReadableTransportOptionsMock(string fullyQualifiedNamespace, + AzureNamedKeyCredential credential, + ServiceBusClientOptions clientOptions = default) : base(fullyQualifiedNamespace, credential, clientOptions ?? new()) + { + } + + public ReadableTransportOptionsMock(string fullyQualifiedNamespace, + AzureSasCredential credential, + ServiceBusClientOptions clientOptions = default) : base(fullyQualifiedNamespace, credential, clientOptions ?? new()) + { + } + + internal override TransportClient CreateTransportClient(ServiceBusTokenCredential credential, ServiceBusClientOptions options, bool useTls = true) + { + UseTls = useTls; + TransportClientOptions = options; + _transportClient = new ObservableTransportClient(); + return _transportClient; + } + } + /// /// Allows for the operations performed by the client to be observed for testing purposes. /// @@ -501,7 +577,8 @@ public ObservableTransportClientMock(string fullyQualifiedNamespace, } internal override TransportClient CreateTransportClient(ServiceBusTokenCredential credential, - ServiceBusClientOptions options) + ServiceBusClientOptions options, + bool useTls) { TransportClientCredential = credential; TransportClient ??= new(); diff --git a/sdk/servicebus/Azure.Messaging.ServiceBus/tests/Processor/ProcessorTests.cs b/sdk/servicebus/Azure.Messaging.ServiceBus/tests/Processor/ProcessorTests.cs index 61e14485b8816..c639917ae7415 100644 --- a/sdk/servicebus/Azure.Messaging.ServiceBus/tests/Processor/ProcessorTests.cs +++ b/sdk/servicebus/Azure.Messaging.ServiceBus/tests/Processor/ProcessorTests.cs @@ -442,7 +442,8 @@ public async Task CannotStartProcessorWhenConnectionIsClosed() mockConnection .Setup(connection => connection.CreateTransportClient( It.IsAny(), - It.IsAny())) + It.IsAny(), + It.IsAny())) .Returns(mockTransportClient.Object); var processor = new ServiceBusProcessor(