Files
ngit-grasp/tests/common/relay.rs
T
DanConwayDev f62ef12fb8 refactor: replace inline purgatory sync registration with timer-only approach
Remove the redundant inline kind-30617 registration block from the sync
event loop and the three is_generic/recompute_new_sync_filters_for_relay
calls from confirm_batch error paths. The purgatory announcement sync
timer (run_purgatory_announcement_sync) is now the sole registration path.

Consolidate NGIT_SYNC_BATCH_WINDOW_MS and NGIT_PURGATORY_SYNC_INTERVAL_MS
into a single NGIT_TEST=1 flag that sets both timers to 200ms, replacing
two ad-hoc env vars with one reusable test-mode flag.
2026-02-23 12:48:26 +00:00

356 lines
12 KiB
Rust

//! Test relay fixture
//!
//! Provides automatic relay lifecycle management for integration tests.
use nostr_sdk::ToBech32;
use std::path::PathBuf;
use std::process::{Child, Command, Stdio};
use std::time::Duration;
use tokio::time::sleep;
/// Test relay fixture that manages relay lifecycle
///
/// Automatically starts and stops the ngit-grasp relay for testing.
/// Uses a random port to avoid conflicts and cleans up created repositories.
pub struct TestRelay {
process: Child,
url: String,
port: u16,
/// Temporary directory for git repositories
/// Kept alive for the lifetime of the relay
_git_data_dir: tempfile::TempDir,
/// Path to git data directory (for test assertions)
git_data_path: PathBuf,
}
impl TestRelay {
/// Start a test relay instance
///
/// # Example
///
/// ```no_run
/// use common::TestRelay;
///
/// #[tokio::test]
/// async fn test_something() {
/// let relay = TestRelay::start().await;
/// // Use relay.url() for testing
/// relay.stop().await;
/// }
/// ```
pub async fn start() -> Self {
Self::start_with_options(Self::find_free_port(), None).await
}
/// Start relay on a specific port
pub async fn start_with_port(port: u16) -> Self {
Self::start_with_full_options(port, None, false).await
}
/// Start relay on a specific port with full options
///
/// This is useful for testing history sync where we need to:
/// 1. Start relay_b (first instance) to get its domain
/// 2. Stop relay_b
/// 3. Start relay_b (second instance) on SAME port with different options
pub async fn start_on_port_with_options(
port: u16,
bootstrap_relay_url: Option<String>,
disable_negentropy: bool,
) -> Self {
Self::start_with_full_options(port, bootstrap_relay_url, disable_negentropy).await
}
/// Start relay with sync from another relay (bootstrap relay)
///
/// # Example
///
/// ```no_run
/// use common::TestRelay;
///
/// #[tokio::test]
/// async fn test_sync() {
/// let source = TestRelay::start().await;
/// let syncing = TestRelay::start_with_sync(source.url()).await;
/// // ... test sync behavior ...
/// syncing.stop().await;
/// source.stop().await;
/// }
/// ```
pub async fn start_with_sync(bootstrap_relay_url: Option<String>) -> Self {
Self::start_with_full_options(Self::find_free_port(), bootstrap_relay_url, false).await
}
/// Start relay with sync and negentropy disabled
///
/// This is useful for testing that sync works without NIP-77 negentropy.
/// History sync will use REQ+EOSE instead of the more efficient negentropy protocol.
///
/// # Example
///
/// ```no_run
/// use common::TestRelay;
///
/// #[tokio::test]
/// async fn test_sync_without_negentropy() {
/// let source = TestRelay::start().await;
/// let syncing = TestRelay::start_with_sync_no_negentropy(Some(source.url().into())).await;
/// // ... test sync behavior without negentropy ...
/// syncing.stop().await;
/// source.stop().await;
/// }
/// ```
pub async fn start_with_sync_no_negentropy(bootstrap_relay_url: Option<String>) -> Self {
Self::start_with_full_options(Self::find_free_port(), bootstrap_relay_url, true).await
}
/// Start relay with archive configuration
///
/// This is useful for testing GRASP-05 archive mode behavior.
///
/// # Arguments
/// * `archive_all` - Accept all repository announcements (GRASP-05)
/// * `archive_read_only` - Reject git pushes (read-only archive mode)
///
/// # Example
///
/// ```no_run
/// use common::TestRelay;
///
/// #[tokio::test]
/// async fn test_archive_mode() {
/// let relay = TestRelay::start_with_archive_config(true, true).await;
/// // ... test archive behavior ...
/// relay.stop().await;
/// }
/// ```
pub async fn start_with_archive_config(archive_all: bool, archive_read_only: bool) -> Self {
Self::start_with_archive_and_sync(
Self::find_free_port(),
None,
false,
archive_all,
archive_read_only,
)
.await
}
/// Start relay with options (internal, maintains backward compatibility)
async fn start_with_options(port: u16, bootstrap_relay_url: Option<String>) -> Self {
Self::start_with_full_options(port, bootstrap_relay_url, false).await
}
/// Start relay with full options
async fn start_with_full_options(
port: u16,
bootstrap_relay_url: Option<String>,
disable_negentropy: bool,
) -> Self {
Self::start_with_archive_and_sync(
port,
bootstrap_relay_url,
disable_negentropy,
false,
false,
)
.await
}
/// Start relay with all options including archive configuration and sync
///
/// This is the most flexible method for starting a test relay with all options.
/// Use this when you need both archive mode AND sync from a bootstrap relay.
///
/// # Arguments
/// * `port` - Port to bind to
/// * `bootstrap_relay_url` - URL of relay to sync from (optional)
/// * `disable_negentropy` - Whether to disable NIP-77 negentropy sync
/// * `archive_all` - Accept all repository announcements (GRASP-05)
/// * `archive_read_only` - Reject git pushes (read-only archive mode)
pub async fn start_with_archive_and_sync(
port: u16,
bootstrap_relay_url: Option<String>,
disable_negentropy: bool,
archive_all: bool,
archive_read_only: bool,
) -> Self {
let bind_address = format!("127.0.0.1:{}", port);
let url = format!("ws://127.0.0.1:{}", port);
// Create temporary directory for git repositories
let git_data_dir =
tempfile::tempdir().expect("Failed to create temporary git data directory");
// Use the built binary directly (faster than cargo run)
let binary_path = std::env::current_exe()
.expect("Failed to get current exe")
.parent()
.expect("Failed to get parent dir")
.parent()
.expect("Failed to get grandparent dir")
.join("ngit-grasp");
// Generate a test owner npub (using a random keypair)
let test_keys = nostr_sdk::Keys::generate();
let test_npub = test_keys
.public_key()
.to_bech32()
.expect("Failed to generate test npub");
// Start the relay process
let mut cmd = Command::new(&binary_path);
cmd.env("NGIT_BIND_ADDRESS", &bind_address)
.env("NGIT_DOMAIN", &bind_address) // Set domain to match bind address
.env("NGIT_GIT_DATA_PATH", git_data_dir.path())
.env("NGIT_DATABASE_BACKEND", "memory") // Force in-memory database for isolation
.env("NGIT_OWNER_NPUB", &test_npub)
.env("NGIT_TEST", "1") // Enable test mode: fast timers (200ms batch window, 200ms purgatory sync)
.env("NGIT_SYNC_STARTUP_DELAY_SECS", "0") // No startup delay for faster tests
.env("NGIT_SYNC_STARTUP_JITTER_MS", "0") // No jitter for tests
.env("NGIT_SYNC_DISCONNECT_CHECK_INTERVAL_SECS", "1") // Fast reconnect attempts for tests
.env("NGIT_SYNC_BASE_BACKOFF_SECS", "1") // Fast backoff for tests (1s instead of 5s default)
.env(
"RUST_LOG",
std::env::var("RUST_LOG").unwrap_or_else(|_| "info".to_string()),
) // Use RUST_LOG from environment or default to info
.stdout(
std::fs::OpenOptions::new()
.create(true)
.append(true)
.open(format!("/tmp/relay-{}.log", port))
.map(Stdio::from)
.unwrap_or(Stdio::null()),
)
.stderr(Stdio::inherit()); // Inherit stderr for test output
// Add bootstrap relay URL if provided
if let Some(ref bootstrap_url) = bootstrap_relay_url {
cmd.env("NGIT_SYNC_BOOTSTRAP_RELAY_URL", bootstrap_url);
}
// Add negentropy disable flag if requested
if disable_negentropy {
cmd.env("NGIT_SYNC_DISABLE_NEGENTROPY", "true");
}
// Add archive configuration if requested
if archive_all {
cmd.env("NGIT_ARCHIVE_ALL", "true");
}
if archive_read_only {
cmd.env("NGIT_ARCHIVE_READ_ONLY", "true");
}
let process = cmd.spawn().expect("Failed to start relay process");
// Store git data path for test assertions
let git_data_path = git_data_dir.path().to_path_buf();
let relay = Self {
process,
url,
port,
_git_data_dir: git_data_dir,
git_data_path,
};
// Wait for relay to be ready
relay.wait_for_ready().await;
relay
}
/// Get the relay WebSocket URL
pub fn url(&self) -> &str {
&self.url
}
/// Get the relay domain (host:port)
pub fn domain(&self) -> String {
format!("127.0.0.1:{}", self.port)
}
/// Get the git data directory path
///
/// This is useful for test assertions that need to verify
/// git repositories were created correctly.
pub fn git_data_path(&self) -> &PathBuf {
&self.git_data_path
}
/// Wait for the relay to be ready to accept connections
async fn wait_for_ready(&self) {
let max_attempts = 50; // 5 seconds total
let delay = Duration::from_millis(100);
for attempt in 0..max_attempts {
// Try to connect to the relay
match tokio::net::TcpStream::connect(format!("127.0.0.1:{}", self.port)).await {
Ok(_) => {
// Connection successful, relay is ready
// Give it a tiny bit more time to fully initialize
sleep(Duration::from_millis(100)).await;
return;
}
Err(_) => {
if attempt == max_attempts - 1 {
panic!("Relay failed to start after {} attempts", max_attempts);
}
sleep(delay).await;
}
}
}
}
/// Stop the relay
pub async fn stop(mut self) {
// Kill the process (gracefully if possible)
let _ = self.process.kill();
// Wait a bit for graceful shutdown
sleep(Duration::from_millis(100)).await;
// Force kill if still running
let _ = self.process.kill();
let _ = self.process.wait();
}
/// Find a free port to use for testing
pub fn find_free_port() -> u16 {
use std::net::TcpListener;
// Bind to port 0 to get a random free port
let listener = TcpListener::bind("127.0.0.1:0").expect("Failed to bind to random port");
let port = listener
.local_addr()
.expect("Failed to get local address")
.port();
// Drop the listener to free the port
drop(listener);
port
}
}
impl Drop for TestRelay {
fn drop(&mut self) {
// Ensure process is killed when TestRelay is dropped
let _ = self.process.kill();
let _ = self.process.wait();
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_find_free_port() {
let port = TestRelay::find_free_port();
assert!(port > 0);
// Port is u16, so it's always < 65536
}
}