Test that the server correctly reports missing certificate errors, rejects invalid hostnames, fails gracefully on port conflicts, and starts successfully with multiple virtual hosts configured.
384 lines
10 KiB
Rust
384 lines
10 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)
|
|
.arg("--quiet")
|
|
.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)
|
|
.arg("--quiet")
|
|
.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)
|
|
.arg("--quiet")
|
|
.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;
|
|
|
|
tracing::debug!(
|
|
"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)
|
|
.arg("--quiet")
|
|
.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),
|
|
}
|
|
}
|