Your browser sucks, JavaScript disabled! Gunnar's console needs JavaScript and WebAssembly. Git over SSH on port 2222 does not.

nordisk/holger

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 ===");
}