diff --git a/driver/src/main/java/org/neo4j/driver/Config.java b/driver/src/main/java/org/neo4j/driver/Config.java index 5e6d8a3914..a77c81396b 100644 --- a/driver/src/main/java/org/neo4j/driver/Config.java +++ b/driver/src/main/java/org/neo4j/driver/Config.java @@ -20,6 +20,7 @@ import static java.lang.String.format; import static org.neo4j.driver.internal.logging.DevNullLogging.DEV_NULL_LOGGING; +import static org.neo4j.driver.internal.util.DriverInfoUtil.driverVersion; import java.io.File; import java.io.Serial; @@ -748,23 +749,6 @@ public ConfigBuilder withNotificationConfig(NotificationConfig notificationConfi return this; } - /** - * Extracts the driver version from the driver jar MANIFEST.MF file. - */ - private static String driverVersion() { - // "Session" is arbitrary - the only thing that matters is that the class we use here is in the - // 'org.neo4j.driver' package, because that is where the jar manifest specifies the version. - // This is done as part of the build, adding a MANIFEST.MF file to the generated jarfile. - Package pkg = Session.class.getPackage(); - if (pkg != null && pkg.getImplementationVersion() != null) { - return pkg.getImplementationVersion(); - } - - // If there is no version, we're not running from a jar file, but from raw compiled class files. - // This should only happen during development, so call the version 'dev'. - return "dev"; - } - /** * Create a config instance from this builder. * diff --git a/driver/src/main/java/org/neo4j/driver/internal/BoltAgent.java b/driver/src/main/java/org/neo4j/driver/internal/BoltAgent.java new file mode 100644 index 0000000000..d440371162 --- /dev/null +++ b/driver/src/main/java/org/neo4j/driver/internal/BoltAgent.java @@ -0,0 +1,21 @@ +/* + * Copyright (c) "Neo4j" + * Neo4j Sweden AB [http://neo4j.com] + * + * This file is part of Neo4j. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.neo4j.driver.internal; + +public record BoltAgent(String product, String platform, String language, String languageDetails) {} diff --git a/driver/src/main/java/org/neo4j/driver/internal/DriverFactory.java b/driver/src/main/java/org/neo4j/driver/internal/DriverFactory.java index 5b60d7a317..70156e8952 100644 --- a/driver/src/main/java/org/neo4j/driver/internal/DriverFactory.java +++ b/driver/src/main/java/org/neo4j/driver/internal/DriverFactory.java @@ -61,6 +61,7 @@ import org.neo4j.driver.internal.security.StaticAuthTokenManager; import org.neo4j.driver.internal.spi.ConnectionPool; import org.neo4j.driver.internal.spi.ConnectionProvider; +import org.neo4j.driver.internal.util.DriverInfoUtil; import org.neo4j.driver.internal.util.Futures; import org.neo4j.driver.net.ServerAddressResolver; @@ -141,7 +142,8 @@ protected ConnectionPool createConnectionPool( Clock clock = createClock(); ConnectionSettings settings = new ConnectionSettings(authTokenManager, config.userAgent(), config.connectionTimeoutMillis()); - ChannelConnector connector = createConnector(settings, securityPlan, config, clock, routingContext); + var boltAgent = DriverInfoUtil.boltAgent(); + ChannelConnector connector = createConnector(settings, securityPlan, config, clock, routingContext, boltAgent); PoolSettings poolSettings = new PoolSettings( config.maxConnectionPoolSize(), config.connectionAcquisitionTimeoutMillis(), @@ -179,7 +181,8 @@ protected ChannelConnector createConnector( SecurityPlan securityPlan, Config config, Clock clock, - RoutingContext routingContext) { + RoutingContext routingContext, + BoltAgent boltAgent) { return new ChannelConnectorImpl( settings, securityPlan, @@ -187,7 +190,8 @@ protected ChannelConnector createConnector( clock, routingContext, getDomainNameResolver(), - config.notificationConfig()); + config.notificationConfig(), + boltAgent); } private InternalDriver createDriver( diff --git a/driver/src/main/java/org/neo4j/driver/internal/async/connection/BoltProtocolUtil.java b/driver/src/main/java/org/neo4j/driver/internal/async/connection/BoltProtocolUtil.java index c87d2cab7e..b41ec92d7f 100644 --- a/driver/src/main/java/org/neo4j/driver/internal/async/connection/BoltProtocolUtil.java +++ b/driver/src/main/java/org/neo4j/driver/internal/async/connection/BoltProtocolUtil.java @@ -29,7 +29,7 @@ import org.neo4j.driver.internal.messaging.v42.BoltProtocolV42; import org.neo4j.driver.internal.messaging.v44.BoltProtocolV44; import org.neo4j.driver.internal.messaging.v5.BoltProtocolV5; -import org.neo4j.driver.internal.messaging.v52.BoltProtocolV52; +import org.neo4j.driver.internal.messaging.v53.BoltProtocolV53; public final class BoltProtocolUtil { public static final int BOLT_MAGIC_PREAMBLE = 0x6060B017; @@ -41,7 +41,7 @@ public final class BoltProtocolUtil { private static final ByteBuf HANDSHAKE_BUF = unreleasableBuffer(copyInt( BOLT_MAGIC_PREAMBLE, - BoltProtocolV52.VERSION.toIntRange(BoltProtocolV5.VERSION), + BoltProtocolV53.VERSION.toIntRange(BoltProtocolV5.VERSION), BoltProtocolV44.VERSION.toIntRange(BoltProtocolV42.VERSION), BoltProtocolV41.VERSION.toInt(), BoltProtocolV3.VERSION.toInt())) diff --git a/driver/src/main/java/org/neo4j/driver/internal/async/connection/ChannelConnectorImpl.java b/driver/src/main/java/org/neo4j/driver/internal/async/connection/ChannelConnectorImpl.java index a2fd833ec5..379397130d 100644 --- a/driver/src/main/java/org/neo4j/driver/internal/async/connection/ChannelConnectorImpl.java +++ b/driver/src/main/java/org/neo4j/driver/internal/async/connection/ChannelConnectorImpl.java @@ -33,6 +33,7 @@ import org.neo4j.driver.AuthTokenManager; import org.neo4j.driver.Logging; import org.neo4j.driver.NotificationConfig; +import org.neo4j.driver.internal.BoltAgent; import org.neo4j.driver.internal.BoltServerAddress; import org.neo4j.driver.internal.ConnectionSettings; import org.neo4j.driver.internal.DomainNameResolver; @@ -42,6 +43,7 @@ public class ChannelConnectorImpl implements ChannelConnector { private final String userAgent; + private final BoltAgent boltAgent; private final AuthTokenManager authTokenManager; private final RoutingContext routingContext; private final SecurityPlan securityPlan; @@ -60,7 +62,8 @@ public ChannelConnectorImpl( Clock clock, RoutingContext routingContext, DomainNameResolver domainNameResolver, - NotificationConfig notificationConfig) { + NotificationConfig notificationConfig, + BoltAgent boltAgent) { this( connectionSettings, securityPlan, @@ -69,7 +72,8 @@ public ChannelConnectorImpl( clock, routingContext, domainNameResolver, - notificationConfig); + notificationConfig, + boltAgent); } public ChannelConnectorImpl( @@ -80,8 +84,10 @@ public ChannelConnectorImpl( Clock clock, RoutingContext routingContext, DomainNameResolver domainNameResolver, - NotificationConfig notificationConfig) { + NotificationConfig notificationConfig, + BoltAgent boltAgent) { this.userAgent = connectionSettings.userAgent(); + this.boltAgent = requireNonNull(boltAgent); this.authTokenManager = connectionSettings.authTokenProvider(); this.routingContext = routingContext; this.connectTimeoutMillis = connectionSettings.connectTimeoutMillis(); @@ -145,6 +151,6 @@ private void installHandshakeCompletedListeners( // add listener that sends an INIT message. connection is now fully established. channel pipeline if fully // set to send/receive messages for a selected protocol version handshakeCompleted.addListener(new HandshakeCompletedListener( - userAgent, routingContext, connectionInitialized, notificationConfig, clock)); + userAgent, boltAgent, routingContext, connectionInitialized, notificationConfig, clock)); } } diff --git a/driver/src/main/java/org/neo4j/driver/internal/async/connection/HandshakeCompletedListener.java b/driver/src/main/java/org/neo4j/driver/internal/async/connection/HandshakeCompletedListener.java index 93da0778dd..43451ff40f 100644 --- a/driver/src/main/java/org/neo4j/driver/internal/async/connection/HandshakeCompletedListener.java +++ b/driver/src/main/java/org/neo4j/driver/internal/async/connection/HandshakeCompletedListener.java @@ -26,12 +26,14 @@ import io.netty.channel.ChannelPromise; import java.time.Clock; import org.neo4j.driver.NotificationConfig; +import org.neo4j.driver.internal.BoltAgent; import org.neo4j.driver.internal.cluster.RoutingContext; import org.neo4j.driver.internal.messaging.BoltProtocol; import org.neo4j.driver.internal.messaging.v51.BoltProtocolV51; public class HandshakeCompletedListener implements ChannelFutureListener { private final String userAgent; + private final BoltAgent boltAgent; private final RoutingContext routingContext; private final ChannelPromise connectionInitializedPromise; private final NotificationConfig notificationConfig; @@ -39,12 +41,14 @@ public class HandshakeCompletedListener implements ChannelFutureListener { public HandshakeCompletedListener( String userAgent, + BoltAgent boltAgent, RoutingContext routingContext, ChannelPromise connectionInitializedPromise, NotificationConfig notificationConfig, Clock clock) { requireNonNull(clock, "clock must not be null"); this.userAgent = requireNonNull(userAgent); + this.boltAgent = requireNonNull(boltAgent); this.routingContext = routingContext; this.connectionInitializedPromise = requireNonNull(connectionInitializedPromise); this.notificationConfig = notificationConfig; @@ -71,6 +75,7 @@ public void operationComplete(ChannelFuture future) { authContext.setValidToken(authToken); protocol.initializeChannel( userAgent, + boltAgent, authToken, routingContext, connectionInitializedPromise, @@ -81,7 +86,13 @@ public void operationComplete(ChannelFuture future) { channel.eventLoop()); } else { protocol.initializeChannel( - userAgent, null, routingContext, connectionInitializedPromise, notificationConfig, clock); + userAgent, + boltAgent, + null, + routingContext, + connectionInitializedPromise, + notificationConfig, + clock); } } else { connectionInitializedPromise.setFailure(future.cause()); diff --git a/driver/src/main/java/org/neo4j/driver/internal/messaging/BoltProtocol.java b/driver/src/main/java/org/neo4j/driver/internal/messaging/BoltProtocol.java index 799aba7d7d..226ab4bd69 100644 --- a/driver/src/main/java/org/neo4j/driver/internal/messaging/BoltProtocol.java +++ b/driver/src/main/java/org/neo4j/driver/internal/messaging/BoltProtocol.java @@ -34,6 +34,7 @@ import org.neo4j.driver.Transaction; import org.neo4j.driver.TransactionConfig; import org.neo4j.driver.exceptions.ClientException; +import org.neo4j.driver.internal.BoltAgent; import org.neo4j.driver.internal.DatabaseBookmark; import org.neo4j.driver.internal.async.UnmanagedTransaction; import org.neo4j.driver.internal.cluster.RoutingContext; @@ -47,6 +48,7 @@ import org.neo4j.driver.internal.messaging.v5.BoltProtocolV5; import org.neo4j.driver.internal.messaging.v51.BoltProtocolV51; import org.neo4j.driver.internal.messaging.v52.BoltProtocolV52; +import org.neo4j.driver.internal.messaging.v53.BoltProtocolV53; import org.neo4j.driver.internal.spi.Connection; public interface BoltProtocol { @@ -61,14 +63,16 @@ public interface BoltProtocol { * Initialize channel after it is connected and handshake selected this protocol version. * * @param userAgent the user agent string. + * @param boltAgent the bolt agent * @param authToken the authentication token. * @param routingContext the configured routing context * @param channelInitializedPromise the promise to be notified when initialization is completed. - * @param notificationConfig the notification configuration - * @param clock the clock to use + * @param notificationConfig the notification configuration + * @param clock the clock to use */ void initializeChannel( String userAgent, + BoltAgent boltAgent, AuthToken authToken, RoutingContext routingContext, ChannelPromise channelInitializedPromise, @@ -189,6 +193,8 @@ static BoltProtocol forVersion(BoltProtocolVersion version) { return BoltProtocolV51.INSTANCE; } else if (BoltProtocolV52.VERSION.equals(version)) { return BoltProtocolV52.INSTANCE; + } else if (BoltProtocolV53.VERSION.equals(version)) { + return BoltProtocolV53.INSTANCE; } throw new ClientException("Unknown protocol version: " + version); } diff --git a/driver/src/main/java/org/neo4j/driver/internal/messaging/request/HelloMessage.java b/driver/src/main/java/org/neo4j/driver/internal/messaging/request/HelloMessage.java index 846632350d..c9e0b7ce54 100644 --- a/driver/src/main/java/org/neo4j/driver/internal/messaging/request/HelloMessage.java +++ b/driver/src/main/java/org/neo4j/driver/internal/messaging/request/HelloMessage.java @@ -27,11 +27,17 @@ import java.util.Objects; import org.neo4j.driver.NotificationConfig; import org.neo4j.driver.Value; +import org.neo4j.driver.internal.BoltAgent; public class HelloMessage extends MessageWithMetadata { public static final byte SIGNATURE = 0x01; private static final String USER_AGENT_METADATA_KEY = "user_agent"; + private static final String BOLT_AGENT_METADATA_KEY = "bolt_agent"; + private static final String BOLT_AGENT_PRODUCT_KEY = "product"; + private static final String BOLT_AGENT_PLATFORM_KEY = "platform"; + private static final String BOLT_AGENT_LANGUAGE_KEY = "language"; + private static final String BOLT_AGENT_LANGUAGE_DETAIL_KEY = "language_details"; private static final String ROUTING_CONTEXT_METADATA_KEY = "routing"; private static final String PATCH_BOLT_METADATA_KEY = "patch_bolt"; @@ -39,11 +45,12 @@ public class HelloMessage extends MessageWithMetadata { public HelloMessage( String userAgent, + BoltAgent boltAgent, Map authToken, Map routingContext, boolean includeDateTimeUtc, NotificationConfig notificationConfig) { - super(buildMetadata(userAgent, authToken, routingContext, includeDateTimeUtc, notificationConfig)); + super(buildMetadata(userAgent, boltAgent, authToken, routingContext, includeDateTimeUtc, notificationConfig)); } @Override @@ -77,12 +84,29 @@ public String toString() { private static Map buildMetadata( String userAgent, + BoltAgent boltAgent, Map authToken, Map routingContext, boolean includeDateTimeUtc, NotificationConfig notificationConfig) { Map result = new HashMap<>(authToken); - result.put(USER_AGENT_METADATA_KEY, value(userAgent)); + if (userAgent != null) { + result.put(USER_AGENT_METADATA_KEY, value(userAgent)); + } + if (boltAgent != null) { + var boltAgentMap = new HashMap(); + boltAgentMap.put(BOLT_AGENT_PRODUCT_KEY, boltAgent.product()); + if (boltAgent.platform() != null) { + boltAgentMap.put(BOLT_AGENT_PLATFORM_KEY, boltAgent.platform()); + } + if (boltAgent.language() != null) { + boltAgentMap.put(BOLT_AGENT_LANGUAGE_KEY, boltAgent.language()); + } + if (boltAgent.languageDetails() != null) { + boltAgentMap.put(BOLT_AGENT_LANGUAGE_DETAIL_KEY, boltAgent.languageDetails()); + } + result.put(BOLT_AGENT_METADATA_KEY, value(boltAgentMap)); + } if (routingContext != null) { result.put(ROUTING_CONTEXT_METADATA_KEY, value(routingContext)); } diff --git a/driver/src/main/java/org/neo4j/driver/internal/messaging/v3/BoltProtocolV3.java b/driver/src/main/java/org/neo4j/driver/internal/messaging/v3/BoltProtocolV3.java index 3c9de0fa25..678a2877e5 100644 --- a/driver/src/main/java/org/neo4j/driver/internal/messaging/v3/BoltProtocolV3.java +++ b/driver/src/main/java/org/neo4j/driver/internal/messaging/v3/BoltProtocolV3.java @@ -40,6 +40,7 @@ import org.neo4j.driver.TransactionConfig; import org.neo4j.driver.exceptions.Neo4jException; import org.neo4j.driver.exceptions.UnsupportedFeatureException; +import org.neo4j.driver.internal.BoltAgent; import org.neo4j.driver.internal.DatabaseBookmark; import org.neo4j.driver.internal.DatabaseName; import org.neo4j.driver.internal.async.UnmanagedTransaction; @@ -81,6 +82,7 @@ public MessageFormat createMessageFormat() { @Override public void initializeChannel( String userAgent, + BoltAgent boltAgent, AuthToken authToken, RoutingContext routingContext, ChannelPromise channelInitializedPromise, @@ -97,6 +99,7 @@ public void initializeChannel( if (routingContext.isServerRoutingEnabled()) { message = new HelloMessage( userAgent, + null, ((InternalAuthToken) authToken).toMap(), routingContext.toMap(), includeDateTimeUtcPatchInHello(), @@ -104,6 +107,7 @@ public void initializeChannel( } else { message = new HelloMessage( userAgent, + null, ((InternalAuthToken) authToken).toMap(), null, includeDateTimeUtcPatchInHello(), diff --git a/driver/src/main/java/org/neo4j/driver/internal/messaging/v51/BoltProtocolV51.java b/driver/src/main/java/org/neo4j/driver/internal/messaging/v51/BoltProtocolV51.java index 8c7fa70288..7fb18182ab 100644 --- a/driver/src/main/java/org/neo4j/driver/internal/messaging/v51/BoltProtocolV51.java +++ b/driver/src/main/java/org/neo4j/driver/internal/messaging/v51/BoltProtocolV51.java @@ -27,6 +27,7 @@ import java.util.concurrent.CompletableFuture; import org.neo4j.driver.AuthToken; import org.neo4j.driver.NotificationConfig; +import org.neo4j.driver.internal.BoltAgent; import org.neo4j.driver.internal.cluster.RoutingContext; import org.neo4j.driver.internal.handlers.HelloV51ResponseHandler; import org.neo4j.driver.internal.messaging.BoltProtocol; @@ -42,6 +43,7 @@ public class BoltProtocolV51 extends BoltProtocolV5 { @Override public void initializeChannel( String userAgent, + BoltAgent boltAgent, AuthToken authToken, RoutingContext routingContext, ChannelPromise channelInitializedPromise, @@ -57,9 +59,9 @@ public void initializeChannel( if (routingContext.isServerRoutingEnabled()) { message = new HelloMessage( - userAgent, Collections.emptyMap(), routingContext.toMap(), false, notificationConfig); + userAgent, null, Collections.emptyMap(), routingContext.toMap(), false, notificationConfig); } else { - message = new HelloMessage(userAgent, Collections.emptyMap(), null, false, notificationConfig); + message = new HelloMessage(userAgent, null, Collections.emptyMap(), null, false, notificationConfig); } var helloFuture = new CompletableFuture(); diff --git a/driver/src/main/java/org/neo4j/driver/internal/messaging/v53/BoltProtocolV53.java b/driver/src/main/java/org/neo4j/driver/internal/messaging/v53/BoltProtocolV53.java new file mode 100644 index 0000000000..af8959f9ad --- /dev/null +++ b/driver/src/main/java/org/neo4j/driver/internal/messaging/v53/BoltProtocolV53.java @@ -0,0 +1,77 @@ +/* + * Copyright (c) "Neo4j" + * Neo4j Sweden AB [http://neo4j.com] + * + * This file is part of Neo4j. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.neo4j.driver.internal.messaging.v53; + +import static org.neo4j.driver.internal.async.connection.ChannelAttributes.messageDispatcher; +import static org.neo4j.driver.internal.async.connection.ChannelAttributes.setHelloStage; + +import io.netty.channel.ChannelPromise; +import java.time.Clock; +import java.util.Collections; +import java.util.concurrent.CompletableFuture; +import org.neo4j.driver.AuthToken; +import org.neo4j.driver.NotificationConfig; +import org.neo4j.driver.internal.BoltAgent; +import org.neo4j.driver.internal.cluster.RoutingContext; +import org.neo4j.driver.internal.handlers.HelloV51ResponseHandler; +import org.neo4j.driver.internal.messaging.BoltProtocol; +import org.neo4j.driver.internal.messaging.BoltProtocolVersion; +import org.neo4j.driver.internal.messaging.request.HelloMessage; +import org.neo4j.driver.internal.messaging.v52.BoltProtocolV52; + +public class BoltProtocolV53 extends BoltProtocolV52 { + public static final BoltProtocolVersion VERSION = new BoltProtocolVersion(5, 3); + public static final BoltProtocol INSTANCE = new BoltProtocolV53(); + + @Override + public void initializeChannel( + String userAgent, + BoltAgent boltAgent, + AuthToken authToken, + RoutingContext routingContext, + ChannelPromise channelInitializedPromise, + NotificationConfig notificationConfig, + Clock clock) { + var exception = verifyNotificationConfigSupported(notificationConfig); + if (exception != null) { + channelInitializedPromise.setFailure(exception); + return; + } + var channel = channelInitializedPromise.channel(); + HelloMessage message; + + if (routingContext.isServerRoutingEnabled()) { + message = new HelloMessage( + userAgent, boltAgent, Collections.emptyMap(), routingContext.toMap(), false, notificationConfig); + } else { + message = new HelloMessage(userAgent, boltAgent, Collections.emptyMap(), null, false, notificationConfig); + } + + var helloFuture = new CompletableFuture(); + setHelloStage(channel, helloFuture); + messageDispatcher(channel).enqueue(new HelloV51ResponseHandler(channel, helloFuture)); + channel.write(message, channel.voidPromise()); + channelInitializedPromise.setSuccess(); + } + + @Override + public BoltProtocolVersion version() { + return VERSION; + } +} diff --git a/driver/src/main/java/org/neo4j/driver/internal/util/DriverInfoUtil.java b/driver/src/main/java/org/neo4j/driver/internal/util/DriverInfoUtil.java new file mode 100644 index 0000000000..acafbf1bb8 --- /dev/null +++ b/driver/src/main/java/org/neo4j/driver/internal/util/DriverInfoUtil.java @@ -0,0 +1,88 @@ +/* + * Copyright (c) "Neo4j" + * Neo4j Sweden AB [http://neo4j.com] + * + * This file is part of Neo4j. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.neo4j.driver.internal.util; + +import static java.lang.String.format; + +import java.util.Optional; +import org.neo4j.driver.Session; +import org.neo4j.driver.internal.BoltAgent; + +public class DriverInfoUtil { + public static BoltAgent boltAgent() { + var productInfo = format("neo4j-java/%s", driverVersion()); + + var platformBuilder = new StringBuilder(); + getProperty("os.name").ifPresent(value -> append(value, platformBuilder)); + getProperty("os.version").ifPresent(value -> append(value, platformBuilder)); + getProperty("os.arch").ifPresent(value -> append(value, platformBuilder)); + + var language = getProperty("java.version").map(version -> "Java/" + version); + + var languageDetails = language.map(ignored -> { + var languageDetailsBuilder = new StringBuilder(); + getProperty("java.vm.vendor").ifPresent(value -> append(value, languageDetailsBuilder)); + getProperty("java.vm.name").ifPresent(value -> append(value, languageDetailsBuilder)); + getProperty("java.vm.version").ifPresent(value -> append(value, languageDetailsBuilder)); + return languageDetailsBuilder.isEmpty() ? null : languageDetailsBuilder; + }); + + return new BoltAgent( + productInfo, + platformBuilder.isEmpty() ? null : platformBuilder.toString(), + language.orElse(null), + languageDetails.isEmpty() ? null : languageDetails.toString()); + } + + /** + * Extracts the driver version from the driver jar MANIFEST.MF file. + */ + public static String driverVersion() { + // "Session" is arbitrary - the only thing that matters is that the class we use here is in the + // 'org.neo4j.driver' package, because that is where the jar manifest specifies the version. + // This is done as part of the build, adding a MANIFEST.MF file to the generated jarfile. + Package pkg = Session.class.getPackage(); + if (pkg != null && pkg.getImplementationVersion() != null) { + return pkg.getImplementationVersion(); + } + + // If there is no version, we're not running from a jar file, but from raw compiled class files. + // This should only happen during development, so call the version 'dev'. + return "dev"; + } + + private static Optional getProperty(String key) { + try { + var value = System.getProperty(key); + if (value != null) { + value = value.trim(); + } + return value != null && !value.isEmpty() ? Optional.of(value) : Optional.empty(); + } catch (SecurityException exception) { + return Optional.empty(); + } + } + + private static void append(String value, StringBuilder builder) { + if (value != null && !value.isEmpty()) { + var separator = builder.isEmpty() ? "" : "; "; + builder.append(separator).append(value); + } + } +} diff --git a/driver/src/test/java/org/neo4j/driver/ConfigTest.java b/driver/src/test/java/org/neo4j/driver/ConfigTest.java index f07089b1e6..0d0b56ed8f 100644 --- a/driver/src/test/java/org/neo4j/driver/ConfigTest.java +++ b/driver/src/test/java/org/neo4j/driver/ConfigTest.java @@ -503,4 +503,11 @@ void officialLoggingProvidersShouldBeSerializable(Class loggi assertTrue(Serializable.class.isAssignableFrom(loggingClass)); } } + + @Test + void shouldHaveDefaultUserAgent() { + var config = Config.defaultConfig(); + + assertTrue(config.userAgent().matches("^neo4j-java/.+$")); + } } diff --git a/driver/src/test/java/org/neo4j/driver/integration/ChannelConnectorImplIT.java b/driver/src/test/java/org/neo4j/driver/integration/ChannelConnectorImplIT.java index a95b7e3072..c67d1394b0 100644 --- a/driver/src/test/java/org/neo4j/driver/integration/ChannelConnectorImplIT.java +++ b/driver/src/test/java/org/neo4j/driver/integration/ChannelConnectorImplIT.java @@ -54,6 +54,7 @@ import org.neo4j.driver.RevocationCheckingStrategy; import org.neo4j.driver.exceptions.AuthenticationException; import org.neo4j.driver.exceptions.ServiceUnavailableException; +import org.neo4j.driver.internal.BoltAgentUtil; import org.neo4j.driver.internal.BoltServerAddress; import org.neo4j.driver.internal.ConnectionSettings; import org.neo4j.driver.internal.DefaultDomainNameResolver; @@ -226,7 +227,8 @@ private ChannelConnectorImpl newConnector( new FakeClock(), RoutingContext.EMPTY, DefaultDomainNameResolver.getInstance(), - null); + null, + BoltAgentUtil.VALUE); } private static SecurityPlan trustAllCertificates() throws GeneralSecurityException { diff --git a/driver/src/test/java/org/neo4j/driver/integration/ConnectionHandlingIT.java b/driver/src/test/java/org/neo4j/driver/integration/ConnectionHandlingIT.java index e960bfbf48..70ab9553dc 100644 --- a/driver/src/test/java/org/neo4j/driver/integration/ConnectionHandlingIT.java +++ b/driver/src/test/java/org/neo4j/driver/integration/ConnectionHandlingIT.java @@ -57,6 +57,7 @@ import org.neo4j.driver.Session; import org.neo4j.driver.Transaction; import org.neo4j.driver.exceptions.ClientException; +import org.neo4j.driver.internal.BoltAgentUtil; import org.neo4j.driver.internal.BoltServerAddress; import org.neo4j.driver.internal.ConnectionSettings; import org.neo4j.driver.internal.DriverFactory; @@ -467,8 +468,8 @@ protected ConnectionPool createConnectionPool( config.maxConnectionLifetimeMillis(), config.idleTimeBeforeConnectionTest()); Clock clock = createClock(); - ChannelConnector connector = - super.createConnector(connectionSettings, securityPlan, config, clock, routingContext); + ChannelConnector connector = super.createConnector( + connectionSettings, securityPlan, config, clock, routingContext, BoltAgentUtil.VALUE); connectionPool = new MemorizingConnectionPool( connector, bootstrap, poolSettings, config.logging(), clock, ownsEventLoopGroup); return connectionPool; diff --git a/driver/src/test/java/org/neo4j/driver/internal/BoltAgentUtil.java b/driver/src/test/java/org/neo4j/driver/internal/BoltAgentUtil.java new file mode 100644 index 0000000000..6e5ff6946b --- /dev/null +++ b/driver/src/test/java/org/neo4j/driver/internal/BoltAgentUtil.java @@ -0,0 +1,23 @@ +/* + * Copyright (c) "Neo4j" + * Neo4j Sweden AB [http://neo4j.com] + * + * This file is part of Neo4j. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.neo4j.driver.internal; + +public interface BoltAgentUtil { + BoltAgent VALUE = new BoltAgent("agent", null, null, null); +} diff --git a/driver/src/test/java/org/neo4j/driver/internal/async/connection/BoltProtocolUtilTest.java b/driver/src/test/java/org/neo4j/driver/internal/async/connection/BoltProtocolUtilTest.java index 3736d7d743..09e7655c34 100644 --- a/driver/src/test/java/org/neo4j/driver/internal/async/connection/BoltProtocolUtilTest.java +++ b/driver/src/test/java/org/neo4j/driver/internal/async/connection/BoltProtocolUtilTest.java @@ -33,7 +33,7 @@ import org.neo4j.driver.internal.messaging.v3.BoltProtocolV3; import org.neo4j.driver.internal.messaging.v41.BoltProtocolV41; import org.neo4j.driver.internal.messaging.v44.BoltProtocolV44; -import org.neo4j.driver.internal.messaging.v52.BoltProtocolV52; +import org.neo4j.driver.internal.messaging.v53.BoltProtocolV53; class BoltProtocolUtilTest { @Test @@ -41,7 +41,7 @@ void shouldReturnHandshakeBuf() { assertByteBufContains( handshakeBuf(), BOLT_MAGIC_PREAMBLE, - (2 << 16) | BoltProtocolV52.VERSION.toInt(), + (3 << 16) | BoltProtocolV53.VERSION.toInt(), (2 << 16) | BoltProtocolV44.VERSION.toInt(), BoltProtocolV41.VERSION.toInt(), BoltProtocolV3.VERSION.toInt()); @@ -49,7 +49,7 @@ void shouldReturnHandshakeBuf() { @Test void shouldReturnHandshakeString() { - assertEquals("[0x6060b017, 131589, 132100, 260, 3]", handshakeString()); + assertEquals("[0x6060b017, 197381, 132100, 260, 3]", handshakeString()); } @Test diff --git a/driver/src/test/java/org/neo4j/driver/internal/async/connection/HandshakeCompletedListenerTest.java b/driver/src/test/java/org/neo4j/driver/internal/async/connection/HandshakeCompletedListenerTest.java index 61869859d5..97e776eae1 100644 --- a/driver/src/test/java/org/neo4j/driver/internal/async/connection/HandshakeCompletedListenerTest.java +++ b/driver/src/test/java/org/neo4j/driver/internal/async/connection/HandshakeCompletedListenerTest.java @@ -43,6 +43,7 @@ import org.junit.jupiter.api.Test; import org.neo4j.driver.AuthTokenManager; import org.neo4j.driver.AuthTokens; +import org.neo4j.driver.internal.BoltAgentUtil; import org.neo4j.driver.internal.async.inbound.InboundMessageDispatcher; import org.neo4j.driver.internal.async.pool.AuthContext; import org.neo4j.driver.internal.cluster.RoutingContext; @@ -70,7 +71,12 @@ void tearDown() { void shouldFailConnectionInitializedPromiseWhenHandshakeFails() { ChannelPromise channelInitializedPromise = channel.newPromise(); HandshakeCompletedListener listener = new HandshakeCompletedListener( - "user-agent", RoutingContext.EMPTY, channelInitializedPromise, null, mock(Clock.class)); + USER_AGENT, + BoltAgentUtil.VALUE, + RoutingContext.EMPTY, + channelInitializedPromise, + null, + mock(Clock.class)); ChannelPromise handshakeCompletedPromise = channel.newPromise(); IOException cause = new IOException("Bad handshake"); @@ -92,7 +98,7 @@ void shouldWriteInitializationMessageInBoltV3WhenHandshakeCompleted() { setAuthContext(channel, authContext); testWritingOfInitializationMessage( BoltProtocolV3.VERSION, - new HelloMessage(USER_AGENT, authToken().toMap(), Collections.emptyMap(), false, null), + new HelloMessage(USER_AGENT, null, authToken().toMap(), Collections.emptyMap(), false, null), HelloResponseHandler.class); then(authContext).should().initiateAuth(authToken); } @@ -102,7 +108,12 @@ void shouldFailPromiseWhenTokenStageCompletesExceptionally() { // given var channelInitializedPromise = channel.newPromise(); var listener = new HandshakeCompletedListener( - "agent", mock(RoutingContext.class), channelInitializedPromise, null, mock(Clock.class)); + USER_AGENT, + BoltAgentUtil.VALUE, + mock(RoutingContext.class), + channelInitializedPromise, + null, + mock(Clock.class)); var handshakeCompletedPromise = channel.newPromise(); handshakeCompletedPromise.setSuccess(); setProtocolVersion(channel, BoltProtocolV5.VERSION); @@ -134,7 +145,12 @@ private void testWritingOfInitializationMessage( ChannelPromise channelInitializedPromise = channel.newPromise(); HandshakeCompletedListener listener = new HandshakeCompletedListener( - USER_AGENT, RoutingContext.EMPTY, channelInitializedPromise, null, mock(Clock.class)); + USER_AGENT, + BoltAgentUtil.VALUE, + RoutingContext.EMPTY, + channelInitializedPromise, + null, + mock(Clock.class)); ChannelPromise handshakeCompletedPromise = channel.newPromise(); handshakeCompletedPromise.setSuccess(); diff --git a/driver/src/test/java/org/neo4j/driver/internal/async/pool/ConnectionPoolImplIT.java b/driver/src/test/java/org/neo4j/driver/internal/async/pool/ConnectionPoolImplIT.java index 4c25ff1dd8..70bd7455e5 100644 --- a/driver/src/test/java/org/neo4j/driver/internal/async/pool/ConnectionPoolImplIT.java +++ b/driver/src/test/java/org/neo4j/driver/internal/async/pool/ConnectionPoolImplIT.java @@ -37,6 +37,7 @@ import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.RegisterExtension; import org.neo4j.driver.exceptions.ServiceUnavailableException; +import org.neo4j.driver.internal.BoltAgentUtil; import org.neo4j.driver.internal.BoltServerAddress; import org.neo4j.driver.internal.ConnectionSettings; import org.neo4j.driver.internal.DefaultDomainNameResolver; @@ -59,7 +60,7 @@ class ConnectionPoolImplIT { private ConnectionPoolImpl pool; @BeforeEach - void setUp() throws Exception { + void setUp() { pool = newPool(); } @@ -143,7 +144,8 @@ private ConnectionPoolImpl newPool() { clock, RoutingContext.EMPTY, DefaultDomainNameResolver.getInstance(), - null); + null, + BoltAgentUtil.VALUE); PoolSettings poolSettings = newSettings(); Bootstrap bootstrap = BootstrapFactory.newBootstrap(1); return new ConnectionPoolImpl( diff --git a/driver/src/test/java/org/neo4j/driver/internal/async/pool/NettyChannelPoolIT.java b/driver/src/test/java/org/neo4j/driver/internal/async/pool/NettyChannelPoolIT.java index f94cd03db8..19dca8780f 100644 --- a/driver/src/test/java/org/neo4j/driver/internal/async/pool/NettyChannelPoolIT.java +++ b/driver/src/test/java/org/neo4j/driver/internal/async/pool/NettyChannelPoolIT.java @@ -47,6 +47,7 @@ import org.neo4j.driver.AuthTokens; import org.neo4j.driver.Value; import org.neo4j.driver.exceptions.AuthenticationException; +import org.neo4j.driver.internal.BoltAgentUtil; import org.neo4j.driver.internal.ConnectionSettings; import org.neo4j.driver.internal.DefaultDomainNameResolver; import org.neo4j.driver.internal.async.connection.BootstrapFactory; @@ -198,7 +199,8 @@ private NettyChannelPool newPool(AuthTokenManager authTokenManager, int maxConne new FakeClock(), RoutingContext.EMPTY, DefaultDomainNameResolver.getInstance(), - null); + null, + BoltAgentUtil.VALUE); var nettyChannelHealthChecker = mock(NettyChannelHealthChecker.class); when(nettyChannelHealthChecker.isHealthy(any())).thenAnswer(NettyChannelPoolIT::answer); return new NettyChannelPool( diff --git a/driver/src/test/java/org/neo4j/driver/internal/messaging/encode/HelloMessageEncoderTest.java b/driver/src/test/java/org/neo4j/driver/internal/messaging/encode/HelloMessageEncoderTest.java index aa2c169492..9198ab674e 100644 --- a/driver/src/test/java/org/neo4j/driver/internal/messaging/encode/HelloMessageEncoderTest.java +++ b/driver/src/test/java/org/neo4j/driver/internal/messaging/encode/HelloMessageEncoderTest.java @@ -29,6 +29,7 @@ import org.junit.jupiter.api.Test; import org.mockito.InOrder; import org.neo4j.driver.Value; +import org.neo4j.driver.internal.BoltAgentUtil; import org.neo4j.driver.internal.messaging.ValuePacker; import org.neo4j.driver.internal.messaging.request.HelloMessage; @@ -42,13 +43,14 @@ void shouldEncodeHelloMessage() throws Exception { authToken.put("username", value("bob")); authToken.put("password", value("secret")); - encoder.encode(new HelloMessage("MyDriver", authToken, null, false, null), packer); + encoder.encode(new HelloMessage("MyDriver", BoltAgentUtil.VALUE, authToken, null, false, null), packer); InOrder order = inOrder(packer); order.verify(packer).packStructHeader(1, HelloMessage.SIGNATURE); Map expectedMetadata = new HashMap<>(authToken); expectedMetadata.put("user_agent", value("MyDriver")); + expectedMetadata.put("bolt_agent", value(Map.of("product", BoltAgentUtil.VALUE.product()))); order.verify(packer).pack(expectedMetadata); } @@ -61,13 +63,15 @@ void shouldEncodeHelloMessageWithRoutingContext() throws Exception { Map routingContext = new HashMap<>(); routingContext.put("policy", "eu-fast"); - encoder.encode(new HelloMessage("MyDriver", authToken, routingContext, false, null), packer); + encoder.encode( + new HelloMessage("MyDriver", BoltAgentUtil.VALUE, authToken, routingContext, false, null), packer); InOrder order = inOrder(packer); order.verify(packer).packStructHeader(1, HelloMessage.SIGNATURE); Map expectedMetadata = new HashMap<>(authToken); expectedMetadata.put("user_agent", value("MyDriver")); + expectedMetadata.put("bolt_agent", value(Map.of("product", BoltAgentUtil.VALUE.product()))); expectedMetadata.put("routing", value(routingContext)); order.verify(packer).pack(expectedMetadata); } diff --git a/driver/src/test/java/org/neo4j/driver/internal/messaging/request/HelloMessageTest.java b/driver/src/test/java/org/neo4j/driver/internal/messaging/request/HelloMessageTest.java index f099aad8c4..845e7b3d3b 100644 --- a/driver/src/test/java/org/neo4j/driver/internal/messaging/request/HelloMessageTest.java +++ b/driver/src/test/java/org/neo4j/driver/internal/messaging/request/HelloMessageTest.java @@ -31,6 +31,8 @@ import java.util.Map; import org.junit.jupiter.api.Test; import org.neo4j.driver.Value; +import org.neo4j.driver.internal.BoltAgent; +import org.neo4j.driver.internal.BoltAgentUtil; class HelloMessageTest { @Test @@ -39,10 +41,12 @@ void shouldHaveCorrectMetadata() { authToken.put("user", value("Alice")); authToken.put("credentials", value("SecretPassword")); - HelloMessage message = new HelloMessage("MyDriver/1.0.2", authToken, Collections.emptyMap(), false, null); + HelloMessage message = + new HelloMessage("MyDriver/1.0.2", BoltAgentUtil.VALUE, authToken, Collections.emptyMap(), false, null); Map expectedMetadata = new HashMap<>(authToken); expectedMetadata.put("user_agent", value("MyDriver/1.0.2")); + expectedMetadata.put("bolt_agent", value(Map.of("product", BoltAgentUtil.VALUE.product()))); expectedMetadata.put("routing", value(Collections.emptyMap())); assertEquals(expectedMetadata, message.metadata()); } @@ -57,10 +61,12 @@ void shouldHaveCorrectRoutingContext() { routingContext.put("region", "China"); routingContext.put("speed", "Slow"); - HelloMessage message = new HelloMessage("MyDriver/1.0.2", authToken, routingContext, false, null); + HelloMessage message = + new HelloMessage("MyDriver/1.0.2", BoltAgentUtil.VALUE, authToken, routingContext, false, null); Map expectedMetadata = new HashMap<>(authToken); expectedMetadata.put("user_agent", value("MyDriver/1.0.2")); + expectedMetadata.put("bolt_agent", value(Map.of("product", BoltAgentUtil.VALUE.product()))); expectedMetadata.put("routing", value(routingContext)); assertEquals(expectedMetadata, message.metadata()); } @@ -71,8 +77,50 @@ void shouldNotExposeCredentialsInToString() { authToken.put(PRINCIPAL_KEY, value("Alice")); authToken.put(CREDENTIALS_KEY, value("SecretPassword")); - HelloMessage message = new HelloMessage("MyDriver/1.0.2", authToken, Collections.emptyMap(), false, null); + HelloMessage message = + new HelloMessage("MyDriver/1.0.2", BoltAgentUtil.VALUE, authToken, Collections.emptyMap(), false, null); assertThat(message.toString(), not(containsString("SecretPassword"))); } + + @Test + void shouldAcceptNullBoltAgent() { + var authToken = new HashMap(); + authToken.put("user", value("Alice")); + authToken.put("credentials", value("SecretPassword")); + + HelloMessage message = new HelloMessage("MyDriver/1.0.2", null, authToken, Collections.emptyMap(), false, null); + + var expectedMetadata = new HashMap<>(authToken); + expectedMetadata.put("user_agent", value("MyDriver/1.0.2")); + expectedMetadata.put("routing", value(Collections.emptyMap())); + assertEquals(expectedMetadata, message.metadata()); + } + + @Test + void shouldAcceptDetailedBoltAgent() { + var authToken = new HashMap(); + authToken.put("user", value("Alice")); + authToken.put("credentials", value("SecretPassword")); + var boltAgent = new BoltAgent("1", "2", "3", "4"); + + HelloMessage message = + new HelloMessage("MyDriver/1.0.2", boltAgent, authToken, Collections.emptyMap(), false, null); + + var expectedMetadata = new HashMap<>(authToken); + expectedMetadata.put("user_agent", value("MyDriver/1.0.2")); + expectedMetadata.put( + "bolt_agent", + value(Map.of( + "product", + boltAgent.product(), + "platform", + boltAgent.platform(), + "language", + boltAgent.language(), + "language_details", + boltAgent.languageDetails()))); + expectedMetadata.put("routing", value(Collections.emptyMap())); + assertEquals(expectedMetadata, message.metadata()); + } } diff --git a/driver/src/test/java/org/neo4j/driver/internal/messaging/v3/BoltProtocolV3Test.java b/driver/src/test/java/org/neo4j/driver/internal/messaging/v3/BoltProtocolV3Test.java index 5716af89b4..928480465e 100644 --- a/driver/src/test/java/org/neo4j/driver/internal/messaging/v3/BoltProtocolV3Test.java +++ b/driver/src/test/java/org/neo4j/driver/internal/messaging/v3/BoltProtocolV3Test.java @@ -144,7 +144,8 @@ void shouldInitializeChannel() { when(authContext.getAuthToken()).thenReturn(dummyAuthToken()); ChannelAttributes.setAuthContext(channel, authContext); - protocol.initializeChannel("MyDriver/0.0.1", dummyAuthToken(), RoutingContext.EMPTY, promise, null, clock); + protocol.initializeChannel( + "MyDriver/0.0.1", null, dummyAuthToken(), RoutingContext.EMPTY, promise, null, clock); assertThat(channel.outboundMessages(), hasSize(1)); assertThat(channel.outboundMessages().poll(), instanceOf(HelloMessage.class)); @@ -177,7 +178,7 @@ void shouldFailToInitializeChannelWhenErrorIsReceived() { ChannelPromise promise = channel.newPromise(); protocol.initializeChannel( - "MyDriver/2.2.1", dummyAuthToken(), RoutingContext.EMPTY, promise, null, mock(Clock.class)); + "MyDriver/2.2.1", null, dummyAuthToken(), RoutingContext.EMPTY, promise, null, mock(Clock.class)); assertThat(channel.outboundMessages(), hasSize(1)); assertThat(channel.outboundMessages().poll(), instanceOf(HelloMessage.class)); diff --git a/driver/src/test/java/org/neo4j/driver/internal/messaging/v3/MessageWriterV3Test.java b/driver/src/test/java/org/neo4j/driver/internal/messaging/v3/MessageWriterV3Test.java index 1faaebece0..c587d94a8f 100644 --- a/driver/src/test/java/org/neo4j/driver/internal/messaging/v3/MessageWriterV3Test.java +++ b/driver/src/test/java/org/neo4j/driver/internal/messaging/v3/MessageWriterV3Test.java @@ -47,6 +47,7 @@ import java.util.Collections; import java.util.stream.Stream; import org.neo4j.driver.Query; +import org.neo4j.driver.internal.BoltAgentUtil; import org.neo4j.driver.internal.InternalBookmark; import org.neo4j.driver.internal.messaging.Message; import org.neo4j.driver.internal.messaging.MessageFormat; @@ -98,6 +99,7 @@ protected Stream supportedMessages() { // Bolt V3 messages new HelloMessage( "MyDriver/1.2.3", + BoltAgentUtil.VALUE, ((InternalAuthToken) basic("neo4j", "neo4j")).toMap(), Collections.emptyMap(), false, diff --git a/driver/src/test/java/org/neo4j/driver/internal/messaging/v4/BoltProtocolV4Test.java b/driver/src/test/java/org/neo4j/driver/internal/messaging/v4/BoltProtocolV4Test.java index 9790bf5fda..4205c6cb04 100644 --- a/driver/src/test/java/org/neo4j/driver/internal/messaging/v4/BoltProtocolV4Test.java +++ b/driver/src/test/java/org/neo4j/driver/internal/messaging/v4/BoltProtocolV4Test.java @@ -139,7 +139,8 @@ void shouldInitializeChannel() { when(authContext.getAuthToken()).thenReturn(dummyAuthToken()); ChannelAttributes.setAuthContext(channel, authContext); - protocol.initializeChannel("MyDriver/0.0.1", dummyAuthToken(), RoutingContext.EMPTY, promise, null, clock); + protocol.initializeChannel( + "MyDriver/0.0.1", null, dummyAuthToken(), RoutingContext.EMPTY, promise, null, clock); assertThat(channel.outboundMessages(), hasSize(1)); assertThat(channel.outboundMessages().poll(), instanceOf(HelloMessage.class)); @@ -172,7 +173,7 @@ void shouldFailToInitializeChannelWhenErrorIsReceived() { ChannelPromise promise = channel.newPromise(); protocol.initializeChannel( - "MyDriver/2.2.1", dummyAuthToken(), RoutingContext.EMPTY, promise, null, mock(Clock.class)); + "MyDriver/2.2.1", null, dummyAuthToken(), RoutingContext.EMPTY, promise, null, mock(Clock.class)); assertThat(channel.outboundMessages(), hasSize(1)); assertThat(channel.outboundMessages().poll(), instanceOf(HelloMessage.class)); diff --git a/driver/src/test/java/org/neo4j/driver/internal/messaging/v4/MessageWriterV4Test.java b/driver/src/test/java/org/neo4j/driver/internal/messaging/v4/MessageWriterV4Test.java index c355cd68d4..62afc38697 100644 --- a/driver/src/test/java/org/neo4j/driver/internal/messaging/v4/MessageWriterV4Test.java +++ b/driver/src/test/java/org/neo4j/driver/internal/messaging/v4/MessageWriterV4Test.java @@ -48,6 +48,7 @@ import java.util.Collections; import java.util.stream.Stream; import org.neo4j.driver.Query; +import org.neo4j.driver.internal.BoltAgentUtil; import org.neo4j.driver.internal.InternalBookmark; import org.neo4j.driver.internal.messaging.Message; import org.neo4j.driver.internal.messaging.MessageFormat; @@ -106,6 +107,7 @@ protected Stream supportedMessages() { // Bolt V3 messages new HelloMessage( "MyDriver/1.2.3", + BoltAgentUtil.VALUE, ((InternalAuthToken) basic("neo4j", "neo4j")).toMap(), Collections.emptyMap(), false, diff --git a/driver/src/test/java/org/neo4j/driver/internal/messaging/v41/BoltProtocolV41Test.java b/driver/src/test/java/org/neo4j/driver/internal/messaging/v41/BoltProtocolV41Test.java index b02cb8dea3..671907e49a 100644 --- a/driver/src/test/java/org/neo4j/driver/internal/messaging/v41/BoltProtocolV41Test.java +++ b/driver/src/test/java/org/neo4j/driver/internal/messaging/v41/BoltProtocolV41Test.java @@ -143,7 +143,8 @@ void shouldInitializeChannel() { when(authContext.getAuthToken()).thenReturn(dummyAuthToken()); ChannelAttributes.setAuthContext(channel, authContext); - protocol.initializeChannel("MyDriver/0.0.1", dummyAuthToken(), RoutingContext.EMPTY, promise, null, clock); + protocol.initializeChannel( + "MyDriver/0.0.1", null, dummyAuthToken(), RoutingContext.EMPTY, promise, null, clock); assertThat(channel.outboundMessages(), hasSize(1)); assertThat(channel.outboundMessages().poll(), instanceOf(HelloMessage.class)); @@ -176,7 +177,7 @@ void shouldFailToInitializeChannelWhenErrorIsReceived() { ChannelPromise promise = channel.newPromise(); protocol.initializeChannel( - "MyDriver/2.2.1", dummyAuthToken(), RoutingContext.EMPTY, promise, null, mock(Clock.class)); + "MyDriver/2.2.1", null, dummyAuthToken(), RoutingContext.EMPTY, promise, null, mock(Clock.class)); assertThat(channel.outboundMessages(), hasSize(1)); assertThat(channel.outboundMessages().poll(), instanceOf(HelloMessage.class)); diff --git a/driver/src/test/java/org/neo4j/driver/internal/messaging/v41/MessageWriterV41Test.java b/driver/src/test/java/org/neo4j/driver/internal/messaging/v41/MessageWriterV41Test.java index 8f740825f5..844c27691c 100644 --- a/driver/src/test/java/org/neo4j/driver/internal/messaging/v41/MessageWriterV41Test.java +++ b/driver/src/test/java/org/neo4j/driver/internal/messaging/v41/MessageWriterV41Test.java @@ -48,6 +48,7 @@ import java.util.Collections; import java.util.stream.Stream; import org.neo4j.driver.Query; +import org.neo4j.driver.internal.BoltAgentUtil; import org.neo4j.driver.internal.InternalBookmark; import org.neo4j.driver.internal.messaging.Message; import org.neo4j.driver.internal.messaging.MessageFormat; @@ -105,6 +106,7 @@ protected Stream supportedMessages() { // Bolt V3 messages new HelloMessage( "MyDriver/1.2.3", + BoltAgentUtil.VALUE, ((InternalAuthToken) basic("neo4j", "neo4j")).toMap(), Collections.emptyMap(), false, diff --git a/driver/src/test/java/org/neo4j/driver/internal/messaging/v42/BoltProtocolV42Test.java b/driver/src/test/java/org/neo4j/driver/internal/messaging/v42/BoltProtocolV42Test.java index 947a1053ed..a50d49724f 100644 --- a/driver/src/test/java/org/neo4j/driver/internal/messaging/v42/BoltProtocolV42Test.java +++ b/driver/src/test/java/org/neo4j/driver/internal/messaging/v42/BoltProtocolV42Test.java @@ -143,7 +143,8 @@ void shouldInitializeChannel() { when(authContext.getAuthToken()).thenReturn(dummyAuthToken()); ChannelAttributes.setAuthContext(channel, authContext); - protocol.initializeChannel("MyDriver/0.0.1", dummyAuthToken(), RoutingContext.EMPTY, promise, null, clock); + protocol.initializeChannel( + "MyDriver/0.0.1", null, dummyAuthToken(), RoutingContext.EMPTY, promise, null, clock); assertThat(channel.outboundMessages(), hasSize(1)); assertThat(channel.outboundMessages().poll(), instanceOf(HelloMessage.class)); @@ -176,7 +177,7 @@ void shouldFailToInitializeChannelWhenErrorIsReceived() { ChannelPromise promise = channel.newPromise(); protocol.initializeChannel( - "MyDriver/2.2.1", dummyAuthToken(), RoutingContext.EMPTY, promise, null, mock(Clock.class)); + "MyDriver/2.2.1", null, dummyAuthToken(), RoutingContext.EMPTY, promise, null, mock(Clock.class)); assertThat(channel.outboundMessages(), hasSize(1)); assertThat(channel.outboundMessages().poll(), instanceOf(HelloMessage.class)); diff --git a/driver/src/test/java/org/neo4j/driver/internal/messaging/v42/MessageWriterV42Test.java b/driver/src/test/java/org/neo4j/driver/internal/messaging/v42/MessageWriterV42Test.java index aca14b8f9c..fb9518214f 100644 --- a/driver/src/test/java/org/neo4j/driver/internal/messaging/v42/MessageWriterV42Test.java +++ b/driver/src/test/java/org/neo4j/driver/internal/messaging/v42/MessageWriterV42Test.java @@ -48,6 +48,7 @@ import java.util.Collections; import java.util.stream.Stream; import org.neo4j.driver.Query; +import org.neo4j.driver.internal.BoltAgentUtil; import org.neo4j.driver.internal.InternalBookmark; import org.neo4j.driver.internal.messaging.Message; import org.neo4j.driver.internal.messaging.MessageFormat; @@ -105,6 +106,7 @@ protected Stream supportedMessages() { // Bolt V3 messages new HelloMessage( "MyDriver/1.2.3", + BoltAgentUtil.VALUE, ((InternalAuthToken) basic("neo4j", "neo4j")).toMap(), Collections.emptyMap(), false, diff --git a/driver/src/test/java/org/neo4j/driver/internal/messaging/v43/BoltProtocolV43Test.java b/driver/src/test/java/org/neo4j/driver/internal/messaging/v43/BoltProtocolV43Test.java index 442d26d5e5..f18e4565c5 100644 --- a/driver/src/test/java/org/neo4j/driver/internal/messaging/v43/BoltProtocolV43Test.java +++ b/driver/src/test/java/org/neo4j/driver/internal/messaging/v43/BoltProtocolV43Test.java @@ -142,7 +142,8 @@ void shouldInitializeChannel() { when(authContext.getAuthToken()).thenReturn(dummyAuthToken()); ChannelAttributes.setAuthContext(channel, authContext); - protocol.initializeChannel("MyDriver/0.0.1", dummyAuthToken(), RoutingContext.EMPTY, promise, null, clock); + protocol.initializeChannel( + "MyDriver/0.0.1", null, dummyAuthToken(), RoutingContext.EMPTY, promise, null, clock); assertThat(channel.outboundMessages(), hasSize(1)); assertThat(channel.outboundMessages().poll(), instanceOf(HelloMessage.class)); @@ -175,7 +176,7 @@ void shouldFailToInitializeChannelWhenErrorIsReceived() { ChannelPromise promise = channel.newPromise(); protocol.initializeChannel( - "MyDriver/2.2.1", dummyAuthToken(), RoutingContext.EMPTY, promise, null, mock(Clock.class)); + "MyDriver/2.2.1", null, dummyAuthToken(), RoutingContext.EMPTY, promise, null, mock(Clock.class)); assertThat(channel.outboundMessages(), hasSize(1)); assertThat(channel.outboundMessages().poll(), instanceOf(HelloMessage.class)); diff --git a/driver/src/test/java/org/neo4j/driver/internal/messaging/v43/MessageWriterV43Test.java b/driver/src/test/java/org/neo4j/driver/internal/messaging/v43/MessageWriterV43Test.java index 0e8b2c5319..07eabc49bc 100644 --- a/driver/src/test/java/org/neo4j/driver/internal/messaging/v43/MessageWriterV43Test.java +++ b/driver/src/test/java/org/neo4j/driver/internal/messaging/v43/MessageWriterV43Test.java @@ -52,6 +52,7 @@ import org.neo4j.driver.Query; import org.neo4j.driver.Value; import org.neo4j.driver.Values; +import org.neo4j.driver.internal.BoltAgentUtil; import org.neo4j.driver.internal.InternalBookmark; import org.neo4j.driver.internal.messaging.Message; import org.neo4j.driver.internal.messaging.MessageFormat; @@ -110,6 +111,7 @@ protected Stream supportedMessages() { // Bolt V3 messages new HelloMessage( "MyDriver/1.2.3", + BoltAgentUtil.VALUE, ((InternalAuthToken) basic("neo4j", "neo4j")).toMap(), Collections.emptyMap(), false, diff --git a/driver/src/test/java/org/neo4j/driver/internal/messaging/v44/BoltProtocolV44Test.java b/driver/src/test/java/org/neo4j/driver/internal/messaging/v44/BoltProtocolV44Test.java index 7db9d34a64..f98f4f2269 100644 --- a/driver/src/test/java/org/neo4j/driver/internal/messaging/v44/BoltProtocolV44Test.java +++ b/driver/src/test/java/org/neo4j/driver/internal/messaging/v44/BoltProtocolV44Test.java @@ -142,7 +142,8 @@ void shouldInitializeChannel() { when(authContext.getAuthToken()).thenReturn(dummyAuthToken()); ChannelAttributes.setAuthContext(channel, authContext); - protocol.initializeChannel("MyDriver/0.0.1", dummyAuthToken(), RoutingContext.EMPTY, promise, null, clock); + protocol.initializeChannel( + "MyDriver/0.0.1", null, dummyAuthToken(), RoutingContext.EMPTY, promise, null, clock); assertThat(channel.outboundMessages(), hasSize(1)); assertThat(channel.outboundMessages().poll(), instanceOf(HelloMessage.class)); @@ -175,7 +176,7 @@ void shouldFailToInitializeChannelWhenErrorIsReceived() { ChannelPromise promise = channel.newPromise(); protocol.initializeChannel( - "MyDriver/2.2.1", dummyAuthToken(), RoutingContext.EMPTY, promise, null, mock(Clock.class)); + "MyDriver/2.2.1", null, dummyAuthToken(), RoutingContext.EMPTY, promise, null, mock(Clock.class)); assertThat(channel.outboundMessages(), hasSize(1)); assertThat(channel.outboundMessages().poll(), instanceOf(HelloMessage.class)); diff --git a/driver/src/test/java/org/neo4j/driver/internal/messaging/v44/MessageWriterV44Test.java b/driver/src/test/java/org/neo4j/driver/internal/messaging/v44/MessageWriterV44Test.java index 9734e81ea4..3b3751565f 100644 --- a/driver/src/test/java/org/neo4j/driver/internal/messaging/v44/MessageWriterV44Test.java +++ b/driver/src/test/java/org/neo4j/driver/internal/messaging/v44/MessageWriterV44Test.java @@ -52,6 +52,7 @@ import org.neo4j.driver.Query; import org.neo4j.driver.Value; import org.neo4j.driver.Values; +import org.neo4j.driver.internal.BoltAgentUtil; import org.neo4j.driver.internal.InternalBookmark; import org.neo4j.driver.internal.messaging.Message; import org.neo4j.driver.internal.messaging.MessageFormat; @@ -110,6 +111,7 @@ protected Stream supportedMessages() { // Bolt V3 messages new HelloMessage( "MyDriver/1.2.3", + BoltAgentUtil.VALUE, ((InternalAuthToken) basic("neo4j", "neo4j")).toMap(), Collections.emptyMap(), false, diff --git a/driver/src/test/java/org/neo4j/driver/internal/messaging/v5/BoltProtocolV5Test.java b/driver/src/test/java/org/neo4j/driver/internal/messaging/v5/BoltProtocolV5Test.java index cae0e5cbd9..9777c7ef49 100644 --- a/driver/src/test/java/org/neo4j/driver/internal/messaging/v5/BoltProtocolV5Test.java +++ b/driver/src/test/java/org/neo4j/driver/internal/messaging/v5/BoltProtocolV5Test.java @@ -142,7 +142,8 @@ void shouldInitializeChannel() { when(authContext.getAuthToken()).thenReturn(dummyAuthToken()); ChannelAttributes.setAuthContext(channel, authContext); - protocol.initializeChannel("MyDriver/0.0.1", dummyAuthToken(), RoutingContext.EMPTY, promise, null, clock); + protocol.initializeChannel( + "MyDriver/0.0.1", null, dummyAuthToken(), RoutingContext.EMPTY, promise, null, clock); assertThat(channel.outboundMessages(), hasSize(1)); assertThat(channel.outboundMessages().poll(), instanceOf(HelloMessage.class)); @@ -175,7 +176,7 @@ void shouldFailToInitializeChannelWhenErrorIsReceived() { ChannelPromise promise = channel.newPromise(); protocol.initializeChannel( - "MyDriver/2.2.1", dummyAuthToken(), RoutingContext.EMPTY, promise, null, mock(Clock.class)); + "MyDriver/2.2.1", null, dummyAuthToken(), RoutingContext.EMPTY, promise, null, mock(Clock.class)); assertThat(channel.outboundMessages(), hasSize(1)); assertThat(channel.outboundMessages().poll(), instanceOf(HelloMessage.class)); diff --git a/driver/src/test/java/org/neo4j/driver/internal/messaging/v5/MessageWriterV5Test.java b/driver/src/test/java/org/neo4j/driver/internal/messaging/v5/MessageWriterV5Test.java index fa4b12be08..3ef407f80a 100644 --- a/driver/src/test/java/org/neo4j/driver/internal/messaging/v5/MessageWriterV5Test.java +++ b/driver/src/test/java/org/neo4j/driver/internal/messaging/v5/MessageWriterV5Test.java @@ -52,6 +52,7 @@ import org.neo4j.driver.Query; import org.neo4j.driver.Value; import org.neo4j.driver.Values; +import org.neo4j.driver.internal.BoltAgentUtil; import org.neo4j.driver.internal.InternalBookmark; import org.neo4j.driver.internal.messaging.Message; import org.neo4j.driver.internal.messaging.MessageFormat; @@ -110,6 +111,7 @@ protected Stream supportedMessages() { // Bolt V3 messages new HelloMessage( "MyDriver/1.2.3", + BoltAgentUtil.VALUE, ((InternalAuthToken) basic("neo4j", "neo4j")).toMap(), Collections.emptyMap(), false, diff --git a/driver/src/test/java/org/neo4j/driver/internal/messaging/v51/BoltProtocolV51Test.java b/driver/src/test/java/org/neo4j/driver/internal/messaging/v51/BoltProtocolV51Test.java index 5a96bbda4f..c5ddea1ba1 100644 --- a/driver/src/test/java/org/neo4j/driver/internal/messaging/v51/BoltProtocolV51Test.java +++ b/driver/src/test/java/org/neo4j/driver/internal/messaging/v51/BoltProtocolV51Test.java @@ -135,7 +135,7 @@ void shouldInitializeChannel() { ChannelPromise promise = channel.newPromise(); protocol.initializeChannel( - "MyDriver/0.0.1", dummyAuthToken(), RoutingContext.EMPTY, promise, null, mock(Clock.class)); + "MyDriver/0.0.1", null, dummyAuthToken(), RoutingContext.EMPTY, promise, null, mock(Clock.class)); assertThat(channel.outboundMessages(), hasSize(0)); assertEquals(1, messageDispatcher.queuedHandlersCount()); diff --git a/driver/src/test/java/org/neo4j/driver/internal/messaging/v51/MessageWriterV51Test.java b/driver/src/test/java/org/neo4j/driver/internal/messaging/v51/MessageWriterV51Test.java index 4adccaf2d8..80f53ec9d0 100644 --- a/driver/src/test/java/org/neo4j/driver/internal/messaging/v51/MessageWriterV51Test.java +++ b/driver/src/test/java/org/neo4j/driver/internal/messaging/v51/MessageWriterV51Test.java @@ -52,6 +52,7 @@ import org.neo4j.driver.Query; import org.neo4j.driver.Value; import org.neo4j.driver.Values; +import org.neo4j.driver.internal.BoltAgentUtil; import org.neo4j.driver.internal.InternalBookmark; import org.neo4j.driver.internal.messaging.Message; import org.neo4j.driver.internal.messaging.MessageFormat; @@ -105,6 +106,7 @@ protected Stream supportedMessages() { // Bolt V3 messages new HelloMessage( "MyDriver/1.2.3", + BoltAgentUtil.VALUE, ((InternalAuthToken) basic("neo4j", "neo4j")).toMap(), Collections.emptyMap(), false, diff --git a/driver/src/test/java/org/neo4j/driver/internal/messaging/v52/BoltProtocolV52Test.java b/driver/src/test/java/org/neo4j/driver/internal/messaging/v52/BoltProtocolV52Test.java new file mode 100644 index 0000000000..67bdc894e6 --- /dev/null +++ b/driver/src/test/java/org/neo4j/driver/internal/messaging/v52/BoltProtocolV52Test.java @@ -0,0 +1,535 @@ +/* + * Copyright (c) "Neo4j" + * Neo4j Sweden AB [http://neo4j.com] + * + * This file is part of Neo4j. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.neo4j.driver.internal.messaging.v52; + +import static java.time.Duration.ofSeconds; +import static java.util.Collections.emptyMap; +import static java.util.Collections.singletonMap; +import static org.hamcrest.Matchers.hasSize; +import static org.hamcrest.Matchers.instanceOf; +import static org.hamcrest.junit.MatcherAssert.assertThat; +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +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.assertNull; +import static org.junit.jupiter.api.Assertions.assertSame; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.BDDMockito.then; +import static org.mockito.Mockito.doAnswer; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; +import static org.neo4j.driver.AccessMode.WRITE; +import static org.neo4j.driver.Values.value; +import static org.neo4j.driver.internal.DatabaseNameUtil.database; +import static org.neo4j.driver.internal.DatabaseNameUtil.defaultDatabase; +import static org.neo4j.driver.internal.handlers.pulln.FetchSizeUtil.UNLIMITED_FETCH_SIZE; +import static org.neo4j.driver.testutil.TestUtil.await; +import static org.neo4j.driver.testutil.TestUtil.connectionMock; + +import io.netty.channel.ChannelPromise; +import io.netty.channel.embedded.EmbeddedChannel; +import java.time.Clock; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CompletionStage; +import java.util.function.Consumer; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.EnumSource; +import org.mockito.ArgumentCaptor; +import org.neo4j.driver.AccessMode; +import org.neo4j.driver.AuthTokens; +import org.neo4j.driver.Bookmark; +import org.neo4j.driver.Logging; +import org.neo4j.driver.Query; +import org.neo4j.driver.TransactionConfig; +import org.neo4j.driver.Value; +import org.neo4j.driver.internal.DatabaseBookmark; +import org.neo4j.driver.internal.DatabaseName; +import org.neo4j.driver.internal.InternalBookmark; +import org.neo4j.driver.internal.async.UnmanagedTransaction; +import org.neo4j.driver.internal.async.connection.ChannelAttributes; +import org.neo4j.driver.internal.async.inbound.InboundMessageDispatcher; +import org.neo4j.driver.internal.cluster.RoutingContext; +import org.neo4j.driver.internal.cursor.AsyncResultCursor; +import org.neo4j.driver.internal.cursor.ResultCursorFactory; +import org.neo4j.driver.internal.handlers.BeginTxResponseHandler; +import org.neo4j.driver.internal.handlers.CommitTxResponseHandler; +import org.neo4j.driver.internal.handlers.PullAllResponseHandler; +import org.neo4j.driver.internal.handlers.RollbackTxResponseHandler; +import org.neo4j.driver.internal.handlers.RunResponseHandler; +import org.neo4j.driver.internal.messaging.BoltProtocol; +import org.neo4j.driver.internal.messaging.MessageFormat; +import org.neo4j.driver.internal.messaging.request.BeginMessage; +import org.neo4j.driver.internal.messaging.request.CommitMessage; +import org.neo4j.driver.internal.messaging.request.GoodbyeMessage; +import org.neo4j.driver.internal.messaging.request.PullMessage; +import org.neo4j.driver.internal.messaging.request.RollbackMessage; +import org.neo4j.driver.internal.messaging.request.RunWithMetadataMessage; +import org.neo4j.driver.internal.messaging.v51.MessageFormatV51; +import org.neo4j.driver.internal.security.InternalAuthToken; +import org.neo4j.driver.internal.spi.Connection; +import org.neo4j.driver.internal.spi.ResponseHandler; + +public class BoltProtocolV52Test { + protected static final String QUERY_TEXT = "RETURN $x"; + protected static final Map PARAMS = singletonMap("x", value(42)); + protected static final Query QUERY = new Query(QUERY_TEXT, value(PARAMS)); + + protected final BoltProtocol protocol = createProtocol(); + private final EmbeddedChannel channel = new EmbeddedChannel(); + private final InboundMessageDispatcher messageDispatcher = new InboundMessageDispatcher(channel, Logging.none()); + + private final TransactionConfig txConfig = TransactionConfig.builder() + .withTimeout(ofSeconds(12)) + .withMetadata(singletonMap("key", value(42))) + .build(); + + protected BoltProtocol createProtocol() { + return BoltProtocolV52.INSTANCE; + } + + @BeforeEach + void beforeEach() { + ChannelAttributes.setMessageDispatcher(channel, messageDispatcher); + } + + @AfterEach + void afterEach() { + channel.finishAndReleaseAll(); + } + + @Test + void shouldCreateMessageFormat() { + assertThat(protocol.createMessageFormat(), instanceOf(expectedMessageFormatType())); + } + + @Test + void shouldInitializeChannel() { + ChannelPromise promise = channel.newPromise(); + + protocol.initializeChannel( + "MyDriver/0.0.1", null, dummyAuthToken(), RoutingContext.EMPTY, promise, null, mock(Clock.class)); + + assertThat(channel.outboundMessages(), hasSize(0)); + assertEquals(1, messageDispatcher.queuedHandlersCount()); + assertTrue(promise.isDone()); + + Map metadata = new HashMap<>(); + metadata.put("server", value("Neo4j/4.4.0")); + metadata.put("connection_id", value("bolt-42")); + + messageDispatcher.handleSuccessMessage(metadata); + + channel.flush(); + assertTrue(promise.isDone()); + assertTrue(promise.isSuccess()); + } + + @Test + void shouldPrepareToCloseChannel() { + protocol.prepareToCloseChannel(channel); + + assertThat(channel.outboundMessages(), hasSize(1)); + assertThat(channel.outboundMessages().poll(), instanceOf(GoodbyeMessage.class)); + assertEquals(1, messageDispatcher.queuedHandlersCount()); + } + + @Test + void shouldBeginTransactionWithoutBookmark() { + Connection connection = connectionMock(protocol); + + CompletionStage stage = + protocol.beginTransaction(connection, Collections.emptySet(), TransactionConfig.empty(), null, null); + + verify(connection) + .writeAndFlush( + eq(new BeginMessage( + Collections.emptySet(), + TransactionConfig.empty(), + defaultDatabase(), + WRITE, + null, + null, + null)), + any(BeginTxResponseHandler.class)); + assertNull(await(stage)); + } + + @Test + void shouldBeginTransactionWithBookmarks() { + Connection connection = connectionMock(protocol); + Set bookmarks = Collections.singleton(InternalBookmark.parse("neo4j:bookmark:v1:tx100")); + + CompletionStage stage = + protocol.beginTransaction(connection, bookmarks, TransactionConfig.empty(), null, null); + + verify(connection) + .writeAndFlush( + eq(new BeginMessage( + bookmarks, TransactionConfig.empty(), defaultDatabase(), WRITE, null, null, null)), + any(BeginTxResponseHandler.class)); + assertNull(await(stage)); + } + + @Test + void shouldBeginTransactionWithConfig() { + Connection connection = connectionMock(protocol); + + CompletionStage stage = + protocol.beginTransaction(connection, Collections.emptySet(), txConfig, null, null); + + verify(connection) + .writeAndFlush( + eq(new BeginMessage( + Collections.emptySet(), txConfig, defaultDatabase(), WRITE, null, null, null)), + any(BeginTxResponseHandler.class)); + assertNull(await(stage)); + } + + @Test + void shouldBeginTransactionWithBookmarksAndConfig() { + Connection connection = connectionMock(protocol); + Set bookmarks = Collections.singleton(InternalBookmark.parse("neo4j:bookmark:v1:tx4242")); + + CompletionStage stage = protocol.beginTransaction(connection, bookmarks, txConfig, null, null); + + verify(connection) + .writeAndFlush( + eq(new BeginMessage(bookmarks, txConfig, defaultDatabase(), WRITE, null, null, null)), + any(BeginTxResponseHandler.class)); + assertNull(await(stage)); + } + + @Test + void shouldCommitTransaction() { + String bookmarkString = "neo4j:bookmark:v1:tx4242"; + + Connection connection = connectionMock(protocol); + when(connection.protocol()).thenReturn(protocol); + doAnswer(invocation -> { + ResponseHandler commitHandler = invocation.getArgument(1); + commitHandler.onSuccess(singletonMap("bookmark", value(bookmarkString))); + return null; + }) + .when(connection) + .writeAndFlush(eq(CommitMessage.COMMIT), any()); + + CompletionStage stage = protocol.commitTransaction(connection); + + verify(connection).writeAndFlush(eq(CommitMessage.COMMIT), any(CommitTxResponseHandler.class)); + assertEquals(InternalBookmark.parse(bookmarkString), await(stage).bookmark()); + } + + @Test + void shouldRollbackTransaction() { + Connection connection = connectionMock(protocol); + + CompletionStage stage = protocol.rollbackTransaction(connection); + + verify(connection).writeAndFlush(eq(RollbackMessage.ROLLBACK), any(RollbackTxResponseHandler.class)); + assertNull(await(stage)); + } + + @ParameterizedTest + @EnumSource(AccessMode.class) + void shouldRunInAutoCommitTransactionAndWaitForRunResponse(AccessMode mode) throws Exception { + testRunAndWaitForRunResponse(true, TransactionConfig.empty(), mode); + } + + @ParameterizedTest + @EnumSource(AccessMode.class) + void shouldRunInAutoCommitWithConfigTransactionAndWaitForRunResponse(AccessMode mode) throws Exception { + testRunAndWaitForRunResponse(true, txConfig, mode); + } + + @ParameterizedTest + @EnumSource(AccessMode.class) + void shouldRunInAutoCommitTransactionAndWaitForSuccessRunResponse(AccessMode mode) throws Exception { + testSuccessfulRunInAutoCommitTxWithWaitingForResponse(Collections.emptySet(), TransactionConfig.empty(), mode); + } + + @ParameterizedTest + @EnumSource(AccessMode.class) + void shouldRunInAutoCommitTransactionWithBookmarkAndConfigAndWaitForSuccessRunResponse(AccessMode mode) + throws Exception { + testSuccessfulRunInAutoCommitTxWithWaitingForResponse( + Collections.singleton(InternalBookmark.parse("neo4j:bookmark:v1:tx65")), txConfig, mode); + } + + @ParameterizedTest + @EnumSource(AccessMode.class) + void shouldRunInAutoCommitTransactionAndWaitForFailureRunResponse(AccessMode mode) throws Exception { + testFailedRunInAutoCommitTxWithWaitingForResponse(Collections.emptySet(), TransactionConfig.empty(), mode); + } + + @ParameterizedTest + @EnumSource(AccessMode.class) + void shouldRunInAutoCommitTransactionWithBookmarkAndConfigAndWaitForFailureRunResponse(AccessMode mode) + throws Exception { + testFailedRunInAutoCommitTxWithWaitingForResponse( + Collections.singleton(InternalBookmark.parse("neo4j:bookmark:v1:tx163")), txConfig, mode); + } + + @ParameterizedTest + @EnumSource(AccessMode.class) + void shouldRunInUnmanagedTransactionAndWaitForRunResponse(AccessMode mode) throws Exception { + testRunAndWaitForRunResponse(false, TransactionConfig.empty(), mode); + } + + @ParameterizedTest + @EnumSource(AccessMode.class) + void shouldRunInUnmanagedTransactionAndWaitForSuccessRunResponse(AccessMode mode) throws Exception { + testRunInUnmanagedTransactionAndWaitForRunResponse(true, mode); + } + + @ParameterizedTest + @EnumSource(AccessMode.class) + void shouldRunInUnmanagedTransactionAndWaitForFailureRunResponse(AccessMode mode) throws Exception { + testRunInUnmanagedTransactionAndWaitForRunResponse(false, mode); + } + + @Test + void databaseNameInBeginTransaction() { + testDatabaseNameSupport(false); + } + + @Test + void databaseNameForAutoCommitTransactions() { + testDatabaseNameSupport(true); + } + + @Test + void shouldSupportDatabaseNameInBeginTransaction() { + CompletionStage txStage = protocol.beginTransaction( + connectionMock("foo", protocol), Collections.emptySet(), TransactionConfig.empty(), null, null); + + assertDoesNotThrow(() -> await(txStage)); + } + + @Test + void shouldNotSupportDatabaseNameForAutoCommitTransactions() { + assertDoesNotThrow(() -> protocol.runInAutoCommitTransaction( + connectionMock("foo", protocol), + new Query("RETURN 1"), + Collections.emptySet(), + (ignored) -> {}, + TransactionConfig.empty(), + UNLIMITED_FETCH_SIZE, + null)); + } + + private Class expectedMessageFormatType() { + return MessageFormatV51.class; + } + + private void testFailedRunInAutoCommitTxWithWaitingForResponse( + Set bookmarks, TransactionConfig config, AccessMode mode) throws Exception { + // Given + Connection connection = connectionMock(mode, protocol); + @SuppressWarnings("unchecked") + Consumer bookmarkConsumer = mock(Consumer.class); + + CompletableFuture cursorFuture = protocol.runInAutoCommitTransaction( + connection, QUERY, bookmarks, bookmarkConsumer, config, UNLIMITED_FETCH_SIZE, null) + .asyncResult() + .toCompletableFuture(); + + ResponseHandler runHandler = verifySessionRunInvoked(connection, bookmarks, config, mode, defaultDatabase()); + assertFalse(cursorFuture.isDone()); + + // When I response to Run message with a failure + Throwable error = new RuntimeException(); + runHandler.onFailure(error); + + // Then + then(bookmarkConsumer).should(times(0)).accept(any()); + assertTrue(cursorFuture.isDone()); + Throwable actual = + assertThrows(error.getClass(), () -> await(cursorFuture.get().mapSuccessfulRunCompletionAsync())); + assertSame(error, actual); + } + + private void testSuccessfulRunInAutoCommitTxWithWaitingForResponse( + Set bookmarks, TransactionConfig config, AccessMode mode) throws Exception { + // Given + Connection connection = connectionMock(mode, protocol); + @SuppressWarnings("unchecked") + Consumer bookmarkConsumer = mock(Consumer.class); + + CompletableFuture cursorFuture = protocol.runInAutoCommitTransaction( + connection, QUERY, bookmarks, bookmarkConsumer, config, UNLIMITED_FETCH_SIZE, null) + .asyncResult() + .toCompletableFuture(); + + ResponseHandler runHandler = verifySessionRunInvoked(connection, bookmarks, config, mode, defaultDatabase()); + assertFalse(cursorFuture.isDone()); + + // When I response to the run message + runHandler.onSuccess(emptyMap()); + + // Then + then(bookmarkConsumer).should(times(0)).accept(any()); + assertTrue(cursorFuture.isDone()); + assertNotNull(cursorFuture.get()); + } + + private void testRunInUnmanagedTransactionAndWaitForRunResponse(boolean success, AccessMode mode) throws Exception { + // Given + Connection connection = connectionMock(mode, protocol); + + CompletableFuture cursorFuture = protocol.runInUnmanagedTransaction( + connection, QUERY, mock(UnmanagedTransaction.class), UNLIMITED_FETCH_SIZE) + .asyncResult() + .toCompletableFuture(); + + ResponseHandler runHandler = verifyTxRunInvoked(connection); + assertFalse(cursorFuture.isDone()); + Throwable error = new RuntimeException(); + + if (success) { + runHandler.onSuccess(emptyMap()); + } else { + // When responded with a failure + runHandler.onFailure(error); + } + + // Then + assertTrue(cursorFuture.isDone()); + if (success) { + assertNotNull(await(cursorFuture.get().mapSuccessfulRunCompletionAsync())); + } else { + Throwable actual = assertThrows( + error.getClass(), () -> await(cursorFuture.get().mapSuccessfulRunCompletionAsync())); + assertSame(error, actual); + } + } + + private void testRunAndWaitForRunResponse(boolean autoCommitTx, TransactionConfig config, AccessMode mode) + throws Exception { + // Given + Connection connection = connectionMock(mode, protocol); + Set initialBookmarks = Collections.singleton(InternalBookmark.parse("neo4j:bookmark:v1:tx987")); + + CompletionStage cursorStage; + if (autoCommitTx) { + cursorStage = protocol.runInAutoCommitTransaction( + connection, QUERY, initialBookmarks, (ignored) -> {}, config, UNLIMITED_FETCH_SIZE, null) + .asyncResult(); + } else { + cursorStage = protocol.runInUnmanagedTransaction( + connection, QUERY, mock(UnmanagedTransaction.class), UNLIMITED_FETCH_SIZE) + .asyncResult(); + } + + // When & Then + CompletableFuture cursorFuture = cursorStage.toCompletableFuture(); + assertFalse(cursorFuture.isDone()); + + ResponseHandler runResponseHandler = autoCommitTx + ? verifySessionRunInvoked(connection, initialBookmarks, config, mode, defaultDatabase()) + : verifyTxRunInvoked(connection); + runResponseHandler.onSuccess(emptyMap()); + + assertTrue(cursorFuture.isDone()); + assertNotNull(cursorFuture.get()); + } + + private void testDatabaseNameSupport(boolean autoCommitTx) { + Connection connection = connectionMock("foo", protocol); + if (autoCommitTx) { + ResultCursorFactory factory = protocol.runInAutoCommitTransaction( + connection, + QUERY, + Collections.emptySet(), + (ignored) -> {}, + TransactionConfig.empty(), + UNLIMITED_FETCH_SIZE, + null); + CompletionStage resultStage = factory.asyncResult(); + ResponseHandler runHandler = verifySessionRunInvoked( + connection, Collections.emptySet(), TransactionConfig.empty(), AccessMode.WRITE, database("foo")); + runHandler.onSuccess(emptyMap()); + await(resultStage); + verifySessionRunInvoked( + connection, Collections.emptySet(), TransactionConfig.empty(), AccessMode.WRITE, database("foo")); + } else { + CompletionStage txStage = protocol.beginTransaction( + connection, Collections.emptySet(), TransactionConfig.empty(), null, null); + await(txStage); + verifyBeginInvoked( + connection, Collections.emptySet(), TransactionConfig.empty(), AccessMode.WRITE, database("foo")); + } + } + + private ResponseHandler verifyTxRunInvoked(Connection connection) { + return verifyRunInvoked(connection, RunWithMetadataMessage.unmanagedTxRunMessage(QUERY)); + } + + private ResponseHandler verifySessionRunInvoked( + Connection connection, + Set bookmark, + TransactionConfig config, + AccessMode mode, + DatabaseName databaseName) { + RunWithMetadataMessage runMessage = + RunWithMetadataMessage.autoCommitTxRunMessage(QUERY, config, databaseName, mode, bookmark, null, null); + return verifyRunInvoked(connection, runMessage); + } + + private ResponseHandler verifyRunInvoked(Connection connection, RunWithMetadataMessage runMessage) { + ArgumentCaptor runHandlerCaptor = ArgumentCaptor.forClass(ResponseHandler.class); + ArgumentCaptor pullHandlerCaptor = ArgumentCaptor.forClass(ResponseHandler.class); + + verify(connection).write(eq(runMessage), runHandlerCaptor.capture()); + verify(connection).writeAndFlush(any(PullMessage.class), pullHandlerCaptor.capture()); + + assertThat(runHandlerCaptor.getValue(), instanceOf(RunResponseHandler.class)); + assertThat(pullHandlerCaptor.getValue(), instanceOf(PullAllResponseHandler.class)); + + return runHandlerCaptor.getValue(); + } + + private void verifyBeginInvoked( + Connection connection, + Set bookmarks, + TransactionConfig config, + AccessMode mode, + DatabaseName databaseName) { + ArgumentCaptor beginHandlerCaptor = ArgumentCaptor.forClass(ResponseHandler.class); + BeginMessage beginMessage = new BeginMessage(bookmarks, config, databaseName, mode, null, null, null); + verify(connection).writeAndFlush(eq(beginMessage), beginHandlerCaptor.capture()); + assertThat(beginHandlerCaptor.getValue(), instanceOf(BeginTxResponseHandler.class)); + } + + private static InternalAuthToken dummyAuthToken() { + return (InternalAuthToken) AuthTokens.basic("hello", "world"); + } +} diff --git a/driver/src/test/java/org/neo4j/driver/internal/messaging/v53/BoltProtocolV53Test.java b/driver/src/test/java/org/neo4j/driver/internal/messaging/v53/BoltProtocolV53Test.java new file mode 100644 index 0000000000..4a373f12a0 --- /dev/null +++ b/driver/src/test/java/org/neo4j/driver/internal/messaging/v53/BoltProtocolV53Test.java @@ -0,0 +1,542 @@ +/* + * Copyright (c) "Neo4j" + * Neo4j Sweden AB [http://neo4j.com] + * + * This file is part of Neo4j. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.neo4j.driver.internal.messaging.v53; + +import static java.time.Duration.ofSeconds; +import static java.util.Collections.emptyMap; +import static java.util.Collections.singletonMap; +import static org.hamcrest.Matchers.hasSize; +import static org.hamcrest.Matchers.instanceOf; +import static org.hamcrest.junit.MatcherAssert.assertThat; +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +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.assertNull; +import static org.junit.jupiter.api.Assertions.assertSame; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.BDDMockito.then; +import static org.mockito.Mockito.doAnswer; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; +import static org.neo4j.driver.AccessMode.WRITE; +import static org.neo4j.driver.Values.value; +import static org.neo4j.driver.internal.DatabaseNameUtil.database; +import static org.neo4j.driver.internal.DatabaseNameUtil.defaultDatabase; +import static org.neo4j.driver.internal.handlers.pulln.FetchSizeUtil.UNLIMITED_FETCH_SIZE; +import static org.neo4j.driver.testutil.TestUtil.await; +import static org.neo4j.driver.testutil.TestUtil.connectionMock; + +import io.netty.channel.ChannelPromise; +import io.netty.channel.embedded.EmbeddedChannel; +import java.time.Clock; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CompletionStage; +import java.util.function.Consumer; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.EnumSource; +import org.mockito.ArgumentCaptor; +import org.neo4j.driver.AccessMode; +import org.neo4j.driver.AuthTokens; +import org.neo4j.driver.Bookmark; +import org.neo4j.driver.Logging; +import org.neo4j.driver.Query; +import org.neo4j.driver.TransactionConfig; +import org.neo4j.driver.Value; +import org.neo4j.driver.internal.BoltAgentUtil; +import org.neo4j.driver.internal.DatabaseBookmark; +import org.neo4j.driver.internal.DatabaseName; +import org.neo4j.driver.internal.InternalBookmark; +import org.neo4j.driver.internal.async.UnmanagedTransaction; +import org.neo4j.driver.internal.async.connection.ChannelAttributes; +import org.neo4j.driver.internal.async.inbound.InboundMessageDispatcher; +import org.neo4j.driver.internal.cluster.RoutingContext; +import org.neo4j.driver.internal.cursor.AsyncResultCursor; +import org.neo4j.driver.internal.cursor.ResultCursorFactory; +import org.neo4j.driver.internal.handlers.BeginTxResponseHandler; +import org.neo4j.driver.internal.handlers.CommitTxResponseHandler; +import org.neo4j.driver.internal.handlers.PullAllResponseHandler; +import org.neo4j.driver.internal.handlers.RollbackTxResponseHandler; +import org.neo4j.driver.internal.handlers.RunResponseHandler; +import org.neo4j.driver.internal.messaging.BoltProtocol; +import org.neo4j.driver.internal.messaging.MessageFormat; +import org.neo4j.driver.internal.messaging.request.BeginMessage; +import org.neo4j.driver.internal.messaging.request.CommitMessage; +import org.neo4j.driver.internal.messaging.request.GoodbyeMessage; +import org.neo4j.driver.internal.messaging.request.PullMessage; +import org.neo4j.driver.internal.messaging.request.RollbackMessage; +import org.neo4j.driver.internal.messaging.request.RunWithMetadataMessage; +import org.neo4j.driver.internal.messaging.v51.MessageFormatV51; +import org.neo4j.driver.internal.security.InternalAuthToken; +import org.neo4j.driver.internal.spi.Connection; +import org.neo4j.driver.internal.spi.ResponseHandler; + +public class BoltProtocolV53Test { + protected static final String QUERY_TEXT = "RETURN $x"; + protected static final Map PARAMS = singletonMap("x", value(42)); + protected static final Query QUERY = new Query(QUERY_TEXT, value(PARAMS)); + + protected final BoltProtocol protocol = createProtocol(); + private final EmbeddedChannel channel = new EmbeddedChannel(); + private final InboundMessageDispatcher messageDispatcher = new InboundMessageDispatcher(channel, Logging.none()); + + private final TransactionConfig txConfig = TransactionConfig.builder() + .withTimeout(ofSeconds(12)) + .withMetadata(singletonMap("key", value(42))) + .build(); + + protected BoltProtocol createProtocol() { + return BoltProtocolV53.INSTANCE; + } + + @BeforeEach + void beforeEach() { + ChannelAttributes.setMessageDispatcher(channel, messageDispatcher); + } + + @AfterEach + void afterEach() { + channel.finishAndReleaseAll(); + } + + @Test + void shouldCreateMessageFormat() { + assertThat(protocol.createMessageFormat(), instanceOf(expectedMessageFormatType())); + } + + @Test + void shouldInitializeChannel() { + ChannelPromise promise = channel.newPromise(); + + protocol.initializeChannel( + "MyDriver/0.0.1", + BoltAgentUtil.VALUE, + dummyAuthToken(), + RoutingContext.EMPTY, + promise, + null, + mock(Clock.class)); + + assertThat(channel.outboundMessages(), hasSize(0)); + assertEquals(1, messageDispatcher.queuedHandlersCount()); + assertTrue(promise.isDone()); + + Map metadata = new HashMap<>(); + metadata.put("server", value("Neo4j/4.4.0")); + metadata.put("connection_id", value("bolt-42")); + + messageDispatcher.handleSuccessMessage(metadata); + + channel.flush(); + assertTrue(promise.isDone()); + assertTrue(promise.isSuccess()); + } + + @Test + void shouldPrepareToCloseChannel() { + protocol.prepareToCloseChannel(channel); + + assertThat(channel.outboundMessages(), hasSize(1)); + assertThat(channel.outboundMessages().poll(), instanceOf(GoodbyeMessage.class)); + assertEquals(1, messageDispatcher.queuedHandlersCount()); + } + + @Test + void shouldBeginTransactionWithoutBookmark() { + Connection connection = connectionMock(protocol); + + CompletionStage stage = + protocol.beginTransaction(connection, Collections.emptySet(), TransactionConfig.empty(), null, null); + + verify(connection) + .writeAndFlush( + eq(new BeginMessage( + Collections.emptySet(), + TransactionConfig.empty(), + defaultDatabase(), + WRITE, + null, + null, + null)), + any(BeginTxResponseHandler.class)); + assertNull(await(stage)); + } + + @Test + void shouldBeginTransactionWithBookmarks() { + Connection connection = connectionMock(protocol); + Set bookmarks = Collections.singleton(InternalBookmark.parse("neo4j:bookmark:v1:tx100")); + + CompletionStage stage = + protocol.beginTransaction(connection, bookmarks, TransactionConfig.empty(), null, null); + + verify(connection) + .writeAndFlush( + eq(new BeginMessage( + bookmarks, TransactionConfig.empty(), defaultDatabase(), WRITE, null, null, null)), + any(BeginTxResponseHandler.class)); + assertNull(await(stage)); + } + + @Test + void shouldBeginTransactionWithConfig() { + Connection connection = connectionMock(protocol); + + CompletionStage stage = + protocol.beginTransaction(connection, Collections.emptySet(), txConfig, null, null); + + verify(connection) + .writeAndFlush( + eq(new BeginMessage( + Collections.emptySet(), txConfig, defaultDatabase(), WRITE, null, null, null)), + any(BeginTxResponseHandler.class)); + assertNull(await(stage)); + } + + @Test + void shouldBeginTransactionWithBookmarksAndConfig() { + Connection connection = connectionMock(protocol); + Set bookmarks = Collections.singleton(InternalBookmark.parse("neo4j:bookmark:v1:tx4242")); + + CompletionStage stage = protocol.beginTransaction(connection, bookmarks, txConfig, null, null); + + verify(connection) + .writeAndFlush( + eq(new BeginMessage(bookmarks, txConfig, defaultDatabase(), WRITE, null, null, null)), + any(BeginTxResponseHandler.class)); + assertNull(await(stage)); + } + + @Test + void shouldCommitTransaction() { + String bookmarkString = "neo4j:bookmark:v1:tx4242"; + + Connection connection = connectionMock(protocol); + when(connection.protocol()).thenReturn(protocol); + doAnswer(invocation -> { + ResponseHandler commitHandler = invocation.getArgument(1); + commitHandler.onSuccess(singletonMap("bookmark", value(bookmarkString))); + return null; + }) + .when(connection) + .writeAndFlush(eq(CommitMessage.COMMIT), any()); + + CompletionStage stage = protocol.commitTransaction(connection); + + verify(connection).writeAndFlush(eq(CommitMessage.COMMIT), any(CommitTxResponseHandler.class)); + assertEquals(InternalBookmark.parse(bookmarkString), await(stage).bookmark()); + } + + @Test + void shouldRollbackTransaction() { + Connection connection = connectionMock(protocol); + + CompletionStage stage = protocol.rollbackTransaction(connection); + + verify(connection).writeAndFlush(eq(RollbackMessage.ROLLBACK), any(RollbackTxResponseHandler.class)); + assertNull(await(stage)); + } + + @ParameterizedTest + @EnumSource(AccessMode.class) + void shouldRunInAutoCommitTransactionAndWaitForRunResponse(AccessMode mode) throws Exception { + testRunAndWaitForRunResponse(true, TransactionConfig.empty(), mode); + } + + @ParameterizedTest + @EnumSource(AccessMode.class) + void shouldRunInAutoCommitWithConfigTransactionAndWaitForRunResponse(AccessMode mode) throws Exception { + testRunAndWaitForRunResponse(true, txConfig, mode); + } + + @ParameterizedTest + @EnumSource(AccessMode.class) + void shouldRunInAutoCommitTransactionAndWaitForSuccessRunResponse(AccessMode mode) throws Exception { + testSuccessfulRunInAutoCommitTxWithWaitingForResponse(Collections.emptySet(), TransactionConfig.empty(), mode); + } + + @ParameterizedTest + @EnumSource(AccessMode.class) + void shouldRunInAutoCommitTransactionWithBookmarkAndConfigAndWaitForSuccessRunResponse(AccessMode mode) + throws Exception { + testSuccessfulRunInAutoCommitTxWithWaitingForResponse( + Collections.singleton(InternalBookmark.parse("neo4j:bookmark:v1:tx65")), txConfig, mode); + } + + @ParameterizedTest + @EnumSource(AccessMode.class) + void shouldRunInAutoCommitTransactionAndWaitForFailureRunResponse(AccessMode mode) throws Exception { + testFailedRunInAutoCommitTxWithWaitingForResponse(Collections.emptySet(), TransactionConfig.empty(), mode); + } + + @ParameterizedTest + @EnumSource(AccessMode.class) + void shouldRunInAutoCommitTransactionWithBookmarkAndConfigAndWaitForFailureRunResponse(AccessMode mode) + throws Exception { + testFailedRunInAutoCommitTxWithWaitingForResponse( + Collections.singleton(InternalBookmark.parse("neo4j:bookmark:v1:tx163")), txConfig, mode); + } + + @ParameterizedTest + @EnumSource(AccessMode.class) + void shouldRunInUnmanagedTransactionAndWaitForRunResponse(AccessMode mode) throws Exception { + testRunAndWaitForRunResponse(false, TransactionConfig.empty(), mode); + } + + @ParameterizedTest + @EnumSource(AccessMode.class) + void shouldRunInUnmanagedTransactionAndWaitForSuccessRunResponse(AccessMode mode) throws Exception { + testRunInUnmanagedTransactionAndWaitForRunResponse(true, mode); + } + + @ParameterizedTest + @EnumSource(AccessMode.class) + void shouldRunInUnmanagedTransactionAndWaitForFailureRunResponse(AccessMode mode) throws Exception { + testRunInUnmanagedTransactionAndWaitForRunResponse(false, mode); + } + + @Test + void databaseNameInBeginTransaction() { + testDatabaseNameSupport(false); + } + + @Test + void databaseNameForAutoCommitTransactions() { + testDatabaseNameSupport(true); + } + + @Test + void shouldSupportDatabaseNameInBeginTransaction() { + CompletionStage txStage = protocol.beginTransaction( + connectionMock("foo", protocol), Collections.emptySet(), TransactionConfig.empty(), null, null); + + assertDoesNotThrow(() -> await(txStage)); + } + + @Test + void shouldNotSupportDatabaseNameForAutoCommitTransactions() { + assertDoesNotThrow(() -> protocol.runInAutoCommitTransaction( + connectionMock("foo", protocol), + new Query("RETURN 1"), + Collections.emptySet(), + (ignored) -> {}, + TransactionConfig.empty(), + UNLIMITED_FETCH_SIZE, + null)); + } + + private Class expectedMessageFormatType() { + return MessageFormatV51.class; + } + + private void testFailedRunInAutoCommitTxWithWaitingForResponse( + Set bookmarks, TransactionConfig config, AccessMode mode) throws Exception { + // Given + Connection connection = connectionMock(mode, protocol); + @SuppressWarnings("unchecked") + Consumer bookmarkConsumer = mock(Consumer.class); + + CompletableFuture cursorFuture = protocol.runInAutoCommitTransaction( + connection, QUERY, bookmarks, bookmarkConsumer, config, UNLIMITED_FETCH_SIZE, null) + .asyncResult() + .toCompletableFuture(); + + ResponseHandler runHandler = verifySessionRunInvoked(connection, bookmarks, config, mode, defaultDatabase()); + assertFalse(cursorFuture.isDone()); + + // When I response to Run message with a failure + Throwable error = new RuntimeException(); + runHandler.onFailure(error); + + // Then + then(bookmarkConsumer).should(times(0)).accept(any()); + assertTrue(cursorFuture.isDone()); + Throwable actual = + assertThrows(error.getClass(), () -> await(cursorFuture.get().mapSuccessfulRunCompletionAsync())); + assertSame(error, actual); + } + + private void testSuccessfulRunInAutoCommitTxWithWaitingForResponse( + Set bookmarks, TransactionConfig config, AccessMode mode) throws Exception { + // Given + Connection connection = connectionMock(mode, protocol); + @SuppressWarnings("unchecked") + Consumer bookmarkConsumer = mock(Consumer.class); + + CompletableFuture cursorFuture = protocol.runInAutoCommitTransaction( + connection, QUERY, bookmarks, bookmarkConsumer, config, UNLIMITED_FETCH_SIZE, null) + .asyncResult() + .toCompletableFuture(); + + ResponseHandler runHandler = verifySessionRunInvoked(connection, bookmarks, config, mode, defaultDatabase()); + assertFalse(cursorFuture.isDone()); + + // When I response to the run message + runHandler.onSuccess(emptyMap()); + + // Then + then(bookmarkConsumer).should(times(0)).accept(any()); + assertTrue(cursorFuture.isDone()); + assertNotNull(cursorFuture.get()); + } + + private void testRunInUnmanagedTransactionAndWaitForRunResponse(boolean success, AccessMode mode) throws Exception { + // Given + Connection connection = connectionMock(mode, protocol); + + CompletableFuture cursorFuture = protocol.runInUnmanagedTransaction( + connection, QUERY, mock(UnmanagedTransaction.class), UNLIMITED_FETCH_SIZE) + .asyncResult() + .toCompletableFuture(); + + ResponseHandler runHandler = verifyTxRunInvoked(connection); + assertFalse(cursorFuture.isDone()); + Throwable error = new RuntimeException(); + + if (success) { + runHandler.onSuccess(emptyMap()); + } else { + // When responded with a failure + runHandler.onFailure(error); + } + + // Then + assertTrue(cursorFuture.isDone()); + if (success) { + assertNotNull(await(cursorFuture.get().mapSuccessfulRunCompletionAsync())); + } else { + Throwable actual = assertThrows( + error.getClass(), () -> await(cursorFuture.get().mapSuccessfulRunCompletionAsync())); + assertSame(error, actual); + } + } + + private void testRunAndWaitForRunResponse(boolean autoCommitTx, TransactionConfig config, AccessMode mode) + throws Exception { + // Given + Connection connection = connectionMock(mode, protocol); + Set initialBookmarks = Collections.singleton(InternalBookmark.parse("neo4j:bookmark:v1:tx987")); + + CompletionStage cursorStage; + if (autoCommitTx) { + cursorStage = protocol.runInAutoCommitTransaction( + connection, QUERY, initialBookmarks, (ignored) -> {}, config, UNLIMITED_FETCH_SIZE, null) + .asyncResult(); + } else { + cursorStage = protocol.runInUnmanagedTransaction( + connection, QUERY, mock(UnmanagedTransaction.class), UNLIMITED_FETCH_SIZE) + .asyncResult(); + } + + // When & Then + CompletableFuture cursorFuture = cursorStage.toCompletableFuture(); + assertFalse(cursorFuture.isDone()); + + ResponseHandler runResponseHandler = autoCommitTx + ? verifySessionRunInvoked(connection, initialBookmarks, config, mode, defaultDatabase()) + : verifyTxRunInvoked(connection); + runResponseHandler.onSuccess(emptyMap()); + + assertTrue(cursorFuture.isDone()); + assertNotNull(cursorFuture.get()); + } + + private void testDatabaseNameSupport(boolean autoCommitTx) { + Connection connection = connectionMock("foo", protocol); + if (autoCommitTx) { + ResultCursorFactory factory = protocol.runInAutoCommitTransaction( + connection, + QUERY, + Collections.emptySet(), + (ignored) -> {}, + TransactionConfig.empty(), + UNLIMITED_FETCH_SIZE, + null); + CompletionStage resultStage = factory.asyncResult(); + ResponseHandler runHandler = verifySessionRunInvoked( + connection, Collections.emptySet(), TransactionConfig.empty(), AccessMode.WRITE, database("foo")); + runHandler.onSuccess(emptyMap()); + await(resultStage); + verifySessionRunInvoked( + connection, Collections.emptySet(), TransactionConfig.empty(), AccessMode.WRITE, database("foo")); + } else { + CompletionStage txStage = protocol.beginTransaction( + connection, Collections.emptySet(), TransactionConfig.empty(), null, null); + await(txStage); + verifyBeginInvoked( + connection, Collections.emptySet(), TransactionConfig.empty(), AccessMode.WRITE, database("foo")); + } + } + + private ResponseHandler verifyTxRunInvoked(Connection connection) { + return verifyRunInvoked(connection, RunWithMetadataMessage.unmanagedTxRunMessage(QUERY)); + } + + private ResponseHandler verifySessionRunInvoked( + Connection connection, + Set bookmark, + TransactionConfig config, + AccessMode mode, + DatabaseName databaseName) { + RunWithMetadataMessage runMessage = + RunWithMetadataMessage.autoCommitTxRunMessage(QUERY, config, databaseName, mode, bookmark, null, null); + return verifyRunInvoked(connection, runMessage); + } + + private ResponseHandler verifyRunInvoked(Connection connection, RunWithMetadataMessage runMessage) { + ArgumentCaptor runHandlerCaptor = ArgumentCaptor.forClass(ResponseHandler.class); + ArgumentCaptor pullHandlerCaptor = ArgumentCaptor.forClass(ResponseHandler.class); + + verify(connection).write(eq(runMessage), runHandlerCaptor.capture()); + verify(connection).writeAndFlush(any(PullMessage.class), pullHandlerCaptor.capture()); + + assertThat(runHandlerCaptor.getValue(), instanceOf(RunResponseHandler.class)); + assertThat(pullHandlerCaptor.getValue(), instanceOf(PullAllResponseHandler.class)); + + return runHandlerCaptor.getValue(); + } + + private void verifyBeginInvoked( + Connection connection, + Set bookmarks, + TransactionConfig config, + AccessMode mode, + DatabaseName databaseName) { + ArgumentCaptor beginHandlerCaptor = ArgumentCaptor.forClass(ResponseHandler.class); + BeginMessage beginMessage = new BeginMessage(bookmarks, config, databaseName, mode, null, null, null); + verify(connection).writeAndFlush(eq(beginMessage), beginHandlerCaptor.capture()); + assertThat(beginHandlerCaptor.getValue(), instanceOf(BeginTxResponseHandler.class)); + } + + private static InternalAuthToken dummyAuthToken() { + return (InternalAuthToken) AuthTokens.basic("hello", "world"); + } +} diff --git a/driver/src/test/java/org/neo4j/driver/internal/util/DriverInfoUtilTest.java b/driver/src/test/java/org/neo4j/driver/internal/util/DriverInfoUtilTest.java new file mode 100644 index 0000000000..eecb0f7dd7 --- /dev/null +++ b/driver/src/test/java/org/neo4j/driver/internal/util/DriverInfoUtilTest.java @@ -0,0 +1,32 @@ +/* + * Copyright (c) "Neo4j" + * Neo4j Sweden AB [http://neo4j.com] + * + * This file is part of Neo4j. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.neo4j.driver.internal.util; + +import static org.junit.jupiter.api.Assertions.assertTrue; + +import org.junit.jupiter.api.Test; + +class DriverInfoUtilTest { + @Test + void shouldIncludeValidProduct() { + var boltAgent = DriverInfoUtil.boltAgent(); + + assertTrue(boltAgent.product().matches("^neo4j-java/.+$")); + } +} diff --git a/driver/src/test/java/org/neo4j/driver/internal/util/MessageRecordingDriverFactory.java b/driver/src/test/java/org/neo4j/driver/internal/util/MessageRecordingDriverFactory.java index 251787afc8..c327a63889 100644 --- a/driver/src/test/java/org/neo4j/driver/internal/util/MessageRecordingDriverFactory.java +++ b/driver/src/test/java/org/neo4j/driver/internal/util/MessageRecordingDriverFactory.java @@ -29,6 +29,8 @@ import java.util.concurrent.CopyOnWriteArrayList; import org.neo4j.driver.Config; import org.neo4j.driver.Logging; +import org.neo4j.driver.internal.BoltAgent; +import org.neo4j.driver.internal.BoltAgentUtil; import org.neo4j.driver.internal.ConnectionSettings; import org.neo4j.driver.internal.DefaultDomainNameResolver; import org.neo4j.driver.internal.DriverFactory; @@ -55,7 +57,8 @@ protected ChannelConnector createConnector( SecurityPlan securityPlan, Config config, Clock clock, - RoutingContext routingContext) { + RoutingContext routingContext, + BoltAgent boltAgent) { ChannelPipelineBuilder pipelineBuilder = new MessageRecordingChannelPipelineBuilder(); return new ChannelConnectorImpl( settings, @@ -65,7 +68,8 @@ protected ChannelConnector createConnector( clock, routingContext, DefaultDomainNameResolver.getInstance(), - null); + null, + BoltAgentUtil.VALUE); } private class MessageRecordingChannelPipelineBuilder extends ChannelPipelineBuilderImpl { diff --git a/driver/src/test/java/org/neo4j/driver/internal/util/io/ChannelTrackingDriverFactory.java b/driver/src/test/java/org/neo4j/driver/internal/util/io/ChannelTrackingDriverFactory.java index bc1d1aeb1c..cbbbbd7a42 100644 --- a/driver/src/test/java/org/neo4j/driver/internal/util/io/ChannelTrackingDriverFactory.java +++ b/driver/src/test/java/org/neo4j/driver/internal/util/io/ChannelTrackingDriverFactory.java @@ -26,6 +26,8 @@ import java.util.concurrent.CopyOnWriteArrayList; import org.neo4j.driver.AuthTokenManager; import org.neo4j.driver.Config; +import org.neo4j.driver.internal.BoltAgent; +import org.neo4j.driver.internal.BoltAgentUtil; import org.neo4j.driver.internal.BoltServerAddress; import org.neo4j.driver.internal.ConnectionSettings; import org.neo4j.driver.internal.async.connection.BootstrapFactory; @@ -41,10 +43,6 @@ public class ChannelTrackingDriverFactory extends DriverFactoryWithClock { private final int eventLoopThreads; private ConnectionPool pool; - public ChannelTrackingDriverFactory() { - this(0, Clock.systemUTC()); - } - public ChannelTrackingDriverFactory(Clock clock) { this(0, clock); } @@ -65,7 +63,8 @@ protected final ChannelConnector createConnector( SecurityPlan securityPlan, Config config, Clock clock, - RoutingContext routingContext) { + RoutingContext routingContext, + BoltAgent boltAgent) { return createChannelTrackingConnector( createRealConnector(settings, securityPlan, config, clock, routingContext)); } @@ -90,7 +89,7 @@ protected ChannelConnector createRealConnector( Config config, Clock clock, RoutingContext routingContext) { - return super.createConnector(settings, securityPlan, config, clock, routingContext); + return super.createConnector(settings, securityPlan, config, clock, routingContext, BoltAgentUtil.VALUE); } private ChannelTrackingConnector createChannelTrackingConnector(ChannelConnector connector) { @@ -101,12 +100,6 @@ public List channels() { return new ArrayList<>(channels); } - public List pollChannels() { - List result = new ArrayList<>(channels); - channels.clear(); - return result; - } - public int activeChannels(BoltServerAddress address) { return pool == null ? 0 : pool.inUseConnections(address); } diff --git a/driver/src/test/java/org/neo4j/driver/internal/util/io/ChannelTrackingDriverFactoryWithFailingMessageFormat.java b/driver/src/test/java/org/neo4j/driver/internal/util/io/ChannelTrackingDriverFactoryWithFailingMessageFormat.java index 0b686d893e..cd7e79d498 100644 --- a/driver/src/test/java/org/neo4j/driver/internal/util/io/ChannelTrackingDriverFactoryWithFailingMessageFormat.java +++ b/driver/src/test/java/org/neo4j/driver/internal/util/io/ChannelTrackingDriverFactoryWithFailingMessageFormat.java @@ -20,6 +20,7 @@ import java.time.Clock; import org.neo4j.driver.Config; +import org.neo4j.driver.internal.BoltAgentUtil; import org.neo4j.driver.internal.ConnectionSettings; import org.neo4j.driver.internal.DefaultDomainNameResolver; import org.neo4j.driver.internal.async.connection.ChannelConnector; @@ -51,7 +52,8 @@ protected ChannelConnector createRealConnector( clock, routingContext, DefaultDomainNameResolver.getInstance(), - null); + null, + BoltAgentUtil.VALUE); } public FailingMessageFormat getFailingMessageFormat() { diff --git a/driver/src/test/java/org/neo4j/driver/testutil/TestUtil.java b/driver/src/test/java/org/neo4j/driver/testutil/TestUtil.java index a8cd4efc4c..516f1c918b 100644 --- a/driver/src/test/java/org/neo4j/driver/testutil/TestUtil.java +++ b/driver/src/test/java/org/neo4j/driver/testutil/TestUtil.java @@ -95,6 +95,8 @@ import org.neo4j.driver.internal.messaging.v44.BoltProtocolV44; import org.neo4j.driver.internal.messaging.v5.BoltProtocolV5; import org.neo4j.driver.internal.messaging.v51.BoltProtocolV51; +import org.neo4j.driver.internal.messaging.v52.BoltProtocolV52; +import org.neo4j.driver.internal.messaging.v53.BoltProtocolV53; import org.neo4j.driver.internal.retry.RetryLogic; import org.neo4j.driver.internal.spi.Connection; import org.neo4j.driver.internal.spi.ConnectionProvider; @@ -457,7 +459,9 @@ public static Connection connectionMock(String databaseName, AccessMode mode, Bo || version.equals(BoltProtocolV43.VERSION) || version.equals(BoltProtocolV44.VERSION) || version.equals(BoltProtocolV5.VERSION) - || version.equals(BoltProtocolV51.VERSION)) { + || version.equals(BoltProtocolV51.VERSION) + || version.equals(BoltProtocolV52.VERSION) + || version.equals(BoltProtocolV53.VERSION)) { setupSuccessResponse(connection, CommitMessage.class); setupSuccessResponse(connection, RollbackMessage.class); setupSuccessResponse(connection, BeginMessage.class); diff --git a/testkit-backend/src/main/java/neo4j/org/testkit/backend/messages/requests/GetFeatures.java b/testkit-backend/src/main/java/neo4j/org/testkit/backend/messages/requests/GetFeatures.java index f86b573ece..1b320b7f00 100644 --- a/testkit-backend/src/main/java/neo4j/org/testkit/backend/messages/requests/GetFeatures.java +++ b/testkit-backend/src/main/java/neo4j/org/testkit/backend/messages/requests/GetFeatures.java @@ -41,6 +41,7 @@ public class GetFeatures implements TestkitRequest { "Feature:Bolt:5.0", "Feature:Bolt:5.1", "Feature:Bolt:5.2", + "Feature:Bolt:5.3", "AuthorizationExpiredTreatment", "ConfHint:connection.recv_timeout_seconds", "Feature:Auth:Bearer",