agent/cli/tests/integration_tar_zstd.rs
//! Integration test: tar.zst archive creation, reading, and round-trip push to Nexus/Holger.
//!
//! Uses holger-mannequin as a mock Nexus backend.
//! Run with: cargo test -p holger-agent-cli --test integration_tar_zstd -- --nocapture
use agent_lib::operations;
use agent_lib::{ConnectorTrait, NexusConnector};
use std::time::Duration;
use tempfile::TempDir;
#[cfg(feature = "testmatrix")]
use traits::test_support::fstatus;
/// Helper: create a NexusConnector pointed at mannequin with dummy auth.
fn make_connector(base_url: &str) -> NexusConnector {
NexusConnector::new(
base_url.to_string(),
Some("admin".into()),
Some("admin123".into()),
)
.unwrap()
}
/// Test: download from mannequin mock Nexus → tar.zst archive → push back to mannequin.
#[tokio::test]
async fn test_tar_zstd_round_trip_with_mannequin() {
use mannequin::MannequinServer;
// Start mannequin on random port
let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
let port = listener.local_addr().unwrap().port();
let mannequin = MannequinServer::new(port).unwrap();
let mannequin_router = mannequin.router();
tokio::spawn(async move {
axum::serve(listener, mannequin_router).await.unwrap();
});
tokio::time::sleep(Duration::from_millis(50)).await;
let base_url = format!("http://127.0.0.1:{}", port);
let connector = make_connector(&base_url);
// Upload some fake crate data to mannequin
let test_crates = vec![
(
"crates/serde/1.0.219/serde-1.0.219.crate",
b"SERDE_CRATE_DATA_BYTES" as &[u8],
),
(
"crates/tokio/1.47.1/tokio-1.47.1.crate",
b"TOKIO_CRATE_DATA_BYTES",
),
(
"crates/anyhow/1.0.102/anyhow-1.0.102.crate",
b"ANYHOW_CRATE_DATA",
),
];
for (path, data) in &test_crates {
connector.upload_asset("upload", path, data).await.unwrap();
}
// List assets from mannequin (acts as source)
let assets = connector.list_assets("upload").await.unwrap();
assert_eq!(assets.len(), 3, "Should have 3 assets uploaded");
// Create tar.zst archive
let tmp_dir = TempDir::new().unwrap();
let archive_path = tmp_dir.path().join("test-crates.tar.zst");
println!("📦 Creating tar.zst from mannequin assets...");
operations::transfer_to_tar_zstd(&connector, assets.clone(), archive_path.clone())
.await
.unwrap();
// Verify the archive file exists and is non-empty
let metadata = std::fs::metadata(&archive_path).unwrap();
assert!(metadata.len() > 0, "Archive should be non-empty");
println!(" Archive size: {} bytes", metadata.len());
// Push tar.zst back to a different repo in mannequin
println!("📤 Pushing tar.zst back to mannequin...");
operations::push_tar_zstd_to_connector(&connector, archive_path.clone(), "upload")
.await
.unwrap();
// Verify assets arrived (mannequin stores in-memory, so old + new should both be there)
let final_assets = connector.list_assets("upload").await.unwrap();
// The pushed-back assets should be present
#[cfg_attr(not(feature = "testmatrix"), allow(unused_variables))]
let all_present = test_crates
.iter()
.all(|(path, _)| final_assets.iter().any(|a| a.path == *path));
for (path, _) in &test_crates {
assert!(
final_assets.iter().any(|a| a.path == *path),
"Asset {} should exist after round-trip",
path
);
}
#[cfg(feature = "testmatrix")]
fstatus(
"agent-tar-zstd",
"round_trip_with_mannequin",
assets.len() == 3 && metadata.len() > 0 && all_present,
&format!(
"3 assets -> {}-byte tar.zst -> pushed back, all 3 present={all_present}",
metadata.len()
),
);
println!("=== tar.zst round-trip test passed ===");
}
/// Test: verify tar.zst archive contents are correct (decompress and check).
#[tokio::test]
async fn test_tar_zstd_archive_contents_valid() {
use mannequin::MannequinServer;
use std::io::Read;
// Start mannequin
let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
let port = listener.local_addr().unwrap().port();
let mannequin = MannequinServer::new(port).unwrap();
let mannequin_router = mannequin.router();
tokio::spawn(async move {
axum::serve(listener, mannequin_router).await.unwrap();
});
tokio::time::sleep(Duration::from_millis(50)).await;
let base_url = format!("http://127.0.0.1:{}", port);
let connector = make_connector(&base_url);
// Upload known data
let test_data: Vec<(&str, &[u8])> = vec![
("crates/rand/0.8.5/rand-0.8.5.crate", b"RAND_DATA_12345"),
("crates/log/0.4.22/log-0.4.22.crate", b"LOG_DATA_67890"),
];
for (path, data) in &test_data {
connector.upload_asset("upload", path, data).await.unwrap();
}
let assets = connector.list_assets("upload").await.unwrap();
// Create archive
let tmp_dir = TempDir::new().unwrap();
let archive_path = tmp_dir.path().join("verify.tar.zst");
operations::transfer_to_tar_zstd(&connector, assets, archive_path.clone())
.await
.unwrap();
// Read archive back and verify contents
let file = std::fs::File::open(&archive_path).unwrap();
let decoder = zstd::Decoder::new(std::io::BufReader::new(file)).unwrap();
let mut archive = tar::Archive::new(decoder);
let mut found = std::collections::HashMap::new();
for entry in archive.entries().unwrap() {
let mut entry = entry.unwrap();
let path = entry.path().unwrap().to_string_lossy().to_string();
let mut data = Vec::new();
entry.read_to_end(&mut data).unwrap();
found.insert(path, data);
}
// Verify all test data is present and correct
for (path, expected_data) in &test_data {
let actual = found
.get(*path)
.unwrap_or_else(|| panic!("Missing entry: {}", path));
assert_eq!(actual, expected_data, "Data mismatch for {}", path);
}
// Every entry matched (the asserts above hold) AND all entries were found.
let all_match = test_data
.iter()
.all(|(path, expected)| found.get(*path).map(|d| d == expected).unwrap_or(false));
#[cfg(feature = "testmatrix")]
fstatus(
"agent-tar-zstd",
"archive_contents_valid",
found.len() == test_data.len() && all_match,
&format!(
"{} entries decompressed from tar.zst, all bytes match={all_match}",
found.len()
),
);
let _ = all_match;
println!("=== tar.zst archive content verification passed ===");
}
/// Test: OIDC auth + tar.zst → holger server (via mannequin mock).
#[tokio::test]
async fn test_tar_zstd_push_to_holger_with_oidc() {
use connector_nexus::fetch_oidc_token;
use mannequin::MannequinServer;
// Start mannequin
let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
let port = listener.local_addr().unwrap().port();
let mannequin = MannequinServer::new(port).unwrap();
let mannequin_router = mannequin.router();
tokio::spawn(async move {
axum::serve(listener, mannequin_router).await.unwrap();
});
tokio::time::sleep(Duration::from_millis(50)).await;
let base_url = format!("http://127.0.0.1:{}", port);
// Get OIDC token from mannequin
let token_url = format!("{}/token", base_url);
let token = fetch_oidc_token(&token_url, "ci-agent", "").await.unwrap();
#[cfg_attr(not(feature = "testmatrix"), allow(unused_variables))]
let token_non_empty = !token.is_empty();
// Create connector with OIDC token (simulates --to holger --auth oidc)
let connector = NexusConnector::with_oidc_token(base_url.clone(), token).unwrap();
// Prepare test data: upload to mannequin first, then create tar.zst
connector
.upload_asset(
"upload",
"crates/hyper/1.6.0/hyper-1.6.0.crate",
b"HYPER_DATA",
)
.await
.unwrap();
connector
.upload_asset("upload", "crates/axum/0.8.9/axum-0.8.9.crate", b"AXUM_DATA")
.await
.unwrap();
let assets = connector.list_assets("upload").await.unwrap();
let tmp_dir = TempDir::new().unwrap();
let archive_path = tmp_dir.path().join("oidc-test.tar.zst");
operations::transfer_to_tar_zstd(&connector, assets, archive_path.clone())
.await
.unwrap();
// Now push the tar.zst back using the OIDC-authenticated connector
// (simulates: holger-agent --from tar-zstd --input X --to holger --auth oidc ...)
operations::push_tar_zstd_to_connector(&connector, archive_path, "upload")
.await
.unwrap();
// Verify
let final_assets = connector.list_assets("upload").await.unwrap();
let has_hyper = final_assets.iter().any(|a| a.path.contains("hyper"));
let has_axum = final_assets.iter().any(|a| a.path.contains("axum"));
assert!(has_hyper);
assert!(has_axum);
#[cfg(feature = "testmatrix")]
fstatus(
"agent-tar-zstd",
"push_to_holger_with_oidc",
token_non_empty && has_hyper && has_axum,
&format!(
"OIDC token fetched, tar.zst pushed via bearer auth; hyper={has_hyper} axum={has_axum}"
),
);
println!("=== tar.zst + OIDC push to holger test passed ===");
}