//! This is a helper module for using a Neo4J [testcontainers::Container], which uses Docker. //! //! It also supports connecting to a real server using environment variables. //! //! [Original source](https://github.com/neo4j-labs/neo4rs/blob/ec0261895f56e476f4f1eb9c6a2151c7b945d454/lib/tests/container.rs) use lenient_semver::Version; use neo4rs::{ConfigBuilder, Graph}; use testcontainers::{clients::Cli, Container, RunnableImage}; use testcontainers_modules::neo4j::{Neo4j, Neo4jImage}; use std::{error::Error, io::BufRead as _}; #[allow(dead_code)] #[derive(Default)] pub struct Neo4jContainerBuilder { enterprise: bool, config: ConfigBuilder, } #[allow(dead_code)] impl Neo4jContainerBuilder { pub fn new() -> Self { Self::default() } pub fn with_enterprise_edition(mut self) -> Self { self.enterprise = true; self } pub fn with_config(mut self, config: ConfigBuilder) -> Self { self.config = config; self } pub fn modify_config(mut self, block: impl FnOnce(ConfigBuilder) -> ConfigBuilder) -> Self { self.config = block(self.config); self } pub async fn start(self) -> Result> { Neo4jContainer::from_config_and_edition(self.config, self.enterprise).await } } pub struct Neo4jContainer { graph: Graph, version: String, _container: Option>, } impl Neo4jContainer { #[allow(dead_code)] pub async fn new() -> Self { Self::from_config(ConfigBuilder::default()).await } pub async fn from_config(config: ConfigBuilder) -> Self { Self::from_config_and_edition(config, false).await.unwrap() } pub async fn from_config_and_edition( config: ConfigBuilder, enterprise_edition: bool, ) -> Result> { let _ = pretty_env_logger::try_init(); let connection = Self::create_test_endpoint(); let server = Self::server_from_env(); let (uri, _container) = match server { TestServer::TestContainer => { let (uri, container) = Self::create_testcontainer(&connection, enterprise_edition)?; (uri, Some(container)) } TestServer::External(uri) => (uri, None), }; let version = connection.version; let graph = Self::connect(config, uri, &connection.auth).await; Ok(Self { graph, version, _container, }) } pub fn graph(&self) -> Graph { self.graph.clone() } #[allow(dead_code)] pub fn version(&self) -> Version<'static> { Version::parse(&self.version) .unwrap() .disassociate_metadata() .0 } fn server_from_env() -> TestServer { const TEST_URI_VAR: &str = "NEO4J_TEST_URI"; if let Ok(uri) = std::env::var(TEST_URI_VAR) { TestServer::External(uri) } else { TestServer::TestContainer } } fn create_testcontainer( connection: &TestConnection, enterprise: bool, ) -> Result<(String, Container<'static, Neo4jImage>), Box> { let image = Neo4j::new() .with_user(connection.auth.user.to_owned()) .with_password(connection.auth.pass.to_owned()); let docker = Cli::default(); let docker = Box::leak(Box::new(docker)); let container = if enterprise { const ACCEPTANCE_FILE_NAME: &str = "container-license-acceptance.txt"; let version = format!("{}-enterprise", connection.version); let image_name = format!("neo4j:{}", version); let acceptance_file = std::env::current_dir() .ok() .map(|o| o.join(ACCEPTANCE_FILE_NAME)); let has_license_acceptance = acceptance_file .as_deref() .and_then(|o| std::fs::File::open(o).ok()) .into_iter() .flat_map(|o| std::io::BufReader::new(o).lines()) .any(|o| o.map_or(false, |line| line.trim() == image_name)); if !has_license_acceptance { return Err(format!( concat!( "You need to accept the Neo4j Enterprise Edition license by ", "creating the file `{}` with the following content:\n\n\t{}", ), acceptance_file.map_or_else( || ACCEPTANCE_FILE_NAME.to_owned(), |o| { o.display().to_string() } ), image_name ) .into()); } let image: RunnableImage = image.with_version(version).into(); let image = image.with_env_var(("NEO4J_ACCEPT_LICENSE_AGREEMENT", "yes")); docker.run(image) } else { docker.run(image.with_version(connection.version.to_owned())) }; let uri = format!("bolt://127.0.0.1:{}", container.image().bolt_port_ipv4()); Ok((uri, container)) } fn create_test_endpoint() -> TestConnection { const USER_VAR: &str = "NEO4J_TEST_USER"; const PASS_VAR: &str = "NEO4J_TEST_PASS"; const VERSION_VAR: &str = "NEO4J_VERSION_TAG"; const DEFAULT_USER: &str = "neo4j"; const DEFAULT_PASS: &str = "neo"; const DEFAULT_VERSION_TAG: &str = "5"; use std::env::var; let user = var(USER_VAR).unwrap_or_else(|_| DEFAULT_USER.to_owned()); let pass = var(PASS_VAR).unwrap_or_else(|_| DEFAULT_PASS.to_owned()); let auth = TestAuth { user, pass }; let version = var(VERSION_VAR).unwrap_or_else(|_| DEFAULT_VERSION_TAG.to_owned()); TestConnection { auth, version } } async fn connect(config: ConfigBuilder, uri: String, auth: &TestAuth) -> Graph { let config = config .uri(uri) .user(&auth.user) .password(&auth.pass) .build() .unwrap(); Graph::connect(config).await.unwrap() } } struct TestAuth { user: String, pass: String, } struct TestConnection { version: String, auth: TestAuth, } enum TestServer { TestContainer, External(String), }