mongreldb-client 0.64.2

HTTP client library for MongrelDB Server.
Documentation
use std::io::{Read, Write};
use std::net::TcpListener;

use mongreldb_client::ReplicationFollower;

fn temporary_path(label: &str) -> std::path::PathBuf {
    std::env::temp_dir().join(format!(
        "mongreldb-client-{label}-{}-{}",
        std::process::id(),
        std::time::SystemTime::now()
            .duration_since(std::time::UNIX_EPOCH)
            .unwrap()
            .as_nanos()
    ))
}

#[test]
fn replication_follower_rejects_unsafe_urls_and_redacts_secrets() {
    let directory = std::env::temp_dir().join("mongreldb-client-replication-security");
    for url in [
        "file:///tmp/leader",
        "http://user:password@leader:8453",
        "http://leader:8453?token=secret",
        "http://leader:8453/#secret",
    ] {
        assert!(ReplicationFollower::new(url, &directory).is_err());
    }

    let follower = ReplicationFollower::new("https://leader:8453/", &directory)
        .unwrap()
        .with_bearer_token("bearer-secret")
        .with_basic_auth("remote-user", "remote-secret")
        .with_local_encryption_passphrase("local-passphrase")
        .with_local_credentials("local-user", "local-secret");
    let debug = format!("{follower:?}");
    assert!(debug.contains("https://leader:8453"));
    assert!(debug.contains("remote-user"));
    assert!(debug.contains("local-user"));
    for secret in [
        "bearer-secret",
        "remote-secret",
        "local-passphrase",
        "local-secret",
    ] {
        assert!(!debug.contains(secret));
    }
}

#[test]
fn replication_snapshot_epoch_header_must_match_payload() {
    let source_path = temporary_path("snapshot-source");
    let target_path = temporary_path("snapshot-target");
    let source = mongreldb_core::Database::create(&source_path).unwrap();
    let snapshot = source.replication_snapshot().unwrap();
    let body = snapshot.encode().unwrap();
    let source_id = snapshot
        .source_id()
        .iter()
        .map(|byte| format!("{byte:02x}"))
        .collect::<String>();
    let wrong_epoch = snapshot.epoch().saturating_add(1);

    let listener = TcpListener::bind("127.0.0.1:0").unwrap();
    let address = listener.local_addr().unwrap();
    let server = std::thread::spawn(move || {
        let (mut stream, _) = listener.accept().unwrap();
        let mut request = [0_u8; 4096];
        let read = stream.read(&mut request).unwrap();
        assert!(String::from_utf8_lossy(&request[..read]).starts_with("GET /replication/snapshot "));
        write!(
            stream,
            concat!(
                "HTTP/1.1 200 OK\r\n",
                "x-mongreldb-source-id: {}\r\n",
                "x-mongreldb-current-epoch: {}\r\n",
                "Content-Length: {}\r\n",
                "Connection: close\r\n\r\n"
            ),
            source_id,
            wrong_epoch,
            body.len()
        )
        .unwrap();
        stream.write_all(&body).unwrap();
    });

    let error = ReplicationFollower::new(&format!("http://{address}"), &target_path)
        .unwrap()
        .bootstrap()
        .unwrap_err();
    assert!(error.contains("epoch header does not match"));
    assert!(!target_path.exists());
    server.join().unwrap();
    drop(source);
    std::fs::remove_dir_all(source_path).unwrap();
}