use std::time::Duration;
use rightsize::{Container, ContainerGuard, Result, Wait};
const HTTP_PORT: u16 = 7474;
const BOLT_PORT: u16 = 7687;
pub struct Neo4jContainer {
container: Container,
password: String,
}
impl Neo4jContainer {
pub fn new() -> Self {
Self::with_image("neo4j:5-community")
}
pub fn with_image(image: &str) -> Self {
let password = "rightsize-test".to_string();
let container = Container::new(image)
.with_exposed_ports(&[HTTP_PORT, BOLT_PORT])
.with_env("NEO4J_AUTH", &format!("neo4j/{password}"))
.with_memory_limit(1024)
.waiting_for(
Wait::for_log_message(r".*Started\..*", 1)
.with_startup_timeout(Duration::from_secs(120)),
);
Self {
container,
password,
}
}
pub fn with_password(mut self, password: &str) -> Self {
self.password = password.to_string();
self.container = self
.container
.with_env("NEO4J_AUTH", &format!("neo4j/{password}"));
self
}
pub async fn start(self) -> Result<Neo4jGuard> {
crate::register_default_backends();
let guard = self.container.start().await?;
Ok(Neo4jGuard {
guard,
password: self.password,
})
}
}
impl Default for Neo4jContainer {
fn default() -> Self {
Self::new()
}
}
pub struct Neo4jGuard {
guard: ContainerGuard,
password: String,
}
impl Neo4jGuard {
pub fn username(&self) -> &str {
"neo4j"
}
pub fn password(&self) -> &str {
&self.password
}
pub fn http_url(&self) -> String {
format!(
"http://{}:{}",
self.guard.host(),
self.guard.get_mapped_port(HTTP_PORT).unwrap()
)
}
pub fn bolt_url(&self) -> String {
format!(
"bolt://{}:{}",
self.guard.host(),
self.guard.get_mapped_port(BOLT_PORT).unwrap()
)
}
pub async fn stop(self) -> Result<()> {
self.guard.stop().await
}
}
impl std::ops::Deref for Neo4jGuard {
type Target = ContainerGuard;
fn deref(&self) -> &ContainerGuard {
&self.guard
}
}
#[cfg(test)]
mod tests {
use super::*;
use rightsize::wait::WaitTarget;
#[test]
fn default_password_is_rightsize_test() {
let c = Neo4jContainer::new();
assert_eq!(c.password, "rightsize-test");
}
#[test]
fn with_password_overrides_the_default() {
let c = Neo4jContainer::new().with_password("s3cret123");
assert_eq!(c.password, "s3cret123");
}
#[tokio::test]
async fn readiness_pattern_matches_the_captured_started_line() {
struct FakeTarget(String);
#[async_trait::async_trait]
impl WaitTarget for FakeTarget {
fn host(&self) -> &str {
"127.0.0.1"
}
fn mapped_port(&self, guest_port: u16) -> u16 {
guest_port
}
fn exposed_guest_ports(&self) -> Vec<u16> {
vec![HTTP_PORT, BOLT_PORT]
}
async fn current_logs(&self) -> String {
self.0.clone()
}
fn describe(&self) -> String {
"fake-neo4j".to_string()
}
}
let captured_log = "\
... INFO Bolt enabled on 0.0.0.0:7687.
... INFO HTTP enabled on 0.0.0.0:7474.
... INFO Remote interface available at http://localhost:7474/
... INFO Started.";
let target = FakeTarget(captured_log.to_string());
let strategy = rightsize::Wait::for_log_message(r".*Started\..*", 1);
strategy
.wait_until_ready(&target)
.await
.expect("the escaped pattern must match the real captured log line");
}
#[tokio::test]
async fn escaped_dot_does_not_match_an_arbitrary_character_in_its_place() {
struct FakeTarget(&'static str);
#[async_trait::async_trait]
impl WaitTarget for FakeTarget {
fn host(&self) -> &str {
"127.0.0.1"
}
fn mapped_port(&self, guest_port: u16) -> u16 {
guest_port
}
fn exposed_guest_ports(&self) -> Vec<u16> {
vec![]
}
async fn current_logs(&self) -> String {
self.0.to_string()
}
fn describe(&self) -> String {
"fake-neo4j".to_string()
}
}
let target = FakeTarget("... INFO Startedx");
let err = rightsize::Wait::for_log_message(r".*Started\..*", 1)
.with_startup_timeout(Duration::from_millis(300))
.wait_until_ready(&target)
.await
.expect_err("a non-dot character where the escaped dot is must not match");
assert!(err.to_string().contains(r"Started\..*' x1"));
}
}