use arcbox_core::{Config, Runtime, VmLifecycleConfig};
use arcbox_docker::api::create_router;
use arcbox_docker::proxy::VsockConnector;
use axum::body::Body;
use axum::http::{Request, StatusCode};
use http_body_util::BodyExt;
use std::sync::Arc;
use tempfile::TempDir;
use tower::ServiceExt;
async fn create_test_runtime() -> (Arc<Runtime>, Arc<VsockConnector>, TempDir) {
let tmp_dir = TempDir::new().expect("Failed to create temp dir");
let config = Config {
data_dir: tmp_dir.path().to_path_buf(),
..Default::default()
};
let vm_lifecycle_config = VmLifecycleConfig {
skip_vm_check: true,
..Default::default()
};
let runtime = Arc::new(
Runtime::with_vm_lifecycle_config(config, vm_lifecycle_config)
.expect("Failed to create runtime"),
);
runtime.init().await.expect("Failed to init runtime");
let connector = Arc::new(VsockConnector::new(Arc::clone(&runtime)));
(runtime, connector, tmp_dir)
}
mod system_api {
use super::*;
#[tokio::test]
#[ignore = "requires running daemon with guest dockerd"]
async fn test_ping() {
let (runtime, connector, _tmp) = create_test_runtime().await;
let app = create_router(runtime, connector);
let response = app
.oneshot(
Request::builder()
.uri("/_ping")
.body(Body::empty())
.unwrap(),
)
.await
.unwrap();
assert_eq!(response.status(), StatusCode::OK);
}
#[tokio::test]
#[ignore = "requires running daemon with guest dockerd"]
async fn test_version() {
let (runtime, connector, _tmp) = create_test_runtime().await;
let app = create_router(runtime, connector);
let response = app
.oneshot(
Request::builder()
.uri("/version")
.body(Body::empty())
.unwrap(),
)
.await
.unwrap();
assert_eq!(response.status(), StatusCode::OK);
let body = response.into_body().collect().await.unwrap().to_bytes();
let json: serde_json::Value = serde_json::from_slice(&body).unwrap();
assert!(json.get("Version").is_some());
assert!(json.get("ApiVersion").is_some());
assert!(json.get("Os").is_some());
assert!(json.get("Arch").is_some());
}
#[tokio::test]
#[ignore = "requires running daemon with guest dockerd"]
async fn test_info() {
let (runtime, connector, _tmp) = create_test_runtime().await;
let app = create_router(runtime, connector);
let response = app
.oneshot(Request::builder().uri("/info").body(Body::empty()).unwrap())
.await
.unwrap();
assert_eq!(response.status(), StatusCode::OK);
let body = response.into_body().collect().await.unwrap().to_bytes();
let json: serde_json::Value = serde_json::from_slice(&body).unwrap();
assert!(json.get("Containers").is_some());
assert!(json.get("Images").is_some());
assert!(json.get("ServerVersion").is_some());
}
}
mod container_api {
use super::*;
#[tokio::test]
#[ignore = "requires running daemon with guest dockerd"]
async fn test_list_containers_empty() {
let (runtime, connector, _tmp) = create_test_runtime().await;
let app = create_router(runtime, connector);
let response = app
.oneshot(
Request::builder()
.uri("/containers/json")
.body(Body::empty())
.unwrap(),
)
.await
.unwrap();
assert_eq!(response.status(), StatusCode::OK);
let body = response.into_body().collect().await.unwrap().to_bytes();
let json: serde_json::Value = serde_json::from_slice(&body).unwrap();
assert!(json.is_array());
assert_eq!(json.as_array().unwrap().len(), 0);
}
#[tokio::test]
#[ignore = "requires image alpine:latest in local store"]
async fn test_create_container() {
let (runtime, connector, _tmp) = create_test_runtime().await;
let app = create_router(runtime, connector);
let body = serde_json::json!({
"Image": "alpine:latest",
"Cmd": ["echo", "hello"]
});
let response = app
.oneshot(
Request::builder()
.method("POST")
.uri("/containers/create?name=test-container")
.header("Content-Type", "application/json")
.body(Body::from(serde_json::to_string(&body).unwrap()))
.unwrap(),
)
.await
.unwrap();
assert_eq!(response.status(), StatusCode::CREATED);
let body = response.into_body().collect().await.unwrap().to_bytes();
let json: serde_json::Value = serde_json::from_slice(&body).unwrap();
assert!(json.get("Id").is_some());
assert!(json.get("Warnings").is_some());
}
#[tokio::test]
#[ignore = "requires image nginx:latest in local store"]
async fn test_container_lifecycle() {
let (runtime, connector, _tmp) = create_test_runtime().await;
let app = create_router(Arc::clone(&runtime), Arc::clone(&connector) as _);
let create_body = serde_json::json!({
"Image": "nginx:latest"
});
let response = app
.oneshot(
Request::builder()
.method("POST")
.uri("/containers/create")
.header("Content-Type", "application/json")
.body(Body::from(serde_json::to_string(&create_body).unwrap()))
.unwrap(),
)
.await
.unwrap();
assert_eq!(response.status(), StatusCode::CREATED);
let body = response.into_body().collect().await.unwrap().to_bytes();
let json: serde_json::Value = serde_json::from_slice(&body).unwrap();
let container_id = json["Id"].as_str().unwrap().to_string();
let app = create_router(Arc::clone(&runtime), Arc::clone(&connector) as _);
let response = app
.oneshot(
Request::builder()
.method("POST")
.uri(format!("/containers/{}/start", container_id))
.body(Body::empty())
.unwrap(),
)
.await
.unwrap();
assert_eq!(response.status(), StatusCode::NO_CONTENT);
let app = create_router(Arc::clone(&runtime), Arc::clone(&connector) as _);
let response = app
.oneshot(
Request::builder()
.uri("/containers/json")
.body(Body::empty())
.unwrap(),
)
.await
.unwrap();
assert_eq!(response.status(), StatusCode::OK);
let body = response.into_body().collect().await.unwrap().to_bytes();
let json: serde_json::Value = serde_json::from_slice(&body).unwrap();
assert_eq!(json.as_array().unwrap().len(), 1);
let app = create_router(Arc::clone(&runtime), Arc::clone(&connector) as _);
let response = app
.oneshot(
Request::builder()
.method("POST")
.uri(format!("/containers/{}/stop", container_id))
.body(Body::empty())
.unwrap(),
)
.await
.unwrap();
assert_eq!(response.status(), StatusCode::NO_CONTENT);
let app = create_router(Arc::clone(&runtime), Arc::clone(&connector) as _);
let response = app
.oneshot(
Request::builder()
.method("DELETE")
.uri(format!("/containers/{}", container_id))
.body(Body::empty())
.unwrap(),
)
.await
.unwrap();
assert_eq!(response.status(), StatusCode::NO_CONTENT);
let app = create_router(Arc::clone(&runtime), Arc::clone(&connector) as _);
let response = app
.oneshot(
Request::builder()
.uri("/containers/json?all=true")
.body(Body::empty())
.unwrap(),
)
.await
.unwrap();
let body = response.into_body().collect().await.unwrap().to_bytes();
let json: serde_json::Value = serde_json::from_slice(&body).unwrap();
assert_eq!(json.as_array().unwrap().len(), 0);
}
#[tokio::test]
#[ignore = "requires image alpine:latest in local store"]
async fn test_inspect_container() {
let (runtime, connector, _tmp) = create_test_runtime().await;
let app = create_router(Arc::clone(&runtime), Arc::clone(&connector) as _);
let create_body = serde_json::json!({
"Image": "alpine:latest"
});
let response = app
.oneshot(
Request::builder()
.method("POST")
.uri("/containers/create?name=inspect-test")
.header("Content-Type", "application/json")
.body(Body::from(serde_json::to_string(&create_body).unwrap()))
.unwrap(),
)
.await
.unwrap();
assert_eq!(response.status(), StatusCode::CREATED);
let body = response.into_body().collect().await.unwrap().to_bytes();
let json: serde_json::Value = serde_json::from_slice(&body).unwrap();
let container_id = json["Id"].as_str().unwrap().to_string();
let app = create_router(Arc::clone(&runtime), Arc::clone(&connector) as _);
let response = app
.oneshot(
Request::builder()
.uri(format!("/containers/{}/json", container_id))
.body(Body::empty())
.unwrap(),
)
.await
.unwrap();
assert_eq!(response.status(), StatusCode::OK);
let body = response.into_body().collect().await.unwrap().to_bytes();
let json: serde_json::Value = serde_json::from_slice(&body).unwrap();
assert!(json.get("Id").is_some());
assert!(json.get("State").is_some());
assert!(json.get("Config").is_some());
assert!(json.get("Name").is_some());
}
#[tokio::test]
#[ignore = "requires running daemon with guest dockerd"]
async fn test_container_not_found() {
let (runtime, connector, _tmp) = create_test_runtime().await;
let app = create_router(runtime, connector);
let response = app
.oneshot(
Request::builder()
.uri("/containers/nonexistent/json")
.body(Body::empty())
.unwrap(),
)
.await
.unwrap();
assert_eq!(response.status(), StatusCode::NOT_FOUND);
}
#[tokio::test]
#[ignore = "requires running daemon with guest dockerd"]
async fn test_wait_container_invalid_condition_returns_bad_request() {
let (runtime, connector, _tmp) = create_test_runtime().await;
let app = create_router(Arc::clone(&runtime), Arc::clone(&connector) as _);
let response = app
.oneshot(
Request::builder()
.method("POST")
.uri("/containers/nonexistent/wait?condition=invalid-condition")
.body(Body::empty())
.unwrap(),
)
.await
.unwrap();
assert!(matches!(
response.status(),
StatusCode::BAD_REQUEST | StatusCode::NOT_FOUND
));
}
}
mod exec_api {
use super::*;
#[tokio::test]
#[ignore = "requires image alpine:latest in local store"]
async fn test_exec_create() {
let (runtime, connector, _tmp) = create_test_runtime().await;
let app = create_router(Arc::clone(&runtime), Arc::clone(&connector) as _);
let create_body = serde_json::json!({
"Image": "alpine:latest"
});
let response = app
.oneshot(
Request::builder()
.method("POST")
.uri("/containers/create")
.header("Content-Type", "application/json")
.body(Body::from(serde_json::to_string(&create_body).unwrap()))
.unwrap(),
)
.await
.unwrap();
let body = response.into_body().collect().await.unwrap().to_bytes();
let json: serde_json::Value = serde_json::from_slice(&body).unwrap();
let container_id = json["Id"].as_str().unwrap().to_string();
let app = create_router(Arc::clone(&runtime), Arc::clone(&connector) as _);
app.oneshot(
Request::builder()
.method("POST")
.uri(format!("/containers/{}/start", container_id))
.body(Body::empty())
.unwrap(),
)
.await
.unwrap();
let app = create_router(Arc::clone(&runtime), Arc::clone(&connector) as _);
let exec_body = serde_json::json!({
"Cmd": ["ls", "-la"],
"AttachStdout": true,
"AttachStderr": true
});
let response = app
.oneshot(
Request::builder()
.method("POST")
.uri(format!("/containers/{}/exec", container_id))
.header("Content-Type", "application/json")
.body(Body::from(serde_json::to_string(&exec_body).unwrap()))
.unwrap(),
)
.await
.unwrap();
assert_eq!(response.status(), StatusCode::CREATED);
let body = response.into_body().collect().await.unwrap().to_bytes();
let json: serde_json::Value = serde_json::from_slice(&body).unwrap();
assert!(json.get("Id").is_some());
}
}
mod network_api {
use super::*;
#[tokio::test]
#[ignore = "requires running daemon with guest dockerd"]
async fn test_list_networks() {
let (runtime, connector, _tmp) = create_test_runtime().await;
let app = create_router(runtime, connector);
let response = app
.oneshot(
Request::builder()
.uri("/networks")
.body(Body::empty())
.unwrap(),
)
.await
.unwrap();
assert_eq!(response.status(), StatusCode::OK);
let body = response.into_body().collect().await.unwrap().to_bytes();
let json: serde_json::Value = serde_json::from_slice(&body).unwrap();
assert!(json.is_array());
let networks = json.as_array().unwrap();
assert!(networks.iter().any(|n| n["Name"] == "bridge"));
}
#[tokio::test]
#[ignore = "requires running daemon with guest dockerd"]
async fn test_create_network() {
let (runtime, connector, _tmp) = create_test_runtime().await;
let app = create_router(runtime, connector);
let body = serde_json::json!({
"Name": "test-network",
"Driver": "bridge"
});
let response = app
.oneshot(
Request::builder()
.method("POST")
.uri("/networks/create")
.header("Content-Type", "application/json")
.body(Body::from(serde_json::to_string(&body).unwrap()))
.unwrap(),
)
.await
.unwrap();
assert_eq!(response.status(), StatusCode::CREATED);
let body = response.into_body().collect().await.unwrap().to_bytes();
let json: serde_json::Value = serde_json::from_slice(&body).unwrap();
assert!(json.get("Id").is_some());
}
}
mod volume_api {
use super::*;
#[tokio::test]
#[ignore = "requires running daemon with guest dockerd"]
async fn test_list_volumes() {
let (runtime, connector, _tmp) = create_test_runtime().await;
let app = create_router(runtime, connector);
let response = app
.oneshot(
Request::builder()
.uri("/volumes")
.body(Body::empty())
.unwrap(),
)
.await
.unwrap();
assert_eq!(response.status(), StatusCode::OK);
let body = response.into_body().collect().await.unwrap().to_bytes();
let json: serde_json::Value = serde_json::from_slice(&body).unwrap();
assert!(json.get("Volumes").is_some());
assert!(json.get("Warnings").is_some());
}
#[tokio::test]
#[ignore = "requires running daemon with guest dockerd"]
async fn test_create_volume() {
let (runtime, connector, _tmp) = create_test_runtime().await;
let app = create_router(runtime, connector);
let body = serde_json::json!({
"Name": "test-volume"
});
let response = app
.oneshot(
Request::builder()
.method("POST")
.uri("/volumes/create")
.header("Content-Type", "application/json")
.body(Body::from(serde_json::to_string(&body).unwrap()))
.unwrap(),
)
.await
.unwrap();
assert_eq!(response.status(), StatusCode::CREATED);
let body = response.into_body().collect().await.unwrap().to_bytes();
let json: serde_json::Value = serde_json::from_slice(&body).unwrap();
assert_eq!(json["Name"], "test-volume");
assert!(json.get("Mountpoint").is_some());
}
#[tokio::test]
#[ignore = "requires running daemon with guest dockerd"]
async fn test_volume_lifecycle() {
let (runtime, connector, _tmp) = create_test_runtime().await;
let app = create_router(Arc::clone(&runtime), Arc::clone(&connector) as _);
let create_body = serde_json::json!({
"Name": "lifecycle-volume"
});
let response = app
.oneshot(
Request::builder()
.method("POST")
.uri("/volumes/create")
.header("Content-Type", "application/json")
.body(Body::from(serde_json::to_string(&create_body).unwrap()))
.unwrap(),
)
.await
.unwrap();
assert_eq!(response.status(), StatusCode::CREATED);
let app = create_router(Arc::clone(&runtime), Arc::clone(&connector) as _);
let response = app
.oneshot(
Request::builder()
.uri("/volumes/lifecycle-volume")
.body(Body::empty())
.unwrap(),
)
.await
.unwrap();
assert_eq!(response.status(), StatusCode::OK);
let app = create_router(Arc::clone(&runtime), Arc::clone(&connector) as _);
let response = app
.oneshot(
Request::builder()
.method("DELETE")
.uri("/volumes/lifecycle-volume")
.body(Body::empty())
.unwrap(),
)
.await
.unwrap();
assert_eq!(response.status(), StatusCode::NO_CONTENT);
let app = create_router(Arc::clone(&runtime), Arc::clone(&connector) as _);
let response = app
.oneshot(
Request::builder()
.uri("/volumes/lifecycle-volume")
.body(Body::empty())
.unwrap(),
)
.await
.unwrap();
assert_eq!(response.status(), StatusCode::NOT_FOUND);
}
}
mod image_api {
use super::*;
#[tokio::test]
#[ignore = "requires running daemon with guest dockerd"]
async fn test_list_images() {
let (runtime, connector, _tmp) = create_test_runtime().await;
let app = create_router(runtime, connector);
let response = app
.oneshot(
Request::builder()
.uri("/images/json")
.body(Body::empty())
.unwrap(),
)
.await
.unwrap();
assert_eq!(response.status(), StatusCode::OK);
let body = response.into_body().collect().await.unwrap().to_bytes();
let json: serde_json::Value = serde_json::from_slice(&body).unwrap();
assert!(json.is_array());
}
}
mod versioned_api {
use super::*;
#[tokio::test]
#[ignore = "requires running daemon with guest dockerd"]
async fn test_versioned_api() {
let (runtime, connector, _tmp) = create_test_runtime().await;
let app = create_router(runtime, connector);
let response = app
.oneshot(
Request::builder()
.uri("/v1.43/_ping")
.body(Body::empty())
.unwrap(),
)
.await
.unwrap();
assert_eq!(response.status(), StatusCode::OK);
}
#[tokio::test]
#[ignore = "requires running daemon with guest dockerd"]
async fn test_older_api_version() {
let (runtime, connector, _tmp) = create_test_runtime().await;
let app = create_router(runtime, connector);
let response = app
.oneshot(
Request::builder()
.uri("/v1.24/_ping")
.body(Body::empty())
.unwrap(),
)
.await
.unwrap();
assert_eq!(response.status(), StatusCode::OK);
}
}
mod container_ops {
use super::*;
#[tokio::test]
#[ignore = "requires running daemon with guest dockerd"]
async fn test_prune_containers() {
let (runtime, connector, _tmp) = create_test_runtime().await;
let app = create_router(runtime, connector);
let response = app
.oneshot(
Request::builder()
.method("POST")
.uri("/containers/prune")
.body(Body::empty())
.unwrap(),
)
.await
.unwrap();
assert_eq!(response.status(), StatusCode::OK);
let body = response.into_body().collect().await.unwrap().to_bytes();
let json: serde_json::Value = serde_json::from_slice(&body).unwrap();
assert!(json.get("ContainersDeleted").is_some());
assert!(json.get("SpaceReclaimed").is_some());
}
#[tokio::test]
#[ignore = "requires running daemon with guest dockerd"]
async fn test_pause_container_not_found() {
let (runtime, connector, _tmp) = create_test_runtime().await;
let app = create_router(runtime, connector);
let response = app
.oneshot(
Request::builder()
.method("POST")
.uri("/containers/nonexistent/pause")
.body(Body::empty())
.unwrap(),
)
.await
.unwrap();
assert_eq!(response.status(), StatusCode::NOT_FOUND);
}
#[tokio::test]
#[ignore = "requires running daemon with guest dockerd"]
async fn test_unpause_container_not_found() {
let (runtime, connector, _tmp) = create_test_runtime().await;
let app = create_router(runtime, connector);
let response = app
.oneshot(
Request::builder()
.method("POST")
.uri("/containers/nonexistent/unpause")
.body(Body::empty())
.unwrap(),
)
.await
.unwrap();
assert_eq!(response.status(), StatusCode::NOT_FOUND);
}
#[tokio::test]
#[ignore = "requires running daemon with guest dockerd"]
async fn test_rename_container_not_found() {
let (runtime, connector, _tmp) = create_test_runtime().await;
let app = create_router(runtime, connector);
let response = app
.oneshot(
Request::builder()
.method("POST")
.uri("/containers/nonexistent/rename?name=newname")
.body(Body::empty())
.unwrap(),
)
.await
.unwrap();
assert_eq!(response.status(), StatusCode::NOT_FOUND);
}
#[tokio::test]
#[ignore = "requires running daemon with guest dockerd"]
async fn test_container_top_not_found() {
let (runtime, connector, _tmp) = create_test_runtime().await;
let app = create_router(runtime, connector);
let response = app
.oneshot(
Request::builder()
.uri("/containers/nonexistent/top")
.body(Body::empty())
.unwrap(),
)
.await
.unwrap();
assert_eq!(response.status(), StatusCode::NOT_FOUND);
}
#[tokio::test]
#[ignore = "requires running daemon with guest dockerd"]
async fn test_container_stats_not_found() {
let (runtime, connector, _tmp) = create_test_runtime().await;
let app = create_router(runtime, connector);
let response = app
.oneshot(
Request::builder()
.uri("/containers/nonexistent/stats")
.body(Body::empty())
.unwrap(),
)
.await
.unwrap();
assert_eq!(response.status(), StatusCode::NOT_FOUND);
}
#[tokio::test]
#[ignore = "requires running daemon with guest dockerd"]
async fn test_container_changes_not_found() {
let (runtime, connector, _tmp) = create_test_runtime().await;
let app = create_router(runtime, connector);
let response = app
.oneshot(
Request::builder()
.uri("/containers/nonexistent/changes")
.body(Body::empty())
.unwrap(),
)
.await
.unwrap();
assert_eq!(response.status(), StatusCode::NOT_FOUND);
}
}
mod e2e_smoke {
#[allow(unused_imports)]
use super::*;
#[tokio::test]
#[ignore = "requires running daemon with guest dockerd and docker CLI"]
async fn e2e_docker_run_echo() {
let output = tokio::process::Command::new("docker")
.args([
"--context",
"arcbox",
"run",
"--rm",
"alpine",
"echo",
"e2e-smoke-test",
])
.output()
.await
.expect("docker CLI not found");
assert!(
output.status.success(),
"docker run failed: {}",
String::from_utf8_lossy(&output.stderr)
);
let stdout = String::from_utf8_lossy(&output.stdout);
assert!(stdout.trim().contains("e2e-smoke-test"));
}
#[tokio::test]
#[ignore = "requires running daemon with guest dockerd and docker CLI"]
async fn e2e_docker_buildx_build() {
let tmp = tempfile::tempdir().unwrap();
let dockerfile = tmp.path().join("Dockerfile");
tokio::fs::write(&dockerfile, "FROM alpine:latest\nRUN echo built\n")
.await
.unwrap();
let output = tokio::process::Command::new("docker")
.args([
"--context",
"arcbox",
"buildx",
"build",
"--no-cache",
"-t",
"arcbox-e2e-smoke:latest",
tmp.path().to_str().unwrap(),
])
.output()
.await
.expect("docker CLI not found");
assert!(
output.status.success(),
"docker buildx build failed: {}",
String::from_utf8_lossy(&output.stderr)
);
}
}