use anyhow::{Context, Result};
use std::{collections::HashMap, net::SocketAddr, sync::Arc, time::Duration};
use tokio::time::timeout;
mod common;
use common::find_available_port;
use wash_runtime::{
engine::Engine,
host::{HostApi, HostBuilder},
plugin::{wasi_blobstore::WasiBlobstore, wasi_http::HttpServer, wasi_logging::WasiLogging},
types::{Component, LocalResources, Workload, WorkloadStartRequest, WorkloadStopRequest},
wit::WitInterface,
};
const BLOBBY_WASM: &[u8] = include_bytes!("fixtures/blobby.wasm");
#[tokio::test]
async fn test_blobby_integration() -> Result<()> {
tracing_subscriber::fmt()
.with_env_filter(tracing_subscriber::EnvFilter::from_default_env())
.init();
println!("Starting blobby.wasm integration test");
let engine = Engine::builder().build()?;
let port = find_available_port().await?;
let addr: SocketAddr = format!("127.0.0.1:{port}").parse().unwrap();
let http_plugin = HttpServer::new(addr);
let blobstore_plugin = WasiBlobstore::new(None);
let logging_plugin = WasiLogging {};
let host = HostBuilder::new()
.with_engine(engine.clone())
.with_plugin(Arc::new(http_plugin))?
.with_plugin(Arc::new(blobstore_plugin))?
.with_plugin(Arc::new(logging_plugin))?
.build()?;
println!("Created host with HTTP and blobstore plugins for blobby test");
let host = host.start().await.context("Failed to start host")?;
println!("Host started, HTTP server listening on {addr}");
let req = WorkloadStartRequest {
workload: Workload {
namespace: "test".to_string(),
name: "blobby-workload".to_string(),
annotations: HashMap::new(),
service: None,
components: vec![Component {
bytes: bytes::Bytes::from_static(BLOBBY_WASM),
local_resources: LocalResources {
memory_limit_mb: 256,
cpu_limit: 1,
config: HashMap::new(),
environment: HashMap::new(),
volume_mounts: vec![],
allowed_hosts: vec![],
},
pool_size: 1,
max_invocations: 100,
}],
host_interfaces: vec![
WitInterface {
namespace: "wasi".to_string(),
package: "http".to_string(),
interfaces: ["incoming-handler".to_string()].into_iter().collect(),
version: None,
config: {
let mut config = HashMap::new();
config.insert("host".to_string(), "blobby-test".to_string());
config
},
},
WitInterface {
namespace: "wasi".to_string(),
package: "blobstore".to_string(),
interfaces: [
"blobstore".to_string(),
"container".to_string(),
"types".to_string(),
]
.into_iter()
.collect(),
version: Some(semver::Version::parse("0.2.0-draft").unwrap()),
config: HashMap::new(),
},
WitInterface {
namespace: "wasi".to_string(),
package: "logging".to_string(),
interfaces: ["logging".to_string()].into_iter().collect(),
version: Some(semver::Version::parse("0.1.0-draft").unwrap()),
config: HashMap::new(),
},
],
volumes: vec![],
},
};
let workload_response = host
.workload_start(req)
.await
.context("Failed to start blobby workload")?;
println!(
"Started blobby workload: {:?}",
workload_response.workload_status.workload_id
);
println!("Testing blobby component endpoint");
let test_data = "Hello from blobby test!";
let client = reqwest::Client::new();
let get_response = timeout(
Duration::from_secs(5),
client
.get(format!("http://{addr}/"))
.header("HOST", "blobby-test")
.send(),
)
.await
.context("GET request timed out")?
.context("Failed to make GET request")?;
let get_status = get_response.status();
println!("GET Response Status: {}", get_status);
let get_response_text = get_response
.text()
.await
.context("Failed to read GET response body")?;
println!("GET Response Body: {}", get_response_text.trim());
let post_response = timeout(
Duration::from_secs(5),
client
.post(format!("http://{addr}/"))
.header("HOST", "blobby-test")
.body(test_data)
.send(),
)
.await
.context("POST request timed out")?
.context("Failed to make POST request")?;
let post_status = post_response.status();
println!("POST Response Status: {}", post_status);
let post_response_text = post_response
.text()
.await
.context("Failed to read POST response body")?;
println!("POST Response Body: {}", post_response_text.trim());
assert!(
get_status.is_success() || get_status == reqwest::StatusCode::NOT_FOUND,
"GET request expected success or 404, got {}",
get_status
);
assert!(
post_status.is_success(),
"POST request expected success, got {}",
post_status
);
assert!(
!get_response_text.trim().is_empty() || !post_response_text.trim().is_empty(),
"Expected at least one response to have content"
);
let res = host
.workload_stop(WorkloadStopRequest {
workload_id: workload_response.workload_status.workload_id,
})
.await;
res.context("Failed to stop blobby workload")?;
let failed_response = client
.get(format!("http://{addr}/"))
.header("HOST", "blobby-test")
.send()
.await;
match failed_response {
Ok(resp) => {
let status = resp.status();
assert!(
status.is_client_error() || status.is_server_error(),
"Expected HTTP error after workload stopped, got status {}",
status
);
println!(
"Confirmed HTTP request fails after blobby workload stopped (status: {})",
status
);
}
Err(e) => {
println!(
"HTTP request failed as expected after workload stopped: {}",
e
);
}
}
println!("blobby.wasm component responded successfully to HTTP requests");
println!("HTTP and blobstore plugins are working with blobby component");
println!("Blobby integration test passed!");
Ok(())
}
#[tokio::test]
async fn test_blobby_error_handling() -> Result<()> {
println!("Testing blobby component error handling");
let engine = Engine::builder().build()?;
let port = find_available_port().await?;
let addr: SocketAddr = format!("127.0.0.1:{port}").parse().unwrap();
let http_plugin = HttpServer::new(addr);
let blobstore_plugin = WasiBlobstore::new(Some(1024 * 1024)); let logging_plugin = WasiLogging {};
let host = HostBuilder::new()
.with_engine(engine)
.with_plugin(Arc::new(http_plugin))?
.with_plugin(Arc::new(blobstore_plugin))?
.with_plugin(Arc::new(logging_plugin))?
.build()?;
let host = host
.start()
.await
.context("Failed to start host for error test")?;
let req = WorkloadStartRequest {
workload: Workload {
namespace: "error-test".to_string(),
name: "blobby-error-workload".to_string(),
annotations: HashMap::new(),
service: None,
components: vec![Component {
bytes: bytes::Bytes::from_static(BLOBBY_WASM),
local_resources: LocalResources {
memory_limit_mb: 128,
cpu_limit: 1,
config: HashMap::new(),
environment: HashMap::new(),
volume_mounts: vec![],
allowed_hosts: vec![],
},
pool_size: 1,
max_invocations: 50,
}],
host_interfaces: vec![
WitInterface {
namespace: "wasi".to_string(),
package: "http".to_string(),
interfaces: ["incoming-handler".to_string()].into_iter().collect(),
version: Some(semver::Version::parse("0.2.2").unwrap()),
config: {
let mut config = HashMap::new();
config.insert("host".to_string(), "blobby-error-test".to_string());
config
},
},
WitInterface {
namespace: "wasi".to_string(),
package: "blobstore".to_string(),
interfaces: [
"blobstore".to_string(),
"container".to_string(),
"types".to_string(),
]
.into_iter()
.collect(),
version: Some(semver::Version::parse("0.2.0-draft").unwrap()),
config: HashMap::new(),
},
WitInterface {
namespace: "wasi".to_string(),
package: "logging".to_string(),
interfaces: ["logging".to_string()].into_iter().collect(),
version: Some(semver::Version::parse("0.1.0-draft").unwrap()),
config: HashMap::new(),
},
],
volumes: vec![],
},
};
let workload_response = host
.workload_start(req)
.await
.context("Failed to start error test workload")?;
println!(
"Started error test workload: {:?}",
workload_response.workload_status.workload_id
);
let client = reqwest::Client::new();
let put_response = client
.put(format!("http://{addr}/"))
.header("HOST", "blobby-error-test")
.body("test data")
.send()
.await;
match put_response {
Ok(response) => {
let status = response.status();
println!("PUT Response Status: {}", status);
assert!(
status.is_success() || status.is_client_error() || status.is_server_error(),
"Expected valid HTTP status code"
);
}
Err(e) => {
println!("PUT request failed as expected: {}", e);
}
}
println!("Error handling test completed");
Ok(())
}