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/core/azure-core-management/src/main/java/com/azure/core/management/http/policy/ArmChallengeAuthenticationPolicy.java b/sdk/core/azure-core-management/src/main/java/com/azure/core/management/http/policy/ArmChallengeAuthenticationPolicy.java index 3b74abbcd487c..599f18db6537d 100644 --- a/sdk/core/azure-core-management/src/main/java/com/azure/core/management/http/policy/ArmChallengeAuthenticationPolicy.java +++ b/sdk/core/azure-core-management/src/main/java/com/azure/core/management/http/policy/ArmChallengeAuthenticationPolicy.java @@ -9,6 +9,7 @@ import com.azure.core.http.HttpResponse; import com.azure.core.http.policy.BearerTokenAuthenticationPolicy; import com.azure.core.management.implementation.http.AuthenticationChallenge; +import com.azure.core.util.CoreUtils; import reactor.core.publisher.Mono; import java.nio.charset.StandardCharsets; @@ -98,8 +99,15 @@ public Mono authorizeRequestOnChallenge(HttpPipelineCallContext context }); } - protected String[] getScopes(HttpPipelineCallContext context, String[] scopes) { - return scopes; + /** + * Gets the scopes for the specific request. + * + * @param context The request. + * @param scopes Default scopes used by the policy. + * @return The scopes for the specific request. + */ + public String[] getScopes(HttpPipelineCallContext context, String[] scopes) { + return CoreUtils.clone(scopes); } List parseChallenges(String header) { diff --git a/sdk/core/azure-core/src/main/java/com/azure/core/http/HttpRange.java b/sdk/core/azure-core/src/main/java/com/azure/core/http/HttpRange.java index 293b02940a2f5..5f9743d8a40f2 100644 --- a/sdk/core/azure-core/src/main/java/com/azure/core/http/HttpRange.java +++ b/sdk/core/azure-core/src/main/java/com/azure/core/http/HttpRange.java @@ -3,6 +3,7 @@ package com.azure.core.http; +import com.azure.core.annotation.Immutable; import com.azure.core.util.logging.ClientLogger; import java.util.Objects; @@ -15,6 +16,7 @@ *

* If {@link #getLength() length} is unspecified, null, then the range extends to the end of the HTTP resource. */ +@Immutable public final class HttpRange { private final long offset; private final Long length; diff --git a/sdk/cosmos/azure-cosmos-spark_3-1_2-12/CHANGELOG.md b/sdk/cosmos/azure-cosmos-spark_3-1_2-12/CHANGELOG.md index a18590f591650..66680eb6342f1 100644 --- a/sdk/cosmos/azure-cosmos-spark_3-1_2-12/CHANGELOG.md +++ b/sdk/cosmos/azure-cosmos-spark_3-1_2-12/CHANGELOG.md @@ -1,6 +1,8 @@ ## Release History ### 4.2.0-beta.1 (Unreleased) +#### Configuration Changes +* Changed the default value of `spark.cosmos.read.inferSchema.forceNullableProperties` from `false` to `true` based on user feedback, see [PR](https://github.com/Azure/azure-sdk-for-java/pull/22049). ### 4.1.0 (2021-05-27) #### New Features diff --git a/sdk/cosmos/azure-cosmos-spark_3-1_2-12/docs/configuration-reference.md b/sdk/cosmos/azure-cosmos-spark_3-1_2-12/docs/configuration-reference.md index 3a0afb9a4a09a..40265f5a4179d 100644 --- a/sdk/cosmos/azure-cosmos-spark_3-1_2-12/docs/configuration-reference.md +++ b/sdk/cosmos/azure-cosmos-spark_3-1_2-12/docs/configuration-reference.md @@ -48,7 +48,7 @@ When doing read operations, users can specify a custom schema or allow the conne | `spark.cosmos.read.inferSchema.samplingSize` | `1000` | Sampling size to use when inferring schema and not using a query. | | `spark.cosmos.read.inferSchema.includeSystemProperties` | `false` | When schema inference is enabled, whether the resulting schema will include all [Cosmos DB system properties](https://docs.microsoft.com/azure/cosmos-db/account-databases-containers-items#properties-of-an-item). | | `spark.cosmos.read.inferSchema.includeTimestamp` | `false` | When schema inference is enabled, whether the resulting schema will include the document Timestamp (`_ts`). Not required if `spark.cosmos.read.inferSchema.includeSystemProperties` is enabled, as it will already include all system properties. | -| `spark.cosmos.read.inferSchema.forceNullableProperties` | `false` | When schema inference is enabled, whether the resulting schema will make all columns nullable. By default whether inferred columns are treated as nullable or not will depend on whether any record in the sample set has null-values within a column. If set to `true` all columns will be treated as nullable even if all rows within the sample set have non-null values. | +| `spark.cosmos.read.inferSchema.forceNullableProperties` | `true` | When schema inference is enabled, whether the resulting schema will make all columns nullable. By default, all columns (except cosmos system properties) will be treated as nullable even if all rows within the sample set have non-null values. When disabled, the inferred columns are treated as nullable or not depending on whether any record in the sample set has null-values within a column. | #### Json conversion configuration diff --git a/sdk/cosmos/azure-cosmos-spark_3-1_2-12/src/main/scala/com/azure/cosmos/spark/CosmosConfig.scala b/sdk/cosmos/azure-cosmos-spark_3-1_2-12/src/main/scala/com/azure/cosmos/spark/CosmosConfig.scala index 38ded7ceb95d5..48f1152bde45f 100644 --- a/sdk/cosmos/azure-cosmos-spark_3-1_2-12/src/main/scala/com/azure/cosmos/spark/CosmosConfig.scala +++ b/sdk/cosmos/azure-cosmos-spark_3-1_2-12/src/main/scala/com/azure/cosmos/spark/CosmosConfig.scala @@ -533,7 +533,7 @@ private object CosmosSchemaInferenceConfig { private val inferSchemaForceNullableProperties = CosmosConfigEntry[Boolean]( key = CosmosConfigNames.ReadInferSchemaForceNullableProperties, mandatory = false, - defaultValue = Some(false), + defaultValue = Some(true), parseFromStringFunction = include => include.toBoolean, helpMessage = "Whether schema inference should enforce inferred properties to be nullable - even when no null-values are contained in the sample set") diff --git a/sdk/cosmos/azure-cosmos-spark_3-1_2-12/src/test/scala/com/azure/cosmos/spark/SparkE2EQueryITest.scala b/sdk/cosmos/azure-cosmos-spark_3-1_2-12/src/test/scala/com/azure/cosmos/spark/SparkE2EQueryITest.scala index d16cd05e6d79c..bae610c7bf0eb 100644 --- a/sdk/cosmos/azure-cosmos-spark_3-1_2-12/src/test/scala/com/azure/cosmos/spark/SparkE2EQueryITest.scala +++ b/sdk/cosmos/azure-cosmos-spark_3-1_2-12/src/test/scala/com/azure/cosmos/spark/SparkE2EQueryITest.scala @@ -460,6 +460,57 @@ class SparkE2EQueryITest fieldNames.contains(CosmosTableSchemaInferrer.AttachmentsAttributeName) shouldBe false } + "spark query" can "when forceNullableProperties is false and rows have different schema" in { + val cosmosEndpoint = TestConfigurations.HOST + val cosmosMasterKey = TestConfigurations.MASTER_KEY + val samplingSize = 100 + val expectedResults = samplingSize * 2 + val container = cosmosClient.getDatabase(cosmosDatabase).getContainer(cosmosContainer) + + // Inserting documents with slightly different schema + for( _ <- 1 to expectedResults) { + val objectNode = Utils.getSimpleObjectMapper.createObjectNode() + val arr = objectNode.putArray("object_array") + val nested = Utils.getSimpleObjectMapper.createObjectNode() + nested.put("A", "test") + nested.put("B", "test") + arr.add(nested) + objectNode.put("id", UUID.randomUUID().toString) + container.createItem(objectNode).block() + } + + for( _ <- 1 to samplingSize) { + val objectNode2 = Utils.getSimpleObjectMapper.createObjectNode() + val arr = objectNode2.putArray("object_array") + val nested = Utils.getSimpleObjectMapper.createObjectNode() + nested.put("A", "test") + arr.add(nested) + objectNode2.put("id", UUID.randomUUID().toString) + container.createItem(objectNode2).block() + } + + val cfgWithInference = Map("spark.cosmos.accountEndpoint" -> cosmosEndpoint, + "spark.cosmos.accountKey" -> cosmosMasterKey, + "spark.cosmos.database" -> cosmosDatabase, + "spark.cosmos.container" -> cosmosContainer, + "spark.cosmos.read.inferSchema.enabled" -> "true", + "spark.cosmos.read.inferSchema.forceNullableProperties" -> "false", + "spark.cosmos.read.inferSchema.samplingSize" -> samplingSize.toString, + "spark.cosmos.read.inferSchema.query" -> "SELECT * FROM c ORDER BY c._ts", + "spark.cosmos.read.partitioning.strategy" -> "Restrictive" + ) + + val dfWithInference = spark.read.format("cosmos.oltp").options(cfgWithInference).load() + try { + dfWithInference.collect() + fail("Should have thrown an exception") + } + catch { + case inner: Exception => + inner.toString.contains("The 1th field 'B' of input row cannot be null") shouldBe true + } + } + "spark query" can "use custom sampling size" in { val cosmosEndpoint = TestConfigurations.HOST val cosmosMasterKey = TestConfigurations.MASTER_KEY @@ -580,6 +631,7 @@ class SparkE2EQueryITest "spark.cosmos.accountKey" -> cosmosMasterKey, "spark.cosmos.database" -> cosmosDatabase, "spark.cosmos.container" -> cosmosContainer, + "spark.cosmos.read.inferSchema.forceNullableProperties" -> "false", "spark.cosmos.read.partitioning.strategy" -> "Restrictive" ) @@ -652,6 +704,62 @@ class SparkE2EQueryITest fieldNames.contains(CosmosTableSchemaInferrer.AttachmentsAttributeName) shouldBe false } + "spark query" can "return proper Cosmos specific query plan on explain with nullable properties" in { + val cosmosEndpoint = TestConfigurations.HOST + val cosmosMasterKey = TestConfigurations.MASTER_KEY + + val id = UUID.randomUUID().toString + + val rawItem = s""" + | { + | "id" : "${id}", + | "nestedObject" : { + | "prop1" : 5, + | "prop2" : "6" + | } + | } + |""".stripMargin + + val objectNode = objectMapper.readValue(rawItem, classOf[ObjectNode]) + + val container = cosmosClient.getDatabase(cosmosDatabase).getContainer(cosmosContainer) + container.createItem(objectNode).block() + + val cfg = Map("spark.cosmos.accountEndpoint" -> cosmosEndpoint, + "spark.cosmos.accountKey" -> cosmosMasterKey, + "spark.cosmos.database" -> cosmosDatabase, + "spark.cosmos.container" -> cosmosContainer, + "spark.cosmos.read.inferSchema.forceNullableProperties" -> "true", + "spark.cosmos.read.partitioning.strategy" -> "Restrictive" + ) + + val df = spark.read.format("cosmos.oltp").options(cfg).load() + val rowsArray = df.where("nestedObject.prop2 = '6'").collect() + rowsArray should have size 1 + + var output = new java.io.ByteArrayOutputStream() + Console.withOut(output) { + df.explain() + } + var queryPlan = output.toString.replaceAll("#\\d+", "#x") + logInfo(s"Query Plan: $queryPlan") + queryPlan.contains("Cosmos Query: SELECT * FROM r") shouldEqual true + + output = new java.io.ByteArrayOutputStream() + Console.withOut(output) { + df.where("nestedObject.prop2 = '6'").explain() + } + queryPlan = output.toString.replaceAll("#\\d+", "#x") + logInfo(s"Query Plan: $queryPlan") + val expected = s"Cosmos Query: SELECT * FROM r WHERE NOT(IS_NULL(r['nestedObject'])) " + + s"AND r['nestedObject']['prop2']=" + + s"@param0${System.getProperty("line.separator")} > param: @param0 = 6" + queryPlan.contains(expected) shouldEqual true + + val item = rowsArray(0) + item.getAs[String]("id") shouldEqual id + } + //scalastyle:on magic.number //scalastyle:on multiple.string.literals } 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/resourcemanager/azure-resourcemanager-resources/src/main/java/com/azure/resourcemanager/resources/fluentcore/policy/AuthenticationPolicy.java b/sdk/resourcemanager/azure-resourcemanager-resources/src/main/java/com/azure/resourcemanager/resources/fluentcore/policy/AuthenticationPolicy.java index ae8a4c06a37cc..fade979209d55 100644 --- a/sdk/resourcemanager/azure-resourcemanager-resources/src/main/java/com/azure/resourcemanager/resources/fluentcore/policy/AuthenticationPolicy.java +++ b/sdk/resourcemanager/azure-resourcemanager-resources/src/main/java/com/azure/resourcemanager/resources/fluentcore/policy/AuthenticationPolicy.java @@ -30,7 +30,7 @@ public AuthenticationPolicy(TokenCredential credential, AzureEnvironment environ } @Override - protected String[] getScopes(HttpPipelineCallContext context, String[] scopes) { + public String[] getScopes(HttpPipelineCallContext context, String[] scopes) { if (CoreUtils.isNullOrEmpty(scopes)) { scopes = new String[1]; scopes[0] = ResourceManagerUtils.getDefaultScopeFromRequest(context.getHttpRequest(), environment); 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(),