pollux/tests/virtual_host_integration.rs
Jeena 0459cb6220 feat: Implement virtual hosting for multi-domain Gemini server
- 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.
2026-01-22 02:38:09 +00:00

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),
}
}