diff --git a/spring-data-neo4j/src/test/java/org/neo4j/springframework/data/integration/imperative/AuditingIT.java b/spring-data-neo4j/src/test/java/org/neo4j/springframework/data/integration/imperative/AuditingIT.java index 08a266ecc..17e68894c 100644 --- a/spring-data-neo4j/src/test/java/org/neo4j/springframework/data/integration/imperative/AuditingIT.java +++ b/spring-data-neo4j/src/test/java/org/neo4j/springframework/data/integration/imperative/AuditingIT.java @@ -98,7 +98,7 @@ class AuditingIT extends AuditingITBase { @Bean public Driver driver() { - return neo4jConnectionSupport.openConnection(); + return neo4jConnectionSupport.getDriver(); } @Override diff --git a/spring-data-neo4j/src/test/java/org/neo4j/springframework/data/integration/imperative/CallbacksIT.java b/spring-data-neo4j/src/test/java/org/neo4j/springframework/data/integration/imperative/CallbacksIT.java index 5f2c31186..721e36025 100644 --- a/spring-data-neo4j/src/test/java/org/neo4j/springframework/data/integration/imperative/CallbacksIT.java +++ b/spring-data-neo4j/src/test/java/org/neo4j/springframework/data/integration/imperative/CallbacksIT.java @@ -93,7 +93,7 @@ class CallbacksIT extends CallbacksITBase { @Bean public Driver driver() { - return neo4jConnectionSupport.openConnection(); + return neo4jConnectionSupport.getDriver(); } @Override diff --git a/spring-data-neo4j/src/test/java/org/neo4j/springframework/data/integration/imperative/IdGeneratorsIT.java b/spring-data-neo4j/src/test/java/org/neo4j/springframework/data/integration/imperative/IdGeneratorsIT.java index f9feb4162..16a7e0a51 100644 --- a/spring-data-neo4j/src/test/java/org/neo4j/springframework/data/integration/imperative/IdGeneratorsIT.java +++ b/spring-data-neo4j/src/test/java/org/neo4j/springframework/data/integration/imperative/IdGeneratorsIT.java @@ -131,7 +131,7 @@ class IdGeneratorsIT extends IdGeneratorsITBase { @Bean public Driver driver() { - return neo4jConnectionSupport.openConnection(); + return neo4jConnectionSupport.getDriver(); } @Override diff --git a/spring-data-neo4j/src/test/java/org/neo4j/springframework/data/integration/imperative/RepositoryIT.java b/spring-data-neo4j/src/test/java/org/neo4j/springframework/data/integration/imperative/RepositoryIT.java index 65562fc24..8f5d33c30 100644 --- a/spring-data-neo4j/src/test/java/org/neo4j/springframework/data/integration/imperative/RepositoryIT.java +++ b/spring-data-neo4j/src/test/java/org/neo4j/springframework/data/integration/imperative/RepositoryIT.java @@ -1025,7 +1025,7 @@ class RepositoryIT { @Bean public Driver driver() { - return neo4jConnectionSupport.openConnection(); + return neo4jConnectionSupport.getDriver(); } @Override diff --git a/spring-data-neo4j/src/test/java/org/neo4j/springframework/data/integration/kotlin/KotlinIT.java b/spring-data-neo4j/src/test/java/org/neo4j/springframework/data/integration/kotlin/KotlinIT.java index 8e87609a1..5f6ef6da5 100644 --- a/spring-data-neo4j/src/test/java/org/neo4j/springframework/data/integration/kotlin/KotlinIT.java +++ b/spring-data-neo4j/src/test/java/org/neo4j/springframework/data/integration/kotlin/KotlinIT.java @@ -84,7 +84,7 @@ class KotlinIT { @Bean public Driver driver() { - return neo4jConnectionSupport.openConnection(); + return neo4jConnectionSupport.getDriver(); } public Collection getMappingBasePackages() { diff --git a/spring-data-neo4j/src/test/java/org/neo4j/springframework/data/integration/reactive/ReactiveAuditingIT.java b/spring-data-neo4j/src/test/java/org/neo4j/springframework/data/integration/reactive/ReactiveAuditingIT.java index f8e0d822d..3b1a33ee1 100644 --- a/spring-data-neo4j/src/test/java/org/neo4j/springframework/data/integration/reactive/ReactiveAuditingIT.java +++ b/spring-data-neo4j/src/test/java/org/neo4j/springframework/data/integration/reactive/ReactiveAuditingIT.java @@ -19,6 +19,7 @@ package org.neo4j.springframework.data.integration.reactive; import static org.assertj.core.api.Assertions.*; +import static org.neo4j.springframework.data.test.Neo4jExtension.*; import reactor.core.publisher.Mono; import reactor.test.StepVerifier; @@ -29,6 +30,7 @@ import java.util.Collections; import java.util.List; import java.util.Optional; +import org.junit.jupiter.api.Tag; import org.junit.jupiter.api.Test; import org.neo4j.driver.Driver; import org.neo4j.springframework.data.config.AbstractReactiveNeo4jConfig; @@ -50,6 +52,7 @@ import org.springframework.transaction.reactive.TransactionalOperator; /** * @author Michael J. Simons */ +@Tag(NEEDS_REACTIVE_SUPPORT) @ContextConfiguration(classes = ReactiveAuditingIT.Config.class) class ReactiveAuditingIT extends AuditingITBase { @@ -125,7 +128,7 @@ class ReactiveAuditingIT extends AuditingITBase { @Bean public Driver driver() { - return neo4jConnectionSupport.openConnection(); + return neo4jConnectionSupport.getDriver(); } @Override diff --git a/spring-data-neo4j/src/test/java/org/neo4j/springframework/data/integration/reactive/ReactiveCallbacksIT.java b/spring-data-neo4j/src/test/java/org/neo4j/springframework/data/integration/reactive/ReactiveCallbacksIT.java index 0322202af..1cebd24bc 100644 --- a/spring-data-neo4j/src/test/java/org/neo4j/springframework/data/integration/reactive/ReactiveCallbacksIT.java +++ b/spring-data-neo4j/src/test/java/org/neo4j/springframework/data/integration/reactive/ReactiveCallbacksIT.java @@ -18,6 +18,8 @@ */ package org.neo4j.springframework.data.integration.reactive; +import static org.neo4j.springframework.data.test.Neo4jExtension.*; + import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; import reactor.test.StepVerifier; @@ -28,6 +30,7 @@ import java.util.Collection; import java.util.Collections; import java.util.List; +import org.junit.jupiter.api.Tag; import org.junit.jupiter.api.Test; import org.neo4j.driver.Driver; import org.neo4j.springframework.data.config.AbstractReactiveNeo4jConfig; @@ -47,6 +50,7 @@ import org.springframework.transaction.reactive.TransactionalOperator; /** * @author Michael J. Simons */ +@Tag(NEEDS_REACTIVE_SUPPORT) @ContextConfiguration(classes = ReactiveCallbacksIT.Config.class) class ReactiveCallbacksIT extends CallbacksITBase { @@ -148,7 +152,7 @@ class ReactiveCallbacksIT extends CallbacksITBase { @Bean public Driver driver() { - return neo4jConnectionSupport.openConnection(); + return neo4jConnectionSupport.getDriver(); } @Override diff --git a/spring-data-neo4j/src/test/java/org/neo4j/springframework/data/integration/reactive/ReactiveIdGeneratorsIT.java b/spring-data-neo4j/src/test/java/org/neo4j/springframework/data/integration/reactive/ReactiveIdGeneratorsIT.java index 72de53449..da46380c0 100644 --- a/spring-data-neo4j/src/test/java/org/neo4j/springframework/data/integration/reactive/ReactiveIdGeneratorsIT.java +++ b/spring-data-neo4j/src/test/java/org/neo4j/springframework/data/integration/reactive/ReactiveIdGeneratorsIT.java @@ -20,6 +20,7 @@ package org.neo4j.springframework.data.integration.reactive; import static java.util.stream.Collectors.*; import static org.assertj.core.api.Assertions.*; +import static org.neo4j.springframework.data.test.Neo4jExtension.*; import reactor.core.publisher.Mono; import reactor.test.StepVerifier; @@ -32,6 +33,7 @@ import java.util.List; import java.util.Set; import java.util.stream.IntStream; +import org.junit.jupiter.api.Tag; import org.junit.jupiter.api.Test; import org.neo4j.driver.Driver; import org.neo4j.springframework.data.config.AbstractReactiveNeo4jConfig; @@ -52,6 +54,7 @@ import org.springframework.transaction.reactive.TransactionalOperator; /** * @author Michael J. Simons */ +@Tag(NEEDS_REACTIVE_SUPPORT) @ContextConfiguration(classes = ReactiveIdGeneratorsIT.Config.class) class ReactiveIdGeneratorsIT extends IdGeneratorsITBase { @@ -171,7 +174,7 @@ class ReactiveIdGeneratorsIT extends IdGeneratorsITBase { @Bean public Driver driver() { - return neo4jConnectionSupport.openConnection(); + return neo4jConnectionSupport.getDriver(); } @Override diff --git a/spring-data-neo4j/src/test/java/org/neo4j/springframework/data/integration/reactive/ReactiveRepositoryIT.java b/spring-data-neo4j/src/test/java/org/neo4j/springframework/data/integration/reactive/ReactiveRepositoryIT.java index 011d64680..63daf023d 100644 --- a/spring-data-neo4j/src/test/java/org/neo4j/springframework/data/integration/reactive/ReactiveRepositoryIT.java +++ b/spring-data-neo4j/src/test/java/org/neo4j/springframework/data/integration/reactive/ReactiveRepositoryIT.java @@ -19,6 +19,7 @@ package org.neo4j.springframework.data.integration.reactive; import static org.neo4j.driver.Values.*; +import static org.neo4j.springframework.data.test.Neo4jExtension.*; import lombok.extern.slf4j.Slf4j; import reactor.core.publisher.Flux; @@ -34,6 +35,7 @@ import java.util.List; import java.util.stream.IntStream; import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Tag; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.neo4j.driver.Driver; @@ -47,7 +49,7 @@ import org.neo4j.springframework.data.integration.shared.PersonWithAllConstructo import org.neo4j.springframework.data.integration.shared.ThingWithAssignedId; import org.neo4j.springframework.data.repository.config.EnableReactiveNeo4jRepositories; import org.neo4j.springframework.data.test.Neo4jExtension; -import org.neo4j.springframework.data.test.Neo4jExtension.Neo4jConnectionSupport; +import org.neo4j.springframework.data.test.Neo4jExtension.*; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @@ -64,6 +66,7 @@ import org.springframework.transaction.reactive.TransactionalOperator; * @author Gerrit Meier * @author Michael J. Simons */ +@Tag(NEEDS_REACTIVE_SUPPORT) @ExtendWith(Neo4jExtension.class) @ExtendWith(SpringExtension.class) @ContextConfiguration(classes = ReactiveRepositoryIT.Config.class) @@ -725,7 +728,7 @@ class ReactiveRepositoryIT { @Bean public Driver driver() { - return neo4jConnectionSupport.openConnection(); + return neo4jConnectionSupport.getDriver(); } @Override diff --git a/spring-data-neo4j/src/test/java/org/neo4j/springframework/data/test/Neo4jExtension.java b/spring-data-neo4j/src/test/java/org/neo4j/springframework/data/test/Neo4jExtension.java index feb83b636..28c472669 100644 --- a/spring-data-neo4j/src/test/java/org/neo4j/springframework/data/test/Neo4jExtension.java +++ b/spring-data-neo4j/src/test/java/org/neo4j/springframework/data/test/Neo4jExtension.java @@ -18,21 +18,33 @@ */ package org.neo4j.springframework.data.test; +import static org.assertj.core.api.Assumptions.*; + import lombok.extern.slf4j.Slf4j; import java.lang.reflect.Field; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashSet; import java.util.List; +import java.util.Locale; import java.util.Optional; +import java.util.Set; import org.junit.jupiter.api.extension.BeforeAllCallback; +import org.junit.jupiter.api.extension.BeforeEachCallback; import org.junit.jupiter.api.extension.ExtensionContext; import org.junit.platform.commons.util.ReflectionUtils; +import org.neo4j.driver.AccessMode; import org.neo4j.driver.AuthToken; import org.neo4j.driver.AuthTokens; import org.neo4j.driver.Config; import org.neo4j.driver.Driver; import org.neo4j.driver.GraphDatabase; import org.neo4j.driver.Logging; +import org.neo4j.driver.Session; +import org.neo4j.driver.internal.SessionConfig; +import org.neo4j.driver.internal.util.ServerVersion; import org.testcontainers.containers.Neo4jContainer; /** @@ -45,12 +57,21 @@ import org.testcontainers.containers.Neo4jContainer; * @since 1.0 */ @Slf4j -public class Neo4jExtension implements BeforeAllCallback { +public class Neo4jExtension implements BeforeAllCallback, BeforeEachCallback { + + public final static String NEEDS_REACTIVE_SUPPORT = "reactiveTest"; + public final static String COMMUNITY_EDITION_ONLY = "communityEdition"; + public final static String COMMERCIAL_EDITION_ONLY = "commercialEdition"; private static final ExtensionContext.Namespace NAMESPACE = ExtensionContext.Namespace .create(Neo4jExtension.class); - private static final String KEY = "neo4j.standalone"; + private static final String KEY_NEO4J_INSTANCE = "neo4j.standalone"; + private static final String KEY_DRIVER_INSTANCE = "neo4j.driver"; + + private static Set COMMUNITY_EDITION_INDICATOR = Collections.singleton("community"); + + private static Set COMMERCIAL_EDITION_INDICATOR = new HashSet<>(Arrays.asList("commercial", "enterprise")); @Override public void beforeAll(ExtensionContext context) throws Exception { @@ -66,30 +87,65 @@ public class Neo4jExtension implements BeforeAllCallback { String neo4jUrl = Optional.ofNullable(System.getenv("SDN_RX_NEO4J_URL")).orElse(""); String neo4jPassword = Optional.ofNullable(System.getenv("SDN_RX_NEO4J_PASSWORD")).orElse(""); - Neo4jConnectionSupport neo4jConnectionSupport; - if (!(neo4jUrl.isEmpty() || neo4jPassword.isEmpty())) { - log.warn("Using Neo4j instance at {}.", neo4jUrl); - neo4jConnectionSupport = new Neo4jConnectionSupport(neo4jUrl, - AuthTokens.basic("neo4j", neo4jPassword)); - } else { - log.warn("Using Neo4j test container."); - ExtensionContext.Store store = context.getStore(NAMESPACE); - ContainerAdapter adapter = store - .getOrComputeIfAbsent(KEY, key -> new Neo4jExtension.ContainerAdapter(), ContainerAdapter.class); - adapter.start(); - neo4jConnectionSupport = new Neo4jConnectionSupport(adapter.getBoltUrl(), AuthTokens.none()); + ExtensionContext.Store contextStore = context.getStore(NAMESPACE); + Neo4jConnectionSupport neo4jConnectionSupport = contextStore + .get(KEY_DRIVER_INSTANCE, Neo4jConnectionSupport.class); + + if (neo4jConnectionSupport == null) { + if (!(neo4jUrl.isEmpty() || neo4jPassword.isEmpty())) { + log.warn("Using Neo4j instance at {}.", neo4jUrl); + neo4jConnectionSupport = new Neo4jConnectionSupport(neo4jUrl, AuthTokens.basic("neo4j", neo4jPassword)); + } else { + log.warn("Using Neo4j test container."); + ContainerAdapter adapter = contextStore + .getOrComputeIfAbsent(KEY_NEO4J_INSTANCE, key -> new Neo4jExtension.ContainerAdapter(), + ContainerAdapter.class); + adapter.start(); + neo4jConnectionSupport = new Neo4jConnectionSupport(adapter.getBoltUrl(), AuthTokens.none()); + } + contextStore.put(KEY_DRIVER_INSTANCE, neo4jConnectionSupport); } + checkRequiredFeatures(neo4jConnectionSupport, context.getTags()); + Field field = injectableFields.get(0); field.setAccessible(true); field.set(null, neo4jConnectionSupport); } + @Override + public void beforeEach(ExtensionContext context) throws Exception { + ExtensionContext.Store contextStore = context.getStore(NAMESPACE); + Neo4jConnectionSupport neo4jConnectionSupport = contextStore + .get(KEY_DRIVER_INSTANCE, Neo4jConnectionSupport.class); + checkRequiredFeatures(neo4jConnectionSupport, context.getTags()); + } + + private void checkRequiredFeatures(Neo4jConnectionSupport neo4jConnectionSupport, Set tags) { + if (tags.contains(NEEDS_REACTIVE_SUPPORT)) { + assumeThat(neo4jConnectionSupport.getServerVersion().greaterThanOrEqual(ServerVersion.v4_0_0)) + .describedAs("This test requires at least Neo4j 4.0 for reactive database connectivity.") + .isTrue(); + } + + if (tags.contains(COMMUNITY_EDITION_ONLY)) { + assumeThat(neo4jConnectionSupport.isCommunityEdition()) + .describedAs("This test should be run on the community edition only") + .isTrue(); + } + + if (tags.contains(COMMERCIAL_EDITION_ONLY)) { + assumeThat(neo4jConnectionSupport.isCommercialEdition()) + .describedAs("This test should be run on the commercial edition only") + .isTrue(); + } + } + /** * Support class that holds the connection information and opens a new connection on demand. * @since 1.0 */ - public static class Neo4jConnectionSupport { + public static class Neo4jConnectionSupport implements ExtensionContext.Store.CloseableResource { public final String url; @@ -97,14 +153,88 @@ public class Neo4jExtension implements BeforeAllCallback { public final Config config; + public volatile ServerVersion cachedServerVersion; + + /** + * Shared instance of the standard (non-routing) driver. + */ + public volatile Driver driverInstance; + public Neo4jConnectionSupport(String url, AuthToken authToken) { this.url = url; this.authToken = authToken; this.config = Config.builder().withLogging(Logging.slf4j()).build(); } - public Driver openConnection() { - return GraphDatabase.driver(url, authToken, config); + /** + * @return A possible shared driver instance, connected to either a database running inside test containers or + * running locally. + */ + public Driver getDriver() { + + Driver driver = this.driverInstance; + if (driver == null) { + synchronized (this) { + driver = this.driverInstance; + if (driver == null) { + this.driverInstance = GraphDatabase.driver(url, authToken, config); + driver = this.driverInstance; + } + } + } + return driver; + } + + ServerVersion getServerVersion() { + + ServerVersion serverVersion = this.cachedServerVersion; + if (serverVersion == null) { + synchronized (this) { + serverVersion = this.cachedServerVersion; + if (serverVersion == null) { + this.cachedServerVersion = ServerVersion.version(getDriver()); + serverVersion = this.cachedServerVersion; + } + } + } + + return serverVersion; + } + + String getEdition() { + String edition = "n/a"; + SessionConfig sessionConfig = SessionConfig.builder().withDefaultAccessMode(AccessMode.READ).build(); + try (Session session = getDriver().session( + sessionConfig)) { + edition = session.run("call dbms.components() yield edition").single().get("edition").asString(); + } + return edition.toLowerCase(Locale.ENGLISH); + } + + boolean isCommunityEdition() { + + return COMMUNITY_EDITION_INDICATOR.contains(getEdition()); + } + + boolean isCommercialEdition() { + + return COMMERCIAL_EDITION_INDICATOR.contains(getEdition()); + } + + @Override + public void close() { + + // Don't open up a driver for just closing it + if (driverInstance == null) { + return; + } + + // Catch all the things... The driver has been closed maybe by a Spring Context already + try { + log.debug("Closing Neo4j connection support."); + driverInstance.close(); + } catch (Exception e) { + } } }