From ad752734bc2cfec0e08b9fff88b7c6f2d78dd2b0 Mon Sep 17 00:00:00 2001 From: Connie Yau Date: Mon, 7 Jun 2021 12:48:52 -0700 Subject: [PATCH] Exposes management node in azure-core-amqp (#22095) * Update AmqpConnection to have a getManagementNode. * Adding AmqpManagementNode. * Update AmqpConnection, AmqpManagementNode, AmqpSession to use AsyncCloseable. * Adding AsyncCloseable to AmqpLink. * ClaimsBasedSecurityNode.java uses AsyncCloseable. * Implements CbsNode's closeAsync() and adds tests. * ReactorSession implements closeAsync() * ReactorConnection uses closeAsync(). Renames dispose() to closeAsync(). Fixes errors where some close operations were not subscribed to. * RequestResponseChannel. Remove close operation with message. * Adding DeliveryOutcome models and DeliveryState enum. * Add authorization scope to connection options. * Add MessageUtils to serialize and deserialize AmqpAnnotatedMessage * Update AmqpManagementNode to expose delivery outcomes because they can be associated with messages. * Adding MessageUtil support for converting DeliveryOutcome and Outcomes. * Fixing build breaks from ConnectionOptions. * Adding management channel class. * Adding management channel into ReactorConnection. * Update ExceptionUtil to return instead of throwing on unknown amqp error codes. * Moving ManagementChannel formatting. * Add javadocs to ReceivedDeliveryOutcome. * Add tests for ManagementChannel * Adding tests for message utils. * Fix javadoc on ModifiedDeliveryOutcome * ReactorConnection: Hook up dispose method. * EventHubs: Fixing instances of ConnectionOptions. * ServiceBus: Fix build errors using ConnectionOptions. * Adding MessageUtilsTests. * Updating CHANGELOG. --- sdk/core/azure-core-amqp/CHANGELOG.md | 3 + .../com/azure/core/amqp/AmqpConnection.java | 27 +- .../java/com/azure/core/amqp/AmqpLink.java | 14 +- .../azure/core/amqp/AmqpManagementNode.java | 33 + .../java/com/azure/core/amqp/AmqpSession.java | 8 +- .../core/amqp/ClaimsBasedSecurityNode.java | 8 +- .../ClaimsBasedSecurityChannel.java | 12 +- .../implementation/ConnectionOptions.java | 26 +- .../amqp/implementation/ExceptionUtil.java | 6 +- .../implementation/ManagementChannel.java | 134 +++ .../amqp/implementation/MessageUtils.java | 593 +++++++++++ .../implementation/ReactorConnection.java | 93 +- .../amqp/implementation/ReactorSession.java | 12 +- .../RequestResponseChannel.java | 16 +- .../core/amqp/models/DeliveryOutcome.java | 42 + .../azure/core/amqp/models/DeliveryState.java | 63 ++ .../amqp/models/ModifiedDeliveryOutcome.java | 109 ++ .../amqp/models/ReceivedDeliveryOutcome.java | 64 ++ .../amqp/models/RejectedDeliveryOutcome.java | 81 ++ .../models/TransactionalDeliveryOutcome.java | 73 ++ .../ClaimsBasedSecurityChannelTest.java | 38 + .../implementation/ConnectionOptionsTest.java | 6 +- .../implementation/ManagementChannelTest.java | 352 +++++++ .../amqp/implementation/MessageUtilsTest.java | 936 ++++++++++++++++++ .../implementation/ReactorConnectionTest.java | 52 +- .../ReactorHandlerProviderTest.java | 24 +- .../RequestResponseChannelTest.java | 4 +- .../handler/ConnectionHandlerTest.java | 10 +- .../WebSocketsConnectionHandlerTest.java | 12 +- .../WebSocketsProxyConnectionHandlerTest.java | 5 +- .../core/amqp/models/DeliveryStateTest.java | 64 ++ .../eventhubs/EventHubClientBuilder.java | 14 +- .../AmqpReceiveLinkProcessor.java | 6 +- .../EventHubConsumerAsyncClientTest.java | 9 +- .../eventhubs/EventHubConsumerClientTest.java | 10 +- .../EventHubPartitionAsyncConsumerTest.java | 4 +- .../EventHubProducerAsyncClientTest.java | 10 +- .../eventhubs/EventHubProducerClientTest.java | 8 +- .../implementation/CBSChannelTest.java | 10 +- .../EventHubConnectionProcessorTest.java | 4 +- .../EventHubReactorConnectionTest.java | 5 +- .../servicebus/ServiceBusClientBuilder.java | 7 +- .../ServiceBusReceiveLinkProcessor.java | 6 +- .../ServiceBusReceiverAsyncClientTest.java | 7 +- .../ServiceBusSenderAsyncClientTest.java | 7 +- .../ServiceBusSessionManagerTest.java | 8 +- ...viceBusSessionReceiverAsyncClientTest.java | 10 +- 47 files changed, 2910 insertions(+), 135 deletions(-) create mode 100644 sdk/core/azure-core-amqp/src/main/java/com/azure/core/amqp/AmqpManagementNode.java create mode 100644 sdk/core/azure-core-amqp/src/main/java/com/azure/core/amqp/implementation/ManagementChannel.java create mode 100644 sdk/core/azure-core-amqp/src/main/java/com/azure/core/amqp/implementation/MessageUtils.java create mode 100644 sdk/core/azure-core-amqp/src/main/java/com/azure/core/amqp/models/DeliveryOutcome.java create mode 100644 sdk/core/azure-core-amqp/src/main/java/com/azure/core/amqp/models/DeliveryState.java create mode 100644 sdk/core/azure-core-amqp/src/main/java/com/azure/core/amqp/models/ModifiedDeliveryOutcome.java create mode 100644 sdk/core/azure-core-amqp/src/main/java/com/azure/core/amqp/models/ReceivedDeliveryOutcome.java create mode 100644 sdk/core/azure-core-amqp/src/main/java/com/azure/core/amqp/models/RejectedDeliveryOutcome.java create mode 100644 sdk/core/azure-core-amqp/src/main/java/com/azure/core/amqp/models/TransactionalDeliveryOutcome.java create mode 100644 sdk/core/azure-core-amqp/src/test/java/com/azure/core/amqp/implementation/ManagementChannelTest.java create mode 100644 sdk/core/azure-core-amqp/src/test/java/com/azure/core/amqp/implementation/MessageUtilsTest.java create mode 100644 sdk/core/azure-core-amqp/src/test/java/com/azure/core/amqp/models/DeliveryStateTest.java diff --git a/sdk/core/azure-core-amqp/CHANGELOG.md b/sdk/core/azure-core-amqp/CHANGELOG.md index db88e271f4f70..6eba1b8a641a0 100644 --- a/sdk/core/azure-core-amqp/CHANGELOG.md +++ b/sdk/core/azure-core-amqp/CHANGELOG.md @@ -4,6 +4,9 @@ ### New Features - Exposing CbsAuthorizationType. +- Exposing ManagementNode that can perform management and metadata operations on an AMQP message broker. +- AmqpConnection, AmqpSession, AmqpSendLink, and AmqpReceiveLink extend from AsyncCloseable. +- Delivery outcomes and delivery states are added. ### Bug Fixes - Fixed a bug where connection and sessions would not be disposed when their endpoint closed. diff --git a/sdk/core/azure-core-amqp/src/main/java/com/azure/core/amqp/AmqpConnection.java b/sdk/core/azure-core-amqp/src/main/java/com/azure/core/amqp/AmqpConnection.java index e42c696d0cab6..36721d9789fcb 100644 --- a/sdk/core/azure-core-amqp/src/main/java/com/azure/core/amqp/AmqpConnection.java +++ b/sdk/core/azure-core-amqp/src/main/java/com/azure/core/amqp/AmqpConnection.java @@ -4,6 +4,7 @@ package com.azure.core.amqp; import com.azure.core.amqp.exception.AmqpException; +import com.azure.core.util.AsyncCloseable; import reactor.core.Disposable; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; @@ -13,7 +14,7 @@ /** * Represents a TCP connection between the client and a service that uses the AMQP protocol. */ -public interface AmqpConnection extends Disposable { +public interface AmqpConnection extends Disposable, AsyncCloseable { /** * Gets the connection identifier. * @@ -53,6 +54,7 @@ public interface AmqpConnection extends Disposable { * Creates a new session with the given session name. * * @param sessionName Name of the session. + * * @return The AMQP session that was created. */ Mono createSession(String sessionName); @@ -61,6 +63,7 @@ public interface AmqpConnection extends Disposable { * Removes a session with the {@code sessionName} from the AMQP connection. * * @param sessionName Name of the session to remove. + * * @return {@code true} if a session with the name was removed; {@code false} otherwise. */ boolean removeSession(String sessionName); @@ -79,4 +82,26 @@ public interface AmqpConnection extends Disposable { * @return A stream of shutdown signals that occur in the AMQP endpoint. */ Flux getShutdownSignals(); + + /** + * Gets or creates the management node. + * + * @param entityPath Entity for which to get the management node of. + * + * @return A Mono that completes with the management node. + * + * @throws UnsupportedOperationException if there is no implementation of fetching a management node. + */ + default Mono getManagementNode(String entityPath) { + return Mono.error(new UnsupportedOperationException("This has not been implemented.")); + } + + /** + * Disposes of the AMQP connection. + * + * @return Mono that completes when the close operation is complete. + */ + default Mono closeAsync() { + return Mono.fromRunnable(this::dispose); + } } diff --git a/sdk/core/azure-core-amqp/src/main/java/com/azure/core/amqp/AmqpLink.java b/sdk/core/azure-core-amqp/src/main/java/com/azure/core/amqp/AmqpLink.java index 0021aa4006152..4b1756826078a 100644 --- a/sdk/core/azure-core-amqp/src/main/java/com/azure/core/amqp/AmqpLink.java +++ b/sdk/core/azure-core-amqp/src/main/java/com/azure/core/amqp/AmqpLink.java @@ -4,13 +4,16 @@ package com.azure.core.amqp; import com.azure.core.amqp.exception.AmqpException; +import com.azure.core.util.AsyncCloseable; import reactor.core.Disposable; import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; /** * Represents a unidirectional AMQP link. */ -public interface AmqpLink extends Disposable { +public interface AmqpLink extends Disposable, AsyncCloseable { + /** * Gets the name of the link. * @@ -39,4 +42,13 @@ public interface AmqpLink extends Disposable { * @return A stream of endpoint states for the AMQP link. */ Flux getEndpointStates(); + + /** + * Disposes of the AMQP link. + * + * @return A mono that completes when the link is disposed. + */ + default Mono closeAsync() { + return Mono.fromRunnable(() -> dispose()); + } } diff --git a/sdk/core/azure-core-amqp/src/main/java/com/azure/core/amqp/AmqpManagementNode.java b/sdk/core/azure-core-amqp/src/main/java/com/azure/core/amqp/AmqpManagementNode.java new file mode 100644 index 0000000000000..f7a1e93eb1363 --- /dev/null +++ b/sdk/core/azure-core-amqp/src/main/java/com/azure/core/amqp/AmqpManagementNode.java @@ -0,0 +1,33 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.azure.core.amqp; + +import com.azure.core.amqp.models.AmqpAnnotatedMessage; +import com.azure.core.amqp.models.DeliveryOutcome; +import com.azure.core.util.AsyncCloseable; +import reactor.core.publisher.Mono; + +/** + * An AMQP endpoint that allows users to perform management and metadata operations on it. + */ +public interface AmqpManagementNode extends AsyncCloseable { + /** + * Sends a message to the management node. + * + * @param message Message to send. + * + * @return Response from management node. + */ + Mono send(AmqpAnnotatedMessage message); + + /** + * Sends a message to the management node and associates the {@code deliveryOutcome} with that message. + * + * @param message Message to send. + * @param deliveryOutcome Delivery outcome to associate with the message. + * + * @return Response from management node. + */ + Mono send(AmqpAnnotatedMessage message, DeliveryOutcome deliveryOutcome); +} diff --git a/sdk/core/azure-core-amqp/src/main/java/com/azure/core/amqp/AmqpSession.java b/sdk/core/azure-core-amqp/src/main/java/com/azure/core/amqp/AmqpSession.java index a28b346d3b3b0..3cea71f81b2bd 100644 --- a/sdk/core/azure-core-amqp/src/main/java/com/azure/core/amqp/AmqpSession.java +++ b/sdk/core/azure-core-amqp/src/main/java/com/azure/core/amqp/AmqpSession.java @@ -4,6 +4,7 @@ package com.azure.core.amqp; import com.azure.core.amqp.exception.AmqpException; +import com.azure.core.util.AsyncCloseable; import reactor.core.Disposable; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; @@ -13,7 +14,7 @@ /** * An AMQP session representing bidirectional communication that supports multiple {@link AmqpLink AMQP links}. */ -public interface AmqpSession extends Disposable { +public interface AmqpSession extends Disposable, AsyncCloseable { /** * Gets the name for this AMQP session. * @@ -91,4 +92,9 @@ public interface AmqpSession extends Disposable { * @return A completable mono. */ Mono rollbackTransaction(AmqpTransaction transaction); + + @Override + default Mono closeAsync() { + return Mono.fromRunnable(() -> dispose()); + } } diff --git a/sdk/core/azure-core-amqp/src/main/java/com/azure/core/amqp/ClaimsBasedSecurityNode.java b/sdk/core/azure-core-amqp/src/main/java/com/azure/core/amqp/ClaimsBasedSecurityNode.java index 218f379a6509a..992393a272711 100644 --- a/sdk/core/azure-core-amqp/src/main/java/com/azure/core/amqp/ClaimsBasedSecurityNode.java +++ b/sdk/core/azure-core-amqp/src/main/java/com/azure/core/amqp/ClaimsBasedSecurityNode.java @@ -4,6 +4,7 @@ package com.azure.core.amqp; import com.azure.core.credential.TokenCredential; +import com.azure.core.util.AsyncCloseable; import reactor.core.publisher.Mono; import java.time.OffsetDateTime; @@ -14,7 +15,7 @@ * @see * AMPQ Claims-based Security v1.0 */ -public interface ClaimsBasedSecurityNode extends AutoCloseable { +public interface ClaimsBasedSecurityNode extends AutoCloseable, AsyncCloseable { /** * Authorizes the caller with the CBS node to access resources for the {@code audience}. * @@ -31,4 +32,9 @@ public interface ClaimsBasedSecurityNode extends AutoCloseable { */ @Override void close(); + + @Override + default Mono closeAsync() { + return Mono.fromRunnable(() -> close()); + } } diff --git a/sdk/core/azure-core-amqp/src/main/java/com/azure/core/amqp/implementation/ClaimsBasedSecurityChannel.java b/sdk/core/azure-core-amqp/src/main/java/com/azure/core/amqp/implementation/ClaimsBasedSecurityChannel.java index 5dee735fa6882..1c9309a900f10 100644 --- a/sdk/core/azure-core-amqp/src/main/java/com/azure/core/amqp/implementation/ClaimsBasedSecurityChannel.java +++ b/sdk/core/azure-core-amqp/src/main/java/com/azure/core/amqp/implementation/ClaimsBasedSecurityChannel.java @@ -10,7 +10,6 @@ import com.azure.core.amqp.models.CbsAuthorizationType; import com.azure.core.credential.TokenCredential; import com.azure.core.credential.TokenRequestContext; -import com.azure.core.util.logging.ClientLogger; import org.apache.qpid.proton.Proton; import org.apache.qpid.proton.amqp.messaging.AmqpValue; import org.apache.qpid.proton.amqp.messaging.ApplicationProperties; @@ -35,7 +34,6 @@ public class ClaimsBasedSecurityChannel implements ClaimsBasedSecurityNode { private static final String PUT_TOKEN_OPERATION = "operation"; private static final String PUT_TOKEN_OPERATION_VALUE = "put-token"; - private final ClientLogger logger = new ClientLogger(ClaimsBasedSecurityChannel.class); private final TokenCredential credential; private final Mono cbsChannelMono; private final CbsAuthorizationType authorizationType; @@ -87,9 +85,11 @@ public Mono authorize(String tokenAudience, String scopes) { @Override public void close() { - final RequestResponseChannel channel = cbsChannelMono.block(retryOptions.getTryTimeout()); - if (channel != null) { - channel.closeAsync().block(); - } + closeAsync().block(retryOptions.getTryTimeout()); + } + + @Override + public Mono closeAsync() { + return cbsChannelMono.flatMap(channel -> channel.closeAsync()); } } diff --git a/sdk/core/azure-core-amqp/src/main/java/com/azure/core/amqp/implementation/ConnectionOptions.java b/sdk/core/azure-core-amqp/src/main/java/com/azure/core/amqp/implementation/ConnectionOptions.java index 5075a6c7529a2..f6a74799caf43 100644 --- a/sdk/core/azure-core-amqp/src/main/java/com/azure/core/amqp/implementation/ConnectionOptions.java +++ b/sdk/core/azure-core-amqp/src/main/java/com/azure/core/amqp/implementation/ConnectionOptions.java @@ -23,11 +23,6 @@ */ @Immutable public class ConnectionOptions { - // These name version keys are used in our properties files to specify client product and version information. - static final String NAME_KEY = "name"; - static final String VERSION_KEY = "version"; - static final String UNKNOWN = "UNKNOWN"; - private final TokenCredential tokenCredential; private final AmqpTransportType transport; private final AmqpRetryOptions retryOptions; @@ -35,6 +30,7 @@ public class ConnectionOptions { private final Scheduler scheduler; private final String fullyQualifiedNamespace; private final CbsAuthorizationType authorizationType; + private final String authorizationScope; private final ClientOptions clientOptions; private final String product; private final String clientVersion; @@ -62,10 +58,10 @@ public class ConnectionOptions { * {@code proxyOptions} or {@code verifyMode} is null. */ public ConnectionOptions(String fullyQualifiedNamespace, TokenCredential tokenCredential, - CbsAuthorizationType authorizationType, AmqpTransportType transport, AmqpRetryOptions retryOptions, - ProxyOptions proxyOptions, Scheduler scheduler, ClientOptions clientOptions, + CbsAuthorizationType authorizationType, String authorizationScope, AmqpTransportType transport, + AmqpRetryOptions retryOptions, ProxyOptions proxyOptions, Scheduler scheduler, ClientOptions clientOptions, SslDomain.VerifyMode verifyMode, String product, String clientVersion) { - this(fullyQualifiedNamespace, tokenCredential, authorizationType, transport, retryOptions, + this(fullyQualifiedNamespace, tokenCredential, authorizationType, authorizationScope, transport, retryOptions, proxyOptions, scheduler, clientOptions, verifyMode, product, clientVersion, fullyQualifiedNamespace, getPort(transport)); } @@ -94,14 +90,15 @@ public ConnectionOptions(String fullyQualifiedNamespace, TokenCredential tokenCr * {@code clientOptions}, {@code hostname}, or {@code verifyMode} is null. */ public ConnectionOptions(String fullyQualifiedNamespace, TokenCredential tokenCredential, - CbsAuthorizationType authorizationType, AmqpTransportType transport, AmqpRetryOptions retryOptions, - ProxyOptions proxyOptions, Scheduler scheduler, ClientOptions clientOptions, + CbsAuthorizationType authorizationType, String authorizationScope, AmqpTransportType transport, + AmqpRetryOptions retryOptions, ProxyOptions proxyOptions, Scheduler scheduler, ClientOptions clientOptions, SslDomain.VerifyMode verifyMode, String product, String clientVersion, String hostname, int port) { this.fullyQualifiedNamespace = Objects.requireNonNull(fullyQualifiedNamespace, "'fullyQualifiedNamespace' is required."); this.tokenCredential = Objects.requireNonNull(tokenCredential, "'tokenCredential' is required."); this.authorizationType = Objects.requireNonNull(authorizationType, "'authorizationType' is required."); + this.authorizationScope = Objects.requireNonNull(authorizationScope, "'authorizationScope' is required."); this.transport = Objects.requireNonNull(transport, "'transport' is required."); this.retryOptions = Objects.requireNonNull(retryOptions, "'retryOptions' is required."); this.scheduler = Objects.requireNonNull(scheduler, "'scheduler' is required."); @@ -115,6 +112,15 @@ public ConnectionOptions(String fullyQualifiedNamespace, TokenCredential tokenCr this.clientVersion = Objects.requireNonNull(clientVersion, "'clientVersion' cannot be null."); } + /** + * Gets the scope to use when authorizing. + * + * @return The scope to use when authorizing. + */ + public String getAuthorizationScope() { + return authorizationScope; + } + /** * Gets the authorisation type for the CBS node. * diff --git a/sdk/core/azure-core-amqp/src/main/java/com/azure/core/amqp/implementation/ExceptionUtil.java b/sdk/core/azure-core-amqp/src/main/java/com/azure/core/amqp/implementation/ExceptionUtil.java index da4f2cd989646..27cff01d81583 100644 --- a/sdk/core/azure-core-amqp/src/main/java/com/azure/core/amqp/implementation/ExceptionUtil.java +++ b/sdk/core/azure-core-amqp/src/main/java/com/azure/core/amqp/implementation/ExceptionUtil.java @@ -8,7 +8,6 @@ import com.azure.core.amqp.exception.AmqpException; import com.azure.core.amqp.exception.AmqpResponseCode; -import java.util.Locale; import java.util.regex.Matcher; import java.util.regex.Pattern; @@ -78,8 +77,9 @@ public static Exception toException(String errorCondition, String description, A case NOT_FOUND: return distinguishNotFound(description, errorContext); default: - throw new IllegalArgumentException(String.format(Locale.ROOT, "This condition '%s' is not known.", - condition)); + return new AmqpException(false, condition, String.format("errorCondition[%s]. description[%s] " + + "Condition could not be mapped to a transient condition.", + errorCondition, description), errorContext); } return new AmqpException(isTransient, condition, description, errorContext); diff --git a/sdk/core/azure-core-amqp/src/main/java/com/azure/core/amqp/implementation/ManagementChannel.java b/sdk/core/azure-core-amqp/src/main/java/com/azure/core/amqp/implementation/ManagementChannel.java new file mode 100644 index 0000000000000..f469f879dede0 --- /dev/null +++ b/sdk/core/azure-core-amqp/src/main/java/com/azure/core/amqp/implementation/ManagementChannel.java @@ -0,0 +1,134 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.azure.core.amqp.implementation; + +import com.azure.core.amqp.AmqpManagementNode; +import com.azure.core.amqp.exception.AmqpErrorCondition; +import com.azure.core.amqp.exception.AmqpErrorContext; +import com.azure.core.amqp.exception.AmqpException; +import com.azure.core.amqp.exception.AmqpResponseCode; +import com.azure.core.amqp.exception.SessionErrorContext; +import com.azure.core.amqp.models.AmqpAnnotatedMessage; +import com.azure.core.amqp.models.DeliveryOutcome; +import com.azure.core.util.logging.ClientLogger; +import org.apache.qpid.proton.amqp.transport.DeliveryState; +import org.apache.qpid.proton.message.Message; +import reactor.core.publisher.Mono; +import reactor.core.publisher.SynchronousSink; + +import java.util.Objects; + +/** + * AMQP node responsible for performing management and metadata operations on an Azure AMQP message broker. + */ +public class ManagementChannel implements AmqpManagementNode { + private final TokenManager tokenManager; + private final AmqpChannelProcessor createChannel; + private final String fullyQualifiedNamespace; + private final ClientLogger logger; + private final String entityPath; + + public ManagementChannel(AmqpChannelProcessor createChannel, + String fullyQualifiedNamespace, String entityPath, TokenManager tokenManager) { + this.createChannel = Objects.requireNonNull(createChannel, "'createChannel' cannot be null."); + this.fullyQualifiedNamespace = Objects.requireNonNull(fullyQualifiedNamespace, + "'fullyQualifiedNamespace' cannot be null."); + this.logger = new ClientLogger(String.format("%s<%s>", ManagementChannel.class, entityPath)); + this.entityPath = Objects.requireNonNull(entityPath, "'entityPath' cannot be null."); + this.tokenManager = Objects.requireNonNull(tokenManager, "'tokenManager' cannot be null."); + } + + @Override + public Mono send(AmqpAnnotatedMessage message) { + return isAuthorized().then(createChannel.flatMap(channel -> { + final Message protonJMessage = MessageUtils.toProtonJMessage(message); + + return channel.sendWithAck(protonJMessage) + .handle((Message responseMessage, SynchronousSink sink) -> + handleResponse(responseMessage, sink, channel.getErrorContext())) + .switchIfEmpty(Mono.error(new AmqpException(true, String.format( + "entityPath[%s] No response received from management channel.", entityPath), + channel.getErrorContext()))); + })); + } + + @Override + public Mono send(AmqpAnnotatedMessage message, DeliveryOutcome deliveryOutcome) { + return isAuthorized().then(createChannel.flatMap(channel -> { + final Message protonJMessage = MessageUtils.toProtonJMessage(message); + final DeliveryState protonJDeliveryState = MessageUtils.toProtonJDeliveryState(deliveryOutcome); + + return channel.sendWithAck(protonJMessage, protonJDeliveryState) + .handle((Message responseMessage, SynchronousSink sink) -> + handleResponse(responseMessage, sink, channel.getErrorContext())) + .switchIfEmpty(Mono.error(new AmqpException(true, String.format( + "entityPath[%s] outcome[%s] No response received from management channel.", entityPath, + deliveryOutcome.getDeliveryState()), channel.getErrorContext()))); + })); + } + + @Override + public Mono closeAsync() { + return createChannel.flatMap(channel -> channel.closeAsync()).cache(); + } + + private void handleResponse(Message response, SynchronousSink sink, + AmqpErrorContext errorContext) { + + if (RequestResponseUtils.isSuccessful(response)) { + sink.next(MessageUtils.toAmqpAnnotatedMessage(response)); + return; + } + + final AmqpResponseCode statusCode = RequestResponseUtils.getStatusCode(response); + if (statusCode == AmqpResponseCode.NO_CONTENT) { + sink.next(MessageUtils.toAmqpAnnotatedMessage(response)); + return; + } + + final String errorCondition = RequestResponseUtils.getErrorCondition(response); + if (statusCode == AmqpResponseCode.NOT_FOUND) { + final AmqpErrorCondition amqpErrorCondition = AmqpErrorCondition.fromString(errorCondition); + + if (amqpErrorCondition == AmqpErrorCondition.MESSAGE_NOT_FOUND) { + logger.info("There was no matching message found."); + sink.next(MessageUtils.toAmqpAnnotatedMessage(response)); + } else if (amqpErrorCondition == AmqpErrorCondition.SESSION_NOT_FOUND) { + logger.info("There was no matching session found."); + sink.next(MessageUtils.toAmqpAnnotatedMessage(response)); + } + + return; + } + + final String statusDescription = RequestResponseUtils.getStatusDescription(response); + + logger.warning("status[{}] description[{}] condition[{}] Operation not successful.", + statusCode, statusDescription, errorCondition); + + final Throwable throwable = ExceptionUtil.toException(errorCondition, statusDescription, errorContext); + sink.error(throwable); + } + + private Mono isAuthorized() { + return tokenManager.getAuthorizationResults() + .next() + .switchIfEmpty(Mono.error(new AmqpException(false, "Did not get response from tokenManager: " + entityPath, getErrorContext()))) + .handle((response, sink) -> { + if (response != AmqpResponseCode.ACCEPTED && response != AmqpResponseCode.OK) { + final String message = String.format("User does not have authorization to perform operation " + + "on entity [%s]. Response: [%s]", entityPath, response); + sink.error(ExceptionUtil.amqpResponseCodeToException(response.getValue(), message, + getErrorContext())); + + } else { + sink.complete(); + } + }); + } + + private AmqpErrorContext getErrorContext() { + return new SessionErrorContext(fullyQualifiedNamespace, entityPath); + } +} diff --git a/sdk/core/azure-core-amqp/src/main/java/com/azure/core/amqp/implementation/MessageUtils.java b/sdk/core/azure-core-amqp/src/main/java/com/azure/core/amqp/implementation/MessageUtils.java new file mode 100644 index 0000000000000..e7ed276300c00 --- /dev/null +++ b/sdk/core/azure-core-amqp/src/main/java/com/azure/core/amqp/implementation/MessageUtils.java @@ -0,0 +1,593 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.azure.core.amqp.implementation; + +import com.azure.core.amqp.AmqpTransaction; +import com.azure.core.amqp.exception.AmqpErrorCondition; +import com.azure.core.amqp.models.AmqpAddress; +import com.azure.core.amqp.models.AmqpAnnotatedMessage; +import com.azure.core.amqp.models.AmqpMessageBody; +import com.azure.core.amqp.models.AmqpMessageHeader; +import com.azure.core.amqp.models.AmqpMessageId; +import com.azure.core.amqp.models.AmqpMessageProperties; +import com.azure.core.amqp.models.DeliveryOutcome; +import com.azure.core.amqp.models.DeliveryState; +import com.azure.core.amqp.models.ModifiedDeliveryOutcome; +import com.azure.core.amqp.models.ReceivedDeliveryOutcome; +import com.azure.core.amqp.models.RejectedDeliveryOutcome; +import com.azure.core.amqp.models.TransactionalDeliveryOutcome; +import com.azure.core.util.logging.ClientLogger; +import org.apache.qpid.proton.Proton; +import org.apache.qpid.proton.amqp.Binary; +import org.apache.qpid.proton.amqp.Symbol; +import org.apache.qpid.proton.amqp.UnsignedInteger; +import org.apache.qpid.proton.amqp.UnsignedLong; +import org.apache.qpid.proton.amqp.messaging.Accepted; +import org.apache.qpid.proton.amqp.messaging.ApplicationProperties; +import org.apache.qpid.proton.amqp.messaging.Data; +import org.apache.qpid.proton.amqp.messaging.DeliveryAnnotations; +import org.apache.qpid.proton.amqp.messaging.Footer; +import org.apache.qpid.proton.amqp.messaging.MessageAnnotations; +import org.apache.qpid.proton.amqp.messaging.Modified; +import org.apache.qpid.proton.amqp.messaging.Outcome; +import org.apache.qpid.proton.amqp.messaging.Properties; +import org.apache.qpid.proton.amqp.messaging.Received; +import org.apache.qpid.proton.amqp.messaging.Rejected; +import org.apache.qpid.proton.amqp.messaging.Released; +import org.apache.qpid.proton.amqp.messaging.Section; +import org.apache.qpid.proton.amqp.transaction.Declared; +import org.apache.qpid.proton.amqp.transaction.TransactionalState; +import org.apache.qpid.proton.amqp.transport.DeliveryState.DeliveryStateType; +import org.apache.qpid.proton.amqp.transport.ErrorCondition; +import org.apache.qpid.proton.message.Message; + +import java.time.Duration; +import java.time.ZoneOffset; +import java.util.Date; +import java.util.HashMap; +import java.util.Map; +import java.util.Objects; + +/** + * Converts {@link AmqpAnnotatedMessage messages} to and from proton-j messages. + */ +final class MessageUtils { + private static final ClientLogger LOGGER = new ClientLogger(MessageUtils.class); + private static final byte[] EMPTY_BYTE_ARRAY = new byte[0]; + + /** + * Converts an {@link AmqpAnnotatedMessage} to a proton-j message. + * + * @param message The message to convert. + * + * @return The corresponding proton-j message. + * + * @throws NullPointerException if {@code message} is null. + */ + static Message toProtonJMessage(AmqpAnnotatedMessage message) { + Objects.requireNonNull(message, "'message' to serialize cannot be null."); + + final Message response = Proton.message(); + + //TODO (conniey): support AMQP sequence and AMQP value. + final AmqpMessageBody body = message.getBody(); + switch (body.getBodyType()) { + case DATA: + response.setBody(new Data(new Binary(body.getFirstData()))); + break; + case VALUE: + case SEQUENCE: + default: + throw LOGGER.logExceptionAsError(new UnsupportedOperationException( + "bodyType [" + body.getBodyType() + "] is not supported yet.")); + } + + // Setting message properties. + final AmqpMessageProperties properties = message.getProperties(); + response.setMessageId(properties.getMessageId()); + response.setContentType(properties.getContentType()); + response.setCorrelationId(properties.getCorrelationId()); + response.setSubject(properties.getSubject()); + + final AmqpAddress replyTo = properties.getReplyTo(); + response.setReplyTo(replyTo != null ? replyTo.toString() : null); + + response.setReplyToGroupId(properties.getReplyToGroupId()); + response.setGroupId(properties.getGroupId()); + response.setContentEncoding(properties.getContentEncoding()); + + if (properties.getGroupSequence() != null) { + response.setGroupSequence(properties.getGroupSequence()); + } + + final AmqpAddress messageTo = properties.getTo(); + if (response.getProperties() == null) { + response.setProperties(new Properties()); + } + + response.getProperties().setTo(messageTo != null ? messageTo.toString() : null); + + response.getProperties().setUserId(new Binary(properties.getUserId())); + + if (properties.getAbsoluteExpiryTime() != null) { + response.getProperties().setAbsoluteExpiryTime( + Date.from(properties.getAbsoluteExpiryTime().toInstant())); + } + + if (properties.getCreationTime() != null) { + response.getProperties().setCreationTime(Date.from(properties.getCreationTime().toInstant())); + } + + // Set header + final AmqpMessageHeader header = message.getHeader(); + if (header.getTimeToLive() != null) { + response.setTtl(header.getTimeToLive().toMillis()); + } + if (header.getDeliveryCount() != null) { + response.setDeliveryCount(header.getDeliveryCount()); + } + if (header.getPriority() != null) { + response.setPriority(header.getPriority()); + } + if (header.isDurable() != null) { + response.setDurable(header.isDurable()); + } + if (header.isFirstAcquirer() != null) { + response.setFirstAcquirer(header.isFirstAcquirer()); + } + if (header.getTimeToLive() != null) { + response.setTtl(header.getTimeToLive().toMillis()); + } + + // Set footer + response.setFooter(new Footer(message.getFooter())); + + // Set message annotations. + final Map messageAnnotations = convert(message.getMessageAnnotations()); + response.setMessageAnnotations(new MessageAnnotations(messageAnnotations)); + + // Set Delivery Annotations. + final Map deliveryAnnotations = convert(message.getDeliveryAnnotations()); + response.setDeliveryAnnotations(new DeliveryAnnotations(deliveryAnnotations)); + + // Set application properties + response.setApplicationProperties(new ApplicationProperties(message.getApplicationProperties())); + + return response; + } + + /** + * Converts a proton-j message to {@link AmqpAnnotatedMessage}. + * + * @param message The message to convert. + * + * @return The corresponding {@link AmqpAnnotatedMessage message}. + * + * @throws NullPointerException if {@code message} is null. + */ + static AmqpAnnotatedMessage toAmqpAnnotatedMessage(Message message) { + Objects.requireNonNull(message, "'message' cannot be null"); + + final byte[] bytes; + final Section body = message.getBody(); + if (body != null) { + //TODO (conniey): Support other AMQP types like AmqpValue and AmqpSequence. + if (body instanceof Data) { + final Binary messageData = ((Data) body).getValue(); + bytes = messageData.getArray(); + } else { + LOGGER.warning("Message not of type Data. Actual: {}", + body.getType()); + bytes = EMPTY_BYTE_ARRAY; + } + } else { + LOGGER.warning("Message does not have a body."); + bytes = EMPTY_BYTE_ARRAY; + } + + final AmqpAnnotatedMessage response = new AmqpAnnotatedMessage(AmqpMessageBody.fromData(bytes)); + + // Application properties + final ApplicationProperties applicationProperties = message.getApplicationProperties(); + if (applicationProperties != null) { + final Map propertiesValue = applicationProperties.getValue(); + response.getApplicationProperties().putAll(propertiesValue); + } + + // Header + final AmqpMessageHeader responseHeader = response.getHeader(); + responseHeader.setTimeToLive(Duration.ofMillis(message.getTtl())); + responseHeader.setDeliveryCount(message.getDeliveryCount()); + responseHeader.setPriority(message.getPriority()); + + if (message.getHeader() != null) { + responseHeader.setDurable(message.getHeader().getDurable()); + responseHeader.setFirstAcquirer(message.getHeader().getFirstAcquirer()); + } + + // Footer + final Footer footer = message.getFooter(); + if (footer != null && footer.getValue() != null) { + @SuppressWarnings("unchecked") final Map footerValue = footer.getValue(); + + setValues(footerValue, response.getFooter()); + } + + // Properties + final AmqpMessageProperties responseProperties = response.getProperties(); + responseProperties.setReplyToGroupId(message.getReplyToGroupId()); + final String replyTo = message.getReplyTo(); + if (replyTo != null) { + responseProperties.setReplyTo(new AmqpAddress(message.getReplyTo())); + } + final Object messageId = message.getMessageId(); + if (messageId != null) { + responseProperties.setMessageId(new AmqpMessageId(messageId.toString())); + } + + responseProperties.setContentType(message.getContentType()); + final Object correlationId = message.getCorrelationId(); + if (correlationId != null) { + responseProperties.setCorrelationId(new AmqpMessageId(correlationId.toString())); + } + + final Properties amqpProperties = message.getProperties(); + if (amqpProperties != null) { + final String to = amqpProperties.getTo(); + if (to != null) { + responseProperties.setTo(new AmqpAddress(amqpProperties.getTo())); + } + + if (amqpProperties.getAbsoluteExpiryTime() != null) { + responseProperties.setAbsoluteExpiryTime(amqpProperties.getAbsoluteExpiryTime().toInstant() + .atOffset(ZoneOffset.UTC)); + } + if (amqpProperties.getCreationTime() != null) { + responseProperties.setCreationTime(amqpProperties.getCreationTime().toInstant() + .atOffset(ZoneOffset.UTC)); + } + } + + responseProperties.setSubject(message.getSubject()); + responseProperties.setGroupId(message.getGroupId()); + responseProperties.setContentEncoding(message.getContentEncoding()); + responseProperties.setGroupSequence(message.getGroupSequence()); + responseProperties.setUserId(message.getUserId()); + + // DeliveryAnnotations + final DeliveryAnnotations deliveryAnnotations = message.getDeliveryAnnotations(); + if (deliveryAnnotations != null) { + setValues(deliveryAnnotations.getValue(), response.getDeliveryAnnotations()); + } + + // Message Annotations + final MessageAnnotations messageAnnotations = message.getMessageAnnotations(); + if (messageAnnotations != null) { + setValues(messageAnnotations.getValue(), response.getMessageAnnotations()); + } + + return response; + } + + /** + * Converts a proton-j delivery state to one supported by azure-core-amqp. + * + * @param deliveryState Delivery state to convert. + * + * @return The corresponding delivery outcome or null if parameter was null. + * + * @throws IllegalArgumentException if {@code deliveryState} type but there is no transactional state associated + * or transaction id. If {@code deliveryState} is declared but there is no transaction id or the type is not + * {@link Declared}. + * @throws UnsupportedOperationException If the {@link DeliveryStateType} is unknown. + */ + static DeliveryOutcome toDeliveryOutcome(org.apache.qpid.proton.amqp.transport.DeliveryState deliveryState) { + if (deliveryState == null) { + return null; + } + + switch (deliveryState.getType()) { + case Accepted: + return new DeliveryOutcome(DeliveryState.ACCEPTED); + case Modified: + if (!(deliveryState instanceof Modified)) { + return new ModifiedDeliveryOutcome(); + } + + return toDeliveryOutcome((Modified) deliveryState); + case Received: + if (!(deliveryState instanceof Received)) { + throw LOGGER.logExceptionAsError(new IllegalArgumentException( + "Received delivery state should have a Received state.")); + } + + final Received received = (Received) deliveryState; + if (received.getSectionNumber() == null || received.getSectionOffset() == null) { + throw LOGGER.logExceptionAsError(new IllegalArgumentException( + "Received delivery state does not have any offset or section number. " + received)); + } + + return new ReceivedDeliveryOutcome(received.getSectionNumber().intValue(), + received.getSectionOffset().longValue()); + case Rejected: + if (!(deliveryState instanceof Rejected)) { + return new DeliveryOutcome(DeliveryState.REJECTED); + } + + return toDeliveryOutcome((Rejected) deliveryState); + case Released: + return new DeliveryOutcome(DeliveryState.RELEASED); + case Declared: + if (!(deliveryState instanceof Declared)) { + throw LOGGER.logExceptionAsError(new IllegalArgumentException( + "Declared delivery type should have a declared outcome")); + } + return toDeliveryOutcome((Declared) deliveryState); + case Transactional: + if (!(deliveryState instanceof TransactionalState)) { + throw LOGGER.logExceptionAsError(new IllegalArgumentException( + "Transactional delivery type should have a TransactionalState outcome.")); + } + + final TransactionalState transactionalState = (TransactionalState) deliveryState; + if (transactionalState.getTxnId() == null) { + throw LOGGER.logExceptionAsError(new IllegalArgumentException( + "Transactional delivery states should have an associated transaction id.")); + } + + final AmqpTransaction transaction = new AmqpTransaction(transactionalState.getTxnId().asByteBuffer()); + final DeliveryOutcome outcome = toDeliveryOutcome(transactionalState.getOutcome()); + return new TransactionalDeliveryOutcome(transaction).setOutcome(outcome); + default: + throw LOGGER.logExceptionAsError(new UnsupportedOperationException( + "Delivery state not supported: " + deliveryState.getType())); + } + } + + /** + * Converts from a proton-j outcome to its corresponding {@link DeliveryOutcome}. + * + * @param outcome Outcome to convert. + * + * @return Corresponding {@link DeliveryOutcome} or null if parameter was null. + * + * @throws UnsupportedOperationException If the type of {@link Outcome} is unknown. + */ + static DeliveryOutcome toDeliveryOutcome(Outcome outcome) { + if (outcome == null) { + return null; + } + + if (outcome instanceof Accepted) { + return new DeliveryOutcome(DeliveryState.ACCEPTED); + } else if (outcome instanceof Modified) { + return toDeliveryOutcome((Modified) outcome); + } else if (outcome instanceof Rejected) { + return toDeliveryOutcome((Rejected) outcome); + } else if (outcome instanceof Released) { + return new DeliveryOutcome(DeliveryState.RELEASED); + } else if (outcome instanceof Declared) { + return toDeliveryOutcome((Declared) outcome); + } else { + throw LOGGER.logExceptionAsError(new UnsupportedOperationException( + "Outcome is not known: " + outcome)); + } + } + + /** + * Converts from a delivery outcome to its corresponding proton-j delivery state. + * + * @param deliveryOutcome Outcome to convert. {@code null} if the outcome is null. + * + * @return Proton-j delivery state. + * + * @throws IllegalArgumentException if deliveryState is {@link DeliveryState#RECEIVED} but its {@code + * deliveryOutcome} is not {@link ReceivedDeliveryOutcome}. If {@code deliveryOutcome} is {@link + * TransactionalDeliveryOutcome} but there is no transaction id. + * @throws UnsupportedOperationException if {@code deliveryState} is unsupported. + */ + static org.apache.qpid.proton.amqp.transport.DeliveryState toProtonJDeliveryState(DeliveryOutcome deliveryOutcome) { + if (deliveryOutcome == null) { + return null; + } + + if (DeliveryState.ACCEPTED.equals(deliveryOutcome.getDeliveryState())) { + return Accepted.getInstance(); + } else if (DeliveryState.REJECTED.equals(deliveryOutcome.getDeliveryState())) { + return toProtonJRejected(deliveryOutcome); + } else if (DeliveryState.RELEASED.equals(deliveryOutcome.getDeliveryState())) { + return Released.getInstance(); + } else if (DeliveryState.MODIFIED.equals(deliveryOutcome.getDeliveryState())) { + return toProtonJModified(deliveryOutcome); + } else if (DeliveryState.RECEIVED.equals(deliveryOutcome.getDeliveryState())) { + if (!(deliveryOutcome instanceof ReceivedDeliveryOutcome)) { + throw LOGGER.logExceptionAsError(new IllegalArgumentException("Received delivery type should be " + + "ReceivedDeliveryOutcome. Actual: " + deliveryOutcome.getClass())); + } + + final ReceivedDeliveryOutcome receivedDeliveryOutcome = (ReceivedDeliveryOutcome) deliveryOutcome; + final Received received = new Received(); + + received.setSectionNumber(UnsignedInteger.valueOf(receivedDeliveryOutcome.getSectionNumber())); + received.setSectionOffset(UnsignedLong.valueOf(receivedDeliveryOutcome.getSectionOffset())); + return received; + } else if (deliveryOutcome instanceof TransactionalDeliveryOutcome) { + final TransactionalDeliveryOutcome transaction = ((TransactionalDeliveryOutcome) deliveryOutcome); + final TransactionalState state = new TransactionalState(); + if (transaction.getTransactionId() == null) { + throw LOGGER.logExceptionAsError(new IllegalArgumentException( + "Transactional deliveries require an id.")); + } + + final Binary binary = Objects.requireNonNull(Binary.create(transaction.getTransactionId()), + "Transaction Ids are required for a transaction."); + + state.setOutcome(toProtonJOutcome(transaction.getOutcome())); + state.setTxnId(binary); + return state; + } else { + throw LOGGER.logExceptionAsError(new UnsupportedOperationException( + "Outcome could not be translated to a proton-j delivery outcome:" + deliveryOutcome.getDeliveryState())); + } + } + + /** + * Converts from delivery outcome to its corresponding proton-j outcome. + * + * @param deliveryOutcome Delivery outcome. + * + * @return Corresponding proton-j outcome. + * + * @throws UnsupportedOperationException when an unsupported delivery state is passed such as {@link + * DeliveryState#RECEIVED}; + */ + static Outcome toProtonJOutcome(DeliveryOutcome deliveryOutcome) { + if (deliveryOutcome == null) { + return null; + } + + if (DeliveryState.ACCEPTED.equals(deliveryOutcome.getDeliveryState())) { + return Accepted.getInstance(); + } else if (DeliveryState.REJECTED.equals(deliveryOutcome.getDeliveryState())) { + return toProtonJRejected(deliveryOutcome); + } else if (DeliveryState.RELEASED.equals(deliveryOutcome.getDeliveryState())) { + return Released.getInstance(); + } else if (DeliveryState.MODIFIED.equals(deliveryOutcome.getDeliveryState())) { + return toProtonJModified(deliveryOutcome); + } else { + throw LOGGER.logExceptionAsError(new UnsupportedOperationException( + "DeliveryOutcome cannot be converted to proton-j outcome: " + deliveryOutcome.getDeliveryState())); + } + } + + private static Modified toProtonJModified(DeliveryOutcome outcome) { + final Modified modified = new Modified(); + + if (!(outcome instanceof ModifiedDeliveryOutcome)) { + return modified; + } + + final ModifiedDeliveryOutcome modifiedDeliveryOutcome = (ModifiedDeliveryOutcome) outcome; + final Map annotations = convert(modifiedDeliveryOutcome.getMessageAnnotations()); + + modified.setMessageAnnotations(annotations); + modified.setUndeliverableHere(modifiedDeliveryOutcome.isUndeliverableHere()); + modified.setDeliveryFailed(modifiedDeliveryOutcome.isDeliveryFailed()); + + return modified; + } + + private static Rejected toProtonJRejected(DeliveryOutcome outcome) { + if (!(outcome instanceof RejectedDeliveryOutcome)) { + return new Rejected(); + } + final Rejected rejected = new Rejected(); + + final RejectedDeliveryOutcome rejectedDeliveryOutcome = (RejectedDeliveryOutcome) outcome; + final AmqpErrorCondition errorCondition = rejectedDeliveryOutcome.getErrorCondition(); + if (errorCondition == null) { + return rejected; + } + + + final ErrorCondition condition = new ErrorCondition( + Symbol.getSymbol(errorCondition.getErrorCondition()), errorCondition.toString()); + + condition.setInfo(convert(rejectedDeliveryOutcome.getErrorInfo())); + + rejected.setError(condition); + return rejected; + } + + private static DeliveryOutcome toDeliveryOutcome(Modified modified) { + final ModifiedDeliveryOutcome modifiedOutcome = new ModifiedDeliveryOutcome(); + + if (modified.getDeliveryFailed() != null) { + modifiedOutcome.setDeliveryFailed(modified.getDeliveryFailed()); + } + + if (modified.getUndeliverableHere() != null) { + modifiedOutcome.setUndeliverableHere(modified.getUndeliverableHere()); + } + + return modifiedOutcome.setMessageAnnotations(convertMap(modified.getMessageAnnotations())); + } + + private static DeliveryOutcome toDeliveryOutcome(Rejected rejected) { + final ErrorCondition rejectedError = rejected.getError(); + + if (rejectedError == null || rejectedError.getCondition() == null) { + return new DeliveryOutcome(DeliveryState.REJECTED); + } + + AmqpErrorCondition errorCondition = + AmqpErrorCondition.fromString(rejectedError.getCondition().toString()); + if (errorCondition == null) { + LOGGER.warning("Error condition is unknown: {}", rejected.getError()); + errorCondition = AmqpErrorCondition.INTERNAL_ERROR; + } + + return new RejectedDeliveryOutcome(errorCondition) + .setErrorInfo(convertMap(rejectedError.getInfo())); + } + + private static DeliveryOutcome toDeliveryOutcome(Declared declared) { + if (declared.getTxnId() == null) { + throw LOGGER.logExceptionAsError(new IllegalArgumentException( + "Declared delivery states should have an associated transaction id.")); + } + + return new TransactionalDeliveryOutcome(new AmqpTransaction(declared.getTxnId().asByteBuffer())); + } + + /** + * Converts from the "raw" map type exposed by proton-j (which is backed by a Symbol, Object to a generic map. + * + * @param map the map to use. + * + * @return A corresponding map. + */ + @SuppressWarnings({"rawtypes", "unchecked"}) + private static Map convertMap(Map map) { + // proton-j only exposes "Map" even though the underlying data structure is this. + final Map outcomeMessageAnnotations = new HashMap<>(); + setValues(map, outcomeMessageAnnotations); + + return outcomeMessageAnnotations; + } + + private static void setValues(Map sourceMap, Map targetMap) { + if (sourceMap == null) { + return; + } + + for (Map.Entry entry : sourceMap.entrySet()) { + targetMap.put(entry.getKey().toString(), entry.getValue()); + } + } + + /** + * Converts a map from it's string keys to use {@link Symbol}. + * + * @param sourceMap Source map. + * + * @return A map with corresponding keys as symbols. + */ + private static Map convert(Map sourceMap) { + if (sourceMap == null) { + return null; + } + + return sourceMap.entrySet().stream() + .collect(HashMap::new, + (existing, entry) -> existing.put(Symbol.valueOf(entry.getKey()), entry.getValue()), + (HashMap::putAll)); + } + + /** + * Private constructor. + */ + private MessageUtils() { + } +} diff --git a/sdk/core/azure-core-amqp/src/main/java/com/azure/core/amqp/implementation/ReactorConnection.java b/sdk/core/azure-core-amqp/src/main/java/com/azure/core/amqp/implementation/ReactorConnection.java index 0c38ed2b5d3cc..4e9bf0b850a16 100644 --- a/sdk/core/azure-core-amqp/src/main/java/com/azure/core/amqp/implementation/ReactorConnection.java +++ b/sdk/core/azure-core-amqp/src/main/java/com/azure/core/amqp/implementation/ReactorConnection.java @@ -5,6 +5,7 @@ import com.azure.core.amqp.AmqpConnection; import com.azure.core.amqp.AmqpEndpointState; +import com.azure.core.amqp.AmqpManagementNode; import com.azure.core.amqp.AmqpRetryOptions; import com.azure.core.amqp.AmqpRetryPolicy; import com.azure.core.amqp.AmqpSession; @@ -39,13 +40,21 @@ import java.util.concurrent.RejectedExecutionException; import java.util.concurrent.atomic.AtomicBoolean; +/** + * An AMQP connection backed by proton-j. + */ public class ReactorConnection implements AmqpConnection { private static final String CBS_SESSION_NAME = "cbs-session"; private static final String CBS_ADDRESS = "$cbs"; private static final String CBS_LINK_NAME = "cbs"; + private static final String MANAGEMENT_SESSION_NAME = "mgmt-session"; + private static final String MANAGEMENT_ADDRESS = "$management"; + private static final String MANAGEMENT_LINK_NAME = "mgmt"; + private final ClientLogger logger = new ClientLogger(ReactorConnection.class); private final ConcurrentMap sessionMap = new ConcurrentHashMap<>(); + private final ConcurrentHashMap managementNodes = new ConcurrentHashMap<>(); private final AtomicBoolean isDisposed = new AtomicBoolean(); private final Sinks.One shutdownSignalSink = Sinks.one(); @@ -172,6 +181,48 @@ public Flux getShutdownSignals() { return shutdownSignalSink.asMono().cache().flux(); } + @Override + public Mono getManagementNode(String entityPath) { + return Mono.defer(() -> { + if (isDisposed()) { + return Mono.error(logger.logExceptionAsError(new IllegalStateException(String.format( + "connectionId[%s]: Connection is disposed. Cannot get management instance for '%s'", + connectionId, entityPath)))); + } + + final AmqpManagementNode existing = managementNodes.get(entityPath); + if (existing != null) { + return Mono.just(existing); + } + + final TokenManager tokenManager = new AzureTokenManagerProvider(connectionOptions.getAuthorizationType(), + connectionOptions.getFullyQualifiedNamespace(), connectionOptions.getAuthorizationScope()) + .getTokenManager(getClaimsBasedSecurityNode(), entityPath); + + return tokenManager.authorize().thenReturn(managementNodes.compute(entityPath, (key, current) -> { + if (current != null) { + logger.info("A management node exists already, returning it."); + + // Close the token manager we had created during this because it is unneeded now. + tokenManager.close(); + return current; + } + + final String sessionName = entityPath + "-" + MANAGEMENT_SESSION_NAME; + final String linkName = entityPath + "-" + MANAGEMENT_LINK_NAME; + final String address = entityPath + "/" + MANAGEMENT_ADDRESS; + + logger.info("Creating management node. entityPath[{}], address[{}], linkName[{}]", + entityPath, address, linkName); + + final AmqpChannelProcessor requestResponseChannel = + createRequestResponseChannel(sessionName, linkName, address); + return new ManagementChannel(requestResponseChannel, getFullyQualifiedNamespace(), entityPath, + tokenManager); + })); + }); + } + /** * {@inheritDoc} */ @@ -302,17 +353,10 @@ public boolean isDisposed() { */ @Override public void dispose() { - if (isDisposed.getAndSet(true)) { - logger.verbose("connectionId[{}] Was already closed. Not disposing again.", connectionId); - return; - } - // Because the reactor executor schedules the pending close after the timeout, we want to give sufficient time // for the rest of the tasks to run. final Duration timeout = operationTimeout.plus(operationTimeout); - closeAsync(new AmqpShutdownSignal(false, true, "Disposed by client.")) - .publishOn(Schedulers.boundedElastic()) - .block(timeout); + closeAsync().block(timeout); } /** @@ -356,20 +400,37 @@ protected AmqpChannelProcessor createRequestResponseChan new ClientLogger(RequestResponseChannel.class + ":" + entityPath))); } + @Override + public Mono closeAsync() { + if (isDisposed.getAndSet(true)) { + logger.verbose("connectionId[{}] Was already closed. Not disposing again.", connectionId); + return isClosedMono.asMono(); + } + + return closeAsync(new AmqpShutdownSignal(false, true, + "Disposed by client.")); + } + Mono closeAsync(AmqpShutdownSignal shutdownSignal) { logger.info("connectionId[{}] signal[{}]: Disposing of ReactorConnection.", connectionId, shutdownSignal); - if (cbsChannelProcessor != null) { - cbsChannelProcessor.dispose(); - } - final Sinks.EmitResult result = shutdownSignalSink.tryEmitValue(shutdownSignal); if (result.isFailure()) { // It's possible that another one was already emitted, so it's all good. logger.info("connectionId[{}] signal[{}] result[{}] Unable to emit shutdown signal.", connectionId, result); } - return Mono.fromRunnable(() -> { + final Mono cbsCloseOperation; + if (cbsChannelProcessor != null) { + cbsCloseOperation = cbsChannelProcessor.flatMap(channel -> channel.closeAsync()); + } else { + cbsCloseOperation = Mono.empty(); + } + + final Mono managementNodeCloseOperations = Mono.when( + Flux.fromStream(managementNodes.values().stream()).flatMap(node -> node.closeAsync())); + + final Mono closeReactor = Mono.fromRunnable(() -> { final ReactorDispatcher dispatcher = reactorProvider.getReactorDispatcher(); try { @@ -383,7 +444,11 @@ Mono closeAsync(AmqpShutdownSignal shutdownSignal) { connectionId, e); closeConnectionWork(); } - }).then(isClosedMono.asMono()); + }); + + return Mono.whenDelayError(cbsCloseOperation, managementNodeCloseOperations) + .then(closeReactor) + .then(isClosedMono.asMono()); } private synchronized void closeConnectionWork() { diff --git a/sdk/core/azure-core-amqp/src/main/java/com/azure/core/amqp/implementation/ReactorSession.java b/sdk/core/azure-core-amqp/src/main/java/com/azure/core/amqp/implementation/ReactorSession.java index 5d58e2bd75922..e0274589a9f8c 100644 --- a/sdk/core/azure-core-amqp/src/main/java/com/azure/core/amqp/implementation/ReactorSession.java +++ b/sdk/core/azure-core-amqp/src/main/java/com/azure/core/amqp/implementation/ReactorSession.java @@ -152,8 +152,7 @@ public boolean isDisposed() { */ @Override public void dispose() { - closeAsync("Dispose called.", null, true) - .block(retryOptions.getTryTimeout()); + closeAsync().block(retryOptions.getTryTimeout()); } /** @@ -240,6 +239,11 @@ Mono isClosed() { return isClosedMono.asMono(); } + @Override + public Mono closeAsync() { + return closeAsync(null, null, true); + } + Mono closeAsync(String message, ErrorCondition errorCondition, boolean disposeLinks) { if (isDisposed.getAndSet(true)) { return isClosedMono.asMono(); @@ -248,7 +252,7 @@ Mono closeAsync(String message, ErrorCondition errorCondition, boolean dis final String condition = errorCondition != null ? errorCondition.toString() : NOT_APPLICABLE; logger.verbose("connectionId[{}], sessionName[{}], errorCondition[{}]. Setting error condition and " + "disposing session. {}", - sessionHandler.getConnectionId(), sessionName, condition, message); + sessionHandler.getConnectionId(), sessionName, condition, message != null ? message : ""); return Mono.fromRunnable(() -> { try { @@ -596,7 +600,7 @@ private void handleClose() { "connectionId[{}] sessionName[{}] Disposing of active send and receive links due to session close.", sessionHandler.getConnectionId(), sessionName); - closeAsync("", null, true).subscribe(); + closeAsync().subscribe(); } private void handleError(Throwable error) { diff --git a/sdk/core/azure-core-amqp/src/main/java/com/azure/core/amqp/implementation/RequestResponseChannel.java b/sdk/core/azure-core-amqp/src/main/java/com/azure/core/amqp/implementation/RequestResponseChannel.java index f7bed8c8242d8..cc1d55827689f 100644 --- a/sdk/core/azure-core-amqp/src/main/java/com/azure/core/amqp/implementation/RequestResponseChannel.java +++ b/sdk/core/azure-core-amqp/src/main/java/com/azure/core/amqp/implementation/RequestResponseChannel.java @@ -156,7 +156,7 @@ protected RequestResponseChannel(AmqpConnection amqpConnection, String connectio handleError(error, "Error in ReceiveLinkHandler."); onTerminalState("ReceiveLinkHandler"); }, () -> { - closeAsync("ReceiveLinkHandler. Endpoint states complete.").subscribe(); + closeAsync().subscribe(); onTerminalState("ReceiveLinkHandler"); }), @@ -166,13 +166,13 @@ protected RequestResponseChannel(AmqpConnection amqpConnection, String connectio handleError(error, "Error in SendLinkHandler."); onTerminalState("SendLinkHandler"); }, () -> { - closeAsync("SendLinkHandler. Endpoint states complete.").subscribe(); + closeAsync().subscribe(); onTerminalState("SendLinkHandler"); }), amqpConnection.getShutdownSignals().next().flatMap(signal -> { logger.verbose("connectionId[{}] linkName[{}]: Shutdown signal received.", connectionId, linkName); - return closeAsync(" Shutdown signal received."); + return closeAsync(); }).subscribe() ); //@formatter:on @@ -201,17 +201,13 @@ public Flux getEndpointStates() { @Override public Mono closeAsync() { - return this.closeAsync(""); - } - - public Mono closeAsync(String message) { if (isDisposed.getAndSet(true)) { return closeMono.asMono().subscribeOn(Schedulers.boundedElastic()); } - return Mono.fromRunnable(() -> { - logger.verbose("connectionId[{}] linkName[{}] {}", connectionId, linkName, message); + logger.verbose("connectionId[{}] linkName[{}] Closing request/response channel.", connectionId, linkName); + return Mono.fromRunnable(() -> { try { provider.getReactorDispatcher().invoke(() -> { sendLink.close(); @@ -365,7 +361,7 @@ private void handleError(Throwable error, String message) { unconfirmedSends.forEach((key, value) -> value.error(error)); unconfirmedSends.clear(); - closeAsync("Disposing channel due to error.").subscribe(); + closeAsync().subscribe(); } private void onTerminalState(String handlerName) { diff --git a/sdk/core/azure-core-amqp/src/main/java/com/azure/core/amqp/models/DeliveryOutcome.java b/sdk/core/azure-core-amqp/src/main/java/com/azure/core/amqp/models/DeliveryOutcome.java new file mode 100644 index 0000000000000..ae90b87f69def --- /dev/null +++ b/sdk/core/azure-core-amqp/src/main/java/com/azure/core/amqp/models/DeliveryOutcome.java @@ -0,0 +1,42 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.azure.core.amqp.models; + +import com.azure.core.annotation.Fluent; + +/** + * Outcomes accepted by the AMQP protocol layer. Some outcomes have metadata associated with them, such as {@link + * ModifiedDeliveryOutcome Modified} while others require only a {@link DeliveryState}. An outcome with no metadata is + * {@link DeliveryState#ACCEPTED}. + * + * @see Delivery + * State: Accepted + * @see Delivery + * State: Released + * @see ModifiedDeliveryOutcome + * @see RejectedDeliveryOutcome + * @see TransactionalDeliveryOutcome + */ +@Fluent +public class DeliveryOutcome { + private final DeliveryState deliveryState; + + /** + * Creates an instance of the delivery outcome with its state. + * + * @param deliveryState The state of the delivery. + */ + public DeliveryOutcome(DeliveryState deliveryState) { + this.deliveryState = deliveryState; + } + + /** + * Gets the delivery state. + * + * @return The delivery state. + */ + public DeliveryState getDeliveryState() { + return deliveryState; + } +} diff --git a/sdk/core/azure-core-amqp/src/main/java/com/azure/core/amqp/models/DeliveryState.java b/sdk/core/azure-core-amqp/src/main/java/com/azure/core/amqp/models/DeliveryState.java new file mode 100644 index 0000000000000..8b2933701eeb2 --- /dev/null +++ b/sdk/core/azure-core-amqp/src/main/java/com/azure/core/amqp/models/DeliveryState.java @@ -0,0 +1,63 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.azure.core.amqp.models; + +import com.azure.core.util.ExpandableStringEnum; + +import java.util.Collection; + +/** + * States for a message delivery. + * + * @see Delivery + * state + * @see Transactional + * work + */ +public final class DeliveryState extends ExpandableStringEnum { + /** + * Indicates successful processing at the receiver. + */ + public static final DeliveryState ACCEPTED = fromString("ACCEPTED", DeliveryState.class); + /** + * Indicates an invalid and unprocessable message. + */ + public static final DeliveryState REJECTED = fromString("REJECTED", DeliveryState.class); + /** + * Indicates that the message was not (and will not be) processed. + */ + public static final DeliveryState RELEASED = fromString("RELEASED", DeliveryState.class); + /** + * indicates that the message was modified, but not processed. + */ + public static final DeliveryState MODIFIED = fromString("MODIFIED", DeliveryState.class); + /** + * indicates partial message data seen by the receiver as well as the starting point for a resumed transfer. + */ + public static final DeliveryState RECEIVED = fromString("RECEIVED", DeliveryState.class); + /** + * Indicates that this delivery is part of a transaction. + */ + public static final DeliveryState TRANSACTIONAL = fromString("TRANSACTIONAL", DeliveryState.class); + + /** + * Gets the corresponding delivery state from its string representation. + * + * @param name The delivery state to convert. + * + * @return The corresponding delivery state. + */ + public static DeliveryState fromString(String name) { + return fromString(name, DeliveryState.class); + } + + /** + * Gets all the current delivery states. + * + * @return Gets the current delivery states. + */ + public static Collection values() { + return values(DeliveryState.class); + } +} diff --git a/sdk/core/azure-core-amqp/src/main/java/com/azure/core/amqp/models/ModifiedDeliveryOutcome.java b/sdk/core/azure-core-amqp/src/main/java/com/azure/core/amqp/models/ModifiedDeliveryOutcome.java new file mode 100644 index 0000000000000..d870f78afb179 --- /dev/null +++ b/sdk/core/azure-core-amqp/src/main/java/com/azure/core/amqp/models/ModifiedDeliveryOutcome.java @@ -0,0 +1,109 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.azure.core.amqp.models; + +import com.azure.core.annotation.Fluent; + +import java.util.Map; + +/** + * The modified outcome. + *

+ * At the source the modified outcome means that the message is no longer acquired by the receiver, and has been made + * available for (re-)delivery to the same or other targets receiving from the node. The message has been changed at the + * node in the ways indicated by the fields of the outcome. As modified is a terminal outcome, transfer of payload data + * will not be able to be resumed if the link becomes suspended. A delivery can become modified at the source even + * before all transfer frames have been sent. This does not imply that the remaining transfers for the delivery will not + * be sent. The source MAY spontaneously attain the modified outcome for a message (for example the source might + * implement some sort of time-bound acquisition lock, after which the acquisition of a message at a node is revoked to + * allow for delivery to an alternative consumer with the message modified in some way to denote the previous failed, + * e.g., with delivery-failed set to true). + *

+ *

+ * At the target, the modified outcome is used to indicate that a given transfer was not and will not be acted upon, and + * that the message SHOULD be modified in the specified ways at the node. + *

+ * + * @see Modified + * outcome + */ +@Fluent +public final class ModifiedDeliveryOutcome extends DeliveryOutcome { + private Map messageAnnotations; + private Boolean isUndeliverableHere; + private Boolean isDeliveryFailed; + + /** + * Creates an instance with the delivery state modified set. + */ + public ModifiedDeliveryOutcome() { + super(DeliveryState.MODIFIED); + } + + /** + * Gets whether or not the message is undeliverable here. + * + * @return {@code true} to not redeliver message. + */ + public Boolean isUndeliverableHere() { + return this.isUndeliverableHere; + } + + /** + * Sets whether or not the message is undeliverable here. + * + * @param isUndeliverable If the message is undeliverable here. + * + * @return The updated {@link ModifiedDeliveryOutcome} outcome. + */ + public ModifiedDeliveryOutcome setUndeliverableHere(boolean isUndeliverable) { + this.isUndeliverableHere = isUndeliverable; + return this; + } + + /** + * Gets whether or not to count the transfer as an unsuccessful delivery attempt. + * + * @return {@code true} to increment the delivery count. + */ + public Boolean isDeliveryFailed() { + return isDeliveryFailed; + } + + /** + * Sets whether or not to count the transfer as an unsuccessful delivery attempt. + * + * @param isDeliveryFailed {@code true} to count the transfer as an unsuccessful delivery attempt. + * + * @return The updated {@link ModifiedDeliveryOutcome} outcome. + */ + public ModifiedDeliveryOutcome setDeliveryFailed(boolean isDeliveryFailed) { + this.isDeliveryFailed = isDeliveryFailed; + return this; + } + + /** + * Gets a map containing attributes to combine with the existing message-annotations held in the message's header + * section. Where the existing message-annotations of the message contain an entry with the same key as an entry in + * this field, the value in this field associated with that key replaces the one in the existing headers; where the + * existing message-annotations has no such value, the value in this map is added. + * + * @return Map containing attributes to combine with existing message annotations on the message. + */ + public Map getMessageAnnotations() { + return messageAnnotations; + } + + /** + * Sets the message annotations to add to the message. + * + * @param messageAnnotations the message annotations to add to the message. + * + * @return The updated {@link ModifiedDeliveryOutcome} object. + */ + public ModifiedDeliveryOutcome setMessageAnnotations(Map messageAnnotations) { + this.messageAnnotations = messageAnnotations; + return this; + } +} diff --git a/sdk/core/azure-core-amqp/src/main/java/com/azure/core/amqp/models/ReceivedDeliveryOutcome.java b/sdk/core/azure-core-amqp/src/main/java/com/azure/core/amqp/models/ReceivedDeliveryOutcome.java new file mode 100644 index 0000000000000..2c4cc73a7990c --- /dev/null +++ b/sdk/core/azure-core-amqp/src/main/java/com/azure/core/amqp/models/ReceivedDeliveryOutcome.java @@ -0,0 +1,64 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.azure.core.amqp.models; + +/** + * Represents a partial message that was received. + * + * @see DeliveryState + * @see Received + * outcome + */ +public final class ReceivedDeliveryOutcome extends DeliveryOutcome { + private final int sectionNumber; + private final long sectionOffset; + + /** + * Creates an instance of the delivery outcome with its state. + * + * @param sectionNumber Section number within the message that can be resent or may not have been received. + * @param sectionOffset First byte of the section where data can be resent, or first byte of the section where + * it may not have been received. + */ + public ReceivedDeliveryOutcome(int sectionNumber, long sectionOffset) { + super(DeliveryState.RECEIVED); + this.sectionNumber = sectionNumber; + this.sectionOffset = sectionOffset; + } + + /** + * Gets the section number. + *

+ * When sent by the sender this indicates the first section of the message (with section-number 0 being the first + * section) for which data can be resent. Data from sections prior to the given section cannot be retransmitted for + * this delivery. + *

+ * When sent by the receiver this indicates the first section of the message for which all data might not yet have + * been received. + * + * @return Gets the section number of this outcome. + */ + public int getSectionNumber() { + return sectionNumber; + } + + /** + * Gets the section offset. + *

+ * When sent by the sender this indicates the first byte of the encoded section data of the section given by + * section-number for which data can be resent (with section-offset 0 being the first byte). Bytes from the same + * section prior to the given offset section cannot be retransmitted for this delivery. + *

+ * When sent by the receiver this indicates the first byte of the given section which has not yet been received. + * Note that if a receiver has received all of section number X (which contains N bytes of data), but none of + * section number X + 1, then it can indicate this by sending either Received(section-number=X, section-offset=N) or + * Received(section-number=X+1, section-offset=0). The state Received(section-number=0, section-offset=0) indicates + * that no message data at all has been transferred. + * + * @return The section offset. + */ + public long getSectionOffset() { + return sectionOffset; + } +} diff --git a/sdk/core/azure-core-amqp/src/main/java/com/azure/core/amqp/models/RejectedDeliveryOutcome.java b/sdk/core/azure-core-amqp/src/main/java/com/azure/core/amqp/models/RejectedDeliveryOutcome.java new file mode 100644 index 0000000000000..d084645d19b30 --- /dev/null +++ b/sdk/core/azure-core-amqp/src/main/java/com/azure/core/amqp/models/RejectedDeliveryOutcome.java @@ -0,0 +1,81 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.azure.core.amqp.models; + +import com.azure.core.amqp.exception.AmqpErrorCondition; +import com.azure.core.annotation.Fluent; + +import java.util.Map; +import java.util.Objects; + +/** + * The rejected delivery outcome. + *

+ * At the target, the rejected outcome is used to indicate that an incoming message is invalid and therefore + * unprocessable. The rejected outcome when applied to a message will cause the delivery-count to be incremented in the + * header of the rejected message. + *

+ *

+ * At the source, the rejected outcome means that the target has informed the source that the message was rejected, and + * the source has taken the necessary action. The delivery SHOULD NOT ever spontaneously attain the rejected state at + * the source. + *

+ * + * @see Rejected + * outcome + */ +@Fluent +public final class RejectedDeliveryOutcome extends DeliveryOutcome { + private final AmqpErrorCondition errorCondition; + private Map errorInfo; + + /** + * Creates an instance with the given error condition. + * + * @param errorCondition The error condition. + */ + public RejectedDeliveryOutcome(AmqpErrorCondition errorCondition) { + super(DeliveryState.REJECTED); + this.errorCondition = Objects.requireNonNull(errorCondition, "'errorCondition' cannot be null."); + } + + /** + * Diagnostic information about the cause of the message rejection. + * + * @return Diagnostic information about the cause of the message rejection. + */ + public AmqpErrorCondition getErrorCondition() { + return errorCondition; + } + + /** + * Gets the error description. + * + * @return Gets the error condition. + */ + public String getErrorDescription() { + return errorCondition.getErrorCondition(); + } + + /** + * Gets a map of additional error information. + * + * @return Map of additional error information. + */ + public Map getErrorInfo() { + return errorInfo; + } + + /** + * Sets a map with additional error information. + * + * @param errorInfo Error information associated with the rejection. + * + * @return The updated {@link RejectedDeliveryOutcome} object. + */ + public RejectedDeliveryOutcome setErrorInfo(Map errorInfo) { + this.errorInfo = errorInfo; + return this; + } +} diff --git a/sdk/core/azure-core-amqp/src/main/java/com/azure/core/amqp/models/TransactionalDeliveryOutcome.java b/sdk/core/azure-core-amqp/src/main/java/com/azure/core/amqp/models/TransactionalDeliveryOutcome.java new file mode 100644 index 0000000000000..9d342fcc5e755 --- /dev/null +++ b/sdk/core/azure-core-amqp/src/main/java/com/azure/core/amqp/models/TransactionalDeliveryOutcome.java @@ -0,0 +1,73 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.azure.core.amqp.models; + +import com.azure.core.amqp.AmqpTransaction; +import com.azure.core.annotation.Fluent; +import com.azure.core.util.logging.ClientLogger; + +import java.nio.ByteBuffer; +import java.util.Objects; + +/** + * A transaction delivery outcome. + * + * @see Transactional + * state + */ +@Fluent +public final class TransactionalDeliveryOutcome extends DeliveryOutcome { + private final AmqpTransaction amqpTransaction; + private final ClientLogger logger = new ClientLogger(TransactionalDeliveryOutcome.class); + private DeliveryOutcome outcome; + + /** + * Creates an outcome with the given transaction. + * + * @param transaction The transaction. + * @throws NullPointerException if {@code transaction} is {@code null}. + */ + public TransactionalDeliveryOutcome(AmqpTransaction transaction) { + super(DeliveryState.TRANSACTIONAL); + this.amqpTransaction = Objects.requireNonNull(transaction, "'transaction' cannot be null."); + } + + /** + * Gets the transaction id associated with this delivery outcome. + * + * @return The transaction id. + */ + public ByteBuffer getTransactionId() { + return amqpTransaction.getTransactionId(); + } + + /** + * Gets the delivery outcome associated with this transaction. + * + * @return the delivery outcome associated with this transaction, {@code null} if there is no outcome. + */ + public DeliveryOutcome getOutcome() { + return outcome; + } + + /** + * Sets the outcome associated with this delivery state. + * + * @param outcome Outcome associated with this transaction delivery. + * + * @return The updated {@link TransactionalDeliveryOutcome} object. + * + * @throws IllegalArgumentException if {@code outcome} is an instance of {@link TransactionalDeliveryOutcome}. + * Cannot have nested transaction outcomes. + */ + public TransactionalDeliveryOutcome setOutcome(DeliveryOutcome outcome) { + if (outcome instanceof TransactionalDeliveryOutcome) { + throw logger.logExceptionAsError( + new IllegalArgumentException("Cannot set the outcome as another nested transaction outcome.")); + } + + this.outcome = outcome; + return this; + } +} diff --git a/sdk/core/azure-core-amqp/src/test/java/com/azure/core/amqp/implementation/ClaimsBasedSecurityChannelTest.java b/sdk/core/azure-core-amqp/src/test/java/com/azure/core/amqp/implementation/ClaimsBasedSecurityChannelTest.java index 436f8faadce02..e4972f1421aec 100644 --- a/sdk/core/azure-core-amqp/src/test/java/com/azure/core/amqp/implementation/ClaimsBasedSecurityChannelTest.java +++ b/sdk/core/azure-core-amqp/src/test/java/com/azure/core/amqp/implementation/ClaimsBasedSecurityChannelTest.java @@ -215,4 +215,42 @@ void errorsWhenNoResponse() { }) .verify(); } + + /** + * Verifies that it closes the CBS node asynchronously. + */ + @Test + void closesAsync() { + // Arrange + final ClaimsBasedSecurityChannel cbsChannel = new ClaimsBasedSecurityChannel( + Mono.defer(() -> Mono.just(requestResponseChannel)), tokenCredential, + CbsAuthorizationType.SHARED_ACCESS_SIGNATURE, options); + + when(requestResponseChannel.closeAsync()).thenReturn(Mono.empty()); + + // Act & Assert + StepVerifier.create(cbsChannel.closeAsync()) + .expectComplete() + .verify(); + + verify(requestResponseChannel).closeAsync(); + } + + /** + * Verifies that it closes the cbs node synchronously. + */ + @Test + void closes() { + // Arrange + final ClaimsBasedSecurityChannel cbsChannel = new ClaimsBasedSecurityChannel( + Mono.defer(() -> Mono.just(requestResponseChannel)), tokenCredential, + CbsAuthorizationType.SHARED_ACCESS_SIGNATURE, options); + + when(requestResponseChannel.closeAsync()).thenReturn(Mono.empty()); + + // Act & Assert + cbsChannel.close(); + + verify(requestResponseChannel).closeAsync(); + } } diff --git a/sdk/core/azure-core-amqp/src/test/java/com/azure/core/amqp/implementation/ConnectionOptionsTest.java b/sdk/core/azure-core-amqp/src/test/java/com/azure/core/amqp/implementation/ConnectionOptionsTest.java index a6426dfa83aa5..35aaaea59ffa9 100644 --- a/sdk/core/azure-core-amqp/src/test/java/com/azure/core/amqp/implementation/ConnectionOptionsTest.java +++ b/sdk/core/azure-core-amqp/src/test/java/com/azure/core/amqp/implementation/ConnectionOptionsTest.java @@ -48,6 +48,7 @@ public void propertiesSet() { // Arrange final String productName = "test-product"; final String clientVersion = "1.5.10"; + final String scope = "test-scope"; final String hostname = "host-name.com"; final SslDomain.VerifyMode verifyMode = SslDomain.VerifyMode.VERIFY_PEER; @@ -56,8 +57,8 @@ public void propertiesSet() { // Act final ConnectionOptions actual = new ConnectionOptions(hostname, tokenCredential, - CbsAuthorizationType.JSON_WEB_TOKEN, AmqpTransportType.AMQP, retryOptions, ProxyOptions.SYSTEM_DEFAULTS, - scheduler, clientOptions, verifyMode, productName, clientVersion); + CbsAuthorizationType.JSON_WEB_TOKEN, scope, AmqpTransportType.AMQP, retryOptions, + ProxyOptions.SYSTEM_DEFAULTS, scheduler, clientOptions, verifyMode, productName, clientVersion); // Assert assertEquals(hostname, actual.getHostname()); @@ -72,6 +73,7 @@ public void propertiesSet() { assertEquals(tokenCredential, actual.getTokenCredential()); assertEquals(CbsAuthorizationType.JSON_WEB_TOKEN, actual.getAuthorizationType()); + assertEquals(scope, actual.getAuthorizationScope()); assertEquals(retryOptions, actual.getRetry()); assertEquals(verifyMode, actual.getSslVerifyMode()); } diff --git a/sdk/core/azure-core-amqp/src/test/java/com/azure/core/amqp/implementation/ManagementChannelTest.java b/sdk/core/azure-core-amqp/src/test/java/com/azure/core/amqp/implementation/ManagementChannelTest.java new file mode 100644 index 0000000000000..d5270c765886c --- /dev/null +++ b/sdk/core/azure-core-amqp/src/test/java/com/azure/core/amqp/implementation/ManagementChannelTest.java @@ -0,0 +1,352 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.azure.core.amqp.implementation; + +import com.azure.core.amqp.AmqpRetryPolicy; +import com.azure.core.amqp.exception.AmqpErrorCondition; +import com.azure.core.amqp.exception.AmqpErrorContext; +import com.azure.core.amqp.exception.AmqpException; +import com.azure.core.amqp.exception.AmqpResponseCode; +import com.azure.core.amqp.models.AmqpAnnotatedMessage; +import com.azure.core.amqp.models.AmqpMessageBody; +import com.azure.core.amqp.models.AmqpMessageBodyType; +import com.azure.core.amqp.models.DeliveryOutcome; +import com.azure.core.amqp.models.DeliveryState; +import com.azure.core.amqp.models.ModifiedDeliveryOutcome; +import com.azure.core.util.logging.ClientLogger; +import org.apache.qpid.proton.Proton; +import org.apache.qpid.proton.amqp.Binary; +import org.apache.qpid.proton.amqp.Symbol; +import org.apache.qpid.proton.amqp.messaging.Accepted; +import org.apache.qpid.proton.amqp.messaging.ApplicationProperties; +import org.apache.qpid.proton.amqp.messaging.Data; +import org.apache.qpid.proton.amqp.messaging.Modified; +import org.apache.qpid.proton.message.Message; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestInfo; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.MethodSource; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.MockitoAnnotations; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; +import reactor.test.StepVerifier; +import reactor.test.publisher.TestPublisher; + +import java.nio.ByteBuffer; +import java.nio.charset.StandardCharsets; +import java.time.Duration; +import java.util.HashMap; +import java.util.Map; +import java.util.stream.Stream; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.argThat; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +/** + * Tests for {@link ManagementChannel}. + */ +public class ManagementChannelTest { + private static final String STATUS_CODE_KEY = "status-code"; + private static final String STATUS_DESCRIPTION_KEY = "status-description"; + private static final String ERROR_CONDITION_KEY = "errorCondition"; + + private static final String NAMESPACE = "my-namespace-foo.net"; + private static final String ENTITY_PATH = "queue-name"; + + private final ClientLogger logger = new ClientLogger(ManagementChannelTest.class); + + // Mocked response values from the RequestResponseChannel. + private final Map applicationProperties = new HashMap<>(); + private final Message responseMessage = Proton.message(); + private final TestPublisher tokenProviderResults = TestPublisher.createCold(); + private final AmqpErrorContext errorContext = new AmqpErrorContext("Foo-bar"); + private final AmqpMessageBody messageBody = AmqpMessageBody.fromData("test-body".getBytes(StandardCharsets.UTF_8)); + private final AmqpAnnotatedMessage annotatedMessage = new AmqpAnnotatedMessage(messageBody); + + private ManagementChannel managementChannel; + private AutoCloseable autoCloseable; + + @Mock + private TokenManager tokenManager; + @Mock + private RequestResponseChannel requestResponseChannel; + @Mock + private AmqpRetryPolicy retryPolicy; + + @BeforeAll + public static void beforeAll() { + StepVerifier.setDefaultTimeout(Duration.ofSeconds(10)); + } + + @AfterAll + public static void afterAll() { + StepVerifier.resetDefaultTimeout(); + } + + @BeforeEach + public void setup(TestInfo testInfo) { + logger.info("[{}] Setting up.", testInfo.getDisplayName()); + + autoCloseable = MockitoAnnotations.openMocks(this); + + final AmqpChannelProcessor requestResponseMono = + Mono.defer(() -> Mono.just(requestResponseChannel)).subscribeWith(new AmqpChannelProcessor<>( + "foo", "bar", RequestResponseChannel::getEndpointStates, + retryPolicy, logger)); + + when(tokenManager.authorize()).thenReturn(Mono.just(1000L)); + when(tokenManager.getAuthorizationResults()).thenReturn(tokenProviderResults.flux()); + + when(requestResponseChannel.getErrorContext()).thenReturn(errorContext); + when(requestResponseChannel.getEndpointStates()).thenReturn(Flux.never()); + + managementChannel = new ManagementChannel(requestResponseMono, NAMESPACE, ENTITY_PATH, tokenManager); + } + + @AfterEach + public void teardown(TestInfo testInfo) throws Exception { + logger.info("[{}] Tearing down.", testInfo.getDisplayName()); + if (autoCloseable != null) { + autoCloseable.close(); + } + + Mockito.framework().clearInlineMocks(); + } + + /** + * When an empty response is returned, an error is returned. + */ + @Test + public void sendMessageEmptyResponseErrors() { + // Arrange + when(requestResponseChannel.getErrorContext()).thenReturn(errorContext); + when(requestResponseChannel.sendWithAck(any(Message.class))).thenReturn(Mono.empty()); + + // Act & Assert + StepVerifier.create(managementChannel.send(annotatedMessage)) + .then(() -> tokenProviderResults.next(AmqpResponseCode.OK)) + .expectErrorSatisfies(error -> { + assertTrue(error instanceof AmqpException); + assertEquals(errorContext, ((AmqpException) error).getContext()); + assertTrue(((AmqpException) error).isTransient()); + }) + .verify(); + } + + /** + * Sends a message with success and asserts the response. + */ + @MethodSource("successfulResponseCodes") + @ParameterizedTest + public void sendMessage(AmqpResponseCode responseCode) { + // Arrange + when(requestResponseChannel.getErrorContext()).thenReturn(errorContext); + when(requestResponseChannel.sendWithAck(any(Message.class))).thenReturn(Mono.just(responseMessage)); + + applicationProperties.put(STATUS_CODE_KEY, responseCode.getValue()); + responseMessage.setApplicationProperties(new ApplicationProperties(applicationProperties)); + + final byte[] body = "foo".getBytes(StandardCharsets.UTF_8); + final Data dataBody = new Data(Binary.create(ByteBuffer.wrap(body))); + responseMessage.setBody(dataBody); + + // Act & Assert + StepVerifier.create(managementChannel.send(annotatedMessage)) + .then(() -> tokenProviderResults.next(AmqpResponseCode.OK)) + .assertNext(actual -> { + assertNotNull(actual.getApplicationProperties()); + assertEquals(responseCode.getValue(), actual.getApplicationProperties().get(STATUS_CODE_KEY)); + + assertEquals(AmqpMessageBodyType.DATA, actual.getBody().getBodyType()); + assertEquals(body, actual.getBody().getFirstData()); + }) + .expectComplete() + .verify(); + } + + /** + * Sends a message and a delivery outcome with success and asserts the response. + */ + @MethodSource("successfulResponseCodes") + @ParameterizedTest + public void sendMessageWithOutcome(AmqpResponseCode responseCode) { + // Arrange + final ModifiedDeliveryOutcome outcome = new ModifiedDeliveryOutcome(); + when(requestResponseChannel.getErrorContext()).thenReturn(errorContext); + when(requestResponseChannel.sendWithAck(any(Message.class), argThat(p -> p instanceof Modified))) + .thenReturn(Mono.just(responseMessage)); + + applicationProperties.put(STATUS_CODE_KEY, responseCode.getValue()); + responseMessage.setApplicationProperties(new ApplicationProperties(applicationProperties)); + + final byte[] body = "foo-bar".getBytes(StandardCharsets.UTF_8); + final Data dataBody = new Data(Binary.create(ByteBuffer.wrap(body))); + responseMessage.setBody(dataBody); + + // Act & Assert + StepVerifier.create(managementChannel.send(annotatedMessage, outcome)) + .then(() -> tokenProviderResults.next(AmqpResponseCode.OK)) + .assertNext(actual -> { + assertNotNull(actual.getApplicationProperties()); + assertEquals(responseCode.getValue(), actual.getApplicationProperties().get(STATUS_CODE_KEY)); + + assertEquals(AmqpMessageBodyType.DATA, actual.getBody().getBodyType()); + assertEquals(body, actual.getBody().getFirstData()); + }) + .expectComplete() + .verify(); + } + + /** + * When an empty response is returned for sending a message with deliveryOutcome, an error is returned. + */ + @Test + public void sendMessageDeliveryOutcomeEmptyResponseErrors() { + // Arrange + final DeliveryOutcome outcome = new DeliveryOutcome(DeliveryState.ACCEPTED); + + when(requestResponseChannel.getErrorContext()).thenReturn(errorContext); + when(requestResponseChannel.sendWithAck(any(Message.class), eq(Accepted.getInstance()))) + .thenReturn(Mono.empty()); + + // Act & Assert + StepVerifier.create(managementChannel.send(annotatedMessage, outcome)) + .then(() -> tokenProviderResults.next(AmqpResponseCode.OK)) + .expectErrorSatisfies(error -> { + assertTrue(error instanceof AmqpException); + assertEquals(errorContext, ((AmqpException) error).getContext()); + assertTrue(((AmqpException) error).isTransient()); + }) + .verify(); + } + + /** + * When an authorization returns no response, it errors. + */ + @Test + public void sendMessageDeliveryOutcomeNoAuthErrors() { + // Arrange + final DeliveryOutcome outcome = new DeliveryOutcome(DeliveryState.ACCEPTED); + + when(requestResponseChannel.getErrorContext()).thenReturn(errorContext); + when(requestResponseChannel.sendWithAck(any(Message.class), eq(Accepted.getInstance()))) + .thenReturn(Mono.empty()); + + // Act & Assert + StepVerifier.create(managementChannel.send(annotatedMessage, outcome)) + .then(() -> tokenProviderResults.complete()) + .expectErrorSatisfies(error -> { + assertTrue(error instanceof AmqpException); + assertFalse(((AmqpException) error).isTransient()); + }) + .verify(); + + verify(requestResponseChannel, never()).sendWithAck(any(), any()); + } + + /** + * Sends a message with {@link AmqpResponseCode#NOT_FOUND} and asserts the response. + */ + @MethodSource + @ParameterizedTest + public void sendMessageNotFound(AmqpErrorCondition errorCondition) { + // Arrange + when(requestResponseChannel.getErrorContext()).thenReturn(errorContext); + when(requestResponseChannel.sendWithAck(any(Message.class))).thenReturn(Mono.just(responseMessage)); + + final AmqpResponseCode responseCode = AmqpResponseCode.NOT_FOUND; + applicationProperties.put(STATUS_CODE_KEY, responseCode.getValue()); + applicationProperties.put(ERROR_CONDITION_KEY, Symbol.getSymbol(errorCondition.getErrorCondition())); + + responseMessage.setApplicationProperties(new ApplicationProperties(applicationProperties)); + + final byte[] body = "foo".getBytes(StandardCharsets.UTF_8); + final Data dataBody = new Data(Binary.create(ByteBuffer.wrap(body))); + responseMessage.setBody(dataBody); + + // Act & Assert + StepVerifier.create(managementChannel.send(annotatedMessage)) + .then(() -> tokenProviderResults.next(AmqpResponseCode.OK)) + .assertNext(actual -> { + assertNotNull(actual.getApplicationProperties()); + assertEquals(responseCode.getValue(), actual.getApplicationProperties().get(STATUS_CODE_KEY)); + + assertEquals(AmqpMessageBodyType.DATA, actual.getBody().getBodyType()); + assertEquals(body, actual.getBody().getFirstData()); + }) + .expectComplete() + .verify(); + } + + /** + * Tests that we propagate any management errors. + */ + @Test + public void sendMessageUnsuccessful() { + // Arrange + when(requestResponseChannel.getErrorContext()).thenReturn(errorContext); + when(requestResponseChannel.sendWithAck(any(Message.class))).thenReturn(Mono.just(responseMessage)); + + final String statusDescription = "a status description"; + final AmqpResponseCode responseCode = AmqpResponseCode.FORBIDDEN; + final AmqpErrorCondition errorCondition = AmqpErrorCondition.ILLEGAL_STATE; + applicationProperties.put(STATUS_CODE_KEY, responseCode.getValue()); + applicationProperties.put(STATUS_DESCRIPTION_KEY, statusDescription); + applicationProperties.put(ERROR_CONDITION_KEY, Symbol.getSymbol(errorCondition.getErrorCondition())); + + responseMessage.setApplicationProperties(new ApplicationProperties(applicationProperties)); + + // Act & Assert + StepVerifier.create(managementChannel.send(annotatedMessage)) + .then(() -> tokenProviderResults.next(AmqpResponseCode.OK)) + .expectErrorSatisfies(error -> { + assertTrue(error instanceof AmqpException); + assertFalse(((AmqpException) error).isTransient()); + assertEquals(errorCondition, ((AmqpException) error).getErrorCondition()); + assertEquals(errorContext, ((AmqpException) error).getContext()); + }) + .verify(); + } + + public static Stream sendMessageNotFound() { + return Stream.of(AmqpErrorCondition.MESSAGE_NOT_FOUND, AmqpErrorCondition.SESSION_NOT_FOUND); + } + + public static Stream successfulResponseCodes() { + return Stream.of(AmqpResponseCode.ACCEPTED, AmqpResponseCode.OK, AmqpResponseCode.NO_CONTENT); + } + + /** + * Verifies that an error is emitted when user is unauthorized. + */ + @Test + void unauthorized() { + // Arrange + final AmqpResponseCode responseCode = AmqpResponseCode.UNAUTHORIZED; + final AmqpErrorCondition expected = AmqpErrorCondition.UNAUTHORIZED_ACCESS; + + // Act & Assert + StepVerifier.create(managementChannel.send(annotatedMessage)) + .then(() -> tokenProviderResults.next(responseCode)) + .expectErrorSatisfies(error -> { + assertTrue(error instanceof AmqpException); + assertEquals(expected, ((AmqpException) error).getErrorCondition()); + }) + .verify(); + } +} diff --git a/sdk/core/azure-core-amqp/src/test/java/com/azure/core/amqp/implementation/MessageUtilsTest.java b/sdk/core/azure-core-amqp/src/test/java/com/azure/core/amqp/implementation/MessageUtilsTest.java new file mode 100644 index 0000000000000..161f75402209d --- /dev/null +++ b/sdk/core/azure-core-amqp/src/test/java/com/azure/core/amqp/implementation/MessageUtilsTest.java @@ -0,0 +1,936 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.azure.core.amqp.implementation; + +import com.azure.core.amqp.exception.AmqpErrorCondition; +import com.azure.core.amqp.models.AmqpAddress; +import com.azure.core.amqp.models.AmqpAnnotatedMessage; +import com.azure.core.amqp.models.AmqpMessageBody; +import com.azure.core.amqp.models.AmqpMessageBodyType; +import com.azure.core.amqp.models.AmqpMessageHeader; +import com.azure.core.amqp.models.AmqpMessageId; +import com.azure.core.amqp.models.AmqpMessageProperties; +import com.azure.core.amqp.models.DeliveryOutcome; +import com.azure.core.amqp.models.DeliveryState; +import com.azure.core.amqp.models.ModifiedDeliveryOutcome; +import com.azure.core.amqp.models.ReceivedDeliveryOutcome; +import com.azure.core.amqp.models.RejectedDeliveryOutcome; +import com.azure.core.amqp.models.TransactionalDeliveryOutcome; +import org.apache.qpid.proton.Proton; +import org.apache.qpid.proton.amqp.Binary; +import org.apache.qpid.proton.amqp.Symbol; +import org.apache.qpid.proton.amqp.UnsignedByte; +import org.apache.qpid.proton.amqp.UnsignedInteger; +import org.apache.qpid.proton.amqp.messaging.Accepted; +import org.apache.qpid.proton.amqp.messaging.ApplicationProperties; +import org.apache.qpid.proton.amqp.messaging.Data; +import org.apache.qpid.proton.amqp.messaging.DeliveryAnnotations; +import org.apache.qpid.proton.amqp.messaging.Footer; +import org.apache.qpid.proton.amqp.messaging.Header; +import org.apache.qpid.proton.amqp.messaging.MessageAnnotations; +import org.apache.qpid.proton.amqp.messaging.Modified; +import org.apache.qpid.proton.amqp.messaging.Outcome; +import org.apache.qpid.proton.amqp.messaging.Properties; +import org.apache.qpid.proton.amqp.messaging.Received; +import org.apache.qpid.proton.amqp.messaging.Rejected; +import org.apache.qpid.proton.amqp.messaging.Released; +import org.apache.qpid.proton.amqp.transaction.Declared; +import org.apache.qpid.proton.amqp.transaction.TransactionalState; +import org.apache.qpid.proton.amqp.transport.DeliveryState.DeliveryStateType; +import org.apache.qpid.proton.amqp.transport.ErrorCondition; +import org.apache.qpid.proton.message.Message; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; + +import java.nio.ByteBuffer; +import java.nio.charset.StandardCharsets; +import java.time.Duration; +import java.time.OffsetDateTime; +import java.util.Date; +import java.util.HashMap; +import java.util.Map; +import java.util.stream.Stream; + +import static org.junit.jupiter.api.Assertions.assertArrayEquals; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +/** + * Tests utility methods in {@link MessageUtilsTest}. + */ +public class MessageUtilsTest { + + /** + * Parameters to pass into {@link #toDeliveryOutcomeFromOutcome(Outcome, DeliveryOutcome)} and {@link + * #toDeliveryOutcomeFromDeliveryState(org.apache.qpid.proton.amqp.transport.DeliveryState, DeliveryOutcome)}. + * Proton-j classes inherit from two interfaces, so can be used as inputs to both tests. + * + * @return Stream of arguments. + */ + public static Stream getProtonJOutcomesAndDeliveryStates() { + return Stream.of( + Arguments.of(Accepted.getInstance(), new DeliveryOutcome(DeliveryState.ACCEPTED)), + Arguments.of(Released.getInstance(), new DeliveryOutcome(DeliveryState.RELEASED))); + } + + /** + * Simple arguments where the proton-j delivery state is also its outcome. + * + * @return A stream of arguments. + */ + public static Stream getDeliveryStatesToTest() { + return Stream.of( + Arguments.arguments(DeliveryState.ACCEPTED, Accepted.getInstance(), + DeliveryStateType.Accepted), + Arguments.arguments(DeliveryState.RELEASED, Released.getInstance(), + DeliveryStateType.Released), + Arguments.arguments(DeliveryState.MODIFIED, new Modified(), + DeliveryStateType.Modified), + Arguments.arguments(DeliveryState.REJECTED, new Rejected(), + DeliveryStateType.Rejected)); + } + + /** + * Unsupported message bodies. + * + * @return Unsupported messaged bodies. + */ + public static Stream getUnsupportedMessageBody() { + return Stream.of(AmqpMessageBodyType.VALUE, AmqpMessageBodyType.SEQUENCE); + } + + /** + * Converts from a proton-j message to an AMQP annotated message. + */ + @Test + public void toAmqpAnnotatedMessage() { + final byte[] contents = "foo-bar".getBytes(StandardCharsets.UTF_8); + final Data body = new Data(Binary.create(ByteBuffer.wrap(contents))); + + final Header header = new Header(); + header.setDurable(true); + header.setDeliveryCount(new UnsignedInteger(17)); + header.setPriority(new UnsignedByte((byte) 2)); + header.setFirstAcquirer(false); + header.setTtl(new UnsignedInteger(10)); + final String messageId = "Test-message-id"; + final String correlationId = "correlation-id-test"; + final byte[] userId = "baz".getBytes(StandardCharsets.UTF_8); + final Properties properties = new Properties(); + + final OffsetDateTime absoluteDate = OffsetDateTime.parse("2021-02-04T10:15:30+00:00"); + properties.setAbsoluteExpiryTime(Date.from(absoluteDate.toInstant())); + properties.setContentEncoding(Symbol.valueOf("content-encoding-test")); + properties.setContentType(Symbol.valueOf("content-type-test")); + properties.setCorrelationId(correlationId); + + final OffsetDateTime creationTime = OffsetDateTime.parse("2021-02-03T10:15:30+00:00"); + properties.setCreationTime(Date.from(creationTime.toInstant())); + properties.setGroupId("group-id-test"); + properties.setGroupSequence(new UnsignedInteger(16)); + properties.setMessageId(messageId); + properties.setReplyToGroupId("reply-to-group-id-test"); + properties.setReplyTo("foo"); + properties.setTo("bar"); + properties.setSubject("subject-item"); + properties.setUserId(Binary.create(ByteBuffer.wrap(userId))); + + final Map applicationProperties = new HashMap<>(); + applicationProperties.put("1", "one"); + applicationProperties.put("two", 2); + + final Map deliveryAnnotations = new HashMap<>(); + deliveryAnnotations.put(Symbol.valueOf("delivery1"), 1); + deliveryAnnotations.put(Symbol.valueOf("delivery2"), 2); + + final Map messageAnnotations = new HashMap<>(); + messageAnnotations.put(Symbol.valueOf("something"), "else"); + + final Map footer = new HashMap<>(); + footer.put(Symbol.valueOf("1"), false); + + final Message message = Proton.message(); + message.setBody(body); + message.setHeader(header); + message.setProperties(properties); + message.setApplicationProperties(new ApplicationProperties(applicationProperties)); + message.setMessageAnnotations(new MessageAnnotations(messageAnnotations)); + message.setDeliveryAnnotations(new DeliveryAnnotations(deliveryAnnotations)); + message.setFooter(new Footer(footer)); + + // Act + final AmqpAnnotatedMessage actual = MessageUtils.toAmqpAnnotatedMessage(message); + + // Assert + assertNotNull(actual); + assertNotNull(actual.getBody()); + assertArrayEquals(contents, actual.getBody().getFirstData()); + + assertHeader(actual.getHeader(), header); + assertProperties(actual.getProperties(), properties); + + assertNotNull(actual.getApplicationProperties()); + assertEquals(applicationProperties.size(), actual.getApplicationProperties().size()); + applicationProperties.forEach((key, value) -> assertEquals(value, actual.getApplicationProperties().get(key))); + + assertSymbolMap(deliveryAnnotations, actual.getDeliveryAnnotations()); + assertSymbolMap(messageAnnotations, actual.getMessageAnnotations()); + assertSymbolMap(footer, actual.getFooter()); + } + + /** + * Tests a conversion from {@link AmqpAnnotatedMessage} to proton-j Message. + */ + @Test + public void toProtonJMessage() { + // Arrange + final byte[] contents = "foo-bar".getBytes(StandardCharsets.UTF_8); + final AmqpMessageBody body = AmqpMessageBody.fromData(contents); + final AmqpAnnotatedMessage expected = new AmqpAnnotatedMessage(body); + final AmqpMessageHeader header = expected.getHeader().setDurable(true) + .setDeliveryCount(17L) + .setPriority((short) 2) + .setFirstAcquirer(false) + .setTimeToLive(Duration.ofSeconds(10)); + final String messageId = "Test-message-id"; + final AmqpMessageId amqpMessageId = new AmqpMessageId(messageId); + final AmqpMessageId correlationId = new AmqpMessageId("correlation-id-test"); + final AmqpAddress replyTo = new AmqpAddress("foo"); + final AmqpAddress to = new AmqpAddress("bar"); + final byte[] userId = "baz".getBytes(StandardCharsets.UTF_8); + final AmqpMessageProperties properties = expected.getProperties() + .setAbsoluteExpiryTime(OffsetDateTime.parse("2021-02-04T10:15:30+00:00")) + .setContentEncoding("content-encoding-test") + .setContentType("content-type-test") + .setCorrelationId(correlationId) + .setCreationTime(OffsetDateTime.parse("2021-02-03T10:15:30+00:00")) + .setGroupId("group-id-test") + .setGroupSequence(22L) + .setMessageId(amqpMessageId) + .setReplyToGroupId("reply-to-group-id-test") + .setReplyTo(replyTo) + .setTo(to) + .setSubject("subject-item") + .setUserId(userId); + + final Map applicationProperties = new HashMap<>(); + applicationProperties.put("1", "one"); + applicationProperties.put("two", 2); + + applicationProperties.forEach((key, value) -> + expected.getApplicationProperties().put(key, value)); + + final Map deliveryAnnotations = new HashMap<>(); + deliveryAnnotations.put("delivery1", 1); + deliveryAnnotations.put("delivery2", 2); + + deliveryAnnotations.forEach((key, value) -> expected.getDeliveryAnnotations().put(key, value)); + + final Map messageAnnotations = new HashMap<>(); + messageAnnotations.put("something", "else"); + + messageAnnotations.forEach((key, value) -> expected.getMessageAnnotations().put(key, value)); + + final Map footer = new HashMap<>(); + footer.put("1", false); + + footer.forEach((key, value) -> expected.getFooter().put(key, value)); + + // Act + final Message actual = MessageUtils.toProtonJMessage(expected); + + // Assert + assertNotNull(actual); + + assertTrue(actual.getBody() instanceof Data); + + final Data dataBody = (Data) actual.getBody(); + assertArrayEquals(body.getFirstData(), dataBody.getValue().getArray()); + + assertHeader(header, actual.getHeader()); + assertProperties(properties, actual.getProperties()); + } + + /** + * Tests the unsupported message bodies. AMQP sequence and value. + */ + @MethodSource("getUnsupportedMessageBody") + @ParameterizedTest + public void toProtonJMessageUnsupportedMessageBody(AmqpMessageBodyType bodyType) { + final AmqpMessageBody messageBody = mock(AmqpMessageBody.class); + when(messageBody.getBodyType()).thenReturn(bodyType); + + final AmqpAnnotatedMessage message = new AmqpAnnotatedMessage(messageBody); + + // Act & Assert + assertThrows(UnsupportedOperationException.class, () -> MessageUtils.toProtonJMessage(message)); + } + + /** + * Converts from proton-j DeliveryState to delivery outcome. + */ + @MethodSource("getProtonJOutcomesAndDeliveryStates") + @ParameterizedTest + public void toDeliveryOutcomeFromDeliveryState(org.apache.qpid.proton.amqp.transport.DeliveryState deliveryState, + DeliveryOutcome expected) { + + // Act + final DeliveryOutcome actual = MessageUtils.toDeliveryOutcome(deliveryState); + + // Assert + assertNotNull(actual); + assertEquals(expected.getDeliveryState(), actual.getDeliveryState()); + } + + /** + * Tests that we can convert from a Modified delivery state to the appropriate delivery outcome. + */ + @Test + public void toDeliveryOutcomeFromModifiedDeliveryState() { + // Arrange + final Map messageAnnotations = new HashMap<>(); + messageAnnotations.put(Symbol.getSymbol("bar"), "foo"); + messageAnnotations.put(Symbol.getSymbol("baz"), 10); + + final Modified modified = new Modified(); + modified.setDeliveryFailed(true); + modified.setMessageAnnotations(messageAnnotations); + + // Act + final DeliveryOutcome actual = MessageUtils.toDeliveryOutcome( + (org.apache.qpid.proton.amqp.transport.DeliveryState) modified); + + // Assert + assertTrue(actual instanceof ModifiedDeliveryOutcome); + assertModified((ModifiedDeliveryOutcome) actual, modified); + } + + /** + * Tests that we can convert from Modified delivery state type to the appropriate delivery outcome. The difference + * is that this does not use the {@link Modified} class. + */ + @Test + public void toDeliveryOutcomeFromModifiedDeliveryStateNotSameClass() { + // Arrange + final org.apache.qpid.proton.amqp.transport.DeliveryState state = + mock(org.apache.qpid.proton.amqp.transport.DeliveryState.class); + + when(state.getType()).thenReturn(DeliveryStateType.Modified); + + // Act + final DeliveryOutcome actual = MessageUtils.toDeliveryOutcome(state); + + // Assert + assertTrue(actual instanceof ModifiedDeliveryOutcome); + assertEquals(DeliveryState.MODIFIED, actual.getDeliveryState()); + } + + /** + * Tests that we can convert from a Rejected delivery state to the appropriate delivery outcome. + */ + @Test + public void toDeliveryOutcomeFromRejectedDeliveryState() { + // Arrange + final Map errorInfo = new HashMap<>(); + errorInfo.put(Symbol.getSymbol("bar"), "foo"); + errorInfo.put(Symbol.getSymbol("baz"), 10); + + final AmqpErrorCondition error = AmqpErrorCondition.INTERNAL_ERROR; + final String errorDescription = "test: " + error.getErrorCondition(); + + final ErrorCondition errorCondition = new ErrorCondition(Symbol.getSymbol(error.getErrorCondition()), + errorDescription); + errorCondition.setInfo(errorInfo); + + final Rejected rejected = new Rejected(); + rejected.setError(errorCondition); + + // Act + final DeliveryOutcome actual = MessageUtils.toDeliveryOutcome( + (org.apache.qpid.proton.amqp.transport.DeliveryState) rejected); + + // Assert + assertTrue(actual instanceof RejectedDeliveryOutcome); + assertRejected((RejectedDeliveryOutcome) actual, rejected); + } + + /** + * Tests that we can convert from Rejected delivery state type to the appropriate delivery outcome. The difference + * is that this does not use the {@link Rejected} class. + */ + @Test + public void toDeliveryOutcomeFromRejectedDeliveryStateNotSameClass() { + // Arrange + final org.apache.qpid.proton.amqp.transport.DeliveryState state = + mock(org.apache.qpid.proton.amqp.transport.DeliveryState.class); + + when(state.getType()).thenReturn(DeliveryStateType.Rejected); + + // Act + final DeliveryOutcome actual = MessageUtils.toDeliveryOutcome(state); + + // Assert + assertEquals(DeliveryState.REJECTED, actual.getDeliveryState()); + } + + /** + * Tests that we can convert from a Declared delivery state to the appropriate delivery outcome. + */ + @Test + public void toDeliveryOutcomeFromDeclaredDeliveryState() { + // Arrange + final ByteBuffer transactionId = ByteBuffer.wrap("foo".getBytes(StandardCharsets.UTF_8)); + final Binary binary = Binary.create(transactionId); + final Declared declared = new Declared(); + declared.setTxnId(binary); + + // Act + final DeliveryOutcome actual = MessageUtils.toDeliveryOutcome( + (org.apache.qpid.proton.amqp.transport.DeliveryState) declared); + + // Assert + assertTrue(actual instanceof TransactionalDeliveryOutcome); + + final TransactionalDeliveryOutcome actualOutcome = (TransactionalDeliveryOutcome) actual; + assertEquals(DeliveryState.TRANSACTIONAL, actualOutcome.getDeliveryState()); + assertNull(actualOutcome.getOutcome()); + + assertEquals(transactionId, actualOutcome.getTransactionId()); + } + + /** + * Tests that Declared delivery state with no transaction id has an exception thrown. + */ + @Test + public void toDeliveryOutcomeFromDeclaredDeliveryStateNoTransactionId() { + // Arrange + final Declared declared = new Declared(); + + // Act & Assert + assertThrows(IllegalArgumentException.class, () -> MessageUtils.toDeliveryOutcome( + (org.apache.qpid.proton.amqp.transport.DeliveryState) declared)); + } + + /** + * Tests that an Declared delivery state type that is not also {@link Declared} throws. + */ + @Test + public void toDeliveryOutcomeDeclaredDeliveryStateNotSameClass() { + // Arrange + final org.apache.qpid.proton.amqp.transport.DeliveryState deliveryState = mock( + org.apache.qpid.proton.amqp.transport.DeliveryState.class); + + when(deliveryState.getType()).thenReturn(DeliveryStateType.Declared); + + // Act & Assert + assertThrows(IllegalArgumentException.class, () -> MessageUtils.toDeliveryOutcome(deliveryState)); + } + + /** + * Tests that we can convert from a Transactional delivery state to the appropriate delivery outcome. The + * transaction does not have an outcome associated with it. + */ + @Test + public void toDeliveryOutcomeFromTransactionalDeliveryStateNoOutcome() { + // Arrange + final ByteBuffer transactionId = ByteBuffer.wrap("foo".getBytes(StandardCharsets.UTF_8)); + final Binary binary = Binary.create(transactionId); + final TransactionalState transactionalState = new TransactionalState(); + transactionalState.setTxnId(binary); + + // Act + final DeliveryOutcome actual = MessageUtils.toDeliveryOutcome(transactionalState); + + // Assert + assertTrue(actual instanceof TransactionalDeliveryOutcome); + + final TransactionalDeliveryOutcome actualOutcome = (TransactionalDeliveryOutcome) actual; + assertEquals(DeliveryState.TRANSACTIONAL, actualOutcome.getDeliveryState()); + assertEquals(transactionId, actualOutcome.getTransactionId()); + + assertNull(actualOutcome.getOutcome()); + } + + /** + * Tests that we can convert from a Transactional delivery state to the appropriate delivery outcome. + */ + @Test + public void toDeliveryOutcomeFromTransactionalDeliveryState() { + // Arrange + final Map messageAnnotations = new HashMap<>(); + messageAnnotations.put(Symbol.getSymbol("bar"), "foo"); + messageAnnotations.put(Symbol.getSymbol("baz"), 10); + + final Modified modifiedOutcome = new Modified(); + modifiedOutcome.setDeliveryFailed(false); + modifiedOutcome.setUndeliverableHere(false); + modifiedOutcome.setMessageAnnotations(messageAnnotations); + + final ByteBuffer transactionId = ByteBuffer.wrap("foo".getBytes(StandardCharsets.UTF_8)); + final Binary binary = Binary.create(transactionId); + final TransactionalState transactionalState = new TransactionalState(); + transactionalState.setTxnId(binary); + transactionalState.setOutcome(modifiedOutcome); + + // Act + final DeliveryOutcome actual = MessageUtils.toDeliveryOutcome(transactionalState); + + // Assert + assertTrue(actual instanceof TransactionalDeliveryOutcome); + + final TransactionalDeliveryOutcome actualOutcome = (TransactionalDeliveryOutcome) actual; + assertEquals(DeliveryState.TRANSACTIONAL, actualOutcome.getDeliveryState()); + assertEquals(transactionId, actualOutcome.getTransactionId()); + + assertNotNull(actualOutcome.getOutcome()); + assertTrue(actualOutcome.getOutcome() instanceof ModifiedDeliveryOutcome); + assertModified((ModifiedDeliveryOutcome) actualOutcome.getOutcome(), modifiedOutcome); + } + + /** + * Tests that Transactional delivery state with no transaction id has an exception thrown. + */ + @Test + public void toDeliveryOutcomeFromTransactionalDeliveryStateNoTransactionId() { + // Arrange + final TransactionalState transactionalState = new TransactionalState(); + + // Act & Assert + assertThrows(IllegalArgumentException.class, () -> MessageUtils.toDeliveryOutcome(transactionalState)); + } + + /** + * Tests that an Transactional delivery state type that is not also {@link TransactionalState} throws. + */ + @Test + public void toDeliveryOutcomeTransactionDeliveryStateNotSameClass() { + // Arrange + final org.apache.qpid.proton.amqp.transport.DeliveryState deliveryState = mock( + org.apache.qpid.proton.amqp.transport.DeliveryState.class); + + when(deliveryState.getType()).thenReturn(DeliveryStateType.Transactional); + + // Act & Assert + assertThrows(IllegalArgumentException.class, () -> MessageUtils.toDeliveryOutcome(deliveryState)); + } + + /** + * Converts from proton-j outcome to delivery outcome. + */ + @MethodSource("getProtonJOutcomesAndDeliveryStates") + @ParameterizedTest + public void toDeliveryOutcomeFromOutcome(Outcome outcome, DeliveryOutcome expected) { + // Act + final DeliveryOutcome actual = MessageUtils.toDeliveryOutcome(outcome); + + // Assert + assertNotNull(actual); + assertEquals(expected.getDeliveryState(), actual.getDeliveryState()); + } + + /** + * Tests that we can convert from a Modified outcome to the appropriate delivery outcome. + */ + @Test + public void toDeliveryOutcomeFromModifiedOutcome() { + // Arrange + final Map messageAnnotations = new HashMap<>(); + messageAnnotations.put(Symbol.getSymbol("bar"), "foo"); + messageAnnotations.put(Symbol.getSymbol("baz"), 10); + + final Modified modified = new Modified(); + modified.setDeliveryFailed(true); + modified.setMessageAnnotations(messageAnnotations); + + // Act + final DeliveryOutcome actual = MessageUtils.toDeliveryOutcome((Outcome) modified); + + // Assert + assertTrue(actual instanceof ModifiedDeliveryOutcome); + + final ModifiedDeliveryOutcome actualOutcome = (ModifiedDeliveryOutcome) actual; + assertEquals(DeliveryState.MODIFIED, actualOutcome.getDeliveryState()); + assertEquals(modified.getUndeliverableHere(), actualOutcome.isUndeliverableHere()); + assertEquals(modified.getDeliveryFailed(), actualOutcome.isDeliveryFailed()); + + assertSymbolMap(messageAnnotations, actualOutcome.getMessageAnnotations()); + } + + /** + * Tests that we can convert from a Rejected outcome to the appropriate delivery outcome. + */ + @Test + public void toDeliveryOutcomeFromRejectedOutcome() { + // Arrange + final Map errorInfo = new HashMap<>(); + errorInfo.put(Symbol.getSymbol("bar"), "foo"); + errorInfo.put(Symbol.getSymbol("baz"), 10); + + final AmqpErrorCondition error = AmqpErrorCondition.INTERNAL_ERROR; + final String errorDescription = "test: " + error.getErrorCondition(); + + final ErrorCondition errorCondition = new ErrorCondition(Symbol.getSymbol(error.getErrorCondition()), + errorDescription); + errorCondition.setInfo(errorInfo); + + final Rejected rejected = new Rejected(); + rejected.setError(errorCondition); + + // Act + final DeliveryOutcome actual = MessageUtils.toDeliveryOutcome((Outcome) rejected); + + // Assert + assertTrue(actual instanceof RejectedDeliveryOutcome); + + final RejectedDeliveryOutcome actualOutcome = (RejectedDeliveryOutcome) actual; + assertEquals(DeliveryState.REJECTED, actualOutcome.getDeliveryState()); + assertEquals(error, actualOutcome.getErrorCondition()); + assertEquals(actualOutcome.getErrorCondition().getErrorCondition(), + actualOutcome.getErrorDescription()); + assertSymbolMap(errorInfo, actualOutcome.getErrorInfo()); + } + + /** + * Tests that an unsupported outcome will throw an exception. + */ + @Test + public void toDeliveryOutcomeUnsupportedOutcome() { + // Arrange + final Outcome outcome = mock(Outcome.class); + + // Act & Assert + assertThrows(UnsupportedOperationException.class, () -> MessageUtils.toDeliveryOutcome(outcome)); + } + + /** + * Tests simple conversions where the delivery states are just their statuses. + * + * @param deliveryState Delivery state. + * @param expected Expected outcome. + * @param expectedType Expected type. + */ + @MethodSource("getDeliveryStatesToTest") + @ParameterizedTest + public void toProtonJDeliveryState(DeliveryState deliveryState, + org.apache.qpid.proton.amqp.transport.DeliveryState expected, + DeliveryStateType expectedType) { + + // Arrange + final DeliveryOutcome outcome = new DeliveryOutcome(deliveryState); + + // Act + final org.apache.qpid.proton.amqp.transport.DeliveryState actual = MessageUtils.toProtonJDeliveryState(outcome); + + // Assert + assertEquals(expected.getClass(), actual.getClass()); + assertEquals(expected.getType(), actual.getType()); + + assertEquals(expectedType, actual.getType()); + } + + /** + * Tests the received outcome is mapped to its delivery state. + */ + @Test + public void toProtonJDeliveryStateReceived() { + // Arrange + final ReceivedDeliveryOutcome expected = new ReceivedDeliveryOutcome(10, 1053L); + + // Act + org.apache.qpid.proton.amqp.transport.DeliveryState actual = MessageUtils.toProtonJDeliveryState(expected); + + // Assert + assertTrue(actual instanceof Received); + + final Received received = (Received) actual; + assertNotNull(received.getSectionNumber()); + assertNotNull(received.getSectionOffset()); + + assertEquals(expected.getSectionNumber(), received.getSectionNumber().intValue()); + assertEquals(expected.getSectionOffset(), received.getSectionOffset().longValue()); + } + + /** + * Tests that the rejected delivery state is mapped correctly. + */ + @Test + public void toProtonJDeliveryStateRejected() { + // Arrange + final AmqpErrorCondition condition = AmqpErrorCondition.ILLEGAL_STATE; + final Map errorInfo = new HashMap<>(); + errorInfo.put("foo", 10); + errorInfo.put("bar", "baz"); + final RejectedDeliveryOutcome expected = new RejectedDeliveryOutcome(condition) + .setErrorInfo(errorInfo); + + // Act + org.apache.qpid.proton.amqp.transport.DeliveryState actual = MessageUtils.toProtonJDeliveryState(expected); + + // Assert + assertTrue(actual instanceof Rejected); + assertRejected(expected, (Rejected) actual); + } + + /** + * Tests that the modified delivery state is mapped correctly. + */ + @Test + public void toProtonJDeliveryStateModified() { + // Arrange + final Map annotations = new HashMap<>(); + annotations.put("foo", 10); + annotations.put("bar", "baz"); + final ModifiedDeliveryOutcome expected = new ModifiedDeliveryOutcome() + .setDeliveryFailed(true).setUndeliverableHere(true) + .setMessageAnnotations(annotations); + + // Act + final org.apache.qpid.proton.amqp.transport.DeliveryState actual = MessageUtils.toProtonJDeliveryState(expected); + + // Assert + assertTrue(actual instanceof Modified); + assertModified(expected, (Modified) actual); + } + + /** + * Tests simple conversions where the outcomes are just their statuses. + * + * @param deliveryState Delivery state. + * @param expectedType Expected type. + * @param expected Expected outcome. + */ + @MethodSource("getDeliveryStatesToTest") + @ParameterizedTest + public void toProtonJOutcome(DeliveryState deliveryState, Outcome expected, + DeliveryStateType expectedType) { + // Arrange + final DeliveryOutcome outcome = new DeliveryOutcome(deliveryState); + + // Act + final Outcome actual = MessageUtils.toProtonJOutcome(outcome); + + // Assert + assertEquals(expected.getClass(), actual.getClass()); + + if (actual instanceof org.apache.qpid.proton.amqp.transport.DeliveryState) { + assertEquals(expectedType, ((org.apache.qpid.proton.amqp.transport.DeliveryState) actual).getType()); + } + } + + /** + * Tests that an exception is thrown when an unsupported state is passed. + */ + @Test + public void toProtonJOutcomeUnsupported() { + // Arrange + // Received is not an outcome because it represents a partial message. + final DeliveryOutcome outcome = new DeliveryOutcome(DeliveryState.RECEIVED); + + // Act & Assert + assertThrows(UnsupportedOperationException.class, () -> MessageUtils.toProtonJOutcome(outcome)); + } + + /** + * Tests that the modified outcome is mapped correctly. + */ + @Test + public void toProtonJOutcomeModified() { + // Arrange + final Map annotations = new HashMap<>(); + annotations.put("foo", 10); + annotations.put("bar", "baz"); + final ModifiedDeliveryOutcome expected = new ModifiedDeliveryOutcome() + .setDeliveryFailed(true).setUndeliverableHere(true) + .setMessageAnnotations(annotations); + + // Act + final Outcome actual = MessageUtils.toProtonJOutcome(expected); + + // Assert + assertTrue(actual instanceof Modified); + assertModified(expected, (Modified) actual); + } + + /** + * Tests that the rejected outcome is mapped correctly. + */ + @Test + public void toProtonJOutcomeRejected() { + // Arrange + final AmqpErrorCondition condition = AmqpErrorCondition.RESOURCE_LIMIT_EXCEEDED; + final Map errorInfo = new HashMap<>(); + errorInfo.put("foo", 10); + errorInfo.put("bar", "baz"); + final RejectedDeliveryOutcome expected = new RejectedDeliveryOutcome(condition) + .setErrorInfo(errorInfo); + + // Act + final Outcome actual = MessageUtils.toProtonJOutcome(expected); + + // Assert + assertTrue(actual instanceof Rejected); + assertRejected(expected, (Rejected) actual); + } + + /** + * When input is null, returns null. + */ + @Test + public void nullInputs() { + + assertThrows(NullPointerException.class, () -> MessageUtils.toProtonJMessage(null)); + assertThrows(NullPointerException.class, () -> MessageUtils.toAmqpAnnotatedMessage(null)); + + assertNull(MessageUtils.toProtonJOutcome(null)); + assertNull(MessageUtils.toProtonJDeliveryState(null)); + + assertNull(MessageUtils.toDeliveryOutcome((Outcome) null)); + assertNull(MessageUtils.toDeliveryOutcome((org.apache.qpid.proton.amqp.transport.DeliveryState) null)); + } + + private static void assertRejected(RejectedDeliveryOutcome rejected, Rejected protonJRejected) { + if (rejected == null) { + assertNull(protonJRejected); + return; + } + + assertNotNull(protonJRejected); + final AmqpErrorCondition expectedCondition = rejected.getErrorCondition(); + + assertNotNull(protonJRejected.getError()); + assertEquals(expectedCondition.getErrorCondition(), protonJRejected.getError().getCondition().toString()); + + @SuppressWarnings("unchecked") final Map actualMap = protonJRejected.getError().getInfo(); + assertSymbolMap(actualMap, rejected.getErrorInfo()); + } + + private static void assertModified(ModifiedDeliveryOutcome modified, Modified protonJModified) { + if (modified == null) { + assertNull(protonJModified); + return; + } + + assertNotNull(protonJModified); + assertEquals(modified.isDeliveryFailed(), protonJModified.getDeliveryFailed()); + assertEquals(modified.isUndeliverableHere(), protonJModified.getUndeliverableHere()); + + @SuppressWarnings("unchecked") final Map actualMap = protonJModified.getMessageAnnotations(); + assertSymbolMap(actualMap, modified.getMessageAnnotations()); + } + + private static void assertSymbolMap(Map symbolMap, Map stringMap) { + if (symbolMap == null) { + assertNull(stringMap); + return; + } + + assertNotNull(stringMap); + assertEquals(symbolMap.size(), stringMap.size()); + + symbolMap.forEach((key, value) -> { + assertTrue(stringMap.containsKey(key.toString())); + assertEquals(value, stringMap.get(key.toString())); + }); + } + + private static void assertHeader(AmqpMessageHeader header, Header protonJHeader) { + if (header == null) { + assertNull(protonJHeader); + return; + } + + assertNotNull(protonJHeader); + if (header.getDeliveryCount() == null) { + assertNull(protonJHeader.getDeliveryCount()); + } else { + assertNotNull(protonJHeader.getDeliveryCount()); + assertEquals(header.getDeliveryCount(), protonJHeader.getDeliveryCount().longValue()); + } + + assertEquals(header.isDurable(), protonJHeader.getDurable()); + assertEquals(header.isFirstAcquirer(), protonJHeader.getFirstAcquirer()); + + if (header.getPriority() == null) { + assertNull(protonJHeader.getPriority()); + } else { + assertNotNull(protonJHeader.getPriority()); + assertEquals(header.getPriority(), protonJHeader.getPriority().byteValue()); + } + + if (header.getTimeToLive() == null) { + assertNotNull(protonJHeader.getTtl()); + } else { + assertEquals(header.getTimeToLive().toMillis(), protonJHeader.getTtl().longValue()); + } + } + + private static void assertProperties(AmqpMessageProperties properties, Properties protonJProperties) { + assertDate(properties.getAbsoluteExpiryTime(), protonJProperties.getAbsoluteExpiryTime()); + assertSymbol(properties.getContentEncoding(), protonJProperties.getContentEncoding()); + assertSymbol(properties.getContentType(), protonJProperties.getContentType()); + + assertMessageId(properties.getCorrelationId(), protonJProperties.getCorrelationId()); + assertMessageId(properties.getMessageId(), protonJProperties.getMessageId()); + + assertDate(properties.getCreationTime(), protonJProperties.getCreationTime()); + assertEquals(properties.getGroupId(), protonJProperties.getGroupId()); + + assertAddress(properties.getReplyTo(), protonJProperties.getReplyTo()); + assertEquals(properties.getReplyToGroupId(), protonJProperties.getReplyToGroupId()); + + assertAddress(properties.getTo(), protonJProperties.getTo()); + assertEquals(properties.getSubject(), protonJProperties.getSubject()); + + if (properties.getUserId() != null) { + assertNotNull(protonJProperties.getUserId()); + assertArrayEquals(properties.getUserId(), protonJProperties.getUserId().getArray()); + } else { + assertNull(protonJProperties.getUserId()); + } + } + + private static void assertMessageId(AmqpMessageId amqpMessageId, Object id) { + if (amqpMessageId == null) { + assertNull(id); + return; + } + + assertNotNull(id); + assertEquals(amqpMessageId.toString(), id.toString()); + } + + private static void assertDate(OffsetDateTime offsetDateTime, Date date) { + if (offsetDateTime == null) { + assertNull(date); + } else { + assertNotNull(date); + assertEquals(offsetDateTime.toInstant(), date.toInstant()); + } + } + + private static void assertSymbol(String content, Symbol symbol) { + if (content == null) { + assertNull(symbol); + } else { + assertNotNull(symbol); + assertEquals(content, symbol.toString()); + } + } + + private static void assertAddress(AmqpAddress amqpAddress, String address) { + if (amqpAddress == null) { + assertNull(address); + } else { + assertNotNull(address); + assertEquals(amqpAddress.toString(), address); + } + } +} diff --git a/sdk/core/azure-core-amqp/src/test/java/com/azure/core/amqp/implementation/ReactorConnectionTest.java b/sdk/core/azure-core-amqp/src/test/java/com/azure/core/amqp/implementation/ReactorConnectionTest.java index e2016cca09980..1df2b35753bbb 100644 --- a/sdk/core/azure-core-amqp/src/test/java/com/azure/core/amqp/implementation/ReactorConnectionTest.java +++ b/sdk/core/azure-core-amqp/src/test/java/com/azure/core/amqp/implementation/ReactorConnectionTest.java @@ -11,6 +11,7 @@ import com.azure.core.amqp.ProxyOptions; import com.azure.core.amqp.exception.AmqpErrorCondition; import com.azure.core.amqp.exception.AmqpException; +import com.azure.core.amqp.exception.AmqpResponseCode; import com.azure.core.amqp.implementation.handler.ConnectionHandler; import com.azure.core.amqp.implementation.handler.SessionHandler; import com.azure.core.amqp.models.CbsAuthorizationType; @@ -47,6 +48,7 @@ import reactor.core.scheduler.Scheduler; import reactor.core.scheduler.Schedulers; import reactor.test.StepVerifier; +import reactor.test.publisher.TestPublisher; import java.io.IOException; import java.nio.channels.Pipe; @@ -66,6 +68,7 @@ import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.ArgumentMatchers.argThat; import static org.mockito.Mockito.doAnswer; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.times; @@ -137,8 +140,9 @@ void setup() throws IOException { final AmqpRetryOptions retryOptions = new AmqpRetryOptions().setMaxRetries(0).setTryTimeout(TEST_DURATION); connectionOptions = new ConnectionOptions(CREDENTIAL_INFO.getEndpoint().getHost(), - tokenCredential, CbsAuthorizationType.SHARED_ACCESS_SIGNATURE, AmqpTransportType.AMQP, retryOptions, - ProxyOptions.SYSTEM_DEFAULTS, SCHEDULER, CLIENT_OPTIONS, VERIFY_MODE, PRODUCT, CLIENT_VERSION); + tokenCredential, CbsAuthorizationType.SHARED_ACCESS_SIGNATURE, "scope", + AmqpTransportType.AMQP, retryOptions, ProxyOptions.SYSTEM_DEFAULTS, SCHEDULER, CLIENT_OPTIONS, VERIFY_MODE, + PRODUCT, CLIENT_VERSION); connectionHandler = new ConnectionHandler(CONNECTION_ID, connectionOptions, peerDetails); @@ -397,8 +401,9 @@ void createCBSNodeTimeoutException() throws IOException { .setMode(AmqpRetryMode.FIXED) .setTryTimeout(timeout); final ConnectionOptions connectionOptions = new ConnectionOptions(CREDENTIAL_INFO.getEndpoint().getHost(), - tokenCredential, CbsAuthorizationType.SHARED_ACCESS_SIGNATURE, AmqpTransportType.AMQP, retryOptions, - ProxyOptions.SYSTEM_DEFAULTS, Schedulers.parallel(), CLIENT_OPTIONS, VERIFY_MODE, PRODUCT, CLIENT_VERSION); + tokenCredential, CbsAuthorizationType.SHARED_ACCESS_SIGNATURE, "scope", + AmqpTransportType.AMQP, retryOptions, ProxyOptions.SYSTEM_DEFAULTS, Schedulers.parallel(), + CLIENT_OPTIONS, VERIFY_MODE, PRODUCT, CLIENT_VERSION); final ConnectionHandler handler = new ConnectionHandler(CONNECTION_ID, connectionOptions, peerDetails); final ReactorHandlerProvider handlerProvider = mock(ReactorHandlerProvider.class); @@ -632,7 +637,7 @@ void setsPropertiesUsingCustomEndpoint() throws IOException { final String hostname = "custom-endpoint.com"; final int port = 10002; final ConnectionOptions connectionOptions = new ConnectionOptions(CREDENTIAL_INFO.getEndpoint().getHost(), - tokenCredential, CbsAuthorizationType.SHARED_ACCESS_SIGNATURE, AmqpTransportType.AMQP, + tokenCredential, CbsAuthorizationType.SHARED_ACCESS_SIGNATURE, "scope", AmqpTransportType.AMQP, new AmqpRetryOptions(), ProxyOptions.SYSTEM_DEFAULTS, SCHEDULER, CLIENT_OPTIONS, VERIFY_MODE, PRODUCT, CLIENT_VERSION, hostname, port); @@ -716,4 +721,41 @@ void dispose() throws IOException { connection2.dispose(); } + + @Test + void createManagementNode() { + final String entityPath = "foo"; + final Session session = mock(Session.class); + final Record record = mock(Record.class); + when(session.attachments()).thenReturn(record); + + when(connectionProtonJ.getRemoteState()).thenReturn(EndpointState.ACTIVE); + when(connectionProtonJ.session()).thenReturn(session); + + final Event mock = mock(Event.class); + when(mock.getConnection()).thenReturn(connectionProtonJ); + connectionHandler.onConnectionRemoteOpen(mock); + + final TestPublisher resultsPublisher = TestPublisher.createCold(); + resultsPublisher.next(AmqpResponseCode.ACCEPTED); + + final TokenManager manager = mock(TokenManager.class); + when(manager.authorize()).thenReturn(Mono.just(Duration.ofMinutes(20).toMillis())); + when(manager.getAuthorizationResults()).thenReturn(resultsPublisher.flux()); + + when(tokenManager.getTokenManager(any(), any())).thenReturn(manager); + + final TestPublisher sessionEndpoints = TestPublisher.createCold(); + sessionEndpoints.next(EndpointState.ACTIVE); + + final SessionHandler sessionHandler = mock(SessionHandler.class); + when(sessionHandler.getEndpointStates()).thenReturn(sessionEndpoints.flux()); + when(reactorHandlerProvider.createSessionHandler(any(), argThat(path -> path.contains("mgmt") && path.contains(entityPath)), + anyString(), any())).thenReturn(sessionHandler); + + // Act and Assert + StepVerifier.create(connection.getManagementNode(entityPath)) + .assertNext(node -> assertTrue(node instanceof ManagementChannel)) + .verifyComplete(); + } } diff --git a/sdk/core/azure-core-amqp/src/test/java/com/azure/core/amqp/implementation/ReactorHandlerProviderTest.java b/sdk/core/azure-core-amqp/src/test/java/com/azure/core/amqp/implementation/ReactorHandlerProviderTest.java index 0c3ad885e5a7f..3df45ecea23cb 100644 --- a/sdk/core/azure-core-amqp/src/test/java/com/azure/core/amqp/implementation/ReactorHandlerProviderTest.java +++ b/sdk/core/azure-core-amqp/src/test/java/com/azure/core/amqp/implementation/ReactorHandlerProviderTest.java @@ -129,8 +129,9 @@ public void constructorNull() { public void connectionHandlerNull() { // Arrange final ConnectionOptions connectionOptions = new ConnectionOptions("fqdn", tokenCredential, - CbsAuthorizationType.SHARED_ACCESS_SIGNATURE, AmqpTransportType.AMQP_WEB_SOCKETS, new AmqpRetryOptions(), - null, scheduler, CLIENT_OPTIONS, VERIFY_MODE, PRODUCT, CLIENT_VERSION); + CbsAuthorizationType.SHARED_ACCESS_SIGNATURE, "scope", + AmqpTransportType.AMQP_WEB_SOCKETS, new AmqpRetryOptions(), null, scheduler, CLIENT_OPTIONS, + VERIFY_MODE, PRODUCT, CLIENT_VERSION); // Act assertThrows(NullPointerException.class, @@ -151,7 +152,7 @@ public static Stream getHostnameAndPorts() { public void getsConnectionHandlerAMQP(String hostname, int port, String expectedHostname, int expectedPort) { // Act final ConnectionOptions connectionOptions = new ConnectionOptions(FULLY_QUALIFIED_DOMAIN_NAME, tokenCredential, - CbsAuthorizationType.SHARED_ACCESS_SIGNATURE, AmqpTransportType.AMQP, + CbsAuthorizationType.SHARED_ACCESS_SIGNATURE, "scope", AmqpTransportType.AMQP, new AmqpRetryOptions(), ProxyOptions.SYSTEM_DEFAULTS, scheduler, CLIENT_OPTIONS, VERIFY_MODE, PRODUCT, CLIENT_VERSION, hostname, port); @@ -171,7 +172,7 @@ public void getsConnectionHandlerAMQP(String hostname, int port, String expected public void getsConnectionHandlerWebSockets(ProxyOptions configuration) { // Act final ConnectionOptions connectionOptions = new ConnectionOptions(FULLY_QUALIFIED_DOMAIN_NAME, tokenCredential, - CbsAuthorizationType.SHARED_ACCESS_SIGNATURE, AmqpTransportType.AMQP_WEB_SOCKETS, + CbsAuthorizationType.SHARED_ACCESS_SIGNATURE, "scope", AmqpTransportType.AMQP_WEB_SOCKETS, new AmqpRetryOptions(), configuration, scheduler, CLIENT_OPTIONS, VERIFY_MODE, PRODUCT, CLIENT_VERSION); // Act @@ -194,7 +195,7 @@ public void getsConnectionHandlerProxy() { PASSWORD); final String hostname = "foo.eventhubs.azure.com"; final ConnectionOptions connectionOptions = new ConnectionOptions(hostname, tokenCredential, - CbsAuthorizationType.SHARED_ACCESS_SIGNATURE, AmqpTransportType.AMQP_WEB_SOCKETS, + CbsAuthorizationType.SHARED_ACCESS_SIGNATURE, "scope", AmqpTransportType.AMQP_WEB_SOCKETS, new AmqpRetryOptions(), configuration, scheduler, CLIENT_OPTIONS, VERIFY_MODE, PRODUCT, CLIENT_VERSION); // Act @@ -228,8 +229,9 @@ public void getsConnectionHandlerSystemProxy(String hostname, Integer port, Stri final String fullyQualifiedDomainName = "foo.eventhubs.azure.com"; final ConnectionOptions connectionOptions = new ConnectionOptions(fullyQualifiedDomainName, tokenCredential, - CbsAuthorizationType.SHARED_ACCESS_SIGNATURE, AmqpTransportType.AMQP_WEB_SOCKETS, new AmqpRetryOptions(), - null, scheduler, CLIENT_OPTIONS, VERIFY_MODE, PRODUCT, CLIENT_VERSION, hostname, port); + CbsAuthorizationType.SHARED_ACCESS_SIGNATURE, "scope", AmqpTransportType.AMQP_WEB_SOCKETS, + new AmqpRetryOptions(), null, scheduler, CLIENT_OPTIONS, VERIFY_MODE, PRODUCT, CLIENT_VERSION, + hostname, port); when(proxySelector.select(any())).thenAnswer(invocation -> { final URI uri = invocation.getArgument(0); @@ -269,7 +271,7 @@ public void noProxySelected(ProxyOptions configuration) { .thenReturn(Collections.singletonList(PROXY)); final ConnectionOptions connectionOptions = new ConnectionOptions(hostname, tokenCredential, - CbsAuthorizationType.SHARED_ACCESS_SIGNATURE, AmqpTransportType.AMQP_WEB_SOCKETS, + CbsAuthorizationType.SHARED_ACCESS_SIGNATURE, "scope", AmqpTransportType.AMQP_WEB_SOCKETS, new AmqpRetryOptions(), configuration, scheduler, CLIENT_OPTIONS, VERIFY_MODE, PRODUCT, CLIENT_VERSION); // Act @@ -322,7 +324,7 @@ public void correctPeerDetailsProxy() { final String anotherFakeHostname = "hostname.fake"; final ProxyOptions proxyOptions = new ProxyOptions(ProxyAuthenticationType.BASIC, PROXY, USERNAME, PASSWORD); final ConnectionOptions connectionOptions = new ConnectionOptions(HOSTNAME, tokenCredential, - CbsAuthorizationType.SHARED_ACCESS_SIGNATURE, AmqpTransportType.AMQP_WEB_SOCKETS, + CbsAuthorizationType.SHARED_ACCESS_SIGNATURE, "scope", AmqpTransportType.AMQP_WEB_SOCKETS, new AmqpRetryOptions(), proxyOptions, scheduler, CLIENT_OPTIONS, SslDomain.VerifyMode.VERIFY_PEER_NAME, PRODUCT, CLIENT_VERSION); @@ -357,7 +359,7 @@ public void correctPeerDetailsCustomEndpoint() throws MalformedURLException { final URL customEndpoint = new URL("https://myappservice.windows.net"); final String anotherFakeHostname = "hostname.fake"; final ConnectionOptions connectionOptions = new ConnectionOptions(HOSTNAME, tokenCredential, - CbsAuthorizationType.SHARED_ACCESS_SIGNATURE, AmqpTransportType.AMQP_WEB_SOCKETS, + CbsAuthorizationType.SHARED_ACCESS_SIGNATURE, "scope", AmqpTransportType.AMQP_WEB_SOCKETS, new AmqpRetryOptions(), ProxyOptions.SYSTEM_DEFAULTS, scheduler, CLIENT_OPTIONS, SslDomain.VerifyMode.VERIFY_PEER_NAME, PRODUCT, CLIENT_VERSION, customEndpoint.getHost(), customEndpoint.getDefaultPort()); @@ -393,7 +395,7 @@ public void correctPeerDetails(AmqpTransportType transportType) { // Arrange final String anotherFakeHostname = "hostname.fake"; final ConnectionOptions connectionOptions = new ConnectionOptions(HOSTNAME, tokenCredential, - CbsAuthorizationType.SHARED_ACCESS_SIGNATURE, transportType, new AmqpRetryOptions(), + CbsAuthorizationType.SHARED_ACCESS_SIGNATURE, "scope", transportType, new AmqpRetryOptions(), ProxyOptions.SYSTEM_DEFAULTS, scheduler, CLIENT_OPTIONS, SslDomain.VerifyMode.VERIFY_PEER_NAME, PRODUCT, CLIENT_VERSION); diff --git a/sdk/core/azure-core-amqp/src/test/java/com/azure/core/amqp/implementation/RequestResponseChannelTest.java b/sdk/core/azure-core-amqp/src/test/java/com/azure/core/amqp/implementation/RequestResponseChannelTest.java index b6691edf2e72d..de01026e502d4 100644 --- a/sdk/core/azure-core-amqp/src/test/java/com/azure/core/amqp/implementation/RequestResponseChannelTest.java +++ b/sdk/core/azure-core-amqp/src/test/java/com/azure/core/amqp/implementation/RequestResponseChannelTest.java @@ -163,7 +163,7 @@ void getsProperties() { receiverSettleMode); final AmqpErrorContext errorContext = channel.getErrorContext(); - StepVerifier.create(channel.closeAsync("Test-method")) + StepVerifier.create(channel.closeAsync()) .then(() -> { sendEndpoints.complete(); receiveEndpoints.complete(); @@ -192,7 +192,7 @@ void disposeAsync() { sendEndpoints.next(EndpointState.ACTIVE); // Act - StepVerifier.create(channel.closeAsync("Test")) + StepVerifier.create(channel.closeAsync()) .then(() -> { sendEndpoints.complete(); receiveEndpoints.complete(); diff --git a/sdk/core/azure-core-amqp/src/test/java/com/azure/core/amqp/implementation/handler/ConnectionHandlerTest.java b/sdk/core/azure-core-amqp/src/test/java/com/azure/core/amqp/implementation/handler/ConnectionHandlerTest.java index 763c2e6eca717..e121cb4f34dbc 100644 --- a/sdk/core/azure-core-amqp/src/test/java/com/azure/core/amqp/implementation/handler/ConnectionHandlerTest.java +++ b/sdk/core/azure-core-amqp/src/test/java/com/azure/core/amqp/implementation/handler/ConnectionHandlerTest.java @@ -85,8 +85,9 @@ public void setup() { mocksCloseable = MockitoAnnotations.openMocks(this); this.connectionOptions = new ConnectionOptions(HOSTNAME, tokenCredential, - CbsAuthorizationType.SHARED_ACCESS_SIGNATURE, AmqpTransportType.AMQP, new AmqpRetryOptions(), - ProxyOptions.SYSTEM_DEFAULTS, scheduler, CLIENT_OPTIONS, VERIFY_MODE, CLIENT_PRODUCT, CLIENT_VERSION); + CbsAuthorizationType.SHARED_ACCESS_SIGNATURE, "authorization-scope", + AmqpTransportType.AMQP, new AmqpRetryOptions(), ProxyOptions.SYSTEM_DEFAULTS, scheduler, CLIENT_OPTIONS, + VERIFY_MODE, CLIENT_PRODUCT, CLIENT_VERSION); this.handler = new ConnectionHandler(CONNECTION_ID, connectionOptions, peerDetails); } @@ -117,8 +118,9 @@ void applicationIdNotSet() { .setHeaders(HEADER_LIST); final String expected = UserAgentUtil.toUserAgentString(null, CLIENT_PRODUCT, CLIENT_VERSION, null); final ConnectionOptions options = new ConnectionOptions(HOSTNAME, tokenCredential, - CbsAuthorizationType.SHARED_ACCESS_SIGNATURE, AmqpTransportType.AMQP, new AmqpRetryOptions(), - ProxyOptions.SYSTEM_DEFAULTS, scheduler, clientOptions, VERIFY_MODE, CLIENT_PRODUCT, CLIENT_VERSION); + CbsAuthorizationType.SHARED_ACCESS_SIGNATURE, "scope", AmqpTransportType.AMQP, + new AmqpRetryOptions(), ProxyOptions.SYSTEM_DEFAULTS, scheduler, clientOptions, VERIFY_MODE, CLIENT_PRODUCT, + CLIENT_VERSION); // Act final ConnectionHandler handler = new ConnectionHandler(CONNECTION_ID, options, peerDetails); diff --git a/sdk/core/azure-core-amqp/src/test/java/com/azure/core/amqp/implementation/handler/WebSocketsConnectionHandlerTest.java b/sdk/core/azure-core-amqp/src/test/java/com/azure/core/amqp/implementation/handler/WebSocketsConnectionHandlerTest.java index af00cfea64dd8..be2193a5ebbbd 100644 --- a/sdk/core/azure-core-amqp/src/test/java/com/azure/core/amqp/implementation/handler/WebSocketsConnectionHandlerTest.java +++ b/sdk/core/azure-core-amqp/src/test/java/com/azure/core/amqp/implementation/handler/WebSocketsConnectionHandlerTest.java @@ -69,8 +69,10 @@ public void setup() { mocksCloseable = MockitoAnnotations.openMocks(this); this.connectionOptions = new ConnectionOptions(HOSTNAME, tokenCredential, - CbsAuthorizationType.SHARED_ACCESS_SIGNATURE, AmqpTransportType.AMQP_WEB_SOCKETS, new AmqpRetryOptions(), - ProxyOptions.SYSTEM_DEFAULTS, scheduler, CLIENT_OPTIONS, VERIFY_MODE, PRODUCT, CLIENT_VERSION); + CbsAuthorizationType.SHARED_ACCESS_SIGNATURE, "authorization-scope", + AmqpTransportType.AMQP_WEB_SOCKETS, new AmqpRetryOptions(), ProxyOptions.SYSTEM_DEFAULTS, + scheduler, CLIENT_OPTIONS, VERIFY_MODE, PRODUCT, CLIENT_VERSION); + this.handler = new WebSocketsConnectionHandler(CONNECTION_ID, connectionOptions, peerDetails); } @@ -181,9 +183,9 @@ public void onConnectionInitDifferentEndpoint() { final int port = 9888; final ConnectionOptions connectionOptions = new ConnectionOptions(fullyQualifiedNamespace, tokenCredential, - CbsAuthorizationType.SHARED_ACCESS_SIGNATURE, AmqpTransportType.AMQP_WEB_SOCKETS, new AmqpRetryOptions(), - ProxyOptions.SYSTEM_DEFAULTS, scheduler, CLIENT_OPTIONS, VERIFY_MODE, PRODUCT, CLIENT_VERSION, - customEndpoint, port); + CbsAuthorizationType.SHARED_ACCESS_SIGNATURE, "authorization-scope", + AmqpTransportType.AMQP_WEB_SOCKETS, new AmqpRetryOptions(), ProxyOptions.SYSTEM_DEFAULTS, scheduler, + CLIENT_OPTIONS, VERIFY_MODE, PRODUCT, CLIENT_VERSION, customEndpoint, port); try (WebSocketsConnectionHandler handler = new WebSocketsConnectionHandler(CONNECTION_ID, connectionOptions, peerDetails)) { diff --git a/sdk/core/azure-core-amqp/src/test/java/com/azure/core/amqp/implementation/handler/WebSocketsProxyConnectionHandlerTest.java b/sdk/core/azure-core-amqp/src/test/java/com/azure/core/amqp/implementation/handler/WebSocketsProxyConnectionHandlerTest.java index c11a815e05a45..bb9019cbc7b60 100644 --- a/sdk/core/azure-core-amqp/src/test/java/com/azure/core/amqp/implementation/handler/WebSocketsProxyConnectionHandlerTest.java +++ b/sdk/core/azure-core-amqp/src/test/java/com/azure/core/amqp/implementation/handler/WebSocketsProxyConnectionHandlerTest.java @@ -77,8 +77,9 @@ public void setup() { mocksCloseable = MockitoAnnotations.openMocks(this); this.connectionOptions = new ConnectionOptions(HOSTNAME, tokenCredential, - CbsAuthorizationType.SHARED_ACCESS_SIGNATURE, AmqpTransportType.AMQP, new AmqpRetryOptions(), - ProxyOptions.SYSTEM_DEFAULTS, scheduler, CLIENT_OPTIONS, VERIFY_MODE, PRODUCT, CLIENT_VERSION); + CbsAuthorizationType.SHARED_ACCESS_SIGNATURE, "scope", AmqpTransportType.AMQP, + new AmqpRetryOptions(), ProxyOptions.SYSTEM_DEFAULTS, scheduler, CLIENT_OPTIONS, VERIFY_MODE, PRODUCT, + CLIENT_VERSION); this.originalProxySelector = ProxySelector.getDefault(); this.proxySelector = mock(ProxySelector.class, Mockito.CALLS_REAL_METHODS); diff --git a/sdk/core/azure-core-amqp/src/test/java/com/azure/core/amqp/models/DeliveryStateTest.java b/sdk/core/azure-core-amqp/src/test/java/com/azure/core/amqp/models/DeliveryStateTest.java new file mode 100644 index 0000000000000..6f712e04a6228 --- /dev/null +++ b/sdk/core/azure-core-amqp/src/test/java/com/azure/core/amqp/models/DeliveryStateTest.java @@ -0,0 +1,64 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.azure.core.amqp.models; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.MethodSource; + +import java.util.Collection; +import java.util.stream.Stream; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +/** + * Tests {@link DeliveryState} + */ +public class DeliveryStateTest { + /** + * Tests that all the values are available. + */ + @Test + public void values() { + // Arrange + final DeliveryState[] expected = new DeliveryState[] { + DeliveryState.ACCEPTED, DeliveryState.MODIFIED, DeliveryState.RECEIVED, DeliveryState.REJECTED, + DeliveryState.RELEASED, DeliveryState.TRANSACTIONAL + }; + + // Act + final Collection actual = DeliveryState.values(); + + // Assert + for (DeliveryState state : expected) { + assertTrue(actual.contains(state)); + } + } + + /** + * Arguments for fromString. + * @return Test arguments. + */ + public static Stream fromString() { + return Stream.of("MODIFIED", "FOO-BAR-NEW"); + } + + /** + * Tests that we can get the corresponding value and a new one if it does not exist. + * + * @param deliveryState Delivery states to test. + */ + @MethodSource + @ParameterizedTest + public void fromString(String deliveryState) { + // Act + final DeliveryState state = DeliveryState.fromString(deliveryState); + + // Assert + assertNotNull(state); + assertEquals(deliveryState, state.toString()); + } +} diff --git a/sdk/eventhubs/azure-messaging-eventhubs/src/main/java/com/azure/messaging/eventhubs/EventHubClientBuilder.java b/sdk/eventhubs/azure-messaging-eventhubs/src/main/java/com/azure/messaging/eventhubs/EventHubClientBuilder.java index d2c59007da6df..ec959b903ac02 100644 --- a/sdk/eventhubs/azure-messaging-eventhubs/src/main/java/com/azure/messaging/eventhubs/EventHubClientBuilder.java +++ b/sdk/eventhubs/azure-messaging-eventhubs/src/main/java/com/azure/messaging/eventhubs/EventHubClientBuilder.java @@ -636,7 +636,7 @@ private EventHubConnectionProcessor buildConnectionProcessor(MessageSerializer m final TokenManagerProvider tokenManagerProvider = new AzureTokenManagerProvider( connectionOptions.getAuthorizationType(), connectionOptions.getFullyQualifiedNamespace(), - ClientConstants.AZURE_ACTIVE_DIRECTORY_SCOPE); + connectionOptions.getAuthorizationScope()); final ReactorProvider provider = new ReactorProvider(); final ReactorHandlerProvider handlerProvider = new ReactorHandlerProvider(provider); @@ -694,12 +694,14 @@ private ConnectionOptions getConnectionOptions() { final String clientVersion = properties.getOrDefault(VERSION_KEY, UNKNOWN); if (customEndpointAddress == null) { - return new ConnectionOptions(fullyQualifiedNamespace, credentials, authorizationType, transport, - retryOptions, proxyOptions, scheduler, options, verificationMode, product, clientVersion); + return new ConnectionOptions(fullyQualifiedNamespace, credentials, authorizationType, + ClientConstants.AZURE_ACTIVE_DIRECTORY_SCOPE, transport, retryOptions, proxyOptions, scheduler, + options, verificationMode, product, clientVersion); } else { - return new ConnectionOptions(fullyQualifiedNamespace, credentials, authorizationType, transport, - retryOptions, proxyOptions, scheduler, options, verificationMode, product, clientVersion, - customEndpointAddress.getHost(), customEndpointAddress.getPort()); + return new ConnectionOptions(fullyQualifiedNamespace, credentials, authorizationType, + ClientConstants.AZURE_ACTIVE_DIRECTORY_SCOPE, transport, retryOptions, proxyOptions, scheduler, + options, verificationMode, product, clientVersion, customEndpointAddress.getHost(), + customEndpointAddress.getPort()); } } diff --git a/sdk/eventhubs/azure-messaging-eventhubs/src/main/java/com/azure/messaging/eventhubs/implementation/AmqpReceiveLinkProcessor.java b/sdk/eventhubs/azure-messaging-eventhubs/src/main/java/com/azure/messaging/eventhubs/implementation/AmqpReceiveLinkProcessor.java index abceceef10d3b..5c4bb0c57dd42 100644 --- a/sdk/eventhubs/azure-messaging-eventhubs/src/main/java/com/azure/messaging/eventhubs/implementation/AmqpReceiveLinkProcessor.java +++ b/sdk/eventhubs/azure-messaging-eventhubs/src/main/java/com/azure/messaging/eventhubs/implementation/AmqpReceiveLinkProcessor.java @@ -599,11 +599,7 @@ private void disposeReceiver(AmqpReceiveLink link) { } try { - if (link instanceof AsyncCloseable) { - ((AsyncCloseable) link).closeAsync().subscribe(); - } else { - link.dispose(); - } + ((AsyncCloseable) link).closeAsync().subscribe(); } catch (Exception error) { logger.warning("linkName[{}] entityPath[{}] Unable to dispose of link.", link.getLinkName(), link.getEntityPath(), error); diff --git a/sdk/eventhubs/azure-messaging-eventhubs/src/test/java/com/azure/messaging/eventhubs/EventHubConsumerAsyncClientTest.java b/sdk/eventhubs/azure-messaging-eventhubs/src/test/java/com/azure/messaging/eventhubs/EventHubConsumerAsyncClientTest.java index 377d9e8ce904d..7cb9a44c69d66 100644 --- a/sdk/eventhubs/azure-messaging-eventhubs/src/test/java/com/azure/messaging/eventhubs/EventHubConsumerAsyncClientTest.java +++ b/sdk/eventhubs/azure-messaging-eventhubs/src/test/java/com/azure/messaging/eventhubs/EventHubConsumerAsyncClientTest.java @@ -14,6 +14,7 @@ import com.azure.core.credential.TokenCredential; import com.azure.core.util.ClientOptions; import com.azure.core.util.logging.ClientLogger; +import com.azure.messaging.eventhubs.implementation.ClientConstants; import com.azure.messaging.eventhubs.implementation.EventHubAmqpConnection; import com.azure.messaging.eventhubs.implementation.EventHubConnectionProcessor; import com.azure.messaging.eventhubs.implementation.EventHubManagementNode; @@ -127,8 +128,9 @@ void setup() { when(amqpReceiveLink.addCredits(anyInt())).thenReturn(Mono.empty()); final ConnectionOptions connectionOptions = new ConnectionOptions(HOSTNAME, tokenCredential, - CbsAuthorizationType.SHARED_ACCESS_SIGNATURE, AmqpTransportType.AMQP_WEB_SOCKETS, new AmqpRetryOptions(), - ProxyOptions.SYSTEM_DEFAULTS, Schedulers.parallel(), CLIENT_OPTIONS, SslDomain.VerifyMode.VERIFY_PEER, + CbsAuthorizationType.SHARED_ACCESS_SIGNATURE, ClientConstants.AZURE_ACTIVE_DIRECTORY_SCOPE, + AmqpTransportType.AMQP_WEB_SOCKETS, new AmqpRetryOptions(), ProxyOptions.SYSTEM_DEFAULTS, + Schedulers.parallel(), CLIENT_OPTIONS, SslDomain.VerifyMode.VERIFY_PEER, "test-product", "test-client-version"); when(connection.getEndpointStates()).thenReturn(endpointProcessor.flux()); @@ -136,6 +138,9 @@ CbsAuthorizationType.SHARED_ACCESS_SIGNATURE, AmqpTransportType.AMQP_WEB_SOCKETS when(connection.createReceiveLink(anyString(), argThat(name -> name.endsWith(PARTITION_ID)), any(EventPosition.class), any(ReceiveOptions.class))).thenReturn(Mono.just(amqpReceiveLink)); + + when(connection.closeAsync()).thenReturn(Mono.empty()); + connectionProcessor = Flux.create(sink -> sink.next(connection)) .subscribeWith(new EventHubConnectionProcessor(connectionOptions.getFullyQualifiedNamespace(), "event-hub-name", connectionOptions.getRetry())); diff --git a/sdk/eventhubs/azure-messaging-eventhubs/src/test/java/com/azure/messaging/eventhubs/EventHubConsumerClientTest.java b/sdk/eventhubs/azure-messaging-eventhubs/src/test/java/com/azure/messaging/eventhubs/EventHubConsumerClientTest.java index 49d8c3cb19b05..19a7c07abe83c 100644 --- a/sdk/eventhubs/azure-messaging-eventhubs/src/test/java/com/azure/messaging/eventhubs/EventHubConsumerClientTest.java +++ b/sdk/eventhubs/azure-messaging-eventhubs/src/test/java/com/azure/messaging/eventhubs/EventHubConsumerClientTest.java @@ -14,6 +14,7 @@ import com.azure.core.credential.TokenCredential; import com.azure.core.util.ClientOptions; import com.azure.core.util.IterableStream; +import com.azure.messaging.eventhubs.implementation.ClientConstants; import com.azure.messaging.eventhubs.implementation.EventHubAmqpConnection; import com.azure.messaging.eventhubs.implementation.EventHubConnectionProcessor; import com.azure.messaging.eventhubs.models.EventPosition; @@ -110,9 +111,10 @@ public void setup() { when(amqpReceiveLink.addCredits(anyInt())).thenReturn(Mono.empty()); connectionOptions = new ConnectionOptions(HOSTNAME, tokenCredential, - CbsAuthorizationType.SHARED_ACCESS_SIGNATURE, AmqpTransportType.AMQP_WEB_SOCKETS, new AmqpRetryOptions(), - ProxyOptions.SYSTEM_DEFAULTS, Schedulers.parallel(), CLIENT_OPTIONS, SslDomain.VerifyMode.VERIFY_PEER, - "test-product", "test-client-version"); + CbsAuthorizationType.SHARED_ACCESS_SIGNATURE, ClientConstants.AZURE_ACTIVE_DIRECTORY_SCOPE, + AmqpTransportType.AMQP_WEB_SOCKETS, new AmqpRetryOptions(), ProxyOptions.SYSTEM_DEFAULTS, + Schedulers.parallel(), CLIENT_OPTIONS, SslDomain.VerifyMode.VERIFY_PEER, "test-product", "test-client-version"); + connectionProcessor = Flux.create(sink -> sink.next(connection)) .subscribeWith(new EventHubConnectionProcessor(connectionOptions.getFullyQualifiedNamespace(), "event-hub-path", connectionOptions.getRetry())); @@ -130,6 +132,8 @@ CbsAuthorizationType.SHARED_ACCESS_SIGNATURE, AmqpTransportType.AMQP_WEB_SOCKETS return amqpReceiveLink2; })); + when(connection.closeAsync()).thenReturn(Mono.empty()); + asyncConsumer = new EventHubConsumerAsyncClient(HOSTNAME, EVENT_HUB_NAME, connectionProcessor, messageSerializer, CONSUMER_GROUP, PREFETCH, false, onClientClosed); consumer = new EventHubConsumerClient(asyncConsumer, Duration.ofSeconds(10)); diff --git a/sdk/eventhubs/azure-messaging-eventhubs/src/test/java/com/azure/messaging/eventhubs/EventHubPartitionAsyncConsumerTest.java b/sdk/eventhubs/azure-messaging-eventhubs/src/test/java/com/azure/messaging/eventhubs/EventHubPartitionAsyncConsumerTest.java index c66e0254a99d0..47083dde0f476 100644 --- a/sdk/eventhubs/azure-messaging-eventhubs/src/test/java/com/azure/messaging/eventhubs/EventHubPartitionAsyncConsumerTest.java +++ b/sdk/eventhubs/azure-messaging-eventhubs/src/test/java/com/azure/messaging/eventhubs/EventHubPartitionAsyncConsumerTest.java @@ -49,6 +49,7 @@ import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.ArgumentMatchers.same; +import static org.mockito.Mockito.atMost; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; @@ -229,7 +230,6 @@ void receiveMultipleTimes() { Assertions.assertTrue(linkProcessor.isTerminated()); } - /** * Verifies that the consumer closes and completes any listeners on a shutdown signal. */ @@ -277,7 +277,7 @@ void listensToShutdownSignals() throws InterruptedException { Assertions.assertTrue(successful); Assertions.assertEquals(0, shutdownReceived.getCount()); - verify(link1).dispose(); + verify(link1, atMost(1)).dispose(); } finally { subscriptions.dispose(); } diff --git a/sdk/eventhubs/azure-messaging-eventhubs/src/test/java/com/azure/messaging/eventhubs/EventHubProducerAsyncClientTest.java b/sdk/eventhubs/azure-messaging-eventhubs/src/test/java/com/azure/messaging/eventhubs/EventHubProducerAsyncClientTest.java index 0bf81bc5a2bb8..b95c25a404cf3 100644 --- a/sdk/eventhubs/azure-messaging-eventhubs/src/test/java/com/azure/messaging/eventhubs/EventHubProducerAsyncClientTest.java +++ b/sdk/eventhubs/azure-messaging-eventhubs/src/test/java/com/azure/messaging/eventhubs/EventHubProducerAsyncClientTest.java @@ -140,13 +140,16 @@ void setup(TestInfo testInfo) { tracerProvider = new TracerProvider(Collections.emptyList()); connectionOptions = new ConnectionOptions(HOSTNAME, tokenCredential, - CbsAuthorizationType.SHARED_ACCESS_SIGNATURE, AmqpTransportType.AMQP_WEB_SOCKETS, retryOptions, - ProxyOptions.SYSTEM_DEFAULTS, testScheduler, CLIENT_OPTIONS, SslDomain.VerifyMode.VERIFY_PEER_NAME, + CbsAuthorizationType.SHARED_ACCESS_SIGNATURE, ClientConstants.AZURE_ACTIVE_DIRECTORY_SCOPE, + AmqpTransportType.AMQP_WEB_SOCKETS, retryOptions, ProxyOptions.SYSTEM_DEFAULTS, testScheduler, + CLIENT_OPTIONS, SslDomain.VerifyMode.VERIFY_PEER_NAME, "client-product", "client-version"); when(connection.getEndpointStates()).thenReturn(endpointProcessor); endpointSink.next(AmqpEndpointState.ACTIVE); + when(connection.closeAsync()).thenReturn(Mono.empty()); + connectionProcessor = Mono.fromCallable(() -> connection).repeat(10).subscribeWith( new EventHubConnectionProcessor(connectionOptions.getFullyQualifiedNamespace(), "event-hub-path", connectionOptions.getRetry())); @@ -162,7 +165,8 @@ void setup(TestInfo testInfo) { void teardown(TestInfo testInfo) { testScheduler.dispose(); Mockito.framework().clearInlineMocks(); - Mockito.reset(sendLink, connection); + Mockito.reset(sendLink); + Mockito.reset(connection); singleMessageCaptor = null; messagesCaptor = null; } diff --git a/sdk/eventhubs/azure-messaging-eventhubs/src/test/java/com/azure/messaging/eventhubs/EventHubProducerClientTest.java b/sdk/eventhubs/azure-messaging-eventhubs/src/test/java/com/azure/messaging/eventhubs/EventHubProducerClientTest.java index daa39bc7e6521..a5bf582b62b2f 100644 --- a/sdk/eventhubs/azure-messaging-eventhubs/src/test/java/com/azure/messaging/eventhubs/EventHubProducerClientTest.java +++ b/sdk/eventhubs/azure-messaging-eventhubs/src/test/java/com/azure/messaging/eventhubs/EventHubProducerClientTest.java @@ -101,9 +101,10 @@ public void setup() { final TracerProvider tracerProvider = new TracerProvider(Collections.emptyList()); ConnectionOptions connectionOptions = new ConnectionOptions(HOSTNAME, tokenCredential, - CbsAuthorizationType.SHARED_ACCESS_SIGNATURE, AmqpTransportType.AMQP_WEB_SOCKETS, retryOptions, - ProxyOptions.SYSTEM_DEFAULTS, Schedulers.parallel(), new ClientOptions(), - SslDomain.VerifyMode.ANONYMOUS_PEER, "test-product", "test-client-version"); + CbsAuthorizationType.SHARED_ACCESS_SIGNATURE, ClientConstants.AZURE_ACTIVE_DIRECTORY_SCOPE, + AmqpTransportType.AMQP_WEB_SOCKETS, retryOptions, ProxyOptions.SYSTEM_DEFAULTS, Schedulers.parallel(), + new ClientOptions(), SslDomain.VerifyMode.ANONYMOUS_PEER, "test-product", + "test-client-version"); connectionProcessor = Flux.create(sink -> sink.next(connection)) .subscribeWith(new EventHubConnectionProcessor(connectionOptions.getFullyQualifiedNamespace(), "event-hub-path", connectionOptions.getRetry())); @@ -111,6 +112,7 @@ public void setup() { tracerProvider, messageSerializer, Schedulers.parallel(), false, onClientClosed); when(connection.getEndpointStates()).thenReturn(Flux.create(sink -> sink.next(AmqpEndpointState.ACTIVE))); + when(connection.closeAsync()).thenReturn(Mono.empty()); } @AfterEach diff --git a/sdk/eventhubs/azure-messaging-eventhubs/src/test/java/com/azure/messaging/eventhubs/implementation/CBSChannelTest.java b/sdk/eventhubs/azure-messaging-eventhubs/src/test/java/com/azure/messaging/eventhubs/implementation/CBSChannelTest.java index a1571fd908c87..a5f86baf40573 100644 --- a/sdk/eventhubs/azure-messaging-eventhubs/src/test/java/com/azure/messaging/eventhubs/implementation/CBSChannelTest.java +++ b/sdk/eventhubs/azure-messaging-eventhubs/src/test/java/com/azure/messaging/eventhubs/implementation/CBSChannelTest.java @@ -112,8 +112,8 @@ void successfullyAuthorizes() { TokenCredential tokenCredential = new EventHubSharedKeyCredential( connectionProperties.getSharedAccessKeyName(), connectionProperties.getSharedAccessKey()); ConnectionOptions connectionOptions = new ConnectionOptions(connectionProperties.getEndpoint().getHost(), - tokenCredential, CbsAuthorizationType.SHARED_ACCESS_SIGNATURE, AmqpTransportType.AMQP, - RETRY_OPTIONS, ProxyOptions.SYSTEM_DEFAULTS, Schedulers.elastic(), clientOptions, + tokenCredential, CbsAuthorizationType.SHARED_ACCESS_SIGNATURE, ClientConstants.AZURE_ACTIVE_DIRECTORY_SCOPE, + AmqpTransportType.AMQP, RETRY_OPTIONS, ProxyOptions.SYSTEM_DEFAULTS, Schedulers.elastic(), clientOptions, SslDomain.VerifyMode.VERIFY_PEER_NAME, "test-product", "test-client-version"); connection = new TestReactorConnection(CONNECTION_ID, connectionOptions, reactorProvider, handlerProvider, azureTokenManagerProvider, messageSerializer); @@ -135,9 +135,9 @@ void unsuccessfulAuthorize() { connectionProperties.getSharedAccessKeyName(), "Invalid shared access key."); final ConnectionOptions connectionOptions = new ConnectionOptions(connectionProperties.getEndpoint().getHost(), - invalidToken, CbsAuthorizationType.SHARED_ACCESS_SIGNATURE, AmqpTransportType.AMQP, RETRY_OPTIONS, ProxyOptions.SYSTEM_DEFAULTS, - Schedulers.elastic(), clientOptions, SslDomain.VerifyMode.VERIFY_PEER, - "test-product", "test-client-version"); + invalidToken, CbsAuthorizationType.SHARED_ACCESS_SIGNATURE, ClientConstants.AZURE_ACTIVE_DIRECTORY_SCOPE, + AmqpTransportType.AMQP, RETRY_OPTIONS, ProxyOptions.SYSTEM_DEFAULTS, Schedulers.elastic(), clientOptions, + SslDomain.VerifyMode.VERIFY_PEER, "test-product", "test-client-version"); connection = new TestReactorConnection(CONNECTION_ID, connectionOptions, reactorProvider, handlerProvider, azureTokenManagerProvider, messageSerializer); diff --git a/sdk/eventhubs/azure-messaging-eventhubs/src/test/java/com/azure/messaging/eventhubs/implementation/EventHubConnectionProcessorTest.java b/sdk/eventhubs/azure-messaging-eventhubs/src/test/java/com/azure/messaging/eventhubs/implementation/EventHubConnectionProcessorTest.java index 793963aafe1d8..434f21d798a90 100644 --- a/sdk/eventhubs/azure-messaging-eventhubs/src/test/java/com/azure/messaging/eventhubs/implementation/EventHubConnectionProcessorTest.java +++ b/sdk/eventhubs/azure-messaging-eventhubs/src/test/java/com/azure/messaging/eventhubs/implementation/EventHubConnectionProcessorTest.java @@ -63,6 +63,7 @@ void setup() { when(connection.getEndpointStates()).thenReturn(endpointProcessor); when(connection.getShutdownSignals()).thenReturn(shutdownSignalProcessor); + when(connection.closeAsync()).thenReturn(Mono.empty()); } @AfterEach @@ -125,10 +126,9 @@ void newConnectionOnClose() { connection2Endpoint.next(AmqpEndpointState.ACTIVE); when(connection2.getEndpointStates()).thenReturn(connection2EndpointProcessor); + when(connection2.closeAsync()).thenReturn(Mono.empty()); // Act & Assert - - // Verify that we get the first connection. StepVerifier.create(processor) .then(() -> endpointSink.next(AmqpEndpointState.ACTIVE)) .expectNext(connection) diff --git a/sdk/eventhubs/azure-messaging-eventhubs/src/test/java/com/azure/messaging/eventhubs/implementation/EventHubReactorConnectionTest.java b/sdk/eventhubs/azure-messaging-eventhubs/src/test/java/com/azure/messaging/eventhubs/implementation/EventHubReactorConnectionTest.java index 70a02f5f999e7..478b8aff67c51 100644 --- a/sdk/eventhubs/azure-messaging-eventhubs/src/test/java/com/azure/messaging/eventhubs/implementation/EventHubReactorConnectionTest.java +++ b/sdk/eventhubs/azure-messaging-eventhubs/src/test/java/com/azure/messaging/eventhubs/implementation/EventHubReactorConnectionTest.java @@ -115,8 +115,9 @@ public void setup() throws IOException { final ProxyOptions proxy = ProxyOptions.SYSTEM_DEFAULTS; this.connectionOptions = new ConnectionOptions(HOSTNAME, tokenCredential, - CbsAuthorizationType.SHARED_ACCESS_SIGNATURE, AmqpTransportType.AMQP, new AmqpRetryOptions(), proxy, - scheduler, clientOptions, SslDomain.VerifyMode.VERIFY_PEER_NAME, "product-test", + CbsAuthorizationType.SHARED_ACCESS_SIGNATURE, ClientConstants.AZURE_ACTIVE_DIRECTORY_SCOPE, + AmqpTransportType.AMQP, new AmqpRetryOptions(), proxy, scheduler, clientOptions, + SslDomain.VerifyMode.VERIFY_PEER_NAME, "product-test", "client-test-version"); final SslPeerDetails peerDetails = Proton.sslPeerDetails(HOSTNAME, ConnectionHandler.AMQPS_PORT); diff --git a/sdk/servicebus/azure-messaging-servicebus/src/main/java/com/azure/messaging/servicebus/ServiceBusClientBuilder.java b/sdk/servicebus/azure-messaging-servicebus/src/main/java/com/azure/messaging/servicebus/ServiceBusClientBuilder.java index 110619cac5a77..1da1431a127a0 100644 --- a/sdk/servicebus/azure-messaging-servicebus/src/main/java/com/azure/messaging/servicebus/ServiceBusClientBuilder.java +++ b/sdk/servicebus/azure-messaging-servicebus/src/main/java/com/azure/messaging/servicebus/ServiceBusClientBuilder.java @@ -388,7 +388,7 @@ private ServiceBusConnectionProcessor getOrCreateConnectionProcessor(MessageSeri final ReactorHandlerProvider handlerProvider = new ReactorHandlerProvider(provider); final TokenManagerProvider tokenManagerProvider = new AzureTokenManagerProvider( connectionOptions.getAuthorizationType(), connectionOptions.getFullyQualifiedNamespace(), - ServiceBusConstants.AZURE_ACTIVE_DIRECTORY_SCOPE); + connectionOptions.getAuthorizationScope()); return (ServiceBusAmqpConnection) new ServiceBusReactorAmqpConnection(connectionId, connectionOptions, provider, handlerProvider, tokenManagerProvider, serializer); @@ -439,8 +439,9 @@ private ConnectionOptions getConnectionOptions() { final String product = properties.getOrDefault(NAME_KEY, UNKNOWN); final String clientVersion = properties.getOrDefault(VERSION_KEY, UNKNOWN); - return new ConnectionOptions(fullyQualifiedNamespace, credentials, authorizationType, transport, retryOptions, - proxyOptions, scheduler, options, verificationMode, product, clientVersion); + return new ConnectionOptions(fullyQualifiedNamespace, credentials, authorizationType, + ServiceBusConstants.AZURE_ACTIVE_DIRECTORY_SCOPE, transport, retryOptions, proxyOptions, scheduler, + options, verificationMode, product, clientVersion); } private ProxyOptions getDefaultProxyConfiguration(Configuration configuration) { diff --git a/sdk/servicebus/azure-messaging-servicebus/src/main/java/com/azure/messaging/servicebus/implementation/ServiceBusReceiveLinkProcessor.java b/sdk/servicebus/azure-messaging-servicebus/src/main/java/com/azure/messaging/servicebus/implementation/ServiceBusReceiveLinkProcessor.java index 4d7f18b4902d7..f1ee95d2e1b2e 100644 --- a/sdk/servicebus/azure-messaging-servicebus/src/main/java/com/azure/messaging/servicebus/implementation/ServiceBusReceiveLinkProcessor.java +++ b/sdk/servicebus/azure-messaging-servicebus/src/main/java/com/azure/messaging/servicebus/implementation/ServiceBusReceiveLinkProcessor.java @@ -595,11 +595,7 @@ private void disposeReceiver(AmqpReceiveLink link) { } try { - if (link instanceof AsyncCloseable) { - ((AsyncCloseable) link).closeAsync().subscribe(); - } else { - link.dispose(); - } + ((AsyncCloseable) link).closeAsync().subscribe(); } catch (Exception error) { logger.warning("linkName[{}] entityPath[{}] Unable to dispose of link.", link.getLinkName(), link.getEntityPath(), error); diff --git a/sdk/servicebus/azure-messaging-servicebus/src/test/java/com/azure/messaging/servicebus/ServiceBusReceiverAsyncClientTest.java b/sdk/servicebus/azure-messaging-servicebus/src/test/java/com/azure/messaging/servicebus/ServiceBusReceiverAsyncClientTest.java index bd7207a2dc972..293369a8f9bb8 100644 --- a/sdk/servicebus/azure-messaging-servicebus/src/test/java/com/azure/messaging/servicebus/ServiceBusReceiverAsyncClientTest.java +++ b/sdk/servicebus/azure-messaging-servicebus/src/test/java/com/azure/messaging/servicebus/ServiceBusReceiverAsyncClientTest.java @@ -24,6 +24,7 @@ import com.azure.messaging.servicebus.implementation.MessagingEntityType; import com.azure.messaging.servicebus.implementation.ServiceBusAmqpConnection; import com.azure.messaging.servicebus.implementation.ServiceBusConnectionProcessor; +import com.azure.messaging.servicebus.implementation.ServiceBusConstants; import com.azure.messaging.servicebus.implementation.ServiceBusManagementNode; import com.azure.messaging.servicebus.implementation.ServiceBusReactorReceiver; import com.azure.messaging.servicebus.models.AbandonOptions; @@ -166,9 +167,9 @@ void setup(TestInfo testInfo) { when(sessionReceiveLink.addCredits(anyInt())).thenReturn(Mono.empty()); ConnectionOptions connectionOptions = new ConnectionOptions(NAMESPACE, tokenCredential, - CbsAuthorizationType.SHARED_ACCESS_SIGNATURE, AmqpTransportType.AMQP, new AmqpRetryOptions(), - ProxyOptions.SYSTEM_DEFAULTS, Schedulers.boundedElastic(), CLIENT_OPTIONS, - SslDomain.VerifyMode.VERIFY_PEER_NAME, "test-product", "test-version"); + CbsAuthorizationType.SHARED_ACCESS_SIGNATURE, ServiceBusConstants.AZURE_ACTIVE_DIRECTORY_SCOPE, + AmqpTransportType.AMQP, new AmqpRetryOptions(), ProxyOptions.SYSTEM_DEFAULTS, Schedulers.boundedElastic(), + CLIENT_OPTIONS, SslDomain.VerifyMode.VERIFY_PEER_NAME, "test-product", "test-version"); when(connection.getEndpointStates()).thenReturn(endpointProcessor); endpointSink.next(AmqpEndpointState.ACTIVE); diff --git a/sdk/servicebus/azure-messaging-servicebus/src/test/java/com/azure/messaging/servicebus/ServiceBusSenderAsyncClientTest.java b/sdk/servicebus/azure-messaging-servicebus/src/test/java/com/azure/messaging/servicebus/ServiceBusSenderAsyncClientTest.java index 50f1699f1f2ff..01e39e8d95e07 100644 --- a/sdk/servicebus/azure-messaging-servicebus/src/test/java/com/azure/messaging/servicebus/ServiceBusSenderAsyncClientTest.java +++ b/sdk/servicebus/azure-messaging-servicebus/src/test/java/com/azure/messaging/servicebus/ServiceBusSenderAsyncClientTest.java @@ -24,6 +24,7 @@ import com.azure.messaging.servicebus.implementation.MessagingEntityType; import com.azure.messaging.servicebus.implementation.ServiceBusAmqpConnection; import com.azure.messaging.servicebus.implementation.ServiceBusConnectionProcessor; +import com.azure.messaging.servicebus.implementation.ServiceBusConstants; import com.azure.messaging.servicebus.implementation.ServiceBusManagementNode; import com.azure.messaging.servicebus.models.CreateMessageBatchOptions; import org.apache.qpid.proton.amqp.messaging.Section; @@ -153,9 +154,9 @@ void setup() { MockitoAnnotations.initMocks(this); connectionOptions = new ConnectionOptions(NAMESPACE, tokenCredential, - CbsAuthorizationType.SHARED_ACCESS_SIGNATURE, AmqpTransportType.AMQP, retryOptions, - ProxyOptions.SYSTEM_DEFAULTS, Schedulers.parallel(), CLIENT_OPTIONS, SslDomain.VerifyMode.VERIFY_PEER_NAME, - "test-product", "test-version"); + CbsAuthorizationType.SHARED_ACCESS_SIGNATURE, ServiceBusConstants.AZURE_ACTIVE_DIRECTORY_SCOPE, + AmqpTransportType.AMQP, retryOptions, ProxyOptions.SYSTEM_DEFAULTS, Schedulers.parallel(), CLIENT_OPTIONS, + SslDomain.VerifyMode.VERIFY_PEER_NAME, "test-product", "test-version"); when(connection.getEndpointStates()).thenReturn(endpointProcessor); endpointSink.next(AmqpEndpointState.ACTIVE); diff --git a/sdk/servicebus/azure-messaging-servicebus/src/test/java/com/azure/messaging/servicebus/ServiceBusSessionManagerTest.java b/sdk/servicebus/azure-messaging-servicebus/src/test/java/com/azure/messaging/servicebus/ServiceBusSessionManagerTest.java index cc58b9c3e46e9..9d26ff6d04867 100644 --- a/sdk/servicebus/azure-messaging-servicebus/src/test/java/com/azure/messaging/servicebus/ServiceBusSessionManagerTest.java +++ b/sdk/servicebus/azure-messaging-servicebus/src/test/java/com/azure/messaging/servicebus/ServiceBusSessionManagerTest.java @@ -17,6 +17,7 @@ import com.azure.messaging.servicebus.implementation.MessagingEntityType; import com.azure.messaging.servicebus.implementation.ServiceBusAmqpConnection; import com.azure.messaging.servicebus.implementation.ServiceBusConnectionProcessor; +import com.azure.messaging.servicebus.implementation.ServiceBusConstants; import com.azure.messaging.servicebus.implementation.ServiceBusManagementNode; import com.azure.messaging.servicebus.implementation.ServiceBusReceiveLink; import com.azure.messaging.servicebus.models.ServiceBusReceiveMode; @@ -127,9 +128,10 @@ void beforeEach(TestInfo testInfo) { when(amqpReceiveLink.closeAsync()).thenReturn(Mono.empty()); ConnectionOptions connectionOptions = new ConnectionOptions(NAMESPACE, tokenCredential, - CbsAuthorizationType.SHARED_ACCESS_SIGNATURE, AmqpTransportType.AMQP, - new AmqpRetryOptions().setTryTimeout(TIMEOUT), ProxyOptions.SYSTEM_DEFAULTS, Schedulers.boundedElastic(), - CLIENT_OPTIONS, SslDomain.VerifyMode.VERIFY_PEER_NAME, "test-product", "test-version"); + CbsAuthorizationType.SHARED_ACCESS_SIGNATURE, ServiceBusConstants.AZURE_ACTIVE_DIRECTORY_SCOPE, + AmqpTransportType.AMQP, new AmqpRetryOptions().setTryTimeout(TIMEOUT), ProxyOptions.SYSTEM_DEFAULTS, + Schedulers.boundedElastic(), CLIENT_OPTIONS, SslDomain.VerifyMode.VERIFY_PEER_NAME, + "test-product", "test-version"); when(connection.getEndpointStates()).thenReturn(endpointProcessor); endpointSink.next(AmqpEndpointState.ACTIVE); diff --git a/sdk/servicebus/azure-messaging-servicebus/src/test/java/com/azure/messaging/servicebus/ServiceBusSessionReceiverAsyncClientTest.java b/sdk/servicebus/azure-messaging-servicebus/src/test/java/com/azure/messaging/servicebus/ServiceBusSessionReceiverAsyncClientTest.java index 60710582adddb..beae7284d4678 100644 --- a/sdk/servicebus/azure-messaging-servicebus/src/test/java/com/azure/messaging/servicebus/ServiceBusSessionReceiverAsyncClientTest.java +++ b/sdk/servicebus/azure-messaging-servicebus/src/test/java/com/azure/messaging/servicebus/ServiceBusSessionReceiverAsyncClientTest.java @@ -17,6 +17,7 @@ import com.azure.messaging.servicebus.implementation.MessagingEntityType; import com.azure.messaging.servicebus.implementation.ServiceBusAmqpConnection; import com.azure.messaging.servicebus.implementation.ServiceBusConnectionProcessor; +import com.azure.messaging.servicebus.implementation.ServiceBusConstants; import com.azure.messaging.servicebus.implementation.ServiceBusManagementNode; import com.azure.messaging.servicebus.implementation.ServiceBusReceiveLink; import com.azure.messaging.servicebus.models.ServiceBusReceiveMode; @@ -114,9 +115,10 @@ void beforeEach(TestInfo testInfo) { when(amqpReceiveLink.addCredits(anyInt())).thenReturn(Mono.empty()); ConnectionOptions connectionOptions = new ConnectionOptions(NAMESPACE, tokenCredential, - CbsAuthorizationType.SHARED_ACCESS_SIGNATURE, AmqpTransportType.AMQP, - new AmqpRetryOptions().setTryTimeout(TIMEOUT), ProxyOptions.SYSTEM_DEFAULTS, Schedulers.boundedElastic(), - CLIENT_OPTIONS, SslDomain.VerifyMode.VERIFY_PEER_NAME, "test-product", "test-version"); + CbsAuthorizationType.SHARED_ACCESS_SIGNATURE, ServiceBusConstants.AZURE_ACTIVE_DIRECTORY_SCOPE, + AmqpTransportType.AMQP, new AmqpRetryOptions().setTryTimeout(TIMEOUT), ProxyOptions.SYSTEM_DEFAULTS, + Schedulers.boundedElastic(), CLIENT_OPTIONS, SslDomain.VerifyMode.VERIFY_PEER_NAME, + "test-product", "test-version"); when(connection.getEndpointStates()).thenReturn(endpointProcessor); endpointSink.next(AmqpEndpointState.ACTIVE); @@ -124,6 +126,8 @@ void beforeEach(TestInfo testInfo) { when(connection.getManagementNode(ENTITY_PATH, ENTITY_TYPE)) .thenReturn(Mono.just(managementNode)); + when(connection.closeAsync()).thenReturn(Mono.empty()); + connectionProcessor = Flux.create(sink -> sink.next(connection)) .subscribeWith(new ServiceBusConnectionProcessor(connectionOptions.getFullyQualifiedNamespace(),