diff --git a/sdk/eventhub/Azure.Messaging.EventHubs.Processor/api/Azure.Messaging.EventHubs.Processor.netstandard2.0.cs b/sdk/eventhub/Azure.Messaging.EventHubs.Processor/api/Azure.Messaging.EventHubs.Processor.netstandard2.0.cs index a5aa8f6c0c14..ff8ed3c52970 100644 --- a/sdk/eventhub/Azure.Messaging.EventHubs.Processor/api/Azure.Messaging.EventHubs.Processor.netstandard2.0.cs +++ b/sdk/eventhub/Azure.Messaging.EventHubs.Processor/api/Azure.Messaging.EventHubs.Processor.netstandard2.0.cs @@ -7,6 +7,7 @@ public EventProcessorClient(Azure.Storage.Blobs.BlobContainerClient checkpointSt public EventProcessorClient(Azure.Storage.Blobs.BlobContainerClient checkpointStore, string consumerGroup, string connectionString, Azure.Messaging.EventHubs.EventProcessorClientOptions clientOptions) { } public EventProcessorClient(Azure.Storage.Blobs.BlobContainerClient checkpointStore, string consumerGroup, string connectionString, string eventHubName) { } public EventProcessorClient(Azure.Storage.Blobs.BlobContainerClient checkpointStore, string consumerGroup, string fullyQualifiedNamespace, string eventHubName, Azure.Core.TokenCredential credential, Azure.Messaging.EventHubs.EventProcessorClientOptions clientOptions = null) { } + public EventProcessorClient(Azure.Storage.Blobs.BlobContainerClient checkpointStore, string consumerGroup, string fullyQualifiedNamespace, string eventHubName, Azure.Messaging.EventHubs.EventHubsSharedAccessKeyCredential credential, Azure.Messaging.EventHubs.EventProcessorClientOptions clientOptions = null) { } public EventProcessorClient(Azure.Storage.Blobs.BlobContainerClient checkpointStore, string consumerGroup, string connectionString, string eventHubName, Azure.Messaging.EventHubs.EventProcessorClientOptions clientOptions) { } public new string ConsumerGroup { get { throw null; } } public new string EventHubName { get { throw null; } } diff --git a/sdk/eventhub/Azure.Messaging.EventHubs.Processor/src/EventProcessorClient.cs b/sdk/eventhub/Azure.Messaging.EventHubs.Processor/src/EventProcessorClient.cs index ae44438dc2eb..3ad9d1f90e2b 100644 --- a/sdk/eventhub/Azure.Messaging.EventHubs.Processor/src/EventProcessorClient.cs +++ b/sdk/eventhub/Azure.Messaging.EventHubs.Processor/src/EventProcessorClient.cs @@ -405,6 +405,33 @@ public EventProcessorClient(BlobContainerClient checkpointStore, StorageManager = CreateStorageManager(checkpointStore); } + /// + /// Initializes a new instance of the class. + /// + /// + /// The client responsible for persisting checkpoints and processor state to durable storage. The associated container is expected to exist. + /// The name of the consumer group this processor is associated with. Events are read in the context of this group. + /// The fully qualified Event Hubs namespace to connect to. This is likely to be similar to {yournamespace}.servicebus.windows.net. + /// The name of the specific Event Hub to associate the processor with. + /// The Event Hubs shared access key credential to use for authorization. Access controls may be specified by the Event Hubs namespace or the requested Event Hub, depending on Azure configuration. + /// The set of options to use for this processor. + /// + /// + /// The container associated with the is expected to exist; the + /// does not assume the ability to manage the storage account and is safe to run without permission to manage the storage account. + /// + /// + public EventProcessorClient(BlobContainerClient checkpointStore, + string consumerGroup, + string fullyQualifiedNamespace, + string eventHubName, + EventHubsSharedAccessKeyCredential credential, + EventProcessorClientOptions clientOptions = default) : base((clientOptions ?? DefaultClientOptions).CacheEventCount, consumerGroup, fullyQualifiedNamespace, eventHubName, credential, CreateOptions(clientOptions)) + { + Argument.AssertNotNull(checkpointStore, nameof(checkpointStore)); + StorageManager = CreateStorageManager(checkpointStore); + } + /// /// Initializes a new instance of the class. /// @@ -432,6 +459,36 @@ public EventProcessorClient(BlobContainerClient checkpointStore, StorageManager = CreateStorageManager(checkpointStore); } + /// + /// Initializes a new instance of the class. + /// + /// + /// Responsible for creation of checkpoints and for ownership claim. + /// The name of the consumer group this processor is associated with. Events are read in the context of this group. + /// The fully qualified Event Hubs namespace to connect to. This is likely to be similar to {yournamespace}.servicebus.windows.net. + /// The name of the specific Event Hub to associate the processor with. + /// The maximum number of events that will be read from the Event Hubs service and held in a local memory cache when reading is active and events are being emitted to an enumerator for processing. + /// A shared access key credential to satisfy base class requirements; this credential may not be null but will only be used in the case that has not been overridden. + /// The set of options to use for this processor. + /// + /// + /// This constructor is intended only to support functional testing and mocking; it should not be used for production scenarios. + /// + /// + internal EventProcessorClient(StorageManager storageManager, + string consumerGroup, + string fullyQualifiedNamespace, + string eventHubName, + int cacheEventCount, + EventHubsSharedAccessKeyCredential credential, + EventProcessorOptions clientOptions) : base(cacheEventCount, consumerGroup, fullyQualifiedNamespace, eventHubName, credential, clientOptions) + { + Argument.AssertNotNull(storageManager, nameof(storageManager)); + + DefaultStartingPosition = (clientOptions?.DefaultStartingPosition ?? DefaultStartingPosition); + StorageManager = storageManager; + } + /// /// Initializes a new instance of the class. /// diff --git a/sdk/eventhub/Azure.Messaging.EventHubs.Processor/tests/Processor/EventProcessorClientLiveTests.cs b/sdk/eventhub/Azure.Messaging.EventHubs.Processor/tests/Processor/EventProcessorClientLiveTests.cs index 387e77496a25..cf513a24b803 100644 --- a/sdk/eventhub/Azure.Messaging.EventHubs.Processor/tests/Processor/EventProcessorClientLiveTests.cs +++ b/sdk/eventhub/Azure.Messaging.EventHubs.Processor/tests/Processor/EventProcessorClientLiveTests.cs @@ -134,6 +134,56 @@ public async Task EventsCanBeReadByOneProcessorClientUsingAnIdentityCredential() } } + /// + /// Verifies that the can read a set of published events. + /// + /// + [Test] + public async Task EventsCanBeReadByOneProcessorClientUsingTheSharedKeyCredential() + { + // Setup the environment. + + await using EventHubScope scope = await EventHubScope.CreateAsync(2); + var connectionString = EventHubsTestEnvironment.Instance.BuildConnectionStringForEventHub(scope.EventHubName); + + using var cancellationSource = new CancellationTokenSource(); + cancellationSource.CancelAfter(EventHubsTestEnvironment.Instance.TestExecutionTimeLimit); + + // Send a set of events. + + var sourceEvents = EventGenerator.CreateEvents(50).ToList(); + var sentCount = await SendEvents(connectionString, sourceEvents, cancellationSource.Token); + + Assert.That(sentCount, Is.EqualTo(sourceEvents.Count), "Not all of the source events were sent."); + + // Attempt to read back the events. + + var processedEvents = new ConcurrentDictionary(); + var completionSource = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + var options = new EventProcessorOptions { LoadBalancingUpdateInterval = TimeSpan.FromMilliseconds(250) }; + var processor = CreateProcessorWithSharedAccessKey(scope.ConsumerGroups.First(), scope.EventHubName, options: options); + + processor.ProcessErrorAsync += CreateAssertingErrorHandler(); + processor.ProcessEventAsync += CreateEventTrackingHandler(sentCount, processedEvents, completionSource, cancellationSource.Token); + + await processor.StartProcessingAsync(cancellationSource.Token); + + await Task.WhenAny(completionSource.Task, Task.Delay(Timeout.Infinite, cancellationSource.Token)); + Assert.That(cancellationSource.IsCancellationRequested, Is.False, $"The cancellation token should not have been signaled. { processedEvents.Count } events were processed."); + + await processor.StopProcessingAsync(cancellationSource.Token); + cancellationSource.Cancel(); + + // Validate the events that were processed. + + foreach (var sourceEvent in sourceEvents) + { + var sourceId = sourceEvent.Properties[EventGenerator.IdPropertyName].ToString(); + Assert.That(processedEvents.TryGetValue(sourceId, out var processedEvent), Is.True, $"The event with custom identifier [{ sourceId }] was not processed." ); + Assert.That(sourceEvent.IsEquivalentTo(processedEvent), $"The event with custom identifier [{ sourceId }] did not match the corresponding processed event."); + } + } + /// /// Verifies that the can read a set of published events. /// @@ -477,6 +527,29 @@ private EventProcessorClient CreateProcessorWithIdentity(string consumerGroup, return new TestEventProcessorClient(storageManager, consumerGroup, EventHubsTestEnvironment.Instance.FullyQualifiedNamespace, eventHubName, credential, createConnection, options); } + /// + /// Creates an that uses mock storage and + /// a connection based on an identity credential. + /// + /// + /// The consumer group for the processor. + /// The name of the Event Hub for the processor. + /// The set of client options to pass. + /// + /// The processor instance. + /// + private EventProcessorClient CreateProcessorWithSharedAccessKey(string consumerGroup, + string eventHubName, + StorageManager storageManager = default, + EventProcessorOptions options = default) + { + var credential = new EventHubsSharedAccessKeyCredential(EventHubsTestEnvironment.Instance.SharedAccessKeyName, EventHubsTestEnvironment.Instance.SharedAccessKey); + EventHubConnection createConnection() => new EventHubConnection(EventHubsTestEnvironment.Instance.FullyQualifiedNamespace, eventHubName, credential); + + storageManager ??= new InMemoryStorageManager(_=> {}); + return new TestEventProcessorClient(storageManager, consumerGroup, EventHubsTestEnvironment.Instance.FullyQualifiedNamespace, eventHubName, credential, createConnection, options); + } + /// /// Sends a set of events using a new producer to do so. /// @@ -575,6 +648,17 @@ public class TestEventProcessorClient : EventProcessorClient { private readonly Func InjectedConnectionFactory; + internal TestEventProcessorClient(StorageManager storageManager, + string consumerGroup, + string fullyQualifiedNamespace, + string eventHubName, + EventHubsSharedAccessKeyCredential credential, + Func connectionFactory, + EventProcessorOptions options) : base(storageManager, consumerGroup, fullyQualifiedNamespace, eventHubName, 100, credential, options) + { + InjectedConnectionFactory = connectionFactory; + } + internal TestEventProcessorClient(StorageManager storageManager, string consumerGroup, string fullyQualifiedNamespace, diff --git a/sdk/eventhub/Azure.Messaging.EventHubs.Processor/tests/Processor/EventProcessorClientTests.cs b/sdk/eventhub/Azure.Messaging.EventHubs.Processor/tests/Processor/EventProcessorClientTests.cs index 1dce55fe8e86..053781ca4f96 100644 --- a/sdk/eventhub/Azure.Messaging.EventHubs.Processor/tests/Processor/EventProcessorClientTests.cs +++ b/sdk/eventhub/Azure.Messaging.EventHubs.Processor/tests/Processor/EventProcessorClientTests.cs @@ -37,7 +37,8 @@ public class EventProcessorClientTests public void ConstructorsValidateTheConsumerGroup(string consumerGroup) { Assert.That(() => new EventProcessorClient(Mock.Of(), consumerGroup, "dummyConnection", new EventProcessorClientOptions()), Throws.InstanceOf(), "The connection string constructor should validate the consumer group."); - Assert.That(() => new EventProcessorClient(Mock.Of(), consumerGroup, "dummyNamespace", "dummyEventHub", Mock.Of(), new EventProcessorClientOptions()), Throws.InstanceOf(), "The namespace constructor should validate the consumer group."); + Assert.That(() => new EventProcessorClient(Mock.Of(), consumerGroup, "dummyNamespace", "dummyEventHub", Mock.Of(), new EventProcessorClientOptions()), Throws.InstanceOf(), "The token credential constructor should validate the consumer group."); + Assert.That(() => new EventProcessorClient(Mock.Of(), consumerGroup, "dummyNamespace", "dummyEventHub", new EventHubsSharedAccessKeyCredential("key", "value"), new EventProcessorClientOptions()), Throws.InstanceOf(), "The shared key credential constructor should validate the consumer group."); } /// @@ -51,7 +52,8 @@ public void ConstructorsValidateTheBlobContainerClient() var fakeConnection = "Endpoint=sb://not-real.servicebus.windows.net/;SharedAccessKeyName=DummyKey;SharedAccessKey=[not_real];EntityPath=fake"; Assert.That(() => new EventProcessorClient(null, "consumerGroup", fakeConnection, new EventProcessorClientOptions()), Throws.InstanceOf(), "The connection string constructor should validate the blob container client."); - Assert.That(() => new EventProcessorClient(null, "consumerGroup", "dummyNamespace", "dummyEventHub", Mock.Of(), new EventProcessorClientOptions()), Throws.InstanceOf(), "The namespace constructor should validate the blob container client."); + Assert.That(() => new EventProcessorClient(null, "consumerGroup", "dummyNamespace", "dummyEventHub", Mock.Of(), new EventProcessorClientOptions()), Throws.InstanceOf(), "The token credential constructor should validate the blob container client."); + Assert.That(() => new EventProcessorClient(null, "consumerGroup", "dummyNamespace", "dummyEventHub", new EventHubsSharedAccessKeyCredential("key", "value"), new EventProcessorClientOptions()), Throws.InstanceOf(), "The shared key credential constructor should validate the blob container client."); } /// @@ -77,7 +79,8 @@ public void ConstructorsValidateTheConnectionString(string connectionString) [TestCase("http://namspace.servciebus.windows.com")] public void ConstructorValidatesTheNamespace(string constructorArgument) { - Assert.That(() => new EventProcessorClient(Mock.Of(), EventHubConsumerClient.DefaultConsumerGroupName, constructorArgument, "dummy", Mock.Of()), Throws.InstanceOf()); + Assert.That(() => new EventProcessorClient(Mock.Of(), EventHubConsumerClient.DefaultConsumerGroupName, constructorArgument, "dummy", Mock.Of()), Throws.InstanceOf(), "The token credential should validate."); + Assert.That(() => new EventProcessorClient(Mock.Of(), EventHubConsumerClient.DefaultConsumerGroupName, constructorArgument, "dummy", new EventHubsSharedAccessKeyCredential("key", "value")), Throws.InstanceOf(), "The shared key credential should validate."); } /// @@ -89,7 +92,8 @@ public void ConstructorValidatesTheNamespace(string constructorArgument) [TestCase("")] public void ConstructorValidatesTheEventHub(string constructorArgument) { - Assert.That(() => new EventProcessorClient(Mock.Of(), EventHubConsumerClient.DefaultConsumerGroupName, "namespace", constructorArgument, Mock.Of()), Throws.InstanceOf()); + Assert.That(() => new EventProcessorClient(Mock.Of(), EventHubConsumerClient.DefaultConsumerGroupName, "namespace", constructorArgument, Mock.Of()), Throws.InstanceOf(), "The token credential should validate."); + Assert.That(() => new EventProcessorClient(Mock.Of(), EventHubConsumerClient.DefaultConsumerGroupName, "namespace", constructorArgument, new EventHubsSharedAccessKeyCredential("key", "value")), Throws.InstanceOf(), "The shared key credential should validate."); } /// @@ -99,7 +103,8 @@ public void ConstructorValidatesTheEventHub(string constructorArgument) [Test] public void ConstructorValidatesTheCredential() { - Assert.That(() => new EventProcessorClient(Mock.Of(), EventHubConsumerClient.DefaultConsumerGroupName, "namespace", "hubName", default(TokenCredential)), Throws.ArgumentNullException); + Assert.That(() => new EventProcessorClient(Mock.Of(), EventHubConsumerClient.DefaultConsumerGroupName, "namespace", "hubName", default(TokenCredential)), Throws.ArgumentNullException, "The token credential should validate."); + Assert.That(() => new EventProcessorClient(Mock.Of(), EventHubConsumerClient.DefaultConsumerGroupName, "namespace", "hubName", default(EventHubsSharedAccessKeyCredential)), Throws.ArgumentNullException, "The shared key credential should validate."); } /// diff --git a/sdk/eventhub/Azure.Messaging.EventHubs.Shared/src/Authorization/EventHubSharedKeyCredential.cs b/sdk/eventhub/Azure.Messaging.EventHubs.Shared/src/Authorization/EventHubSharedKeyCredential.cs deleted file mode 100755 index 64ccedc1acdc..000000000000 --- a/sdk/eventhub/Azure.Messaging.EventHubs.Shared/src/Authorization/EventHubSharedKeyCredential.cs +++ /dev/null @@ -1,119 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -using System; -using System.Threading; -using System.Threading.Tasks; -using Azure.Core; -using Azure.Messaging.EventHubs.Authorization; - -namespace Azure.Messaging.EventHubs -{ - /// - /// Provides a credential based on a shared access signature for a given - /// Event Hub instance. - /// - /// - /// - /// - internal sealed class EventHubSharedKeyCredential : TokenCredential - { - /// - /// The name of the shared access key to be used for authorization, as - /// reported by the Azure portal. - /// - /// - private string SharedAccessKeyName { get; set; } - - /// - /// The value of the shared access key to be used for authorization, as - /// reported by the Azure portal. - /// - /// - private string SharedAccessKey { get; set; } - - /// - /// A reference to a corresponding SharedAccessSignatureCredential. - /// - /// - private SharedAccessSignatureCredential SharedAccessSignatureCredential { get; set; } - - /// - /// Initializes a new instance of the class. - /// - /// - /// The name of the shared access key to be used for authorization, as reported by the Azure portal. - /// The value of the shared access key to be used for authorization, as reported by the Azure portal. - /// - public EventHubSharedKeyCredential(string sharedAccessKeyName, - string sharedAccessKey) - { - Argument.AssertNotNullOrEmpty(sharedAccessKeyName, nameof(sharedAccessKeyName)); - Argument.AssertNotNullOrEmpty(sharedAccessKey, nameof(sharedAccessKey)); - - SharedAccessKeyName = sharedAccessKeyName; - SharedAccessKey = sharedAccessKey; - } - - /// - /// Retrieves the token that represents the shared access signature credential, for - /// use in authorization against an Event Hub. - /// - /// - /// The details of the authentication request. - /// The token used to request cancellation of the operation. - /// - /// The token representing the shared access signature for this credential. - /// - public override AccessToken GetToken(TokenRequestContext requestContext, CancellationToken cancellationToken) => throw new InvalidOperationException(Resources.SharedKeyCredentialCannotGenerateTokens); - - /// - /// Retrieves the token that represents the shared access signature credential, for - /// use in authorization against an Event Hub. - /// - /// - /// The details of the authentication request. - /// The token used to request cancellation of the operation. - /// - /// The token representing the shared access signature for this credential. - /// - public override ValueTask GetTokenAsync(TokenRequestContext requestContext, CancellationToken cancellationToken) => throw new InvalidOperationException(Resources.SharedKeyCredentialCannotGenerateTokens); - - /// - /// Allows the rotation of Shared Access Signatures. - /// - /// - /// The name of the shared access key that the signature should be based on. - /// The value of the shared access key for the signature. - /// - public void UpdateSharedAccessKey(string keyName, - string keyValue) - { - Argument.AssertNotNullOrEmpty(keyName, nameof(keyName)); - Argument.AssertNotNullOrEmpty(keyValue, nameof(keyValue)); - - SharedAccessKeyName = keyName; - SharedAccessKey = keyValue; - - SharedAccessSignatureCredential?.UpdateSharedAccessKey(keyName, keyValue); - } - - /// - /// Coverts to shared access signature credential. - /// It retains a reference to the generated SharedAccessSignatureCredential. - /// - /// - /// The Event Hubs resource to which the token is intended to serve as authorization. - /// The duration that the signature should be considered valid; if not specified, a default will be assumed. - /// - /// A new based on the requested shared access key. - /// - internal SharedAccessSignatureCredential AsSharedAccessSignatureCredential(string eventHubResource, - TimeSpan? signatureValidityDuration = default) - { - SharedAccessSignatureCredential = new SharedAccessSignatureCredential(new SharedAccessSignature(eventHubResource, SharedAccessKeyName, SharedAccessKey, signatureValidityDuration)); - - return SharedAccessSignatureCredential; - } - } -} diff --git a/sdk/eventhub/Azure.Messaging.EventHubs.Shared/src/Authorization/EventHubTokenCredential.cs b/sdk/eventhub/Azure.Messaging.EventHubs.Shared/src/Authorization/EventHubTokenCredential.cs old mode 100755 new mode 100644 index 0da9234d4590..b6a7f72a6699 --- a/sdk/eventhub/Azure.Messaging.EventHubs.Shared/src/Authorization/EventHubTokenCredential.cs +++ b/sdk/eventhub/Azure.Messaging.EventHubs.Shared/src/Authorization/EventHubTokenCredential.cs @@ -56,8 +56,7 @@ public EventHubTokenCredential(TokenCredential tokenCredential, Resource = eventHubResource; IsSharedAccessSignatureCredential = - (tokenCredential is EventHubSharedKeyCredential) - || (tokenCredential is SharedAccessSignatureCredential) + (tokenCredential is SharedAccessSignatureCredential) || ((tokenCredential as EventHubTokenCredential)?.IsSharedAccessSignatureCredential == true); } diff --git a/sdk/eventhub/Azure.Messaging.EventHubs.Shared/src/Authorization/SharedAccessSignatureCredential.cs b/sdk/eventhub/Azure.Messaging.EventHubs.Shared/src/Authorization/SharedAccessSignatureCredential.cs index 49cc4106e8f5..4029e64a8851 100644 --- a/sdk/eventhub/Azure.Messaging.EventHubs.Shared/src/Authorization/SharedAccessSignatureCredential.cs +++ b/sdk/eventhub/Azure.Messaging.EventHubs.Shared/src/Authorization/SharedAccessSignatureCredential.cs @@ -90,7 +90,7 @@ public override ValueTask GetTokenAsync(TokenRequestContext request CancellationToken cancellationToken) => new ValueTask(GetToken(requestContext, cancellationToken)); /// - /// It creates a new shared signature using the key name and the key value passed as + /// Creates a new shared signature using the key name and the key value passed as /// input allowing credentials rotation. A call will not extend the signature duration. /// /// @@ -108,5 +108,19 @@ internal void UpdateSharedAccessKey(string keyName, string keyValue) SharedAccessSignature.SignatureExpiration); } } + + /// + /// Creates a new shared signature allowing credentials rotation. + /// + /// + /// The shared access signature that forms the basis of this security token. + /// + internal void UpdateSharedAccessSignature(string signature) + { + lock (SignatureSyncRoot) + { + SharedAccessSignature = new SharedAccessSignature(signature); + } + } } } diff --git a/sdk/eventhub/Azure.Messaging.EventHubs.Shared/src/Core/ConnectionStringParser.cs b/sdk/eventhub/Azure.Messaging.EventHubs.Shared/src/Core/ConnectionStringParser.cs deleted file mode 100755 index 3f366c3c3dda..000000000000 --- a/sdk/eventhub/Azure.Messaging.EventHubs.Shared/src/Core/ConnectionStringParser.cs +++ /dev/null @@ -1,174 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -using System; -using Azure.Core; - -namespace Azure.Messaging.EventHubs.Core -{ - /// - /// Allows for parsing Event Hubs connection strings. - /// - /// - internal static class ConnectionStringParser - { - /// The token that identifies the endpoint address for the Event Hubs namespace. - private const string EndpointToken = "Endpoint"; - - /// The token that identifies the name of a specific Event Hub under the namespace. - private const string EventHubNameToken = "EntityPath"; - - /// The token that identifies the name of a shared access key. - private const string SharedAccessKeyNameToken = "SharedAccessKeyName"; - - /// The token that identifies the value of a shared access key. - private const string SharedAccessKeyValueToken = "SharedAccessKey"; - - /// The token that identifies the value of a shared access signature. - private const string SharedAccessSignatureToken = "SharedAccessSignature"; - - /// The character used to separate a token and its value in the connection string. - private const char TokenValueSeparator = '='; - - /// The character used to mark the beginning of a new token/value pair in the connection string. - private const char TokenValuePairDelimiter = ';'; - - /// The name of the protocol used by an Event Hubs endpoint. - private const string EventHubsEndpointSchemeName = "sb"; - - /// The formatted protocol used by an Event Hubs endpoint. - private static readonly string EventHubsEndpointScheme = $"{ EventHubsEndpointSchemeName }{ Uri.SchemeDelimiter }"; - - /// - /// Parses the specified Event Hubs connection string into its component properties. - /// - /// - /// The connection string to parse. - /// - /// The component properties parsed from the connection string. - /// - /// - /// - public static ConnectionStringProperties Parse(string connectionString) - { - Argument.AssertNotNullOrEmpty(connectionString, nameof(connectionString)); - - int tokenPositionModifier = (connectionString[0] == TokenValuePairDelimiter) ? 0 : 1; - int lastPosition = 0; - int currentPosition = 0; - int valueStart; - - string slice; - string token; - string value; - - var parsedValues = - ( - EndpointToken: default(UriBuilder), - EventHubNameToken: default(string), - SharedAccessKeyNameToken: default(string), - SharedAccessKeyValueToken: default(string), - SharedAccessSignatureToken: default(string) - ); - - while (currentPosition != -1) - { - // Slice the string into the next token/value pair. - - currentPosition = connectionString.IndexOf(TokenValuePairDelimiter, lastPosition + 1); - - if (currentPosition >= 0) - { - slice = connectionString.Substring(lastPosition, (currentPosition - lastPosition)); - } - else - { - slice = connectionString.Substring(lastPosition); - } - - // Break the token and value apart, if this is a legal pair. - - valueStart = slice.IndexOf(TokenValueSeparator); - - if (valueStart >= 0) - { - token = slice.Substring((1 - tokenPositionModifier), (valueStart - 1 + tokenPositionModifier)); - value = slice.Substring(valueStart + 1); - - // Guard against leading and trailing spaces, only trimming if there is a need. - - if ((!string.IsNullOrEmpty(token)) && (char.IsWhiteSpace(token[0])) || char.IsWhiteSpace(token[token.Length - 1])) - { - token = token.Trim(); - } - - if ((!string.IsNullOrEmpty(value)) && (char.IsWhiteSpace(value[0]) || char.IsWhiteSpace(value[value.Length - 1]))) - { - value = value.Trim(); - } - - // If there was no value for a key, then consider the connection string to - // be malformed. - - if (string.IsNullOrEmpty(value)) - { - throw new FormatException(Resources.InvalidConnectionString); - } - - // Compare the token against the known connection string properties and capture the - // pair if they are a known attribute. - - if (string.Compare(EndpointToken, token, StringComparison.OrdinalIgnoreCase) == 0) - { - parsedValues.EndpointToken = new UriBuilder(value) - { - Scheme = EventHubsEndpointScheme, - Port = -1 - }; - - if ((string.Compare(parsedValues.EndpointToken.Scheme, EventHubsEndpointSchemeName, StringComparison.OrdinalIgnoreCase) != 0) - || (Uri.CheckHostName(parsedValues.EndpointToken.Host) == UriHostNameType.Unknown)) - { - throw new FormatException(Resources.InvalidConnectionString); - } - } - else if (string.Compare(EventHubNameToken, token, StringComparison.OrdinalIgnoreCase) == 0) - { - parsedValues.EventHubNameToken = value; - } - else if (string.Compare(SharedAccessKeyNameToken, token, StringComparison.OrdinalIgnoreCase) == 0) - { - parsedValues.SharedAccessKeyNameToken = value; - } - else if (string.Compare(SharedAccessKeyValueToken, token, StringComparison.OrdinalIgnoreCase) == 0) - { - parsedValues.SharedAccessKeyValueToken = value; - } - else if (string.Compare(SharedAccessSignatureToken, token, StringComparison.OrdinalIgnoreCase) == 0) - { - parsedValues.SharedAccessSignatureToken = value; - } - } - else if ((slice.Length != 1) || (slice[0] != TokenValuePairDelimiter)) - { - // This wasn't a legal pair and it is not simply a trailing delimiter; consider - // the connection string to be malformed. - - throw new FormatException(Resources.InvalidConnectionString); - } - - tokenPositionModifier = 0; - lastPosition = currentPosition; - } - - return new ConnectionStringProperties - ( - parsedValues.EndpointToken?.Uri, - parsedValues.EventHubNameToken, - parsedValues.SharedAccessKeyNameToken, - parsedValues.SharedAccessKeyValueToken, - parsedValues.SharedAccessSignatureToken - ); - } - } -} diff --git a/sdk/eventhub/Azure.Messaging.EventHubs.Shared/src/Core/ConnectionStringProperties.cs b/sdk/eventhub/Azure.Messaging.EventHubs.Shared/src/Core/ConnectionStringProperties.cs deleted file mode 100644 index f1e6401bfdfc..000000000000 --- a/sdk/eventhub/Azure.Messaging.EventHubs.Shared/src/Core/ConnectionStringProperties.cs +++ /dev/null @@ -1,119 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -using System; - -namespace Azure.Messaging.EventHubs.Core -{ - /// - /// The set of properties that comprise a connection string from the - /// Azure portal. - /// - /// - internal struct ConnectionStringProperties - { - /// - /// The endpoint to be used for connecting to the Event Hubs namespace. - /// - /// - /// The endpoint address, including protocol, from the connection string. - /// - public Uri Endpoint { get; } - - /// - /// The name of the specific Event Hub instance under the associated Event Hubs namespace. - /// - /// - public string EventHubName { get; } - - /// - /// The name of the shared access key, either for the Event Hubs namespace - /// or the Event Hub. - /// - /// - public string SharedAccessKeyName { get; } - - /// - /// The value of the shared access key, either for the Event Hubs namespace - /// or the Event Hub. - /// - /// - public string SharedAccessKey { get; } - - /// - /// The value of the fully-formed shared access signature, either for the Event Hubs - /// namespace or the Event Hub. - /// - /// - public string SharedAccessSignature { get; } - - /// - /// Initializes a new instance of the structure. - /// - /// - /// The endpoint of the Event Hubs namespace. - /// The name of the specific Event Hub under the namespace. - /// The name of the shared access key, to use authorization. - /// The shared access key to use for authorization. - /// The precomputed shared access signature to use for authorization. - /// - public ConnectionStringProperties(Uri endpoint, - string eventHubName, - string sharedAccessKeyName, - string sharedAccessKey, - string sharedAccessSignature) - { - Endpoint = endpoint; - EventHubName = eventHubName; - SharedAccessKeyName = sharedAccessKeyName; - SharedAccessKey = sharedAccessKey; - SharedAccessSignature = sharedAccessSignature; - } - - /// - /// Performs the actions needed to validate the set of connection string properties for connecting to the - /// Event Hubs service. - /// - /// - /// The name of the Event Hub that was explicitly passed independent of the connection string, allowing easier use of a namespace-level connection string. - /// The name of the argument associated with the connection string; to be used when raising variants. - /// - /// In the case that the properties violate an invariant or otherwise represent a combination that is not permissible, an appropriate exception will be thrown. - /// - public void Validate(string explicitEventHubName, - string connectionStringArgumentName) - { - // The Event Hub name may only be specified in one of the possible forms, either as part of the - // connection string or as a stand-alone parameter, but not both. If specified in both to the same - // value, then do not consider this a failure. - - if ((!string.IsNullOrEmpty(explicitEventHubName)) - && (!string.IsNullOrEmpty(EventHubName)) - && (!string.Equals(explicitEventHubName, EventHubName, StringComparison.InvariantCultureIgnoreCase))) - { - throw new ArgumentException(Resources.OnlyOneEventHubNameMayBeSpecified, connectionStringArgumentName); - } - - // The connection string may contain a precomputed shared access signature OR a shared key name and value, - // but not both. - - if ((!string.IsNullOrEmpty(SharedAccessSignature)) - && ((!string.IsNullOrEmpty(SharedAccessKeyName)) || (!string.IsNullOrEmpty(SharedAccessKey)))) - { - throw new ArgumentException(Resources.OnlyOneSharedAccessAuthorizationMayBeSpecified, connectionStringArgumentName); - } - - // Ensure that each of the needed components are present for connecting. - - var hasSharedKey = ((!string.IsNullOrEmpty(SharedAccessKeyName)) && (!string.IsNullOrEmpty(SharedAccessKey))); - var hasSharedSignature = (!string.IsNullOrEmpty(SharedAccessSignature)); - - if (string.IsNullOrEmpty(Endpoint?.Host) - || ((string.IsNullOrEmpty(explicitEventHubName)) && (string.IsNullOrEmpty(EventHubName))) - || ((!hasSharedKey) && (!hasSharedSignature))) - { - throw new ArgumentException(Resources.MissingConnectionInformation, connectionStringArgumentName); - } - } - } -} diff --git a/sdk/eventhub/Azure.Messaging.EventHubs.Shared/src/Resources.Designer.cs b/sdk/eventhub/Azure.Messaging.EventHubs.Shared/src/Resources.Designer.cs index 3777fb7e9710..d0e3034bac96 100644 --- a/sdk/eventhub/Azure.Messaging.EventHubs.Shared/src/Resources.Designer.cs +++ b/sdk/eventhub/Azure.Messaging.EventHubs.Shared/src/Resources.Designer.cs @@ -286,6 +286,17 @@ internal static string InvalidSharedAccessSignature } } + /// + /// Looks up a localized string similar to The endpoint address could not be parsed; it was either malformed or not using the `sb://` scheme.. + /// + internal static string InvalidEndpointAddress + { + get + { + return ResourceManager.GetString("InvalidEndpointAddress", resourceCulture); + } + } + /// /// Looks up a localized string similar to The time period may not be Zero or Infinite.. /// diff --git a/sdk/eventhub/Azure.Messaging.EventHubs.Shared/src/Resources.resx b/sdk/eventhub/Azure.Messaging.EventHubs.Shared/src/Resources.resx index bd1f476eb16c..5ba5bb875a57 100644 --- a/sdk/eventhub/Azure.Messaging.EventHubs.Shared/src/Resources.resx +++ b/sdk/eventhub/Azure.Messaging.EventHubs.Shared/src/Resources.resx @@ -159,6 +159,9 @@ The specified connection type, "{0}", is not recognized as valid in this context. + + The endpoint address could not be parsed; it was either malformed or not using the `sb://` scheme. + The shared access signature could not be parsed; it was either malformed or incorrectly encoded. diff --git a/sdk/eventhub/Azure.Messaging.EventHubs.Shared/src/Testing/EventHubsTestEnvironment.cs b/sdk/eventhub/Azure.Messaging.EventHubs.Shared/src/Testing/EventHubsTestEnvironment.cs index 959dc33f6ce3..e96155181574 100644 --- a/sdk/eventhub/Azure.Messaging.EventHubs.Shared/src/Testing/EventHubsTestEnvironment.cs +++ b/sdk/eventhub/Azure.Messaging.EventHubs.Shared/src/Testing/EventHubsTestEnvironment.cs @@ -43,7 +43,7 @@ public sealed class EventHubsTestEnvironment: TestEnvironment private readonly Lazy ActivePerTestExecutionLimit; /// The connection string for the active Event Hubs namespace for this test run, lazily created. - private readonly Lazy ParsedConnectionString; + private readonly Lazy ParsedConnectionString; /// /// The shared instance of the to be used during test runs. @@ -89,7 +89,7 @@ public sealed class EventHubsTestEnvironment: TestEnvironment /// /// The fully qualified namespace, as contained within the associated connection string. /// - public string FullyQualifiedNamespace => ParsedConnectionString.Value.Endpoint.Host; + public string FullyQualifiedNamespace => ParsedConnectionString.Value.FullyQualifiedNamespace; /// /// The name of the Event Hub to use during Live tests. @@ -139,7 +139,7 @@ public sealed class EventHubsTestEnvironment: TestEnvironment /// private EventHubsTestEnvironment() : base("eventhub") { - ParsedConnectionString = new Lazy(() => ConnectionStringParser.Parse(EventHubsConnectionString), LazyThreadSafetyMode.ExecutionAndPublication); + ParsedConnectionString = new Lazy(() => EventHubsConnectionStringProperties.Parse(EventHubsConnectionString), LazyThreadSafetyMode.ExecutionAndPublication); ActiveEventHubsNamespace = new Lazy(EnsureEventHubsNamespace, LazyThreadSafetyMode.ExecutionAndPublication); ActivePerTestExecutionLimit = new Lazy(() => @@ -183,7 +183,7 @@ public string BuildConnectionStringWithSharedAccessSignature(string eventHubName int validDurationMinutes = 30) { var signature = new SharedAccessSignature(signatureAudience, SharedAccessKeyName, SharedAccessKey, TimeSpan.FromMinutes(validDurationMinutes)); - return $"Endpoint={ ParsedConnectionString.Value.Endpoint };EntityPath={ eventHubName };SharedAccessSignature={ signature.Value }"; + return $"Endpoint=sb://{ ParsedConnectionString.Value.FullyQualifiedNamespace };EntityPath={ eventHubName };SharedAccessSignature={ signature.Value }"; } /// @@ -200,11 +200,11 @@ private NamespaceProperties EnsureEventHubsNamespace() if (!string.IsNullOrEmpty(environmentConnectionString)) { - var parsed = ConnectionStringParser.Parse(environmentConnectionString); + var parsed = EventHubsConnectionStringProperties.Parse(environmentConnectionString); return new NamespaceProperties ( - parsed.Endpoint.Host.Substring(0, parsed.Endpoint.Host.IndexOf('.')), + parsed.FullyQualifiedNamespace.Substring(0, parsed.FullyQualifiedNamespace.IndexOf('.')), environmentConnectionString.Replace($";EntityPath={ parsed.EventHubName }", string.Empty), shouldRemoveAtCompletion: false ); diff --git a/sdk/eventhub/Azure.Messaging.EventHubs.Shared/tests/Authorization/EventHubTokenCredentialTests.cs b/sdk/eventhub/Azure.Messaging.EventHubs.Shared/tests/Authorization/EventHubTokenCredentialTests.cs old mode 100755 new mode 100644 index 7de82da1f25c..52e76620f521 --- a/sdk/eventhub/Azure.Messaging.EventHubs.Shared/tests/Authorization/EventHubTokenCredentialTests.cs +++ b/sdk/eventhub/Azure.Messaging.EventHubs.Shared/tests/Authorization/EventHubTokenCredentialTests.cs @@ -32,8 +32,6 @@ public static IEnumerable SharedAccessSignatureCredentialTestCases() var signature = new SharedAccessSignature("hub", "keyName", "key", "TOkEn!", DateTimeOffset.UtcNow.AddHours(4)); yield return new object[] { new SharedAccessSignatureCredential(signature), true }; - yield return new object[] { new EventHubSharedKeyCredential("blah", "foo"), true }; - yield return new object[] { new EventHubTokenCredential(new EventHubSharedKeyCredential("blah", "foo"), "hub"), true }; yield return new object[] { new EventHubTokenCredential(credentialMock, "thing"), false }; yield return new object[] { credentialMock, false }; } diff --git a/sdk/eventhub/Azure.Messaging.EventHubs.Shared/tests/Authorization/SharedAccessSignatureCredentialTests.cs b/sdk/eventhub/Azure.Messaging.EventHubs.Shared/tests/Authorization/SharedAccessSignatureCredentialTests.cs index 2bcc8beb8406..7d774d720424 100644 --- a/sdk/eventhub/Azure.Messaging.EventHubs.Shared/tests/Authorization/SharedAccessSignatureCredentialTests.cs +++ b/sdk/eventhub/Azure.Messaging.EventHubs.Shared/tests/Authorization/SharedAccessSignatureCredentialTests.cs @@ -179,7 +179,7 @@ public void GetTokenDoesNotExtendATokenCloseToExpiringWhenCreatedWithoutTheKey() /// /// [Test] - public void ShouldUpdateSharedAccessKey() + public void SharedAccessKeyCanBeUpdated() { var value = "TOkEn!"; var tokenExpiration = DateTimeOffset.UtcNow.Add(TimeSpan.FromSeconds(GetSignatureRefreshBuffer().TotalSeconds / 2)); @@ -195,6 +195,28 @@ public void ShouldUpdateSharedAccessKey() Assert.That(newSignature.SignatureExpiration, Is.EqualTo(signature.SignatureExpiration)); } + /// + /// Verifies that a signature can be rotated without refreshing its validity. + /// + /// + [Test] + public void SharedAccesSignatureCanBeUpdated() + { + var tokenExpiration = TimeSpan.FromSeconds(GetSignatureRefreshBuffer().TotalSeconds / 2); + var signature = new SharedAccessSignature("hub-name", "keyName", "key", tokenExpiration); + var updatedSignature = new SharedAccessSignature("hub-name", "newKeyName", "newKey", tokenExpiration.Add(TimeSpan.FromMinutes(30))); + var credential = new SharedAccessSignatureCredential(signature); + + credential.UpdateSharedAccessSignature(updatedSignature.Value); + + var newSignature = GetSharedAccessSignature(credential); + + Assert.That(newSignature.Value, Is.EqualTo(updatedSignature.Value)); + Assert.That(newSignature.SharedAccessKeyName, Is.EqualTo(updatedSignature.SharedAccessKeyName)); + Assert.That(newSignature.SharedAccessKey, Is.Null); + Assert.That(newSignature.SignatureExpiration, Is.EqualTo(updatedSignature.SignatureExpiration).Within(TimeSpan.FromSeconds(5))); + } + /// /// Converts a value to the corresponding Unix-style time stamp. /// diff --git a/sdk/eventhub/Azure.Messaging.EventHubs.Shared/tests/Core/ConnectionStringPropertiesTests.cs b/sdk/eventhub/Azure.Messaging.EventHubs.Shared/tests/Core/ConnectionStringPropertiesTests.cs deleted file mode 100644 index 390d9269eab4..000000000000 --- a/sdk/eventhub/Azure.Messaging.EventHubs.Shared/tests/Core/ConnectionStringPropertiesTests.cs +++ /dev/null @@ -1,110 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -using Azure.Messaging.EventHubs.Core; -using NUnit.Framework; - -namespace Azure.Messaging.EventHubs.Tests -{ - /// - /// The suite of tests for the - /// class. - /// - /// - [TestFixture] - public class ConnectionStringPropertiesTests - { - /// - /// Verifies functionality of the - /// method. - /// - /// - [Test] - [TestCase("SharedAccessKeyName=[value];SharedAccessKey=[value];EntityPath=[value]")] - [TestCase("Endpoint=sb://value.com;SharedAccessKey=[value];EntityPath=[value]")] - [TestCase("Endpoint=sb://value.com;SharedAccessKeyName=[value];EntityPath=[value]")] - [TestCase("Endpoint=sb://value.com;SharedAccessKeyName=[value];SharedAccessKey=[value]")] - [TestCase("HostName=value.azure-devices.net;SharedAccessKeyName=[value];SharedAccessKey=[value]")] - [TestCase("HostName=value.azure-devices.net;SharedAccessKeyName=[value];SharedAccessKey=[value];EntityPath=[value]")] - public void ValidateDetectsMissingConnectionStringInformation(string connectionString) - { - var properties = ConnectionStringParser.Parse(connectionString); - Assert.That(() => properties.Validate(null, "Dummy"), Throws.ArgumentException.And.Message.StartsWith(Resources.MissingConnectionInformation)); - } - - /// - /// Verifies functionality of the - /// method. - /// - /// - [Test] - public void ValidateDetectsMultipleEventHubNames() - { - var eventHubName = "myHub"; - var fakeConnection = "Endpoint=sb://not-real.servicebus.windows.net/;SharedAccessKeyName=DummyKey;SharedAccessKey=[not_real];EntityPath=[unique_fake]"; - var properties = ConnectionStringParser.Parse(fakeConnection); - - Assert.That(() => properties.Validate(eventHubName, "Dummy"), Throws.ArgumentException.And.Message.StartsWith(Resources.OnlyOneEventHubNameMayBeSpecified)); - } - - /// - /// Verifies functionality of the - /// method. - /// - /// - [Test] - public void ValidateAllowsMultipleEventHubNamesIfEqual() - { - var eventHubName = "myHub"; - var fakeConnection = $"Endpoint=sb://not-real.servicebus.windows.net/;SharedAccessKeyName=DummyKey;SharedAccessKey=[not_real];EntityPath={ eventHubName }"; - var properties = ConnectionStringParser.Parse(fakeConnection); - - Assert.That(() => properties.Validate(eventHubName, "dummy"), Throws.Nothing, "Validation should accept the same Event Hub in multiple places."); - } - - /// - /// Verifies functionality of the - /// method. - /// - /// - [Test] - [TestCase("Endpoint=sb://not-real.servicebus.windows.net/;SharedAccessKeyName=DummyKey;SharedAccessKey=[not_real];EntityPath=[unique_fake];SharedAccessSignature=[not_real]")] - [TestCase("Endpoint=sb://not-real.servicebus.windows.net/;SharedAccessKeyName=DummyKey;EntityPath=[unique_fake];SharedAccessSignature=[not_real]")] - [TestCase("Endpoint=sb://not-real.servicebus.windows.net/;SharedAccessKey=[not_real];EntityPath=[unique_fake];SharedAccessSignature=[not_real]")] - public void ValidateDetectsMultipleAuthorizationCredentials(string connectionString) - { - var properties = ConnectionStringParser.Parse(connectionString); - Assert.That(() => properties.Validate(null, "Dummy"), Throws.ArgumentException.And.Message.StartsWith(Resources.OnlyOneSharedAccessAuthorizationMayBeSpecified)); - } - - /// - /// Verifies functionality of the - /// method. - /// - /// - [Test] - public void ValidateAllowsSharedAccessKeyAuthorization() - { - var eventHubName = "myHub"; - var fakeConnection = "Endpoint=sb://not-real.servicebus.windows.net/;SharedAccessKeyName=DummyKey;SharedAccessKey=[not_real]"; - var properties = ConnectionStringParser.Parse(fakeConnection); - - Assert.That(() => properties.Validate(eventHubName, "dummy"), Throws.Nothing, "Validation should accept the shared access key authorization."); - } - - /// - /// Verifies functionality of the - /// method. - /// - /// - [Test] - public void ValidateAllowsSharedAccessSignatureAuthorization() - { - var eventHubName = "myHub"; - var fakeConnection = "Endpoint=sb://not-real.servicebus.windows.net/;SharedAccessSignature=[not_real]"; - var properties = ConnectionStringParser.Parse(fakeConnection); - - Assert.That(() => properties.Validate(eventHubName, "dummy"), Throws.Nothing, "Validation should accept the shared access signature authorization."); - } - } -} diff --git a/sdk/eventhub/Azure.Messaging.EventHubs/api/Azure.Messaging.EventHubs.netstandard2.0.cs b/sdk/eventhub/Azure.Messaging.EventHubs/api/Azure.Messaging.EventHubs.netstandard2.0.cs index 8e469627fbfa..34074c03f942 100644 --- a/sdk/eventhub/Azure.Messaging.EventHubs/api/Azure.Messaging.EventHubs.netstandard2.0.cs +++ b/sdk/eventhub/Azure.Messaging.EventHubs/api/Azure.Messaging.EventHubs.netstandard2.0.cs @@ -34,6 +34,7 @@ public EventHubConnection(string connectionString, Azure.Messaging.EventHubs.Eve public EventHubConnection(string connectionString, string eventHubName) { } public EventHubConnection(string fullyQualifiedNamespace, string eventHubName, Azure.Core.TokenCredential credential, Azure.Messaging.EventHubs.EventHubConnectionOptions connectionOptions = null) { } public EventHubConnection(string connectionString, string eventHubName, Azure.Messaging.EventHubs.EventHubConnectionOptions connectionOptions) { } + public EventHubConnection(string fullyQualifiedNamespace, string eventHubName, Azure.Messaging.EventHubs.EventHubsSharedAccessKeyCredential credential, Azure.Messaging.EventHubs.EventHubConnectionOptions connectionOptions = null) { } public string EventHubName { get { throw null; } } public string FullyQualifiedNamespace { get { throw null; } } public bool IsClosed { get { throw null; } } @@ -64,6 +65,29 @@ protected internal EventHubProperties(string name, System.DateTimeOffset created public System.DateTimeOffset CreatedOn { get { throw null; } } public string Name { get { throw null; } } public string[] PartitionIds { get { throw null; } } + [System.ComponentModel.EditorBrowsableAttribute(System.ComponentModel.EditorBrowsableState.Never)] + public override bool Equals(object obj) { throw null; } + [System.ComponentModel.EditorBrowsableAttribute(System.ComponentModel.EditorBrowsableState.Never)] + public override int GetHashCode() { throw null; } + [System.ComponentModel.EditorBrowsableAttribute(System.ComponentModel.EditorBrowsableState.Never)] + public override string ToString() { throw null; } + } + public partial class EventHubsConnectionStringProperties + { + public EventHubsConnectionStringProperties() { } + public System.Uri Endpoint { get { throw null; } } + public string EventHubName { get { throw null; } } + public string FullyQualifiedNamespace { get { throw null; } } + public string SharedAccessKey { get { throw null; } } + public string SharedAccessKeyName { get { throw null; } } + public string SharedAccessSignature { get { throw null; } } + [System.ComponentModel.EditorBrowsableAttribute(System.ComponentModel.EditorBrowsableState.Never)] + public override bool Equals(object obj) { throw null; } + [System.ComponentModel.EditorBrowsableAttribute(System.ComponentModel.EditorBrowsableState.Never)] + public override int GetHashCode() { throw null; } + public static Azure.Messaging.EventHubs.EventHubsConnectionStringProperties Parse(string connectionString) { throw null; } + [System.ComponentModel.EditorBrowsableAttribute(System.ComponentModel.EditorBrowsableState.Never)] + public override string ToString() { throw null; } } public partial class EventHubsException : System.Exception { @@ -121,6 +145,22 @@ protected EventHubsRetryPolicy() { } [System.ComponentModel.EditorBrowsableAttribute(System.ComponentModel.EditorBrowsableState.Never)] public override string ToString() { throw null; } } + public sealed partial class EventHubsSharedAccessKeyCredential + { + public EventHubsSharedAccessKeyCredential(string sharedAccessSignature) { } + public EventHubsSharedAccessKeyCredential(string sharedAccessKeyName, string sharedAccessKey) { } + public string SharedAccessKey { get { throw null; } } + public string SharedAccessKeyName { get { throw null; } } + public string SharedAccessSignature { get { throw null; } } + [System.ComponentModel.EditorBrowsableAttribute(System.ComponentModel.EditorBrowsableState.Never)] + public override bool Equals(object obj) { throw null; } + [System.ComponentModel.EditorBrowsableAttribute(System.ComponentModel.EditorBrowsableState.Never)] + public override int GetHashCode() { throw null; } + [System.ComponentModel.EditorBrowsableAttribute(System.ComponentModel.EditorBrowsableState.Never)] + public override string ToString() { throw null; } + public void UpdateSharedAccessKey(string keyName, string keyValue) { } + public void UpdateSharedAccessSignature(string sharedAccessSignature) { } + } public enum EventHubsTransportType { AmqpTcp = 0, @@ -136,6 +176,12 @@ protected internal PartitionProperties(string eventHubName, string partitionId, public long LastEnqueuedOffset { get { throw null; } } public long LastEnqueuedSequenceNumber { get { throw null; } } public System.DateTimeOffset LastEnqueuedTime { get { throw null; } } + [System.ComponentModel.EditorBrowsableAttribute(System.ComponentModel.EditorBrowsableState.Never)] + public override bool Equals(object obj) { throw null; } + [System.ComponentModel.EditorBrowsableAttribute(System.ComponentModel.EditorBrowsableState.Never)] + public override int GetHashCode() { throw null; } + [System.ComponentModel.EditorBrowsableAttribute(System.ComponentModel.EditorBrowsableState.Never)] + public override string ToString() { throw null; } } } namespace Azure.Messaging.EventHubs.Consumer @@ -150,6 +196,7 @@ public EventHubConsumerClient(string consumerGroup, string connectionString, Azu public EventHubConsumerClient(string consumerGroup, string connectionString, string eventHubName) { } public EventHubConsumerClient(string consumerGroup, string fullyQualifiedNamespace, string eventHubName, Azure.Core.TokenCredential credential, Azure.Messaging.EventHubs.Consumer.EventHubConsumerClientOptions clientOptions = null) { } public EventHubConsumerClient(string consumerGroup, string connectionString, string eventHubName, Azure.Messaging.EventHubs.Consumer.EventHubConsumerClientOptions clientOptions) { } + public EventHubConsumerClient(string consumerGroup, string fullyQualifiedNamespace, string eventHubName, Azure.Messaging.EventHubs.EventHubsSharedAccessKeyCredential credential, Azure.Messaging.EventHubs.Consumer.EventHubConsumerClientOptions clientOptions = null) { } public string ConsumerGroup { get { throw null; } } public string EventHubName { get { throw null; } } public string FullyQualifiedNamespace { get { throw null; } } @@ -306,6 +353,7 @@ public EventProcessorPartitionOwnership() { } protected EventProcessor() { } protected EventProcessor(int eventBatchMaximumCount, string consumerGroup, string connectionString, Azure.Messaging.EventHubs.Primitives.EventProcessorOptions options = null) { } protected EventProcessor(int eventBatchMaximumCount, string consumerGroup, string fullyQualifiedNamespace, string eventHubName, Azure.Core.TokenCredential credential, Azure.Messaging.EventHubs.Primitives.EventProcessorOptions options = null) { } + protected EventProcessor(int eventBatchMaximumCount, string consumerGroup, string fullyQualifiedNamespace, string eventHubName, Azure.Messaging.EventHubs.EventHubsSharedAccessKeyCredential credential, Azure.Messaging.EventHubs.Primitives.EventProcessorOptions options = null) { } protected EventProcessor(int eventBatchMaximumCount, string consumerGroup, string connectionString, string eventHubName, Azure.Messaging.EventHubs.Primitives.EventProcessorOptions options = null) { } public string ConsumerGroup { get { throw null; } } public string EventHubName { get { throw null; } } @@ -339,6 +387,7 @@ protected PartitionReceiver() { } public PartitionReceiver(string consumerGroup, string partitionId, Azure.Messaging.EventHubs.Consumer.EventPosition eventPosition, Azure.Messaging.EventHubs.EventHubConnection connection, Azure.Messaging.EventHubs.Primitives.PartitionReceiverOptions options = null) { } public PartitionReceiver(string consumerGroup, string partitionId, Azure.Messaging.EventHubs.Consumer.EventPosition eventPosition, string connectionString, Azure.Messaging.EventHubs.Primitives.PartitionReceiverOptions options = null) { } public PartitionReceiver(string consumerGroup, string partitionId, Azure.Messaging.EventHubs.Consumer.EventPosition eventPosition, string fullyQualifiedNamespace, string eventHubName, Azure.Core.TokenCredential credential, Azure.Messaging.EventHubs.Primitives.PartitionReceiverOptions options = null) { } + public PartitionReceiver(string consumerGroup, string partitionId, Azure.Messaging.EventHubs.Consumer.EventPosition eventPosition, string fullyQualifiedNamespace, string eventHubName, Azure.Messaging.EventHubs.EventHubsSharedAccessKeyCredential credential, Azure.Messaging.EventHubs.Primitives.PartitionReceiverOptions options = null) { } public PartitionReceiver(string consumerGroup, string partitionId, Azure.Messaging.EventHubs.Consumer.EventPosition eventPosition, string connectionString, string eventHubName, Azure.Messaging.EventHubs.Primitives.PartitionReceiverOptions options = null) { } public string ConsumerGroup { get { throw null; } } public string EventHubName { get { throw null; } } @@ -458,6 +507,7 @@ public EventHubProducerClient(string connectionString) { } public EventHubProducerClient(string connectionString, Azure.Messaging.EventHubs.Producer.EventHubProducerClientOptions clientOptions) { } public EventHubProducerClient(string connectionString, string eventHubName) { } public EventHubProducerClient(string fullyQualifiedNamespace, string eventHubName, Azure.Core.TokenCredential credential, Azure.Messaging.EventHubs.Producer.EventHubProducerClientOptions clientOptions = null) { } + public EventHubProducerClient(string fullyQualifiedNamespace, string eventHubName, Azure.Messaging.EventHubs.EventHubsSharedAccessKeyCredential credential, Azure.Messaging.EventHubs.Producer.EventHubProducerClientOptions clientOptions = null) { } public EventHubProducerClient(string connectionString, string eventHubName, Azure.Messaging.EventHubs.Producer.EventHubProducerClientOptions clientOptions) { } public string EventHubName { get { throw null; } } public string FullyQualifiedNamespace { get { throw null; } } diff --git a/sdk/eventhub/Azure.Messaging.EventHubs/src/Authorization/EventHubsSharedAccessKeyCredential.cs b/sdk/eventhub/Azure.Messaging.EventHubs/src/Authorization/EventHubsSharedAccessKeyCredential.cs new file mode 100644 index 000000000000..3964ef651302 --- /dev/null +++ b/sdk/eventhub/Azure.Messaging.EventHubs/src/Authorization/EventHubsSharedAccessKeyCredential.cs @@ -0,0 +1,175 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using System.ComponentModel; +using Azure.Core; +using Azure.Messaging.EventHubs.Authorization; + +namespace Azure.Messaging.EventHubs +{ + /// + /// Provides a credential based on a shared access signature for a given + /// Event Hub instance. + /// + /// + public sealed class EventHubsSharedAccessKeyCredential + { + /// + /// The name of the shared access key to be used for authorization, as + /// reported by the Azure portal. + /// + /// + /// + /// This will only be populated when the credential is created using a shared key, not when created + /// using a precomputed shared access signature. + /// + /// + public string SharedAccessKeyName { get; private set; } + + /// + /// The value of the shared access key to be used for authorization, as + /// reported by the Azure portal. + /// + /// + /// + /// This will only be populated when the credential is created using a shared key, not when created + /// using a precomputed shared access signature. + /// + /// + public string SharedAccessKey { get; private set; } + + /// + /// The value of the precomputed shared access signature to be used for authorization. + /// + /// + /// + /// This will only be populated when the credential is created using a precomputed shared access signature, not when created + /// using a shared key. + /// + /// + public string SharedAccessSignature { get; private set; } + + /// + /// A reference to a corresponding SharedAccessSignatureCredential. + /// + /// + private SharedAccessSignatureCredential SharedAccessSignatureCredential { get; set; } + + /// + /// Initializes a new instance of the class. + /// + /// + /// The name of the shared access key to be used for authorization, as reported by the Azure portal. + /// The value of the shared access key to be used for authorization, as reported by the Azure portal. + /// + public EventHubsSharedAccessKeyCredential(string sharedAccessKeyName, + string sharedAccessKey) + { + Argument.AssertNotNullOrEmpty(sharedAccessKeyName, nameof(sharedAccessKeyName)); + Argument.AssertNotNullOrEmpty(sharedAccessKey, nameof(sharedAccessKey)); + + SharedAccessKeyName = sharedAccessKeyName; + SharedAccessKey = sharedAccessKey; + } + + /// + /// Initializes a new instance of the class. + /// + /// + /// The shared access signature that forms the basis of this security token. + /// + public EventHubsSharedAccessKeyCredential(string sharedAccessSignature) + { + Argument.AssertNotNullOrEmpty(sharedAccessSignature, nameof(sharedAccessSignature)); + SharedAccessSignature = sharedAccessSignature; + } + + /// + /// Allows the rotation of Shared Access Signatures. + /// + /// + /// The name of the shared access key that the signature should be based on. + /// The value of the shared access key for the signature. + /// + public void UpdateSharedAccessKey(string keyName, + string keyValue) + { + Argument.AssertNotNullOrEmpty(keyName, nameof(keyName)); + Argument.AssertNotNullOrEmpty(keyValue, nameof(keyValue)); + + SharedAccessKeyName = keyName; + SharedAccessKey = keyValue; + SharedAccessSignature = null; + + SharedAccessSignatureCredential?.UpdateSharedAccessKey(keyName, keyValue); + } + + /// + /// Allows the rotation of Shared Access Signatures. + /// + /// + /// The shared access signature that forms the basis of this security token. + /// + public void UpdateSharedAccessSignature(string sharedAccessSignature) + { + Argument.AssertNotNullOrEmpty(sharedAccessSignature, nameof(sharedAccessSignature)); + + SharedAccessSignature = sharedAccessSignature; + SharedAccessKeyName = null; + SharedAccessKey = null; + + SharedAccessSignatureCredential?.UpdateSharedAccessSignature(sharedAccessSignature); + } + + /// + /// Determines whether the specified is equal to this instance. + /// + /// + /// The to compare with this instance. + /// + /// true if the specified is equal to this instance; otherwise, false. + /// + [EditorBrowsable(EditorBrowsableState.Never)] + public override bool Equals(object obj) => base.Equals(obj); + + /// + /// Returns a hash code for this instance. + /// + /// + /// A hash code for this instance, suitable for use in hashing algorithms and data structures like a hash table. + /// + [EditorBrowsable(EditorBrowsableState.Never)] + public override int GetHashCode() => base.GetHashCode(); + + /// + /// Converts the instance to string representation. + /// + /// + /// A that represents this instance. + /// + [EditorBrowsable(EditorBrowsableState.Never)] + public override string ToString() => base.ToString(); + + /// + /// Coverts to shared access signature credential. + /// It retains a reference to the generated SharedAccessSignatureCredential. + /// + /// + /// The Event Hubs resource to which the token is intended to serve as authorization. + /// The duration that the signature should be considered valid; if not specified, a default will be assumed. + /// + /// A new based on the requested shared access key. + /// + internal SharedAccessSignatureCredential AsSharedAccessSignatureCredential(string eventHubResource, + TimeSpan? signatureValidityDuration = default) + { + + SharedAccessSignatureCredential = string.IsNullOrEmpty(SharedAccessSignature) + ? new SharedAccessSignatureCredential(new SharedAccessSignature(eventHubResource, SharedAccessKeyName, SharedAccessKey, signatureValidityDuration)) + : new SharedAccessSignatureCredential(new SharedAccessSignature(SharedAccessSignature)); + + return SharedAccessSignatureCredential; + } + } +} diff --git a/sdk/eventhub/Azure.Messaging.EventHubs/src/Consumer/EventHubConsumerClient.cs b/sdk/eventhub/Azure.Messaging.EventHubs/src/Consumer/EventHubConsumerClient.cs index fbf8e0808ea5..48a6f3e8fc5e 100644 --- a/sdk/eventhub/Azure.Messaging.EventHubs/src/Consumer/EventHubConsumerClient.cs +++ b/sdk/eventhub/Azure.Messaging.EventHubs/src/Consumer/EventHubConsumerClient.cs @@ -215,6 +215,35 @@ public EventHubConsumerClient(string consumerGroup, RetryPolicy = clientOptions.RetryOptions.ToRetryPolicy(); } + /// + /// Initializes a new instance of the class. + /// + /// + /// The name of the consumer group this consumer is associated with. Events are read in the context of this group. + /// The fully qualified Event Hubs namespace to connect to. This is likely to be similar to {yournamespace}.servicebus.windows.net. + /// The name of the specific Event Hub to associate the consumer with. + /// The Event Hubs shared access key credential to use for authorization. Access controls may be specified by the Event Hubs namespace or the requested Event Hub, depending on Azure configuration. + /// A set of options to apply when configuring the consumer. + /// + public EventHubConsumerClient(string consumerGroup, + string fullyQualifiedNamespace, + string eventHubName, + EventHubsSharedAccessKeyCredential credential, + EventHubConsumerClientOptions clientOptions = default) + { + Argument.AssertNotNullOrEmpty(consumerGroup, nameof(consumerGroup)); + Argument.AssertWellFormedEventHubsNamespace(fullyQualifiedNamespace, nameof(fullyQualifiedNamespace)); + Argument.AssertNotNullOrEmpty(eventHubName, nameof(eventHubName)); + Argument.AssertNotNull(credential, nameof(credential)); + + clientOptions = clientOptions?.Clone() ?? new EventHubConsumerClientOptions(); + + OwnsConnection = true; + Connection = new EventHubConnection(fullyQualifiedNamespace, eventHubName, credential, clientOptions.ConnectionOptions); + ConsumerGroup = consumerGroup; + RetryPolicy = clientOptions.RetryOptions.ToRetryPolicy(); + } + /// /// Initializes a new instance of the class. /// @@ -258,6 +287,7 @@ public EventHubConsumerClient(string consumerGroup, { Argument.AssertNotNullOrEmpty(consumerGroup, nameof(consumerGroup)); Argument.AssertNotNull(connection, nameof(connection)); + clientOptions = clientOptions?.Clone() ?? new EventHubConsumerClientOptions(); OwnsConnection = false; diff --git a/sdk/eventhub/Azure.Messaging.EventHubs/src/EventHubConnection.cs b/sdk/eventhub/Azure.Messaging.EventHubs/src/EventHubConnection.cs index 4c1ae8d7aded..32d3490e9184 100644 --- a/sdk/eventhub/Azure.Messaging.EventHubs/src/EventHubConnection.cs +++ b/sdk/eventhub/Azure.Messaging.EventHubs/src/EventHubConnection.cs @@ -27,6 +27,13 @@ namespace Azure.Messaging.EventHubs /// public class EventHubConnection : IAsyncDisposable { + /// + /// The default transport type to assume when forming credentials, when no + /// transport type was specified. + /// + /// + private static EventHubsTransportType DefaultCredentialTransportType { get; } = new EventHubConnectionOptions().TransportType; + /// /// The fully qualified Event Hubs namespace that the connection is associated with. This is likely /// to be similar to {yournamespace}.servicebus.windows.net. @@ -150,10 +157,10 @@ public EventHubConnection(string connectionString, connectionOptions = connectionOptions?.Clone() ?? new EventHubConnectionOptions(); ValidateConnectionOptions(connectionOptions); - var connectionStringProperties = ConnectionStringParser.Parse(connectionString); + var connectionStringProperties = EventHubsConnectionStringProperties.Parse(connectionString); connectionStringProperties.Validate(eventHubName, nameof(connectionString)); - var fullyQualifiedNamespace = connectionStringProperties.Endpoint.Host; + var fullyQualifiedNamespace = connectionStringProperties.FullyQualifiedNamespace; if (string.IsNullOrEmpty(eventHubName)) { @@ -186,6 +193,25 @@ public EventHubConnection(string connectionString, #pragma warning restore CA2214 // Do not call overridable methods in constructors. } + /// + /// Initializes a new instance of the class. + /// + /// + /// The fully qualified Event Hubs namespace to connect to. This is likely to be similar to {yournamespace}.servicebus.windows.net. + /// The name of the specific Event Hub to associate the connection with. + /// The to use for authorization. Access controls may be specified by the Event Hubs namespace or the requested Event Hub, depending on Azure configuration. + /// A set of options to apply when configuring the connection. + /// + public EventHubConnection(string fullyQualifiedNamespace, + string eventHubName, + EventHubsSharedAccessKeyCredential credential, + EventHubConnectionOptions connectionOptions = default) : this(fullyQualifiedNamespace, + eventHubName, + TranslateSharedKeyCredential(credential, fullyQualifiedNamespace, eventHubName, connectionOptions?.TransportType), + connectionOptions) + { + } + /// /// Initializes a new instance of the class. /// @@ -207,16 +233,6 @@ public EventHubConnection(string fullyQualifiedNamespace, connectionOptions = connectionOptions?.Clone() ?? new EventHubConnectionOptions(); ValidateConnectionOptions(connectionOptions); - switch (credential) - { - case SharedAccessSignatureCredential _: - break; - - case EventHubSharedKeyCredential sharedKeyCredential: - credential = sharedKeyCredential.AsSharedAccessSignatureCredential(BuildConnectionAudience(connectionOptions.TransportType, fullyQualifiedNamespace, eventHubName)); - break; - } - var tokenCredential = new EventHubTokenCredential(credential, BuildConnectionAudience(connectionOptions.TransportType, fullyQualifiedNamespace, eventHubName)); FullyQualifiedNamespace = fullyQualifiedNamespace; @@ -484,6 +500,30 @@ internal static string BuildConnectionAudience(EventHubsTransportType transportT return builder.Uri.AbsoluteUri.ToLowerInvariant(); } + /// + /// Translates an into the equivalent shared access signature credential. + /// + /// + /// The credential to translate. + /// The fully qualified Event Hubs namespace being connected to. + /// The name of the Event Hub being connected to. + /// The type of transport being used for the connection. + /// + /// The which the was translated into. + /// + internal static SharedAccessSignatureCredential TranslateSharedKeyCredential(EventHubsSharedAccessKeyCredential credential, + string fullyQualifiedNamespace, + string eventHubName, + EventHubsTransportType? transportType) + { + if ((credential == null) || (string.IsNullOrEmpty(fullyQualifiedNamespace)) || (string.IsNullOrEmpty(eventHubName))) + { + return null; + } + + return credential.AsSharedAccessSignatureCredential(BuildConnectionAudience(transportType ?? DefaultCredentialTransportType, fullyQualifiedNamespace, eventHubName)); + } + /// /// Performs the actions needed to validate the associated /// with this client. diff --git a/sdk/eventhub/Azure.Messaging.EventHubs/src/EventHubProperties.cs b/sdk/eventhub/Azure.Messaging.EventHubs/src/EventHubProperties.cs old mode 100755 new mode 100644 index edecca1d3525..14a132c29a27 --- a/sdk/eventhub/Azure.Messaging.EventHubs/src/EventHubProperties.cs +++ b/sdk/eventhub/Azure.Messaging.EventHubs/src/EventHubProperties.cs @@ -2,6 +2,7 @@ // Licensed under the MIT License. using System; +using System.ComponentModel; namespace Azure.Messaging.EventHubs { @@ -46,5 +47,34 @@ protected internal EventHubProperties(string name, CreatedOn = createdOn; PartitionIds = partitionIds; } + + /// + /// Determines whether the specified is equal to this instance. + /// + /// + /// The to compare with this instance. + /// + /// true if the specified is equal to this instance; otherwise, false. + /// + [EditorBrowsable(EditorBrowsableState.Never)] + public override bool Equals(object obj) => base.Equals(obj); + + /// + /// Returns a hash code for this instance. + /// + /// + /// A hash code for this instance, suitable for use in hashing algorithms and data structures like a hash table. + /// + [EditorBrowsable(EditorBrowsableState.Never)] + public override int GetHashCode() => base.GetHashCode(); + + /// + /// Converts the instance to string representation. + /// + /// + /// A that represents this instance. + /// + [EditorBrowsable(EditorBrowsableState.Never)] + public override string ToString() => base.ToString(); } } diff --git a/sdk/eventhub/Azure.Messaging.EventHubs/src/EventHubsConnectionStringProperties.cs b/sdk/eventhub/Azure.Messaging.EventHubs/src/EventHubsConnectionStringProperties.cs new file mode 100644 index 000000000000..1eff269ab057 --- /dev/null +++ b/sdk/eventhub/Azure.Messaging.EventHubs/src/EventHubsConnectionStringProperties.cs @@ -0,0 +1,347 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using System.ComponentModel; +using System.Text; +using Azure.Core; + +namespace Azure.Messaging.EventHubs +{ + /// + /// The set of properties that comprise an Event Hubs connection string. + /// + /// + public class EventHubsConnectionStringProperties + { + /// The token that identifies the endpoint address for the Event Hubs namespace. + private const string EndpointToken = "Endpoint"; + + /// The token that identifies the name of a specific Event Hub under the namespace. + private const string EventHubNameToken = "EntityPath"; + + /// The token that identifies the name of a shared access key. + private const string SharedAccessKeyNameToken = "SharedAccessKeyName"; + + /// The token that identifies the value of a shared access key. + private const string SharedAccessKeyValueToken = "SharedAccessKey"; + + /// The token that identifies the value of a shared access signature. + private const string SharedAccessSignatureToken = "SharedAccessSignature"; + + /// The character used to separate a token and its value in the connection string. + private const char TokenValueSeparator = '='; + + /// The character used to mark the beginning of a new token/value pair in the connection string. + private const char TokenValuePairDelimiter = ';'; + + /// The name of the protocol used by an Event Hubs endpoint. + private const string EventHubsEndpointSchemeName = "sb"; + + /// The formatted protocol used by an Event Hubs endpoint. + private static readonly string EventHubsEndpointScheme = $"{ EventHubsEndpointSchemeName }{ Uri.SchemeDelimiter }"; + + /// + /// The fully qualified Event Hubs namespace that the consumer is associated with. This is likely + /// to be similar to {yournamespace}.servicebus.windows.net. + /// + /// + /// The namespace of the Event Hub, as derived from the endpoint address of the connection string. + /// + public string FullyQualifiedNamespace => Endpoint?.Host; + + /// + /// The endpoint to be used for connecting to the Event Hubs namespace. + /// + /// + /// The endpoint address, including protocol, from the connection string. + /// + public Uri Endpoint { get; internal set; } + + /// + /// The name of the specific Event Hub instance under the associated Event Hubs namespace. + /// + /// + public string EventHubName { get; internal set; } + + /// + /// The name of the shared access key, either for the Event Hubs namespace + /// or the Event Hub. + /// + /// + public string SharedAccessKeyName { get; internal set; } + + /// + /// The value of the shared access key, either for the Event Hubs namespace + /// or the Event Hub. + /// + /// + public string SharedAccessKey { get; internal set; } + + /// + /// The value of the fully-formed shared access signature, either for the Event Hubs + /// namespace or the Event Hub. + /// + /// + public string SharedAccessSignature { get; internal set; } + + /// + /// Determines whether the specified is equal to this instance. + /// + /// + /// The to compare with this instance. + /// + /// true if the specified is equal to this instance; otherwise, false. + /// + [EditorBrowsable(EditorBrowsableState.Never)] + public override bool Equals(object obj) => base.Equals(obj); + + /// + /// Returns a hash code for this instance. + /// + /// + /// A hash code for this instance, suitable for use in hashing algorithms and data structures like a hash table. + /// + [EditorBrowsable(EditorBrowsableState.Never)] + public override int GetHashCode() => base.GetHashCode(); + + /// + /// Converts the instance to string representation. + /// + /// + /// A that represents this instance. + /// + [EditorBrowsable(EditorBrowsableState.Never)] + public override string ToString() => base.ToString(); + + /// + /// Creates an Event Hubs connection string based on this set of . + /// + /// + /// + /// A valid Event Hubs connection string; depending on the specified property information, this may + /// represent the namespace-level or Event Hub-level. + /// + /// + /// + internal string ToConnectionString() + { + Validate(null, null); + + var endpointBuilder = new UriBuilder(Endpoint) + { + Scheme = EventHubsEndpointScheme, + Port = -1 + }; + + if ((string.Compare(endpointBuilder.Scheme, EventHubsEndpointSchemeName, StringComparison.OrdinalIgnoreCase) != 0) + || (Uri.CheckHostName(endpointBuilder.Host) == UriHostNameType.Unknown)) + { + throw new ArgumentException(Resources.InvalidEndpointAddress); + } + + var builder = new StringBuilder() + .Append(EndpointToken) + .Append(TokenValueSeparator) + .Append(endpointBuilder.Uri.AbsoluteUri) + .Append(TokenValuePairDelimiter); + + if (!string.IsNullOrEmpty(EventHubName)) + { + builder + .Append(EventHubNameToken) + .Append(TokenValueSeparator) + .Append(EventHubName) + .Append(TokenValuePairDelimiter); + } + + if (!string.IsNullOrEmpty(SharedAccessSignature)) + { + builder + .Append(SharedAccessSignatureToken) + .Append(TokenValueSeparator) + .Append(SharedAccessSignature) + .Append(TokenValuePairDelimiter); + } + else + { + builder + .Append(SharedAccessKeyNameToken) + .Append(TokenValueSeparator) + .Append(SharedAccessKeyName) + .Append(TokenValuePairDelimiter) + .Append(SharedAccessKeyValueToken) + .Append(TokenValueSeparator) + .Append(SharedAccessKey) + .Append(TokenValuePairDelimiter); + } + + return builder.ToString(); + } + + /// + /// Performs the actions needed to validate the set of connection string properties for connecting to the + /// Event Hubs service. + /// + /// + /// The name of the Event Hub that was explicitly passed independent of the connection string, allowing easier use of a namespace-level connection string. + /// The name of the argument associated with the connection string; to be used when raising variants. + /// + /// In the case that the properties violate an invariant or otherwise represent a combination that is not permissible, an appropriate exception will be thrown. + /// + internal void Validate(string explicitEventHubName, + string connectionStringArgumentName) + { + // The Event Hub name may only be specified in one of the possible forms, either as part of the + // connection string or as a stand-alone parameter, but not both. If specified in both to the same + // value, then do not consider this a failure. + + if ((!string.IsNullOrEmpty(explicitEventHubName)) + && (!string.IsNullOrEmpty(EventHubName)) + && (!string.Equals(explicitEventHubName, EventHubName, StringComparison.InvariantCultureIgnoreCase))) + { + throw new ArgumentException(Resources.OnlyOneEventHubNameMayBeSpecified, connectionStringArgumentName); + } + + // The connection string may contain a precomputed shared access signature OR a shared key name and value, + // but not both. + + if ((!string.IsNullOrEmpty(SharedAccessSignature)) + && ((!string.IsNullOrEmpty(SharedAccessKeyName)) || (!string.IsNullOrEmpty(SharedAccessKey)))) + { + throw new ArgumentException(Resources.OnlyOneSharedAccessAuthorizationMayBeSpecified, connectionStringArgumentName); + } + + // Ensure that each of the needed components are present for connecting. + + var hasSharedKey = ((!string.IsNullOrEmpty(SharedAccessKeyName)) && (!string.IsNullOrEmpty(SharedAccessKey))); + var hasSharedSignature = (!string.IsNullOrEmpty(SharedAccessSignature)); + + if (string.IsNullOrEmpty(Endpoint?.Host) + || ((string.IsNullOrEmpty(explicitEventHubName)) && (string.IsNullOrEmpty(EventHubName))) + || ((!hasSharedKey) && (!hasSharedSignature))) + { + throw new ArgumentException(Resources.MissingConnectionInformation, connectionStringArgumentName); + } + } + + /// + /// Parses the specified Event Hubs connection string into its component properties. + /// + /// + /// The connection string to parse. + /// + /// The component properties parsed from the connection string. + /// + /// The specified connection string was malformed and could not be parsed. + /// + public static EventHubsConnectionStringProperties Parse(string connectionString) + { + Argument.AssertNotNullOrEmpty(connectionString, nameof(connectionString)); + + var parsedValues = new EventHubsConnectionStringProperties(); + var tokenPositionModifier = (connectionString[0] == TokenValuePairDelimiter) ? 0 : 1; + var lastPosition = 0; + var currentPosition = 0; + + int valueStart; + string slice; + string token; + string value; + + while (currentPosition != -1) + { + // Slice the string into the next token/value pair. + + currentPosition = connectionString.IndexOf(TokenValuePairDelimiter, lastPosition + 1); + + if (currentPosition >= 0) + { + slice = connectionString.Substring(lastPosition, (currentPosition - lastPosition)); + } + else + { + slice = connectionString.Substring(lastPosition); + } + + // Break the token and value apart, if this is a legal pair. + + valueStart = slice.IndexOf(TokenValueSeparator); + + if (valueStart >= 0) + { + token = slice.Substring((1 - tokenPositionModifier), (valueStart - 1 + tokenPositionModifier)); + value = slice.Substring(valueStart + 1); + + // Guard against leading and trailing spaces, only trimming if there is a need. + + if ((!string.IsNullOrEmpty(token)) && (char.IsWhiteSpace(token[0])) || char.IsWhiteSpace(token[token.Length - 1])) + { + token = token.Trim(); + } + + if ((!string.IsNullOrEmpty(value)) && (char.IsWhiteSpace(value[0]) || char.IsWhiteSpace(value[value.Length - 1]))) + { + value = value.Trim(); + } + + // If there was no value for a key, then consider the connection string to + // be malformed. + + if (string.IsNullOrEmpty(value)) + { + throw new FormatException(Resources.InvalidConnectionString); + } + + // Compare the token against the known connection string properties and capture the + // pair if they are a known attribute. + + if (string.Compare(EndpointToken, token, StringComparison.OrdinalIgnoreCase) == 0) + { + var endpointBuilder = new UriBuilder(value) + { + Scheme = EventHubsEndpointScheme, + Port = -1 + }; + + if ((string.Compare(endpointBuilder.Scheme, EventHubsEndpointSchemeName, StringComparison.OrdinalIgnoreCase) != 0) + || (Uri.CheckHostName(endpointBuilder.Host) == UriHostNameType.Unknown)) + { + throw new FormatException(Resources.InvalidConnectionString); + } + + parsedValues.Endpoint = endpointBuilder.Uri; + } + else if (string.Compare(EventHubNameToken, token, StringComparison.OrdinalIgnoreCase) == 0) + { + parsedValues.EventHubName = value; + } + else if (string.Compare(SharedAccessKeyNameToken, token, StringComparison.OrdinalIgnoreCase) == 0) + { + parsedValues.SharedAccessKeyName = value; + } + else if (string.Compare(SharedAccessKeyValueToken, token, StringComparison.OrdinalIgnoreCase) == 0) + { + parsedValues.SharedAccessKey = value; + } + else if (string.Compare(SharedAccessSignatureToken, token, StringComparison.OrdinalIgnoreCase) == 0) + { + parsedValues.SharedAccessSignature = value; + } + } + else if ((slice.Length != 1) || (slice[0] != TokenValuePairDelimiter)) + { + // This wasn't a legal pair and it is not simply a trailing delimiter; consider + // the connection string to be malformed. + + throw new FormatException(Resources.InvalidConnectionString); + } + + tokenPositionModifier = 0; + lastPosition = currentPosition; + } + + return parsedValues; + } + } +} diff --git a/sdk/eventhub/Azure.Messaging.EventHubs/src/PartitionProperties.cs b/sdk/eventhub/Azure.Messaging.EventHubs/src/PartitionProperties.cs old mode 100755 new mode 100644 index f0070e76140f..122cd4d023dd --- a/sdk/eventhub/Azure.Messaging.EventHubs/src/PartitionProperties.cs +++ b/sdk/eventhub/Azure.Messaging.EventHubs/src/PartitionProperties.cs @@ -2,6 +2,7 @@ // Licensed under the MIT License. using System; +using System.ComponentModel; namespace Azure.Messaging.EventHubs { @@ -84,5 +85,34 @@ protected internal PartitionProperties(string eventHubName, LastEnqueuedTime = lastEnqueuedTime; IsEmpty = isEmpty; } + + /// + /// Determines whether the specified is equal to this instance. + /// + /// + /// The to compare with this instance. + /// + /// true if the specified is equal to this instance; otherwise, false. + /// + [EditorBrowsable(EditorBrowsableState.Never)] + public override bool Equals(object obj) => base.Equals(obj); + + /// + /// Returns a hash code for this instance. + /// + /// + /// A hash code for this instance, suitable for use in hashing algorithms and data structures like a hash table. + /// + [EditorBrowsable(EditorBrowsableState.Never)] + public override int GetHashCode() => base.GetHashCode(); + + /// + /// Converts the instance to string representation. + /// + /// + /// A that represents this instance. + /// + [EditorBrowsable(EditorBrowsableState.Never)] + public override string ToString() => base.ToString(); } } diff --git a/sdk/eventhub/Azure.Messaging.EventHubs/src/Primitives/EventProcessor{TPartition}.cs b/sdk/eventhub/Azure.Messaging.EventHubs/src/Primitives/EventProcessor{TPartition}.cs index ed7e82b0456f..0512721dcfb9 100644 --- a/sdk/eventhub/Azure.Messaging.EventHubs/src/Primitives/EventProcessor{TPartition}.cs +++ b/sdk/eventhub/Azure.Messaging.EventHubs/src/Primitives/EventProcessor{TPartition}.cs @@ -243,10 +243,7 @@ internal EventProcessor(int eventBatchMaximumCount, RetryPolicy = options.RetryOptions.ToRetryPolicy(); Options = options; EventBatchMaximumCount = eventBatchMaximumCount; - -#pragma warning disable CA2214 // Do not call overridable methods in constructors. The virtual methods are internal and used for testing. - LoadBalancer = loadBalancer ?? CreatePartitionLoadBalancer(CreateStorageManager(this), Identifier, ConsumerGroup, FullyQualifiedNamespace, EventHubName, options.PartitionOwnershipExpirationInterval, options.LoadBalancingUpdateInterval); -#pragma warning restore CA2214 // Do not call overridable methods in constructors. + LoadBalancer = loadBalancer ?? new PartitionLoadBalancer(CreateStorageManager(this), Identifier, ConsumerGroup, FullyQualifiedNamespace, EventHubName, options.PartitionOwnershipExpirationInterval, options.LoadBalancingUpdateInterval); } /// @@ -306,7 +303,7 @@ protected EventProcessor(int eventBatchMaximumCount, options = options?.Clone() ?? new EventProcessorOptions(); - var connectionStringProperties = ConnectionStringParser.Parse(connectionString); + var connectionStringProperties = EventHubsConnectionStringProperties.Parse(connectionString); connectionStringProperties.Validate(eventHubName, nameof(connectionString)); ConnectionFactory = () => new EventHubConnection(connectionString, eventHubName, options.ConnectionOptions); @@ -317,10 +314,44 @@ protected EventProcessor(int eventBatchMaximumCount, RetryPolicy = options.RetryOptions.ToRetryPolicy(); Options = options; EventBatchMaximumCount = eventBatchMaximumCount; + LoadBalancer = new PartitionLoadBalancer(CreateStorageManager(this), Identifier, ConsumerGroup, FullyQualifiedNamespace, EventHubName, options.PartitionOwnershipExpirationInterval, options.LoadBalancingUpdateInterval); + } + + /// + /// Initializes a new instance of the class. + /// + /// + /// The desired number of events to include in a batch to be processed. This size is the maximum count in a batch; the actual count may be smaller, depending on whether events are available in the Event Hub. + /// The name of the consumer group the processor is associated with. Events are read in the context of this group. + /// The fully qualified Event Hubs namespace to connect to. This is likely to be similar to {yournamespace}.servicebus.windows.net. + /// The name of the specific Event Hub to associate the processor with. + /// The Event Hubs shared access key credential to use for authorization. Access controls may be specified by the Event Hubs namespace or the requested Event Hub, depending on Azure configuration. + /// The set of options to use for the processor. + /// + protected EventProcessor(int eventBatchMaximumCount, + string consumerGroup, + string fullyQualifiedNamespace, + string eventHubName, + EventHubsSharedAccessKeyCredential credential, + EventProcessorOptions options = default) + { + Argument.AssertInRange(eventBatchMaximumCount, 1, int.MaxValue, nameof(eventBatchMaximumCount)); + Argument.AssertNotNullOrEmpty(consumerGroup, nameof(consumerGroup)); + Argument.AssertWellFormedEventHubsNamespace(fullyQualifiedNamespace, nameof(fullyQualifiedNamespace)); + Argument.AssertNotNullOrEmpty(eventHubName, nameof(eventHubName)); + Argument.AssertNotNull(credential, nameof(credential)); -#pragma warning disable CA2214 // Do not call overridable methods in constructors. The virtual methods are internal and used for testing. - LoadBalancer = CreatePartitionLoadBalancer(CreateStorageManager(this), Identifier, ConsumerGroup, FullyQualifiedNamespace, EventHubName, options.PartitionOwnershipExpirationInterval, options.LoadBalancingUpdateInterval); -#pragma warning restore CA2214 // Do not call overridable methods in constructors + options = options?.Clone() ?? new EventProcessorOptions(); + + ConnectionFactory = () => new EventHubConnection(fullyQualifiedNamespace, eventHubName, credential, options.ConnectionOptions); + FullyQualifiedNamespace = fullyQualifiedNamespace; + EventHubName = eventHubName; + ConsumerGroup = consumerGroup; + Identifier = string.IsNullOrEmpty(options.Identifier) ? Guid.NewGuid().ToString() : options.Identifier; + RetryPolicy = options.RetryOptions.ToRetryPolicy(); + Options = options; + EventBatchMaximumCount = eventBatchMaximumCount; + LoadBalancer = new PartitionLoadBalancer(CreateStorageManager(this), Identifier, ConsumerGroup, FullyQualifiedNamespace, EventHubName, options.PartitionOwnershipExpirationInterval, options.LoadBalancingUpdateInterval); } /// @@ -455,37 +486,6 @@ internal virtual TransportConsumer CreateConsumer(string consumerGroup, EventProcessorOptions options) => connection.CreateTransportConsumer(consumerGroup, partitionId, eventPosition, options.RetryOptions.ToRetryPolicy(), options.TrackLastEnqueuedEventProperties, prefetchCount: (uint?)options.PrefetchCount, prefetchSizeInBytes: options.PrefetchSizeInBytes, ownerLevel: 0); - /// - /// Creates a to use for interacting with durable storage. - /// - /// - /// The instance to associate with the storage manager. - /// - /// A with the requested configuration. - /// - internal virtual StorageManager CreateStorageManager(EventProcessor instance) => new DelegatingStorageManager(instance); - - /// - /// Creates a for managing partition ownership for the event processor. - /// - /// - /// Responsible for managing persistence of the partition ownership data. - /// The identifier of the event processor associated with the load balancer. - /// The name of the consumer group this load balancer is associated with. - /// The fully qualified Event Hubs namespace that the processor is associated with. - /// The name of the Event Hub that the processor is associated with. - /// The minimum amount of time for an ownership to be considered expired without further updates. - /// The minimum amount of time to be elapsed between two load balancing verifications. - /// - internal virtual PartitionLoadBalancer CreatePartitionLoadBalancer(StorageManager storageManager, - string identifier, - string consumerGroup, - string fullyQualifiedNamespace, - string eventHubName, - TimeSpan ownershipExpiration, - TimeSpan loadBalancingInterval) => - new PartitionLoadBalancer(storageManager, identifier, consumerGroup, fullyQualifiedNamespace, eventHubName, ownershipExpiration, loadBalancingInterval); - /// /// Performs the tasks needed to process a batch of events. /// @@ -1517,6 +1517,16 @@ private Task InvokeOnProcessingErrorAsync(Exception exception, string operationDescription, CancellationToken cancellationToken) => Task.Run(() => OnProcessingErrorAsync(exception, partition, operationDescription, cancellationToken)); + /// + /// Creates a to use for interacting with durable storage. + /// + /// + /// The instance to associate with the storage manager. + /// + /// A with the requested configuration. + /// + internal static StorageManager CreateStorageManager(EventProcessor instance) => new DelegatingStorageManager(instance); + /// /// A virtual instance that delegates calls to the /// to which it is associated. diff --git a/sdk/eventhub/Azure.Messaging.EventHubs/src/Primitives/PartitionReceiver.cs b/sdk/eventhub/Azure.Messaging.EventHubs/src/Primitives/PartitionReceiver.cs index 2053ba9fb9ba..fef7fac9cb18 100644 --- a/sdk/eventhub/Azure.Messaging.EventHubs/src/Primitives/PartitionReceiver.cs +++ b/sdk/eventhub/Azure.Messaging.EventHubs/src/Primitives/PartitionReceiver.cs @@ -202,6 +202,46 @@ public PartitionReceiver(string consumerGroup, } + /// + /// Initializes a new instance of the class. + /// + /// + /// The name of the consumer group this client is associated with. Events are read in the context of this group. + /// The identifier of the Event Hub partition from which events will be received. + /// The position within the partition where the client should begin reading events. + /// The fully qualified Event Hubs namespace to connect to. This is likely to be similar to {yournamespace}.servicebus.windows.net. + /// The name of the specific Event Hub to associate the client with. + /// The Event Hubs shared key credential to use for authorization. Access controls may be specified by the Event Hubs namespace or the requested Event Hub, depending on Azure configuration. + /// A set of options to apply when configuring the client. + /// + public PartitionReceiver(string consumerGroup, + string partitionId, + EventPosition eventPosition, + string fullyQualifiedNamespace, + string eventHubName, + EventHubsSharedAccessKeyCredential credential, + PartitionReceiverOptions options = default) + { + Argument.AssertNotNullOrEmpty(consumerGroup, nameof(consumerGroup)); + Argument.AssertNotNullOrEmpty(partitionId, nameof(partitionId)); + Argument.AssertWellFormedEventHubsNamespace(fullyQualifiedNamespace, nameof(fullyQualifiedNamespace)); + Argument.AssertNotNullOrEmpty(eventHubName, nameof(eventHubName)); + Argument.AssertNotNull(credential, nameof(credential)); + + options = options?.Clone() ?? new PartitionReceiverOptions(); + + Connection = new EventHubConnection(fullyQualifiedNamespace, eventHubName, credential, options.ConnectionOptions); + ConsumerGroup = consumerGroup; + PartitionId = partitionId; + InitialPosition = eventPosition; + DefaultMaximumWaitTime = options.DefaultMaximumReceiveWaitTime; + RetryPolicy = options.RetryOptions.ToRetryPolicy(); + +#pragma warning disable CA2214 // Do not call overridable methods in constructors. This internal method is virtual for testing purposes. + InnerConsumer = CreateTransportConsumer(consumerGroup, partitionId, eventPosition, RetryPolicy, options); +#pragma warning restore CA2214 // Do not call overridable methods in constructors. + } + /// /// Initializes a new instance of the class. /// diff --git a/sdk/eventhub/Azure.Messaging.EventHubs/src/Producer/EventHubProducerClient.cs b/sdk/eventhub/Azure.Messaging.EventHubs/src/Producer/EventHubProducerClient.cs index 5a84a3301b06..d8e94e812f88 100644 --- a/sdk/eventhub/Azure.Messaging.EventHubs/src/Producer/EventHubProducerClient.cs +++ b/sdk/eventhub/Azure.Messaging.EventHubs/src/Producer/EventHubProducerClient.cs @@ -239,6 +239,44 @@ public EventHubProducerClient(string connectionString, } } + /// + /// Initializes a new instance of the class. + /// + /// + /// The fully qualified Event Hubs namespace to connect to. This is likely to be similar to {yournamespace}.servicebus.windows.net. + /// The name of the specific Event Hub to associate the producer with. + /// The Event Hubs shared access key credential to use for authorization. Access controls may be specified by the Event Hubs namespace or the requested Event Hub, depending on Azure configuration. + /// A set of options to apply when configuring the producer. + /// + public EventHubProducerClient(string fullyQualifiedNamespace, + string eventHubName, + EventHubsSharedAccessKeyCredential credential, + EventHubProducerClientOptions clientOptions = default) + { + Argument.AssertWellFormedEventHubsNamespace(fullyQualifiedNamespace, nameof(fullyQualifiedNamespace)); + Argument.AssertNotNullOrEmpty(eventHubName, nameof(eventHubName)); + Argument.AssertNotNull(credential, nameof(credential)); + + clientOptions = clientOptions?.Clone() ?? new EventHubProducerClientOptions(); + + OwnsConnection = true; + Connection = new EventHubConnection(fullyQualifiedNamespace, eventHubName, credential, clientOptions.ConnectionOptions); + Options = clientOptions; + RetryPolicy = clientOptions.RetryOptions.ToRetryPolicy(); + + PartitionProducerPool = new TransportProducerPool(partitionId => + Connection.CreateTransportProducer( + partitionId, + clientOptions.CreateFeatureFlags(), + Options.GetPublishingOptionsOrDefaultForPartition(partitionId), + RetryPolicy)); + + if (RequiresStatefulPartitions(clientOptions)) + { + PartitionState = new ConcurrentDictionary(); + } + } + /// /// Initializes a new instance of the class. /// diff --git a/sdk/eventhub/Azure.Messaging.EventHubs.Shared/tests/Authorization/EventHubSharedKeyCredentialTests.cs b/sdk/eventhub/Azure.Messaging.EventHubs/tests/Authorization/EventHubsSharedAccessKeyCredentialTests.cs old mode 100755 new mode 100644 similarity index 58% rename from sdk/eventhub/Azure.Messaging.EventHubs.Shared/tests/Authorization/EventHubSharedKeyCredentialTests.cs rename to sdk/eventhub/Azure.Messaging.EventHubs/tests/Authorization/EventHubsSharedAccessKeyCredentialTests.cs index 099cc4eddb84..5da17e8517e9 --- a/sdk/eventhub/Azure.Messaging.EventHubs.Shared/tests/Authorization/EventHubSharedKeyCredentialTests.cs +++ b/sdk/eventhub/Azure.Messaging.EventHubs/tests/Authorization/EventHubsSharedAccessKeyCredentialTests.cs @@ -10,12 +10,12 @@ namespace Azure.Messaging.EventHubs.Tests { /// - /// The suite of tests for the + /// The suite of tests for the /// class. /// /// [TestFixture] - public class EventHubSharedKeyCredentialTests + public class EventHubsSharedAccessKeyCredentialTests { /// /// Verifies functionality of the constructor. @@ -26,7 +26,7 @@ public class EventHubSharedKeyCredentialTests [TestCase("")] public void ConstructorValidatesTheKeyName(string keyName) { - Assert.That(() => new EventHubSharedKeyCredential(keyName, "someKey"), Throws.InstanceOf()); + Assert.That(() => new EventHubsSharedAccessKeyCredential(keyName, "someKey"), Throws.InstanceOf()); } /// @@ -38,7 +38,7 @@ public void ConstructorValidatesTheKeyName(string keyName) [TestCase("")] public void ConstructorValidatesTheKeyValue(string keyValue) { - Assert.That(() => new EventHubSharedKeyCredential("someName", keyValue), Throws.InstanceOf()); + Assert.That(() => new EventHubsSharedAccessKeyCredential("someName", keyValue), Throws.InstanceOf()); } /// @@ -46,14 +46,27 @@ public void ConstructorValidatesTheKeyValue(string keyValue) /// /// [Test] - public void ConstructorValidatesInitializesProperties() + [TestCase(null)] + [TestCase("")] + public void ConstructorValidatesInitializesSharedAccessSignatureProperties(string signature) + { + Assert.That(() => new EventHubsSharedAccessKeyCredential(signature), Throws.InstanceOf()); + } + + /// + /// Verifies functionality of the constructor. + /// + /// + [Test] + public void ConstructorInitializesSharedKeyProperties() { var name = "KeyName"; var value = "KeyValue"; - var credential = new EventHubSharedKeyCredential(name, value); - var initializedValue = GetSharedAccessKey(credential); + var credential = new EventHubsSharedAccessKeyCredential(name, value); - Assert.That(initializedValue, Is.EqualTo(value), "The shared key should have been set."); + Assert.That(credential.SharedAccessKeyName, Is.EqualTo(name), "The shared key name should have been set."); + Assert.That(credential.SharedAccessKey, Is.EqualTo(value), "The shared key should have been set."); + Assert.That(credential.SharedAccessSignature, Is.Null, "The shared access signature should not have been set."); } /// @@ -61,9 +74,14 @@ public void ConstructorValidatesInitializesProperties() /// /// [Test] - public void GetTokenIsNotPermitted() + public void ConstructorInitializesSharedSignatureProperties() { - Assert.That(() => new EventHubSharedKeyCredential("key", "value").GetToken(new TokenRequestContext(new[] { "test" }), default), Throws.InvalidOperationException); + var signature = new SharedAccessSignature("RESOURCE", "keyname", "keyvalue").Value; + var credential = new EventHubsSharedAccessKeyCredential(signature); + + Assert.That(credential.SharedAccessSignature, Is.EqualTo(signature), "The shared access signature name should have been set."); + Assert.That(credential.SharedAccessKeyName, Is.Null, "The shared key name should have been set."); + Assert.That(credential.SharedAccessKey, Is.Null, "The shared access key should not have been set."); } /// @@ -71,9 +89,52 @@ public void GetTokenIsNotPermitted() /// /// [Test] - public void GetTokenAsyncIsNotPermitted() + public void AsSharedAccessSignatureCredentialProducesTheExpectedCredentialForSharedKeys() { - Assert.That(async () => await (new EventHubSharedKeyCredential("key", "value").GetTokenAsync(new TokenRequestContext(new[] { "thing" }), default)), Throws.InvalidOperationException); + var resource = "amqps://some.hub.com/path"; + var keyName = "sharedKey"; + var keyValue = "keyValue"; + var validSpan = TimeSpan.FromHours(4); + var signature = new SharedAccessSignature(resource, keyName, keyValue, validSpan); + var keyCredential = new EventHubsSharedAccessKeyCredential(keyName, keyValue); + var sasCredential = keyCredential.AsSharedAccessSignatureCredential(resource, validSpan); + + Assert.That(sasCredential, Is.Not.Null, "A shared access signature credential should have been created."); + + var credentialSignature = GetSharedAccessSignature(sasCredential); + Assert.That(credentialSignature, Is.Not.Null, "The SAS credential should contain a shared access signature."); + Assert.That(credentialSignature.Resource, Is.EqualTo(signature.Resource), "The resource should match."); + Assert.That(credentialSignature.SharedAccessKeyName, Is.EqualTo(signature.SharedAccessKeyName), "The shared access key name should match."); + Assert.That(credentialSignature.SharedAccessKey, Is.EqualTo(signature.SharedAccessKey), "The shared access key should match."); + Assert.That(credentialSignature.SignatureExpiration, Is.EqualTo(signature.SignatureExpiration).Within(TimeSpan.FromSeconds(5)), "The expiration should match."); + } + + /// + /// The signature expiration will always be extended after calling AsSharedAccessSignatureCredential. + /// + /// + [Test] + public void AsSharedAccessSignatureCredentialShouldRefreshTokenValidityForSharedKeys() + { + var beforeResource = "amqps://before/path"; + var afterResource = "amqps://after/path"; + var beforeSpan = TimeSpan.FromHours(4); + var afterSpan = TimeSpan.FromHours(8); + var keyName = "keyName"; + var keyValue = "keyValue"; + var expectedSignature = new SharedAccessSignature(beforeResource, keyName, keyValue, beforeSpan); + var keyCredential = new EventHubsSharedAccessKeyCredential(keyName, keyValue); + + SharedAccessSignatureCredential sasCredential = keyCredential.AsSharedAccessSignatureCredential(beforeResource, beforeSpan); + SharedAccessSignature beforeSignature = GetSharedAccessSignature(sasCredential); + + Assert.That(beforeSignature.SignatureExpiration, Is.EqualTo(expectedSignature.SignatureExpiration).Within(TimeSpan.FromSeconds(5)), "The expiration should match."); + + expectedSignature = new SharedAccessSignature(afterResource, keyName, keyValue, afterSpan); + sasCredential = keyCredential.AsSharedAccessSignatureCredential(afterResource, afterSpan); + SharedAccessSignature afterSignature = GetSharedAccessSignature(sasCredential); + + Assert.That(afterSignature.SignatureExpiration, Is.EqualTo(expectedSignature.SignatureExpiration).Within(TimeSpan.FromSeconds(5)), "The expiration should match."); } /// @@ -81,14 +142,14 @@ public void GetTokenAsyncIsNotPermitted() /// /// [Test] - public void AsSharedAccessSignatureCredentialProducesTheExpectedCredential() + public void AsSharedAccessSignatureCredentialProducesTheExpectedCredentialForSharedAccessSignatures() { var resource = "amqps://some.hub.com/path"; var keyName = "sharedKey"; var keyValue = "keyValue"; var validSpan = TimeSpan.FromHours(4); var signature = new SharedAccessSignature(resource, keyName, keyValue, validSpan); - var keyCredential = new EventHubSharedKeyCredential(keyName, keyValue); + var keyCredential = new EventHubsSharedAccessKeyCredential(signature.Value); var sasCredential = keyCredential.AsSharedAccessSignatureCredential(resource, validSpan); Assert.That(sasCredential, Is.Not.Null, "A shared access signature credential should have been created."); @@ -97,7 +158,7 @@ public void AsSharedAccessSignatureCredentialProducesTheExpectedCredential() Assert.That(credentialSignature, Is.Not.Null, "The SAS credential should contain a shared access signature."); Assert.That(credentialSignature.Resource, Is.EqualTo(signature.Resource), "The resource should match."); Assert.That(credentialSignature.SharedAccessKeyName, Is.EqualTo(signature.SharedAccessKeyName), "The shared access key name should match."); - Assert.That(credentialSignature.SharedAccessKey, Is.EqualTo(signature.SharedAccessKey), "The shared access key should match."); + Assert.That(credentialSignature.SharedAccessKey, Is.Null, "The shared access key should not be populated."); Assert.That(credentialSignature.SignatureExpiration, Is.EqualTo(signature.SignatureExpiration).Within(TimeSpan.FromSeconds(5)), "The expiration should match."); } @@ -106,7 +167,7 @@ public void AsSharedAccessSignatureCredentialProducesTheExpectedCredential() /// /// [Test] - public void AsSharedAccessSignatureCredentialShouldRefreshTokenValidity() + public void AsSharedAccessSignatureCredentialShouldNotRefreshTokenValidityForSharedAccessSignatures() { var beforeResource = "amqps://before/path"; var afterResource = "amqps://after/path"; @@ -115,14 +176,13 @@ public void AsSharedAccessSignatureCredentialShouldRefreshTokenValidity() var keyName = "keyName"; var keyValue = "keyValue"; var expectedSignature = new SharedAccessSignature(beforeResource, keyName, keyValue, beforeSpan); - var keyCredential = new EventHubSharedKeyCredential(keyName, keyValue); + var keyCredential = new EventHubsSharedAccessKeyCredential(expectedSignature.Value); SharedAccessSignatureCredential sasCredential = keyCredential.AsSharedAccessSignatureCredential(beforeResource, beforeSpan); SharedAccessSignature beforeSignature = GetSharedAccessSignature(sasCredential); Assert.That(beforeSignature.SignatureExpiration, Is.EqualTo(expectedSignature.SignatureExpiration).Within(TimeSpan.FromSeconds(5)), "The expiration should match."); - expectedSignature = new SharedAccessSignature(afterResource, keyName, keyValue, afterSpan); sasCredential = keyCredential.AsSharedAccessSignatureCredential(afterResource, afterSpan); SharedAccessSignature afterSignature = GetSharedAccessSignature(sasCredential); @@ -141,7 +201,7 @@ public void EventHubSharedKeyCredentialShouldHoldAReferenceToASharedAccessKey() var span = TimeSpan.FromHours(4); var keyName = "keyName"; var keyValue = "keyValue"; - var keyCredential = new EventHubSharedKeyCredential(keyName, keyValue); + var keyCredential = new EventHubsSharedAccessKeyCredential(keyName, keyValue); SharedAccessSignatureCredential sasCredential = keyCredential.AsSharedAccessSignatureCredential(resource, span); SharedAccessSignatureCredential wrappedCredential = GetSharedAccessSignatureCredential(keyCredential); @@ -154,7 +214,7 @@ public void EventHubSharedKeyCredentialShouldHoldAReferenceToASharedAccessKey() /// /// [Test] - public void UpdateSharedAccessKeyShouldAllowToRefreshASharedAccessSignature() + public void UpdateSharedAccessKeyShouldAllowRefreshOfTheSharedAccessSignature() { var resource = "amqps://before/path"; var beforeKeyName = "beforeKeyName"; @@ -163,7 +223,7 @@ public void UpdateSharedAccessKeyShouldAllowToRefreshASharedAccessSignature() var afterKeyValue = "afterKeyValue"; var validSpan = TimeSpan.FromHours(4); var signature = new SharedAccessSignature(resource, beforeKeyName, beforeKeyValue, validSpan); - var keyCredential = new EventHubSharedKeyCredential(beforeKeyName, beforeKeyValue); + var keyCredential = new EventHubsSharedAccessKeyCredential(beforeKeyName, beforeKeyValue); // Needed to instantiate a SharedAccessSignatureCredential var sasCredential = keyCredential.AsSharedAccessSignatureCredential(resource, validSpan); @@ -181,13 +241,44 @@ public void UpdateSharedAccessKeyShouldAllowToRefreshASharedAccessSignature() Assert.That(credentialSignature.SignatureExpiration, Is.EqualTo(signature.SignatureExpiration).Within(TimeSpan.FromSeconds(5)), "The expiration should match."); } + /// + /// Verifies functionality of the constructor. + /// + /// + [Test] + public void UpdateSharedAccessSignatureShouldUpdateTheSharedAccessSignature() + { + var resource = "amqps://before/path"; + var beforeKeyName = "beforeKeyName"; + var afterKeyName = "afterKeyName"; + var beforeKeyValue = "beforeKeyValue"; + var afterKeyValue = "afterKeyValue"; + var validSpan = TimeSpan.FromHours(4); + var signature = new SharedAccessSignature(resource, beforeKeyName, beforeKeyValue, validSpan.Add(TimeSpan.FromHours(2))); + var keyCredential = new EventHubsSharedAccessKeyCredential(signature.Value); + var sasCredential = keyCredential.AsSharedAccessSignatureCredential(resource, validSpan); + + // Updates + var newSignature = new SharedAccessSignature(resource, afterKeyName, afterKeyValue, validSpan); + keyCredential.UpdateSharedAccessSignature(newSignature.Value); + + Assert.That(sasCredential, Is.Not.Null, "A shared access signature credential should have been created."); + + var credentialSignature = GetSharedAccessSignature(sasCredential); + Assert.That(credentialSignature, Is.Not.Null, "The SAS credential should contain a shared access signature."); + Assert.That(credentialSignature.Resource, Is.EqualTo(signature.Resource), "The resource should match."); + Assert.That(credentialSignature.SharedAccessKeyName, Is.EqualTo(afterKeyName), "The shared access key name should match."); + Assert.That(credentialSignature.SharedAccessKey, Is.Null, "The shared access key should not have been set."); + Assert.That(credentialSignature.SignatureExpiration, Is.EqualTo(newSignature.SignatureExpiration).Within(TimeSpan.FromSeconds(5)), "The expiration should match."); + } + /// /// A call to UpdateSharedAccessKey should change properties of the SharedAccessSignature /// that it wraps like the SharedAccessKeyName or the SharedAccessKey. /// /// [Test] - public void UpdateSharedAccessKeyShouldAlwaysRefreshEventHubSharedKeyCredentialNameAndKey() + public void UpdateSharedAccessKeyShoulRefreshEventHubSharedKeyCredentialNameAndKey() { var resource = "amqps://before/path"; var validSpan = TimeSpan.FromHours(4); @@ -195,14 +286,13 @@ public void UpdateSharedAccessKeyShouldAlwaysRefreshEventHubSharedKeyCredentialN var afterKeyName = "afterKeyName"; var beforeKeyValue = "beforeKeyValue"; var afterKeyValue = "afterKeyValue"; - var keyCredential = new EventHubSharedKeyCredential(beforeKeyName, beforeKeyValue); + var keyCredential = new EventHubsSharedAccessKeyCredential(beforeKeyName, beforeKeyValue); keyCredential.UpdateSharedAccessKey(afterKeyName, afterKeyValue); - string keyName = GetSharedAccessKeyName(keyCredential); - string key = GetSharedAccessKey(keyCredential); + var keyName = keyCredential.SharedAccessKeyName; + var key = keyCredential.SharedAccessKey; - // Needed to instantiate a SharedAccessSignatureCredential var sasCredential = keyCredential.AsSharedAccessSignatureCredential(resource, validSpan); var credentialSignature = GetSharedAccessSignature(sasCredential); @@ -218,7 +308,7 @@ public void UpdateSharedAccessKeyShouldAlwaysRefreshEventHubSharedKeyCredentialN /// /// [Test] - public void UpdateSharedAccessKeyShouldNotChangeOtherPropertiesOfASharedAccessSignature() + public void UpdateSharedAccessKeyShouldNotChangeOtherPropertiesForTheSharedAccessSignature() { var resource = "amqps://before/path"; var validSpan = TimeSpan.FromHours(4); @@ -226,7 +316,7 @@ public void UpdateSharedAccessKeyShouldNotChangeOtherPropertiesOfASharedAccessSi var afterKeyName = "afterKeyName"; var beforeKeyValue = "beforeKeyValue"; var afterKeyValue = "afterKeyValue"; - var keyCredential = new EventHubSharedKeyCredential(beforeKeyName, beforeKeyValue); + var keyCredential = new EventHubsSharedAccessKeyCredential(beforeKeyName, beforeKeyValue); // Needed to instantiate a SharedAccessTokenCredential var sasCredential = keyCredential.AsSharedAccessSignatureCredential(resource, validSpan); @@ -240,34 +330,6 @@ public void UpdateSharedAccessKeyShouldNotChangeOtherPropertiesOfASharedAccessSi Assert.That(credentialSignature.Resource, Is.EqualTo(resource), "The resource of a signature should not change when the credentials are rolled."); } - /// - /// Retrieves the shared access key from the credential using its private accessor. - /// - /// - /// The instance to retrieve the key from. - /// - /// The shared access key. - /// - private static string GetSharedAccessKey(EventHubSharedKeyCredential instance) => - (string) - typeof(EventHubSharedKeyCredential) - .GetProperty("SharedAccessKey", BindingFlags.Instance | BindingFlags.NonPublic) - .GetValue(instance, null); - - /// - /// Retrieves the shared access key from the credential using its private accessor. - /// - /// - /// The instance to retrieve the key from. - /// - /// The shared access key. - /// - private static string GetSharedAccessKeyName(EventHubSharedKeyCredential instance) => - (string) - typeof(EventHubSharedKeyCredential) - .GetProperty("SharedAccessKeyName", BindingFlags.Instance | BindingFlags.NonPublic) - .GetValue(instance, null); - /// /// Retrieves the shared access signature from the credential using its private accessor. /// @@ -290,9 +352,9 @@ private static SharedAccessSignature GetSharedAccessSignature(SharedAccessSignat /// /// The shared access key. /// - private static SharedAccessSignatureCredential GetSharedAccessSignatureCredential(EventHubSharedKeyCredential instance) => + private static SharedAccessSignatureCredential GetSharedAccessSignatureCredential(EventHubsSharedAccessKeyCredential instance) => (SharedAccessSignatureCredential) - typeof(EventHubSharedKeyCredential) + typeof(EventHubsSharedAccessKeyCredential) .GetProperty("SharedAccessSignatureCredential", BindingFlags.Instance | BindingFlags.NonPublic) .GetValue(instance, null); } diff --git a/sdk/eventhub/Azure.Messaging.EventHubs/tests/Connection/EventHubConnectionLiveTests.cs b/sdk/eventhub/Azure.Messaging.EventHubs/tests/Connection/EventHubConnectionLiveTests.cs old mode 100755 new mode 100644 index 43b41702177c..d36f56756b3e --- a/sdk/eventhub/Azure.Messaging.EventHubs/tests/Connection/EventHubConnectionLiveTests.cs +++ b/sdk/eventhub/Azure.Messaging.EventHubs/tests/Connection/EventHubConnectionLiveTests.cs @@ -100,7 +100,7 @@ public async Task ConnectionCanConnectToEventHubsUsingSharedKeyCredential() { await using (EventHubScope scope = await EventHubScope.CreateAsync(1)) { - var credential = new EventHubSharedKeyCredential(EventHubsTestEnvironment.Instance.SharedAccessKeyName, EventHubsTestEnvironment.Instance.SharedAccessKey); + var credential = new EventHubsSharedAccessKeyCredential(EventHubsTestEnvironment.Instance.SharedAccessKeyName, EventHubsTestEnvironment.Instance.SharedAccessKey); await using (var connection = new TestConnectionWithTransport(EventHubsTestEnvironment.Instance.FullyQualifiedNamespace, scope.EventHubName, credential)) { @@ -369,6 +369,13 @@ public TestConnectionWithTransport(string fullyQualifiedNamespace, { } + public TestConnectionWithTransport(string fullyQualifiedNamespace, + string eventHubName, + EventHubsSharedAccessKeyCredential credential, + EventHubConnectionOptions connectionOptions = default) : base(fullyQualifiedNamespace, eventHubName, credential, connectionOptions) + { + } + public Task GetPropertiesAsync(CancellationToken cancellationToken = default) => base.GetPropertiesAsync(RetryPolicy, cancellationToken); public Task GetPartitionIdsAsync(CancellationToken cancellationToken = default) => base.GetPartitionIdsAsync(RetryPolicy, cancellationToken); public Task GetPartitionPropertiesAsync(string partitionId, CancellationToken cancellationToken = default) => base.GetPartitionPropertiesAsync(partitionId, RetryPolicy, cancellationToken); diff --git a/sdk/eventhub/Azure.Messaging.EventHubs/tests/Connection/EventHubConnectionTests.cs b/sdk/eventhub/Azure.Messaging.EventHubs/tests/Connection/EventHubConnectionTests.cs index 9aeb7e103c16..a826e70e1da4 100644 --- a/sdk/eventhub/Azure.Messaging.EventHubs/tests/Connection/EventHubConnectionTests.cs +++ b/sdk/eventhub/Azure.Messaging.EventHubs/tests/Connection/EventHubConnectionTests.cs @@ -29,7 +29,7 @@ public class EventHubConnectionTests /// Provides the invalid test cases for the constructor tests. /// /// - public static IEnumerable ConstructorExpandedArgumentInvalidCases() + public static IEnumerable ConstructorTokenCredentialInvalidCases() { var credential = new Mock(Mock.Of(), "{namespace}.servicebus.windows.net"); @@ -41,6 +41,22 @@ public static IEnumerable ConstructorExpandedArgumentInvalidCases() yield return new object[] { "sb://fakenamspace.com", "FakePath", credential.Object }; } + /// + /// Provides the invalid test cases for the constructor tests. + /// + /// + public static IEnumerable ConstructorSharedKeyCredentialInvalidCases() + { + var credential = new EventHubsSharedAccessKeyCredential("keyName", "keyValue"); + + yield return new object[] { null, "fakePath", credential }; + yield return new object[] { "", "fakePath", credential }; + yield return new object[] { "FakeNamespace", null, credential }; + yield return new object[] { "FakNamespace", "", credential }; + yield return new object[] { "FakeNamespace", "FakePath", null }; + yield return new object[] { "sb://fakenamspace.com", "FakePath", credential }; + } + /// /// Provides test cases for the constructor tests. /// @@ -53,6 +69,7 @@ public static IEnumerable ConstructorCreatesDefaultOptionsCases() yield return new object[] { new ReadableOptionsMock(fakeConnection), "simple connection string" }; yield return new object[] { new ReadableOptionsMock(fakeConnection), "connection string with null options" }; yield return new object[] { new ReadableOptionsMock("fullyQualifiedNamespace", "path", credential.Object), "expanded argument" }; + yield return new object[] { new ReadableOptionsMock("fullyQualifiedNamespace", "path", new EventHubsSharedAccessKeyCredential("key", "value")), "expanded argument" }; } /// @@ -72,6 +89,7 @@ public static IEnumerable ConstructorClonesOptionsCases() yield return new object[] { new ReadableOptionsMock(fakeConnection, options), options, "connection string" }; yield return new object[] { new ReadableOptionsMock("fullyQualifiedNamespace", "path", credential.Object, options), options, "expanded argument" }; + yield return new object[] { new ReadableOptionsMock("fullyQualifiedNamespace", "path", new EventHubsSharedAccessKeyCredential("key", "value"), options), options, "expanded argument" }; } /// @@ -193,10 +211,24 @@ public void ConstructorAllowsTheEventHubToBePassedTwiceIfEqual() /// /// [Test] - [TestCaseSource(nameof(ConstructorExpandedArgumentInvalidCases))] - public void ConstructorValidatesExpandedArguments(string fullyQualifiedNamespace, - string eventHubName, - TokenCredential credential) + [TestCaseSource(nameof(ConstructorTokenCredentialInvalidCases))] + public void ConstructorValidatesExpandedArgumentsForTokenCredential(string fullyQualifiedNamespace, + string eventHubName, + TokenCredential credential) + { + Assert.That(() => new EventHubConnection(fullyQualifiedNamespace, eventHubName, credential), Throws.InstanceOf()); + } + + /// + /// Verifies functionality of the + /// constructor. + /// + /// + [Test] + [TestCaseSource(nameof(ConstructorSharedKeyCredentialInvalidCases))] + public void ConstructorValidatesExpandedArgumentsForSharedKeyCredential(string fullyQualifiedNamespace, + string eventHubName, + EventHubsSharedAccessKeyCredential credential) { Assert.That(() => new EventHubConnection(fullyQualifiedNamespace, eventHubName, credential), Throws.InstanceOf()); } @@ -208,11 +240,11 @@ public void ConstructorValidatesExpandedArguments(string fullyQualifiedNamespace /// [Test] [TestCaseSource(nameof(ConstructorCreatesDefaultOptionsCases))] - public void ConstructorCreatesDefaultOptions(ReadableOptionsMock client, + public void ConstructorCreatesDefaultOptions(ReadableOptionsMock connection, string constructorDescription) { var defaultOptions = new EventHubConnectionOptions(); - EventHubConnectionOptions options = client.Options; + EventHubConnectionOptions options = connection.Options; Assert.That(options, Is.Not.Null, $"The { constructorDescription } constructor should have set default options."); Assert.That(options, Is.Not.SameAs(defaultOptions), $"The { constructorDescription } constructor should not have the same options instance."); @@ -227,11 +259,11 @@ public void ConstructorCreatesDefaultOptions(ReadableOptionsMock client, /// [Test] [TestCaseSource(nameof(ConstructorClonesOptionsCases))] - public void ConstructorClonesOptions(ReadableOptionsMock client, + public void ConstructorClonesOptions(ReadableOptionsMock connection, EventHubConnectionOptions constructorOptions, string constructorDescription) { - EventHubConnectionOptions options = client.Options; + EventHubConnectionOptions options = connection.Options; Assert.That(options, Is.Not.Null, $"The { constructorDescription } constructor should have set the options."); Assert.That(options, Is.Not.SameAs(constructorOptions), $"The { constructorDescription } constructor should have cloned the options."); @@ -249,9 +281,9 @@ public void ConstructorWithFullConnectionStringInitializesProperties() { var entityPath = "somePath"; var fakeConnection = $"Endpoint=sb://not-real.servicebus.windows.net/;SharedAccessKeyName=DummyKey;SharedAccessKey=[not_real];EntityPath={ entityPath }"; - var client = new EventHubConnection(fakeConnection); + var connection = new EventHubConnection(fakeConnection); - Assert.That(client.EventHubName, Is.EqualTo(entityPath)); + Assert.That(connection.EventHubName, Is.EqualTo(entityPath)); } /// @@ -264,9 +296,9 @@ public void ConstructorWithConnectionStringAndEventHubInitializesProperties() { var entityPath = "somePath"; var fakeConnection = $"Endpoint=sb://not-real.servicebus.windows.net/;SharedAccessKeyName=DummyKey;SharedAccessKey=[not_real]"; - var client = new EventHubConnection(fakeConnection, entityPath); + var connection = new EventHubConnection(fakeConnection, entityPath); - Assert.That(client.EventHubName, Is.EqualTo(entityPath)); + Assert.That(connection.EventHubName, Is.EqualTo(entityPath)); } /// @@ -275,14 +307,30 @@ public void ConstructorWithConnectionStringAndEventHubInitializesProperties() /// /// [Test] - public void ConstructorWithExpandedArgumentsInitializesProperties() + public void ConstructorWithTokenCredentialInitializesProperties() { var fullyQualifiedNamespace = "host.windows.servicebus.net"; var entityPath = "somePath"; var credential = new Mock(Mock.Of(), "{namespace}.servicebus.windows.net"); - var client = new EventHubConnection(fullyQualifiedNamespace, entityPath, credential.Object); + var connection = new EventHubConnection(fullyQualifiedNamespace, entityPath, credential.Object); + + Assert.That(connection.EventHubName, Is.EqualTo(entityPath)); + } + + /// + /// Verifies functionality of the + /// constructor. + /// + /// + [Test] + public void ConstructorWithSharedKeyCredentialInitializesProperties() + { + var fullyQualifiedNamespace = "host.windows.servicebus.net"; + var entityPath = "somePath"; + var credential = new EventHubsSharedAccessKeyCredential("key", "value"); + var connection = new EventHubConnection(fullyQualifiedNamespace, entityPath, credential); - Assert.That(client.EventHubName, Is.EqualTo(entityPath)); + Assert.That(connection.EventHubName, Is.EqualTo(entityPath)); } /// @@ -320,8 +368,8 @@ public void ConstructorWithExpandedArgumentsValidatesOptions() [Test] public void ContructorWithConnectionStringCreatesTheTransportClient() { - var client = new EventHubConnection("Endpoint=sb://not-real.servicebus.windows.net/;SharedAccessKeyName=DummyKey;SharedAccessKey=[not_real]", "fake", new EventHubConnectionOptions()); - Assert.That(GetTransportClient(client), Is.Not.Null); + var connection = new EventHubConnection("Endpoint=sb://not-real.servicebus.windows.net/;SharedAccessKeyName=DummyKey;SharedAccessKey=[not_real]", "fake", new EventHubConnectionOptions()); + Assert.That(GetTransportClient(connection), Is.Not.Null); } /// @@ -333,10 +381,10 @@ public void ContructorWithConnectionStringCreatesTheTransportClient() public void ContructorWithConnectionStringUsingSharedAccessSignatureCreatesTheCorrectTransportCredential() { var sasToken = new SharedAccessSignature("hub", "root", "abc1234").Value; - var client = new InjectableTransportClientMock(Mock.Of(), $"Endpoint=sb://not-real.servicebus.windows.net/;EntityPath=fake;SharedAccessSignature={ sasToken }"); + var connection = new InjectableTransportClientMock(Mock.Of(), $"Endpoint=sb://not-real.servicebus.windows.net/;EntityPath=fake;SharedAccessSignature={ sasToken }"); - Assert.That(client.TransportClientCredential, Is.Not.Null, "The transport client should have been given a credential."); - Assert.That(client.TransportClientCredential.GetToken(default, default).Token, Is.EqualTo(sasToken), "The transport client credential should use the provided SAS token."); + Assert.That(connection.TransportClientCredential, Is.Not.Null, "The transport client should have been given a credential."); + Assert.That(connection.TransportClientCredential.GetToken(default, default).Token, Is.EqualTo(sasToken), "The transport client credential should use the provided SAS token."); } /// @@ -345,7 +393,7 @@ public void ContructorWithConnectionStringUsingSharedAccessSignatureCreatesTheCo /// /// [Test] - public void ContructorWithExpandedArgumentsCreatesTheTransportClient() + public void ContructorWithTokenCredentailCreatesTheTransportClient() { var fullyQualifiedNamespace = "my.eventhubs.com"; var path = "some-hub"; @@ -354,9 +402,28 @@ public void ContructorWithExpandedArgumentsCreatesTheTransportClient() var resource = $"amqps://{ fullyQualifiedNamespace }/{ path }"; var options = new EventHubConnectionOptions { TransportType = EventHubsTransportType.AmqpTcp }; var signature = new SharedAccessSignature(resource, keyName, key); - var client = new EventHubConnection(fullyQualifiedNamespace, path, new SharedAccessSignatureCredential(signature), options); + var connection = new EventHubConnection(fullyQualifiedNamespace, path, new SharedAccessSignatureCredential(signature), options); + + Assert.That(GetTransportClient(connection), Is.Not.Null); + } + + /// + /// Verifies functionality of the + /// constructor. + /// + /// + [Test] + public void ContructorWithSharedKeyCredentailCreatesTheTransportClient() + { + var fullyQualifiedNamespace = "my.eventhubs.com"; + var path = "some-hub"; + var keyName = "aWonderfulKey"; + var key = "ABC4223"; + var options = new EventHubConnectionOptions { TransportType = EventHubsTransportType.AmqpTcp }; + var credential = new EventHubsSharedAccessKeyCredential(keyName, key); + var connection = new EventHubConnection(fullyQualifiedNamespace, path, credential, options); - Assert.That(GetTransportClient(client), Is.Not.Null); + Assert.That(GetTransportClient(connection), Is.Not.Null); } /// @@ -366,11 +433,11 @@ public void ContructorWithExpandedArgumentsCreatesTheTransportClient() /// [Test] [TestCaseSource(nameof(ConstructorCreatesDefaultOptionsCases))] - public void TransportClientReceivesDefaultOptions(ReadableOptionsMock client, + public void TransportClientReceivesDefaultOptions(ReadableOptionsMock connection, string constructorDescription) { var defaultOptions = new EventHubConnectionOptions(); - EventHubConnectionOptions options = client.TransportClientOptions; + EventHubConnectionOptions options = connection.TransportClientOptions; Assert.That(options, Is.Not.Null, $"The { constructorDescription } constructor should have set default options."); Assert.That(options, Is.Not.SameAs(defaultOptions), $"The { constructorDescription } constructor should not have the same options instance."); @@ -385,11 +452,11 @@ public void TransportClientReceivesDefaultOptions(ReadableOptionsMock client, /// [Test] [TestCaseSource(nameof(ConstructorClonesOptionsCases))] - public void TransportClientReceivesClonedOptions(ReadableOptionsMock client, + public void TransportClientReceivesClonedOptions(ReadableOptionsMock connection, EventHubConnectionOptions constructorOptions, string constructorDescription) { - EventHubConnectionOptions options = client.TransportClientOptions; + EventHubConnectionOptions options = connection.TransportClientOptions; Assert.That(options, Is.Not.Null, $"The { constructorDescription } constructor should have set the options."); Assert.That(options, Is.Not.SameAs(constructorOptions), $"The { constructorDescription } constructor should have cloned the options."); @@ -415,9 +482,9 @@ public void BuildTransportClientAllowsLegalConnectionTypes(EventHubsTransportTyp var signature = new SharedAccessSignature(resource, keyName, key); var credential = new SharedAccessSignatureCredential(signature); var eventHubCredential = new EventHubTokenCredential(credential, resource); - var client = new EventHubConnection(fullyQualifiedNamespace, path, credential); + var connection = new EventHubConnection(fullyQualifiedNamespace, path, credential); - Assert.That(() => client.CreateTransportClient(fullyQualifiedNamespace, path, eventHubCredential, options), Throws.Nothing); + Assert.That(() => connection.CreateTransportClient(fullyQualifiedNamespace, path, eventHubCredential, options), Throws.Nothing); } /// @@ -438,9 +505,9 @@ public void BuildTransportClientRejectsInvalidConnectionTypes() var signature = new SharedAccessSignature(resource, keyName, key); var credential = new SharedAccessSignatureCredential(signature); var eventHubCredential = new EventHubTokenCredential(credential, resource); - var client = new EventHubConnection(fullyQualifiedNamespace, path, credential); + var connection = new EventHubConnection(fullyQualifiedNamespace, path, credential); - Assert.That(() => client.CreateTransportClient(fullyQualifiedNamespace, path, eventHubCredential, options), Throws.InstanceOf()); + Assert.That(() => connection.CreateTransportClient(fullyQualifiedNamespace, path, eventHubCredential, options), Throws.InstanceOf()); } /// @@ -453,8 +520,8 @@ public void BuildTransportClientRejectsInvalidConnectionTypes() [TestCase("")] public void CreateConsumerRequiresConsumerGroup(string consumerGroup) { - var client = new EventHubConnection("Endpoint=sb://not-real.servicebus.windows.net/;SharedAccessKeyName=DummyKey;SharedAccessKey=[not_real]", "fake", new EventHubConnectionOptions()); - Assert.That(() => client.CreateTransportConsumer(consumerGroup, "partition1", EventPosition.Earliest, Mock.Of()), Throws.InstanceOf()); + var connection = new EventHubConnection("Endpoint=sb://not-real.servicebus.windows.net/;SharedAccessKeyName=DummyKey;SharedAccessKey=[not_real]", "fake", new EventHubConnectionOptions()); + Assert.That(() => connection.CreateTransportConsumer(consumerGroup, "partition1", EventPosition.Earliest, Mock.Of()), Throws.InstanceOf()); } /// @@ -467,8 +534,8 @@ public void CreateConsumerRequiresConsumerGroup(string consumerGroup) [TestCase("")] public void CreateConsumerRequiresPartition(string partition) { - var client = new EventHubConnection("Endpoint=sb://not-real.servicebus.windows.net/;SharedAccessKeyName=DummyKey;SharedAccessKey=[not_real]", "fake", new EventHubConnectionOptions()); - Assert.That(() => client.CreateTransportConsumer("someGroup", partition, EventPosition.Earliest, Mock.Of()), Throws.InstanceOf()); + var connection = new EventHubConnection("Endpoint=sb://not-real.servicebus.windows.net/;SharedAccessKeyName=DummyKey;SharedAccessKey=[not_real]", "fake", new EventHubConnectionOptions()); + Assert.That(() => connection.CreateTransportConsumer("someGroup", partition, EventPosition.Earliest, Mock.Of()), Throws.InstanceOf()); } /// @@ -479,8 +546,8 @@ public void CreateConsumerRequiresPartition(string partition) [Test] public void CreateConsumerRequiresRetryPolicy() { - var client = new EventHubConnection("Endpoint=sb://not-real.servicebus.windows.net/;SharedAccessKeyName=DummyKey;SharedAccessKey=[not_real]", "fake", new EventHubConnectionOptions()); - Assert.That(() => client.CreateTransportConsumer("someGroup", "0", EventPosition.Earliest, null), Throws.InstanceOf()); + var connection = new EventHubConnection("Endpoint=sb://not-real.servicebus.windows.net/;SharedAccessKeyName=DummyKey;SharedAccessKey=[not_real]", "fake", new EventHubConnectionOptions()); + Assert.That(() => connection.CreateTransportConsumer("someGroup", "0", EventPosition.Earliest, null), Throws.InstanceOf()); } /// @@ -512,21 +579,21 @@ public async Task GetPartitionIdsAsyncDelegatesToGetProperties() var date = DateTimeOffset.Parse("2015-10-27T12:00:00Z"); var partitionIds = new[] { "first", "second", "third" }; var properties = new EventHubProperties("dummy", date, partitionIds); - var mockClient = new Mock { CallBase = true }; + var mockConnection = new Mock { CallBase = true }; - mockClient - .Setup(client => client.GetPropertiesAsync( + mockConnection + .Setup(connection => connection.GetPropertiesAsync( It.IsAny(), It.IsAny())) .Returns(Task.FromResult(properties)) .Verifiable("GetPropertiesAcync should have been delegated to."); - var actual = await mockClient.Object.GetPartitionIdsAsync(Mock.Of(), CancellationToken.None); + var actual = await mockConnection.Object.GetPartitionIdsAsync(Mock.Of(), CancellationToken.None); Assert.That(actual, Is.Not.Null); Assert.That(actual, Is.EqualTo(partitionIds)); - mockClient.VerifyAll(); + mockConnection.VerifyAll(); } /// @@ -538,9 +605,9 @@ public async Task GetPartitionIdsAsyncDelegatesToGetProperties() public async Task GetPropertiesAsyncInvokesTheTransportClient() { var transportClient = new ObservableTransportClientMock(); - var client = new InjectableTransportClientMock(transportClient, "Endpoint=sb://not-real.servicebus.windows.net/;SharedAccessKeyName=DummyKey;SharedAccessKey=[not_real];EntityPath=fake"); + var connection = new InjectableTransportClientMock(transportClient, "Endpoint=sb://not-real.servicebus.windows.net/;SharedAccessKeyName=DummyKey;SharedAccessKey=[not_real];EntityPath=fake"); - await client.GetPropertiesAsync(Mock.Of(), CancellationToken.None); + await connection.GetPropertiesAsync(Mock.Of(), CancellationToken.None); Assert.That(transportClient.WasGetPropertiesCalled, Is.True); } @@ -554,10 +621,10 @@ public async Task GetPropertiesAsyncInvokesTheTransportClient() public async Task GetPartitionPropertiesAsyncInvokesTheTransportClient() { var transportClient = new ObservableTransportClientMock(); - var client = new InjectableTransportClientMock(transportClient, "Endpoint=sb://not-real.servicebus.windows.net/;SharedAccessKeyName=DummyKey;SharedAccessKey=[not_real];EntityPath=fake"); + var connection = new InjectableTransportClientMock(transportClient, "Endpoint=sb://not-real.servicebus.windows.net/;SharedAccessKeyName=DummyKey;SharedAccessKey=[not_real];EntityPath=fake"); var expectedId = "BB33"; - await client.GetPartitionPropertiesAsync(expectedId, Mock.Of()); + await connection.GetPartitionPropertiesAsync(expectedId, Mock.Of()); Assert.That(transportClient.GetPartitionPropertiesCalledForId, Is.EqualTo(expectedId)); } @@ -571,13 +638,13 @@ public async Task GetPartitionPropertiesAsyncInvokesTheTransportClient() public void CreateProducerInvokesTheTransportClient() { var transportClient = new ObservableTransportClientMock(); - var client = new InjectableTransportClientMock(transportClient, "Endpoint=sb://not-real.servicebus.windows.net/;SharedAccessKeyName=DummyKey;SharedAccessKey=[not_real];EntityPath=fake"); + var connection = new InjectableTransportClientMock(transportClient, "Endpoint=sb://not-real.servicebus.windows.net/;SharedAccessKeyName=DummyKey;SharedAccessKey=[not_real];EntityPath=fake"); var options = new EventHubProducerClientOptions { EnableIdempotentPartitions = true, RetryOptions = new EventHubsRetryOptions { MaximumRetries = 6, TryTimeout = TimeSpan.FromMinutes(4) } }; var expectedFeatures = options.CreateFeatureFlags(); var expectedPartitionOptions = new PartitionPublishingOptions { ProducerGroupId = 123 }; var expectedRetry = options.RetryOptions.ToRetryPolicy(); - client.CreateTransportProducer(null, expectedFeatures, expectedPartitionOptions, expectedRetry); + connection.CreateTransportProducer(null, expectedFeatures, expectedPartitionOptions, expectedRetry); Assert.That(transportClient.CreateProducerCalledWith, Is.Not.Null, "The producer options should have been set."); Assert.That(transportClient.CreateProducerCalledWith.PartitionId, Is.Null, "There should have been no partition specified."); @@ -597,7 +664,7 @@ public void CreateProducerInvokesTheTransportClient() public void CreateConsumerInvokesTheTransportClient() { var transportClient = new ObservableTransportClientMock(); - var client = new InjectableTransportClientMock(transportClient, "Endpoint=sb://not-real.servicebus.windows.net/;SharedAccessKeyName=DummyKey;SharedAccessKey=[not_real];EntityPath=fake"); + var connection = new InjectableTransportClientMock(transportClient, "Endpoint=sb://not-real.servicebus.windows.net/;SharedAccessKeyName=DummyKey;SharedAccessKey=[not_real];EntityPath=fake"); var expectedPosition = EventPosition.FromOffset(65); var expectedPartition = "2123"; var expectedConsumerGroup = EventHubConsumerClient.DefaultConsumerGroupName; @@ -606,7 +673,7 @@ public void CreateConsumerInvokesTheTransportClient() var expectedPrefetch = 99U; var expectedOwnerLevel = 123L; - client.CreateTransportConsumer(expectedConsumerGroup, expectedPartition, expectedPosition, expectedRetryPolicy, expectedTrackLastEnqueued, expectedOwnerLevel, expectedPrefetch); + connection.CreateTransportConsumer(expectedConsumerGroup, expectedPartition, expectedPosition, expectedRetryPolicy, expectedTrackLastEnqueued, expectedOwnerLevel, expectedPrefetch); (var actualConsumerGroup, var actualPartition, EventPosition actualPosition, var actualRetry, var actualTrackLastEnqueued, var actualOwnerLevel, var actualPrefetch) = transportClient.CreateConsumerCalledWith; Assert.That(actualPartition, Is.EqualTo(expectedPartition), "The partition should have been passed."); @@ -627,9 +694,9 @@ public void CreateConsumerInvokesTheTransportClient() public async Task CloseAsyncClosesTheTransportClient() { var transportClient = new ObservableTransportClientMock(); - var client = new InjectableTransportClientMock(transportClient, "Endpoint=sb://not-real.servicebus.windows.net/;SharedAccessKeyName=DummyKey;SharedAccessKey=[not_real];EntityPath=fake"); + var connection = new InjectableTransportClientMock(transportClient, "Endpoint=sb://not-real.servicebus.windows.net/;SharedAccessKeyName=DummyKey;SharedAccessKey=[not_real];EntityPath=fake"); - await client.CloseAsync(); + await connection.CloseAsync(); Assert.That(transportClient.WasCloseCalled, Is.True); } @@ -687,7 +754,7 @@ public void BuildConnectionAudienceConstructsFromNamespaceAndPath() /// /// The client to retrieve the transport client of. /// - /// The transport client contained by the Event Hub client. + /// The transport client contained by the Event Hub connection. /// private TransportClient GetTransportClient(EventHubConnection client) => typeof(EventHubConnection) @@ -721,6 +788,13 @@ public ReadableOptionsMock(string fullyQualifiedNamespace, { } + public ReadableOptionsMock(string fullyQualifiedNamespace, + string eventHubName, + EventHubsSharedAccessKeyCredential credential, + EventHubConnectionOptions clientOptions = default) : base(fullyQualifiedNamespace, eventHubName, credential, clientOptions) + { + } + internal override TransportClient CreateTransportClient(string fullyQualifiedNamespace, string eventHubName, EventHubTokenCredential credential, EventHubConnectionOptions options) { TransportClientOptions = options; @@ -749,6 +823,13 @@ public ObservableOperationsMock(string fullyQualifiedNamespace, { } + public ObservableOperationsMock(string fullyQualifiedNamespace, + string eventHubName, + EventHubsSharedAccessKeyCredential credential, + EventHubConnectionOptions clientOptions = default) : base(fullyQualifiedNamespace, eventHubName, credential, clientOptions) + { + } + public override Task CloseAsync(CancellationToken cancellationToken = default) { WasCloseAsyncCalled = true; @@ -784,6 +865,16 @@ public InjectableTransportClientMock(TransportClient transportClient, SetTransportClient(transportClient); } + public InjectableTransportClientMock(TransportClient transportClient, + string fullyQualifiedNamespace, + string eventHubName, + EventHubsSharedAccessKeyCredential credential, + EventHubConnectionOptions clientOptions = default) : base(fullyQualifiedNamespace, eventHubName, credential, clientOptions) + { + TransportClient = transportClient; + SetTransportClient(transportClient); + } + internal override TransportClient CreateTransportClient(string fullyQualifiedNamespace, string eventHubName, EventHubTokenCredential credential, diff --git a/sdk/eventhub/Azure.Messaging.EventHubs/tests/Consumer/EventHubConsumerClientLiveTests.cs b/sdk/eventhub/Azure.Messaging.EventHubs/tests/Consumer/EventHubConsumerClientLiveTests.cs index c1e04f49a1eb..86a436452860 100644 --- a/sdk/eventhub/Azure.Messaging.EventHubs/tests/Consumer/EventHubConsumerClientLiveTests.cs +++ b/sdk/eventhub/Azure.Messaging.EventHubs/tests/Consumer/EventHubConsumerClientLiveTests.cs @@ -484,6 +484,46 @@ public async Task ConsumerCanReadEventsUsingAnIdentityCredential() } } + /// + /// Verifies that the is able to + /// connect to the Event Hubs service and perform operations. + /// + /// + [Test] + public async Task ConsumerCanReadEventsUsingTheSharedKeyCredential() + { + await using (EventHubScope scope = await EventHubScope.CreateAsync(1)) + { + using var cancellationSource = new CancellationTokenSource(); + cancellationSource.CancelAfter(EventHubsTestEnvironment.Instance.TestExecutionTimeLimit); + + var credential = new EventHubsSharedAccessKeyCredential(EventHubsTestEnvironment.Instance.SharedAccessKeyName, EventHubsTestEnvironment.Instance.SharedAccessKey); + var sourceEvents = EventGenerator.CreateEvents(50).ToList(); + + await using (var consumer = new EventHubConsumerClient(EventHubConsumerClient.DefaultConsumerGroupName, EventHubsTestEnvironment.Instance.FullyQualifiedNamespace, scope.EventHubName, credential)) + { + var partition = (await consumer.GetPartitionIdsAsync(cancellationSource.Token)).First(); + var connectionString = EventHubsTestEnvironment.Instance.BuildConnectionStringForEventHub(scope.EventHubName); + + await SendEventsAsync(connectionString, sourceEvents, new CreateBatchOptions { PartitionId = partition }, cancellationSource.Token); + + // Read the events and validate the resulting state. + + var readState = await ReadEventsFromPartitionAsync(consumer, partition, sourceEvents.Count, cancellationSource.Token); + Assert.That(cancellationSource.IsCancellationRequested, Is.False, "The cancellation token should not have been signaled."); + + foreach (var sourceEvent in sourceEvents) + { + var sourceId = sourceEvent.Properties[EventGenerator.IdPropertyName].ToString(); + Assert.That(readState.Events.TryGetValue(sourceId, out var readEvent), Is.True, $"The event with custom identifier [{ sourceId }] was not processed." ); + Assert.That(sourceEvent.IsEquivalentTo(readEvent.Data), $"The event with custom identifier [{ sourceId }] did not match the corresponding processed event."); + } + } + + cancellationSource.Cancel(); + } + } + /// /// Verifies that the is able to /// connect to the Event Hubs service and perform operations. @@ -1866,6 +1906,47 @@ public async Task ConsumerCanReadFromAllPartitionsUsingAnIdentityCredential() } } + /// + /// Verifies that the is able to + /// connect to the Event Hubs service and perform operations. + /// + /// + [Test] + public async Task ConsumerCanReadFromAllPartitionsUsingTheSharedKeyCredential() + { + await using (EventHubScope scope = await EventHubScope.CreateAsync(4)) + { + using var cancellationSource = new CancellationTokenSource(); + cancellationSource.CancelAfter(EventHubsTestEnvironment.Instance.TestExecutionTimeLimit); + + var credential = new EventHubsSharedAccessKeyCredential(EventHubsTestEnvironment.Instance.SharedAccessKeyName, EventHubsTestEnvironment.Instance.SharedAccessKey); + var sourceEvents = EventGenerator.CreateEvents(100).ToList(); + + await using (var consumer = new EventHubConsumerClient(EventHubConsumerClient.DefaultConsumerGroupName, EventHubsTestEnvironment.Instance.FullyQualifiedNamespace, scope.EventHubName, credential)) + { + var connectionString = EventHubsTestEnvironment.Instance.BuildConnectionStringForEventHub(scope.EventHubName); + var partitions = (await consumer.GetPartitionIdsAsync(cancellationSource.Token)).ToArray(); + + var sendCount = await SendEventsToAllPartitionsAsync(connectionString, sourceEvents, partitions, cancellationSource.Token); + Assert.That(sendCount, Is.EqualTo(sourceEvents.Count), "All of the events should have been sent."); + + // Read the events and validate the resulting state. + + var readState = await ReadEventsFromAllPartitionsAsync(consumer, sourceEvents.Count, cancellationSource.Token); + Assert.That(cancellationSource.IsCancellationRequested, Is.False, "The cancellation token should not have been signaled."); + + foreach (var sourceEvent in sourceEvents) + { + var sourceId = sourceEvent.Properties[EventGenerator.IdPropertyName].ToString(); + Assert.That(readState.Events.TryGetValue(sourceId, out var readEvent), Is.True, $"The event with custom identifier [{ sourceId }] was not processed." ); + Assert.That(sourceEvent.IsEquivalentTo(readEvent.Data), $"The event with custom identifier [{ sourceId }] did not match the corresponding processed event."); + } + } + + cancellationSource.Cancel(); + } + } + /// /// Verifies that the is able to /// connect to the Event Hubs service and perform operations. diff --git a/sdk/eventhub/Azure.Messaging.EventHubs/tests/Consumer/EventHubConsumerClientTests.cs b/sdk/eventhub/Azure.Messaging.EventHubs/tests/Consumer/EventHubConsumerClientTests.cs index b4c04ecc392a..8dbda2a9be8b 100644 --- a/sdk/eventhub/Azure.Messaging.EventHubs/tests/Consumer/EventHubConsumerClientTests.cs +++ b/sdk/eventhub/Azure.Messaging.EventHubs/tests/Consumer/EventHubConsumerClientTests.cs @@ -63,8 +63,9 @@ public static IEnumerable NonFatalRetriableExceptionTestCases() public void ConstructorValidatesTheConsumerGroup(string consumerGroup) { var credential = new Mock(Mock.Of(), "{namespace}.servicebus.windows.net"); + Assert.That(() => new EventHubConsumerClient(consumerGroup, "dummyNamespace", "dummyEventHub", credential.Object, new EventHubConsumerClientOptions()), Throws.InstanceOf(), "The token credential constructor should validate the consumer group."); + Assert.That(() => new EventHubConsumerClient(consumerGroup, "dummyNamespace", "dummyEventHub", new EventHubsSharedAccessKeyCredential("key", "value"), new EventHubConsumerClientOptions()), Throws.InstanceOf(), "The shared key credential constructor should validate the consumer group."); Assert.That(() => new EventHubConsumerClient(consumerGroup, "dummyConnection", new EventHubConsumerClientOptions()), Throws.InstanceOf(), "The connection string constructor should validate the consumer group."); - Assert.That(() => new EventHubConsumerClient(consumerGroup, "dummyNamespace", "dummyEventHub", credential.Object, new EventHubConsumerClientOptions()), Throws.InstanceOf(), "The namespace constructor should validate the consumer group."); } /// @@ -136,7 +137,8 @@ public void ConstructorAllowsMultipleEventHubNamesFromTheConnectionStringIfEqual public void ConstructorValidatesTheNamespace(string constructorArgument) { var credential = new Mock(Mock.Of(), "{namespace}.servicebus.windows.net"); - Assert.That(() => new EventHubConsumerClient(EventHubConsumerClient.DefaultConsumerGroupName, constructorArgument, "dummy", credential.Object), Throws.InstanceOf()); + Assert.That(() => new EventHubConsumerClient(EventHubConsumerClient.DefaultConsumerGroupName, constructorArgument, "dummy", credential.Object), Throws.InstanceOf(), "The token credential constructor should validate."); + Assert.That(() => new EventHubConsumerClient(EventHubConsumerClient.DefaultConsumerGroupName, constructorArgument, "dummy", new EventHubsSharedAccessKeyCredential("key", "value")), Throws.InstanceOf(), "The shared key credential constructor should validate."); } /// @@ -149,7 +151,8 @@ public void ConstructorValidatesTheNamespace(string constructorArgument) public void ConstructorValidatesTheEventHub(string constructorArgument) { var credential = new Mock(Mock.Of(), "{namespace}.servicebus.windows.net"); - Assert.That(() => new EventHubConsumerClient(EventHubConsumerClient.DefaultConsumerGroupName, "namespace", constructorArgument, credential.Object), Throws.InstanceOf()); + Assert.That(() => new EventHubConsumerClient(EventHubConsumerClient.DefaultConsumerGroupName, "namespace", constructorArgument, credential.Object), Throws.InstanceOf(), "The token credential constructor should validate."); + Assert.That(() => new EventHubConsumerClient(EventHubConsumerClient.DefaultConsumerGroupName, "namespace", constructorArgument, new EventHubsSharedAccessKeyCredential("key", "value")), Throws.InstanceOf(), "The shared key credential constructor should validate."); } /// @@ -159,7 +162,8 @@ public void ConstructorValidatesTheEventHub(string constructorArgument) [Test] public void ConstructorValidatesTheCredential() { - Assert.That(() => new EventHubConsumerClient(EventHubConsumerClient.DefaultConsumerGroupName, "namespace", "hubName", default(TokenCredential)), Throws.ArgumentNullException); + Assert.That(() => new EventHubConsumerClient(EventHubConsumerClient.DefaultConsumerGroupName, "namespace", "hubName", default(TokenCredential)), Throws.ArgumentNullException, "The token credential constructor should validate."); + Assert.That(() => new EventHubConsumerClient(EventHubConsumerClient.DefaultConsumerGroupName, "namespace", "hubName", default(EventHubsSharedAccessKeyCredential)), Throws.ArgumentNullException, "The shared key credential constructor should validate."); } /// @@ -192,7 +196,7 @@ public void ConnectionStringConstructorSetsTheRetryPolicy() /// /// [Test] - public void ExpandedConstructorSetsTheRetryPolicy() + public void TokenCredentialConstructorSetsTheRetryPolicy() { var expected = Mock.Of(); var credential = new Mock(Mock.Of(), "{namespace}.servicebus.windows.net"); @@ -202,6 +206,21 @@ public void ExpandedConstructorSetsTheRetryPolicy() Assert.That(GetRetryPolicy(consumer), Is.SameAs(expected)); } + /// + /// Verifies functionality of the constructor. + /// + /// + [Test] + public void SharedKeyCredentialConstructorSetsTheRetryPolicy() + { + var expected = Mock.Of(); + var credential = new EventHubsSharedAccessKeyCredential("key", "value"); + var options = new EventHubConsumerClientOptions { RetryOptions = new EventHubsRetryOptions { CustomRetryPolicy = expected } }; + var consumer = new EventHubConsumerClient(EventHubConsumerClient.DefaultConsumerGroupName, "namespace", "hub", credential, options); + + Assert.That(GetRetryPolicy(consumer), Is.SameAs(expected)); + } + /// /// Verifies functionality of the constructor. /// @@ -241,7 +260,7 @@ public void ConnectionStringConstructorCreatesDefaultOptions() /// /// [Test] - public void ExpandedConstructorCreatesDefaultOptions() + public void TokenCredentialConstructorCreatesDefaultOptions() { var credential = new Mock(Mock.Of(), "{namespace}.servicebus.windows.net"); var expected = new EventHubConsumerClientOptions().RetryOptions; @@ -255,6 +274,25 @@ public void ExpandedConstructorCreatesDefaultOptions() Assert.That(actual.IsEquivalentTo(expected), Is.True, "The default retry policy should be based on the default retry options."); } + /// + /// Verifies functionality of the constructor. + /// + /// + [Test] + public void SharedKeyCredentialConstructorCreatesDefaultOptions() + { + var credential = new EventHubsSharedAccessKeyCredential("key", "value"); + var expected = new EventHubConsumerClientOptions().RetryOptions; + var consumer = new EventHubConsumerClient(EventHubConsumerClient.DefaultConsumerGroupName, "some-namespace", "hubName", credential); + + var policy = GetRetryPolicy(consumer); + Assert.That(policy, Is.Not.Null, "There should have been a retry policy set."); + Assert.That(policy, Is.InstanceOf(), "The default retry policy should be a basic policy."); + + var actual = ((BasicRetryPolicy)policy).Options; + Assert.That(actual.IsEquivalentTo(expected), Is.True, "The default retry policy should be based on the default retry options."); + } + /// /// Verifies functionality of the constructor. /// @@ -293,7 +331,7 @@ public void ConnectionStringConstructorSetsTheConsumerGroup() /// /// [Test] - public void ExpandedConstructorSetsTheConsumerGroup() + public void TokenCredentialConstructorSetsTheConsumerGroup() { var consumerGroup = "SomeGroup"; var credential = new Mock(Mock.Of(), "{namespace}.servicebus.windows.net"); @@ -302,6 +340,20 @@ public void ExpandedConstructorSetsTheConsumerGroup() Assert.That(consumer.ConsumerGroup, Is.EqualTo(consumerGroup)); } + /// + /// Verifies functionality of the constructor. + /// + /// + [Test] + public void SharedKeyCredentialConstructorSetsTheConsumerGroup() + { + var consumerGroup = "SomeGroup"; + var credential = new EventHubsSharedAccessKeyCredential("key", "value"); + var consumer = new EventHubConsumerClient(consumerGroup, "namespace", "eventHub", credential); + + Assert.That(consumer.ConsumerGroup, Is.EqualTo(consumerGroup)); + } + /// /// Verifies functionality of the constructor. /// diff --git a/sdk/eventhub/Azure.Messaging.EventHubs.Shared/tests/Core/ConnectionStringParserTests.cs b/sdk/eventhub/Azure.Messaging.EventHubs/tests/Core/EventHubsConnectionStringPropertiesTests.cs similarity index 50% rename from sdk/eventhub/Azure.Messaging.EventHubs.Shared/tests/Core/ConnectionStringParserTests.cs rename to sdk/eventhub/Azure.Messaging.EventHubs/tests/Core/EventHubsConnectionStringPropertiesTests.cs index b9aab00eae68..abf2f81074b6 100644 --- a/sdk/eventhub/Azure.Messaging.EventHubs.Shared/tests/Core/ConnectionStringParserTests.cs +++ b/sdk/eventhub/Azure.Messaging.EventHubs/tests/Core/EventHubsConnectionStringPropertiesTests.cs @@ -4,22 +4,22 @@ using System; using System.Collections.Generic; using System.Reflection; -using Azure.Messaging.EventHubs.Core; +using Azure.Messaging.EventHubs; using NUnit.Framework; using NUnit.Framework.Constraints; namespace Azure.Messaging.EventHubs.Tests { /// - /// The suite of tests for the + /// The suite of tests for the /// class. /// /// [TestFixture] - public class ConnectionStringParserTests + public class EventHubsConnectionStringPropertiesTests { /// - /// Provides the reordered token test cases for the tests. + /// Provides the reordered token test cases for the tests. /// /// public static IEnumerable ParseDoesNotforceTokenOrderingCases() @@ -40,10 +40,10 @@ public static IEnumerable ParseDoesNotforceTokenOrderingCases() } /// - /// Provides the reordered token test cases for the tests. + /// Provides the reordered token test cases for the tests. /// /// - public static IEnumerable ParseCorrectlyParsesPartialConnectionStrings() + public static IEnumerable ParseCorrectlyParsesPartialConnectionStringCases() { var endpoint = "test.endpoint.com"; var eventHub = "some-path"; @@ -62,7 +62,71 @@ public static IEnumerable ParseCorrectlyParsesPartialConnectionStrings } /// - /// Verifies functionality of the + /// Provides the invalid properties argument cases for the tests. + /// + /// + public static IEnumerable ToConnectionStringValidatesArgumentCases() + { + yield return new object[] { new EventHubsConnectionStringProperties + { + Endpoint = null, + EventHubName = "fake", + SharedAccessSignature = "fake" + }, + "missing endpoint" }; + + yield return new object[] { new EventHubsConnectionStringProperties + { + Endpoint = new Uri(string.Concat(GetEventHubsEndpointScheme(), "someplace.hosname.ext")), + EventHubName = null, + SharedAccessSignature = "fake" + }, + "missing Event Hub name" }; + + yield return new object[] { new EventHubsConnectionStringProperties + { + Endpoint = new Uri(string.Concat(GetEventHubsEndpointScheme(), "someplace.hosname.ext")), + EventHubName = "fake" + }, + "missing authorization" }; + + yield return new object[] { new EventHubsConnectionStringProperties + { + Endpoint = new Uri(string.Concat(GetEventHubsEndpointScheme(), "someplace.hosname.ext")), + EventHubName = "fake", + SharedAccessSignature = "fake", + SharedAccessKey = "fake" + }, + "SAS and key specified" }; + + yield return new object[] { new EventHubsConnectionStringProperties + { + Endpoint = new Uri(string.Concat(GetEventHubsEndpointScheme(), "someplace.hosname.ext")), + EventHubName = "fake", + SharedAccessSignature = "fake", + SharedAccessKeyName = "fake" + }, + "SAS and shared key name specified" }; + + yield return new object[] { new EventHubsConnectionStringProperties + { + Endpoint = new Uri(string.Concat(GetEventHubsEndpointScheme(), "someplace.hosname.ext")), + EventHubName = "fake", + SharedAccessKeyName = "fake" + }, + "only shared key name specified" }; + + yield return new object[] { new EventHubsConnectionStringProperties + { + Endpoint = new Uri(string.Concat(GetEventHubsEndpointScheme(), "someplace.hosname.ext")), + EventHubName = "fake", + SharedAccessKey = "fake" + }, + "only shared key specified" }; + } + + /// + /// Verifies functionality of the /// method. /// /// @@ -73,11 +137,11 @@ public void ParseValidatesArguments(string connectionString) { ExactTypeConstraint typeConstraint = connectionString is null ? Throws.ArgumentNullException : Throws.ArgumentException; - Assert.That(() => ConnectionStringParser.Parse(connectionString), typeConstraint); + Assert.That(() => EventHubsConnectionStringProperties.Parse(connectionString), typeConstraint); } /// - /// Verifies functionality of the + /// Verifies functionality of the /// method. /// /// @@ -89,7 +153,7 @@ public void ParseCorrectlyParsesANamespaceConnectionString() var sasKeyName = "sasName"; var sharedAccessSignature = "fakeSAS"; var connectionString = $"Endpoint=sb://{ endpoint };SharedAccessKeyName={ sasKeyName };SharedAccessKey={ sasKey };SharedAccessSignature={ sharedAccessSignature }"; - ConnectionStringProperties parsed = ConnectionStringParser.Parse(connectionString); + var parsed = EventHubsConnectionStringProperties.Parse(connectionString); Assert.That(parsed.Endpoint?.Host, Is.EqualTo(endpoint).Using((IComparer)StringComparer.OrdinalIgnoreCase), "The endpoint host should match."); Assert.That(parsed.SharedAccessKeyName, Is.EqualTo(sasKeyName), "The SAS key name should match."); @@ -99,7 +163,7 @@ public void ParseCorrectlyParsesANamespaceConnectionString() } /// - /// Verifies functionality of the + /// Verifies functionality of the /// method. /// /// @@ -112,7 +176,7 @@ public void ParseCorrectlyParsesAnEventHubConnectionString() var sasKeyName = "sasName"; var sharedAccessSignature = "fakeSAS"; var connectionString = $"Endpoint=sb://{ endpoint };SharedAccessKeyName={ sasKeyName };SharedAccessKey={ sasKey };EntityPath={ eventHub };SharedAccessSignature={ sharedAccessSignature }"; - ConnectionStringProperties parsed = ConnectionStringParser.Parse(connectionString); + var parsed = EventHubsConnectionStringProperties.Parse(connectionString); Assert.That(parsed.Endpoint?.Host, Is.EqualTo(endpoint).Using((IComparer)StringComparer.OrdinalIgnoreCase), "The endpoint host should match."); Assert.That(parsed.SharedAccessKeyName, Is.EqualTo(sasKeyName), "The SAS key name should match."); @@ -122,7 +186,7 @@ public void ParseCorrectlyParsesAnEventHubConnectionString() } /// - /// Verifies functionality of the + /// Verifies functionality of the /// method. /// /// @@ -135,7 +199,7 @@ public void ParseCorrectlyParsesPartialConnectionStrings(string connectionString string sasKey, string sharedAccessSignature) { - ConnectionStringProperties parsed = ConnectionStringParser.Parse(connectionString); + var parsed = EventHubsConnectionStringProperties.Parse(connectionString); Assert.That(parsed.Endpoint?.Host, Is.EqualTo(endpoint).Using((IComparer)StringComparer.OrdinalIgnoreCase), "The endpoint host should match."); Assert.That(parsed.SharedAccessKeyName, Is.EqualTo(sasKeyName), "The SAS key name should match."); @@ -145,7 +209,7 @@ public void ParseCorrectlyParsesPartialConnectionStrings(string connectionString } /// - /// Verifies functionality of the + /// Verifies functionality of the /// method. /// /// @@ -157,7 +221,7 @@ public void ParseToleratesLeadingDelimiters() var sasKey = "sasKey"; var sasKeyName = "sasName"; var connectionString = $";Endpoint=sb://{ endpoint };SharedAccessKeyName={ sasKeyName };SharedAccessKey={ sasKey };EntityPath={ eventHub }"; - ConnectionStringProperties parsed = ConnectionStringParser.Parse(connectionString); + var parsed = EventHubsConnectionStringProperties.Parse(connectionString); Assert.That(parsed.Endpoint?.Host, Is.EqualTo(endpoint).Using((IComparer)StringComparer.OrdinalIgnoreCase), "The endpoint host should match."); Assert.That(parsed.SharedAccessKeyName, Is.EqualTo(sasKeyName), "The SAS key name should match."); @@ -166,7 +230,7 @@ public void ParseToleratesLeadingDelimiters() } /// - /// Verifies functionality of the + /// Verifies functionality of the /// method. /// /// @@ -178,7 +242,7 @@ public void ParseToleratesTrailingDelimiters() var sasKey = "sasKey"; var sasKeyName = "sasName"; var connectionString = $"Endpoint=sb://{ endpoint };SharedAccessKeyName={ sasKeyName };SharedAccessKey={ sasKey };EntityPath={ eventHub };"; - ConnectionStringProperties parsed = ConnectionStringParser.Parse(connectionString); + var parsed = EventHubsConnectionStringProperties.Parse(connectionString); Assert.That(parsed.Endpoint?.Host, Is.EqualTo(endpoint).Using((IComparer)StringComparer.OrdinalIgnoreCase), "The endpoint host should match."); Assert.That(parsed.SharedAccessKeyName, Is.EqualTo(sasKeyName), "The SAS key name should match."); @@ -187,7 +251,7 @@ public void ParseToleratesTrailingDelimiters() } /// - /// Verifies functionality of the + /// Verifies functionality of the /// method. /// /// @@ -199,7 +263,7 @@ public void ParseToleratesSpacesBetweenPairs() var sasKey = "sasKey"; var sasKeyName = "sasName"; var connectionString = $"Endpoint=sb://{ endpoint }; SharedAccessKeyName={ sasKeyName }; SharedAccessKey={ sasKey }; EntityPath={ eventHub }"; - ConnectionStringProperties parsed = ConnectionStringParser.Parse(connectionString); + var parsed = EventHubsConnectionStringProperties.Parse(connectionString); Assert.That(parsed.Endpoint?.Host, Is.EqualTo(endpoint).Using((IComparer)StringComparer.OrdinalIgnoreCase), "The endpoint host should match."); Assert.That(parsed.SharedAccessKeyName, Is.EqualTo(sasKeyName), "The SAS key name should match."); @@ -208,7 +272,7 @@ public void ParseToleratesSpacesBetweenPairs() } /// - /// Verifies functionality of the + /// Verifies functionality of the /// method. /// /// @@ -220,7 +284,7 @@ public void ParseToleratesSpacesBetweenValues() var sasKey = "sasKey"; var sasKeyName = "sasName"; var connectionString = $"Endpoint = sb://{ endpoint };SharedAccessKeyName ={ sasKeyName };SharedAccessKey= { sasKey }; EntityPath = { eventHub }"; - ConnectionStringProperties parsed = ConnectionStringParser.Parse(connectionString); + var parsed = EventHubsConnectionStringProperties.Parse(connectionString); Assert.That(parsed.Endpoint?.Host, Is.EqualTo(endpoint).Using((IComparer)StringComparer.OrdinalIgnoreCase), "The endpoint host should match."); Assert.That(parsed.SharedAccessKeyName, Is.EqualTo(sasKeyName), "The SAS key name should match."); @@ -229,7 +293,7 @@ public void ParseToleratesSpacesBetweenValues() } /// - /// Verifies functionality of the + /// Verifies functionality of the /// method. /// /// @@ -242,7 +306,7 @@ public void ParseDoesNotForceTokenOrdering(string connectionString, string sasKey, string shardAccessSignature) { - ConnectionStringProperties parsed = ConnectionStringParser.Parse(connectionString); + var parsed = EventHubsConnectionStringProperties.Parse(connectionString); Assert.That(parsed.Endpoint?.Host, Is.EqualTo(endpoint).Using((IComparer)StringComparer.OrdinalIgnoreCase), "The endpoint host should match."); Assert.That(parsed.SharedAccessKeyName, Is.EqualTo(sasKeyName), "The SAS key name should match."); @@ -252,7 +316,7 @@ public void ParseDoesNotForceTokenOrdering(string connectionString, } /// - /// Verifies functionality of the + /// Verifies functionality of the /// method. /// /// @@ -264,7 +328,7 @@ public void ParseIgnoresUnknownTokens() var sasKey = "sasKey"; var sasKeyName = "sasName"; var connectionString = $"Endpoint=sb://{ endpoint };SharedAccessKeyName={ sasKeyName };Unknown=INVALID;SharedAccessKey={ sasKey };EntityPath={ eventHub };Trailing=WHOAREYOU"; - ConnectionStringProperties parsed = ConnectionStringParser.Parse(connectionString); + var parsed = EventHubsConnectionStringProperties.Parse(connectionString); Assert.That(parsed.Endpoint?.Host, Is.EqualTo(endpoint).Using((IComparer)StringComparer.OrdinalIgnoreCase), "The endpoint host should match."); Assert.That(parsed.SharedAccessKeyName, Is.EqualTo(sasKeyName), "The SAS key name should match."); @@ -273,7 +337,7 @@ public void ParseIgnoresUnknownTokens() } /// - /// Verifies functionality of the + /// Verifies functionality of the /// method. /// /// @@ -287,7 +351,7 @@ public void ParseIgnoresUnknownTokens() public void ParseDoesAcceptsHostNamesAndUrisForTheEndpoint(string endpointValue) { var connectionString = $"Endpoint={ endpointValue };EntityPath=dummy"; - var parsed = ConnectionStringParser.Parse(connectionString); + var parsed = EventHubsConnectionStringProperties.Parse(connectionString); if (!Uri.TryCreate(endpointValue, UriKind.Absolute, out var valueUri)) { @@ -303,7 +367,7 @@ public void ParseDoesAcceptsHostNamesAndUrisForTheEndpoint(string endpointValue) } /// - /// Verifies functionality of the + /// Verifies functionality of the /// method. /// /// @@ -313,11 +377,11 @@ public void ParseDoesAcceptsHostNamesAndUrisForTheEndpoint(string endpointValue) public void ParseDoesNotAllowAnInvalidEndpointFormat(string endpointValue) { var connectionString = $"Endpoint={endpointValue }"; - Assert.That(() => ConnectionStringParser.Parse(connectionString), Throws.InstanceOf()); + Assert.That(() => EventHubsConnectionStringProperties.Parse(connectionString), Throws.InstanceOf()); } /// - /// Verifies functionality of the + /// Verifies functionality of the /// method. /// /// @@ -330,11 +394,230 @@ public void ParseDoesNotAllowAnInvalidEndpointFormat(string endpointValue) [TestCase("Endpoint=;SharedAccessKeyName;SharedAccessKey;EntityPath=")] public void ParseConsidersMissingValuesAsMalformed(string connectionString) { - Assert.That(() => ConnectionStringParser.Parse(connectionString), Throws.InstanceOf()); + Assert.That(() => EventHubsConnectionStringProperties.Parse(connectionString), Throws.InstanceOf()); + } + + /// + /// Verifies functionality of the + /// method. + /// + /// + [Test] + [TestCaseSource(nameof(ToConnectionStringValidatesArgumentCases))] + public void ToConnectionStringValidatesProperties(EventHubsConnectionStringProperties properties, + string testDescription) + { + Assert.That(() => properties.ToConnectionString(), Throws.InstanceOf(), $"The case for `{ testDescription }` failed."); + } + + /// + /// Verifies functionality of the + /// method. + /// + /// + [Test] + public void ToConnectionStringProducesTheConnectionStringForSharedAccessSignatures() + { + var properties = new EventHubsConnectionStringProperties + { + Endpoint = new Uri("sb://place.endpoint.ext"), + EventHubName = "HubName", + SharedAccessSignature = "FaKe#$1324@@" + }; + + 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 = EventHubsConnectionStringProperties.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. + /// + /// + [Test] + public void ToConnectionStringProducesTheConnectionStringForSharedKeys() + { + var properties = new EventHubsConnectionStringProperties + { + Endpoint = new Uri("sb://place.endpoint.ext"), + EventHubName = "HubName", + SharedAccessKey = "FaKe#$1324@@", + SharedAccessKeyName = "RootSharedAccessManagementKey" + }; + + 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 = EventHubsConnectionStringProperties.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. + /// + /// + [Test] + [TestCase("sb://")] + [TestCase("amqps://")] + [TestCase("amqp://")] + [TestCase("http://")] + [TestCase("https://")] + [TestCase("fake://")] + public void ToConnectionStringNormalizesTheEndpointScheme(string scheme) + { + var properties = new EventHubsConnectionStringProperties + { + Endpoint = new Uri(string.Concat(scheme, "myhub.servicebus.windows.net")), + EventHubName = "HubName", + SharedAccessKey = "FaKe#$1324@@", + SharedAccessKeyName = "RootSharedAccessManagementKey" + }; + + 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 = EventHubsConnectionStringProperties.Parse(connectionString); + Assert.That(parsed, Is.Not.Null, "The connection string should be parsable."); + Assert.That(parsed.Endpoint.Host, Is.EqualTo(properties.Endpoint.Host), "The host name of the endpoints should match."); + + var expectedScheme = new Uri(string.Concat(GetEventHubsEndpointScheme(), "fake.fake.com")).Scheme; + Assert.That(parsed.Endpoint.Scheme, Is.EqualTo(expectedScheme), "The endpoint scheme should have been overridden."); + } + + /// + /// Verifies functionality of the + /// method. + /// + /// + [Test] + [TestCase("SharedAccessKeyName=[value];SharedAccessKey=[value];EntityPath=[value]")] + [TestCase("Endpoint=sb://value.com;SharedAccessKey=[value];EntityPath=[value]")] + [TestCase("Endpoint=sb://value.com;SharedAccessKeyName=[value];EntityPath=[value]")] + [TestCase("Endpoint=sb://value.com;SharedAccessKeyName=[value];SharedAccessKey=[value]")] + [TestCase("HostName=value.azure-devices.net;SharedAccessKeyName=[value];SharedAccessKey=[value]")] + [TestCase("HostName=value.azure-devices.net;SharedAccessKeyName=[value];SharedAccessKey=[value];EntityPath=[value]")] + public void ValidateDetectsMissingConnectionStringInformation(string connectionString) + { + var properties = EventHubsConnectionStringProperties.Parse(connectionString); + Assert.That(() => properties.Validate(null, "Dummy"), Throws.ArgumentException.And.Message.StartsWith(Resources.MissingConnectionInformation)); + } + + /// + /// Verifies functionality of the + /// method. + /// + /// + [Test] + public void ValidateDetectsMultipleEventHubNames() + { + var eventHubName = "myHub"; + var fakeConnection = "Endpoint=sb://not-real.servicebus.windows.net/;SharedAccessKeyName=DummyKey;SharedAccessKey=[not_real];EntityPath=[unique_fake]"; + var properties = EventHubsConnectionStringProperties.Parse(fakeConnection); + + Assert.That(() => properties.Validate(eventHubName, "Dummy"), Throws.ArgumentException.And.Message.StartsWith(Resources.OnlyOneEventHubNameMayBeSpecified)); + } + + /// + /// Verifies functionality of the + /// method. + /// + /// + [Test] + public void ValidateAllowsMultipleEventHubNamesIfEqual() + { + var eventHubName = "myHub"; + var fakeConnection = $"Endpoint=sb://not-real.servicebus.windows.net/;SharedAccessKeyName=DummyKey;SharedAccessKey=[not_real];EntityPath={ eventHubName }"; + var properties = EventHubsConnectionStringProperties.Parse(fakeConnection); + + Assert.That(() => properties.Validate(eventHubName, "dummy"), Throws.Nothing, "Validation should accept the same Event Hub in multiple places."); + } + + /// + /// Verifies functionality of the + /// method. + /// + /// + [Test] + [TestCase("Endpoint=sb://not-real.servicebus.windows.net/;SharedAccessKeyName=DummyKey;SharedAccessKey=[not_real];EntityPath=[unique_fake];SharedAccessSignature=[not_real]")] + [TestCase("Endpoint=sb://not-real.servicebus.windows.net/;SharedAccessKeyName=DummyKey;EntityPath=[unique_fake];SharedAccessSignature=[not_real]")] + [TestCase("Endpoint=sb://not-real.servicebus.windows.net/;SharedAccessKey=[not_real];EntityPath=[unique_fake];SharedAccessSignature=[not_real]")] + public void ValidateDetectsMultipleAuthorizationCredentials(string connectionString) + { + var properties = EventHubsConnectionStringProperties.Parse(connectionString); + Assert.That(() => properties.Validate(null, "Dummy"), Throws.ArgumentException.And.Message.StartsWith(Resources.OnlyOneSharedAccessAuthorizationMayBeSpecified)); + } + + /// + /// Verifies functionality of the + /// method. + /// + /// + [Test] + public void ValidateAllowsSharedAccessKeyAuthorization() + { + var eventHubName = "myHub"; + var fakeConnection = "Endpoint=sb://not-real.servicebus.windows.net/;SharedAccessKeyName=DummyKey;SharedAccessKey=[not_real]"; + var properties = EventHubsConnectionStringProperties.Parse(fakeConnection); + + Assert.That(() => properties.Validate(eventHubName, "dummy"), Throws.Nothing, "Validation should accept the shared access key authorization."); + } + + /// + /// Verifies functionality of the + /// method. + /// + /// + [Test] + public void ValidateAllowsSharedAccessSignatureAuthorization() + { + var eventHubName = "myHub"; + var fakeConnection = "Endpoint=sb://not-real.servicebus.windows.net/;SharedAccessSignature=[not_real]"; + var properties = EventHubsConnectionStringProperties.Parse(fakeConnection); + + Assert.That(() => properties.Validate(eventHubName, "dummy"), Throws.Nothing, "Validation should accept the shared access signature authorization."); + } + + /// + /// Compares two instances for + /// structural equality. + /// + /// + /// The first instance to consider. + /// The second instance to consider. + /// + /// true if the instances are equivalent; otherwise, false. + /// + private static bool PropertiesAreEquivalent(EventHubsConnectionStringProperties first, + EventHubsConnectionStringProperties second) + { + if (object.ReferenceEquals(first, second)) + { + return true; + } + + if ((first == null) || (second == null)) + { + return false; + } + + return string.Equals(first.Endpoint.AbsoluteUri, second.Endpoint.AbsoluteUri, StringComparison.OrdinalIgnoreCase) + && string.Equals(first.EventHubName, second.EventHubName, 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); } /// - /// Gets the Event Hubs endpoint scheme used by the + /// Gets the Event Hubs endpoint scheme used by the /// using its private field. /// /// @@ -342,7 +625,7 @@ public void ParseConsidersMissingValuesAsMalformed(string connectionString) /// private static string GetEventHubsEndpointScheme() => (string) - typeof(ConnectionStringParser) + typeof(EventHubsConnectionStringProperties) .GetField("EventHubsEndpointScheme", BindingFlags.Static | BindingFlags.NonPublic) .GetValue(null); } diff --git a/sdk/eventhub/Azure.Messaging.EventHubs/tests/Primitives/EventProcessorTests.Constructor.cs b/sdk/eventhub/Azure.Messaging.EventHubs/tests/Primitives/EventProcessorTests.Constructor.cs index 729d984d09aa..cad94db00a7a 100644 --- a/sdk/eventhub/Azure.Messaging.EventHubs/tests/Primitives/EventProcessorTests.Constructor.cs +++ b/sdk/eventhub/Azure.Messaging.EventHubs/tests/Primitives/EventProcessorTests.Constructor.cs @@ -35,7 +35,8 @@ public static IEnumerable ConstructorCreatesDefaultOptionsCases() yield return new object[] { new MinimalProcessorMock(99, "consumerGroup", connectionString), "connection string with default options" }; yield return new object[] { new MinimalProcessorMock(99, "consumerGroup", connectionStringNoHub, "hub", default), "connection string with default options" }; - yield return new object[] { new MinimalProcessorMock(99, "consumerGroup", "namespace", "hub", credential, default(EventProcessorOptions)), "namespace with explicit null options" }; + yield return new object[] { new MinimalProcessorMock(99, "consumerGroup", "namespace", "hub", credential, default(EventProcessorOptions)), "token credential with explicit null options" }; + yield return new object[] { new MinimalProcessorMock(99, "consumerGroup", "namespace", "hub", new EventHubsSharedAccessKeyCredential("key", "value"), default(EventProcessorOptions)), "shared key credential with explicit null options" }; } /// @@ -51,7 +52,8 @@ public static IEnumerable ConstructorCreatesDefaultOptionsCases() public void ConstructorValidatesTheEventBatchMaximumCount(int constructorArgument) { Assert.That(() => new MinimalProcessorMock(constructorArgument, "dummyGroup", "dummyConnection", new EventProcessorOptions()), Throws.InstanceOf(), "The connection string constructor should validate the maximum batch size."); - Assert.That(() => new MinimalProcessorMock(constructorArgument, "dummyGroup", "dummyNamespace", "dummyEventHub", Mock.Of(), new EventProcessorOptions()), Throws.InstanceOf(), "The namespace constructor should validate the maximum batch size."); + Assert.That(() => new MinimalProcessorMock(constructorArgument, "dummyGroup", "dummyNamespace", "dummyEventHub", Mock.Of(), new EventProcessorOptions()), Throws.InstanceOf(), "The token credential constructor should validate the maximum batch size."); + Assert.That(() => new MinimalProcessorMock(constructorArgument, "dummyGroup", "dummyNamespace", "dummyEventHub", new EventHubsSharedAccessKeyCredential("key", "value"), new EventProcessorOptions()), Throws.InstanceOf(), "The shared key credential constructor should validate the maximum batch size."); } /// @@ -65,7 +67,8 @@ public void ConstructorValidatesTheEventBatchMaximumCount(int constructorArgumen public void ConstructorValidatesTheConsumerGroup(string constructorArgument) { Assert.That(() => new MinimalProcessorMock(1, constructorArgument, "dummyConnection", new EventProcessorOptions()), Throws.InstanceOf(), "The connection string constructor should validate the consumer group."); - Assert.That(() => new MinimalProcessorMock(1, constructorArgument, "dummyNamespace", "dummyEventHub", Mock.Of(), new EventProcessorOptions()), Throws.InstanceOf(), "The namespace constructor should validate the consumer group."); + Assert.That(() => new MinimalProcessorMock(1, constructorArgument, "dummyNamespace", "dummyEventHub", Mock.Of(), new EventProcessorOptions()), Throws.InstanceOf(), "The token credential constructor should validate the consumer group."); + Assert.That(() => new MinimalProcessorMock(1, constructorArgument, "dummyNamespace", "dummyEventHub", new EventHubsSharedAccessKeyCredential("key", "value"), new EventProcessorOptions()), Throws.InstanceOf(), "The shared key credential constructor should validate the consumer group."); } /// @@ -138,7 +141,8 @@ public void ConstructorAllowsMultipleEventHubNamesFromTheConnectionStringIfEqual [TestCase("sb://namespace.place.com")] public void ConstructorValidatesTheNamespace(string constructorArgument) { - Assert.That(() => new MinimalProcessorMock(1, EventHubConsumerClient.DefaultConsumerGroupName, constructorArgument, "dummy", Mock.Of()), Throws.InstanceOf()); + Assert.That(() => new MinimalProcessorMock(1, EventHubConsumerClient.DefaultConsumerGroupName, constructorArgument, "dummy", Mock.Of()), Throws.InstanceOf(), "The token credential constructor should validate."); + Assert.That(() => new MinimalProcessorMock(1, EventHubConsumerClient.DefaultConsumerGroupName, constructorArgument, "dummy", new EventHubsSharedAccessKeyCredential("key", "value")), Throws.InstanceOf(), "The shared key credential constructor should validate."); } /// @@ -151,7 +155,8 @@ public void ConstructorValidatesTheNamespace(string constructorArgument) [TestCase("")] public void ConstructorValidatesTheEventHub(string constructorArgument) { - Assert.That(() => new MinimalProcessorMock(100, EventHubConsumerClient.DefaultConsumerGroupName, "namespace", constructorArgument, Mock.Of()), Throws.InstanceOf()); + Assert.That(() => new MinimalProcessorMock(100, EventHubConsumerClient.DefaultConsumerGroupName, "namespace", constructorArgument, Mock.Of()), Throws.InstanceOf(), "The token credential constructor should validate."); + Assert.That(() => new MinimalProcessorMock(100, EventHubConsumerClient.DefaultConsumerGroupName, "namespace", constructorArgument, new EventHubsSharedAccessKeyCredential("key", "value")), Throws.InstanceOf(), "The shared key credential constructor should validate."); } /// @@ -162,7 +167,8 @@ public void ConstructorValidatesTheEventHub(string constructorArgument) [Test] public void ConstructorValidatesTheCredential() { - Assert.That(() => new MinimalProcessorMock(5, EventHubConsumerClient.DefaultConsumerGroupName, "namespace", "hubName", default(TokenCredential)), Throws.ArgumentNullException); + Assert.That(() => new MinimalProcessorMock(5, EventHubConsumerClient.DefaultConsumerGroupName, "namespace", "hubName", default(TokenCredential)), Throws.ArgumentNullException, "The token credential constructor should validate."); + Assert.That(() => new MinimalProcessorMock(5, EventHubConsumerClient.DefaultConsumerGroupName, "namespace", "hubName", default(EventHubsSharedAccessKeyCredential)), Throws.ArgumentNullException, "The shared key credential constructor should validate."); } /// @@ -216,7 +222,7 @@ public void ConnectionStringConstructorClonesTheConnectionOptions() /// /// [Test] - public void NamespaceConstructorClonesTheConnectionOptions() + public void TokenCredentialConstructorClonesTheConnectionOptions() { var expectedTransportType = EventHubsTransportType.AmqpWebSockets; var otherTransportType = EventHubsTransportType.AmqpTcp; @@ -239,6 +245,35 @@ public void NamespaceConstructorClonesTheConnectionOptions() Assert.That(connectionOptions.TransportType, Is.EqualTo(expectedTransportType), $"The connection options should have been cloned."); } + /// + /// Verifies functionality of the + /// constructor. + /// + /// + [Test] + public void SharedKeyCredentialConstructorClonesTheConnectionOptions() + { + var expectedTransportType = EventHubsTransportType.AmqpWebSockets; + var otherTransportType = EventHubsTransportType.AmqpTcp; + + var options = new EventProcessorOptions + { + ConnectionOptions = new EventHubConnectionOptions { TransportType = expectedTransportType } + }; + + var eventProcessor = new MinimalProcessorMock(11, "consumerGroup", "namespace", "hub", new EventHubsSharedAccessKeyCredential("key", "value"), options); + + // Simply retrieving the options from an inner connection won't be enough to prove the processor clones + // its connection options because the cloning step also happens in the EventHubConnection constructor. + // For this reason, we will change the transport type and verify that it won't affect the returned + // connection options. + + options.ConnectionOptions.TransportType = otherTransportType; + + var connectionOptions = GetConnectionOptions(eventProcessor); + Assert.That(connectionOptions.TransportType, Is.EqualTo(expectedTransportType), $"The connection options should have been cloned."); + } + /// /// Verifies functionality of the /// constructor. @@ -264,7 +299,7 @@ public void ConnectionStringConstructorSetsTheIdentifier() /// /// [Test] - public void NamespaceConstructorSetsTheIdentifier() + public void TokenCredentialConstructorSetsTheIdentifier() { var options = new EventProcessorOptions { @@ -277,6 +312,25 @@ public void NamespaceConstructorSetsTheIdentifier() Assert.That(eventProcessor.Identifier, Is.EqualTo(options.Identifier)); } + /// + /// Verifies functionality of the + /// constructor. + /// + /// + [Test] + public void SharedKeyCredentialConstructorSetsTheIdentifier() + { + var options = new EventProcessorOptions + { + Identifier = Guid.NewGuid().ToString() + }; + + var eventProcessor = new MinimalProcessorMock(65, "consumerGroup", "namespace", "hub", new EventHubsSharedAccessKeyCredential("key", "value"), options); + + Assert.That(eventProcessor.Identifier, Is.Not.Null); + Assert.That(eventProcessor.Identifier, Is.EqualTo(options.Identifier)); + } + /// /// Verifies functionality of the /// constructor. @@ -306,7 +360,7 @@ public void ConnectionStringConstructorCreatesTheIdentifierWhenNotSpecified(stri [Test] [TestCase(null)] [TestCase("")] - public void NamespaceConstructorCreatesTheIdentifierWhenNotSpecified(string identifier) + public void TokenCredentialConstructorCreatesTheIdentifierWhenNotSpecified(string identifier) { var options = new EventProcessorOptions { @@ -318,5 +372,26 @@ public void NamespaceConstructorCreatesTheIdentifierWhenNotSpecified(string iden Assert.That(eventProcessor.Identifier, Is.Not.Null); Assert.That(eventProcessor.Identifier, Is.Not.Empty); } + + /// + /// Verifies functionality of the + /// constructor. + /// + /// + [Test] + [TestCase(null)] + [TestCase("")] + public void SharedKeyCredentialConstructorCreatesTheIdentifierWhenNotSpecified(string identifier) + { + var options = new EventProcessorOptions + { + Identifier = identifier + }; + + var eventProcessor = new MinimalProcessorMock(665, "consumerGroup", "namespace", "hub", new EventHubsSharedAccessKeyCredential("key", "value"), options); + + Assert.That(eventProcessor.Identifier, Is.Not.Null); + Assert.That(eventProcessor.Identifier, Is.Not.Empty); + } } } diff --git a/sdk/eventhub/Azure.Messaging.EventHubs/tests/Primitives/EventProcessorTests.Infrastructure.cs b/sdk/eventhub/Azure.Messaging.EventHubs/tests/Primitives/EventProcessorTests.Infrastructure.cs index d5b7b62541f4..f584ac2f24f1 100644 --- a/sdk/eventhub/Azure.Messaging.EventHubs/tests/Primitives/EventProcessorTests.Infrastructure.cs +++ b/sdk/eventhub/Azure.Messaging.EventHubs/tests/Primitives/EventProcessorTests.Infrastructure.cs @@ -189,7 +189,7 @@ public async Task ProcessorStorageManagerDelegatesCalls() .Callback(() => claimOwnershipDelegated = true) .Returns(Task.FromResult(default(IEnumerable))); - var storageManager = mockProcessor.Object.CreateStorageManager(mockProcessor.Object); + var storageManager = EventProcessor.CreateStorageManager(mockProcessor.Object); Assert.That(storageManager, Is.Not.Null, "The storage manager should have been created."); await storageManager.ListCheckpointsAsync("na", "na", "na", CancellationToken.None); @@ -215,7 +215,7 @@ public void ProcessorStorageManagerDoesNotAllowCheckpointUpdate() var consumerGroup = "cg"; var mockProcessor = new Mock>(25, consumerGroup, fqNamespace, eventHub, Mock.Of(), default(EventProcessorOptions)) { CallBase = true }; - var storageManager = mockProcessor.Object.CreateStorageManager(mockProcessor.Object); + var storageManager = EventProcessor.CreateStorageManager(mockProcessor.Object); Assert.That(storageManager, Is.Not.Null, "The storage manager should have been created."); Assert.That(() => storageManager.UpdateCheckpointAsync(new EventProcessorCheckpoint(), new EventData(Array.Empty()), CancellationToken.None), Throws.InstanceOf(), "Calling to update checkpoints should not be implemented."); diff --git a/sdk/eventhub/Azure.Messaging.EventHubs/tests/Primitives/EventProcessorTests.cs b/sdk/eventhub/Azure.Messaging.EventHubs/tests/Primitives/EventProcessorTests.cs index 8663238d222e..4bcd36917a3f 100644 --- a/sdk/eventhub/Azure.Messaging.EventHubs/tests/Primitives/EventProcessorTests.cs +++ b/sdk/eventhub/Azure.Messaging.EventHubs/tests/Primitives/EventProcessorTests.cs @@ -123,6 +123,13 @@ public MinimalProcessorMock(int eventBatchMaximumCount, string eventHubName, EventProcessorOptions options = default) : base(eventBatchMaximumCount, consumerGroup, connectionString, eventHubName, options) { } + public MinimalProcessorMock(int eventBatchMaximumCount, + string consumerGroup, + string fullyQualifiedNamespace, + string eventHubName, + EventHubsSharedAccessKeyCredential credential, + EventProcessorOptions options = default) : base(eventBatchMaximumCount, consumerGroup, fullyQualifiedNamespace, eventHubName, credential, options) { } + public MinimalProcessorMock(int eventBatchMaximumCount, string consumerGroup, string fullyQualifiedNamespace, diff --git a/sdk/eventhub/Azure.Messaging.EventHubs/tests/Primitives/PartitionReceiverLiveTests.cs b/sdk/eventhub/Azure.Messaging.EventHubs/tests/Primitives/PartitionReceiverLiveTests.cs old mode 100755 new mode 100644 index 193e5524c6db..faeec6a5cd49 --- a/sdk/eventhub/Azure.Messaging.EventHubs/tests/Primitives/PartitionReceiverLiveTests.cs +++ b/sdk/eventhub/Azure.Messaging.EventHubs/tests/Primitives/PartitionReceiverLiveTests.cs @@ -390,6 +390,43 @@ public async Task ReceiverCanReadEventsUsingAnIdentityCredential() } } + /// + /// Verifies that the is able to + /// connect to the Event Hubs service and perform operations. + /// + /// + [Test] + public async Task ReceiverCanReadEventsUsingTheSharedKeyCredential() + { + await using (EventHubScope scope = await EventHubScope.CreateAsync(1)) + { + using var cancellationSource = new CancellationTokenSource(); + cancellationSource.CancelAfter(EventHubsTestEnvironment.Instance.TestExecutionTimeLimit); + + var credential = new EventHubsSharedAccessKeyCredential(EventHubsTestEnvironment.Instance.SharedAccessKeyName, EventHubsTestEnvironment.Instance.SharedAccessKey); + var sourceEvents = EventGenerator.CreateEvents(50).ToList(); + + var connectionString = EventHubsTestEnvironment.Instance.BuildConnectionStringForEventHub(scope.EventHubName); + var partition = (await QueryPartitionsAsync(connectionString, cancellationSource.Token)).First(); + await SendEventsAsync(connectionString, sourceEvents, new CreateBatchOptions { PartitionId = partition }, cancellationSource.Token); + + await using (var receiver = new PartitionReceiver(EventHubConsumerClient.DefaultConsumerGroupName, partition, EventPosition.Earliest, EventHubsTestEnvironment.Instance.FullyQualifiedNamespace, scope.EventHubName, credential)) + { + var readState = await ReadEventsAsync(receiver, sourceEvents.Count, cancellationSource.Token); + Assert.That(cancellationSource.IsCancellationRequested, Is.False, "The cancellation token should not have been signaled."); + + foreach (var sourceEvent in sourceEvents) + { + var sourceId = sourceEvent.Properties[EventGenerator.IdPropertyName].ToString(); + Assert.That(readState.Events.TryGetValue(sourceId, out var readEvent), Is.True, $"The event with custom identifier [{ sourceId }] was not processed."); + Assert.That(sourceEvent.IsEquivalentTo(readEvent), $"The event with custom identifier [{ sourceId }] did not match the corresponding processed event."); + } + } + + cancellationSource.Cancel(); + } + } + /// /// Verifies that the is able to /// connect to the Event Hubs service and perform operations. diff --git a/sdk/eventhub/Azure.Messaging.EventHubs/tests/Primitives/PartitionReceiverTests.cs b/sdk/eventhub/Azure.Messaging.EventHubs/tests/Primitives/PartitionReceiverTests.cs index 96a05ff3dd5e..1b12a65b30c4 100644 --- a/sdk/eventhub/Azure.Messaging.EventHubs/tests/Primitives/PartitionReceiverTests.cs +++ b/sdk/eventhub/Azure.Messaging.EventHubs/tests/Primitives/PartitionReceiverTests.cs @@ -35,7 +35,8 @@ public void ConstructorValidatesTheConsumerGroup(string consumerGroup) { Assert.That(() => new PartitionReceiver(consumerGroup, "pid", EventPosition.Earliest, "cs"), Throws.InstanceOf(), "The connection string constructor without event hub should perform validation."); Assert.That(() => new PartitionReceiver(consumerGroup, "pid", EventPosition.Earliest, "cs", "eh"), Throws.InstanceOf(), "The connection string constructor with event hub should perform validation."); - Assert.That(() => new PartitionReceiver(consumerGroup, "pid", EventPosition.Earliest, "fqns", "eh", Mock.Of()), Throws.InstanceOf(), "The namespace constructor should perform validation."); + Assert.That(() => new PartitionReceiver(consumerGroup, "pid", EventPosition.Earliest, "fqns", "eh", Mock.Of()), Throws.InstanceOf(), "The token credential constructor should perform validation."); + Assert.That(() => new PartitionReceiver(consumerGroup, "pid", EventPosition.Earliest, "fqns", "eh", new EventHubsSharedAccessKeyCredential("key", "value")), Throws.InstanceOf(), "The shared key credential constructor should perform validation."); Assert.That(() => new PartitionReceiver(consumerGroup, "pid", EventPosition.Earliest, Mock.Of()), Throws.InstanceOf(), "The connection constructor should perform validation."); } @@ -50,7 +51,8 @@ public void ConstructorValidatesThePartitionId(string partitionId) { Assert.That(() => new PartitionReceiver("cg", partitionId, EventPosition.Earliest, "cs"), Throws.InstanceOf(), "The connection string constructor without event hub should perform validation."); Assert.That(() => new PartitionReceiver("cg", partitionId, EventPosition.Earliest, "cs", "eh"), Throws.InstanceOf(), "The connection string constructor with event hub should perform validation."); - Assert.That(() => new PartitionReceiver("cg", partitionId, EventPosition.Earliest, "fqns", "eh", Mock.Of()), Throws.InstanceOf(), "The namespace constructor should perform validation."); + Assert.That(() => new PartitionReceiver("cg", partitionId, EventPosition.Earliest, "fqns", "eh", Mock.Of()), Throws.InstanceOf(), "The token credential constructor should perform validation."); + Assert.That(() => new PartitionReceiver("cg", partitionId, EventPosition.Earliest, "fqns", "eh", new EventHubsSharedAccessKeyCredential("key", "value")), Throws.InstanceOf(), "The shared key credential constructor should perform validation."); Assert.That(() => new PartitionReceiver("cg", partitionId, EventPosition.Earliest, Mock.Of()), Throws.InstanceOf(), "The connection constructor should perform validation."); } @@ -77,7 +79,8 @@ public void ConstructorValidatesTheConnectionString(string connectionString) [TestCase("amqps://namespace.windows.servicebus.net")] public void ConstructorValidatesTheFullyQualifiedNamespace(string fullyQualifiedNamespace) { - Assert.That(() => new PartitionReceiver("cg", "pid", EventPosition.Earliest, fullyQualifiedNamespace, "eh", Mock.Of()), Throws.InstanceOf(), "The constructor should perform validation."); + Assert.That(() => new PartitionReceiver("cg", "pid", EventPosition.Earliest, fullyQualifiedNamespace, "eh", Mock.Of()), Throws.InstanceOf(), "The token credential constructor should perform validation."); + Assert.That(() => new PartitionReceiver("cg", "pid", EventPosition.Earliest, fullyQualifiedNamespace, "eh", new EventHubsSharedAccessKeyCredential("key", "value")), Throws.InstanceOf(), "The shared key credential constructor should perform validation."); } /// @@ -89,7 +92,8 @@ public void ConstructorValidatesTheFullyQualifiedNamespace(string fullyQualified [TestCase("")] public void ConstructorValidatesTheEventHubName(string eventHubName) { - Assert.That(() => new PartitionReceiver("cg", "pid", EventPosition.Earliest, "fqns", eventHubName, Mock.Of()), Throws.InstanceOf(), "The constructor should perform validation."); + Assert.That(() => new PartitionReceiver("cg", "pid", EventPosition.Earliest, "fqns", eventHubName, Mock.Of()), Throws.InstanceOf(), "The token credential constructor should perform validation."); + Assert.That(() => new PartitionReceiver("cg", "pid", EventPosition.Earliest, "fqns", eventHubName, new EventHubsSharedAccessKeyCredential("key", "value")), Throws.InstanceOf(), "The shared key credential constructor should perform validation."); } /// @@ -99,7 +103,8 @@ public void ConstructorValidatesTheEventHubName(string eventHubName) [Test] public void ConstructorValidatesTheCredential() { - Assert.That(() => new PartitionReceiver("cg", "pid", EventPosition.Earliest, "fqns", "eh", default(TokenCredential)), Throws.InstanceOf(), "The constructor should perform validation."); + Assert.That(() => new PartitionReceiver("cg", "pid", EventPosition.Earliest, "fqns", "eh", default(TokenCredential)), Throws.InstanceOf(), "The token credential constructor should perform validation."); + Assert.That(() => new PartitionReceiver("cg", "pid", EventPosition.Earliest, "fqns", "eh", default(EventHubsSharedAccessKeyCredential)), Throws.InstanceOf(), "The shared key credential constructor should perform validation."); } /// @@ -132,7 +137,7 @@ public void ConnectionStringConstructorSetsTheRetryPolicy() /// /// [Test] - public void ExpandedConstructorSetsTheRetryPolicy() + public void TokenCredentialConstructorSetsTheRetryPolicy() { var expected = Mock.Of(); var options = new PartitionReceiverOptions { RetryOptions = new EventHubsRetryOptions { CustomRetryPolicy = expected } }; @@ -141,6 +146,20 @@ public void ExpandedConstructorSetsTheRetryPolicy() Assert.That(GetRetryPolicy(receiver), Is.SameAs(expected)); } + /// + /// Verifies functionality of the constructor. + /// + /// + [Test] + public void SharedKeyCredentialConstructorSetsTheRetryPolicy() + { + var expected = Mock.Of(); + var options = new PartitionReceiverOptions { RetryOptions = new EventHubsRetryOptions { CustomRetryPolicy = expected } }; + var receiver = new PartitionReceiver("cg", "pid", EventPosition.Earliest, "fqns", "eh", new EventHubsSharedAccessKeyCredential("key", "value"), options); + + Assert.That(GetRetryPolicy(receiver), Is.SameAs(expected)); + } + /// /// Verifies functionality of the constructor. /// @@ -175,7 +194,7 @@ public void ConnectionStringConstructorSetsTheDefaultMaximumWaitTime() /// /// [Test] - public void ExpandedConstructorSetsTheDefaultMaximumWaitTime() + public void TokenCredentialConstructorSetsTheDefaultMaximumWaitTime() { var expected = TimeSpan.FromMinutes(1); var options = new PartitionReceiverOptions { DefaultMaximumReceiveWaitTime = expected }; @@ -184,6 +203,20 @@ public void ExpandedConstructorSetsTheDefaultMaximumWaitTime() Assert.That(GetDefaultMaximumWaitTime(receiver), Is.EqualTo(expected)); } + /// + /// Verifies functionality of the constructor. + /// + /// + [Test] + public void SharedKeyCredentialConstructorSetsTheDefaultMaximumWaitTime() + { + var expected = TimeSpan.FromMinutes(1); + var options = new PartitionReceiverOptions { DefaultMaximumReceiveWaitTime = expected }; + var receiver = new PartitionReceiver("cg", "pid", EventPosition.Earliest, "fqns", "eh", new EventHubsSharedAccessKeyCredential("key", "value"), options); + + Assert.That(GetDefaultMaximumWaitTime(receiver), Is.EqualTo(expected)); + } + /// /// Verifies functionality of the constructor. /// @@ -231,7 +264,7 @@ public void ConnectionStringConstructorCreatesTheTransportConsumer() /// /// [Test] - public void ExpandedConstructorCreatesTheTransportConsumer() + public void TokenCredentialConstructorCreatesTheTransportConsumer() { var expectedRetryPolicy = Mock.Of(); var expectedOptions = new PartitionReceiverOptions @@ -253,6 +286,33 @@ public void ExpandedConstructorCreatesTheTransportConsumer() Assert.That(receiver.TransportConsumerCreatedWithOptions.PrefetchCount, Is.EqualTo(expectedOptions.PrefetchCount), "The constructor should have used the correct prefetch count."); } + /// + /// Verifies functionality of the constructor. + /// + /// + [Test] + public void SharedKeyCredentialConstructorCreatesTheTransportConsumer() + { + var expectedRetryPolicy = Mock.Of(); + var expectedOptions = new PartitionReceiverOptions + { + RetryOptions = new EventHubsRetryOptions { CustomRetryPolicy = expectedRetryPolicy }, + OwnerLevel = 99, + PrefetchCount = 42, + TrackLastEnqueuedEventProperties = false + }; + var receiver = new ObservableConsumerPartitionReceiver("cg", "pid", EventPosition.Earliest, "fqns", "eh", new EventHubsSharedAccessKeyCredential("key", "value"), expectedOptions); + + Assert.That(receiver.TransportConsumerCreatedWithConsumerGroup, Is.EqualTo(receiver.ConsumerGroup), "The constructor should have used the correct consumer group."); + Assert.That(receiver.TransportConsumerCreatedWithPartitionId, Is.EqualTo(receiver.PartitionId), "The constructor should have used the correct partition id."); + Assert.That(receiver.TransportConsumerCreatedWithEventPosition, Is.EqualTo(receiver.InitialPosition), "The constructor should have used the correct initial position."); + Assert.That(receiver.TransportConsumerCreatedWithRetryPolicy, Is.SameAs(expectedRetryPolicy), "The constructor should have used the correct retry policy."); + Assert.That(receiver.TransportConsumerCreatedWithOptions, Is.Not.SameAs(expectedOptions), "The constructor should have cloned the options."); + Assert.That(receiver.TransportConsumerCreatedWithOptions.TrackLastEnqueuedEventProperties, Is.EqualTo(expectedOptions.TrackLastEnqueuedEventProperties), "The constructor should have used the correct track last enqueued event properties."); + Assert.That(receiver.TransportConsumerCreatedWithOptions.OwnerLevel, Is.EqualTo(expectedOptions.OwnerLevel), "The constructor should have used the correct owner level."); + Assert.That(receiver.TransportConsumerCreatedWithOptions.PrefetchCount, Is.EqualTo(expectedOptions.PrefetchCount), "The constructor should have used the correct prefetch count."); + } + /// /// Verifies functionality of the constructor. /// @@ -360,7 +420,7 @@ public void ConnectionStringConstructorSetsTheConsumerGroup() /// /// [Test] - public void ExpandedConstructorSetsTheConsumerGroup() + public void TokenCredentialConstructorSetsTheConsumerGroup() { var consumerGroup = "SomeGroup"; var receiver = new PartitionReceiver(consumerGroup, "pid", EventPosition.Earliest, "fqns", "eh", Mock.Of()); @@ -368,6 +428,19 @@ public void ExpandedConstructorSetsTheConsumerGroup() Assert.That(receiver.ConsumerGroup, Is.EqualTo(consumerGroup)); } + /// + /// Verifies functionality of the constructor. + /// + /// + [Test] + public void SharedKeyCredentialConstructorSetsTheConsumerGroup() + { + var consumerGroup = "SomeGroup"; + var receiver = new PartitionReceiver(consumerGroup, "pid", EventPosition.Earliest, "fqns", "eh", new EventHubsSharedAccessKeyCredential("key", "value")); + + Assert.That(receiver.ConsumerGroup, Is.EqualTo(consumerGroup)); + } + /// /// Verifies functionality of the constructor. /// @@ -400,7 +473,7 @@ public void ConnectionStringConstructorSetsThePartitionId() /// /// [Test] - public void ExpandedConstructorSetsThePartitionId() + public void TokenCredentialConstructorSetsThePartitionId() { var partitionId = "partitionId"; var receiver = new PartitionReceiver("cg", partitionId, EventPosition.Earliest, "fqns", "eh", Mock.Of()); @@ -408,6 +481,19 @@ public void ExpandedConstructorSetsThePartitionId() Assert.That(receiver.PartitionId, Is.EqualTo(partitionId)); } + /// + /// Verifies functionality of the constructor. + /// + /// + [Test] + public void SharedKeyCredentialConstructorSetsThePartitionId() + { + var partitionId = "partitionId"; + var receiver = new PartitionReceiver("cg", partitionId, EventPosition.Earliest, "fqns", "eh", new EventHubsSharedAccessKeyCredential("key", "value")); + + Assert.That(receiver.PartitionId, Is.EqualTo(partitionId)); + } + /// /// Verifies functionality of the constructor. /// @@ -440,7 +526,7 @@ public void ConnectionStringConstructorSetsTheInitialPosition() /// /// [Test] - public void ExpandedConstructorSetsTheInitialPosition() + public void TokenCredentialConstructorSetsTheInitialPosition() { var expectedPosition = EventPosition.FromOffset(999); var receiver = new PartitionReceiver("cg", "pid", expectedPosition, "fqns", "eh", Mock.Of()); @@ -448,6 +534,19 @@ public void ExpandedConstructorSetsTheInitialPosition() Assert.That(receiver.InitialPosition, Is.EqualTo(expectedPosition)); } + /// + /// Verifies functionality of the constructor. + /// + /// + [Test] + public void SharedKeyCredentialConstructorSetsTheInitialPosition() + { + var expectedPosition = EventPosition.FromOffset(999); + var receiver = new PartitionReceiver("cg", "pid", expectedPosition, "fqns", "eh", new EventHubsSharedAccessKeyCredential("key", "value")); + + Assert.That(receiver.InitialPosition, Is.EqualTo(expectedPosition)); + } + /// /// Verifies functionality of the constructor. /// @@ -1292,7 +1391,8 @@ public ObservableConsumerPartitionReceiver(string consumerGroup, EventPosition eventPosition, string fullyQualifiedNamespace, string eventHubName, - TokenCredential credential) : base(consumerGroup, partitionId, eventPosition, fullyQualifiedNamespace, eventHubName, credential) + EventHubsSharedAccessKeyCredential credential, + PartitionReceiverOptions options = default) : base(consumerGroup, partitionId, eventPosition, fullyQualifiedNamespace, eventHubName, credential, options) { } @@ -1302,7 +1402,7 @@ public ObservableConsumerPartitionReceiver(string consumerGroup, string fullyQualifiedNamespace, string eventHubName, TokenCredential credential, - PartitionReceiverOptions options) : base(consumerGroup, partitionId, eventPosition, fullyQualifiedNamespace, eventHubName, credential, options) + PartitionReceiverOptions options = default) : base(consumerGroup, partitionId, eventPosition, fullyQualifiedNamespace, eventHubName, credential, options) { } diff --git a/sdk/eventhub/Azure.Messaging.EventHubs/tests/Producer/EventHubProducerClientLiveTests.cs b/sdk/eventhub/Azure.Messaging.EventHubs/tests/Producer/EventHubProducerClientLiveTests.cs old mode 100755 new mode 100644 index b468103bd8e2..b5c2c651f598 --- a/sdk/eventhub/Azure.Messaging.EventHubs/tests/Producer/EventHubProducerClientLiveTests.cs +++ b/sdk/eventhub/Azure.Messaging.EventHubs/tests/Producer/EventHubProducerClientLiveTests.cs @@ -451,6 +451,34 @@ public async Task ProducerCanSendAnEventBatchUsingAnIdentityCredential() } } + /// + /// Verifies that the is able to + /// connect to the Event Hubs service and perform operations. + /// + /// + [Test] + public async Task ProducerCanSendAnEventBatchUsingTheSharedKeyCredential() + { + await using (EventHubScope scope = await EventHubScope.CreateAsync(1)) + { + var credential = new EventHubsSharedAccessKeyCredential(EventHubsTestEnvironment.Instance.SharedAccessKeyName, EventHubsTestEnvironment.Instance.SharedAccessKey); + + await using (var producer = new EventHubProducerClient(EventHubsTestEnvironment.Instance.FullyQualifiedNamespace, scope.EventHubName, credential)) + { + using EventDataBatch batch = await producer.CreateBatchAsync(); + + batch.TryAdd(new EventData(Encoding.UTF8.GetBytes("This is a message"))); + batch.TryAdd(new EventData(Encoding.UTF8.GetBytes("This is another message"))); + batch.TryAdd(new EventData(Encoding.UTF8.GetBytes("So many messages"))); + batch.TryAdd(new EventData(Encoding.UTF8.GetBytes("Event more messages"))); + batch.TryAdd(new EventData(Encoding.UTF8.GetBytes("Will it ever stop?"))); + + Assert.That(batch.Count, Is.EqualTo(5), "The batch should contain all 5 events."); + Assert.That(async () => await producer.SendAsync(batch), Throws.Nothing); + } + } + } + /// /// Verifies that the is able to /// connect to the Event Hubs service and perform operations. diff --git a/sdk/eventhub/Azure.Messaging.EventHubs/tests/Producer/EventHubProducerClientTests.cs b/sdk/eventhub/Azure.Messaging.EventHubs/tests/Producer/EventHubProducerClientTests.cs index 9274eabe5c4e..406f64638a7d 100644 --- a/sdk/eventhub/Azure.Messaging.EventHubs/tests/Producer/EventHubProducerClientTests.cs +++ b/sdk/eventhub/Azure.Messaging.EventHubs/tests/Producer/EventHubProducerClientTests.cs @@ -94,7 +94,8 @@ public void ConstructorAllowsMultipleEventHubNamesFromTheConnectionStringIfEqual public void ConstructorValidatesTheNamespace(string constructorArgument) { var credential = new Mock(Mock.Of(), "{namespace}.servicebus.windows.net"); - Assert.That(() => new EventHubProducerClient(constructorArgument, "dummy", credential.Object), Throws.InstanceOf()); + Assert.That(() => new EventHubProducerClient(constructorArgument, "dummy", credential.Object), Throws.InstanceOf(), "The token credential constructor should validate."); + Assert.That(() => new EventHubProducerClient(constructorArgument, "dummy", new EventHubsSharedAccessKeyCredential("key", "value")), Throws.InstanceOf(), "The shared key credential constructor should validate."); } /// @@ -107,7 +108,8 @@ public void ConstructorValidatesTheNamespace(string constructorArgument) public void ConstructorValidatesTheEventHub(string constructorArgument) { var credential = new Mock(Mock.Of(), "{namespace}.servicebus.windows.net"); - Assert.That(() => new EventHubProducerClient("namespace", constructorArgument, credential.Object), Throws.InstanceOf()); + Assert.That(() => new EventHubProducerClient("namespace", constructorArgument, credential.Object), Throws.InstanceOf(), "The token credential constructor should validate."); + Assert.That(() => new EventHubProducerClient("namespace", constructorArgument, new EventHubsSharedAccessKeyCredential("key", "value")), Throws.InstanceOf(), "The shared key credential constructor should validate."); } /// @@ -117,7 +119,8 @@ public void ConstructorValidatesTheEventHub(string constructorArgument) [Test] public void ConstructorValidatesTheCredential() { - Assert.That(() => new EventHubProducerClient("namespace", "hubName", default(TokenCredential)), Throws.ArgumentNullException); + Assert.That(() => new EventHubProducerClient("namespace", "hubName", default(TokenCredential)), Throws.ArgumentNullException, "The token credential constructor should validate."); + Assert.That(() => new EventHubProducerClient("namespace", "hubName", default(EventHubsSharedAccessKeyCredential)), Throws.ArgumentNullException, "The sharedKey credential constructor should validate."); } /// @@ -150,7 +153,7 @@ public void ConnectionStringConstructorSetsTheRetryPolicy() /// /// [Test] - public void ExpandedConstructorSetsTheRetryPolicy() + public void TokenCredentialConstructorSetsTheRetryPolicy() { var expected = Mock.Of(); var credential = new Mock(Mock.Of(), "{namespace}.servicebus.windows.net"); @@ -160,6 +163,21 @@ public void ExpandedConstructorSetsTheRetryPolicy() Assert.That(GetRetryPolicy(producer), Is.SameAs(expected)); } + /// + /// Verifies functionality of the constructor. + /// + /// + [Test] + public void SharedKeyCredentialConstructorSetsTheRetryPolicy() + { + var expected = Mock.Of(); + var credential = new EventHubsSharedAccessKeyCredential("key", "value"); + var options = new EventHubProducerClientOptions { RetryOptions = new EventHubsRetryOptions { CustomRetryPolicy = expected } }; + var producer = new EventHubProducerClient("namespace", "eventHub", credential, options); + + Assert.That(GetRetryPolicy(producer), Is.SameAs(expected)); + } + /// /// Verifies functionality of the constructor. /// @@ -199,7 +217,7 @@ public void ConnectionStringConstructorCreatesDefaultOptions() /// /// [Test] - public void ExpandedConstructorCreatesDefaultOptions() + public void TokenCredentailsConstructorCreatesDefaultOptions() { var credential = new Mock(Mock.Of(), "{namespace}.servicebus.windows.net"); var expected = new EventHubProducerClientOptions().RetryOptions; @@ -213,6 +231,25 @@ public void ExpandedConstructorCreatesDefaultOptions() Assert.That(actual.IsEquivalentTo(expected), Is.True, "The default retry policy should be based on the default retry options."); } + /// + /// Verifies functionality of the constructor. + /// + /// + [Test] + public void SharedKeyCredentailsConstructorCreatesDefaultOptions() + { + var credential = new EventHubsSharedAccessKeyCredential("key", "value"); + var expected = new EventHubProducerClientOptions().RetryOptions; + var producer = new EventHubProducerClient("namespace", "eventHub", credential); + + var policy = GetRetryPolicy(producer); + Assert.That(policy, Is.Not.Null, "There should have been a retry policy set."); + Assert.That(policy, Is.InstanceOf(), "The default retry policy should be a basic policy."); + + var actual = ((BasicRetryPolicy)policy).Options; + Assert.That(actual.IsEquivalentTo(expected), Is.True, "The default retry policy should be based on the default retry options."); + } + /// /// Verifies functionality of the constructor. /// diff --git a/sdk/eventhub/Azure.Messaging.EventHubs/tests/Producer/IdempotentPublishingLiveTests.cs b/sdk/eventhub/Azure.Messaging.EventHubs/tests/Producer/IdempotentPublishingLiveTests.cs index de7d0d2e605f..faf133b69354 100644 --- a/sdk/eventhub/Azure.Messaging.EventHubs/tests/Producer/IdempotentPublishingLiveTests.cs +++ b/sdk/eventhub/Azure.Messaging.EventHubs/tests/Producer/IdempotentPublishingLiveTests.cs @@ -8,7 +8,7 @@ using Azure.Messaging.EventHubs.Producer; using NUnit.Framework; -namespace Azure.Messaging.EventHubs.Tests.Producer +namespace Azure.Messaging.EventHubs.Tests { /// /// The suite of live tests for the idempotent publishing feature of the