Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions crates/integration-tests/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ mod tests {
pub mod mount_feature;
pub mod run_ephemeral;
pub mod run_ephemeral_ignition;
pub mod run_ephemeral_scp;
pub mod run_ephemeral_ssh;
pub mod to_disk;
pub mod varlink;
Expand Down
149 changes: 149 additions & 0 deletions crates/integration-tests/src/tests/libvirt_verb.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
//! - `bcvk libvirt list` - List bootc domains
//! - `bcvk libvirt list-volumes` - List available bootc volumes
//! - `bcvk libvirt ssh` - SSH into domains
//! - `bcvk libvirt scp` - Copy files to/from domains
//! - Domain lifecycle management (start/stop/rm/inspect)

use integration_tests::integration_test;
Expand Down Expand Up @@ -143,6 +144,50 @@ fn test_libvirt_ssh_integration() -> TestResult {
}
integration_test!(test_libvirt_ssh_integration);

/// Test SCP integration with domains (syntax only, no running VM needed)
fn test_libvirt_scp_integration() -> TestResult {
let sh = shell()?;
let bck = get_bck_command()?;

// Test that SCP command fails gracefully with nonexistent domain
let output = cmd!(
sh,
"{bck} libvirt scp test-domain domain:/etc/hostname ./hostname"
)
.ignore_status()
.output()?;
let stderr = String::from_utf8_lossy(&output.stderr);

// Will fail since no domain exists, but should not crash
if !output.status.success() {
assert!(
stderr.contains("domain") || stderr.contains("not found"),
"SCP integration should fail gracefully with domain error: {}",
stderr
);
}

// Test that SCP requires exactly one domain: prefix
let output = cmd!(sh, "{bck} libvirt scp test-domain /local/a /local/b")
.ignore_status()
.output()?;
let stderr = String::from_utf8_lossy(&output.stderr);

assert!(
!output.status.success(),
"SCP with two local paths should fail"
);
assert!(
stderr.contains("domain:"),
"Error should mention domain: prefix: {}",
stderr
);

println!("libvirt SCP integration tested");
Ok(())
}
integration_test!(test_libvirt_scp_integration);

/// Comprehensive workflow test: creates a VM and tests multiple features
/// This consolidates several smaller tests to reduce expensive disk image creation
fn test_libvirt_comprehensive_workflow() -> TestResult {
Expand Down Expand Up @@ -1158,3 +1203,107 @@ fn test_libvirt_run_console_log() -> TestResult {
Ok(())
}
integration_test!(test_libvirt_run_console_log);

/// End-to-end SCP test: creates a VM, copies files to and from it
fn test_libvirt_scp_end_to_end() -> TestResult {
use std::fs;
use tempfile::TempDir;

let sh = shell()?;
let bck = get_bck_command()?;
let test_image = get_test_image();
let label = LIBVIRT_INTEGRATION_TEST_LABEL;

let domain_name = format!("test-scp-{}", random_suffix());
println!("Testing SCP end-to-end with domain: {}", domain_name);

cleanup_domain(&domain_name);
defer! {
cleanup_domain(&domain_name);
}

// Create domain with --ssh-wait so SSH is ready when run returns
println!("Creating domain with --ssh-wait...");
cmd!(
sh,
"{bck} libvirt run --name {domain_name} --label {label} --filesystem ext4 --ssh-wait {test_image}"
)
.run()?;
println!("✓ Domain created and SSH ready: {}", domain_name);

let sh = shell()?;

// --- Upload a file to the VM ---
let upload_dir = TempDir::new()?;
let upload_file = upload_dir.path().join("upload-test.txt");
fs::write(&upload_file, "hello from host")?;
let upload_path = upload_file.to_str().expect("non-UTF-8 temp path");

println!("Uploading file to VM...");
cmd!(
sh,
"{bck} libvirt scp {domain_name} {upload_path} domain:/tmp/upload-test.txt"
)
.run()?;
println!("✓ File uploaded");

// Verify it arrived
let cat_stdout = cmd!(
sh,
"{bck} libvirt ssh {domain_name} -- cat /tmp/upload-test.txt"
)
.read()?;
assert_eq!(
cat_stdout.trim(),
"hello from host",
"Uploaded file content mismatch"
);
println!("✓ Uploaded file content verified");

// --- Download a file from the VM ---
// Use /etc/os-release rather than /etc/hostname: the latter may not exist
// in a freshly-installed bootc VM whose hostname is managed by systemd.
let download_dir = TempDir::new()?;
let download_path = download_dir.path().join("os-release");
let download_str = download_path.to_str().expect("non-UTF-8 temp path");

println!("Downloading /etc/os-release from VM...");
cmd!(
sh,
"{bck} libvirt scp {domain_name} domain:/etc/os-release {download_str}"
)
.run()?;

let downloaded = fs::read_to_string(&download_path)?;
assert!(
downloaded.contains("NAME="),
"Downloaded os-release should contain NAME= field"
);
println!("✓ Downloaded /etc/os-release: {} bytes", downloaded.len());

// --- Recursive copy from the VM ---
let rec_dir = download_dir.path().join("etc-yum");
let rec_str = rec_dir.to_str().expect("non-UTF-8 temp path");

println!("Recursive download of /etc/yum.repos.d/ from VM...");
cmd!(
sh,
"{bck} libvirt scp {domain_name} -r domain:/etc/yum.repos.d {rec_str}"
)
.run()?;

assert!(
rec_dir.exists(),
"Recursive download directory should exist"
);
let entries: Vec<_> = fs::read_dir(&rec_dir)?.collect();
assert!(
!entries.is_empty(),
"Recursive download should have produced files"
);
println!("✓ Recursive download got {} entries", entries.len());

println!("✓ SCP end-to-end test passed");
Ok(())
}
integration_test!(test_libvirt_scp_end_to_end);
221 changes: 221 additions & 0 deletions crates/integration-tests/src/tests/run_ephemeral_scp.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,221 @@
//! Integration tests for ephemeral scp command
//!
//! ⚠️ **CRITICAL INTEGRATION TEST POLICY** ⚠️
//!
//! INTEGRATION TESTS MUST NEVER "warn and continue" ON FAILURES!
//!
//! If something is not working:
//! - Use `todo!("reason why this doesn't work yet")`
//! - Use `panic!("clear error message")`
//! - Use `assert!()` and `unwrap()` to fail hard
//!
//! NEVER use patterns like:
//! - "Note: test failed - likely due to..."
//! - "This is acceptable in CI/testing environments"
//! - Warning and continuing on failures

use integration_tests::integration_test;
use itest::TestResult;
use scopeguard::defer;
use xshell::cmd;

use std::fs;
use tempfile::TempDir;

use crate::{get_bck_command, get_test_image, shell, INTEGRATION_TEST_LABEL};

/// Test that ephemeral SCP validates syntax correctly
fn test_ephemeral_scp_syntax() -> TestResult {
let sh = shell()?;
let bck = get_bck_command()?;

// Test that SCP requires at least one DOMAIN: prefix
let output = cmd!(sh, "{bck} ephemeral scp test-container /local/a /local/b")
.ignore_status()
.output()?;
let stderr = String::from_utf8_lossy(&output.stderr);

assert!(
!output.status.success(),
"SCP with two local paths should fail"
);
assert!(
stderr.contains("DOMAIN:"),
"Error should mention DOMAIN: prefix: {}",
stderr
);

// Test that SCP with two remote paths fails
let output2 = cmd!(
sh,
"{bck} ephemeral scp test-container DOMAIN:/remote/a DOMAIN:/remote/b"
)
.ignore_status()
.output()?;
let stderr2 = String::from_utf8_lossy(&output2.stderr);

assert!(
!output2.status.success(),
"SCP with two remote paths should fail"
);
assert!(
stderr2.contains("DOMAIN:"),
"Error should mention DOMAIN: prefix: {}",
stderr2
);

println!("ephemeral SCP syntax tested");
Ok(())
}
integration_test!(test_ephemeral_scp_syntax);

/// End-to-end SCP test: starts an ephemeral VM, copies files to and from it
fn test_ephemeral_scp_end_to_end() -> TestResult {
let sh = shell()?;
let bck = get_bck_command()?;
let image = get_test_image();
let label = INTEGRATION_TEST_LABEL;
let container_name = format!("test-scp-{}", std::process::id());

println!("Starting ephemeral VM for SCP end-to-end test...");
cmd!(
sh,
"{bck} ephemeral run --ssh-keygen --label {label} --detach --name {container_name} {image}"
)
.run()?;

// Ensure the container is cleaned up even if the test fails
defer! {
let sh = shell().unwrap();
let _ = cmd!(sh, "podman rm -f {container_name}")
.ignore_status()
.quiet()
.run();
}

// Wait a little for SSH connectivity to be verified / ready
println!("Waiting for SSH access to become ready...");
let _ = cmd!(sh, "{bck} ephemeral ssh {container_name} echo READY").read()?;

// --- Upload a file to the VM ---
let upload_dir = TempDir::new()?;
let upload_file = upload_dir.path().join("upload-test.txt");
fs::write(&upload_file, "hello from host to ephemeral VM")?;
let upload_path = upload_file.to_str().expect("non-UTF-8 temp path");

println!("Uploading file to ephemeral VM...");
cmd!(
sh,
"{bck} ephemeral scp {container_name} {upload_path} DOMAIN:/tmp/upload-test.txt"
)
.run()?;
println!("✓ File uploaded");

// Verify it arrived
let cat_stdout = cmd!(
sh,
"{bck} ephemeral ssh {container_name} -- cat /tmp/upload-test.txt"
)
.read()?;
assert_eq!(
cat_stdout.trim(),
"hello from host to ephemeral VM",
"Uploaded file content mismatch"
);
println!("✓ Uploaded file content verified");

// --- Download a file from the VM ---
let download_dir = TempDir::new()?;
let download_path = download_dir.path().join("os-release");
let download_str = download_path.to_str().expect("non-UTF-8 temp path");

// Get the expected os-release content first, and normalize CRLF to LF
let expected_os_release = cmd!(
sh,
"{bck} ephemeral ssh {container_name} -- cat /etc/os-release"
)
.read()?
.trim()
.replace("\r", "");

println!("Downloading /etc/os-release from ephemeral VM...");
cmd!(
sh,
"{bck} ephemeral scp {container_name} DOMAIN:/etc/os-release {download_str}"
)
.run()?;

let downloaded = fs::read_to_string(&download_path)?;
assert_eq!(
downloaded.trim(),
expected_os_release,
"Downloaded os-release mismatch"
);
println!(
"✓ Downloaded /etc/os-release verified: {}",
downloaded.trim()
);

// --- Recursive Copy to VM ---
let local_rec_dir = TempDir::new()?;
let file1 = local_rec_dir.path().join("file1.txt");
let file2 = local_rec_dir.path().join("file2.txt");
fs::write(&file1, "nested content 1")?;
fs::write(&file2, "nested content 2")?;
let local_rec_path = local_rec_dir.path().to_str().expect("non-UTF-8 path");

println!("Recursive upload of directory to ephemeral VM...");
cmd!(
sh,
"{bck} ephemeral scp {container_name} -r {local_rec_path} DOMAIN:/tmp/rec_upload"
)
.run()?;

// Verify contents of the recursively uploaded directory
let verify_rec_1 = cmd!(
sh,
"{bck} ephemeral ssh {container_name} -- cat /tmp/rec_upload/file1.txt"
)
.read()?;
assert_eq!(verify_rec_1.trim(), "nested content 1");

let verify_rec_2 = cmd!(
sh,
"{bck} ephemeral ssh {container_name} -- cat /tmp/rec_upload/file2.txt"
)
.read()?;
assert_eq!(verify_rec_2.trim(), "nested content 2");
println!("✓ Recursive upload verified successfully");

// --- Recursive Download from VM ---
let download_rec_dir = TempDir::new()?;
let download_rec_path = download_rec_dir.path().join("downloaded_rec");
let download_rec_str = download_rec_path.to_str().expect("non-UTF-8 path");

println!("Recursive download of directory from ephemeral VM...");
cmd!(
sh,
"{bck} ephemeral scp {container_name} -r DOMAIN:/tmp/rec_upload {download_rec_str}"
)
.run()?;

assert!(
download_rec_path.exists(),
"Downloaded recursive directory should exist"
);
let downloaded_file1 = download_rec_path.join("file1.txt");
let downloaded_file2 = download_rec_path.join("file2.txt");
assert_eq!(
fs::read_to_string(downloaded_file1)?.trim(),
"nested content 1"
);
assert_eq!(
fs::read_to_string(downloaded_file2)?.trim(),
"nested content 2"
);
println!("✓ Recursive download verified successfully");

println!("✓ Ephemeral SCP end-to-end test passed");
Ok(())
}
integration_test!(test_ephemeral_scp_end_to_end);
Loading
Loading