use std::fs;
use std::path::Path;
use std::time::Duration;
use tempfile::TempDir;
#[cfg(unix)]
use std::os::unix::fs::PermissionsExt;
use eggserve_core::primitives::connection_info::{ConnectionInfo, Scheme};
use eggserve_core::primitives::header_block::HeaderBlock;
use eggserve_core::primitives::method::Method;
use eggserve_core::primitives::request::Request;
use eggserve_core::primitives::request_body::RequestBody;
use eggserve_core::primitives::request_head::RequestHead;
use eggserve_core::primitives::request_target::RequestTarget;
use eggserve_core::primitives::version::HttpVersion;
use eggserve_core::server::service::Service;
use eggserve_core::server::StaticService;
use std::net::SocketAddr;
struct FaultTestSetup {
_tmp: TempDir,
svc: StaticService,
}
impl FaultTestSetup {
fn new() -> Self {
let tmp = TempDir::new().unwrap();
let svc = StaticService::builder(tmp.path()).build().unwrap();
FaultTestSetup { _tmp: tmp, svc }
}
fn root(&self) -> &Path {
self._tmp.path()
}
}
fn test_connection() -> ConnectionInfo {
ConnectionInfo {
local_addr: "127.0.0.1:8000".parse::<SocketAddr>().unwrap(),
remote_addr: "127.0.0.1:12345".parse::<SocketAddr>().unwrap(),
scheme: Scheme::Http,
tls: None,
}
}
fn make_request_with_header(
method: Method,
path: &str,
header_name: &str,
header_value: &str,
) -> Request {
let target = RequestTarget::parse(path).unwrap();
let mut headers = HeaderBlock::new();
headers.push_str(header_name, header_value).unwrap();
let head = RequestHead::new(method, target, HttpVersion::Http11, headers);
Request::new(head, RequestBody::empty(), test_connection())
}
fn get_req(path: &str) -> Request {
let target = RequestTarget::parse(path).unwrap();
let head = RequestHead::new(
Method::get(),
target,
HttpVersion::Http11,
HeaderBlock::new(),
);
Request::new(head, RequestBody::empty(), test_connection())
}
#[tokio::test]
async fn fault_file_read_error_after_response_start() {
let setup = FaultTestSetup::new();
let root = setup.root();
fs::write(root.join("file.txt"), "content").unwrap();
let resp = setup.svc.call(get_req("/file.txt")).await.unwrap();
assert_eq!(resp.status().as_u16(), 200);
fs::remove_file(root.join("file.txt")).unwrap();
let result = extract_body_bytes(&resp);
let _ = result;
fs::write(root.join("after.txt"), "ok").unwrap();
let resp2 = setup.svc.call(get_req("/after.txt")).await.unwrap();
assert_eq!(resp2.status().as_u16(), 200);
}
#[tokio::test]
async fn fault_file_read_error_fd_invalidation() {
let setup = FaultTestSetup::new();
let root = setup.root();
fs::write(root.join("data.bin"), vec![b'x'; 64 * 1024]).unwrap();
let resp = setup.svc.call(get_req("/data.bin")).await.unwrap();
assert_eq!(resp.status().as_u16(), 200);
#[cfg(unix)]
{
let _ = fs::set_permissions(root, fs::Permissions::from_mode(0o000));
let _result = extract_body_bytes(&resp);
let _ = fs::set_permissions(root, fs::Permissions::from_mode(0o755));
fs::write(root.join("recovery.txt"), "ok").unwrap();
let resp2 = setup.svc.call(get_req("/recovery.txt")).await.unwrap();
assert_eq!(resp2.status().as_u16(), 200);
}
}
#[tokio::test]
async fn fault_range_read_error_after_response_start() {
let setup = FaultTestSetup::new();
let root = setup.root();
fs::write(root.join("ranged.bin"), vec![b'y'; 1024]).unwrap();
let req = make_request_with_header(Method::get(), "/ranged.bin", "range", "bytes=0-511");
let resp = setup.svc.call(req).await.unwrap();
assert_eq!(resp.status().as_u16(), 206);
fs::remove_file(root.join("ranged.bin")).unwrap();
let result = extract_body_bytes(&resp);
let _ = result;
fs::write(root.join("after.txt"), "ok").unwrap();
let resp2 = setup.svc.call(get_req("/after.txt")).await.unwrap();
assert_eq!(resp2.status().as_u16(), 200);
}
#[tokio::test]
async fn fault_read_only_root() {
let setup = FaultTestSetup::new();
let root = setup.root();
fs::write(root.join("file.txt"), "content").unwrap();
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
let _ = fs::set_permissions(root, fs::Permissions::from_mode(0o555));
}
let resp = setup.svc.call(get_req("/file.txt")).await.unwrap();
assert!(
resp.status().as_u16() == 200
|| resp.status().as_u16() == 403
|| resp.status().as_u16() == 500
);
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
let _ = fs::set_permissions(root, fs::Permissions::from_mode(0o755));
}
}
#[tokio::test]
#[cfg(unix)]
async fn fault_unreadable_file() {
let setup = FaultTestSetup::new();
let root = setup.root();
fs::write(root.join("file.txt"), "content").unwrap();
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
let _ = fs::set_permissions(root.join("file.txt"), fs::Permissions::from_mode(0o000));
}
let resp = setup.svc.call(get_req("/file.txt")).await.unwrap();
assert!(
resp.status().as_u16() == 403
|| resp.status().as_u16() == 404
|| resp.status().as_u16() == 500,
"unreadable file should return 403/404/500, got {}",
resp.status().as_u16()
);
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
let _ = fs::set_permissions(root.join("file.txt"), fs::Permissions::from_mode(0o644));
}
}
#[tokio::test]
async fn fault_concurrent_requests_under_pressure() {
let setup = FaultTestSetup::new();
let root = setup.root();
for i in 0..10 {
fs::write(
root.join(format!("file_{}.txt", i)),
format!("content {}", i),
)
.unwrap();
}
let mut handles = Vec::new();
for i in 0..50 {
let svc = setup.svc.clone();
handles.push(tokio::spawn(async move {
let path = format!("/file_{}.txt", i % 10);
let resp = svc.call(get_req(&path)).await.unwrap();
assert!(
resp.status().as_u16() == 200 || resp.status().as_u16() == 503,
"unexpected status: {}",
resp.status().as_u16()
);
}));
}
for handle in handles {
handle.await.unwrap();
}
}
#[tokio::test]
async fn fault_shutdown_during_requests() {
let setup = FaultTestSetup::new();
let root = setup.root();
for i in 0..5 {
fs::write(
root.join(format!("file_{}.txt", i)),
format!("content {}", i),
)
.unwrap();
}
let mut handles = Vec::new();
for i in 0..10 {
let svc = setup.svc.clone();
handles.push(tokio::spawn(async move {
let path = format!("/file_{}.txt", i % 5);
let resp = svc.call(get_req(&path)).await.unwrap();
let _ = extract_body_bytes(&resp);
}));
}
tokio::time::sleep(Duration::from_millis(10)).await;
drop(setup);
for handle in handles {
let _ = handle.await;
}
}
#[tokio::test]
async fn fault_large_file_streaming_stress() {
let setup = FaultTestSetup::new();
let root = setup.root();
for i in 0..5 {
let data = vec![b'x'; 1024 * 1024]; fs::write(root.join(format!("large_{}.bin", i)), &data).unwrap();
}
let mut handles = Vec::new();
for i in 0..5 {
let svc = setup.svc.clone();
handles.push(tokio::spawn(async move {
let resp = svc
.call(get_req(&format!("/large_{}.bin", i)))
.await
.unwrap();
assert_eq!(resp.status().as_u16(), 200);
let _ = extract_body_bytes(&resp);
}));
}
for handle in handles {
handle.await.unwrap();
}
}
#[tokio::test]
async fn fault_directory_listing_under_modification() {
let setup = FaultTestSetup::new();
let root = setup.root();
fs::create_dir_all(root.join("dir")).unwrap();
let mut handles = Vec::new();
let svc_clone = setup.svc.clone();
handles.push(tokio::spawn(async move {
let svc = svc_clone;
for _ in 0..10 {
let resp = svc.call(get_req("/dir/")).await.unwrap();
assert!(
resp.status().as_u16() == 403
|| resp.status().as_u16() == 404
|| resp.status().as_u16() == 200,
"unexpected status: {}",
resp.status().as_u16()
);
let _ = extract_body_bytes(&resp);
}
}));
let root_clone = root.to_path_buf();
handles.push(tokio::spawn(async move {
for i in 0..20 {
fs::write(
root_clone.join(format!("dir/file_{}.txt", i)),
format!("content {}", i),
)
.unwrap();
if i % 2 == 0 {
let _ = fs::remove_file(root_clone.join(format!("dir/file_{}.txt", i)));
}
}
}));
for handle in handles {
handle.await.unwrap();
}
}
#[tokio::test]
async fn fault_nonexistent_path_handling() {
let setup = FaultTestSetup::new();
let paths = vec![
"/nonexistent.txt",
"/../../etc/passwd",
"/%00%00%00",
"/very/long/path/that/does/not/exist/at/all/file.txt",
];
for path in paths {
let resp = setup.svc.call(get_req(path)).await.unwrap();
assert!(
resp.status().as_u16() == 404
|| resp.status().as_u16() == 400
|| resp.status().as_u16() == 403,
"nonexistent path {} should return 400/403/404, got {}",
path,
resp.status().as_u16()
);
}
}
#[tokio::test]
async fn fault_invalid_http_requests() {
let setup = FaultTestSetup::new();
let root = setup.root();
fs::write(root.join("file.txt"), "content").unwrap();
let invalid_requests = vec![
make_request_with_header(Method::post(), "/file.txt", "content-length", "0"),
get_req("/.env"),
get_req("/does_not_exist.txt"),
];
for req in invalid_requests {
let resp = setup.svc.call(req).await.unwrap();
assert!(
resp.status().as_u16() == 400
|| resp.status().as_u16() == 403
|| resp.status().as_u16() == 404
|| resp.status().as_u16() == 405
|| resp.status().as_u16() == 500,
"invalid request should return error status, got {}",
resp.status().as_u16()
);
}
}
#[tokio::test]
async fn fault_recovery_after_errors() {
let setup = FaultTestSetup::new();
let root = setup.root();
fs::write(root.join("file.txt"), "content").unwrap();
for _ in 0..10 {
let resp = setup.svc.call(get_req("/nonexistent")).await.unwrap();
assert_eq!(resp.status().as_u16(), 404);
}
let resp = setup.svc.call(get_req("/file.txt")).await.unwrap();
assert_eq!(resp.status().as_u16(), 200);
}
#[tokio::test]
async fn fault_mixed_valid_and_invalid_requests() {
let setup = FaultTestSetup::new();
let root = setup.root();
fs::write(root.join("valid.txt"), "valid content").unwrap();
let requests = vec![
(get_req("/valid.txt"), 200),
(get_req("/nonexistent"), 404),
(get_req("/valid.txt"), 200),
(
make_request_with_header(Method::post(), "/valid.txt", "content-length", "0"),
405,
),
(get_req("/valid.txt"), 200),
];
for (req, expected_status) in requests {
let resp = setup.svc.call(req).await.unwrap();
assert_eq!(
resp.status().as_u16(),
expected_status,
"expected status {}, got {}",
expected_status,
resp.status().as_u16()
);
}
}
#[tokio::test]
async fn fault_body_policy_enforcement() {
let setup = FaultTestSetup::new();
let root = setup.root();
fs::write(root.join("file.txt"), "content").unwrap();
let req = make_request_with_header(Method::post(), "/file.txt", "content-length", "5");
let resp = setup.svc.call(req).await.unwrap();
assert!(
resp.status().as_u16() == 400 || resp.status().as_u16() == 405,
"POST should be rejected, got {}",
resp.status().as_u16()
);
}
#[tokio::test]
async fn fault_content_length_mismatch() {
let setup = FaultTestSetup::new();
let root = setup.root();
fs::write(root.join("file.txt"), "content").unwrap();
let resp = setup
.svc
.call(make_request_with_header(
Method::get(),
"/file.txt",
"content-length",
"999999",
))
.await
.unwrap();
assert!(
resp.status().as_u16() == 200
|| resp.status().as_u16() == 400
|| resp.status().as_u16() == 413,
"GET with wrong CL should return 200/400/413, got {}",
resp.status().as_u16()
);
}
#[tokio::test]
async fn fault_concurrent_streaming_stress() {
let setup = FaultTestSetup::new();
let root = setup.root();
for i in 0..20 {
let data = vec![b'x'; 1024 * 64]; fs::write(root.join(format!("file_{}.bin", i)), &data).unwrap();
}
let mut handles = Vec::new();
for i in 0..20 {
let svc = setup.svc.clone();
handles.push(tokio::spawn(async move {
let resp = svc
.call(get_req(&format!("/file_{}.bin", i)))
.await
.unwrap();
assert_eq!(resp.status().as_u16(), 200);
let _ = extract_body_bytes(&resp);
}));
}
for handle in handles {
handle.await.unwrap();
}
}
fn extract_body_bytes(resp: &eggserve_core::primitives::canonical::Response) -> Vec<u8> {
use eggserve_core::primitives::canonical::ResponseBody;
match resp.body() {
Some(ResponseBody::Bytes(b)) => b.clone(),
Some(ResponseBody::Empty) | Some(ResponseBody::EmptyWithLength(_)) => vec![],
Some(ResponseBody::File(_)) => vec![],
None => vec![],
}
}
#[tokio::test]
#[cfg(unix)]
async fn fault_graceful_degradation() {
let setup = FaultTestSetup::new();
let root = setup.root();
fs::write(root.join("file.txt"), "content").unwrap();
fs::write(root.join("secret.txt"), "secret").unwrap();
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
let _ = fs::set_permissions(root.join("secret.txt"), fs::Permissions::from_mode(0o000));
}
let resp = setup.svc.call(get_req("/secret.txt")).await.unwrap();
assert!(
resp.status().as_u16() == 403
|| resp.status().as_u16() == 404
|| resp.status().as_u16() == 500,
"unreadable file should fail gracefully, got {}",
resp.status().as_u16()
);
let resp = setup.svc.call(get_req("/file.txt")).await.unwrap();
assert_eq!(resp.status().as_u16(), 200);
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
let _ = fs::set_permissions(root.join("secret.txt"), fs::Permissions::from_mode(0o644));
}
}
#[tokio::test]
async fn fault_fd_exhaustion_recovery() {
let setup = FaultTestSetup::new();
let root = setup.root();
fs::write(root.join("file.txt"), "content").unwrap();
let mut _open_files: Vec<fs::File> = Vec::new();
for i in 0..128 {
let path = root.join(format!("pressure_{}.txt", i));
fs::write(&path, format!("data {}", i)).unwrap();
match fs::File::open(&path) {
Ok(f) => _open_files.push(f),
Err(_) => break,
}
}
let resp = setup.svc.call(get_req("/file.txt")).await.unwrap();
assert!(
resp.status().as_u16() == 200 || resp.status().as_u16() == 503,
"server should handle FD pressure: {}",
resp.status().as_u16()
);
}
#[tokio::test]
async fn fault_forced_shutdown_under_load() {
let setup = FaultTestSetup::new();
let root = setup.root();
for i in 0..10 {
fs::write(
root.join(format!("file_{}.txt", i)),
format!("content {}", i),
)
.unwrap();
}
let mut handles = Vec::new();
for i in 0..20 {
let svc = setup.svc.clone();
handles.push(tokio::spawn(async move {
let path = format!("/file_{}.txt", i % 10);
let resp = svc.call(get_req(&path)).await.unwrap();
let _ = extract_body_bytes(&resp);
}));
}
tokio::time::sleep(Duration::from_millis(5)).await;
drop(setup);
for handle in handles {
let _ = handle.await;
}
}
#[tokio::test]
async fn fault_rapid_create_delete_cycles() {
let setup = FaultTestSetup::new();
let root = setup.root();
fs::write(root.join("static.txt"), "static content").unwrap();
let root_clone = root.to_path_buf();
let writer = tokio::spawn(async move {
for i in 0..50 {
let path = root_clone.join(format!("temp_{}.txt", i));
fs::write(&path, format!("temp {}", i)).unwrap();
let _ = fs::remove_file(&path);
}
});
let reader = tokio::spawn(async move {
for _ in 0..50 {
let resp = setup.svc.call(get_req("/static.txt")).await.unwrap();
assert_eq!(resp.status().as_u16(), 200);
let _ = extract_body_bytes(&resp);
}
});
writer.await.unwrap();
reader.await.unwrap();
}
#[tokio::test]
async fn fault_deeply_nested_path_traversal() {
let setup = FaultTestSetup::new();
let paths = vec![
"/../../../../../../etc/passwd",
"/sub/../../../sub/../../etc/hostname",
"/%2e%2e/%2e%2e/%2e%2e/etc/passwd",
];
for path in paths {
let resp = setup.svc.call(get_req(path)).await.unwrap();
assert!(
resp.status().as_u16() == 400
|| resp.status().as_u16() == 403
|| resp.status().as_u16() == 404,
"deep traversal {} should be denied: {}",
path,
resp.status().as_u16()
);
}
}
#[tokio::test]
async fn fault_empty_request_handling() {
let setup = FaultTestSetup::new();
let resp = setup
.svc
.call(make_request_with_header(
Method::post(),
"/file.txt",
"content-length",
"0",
))
.await
.unwrap();
assert!(
resp.status().as_u16() == 400
|| resp.status().as_u16() == 403
|| resp.status().as_u16() == 405
|| resp.status().as_u16() == 500,
"empty request should be rejected: {}",
resp.status().as_u16()
);
}