- Add hostname-based request routing for multiple capsules per server - Parse virtual host configs from TOML sections ([hostname]) - Implement per-host certificate and content isolation - Add comprehensive virtual host testing and validation - Update docs and examples for multi-host deployments This enables Pollux to serve multiple Gemini domains from one instance, providing the foundation for multi-tenant Gemini hosting.
298 lines
9.5 KiB
Rust
298 lines
9.5 KiB
Rust
mod common;
|
|
|
|
use std::sync::Arc;
|
|
use std::thread;
|
|
use std::time::{Duration, Instant};
|
|
|
|
#[test]
|
|
fn test_concurrent_requests_multiple_hosts() {
|
|
let temp_dir = common::setup_test_environment();
|
|
|
|
// Create content for multiple hosts
|
|
let hosts = vec!["site1.com", "site2.org", "site3.net"];
|
|
let mut host_roots = Vec::new();
|
|
|
|
for host in &hosts {
|
|
let root_dir = temp_dir.path().join(host.replace(".", "_"));
|
|
std::fs::create_dir(&root_dir).unwrap();
|
|
std::fs::write(
|
|
root_dir.join("index.gmi"),
|
|
format!("Welcome to {}", host),
|
|
).unwrap();
|
|
host_roots.push(root_dir);
|
|
}
|
|
|
|
// Create config with multiple hosts
|
|
let config_path = temp_dir.path().join("config.toml");
|
|
let port = 1969 + (std::process::id() % 1000) as u16;
|
|
|
|
let mut config_content = format!(r#"
|
|
bind_host = "127.0.0.1"
|
|
port = {}
|
|
|
|
"#, port);
|
|
|
|
for (i, host) in hosts.iter().enumerate() {
|
|
config_content.push_str(&format!(r#"
|
|
["{}"]
|
|
root = "{}"
|
|
cert = "{}"
|
|
key = "{}"
|
|
"#,
|
|
host,
|
|
host_roots[i].display(),
|
|
temp_dir.path().join("cert.pem").display(),
|
|
temp_dir.path().join("key.pem").display()));
|
|
}
|
|
|
|
std::fs::write(&config_path, config_content).unwrap();
|
|
|
|
// Start server with TLS
|
|
let mut server_process = std::process::Command::new(env!("CARGO_BIN_EXE_pollux"))
|
|
.arg("--config")
|
|
.arg(&config_path)
|
|
.spawn()
|
|
.unwrap();
|
|
|
|
std::thread::sleep(Duration::from_millis(500));
|
|
|
|
// Spawn multiple threads making concurrent requests
|
|
let mut handles = Vec::new();
|
|
let port_arc = Arc::new(port);
|
|
|
|
for i in 0..10 {
|
|
let host = hosts[i % hosts.len()].to_string();
|
|
let port_clone = Arc::clone(&port_arc);
|
|
|
|
let handle = thread::spawn(move || {
|
|
let response = make_gemini_request("127.0.0.1", *port_clone, &format!("gemini://{}/", host));
|
|
assert!(response.starts_with("20"), "Request {} failed: {}", i, response);
|
|
assert!(response.contains(&format!("Welcome to {}", host)), "Wrong content for request {}: {}", i, response);
|
|
response
|
|
});
|
|
|
|
handles.push(handle);
|
|
}
|
|
|
|
// Collect results
|
|
let mut results = Vec::new();
|
|
for handle in handles {
|
|
results.push(handle.join().unwrap());
|
|
}
|
|
|
|
assert_eq!(results.len(), 10, "All concurrent requests should complete");
|
|
|
|
server_process.kill().unwrap();
|
|
}
|
|
|
|
#[test]
|
|
fn test_mixed_valid_invalid_hostnames() {
|
|
let temp_dir = common::setup_test_environment();
|
|
|
|
// Create content for one valid host
|
|
let root_dir = temp_dir.path().join("valid_site");
|
|
std::fs::create_dir(&root_dir).unwrap();
|
|
std::fs::write(root_dir.join("index.gmi"), "Valid site content").unwrap();
|
|
|
|
// Create config
|
|
let config_path = temp_dir.path().join("config.toml");
|
|
let port = 1970 + (std::process::id() % 1000) as u16;
|
|
let config_content = format!(r#"
|
|
bind_host = "127.0.0.1"
|
|
port = {}
|
|
|
|
["valid.com"]
|
|
root = "{}"
|
|
cert = "{}"
|
|
key = "{}"
|
|
"#,
|
|
port,
|
|
root_dir.display(),
|
|
temp_dir.path().join("cert.pem").display(),
|
|
temp_dir.path().join("key.pem").display());
|
|
std::fs::write(&config_path, config_content).unwrap();
|
|
|
|
// Start server with TLS
|
|
let mut server_process = std::process::Command::new(env!("CARGO_BIN_EXE_pollux"))
|
|
.arg("--config")
|
|
.arg(&config_path)
|
|
.spawn()
|
|
.unwrap();
|
|
|
|
std::thread::sleep(Duration::from_millis(500));
|
|
|
|
// Test valid hostname
|
|
let valid_response = make_gemini_request("127.0.0.1", port, "gemini://valid.com/");
|
|
assert!(valid_response.starts_with("20"), "Valid host should work: {}", valid_response);
|
|
assert!(valid_response.contains("Valid site content"), "Should serve correct content: {}", valid_response);
|
|
|
|
// Test various invalid hostnames
|
|
let invalid_hosts = vec![
|
|
"invalid.com",
|
|
"unknown.net",
|
|
"nonexistent.invalid",
|
|
"site.with.dots.com",
|
|
];
|
|
|
|
for invalid_host in invalid_hosts {
|
|
let response = make_gemini_request("127.0.0.1", port, &format!("gemini://{}/", invalid_host));
|
|
assert!(response.starts_with("53"), "Invalid host '{}' should return 53, got: {}", invalid_host, response);
|
|
}
|
|
|
|
server_process.kill().unwrap();
|
|
}
|
|
|
|
#[test]
|
|
fn test_load_performance_basic() {
|
|
let temp_dir = common::setup_test_environment();
|
|
|
|
// Create a simple host
|
|
let root_dir = temp_dir.path().join("perf_test");
|
|
std::fs::create_dir(&root_dir).unwrap();
|
|
std::fs::write(root_dir.join("index.gmi"), "Performance test content").unwrap();
|
|
|
|
let config_path = temp_dir.path().join("config.toml");
|
|
let port = 1971 + (std::process::id() % 1000) as u16;
|
|
let config_content = format!(r#"
|
|
bind_host = "127.0.0.1"
|
|
port = {}
|
|
|
|
["perf.com"]
|
|
root = "{}"
|
|
cert = "{}"
|
|
key = "{}"
|
|
"#,
|
|
port,
|
|
root_dir.display(),
|
|
temp_dir.path().join("cert.pem").display(),
|
|
temp_dir.path().join("key.pem").display());
|
|
std::fs::write(&config_path, config_content).unwrap();
|
|
|
|
// Start server with TLS
|
|
let mut server_process = std::process::Command::new(env!("CARGO_BIN_EXE_pollux"))
|
|
.arg("--config")
|
|
.arg(&config_path)
|
|
.spawn()
|
|
.unwrap();
|
|
|
|
std::thread::sleep(Duration::from_millis(500));
|
|
|
|
// Measure time for multiple requests
|
|
let start = Instant::now();
|
|
const NUM_REQUESTS: usize = 50;
|
|
|
|
for i in 0..NUM_REQUESTS {
|
|
let response = make_gemini_request("127.0.0.1", port, "gemini://perf.com/");
|
|
assert!(response.starts_with("20"), "Request {} failed: {}", i, response);
|
|
}
|
|
|
|
let elapsed = start.elapsed();
|
|
let avg_time = elapsed.as_millis() as f64 / NUM_REQUESTS as f64;
|
|
|
|
println!("Processed {} requests in {:?} (avg: {:.2}ms per request)",
|
|
NUM_REQUESTS, elapsed, avg_time);
|
|
|
|
// Basic performance check - should be reasonably fast
|
|
assert!(avg_time < 100.0, "Average request time too slow: {:.2}ms", avg_time);
|
|
|
|
server_process.kill().unwrap();
|
|
}
|
|
|
|
#[test]
|
|
fn test_full_request_lifecycle() {
|
|
let temp_dir = common::setup_test_environment();
|
|
|
|
// Create complex content structure
|
|
let root_dir = temp_dir.path().join("lifecycle_test");
|
|
std::fs::create_dir(&root_dir).unwrap();
|
|
|
|
// Create directory with index
|
|
let blog_dir = root_dir.join("blog");
|
|
std::fs::create_dir(&blog_dir).unwrap();
|
|
std::fs::write(blog_dir.join("index.gmi"), "Blog index content").unwrap();
|
|
|
|
// Create individual file
|
|
std::fs::write(root_dir.join("about.gmi"), "About page content").unwrap();
|
|
|
|
// Create root index
|
|
std::fs::write(root_dir.join("index.gmi"), "Main site content").unwrap();
|
|
|
|
let config_path = temp_dir.path().join("config.toml");
|
|
let port = 1972 + (std::process::id() % 1000) as u16;
|
|
let cert_path = temp_dir.path().join("cert.pem");
|
|
let key_path = temp_dir.path().join("key.pem");
|
|
|
|
let config_content = format!(r#"
|
|
bind_host = "127.0.0.1"
|
|
port = {}
|
|
|
|
["lifecycle.com"]
|
|
root = "{}"
|
|
cert = "{}"
|
|
key = "{}"
|
|
"#,
|
|
port,
|
|
root_dir.display(),
|
|
cert_path.display(),
|
|
key_path.display());
|
|
std::fs::write(&config_path, config_content).unwrap();
|
|
|
|
// Start server with TLS
|
|
let mut server_process = std::process::Command::new(env!("CARGO_BIN_EXE_pollux"))
|
|
.arg("--config")
|
|
.arg(&config_path)
|
|
.spawn()
|
|
.unwrap();
|
|
|
|
std::thread::sleep(Duration::from_millis(500));
|
|
|
|
// Test root index
|
|
let root_response = make_gemini_request("127.0.0.1", port, "gemini://lifecycle.com/");
|
|
assert!(root_response.starts_with("20"), "Root request failed: {}", root_response);
|
|
assert!(root_response.contains("Main site content"), "Wrong root content: {}", root_response);
|
|
|
|
// Test explicit index
|
|
let index_response = make_gemini_request("127.0.0.1", port, "gemini://lifecycle.com/index.gmi");
|
|
assert!(index_response.starts_with("20"), "Index request failed: {}", index_response);
|
|
assert!(index_response.contains("Main site content"), "Wrong index content: {}", index_response);
|
|
|
|
// Test subdirectory index
|
|
let blog_response = make_gemini_request("127.0.0.1", port, "gemini://lifecycle.com/blog/");
|
|
assert!(blog_response.starts_with("20"), "Blog request failed: {}", blog_response);
|
|
assert!(blog_response.contains("Blog index content"), "Wrong blog content: {}", blog_response);
|
|
|
|
// Test individual file
|
|
let about_response = make_gemini_request("127.0.0.1", port, "gemini://lifecycle.com/about.gmi");
|
|
assert!(about_response.starts_with("20"), "About request failed: {}", about_response);
|
|
assert!(about_response.contains("About page content"), "Wrong about content: {}", about_response);
|
|
|
|
// Test not found
|
|
let notfound_response = make_gemini_request("127.0.0.1", port, "gemini://lifecycle.com/nonexistent.gmi");
|
|
assert!(notfound_response.starts_with("51"), "Not found should return 51: {}", notfound_response);
|
|
|
|
server_process.kill().unwrap();
|
|
}
|
|
|
|
fn make_gemini_request(host: &str, port: u16, url: &str) -> String {
|
|
// Use the Python client for TLS requests
|
|
use std::process::Command;
|
|
|
|
let output = Command::new("python3")
|
|
.arg("tests/gemini_test_client.py")
|
|
.arg(url)
|
|
.env("GEMINI_PORT", &port.to_string())
|
|
.env("GEMINI_CONNECT_HOST", host)
|
|
.output();
|
|
|
|
match output {
|
|
Ok(result) => {
|
|
if result.status.success() {
|
|
String::from_utf8_lossy(&result.stdout).trim().to_string()
|
|
} else {
|
|
format!("Error: Python client failed with status {}", result.status)
|
|
}
|
|
}
|
|
Err(e) => format!("Error: Failed to run Python client: {}", e),
|
|
}
|
|
}
|
|
|