holger-ui 0.1.4

Operator/admin UI for holger over the HolgerObject core API — egui via facett, embedded (LocalHolger, direct core calls) or remote (RemoteHolger gRPC).
//! Integration test (gui only): the Tools#3 security scan reads its advisory
//! corpus from modgunn's **warehouse** (`cve_advisories`) when `HOLGER_WAREHOUSE`
//! points at a catalog — not the built-in demo corpus. Proves the wiring
//! end-to-end without a display: seed an advisory into a temp warehouse, run the
//! view-model scan over a repo whose archive lists a matching artifact, and assert
//! the finding came from the warehouse (source label + BLOCK).
//!
//! Its own test binary + single test, so the process-global `HOLGER_WAREHOUSE`
//! env never races another test.
#![cfg(feature = "gui")]

use std::collections::HashMap;
use std::sync::{Arc, Mutex};
use std::time::{SystemTime, UNIX_EPOCH};

use holger_ui::data::UiData;
use holger_ui::modgunn::core::Severity;
use holger_ui::modgunn::scan::Advisory;
use holger_ui::modgunn::warehouse::Warehouse;
use server_lib::exposed::fast_routes::FastRoutes;
use server_lib::LocalHolger;
use traits::{ArtifactFormat, ArtifactId, HolgerObject, RepositoryBackendTrait};

/// Minimal in-memory rust repo whose archive lists one vulnerable crate.
struct MemRepo {
    name: String,
    store: Mutex<HashMap<(Option<String>, String, String), Vec<u8>>>,
}
impl MemRepo {
    fn new(name: &str) -> Self {
        Self { name: name.to_string(), store: Mutex::new(HashMap::new()) }
    }
}
impl RepositoryBackendTrait for MemRepo {
    fn name(&self) -> &str {
        &self.name
    }
    fn format(&self) -> ArtifactFormat {
        ArtifactFormat::Rust
    }
    fn is_writable(&self) -> bool {
        true
    }
    fn fetch(&self, id: &ArtifactId) -> anyhow::Result<Option<Vec<u8>>> {
        let k = (id.namespace.clone(), id.name.clone(), id.version.clone());
        Ok(self.store.lock().unwrap().get(&k).cloned())
    }
    fn put(&self, id: &ArtifactId, data: &[u8]) -> anyhow::Result<()> {
        let k = (id.namespace.clone(), id.name.clone(), id.version.clone());
        self.store.lock().unwrap().insert(k, data.to_vec());
        Ok(())
    }
    fn list(&self, _f: Option<&str>, _l: usize) -> anyhow::Result<Vec<traits::ArtifactEntry>> {
        Ok(Vec::new())
    }
    /// The archive lists a known-vulnerable crate (serde 1.0.0) + a clean one.
    fn archive_files(&self, _prefix: Option<&str>) -> anyhow::Result<Vec<String>> {
        Ok(vec!["serde-1.0.0.crate".to_string(), "tokio-1.20.0.crate".to_string()])
    }
    fn archive_info(&self) -> anyhow::Result<traits::ArchiveInfo> {
        Ok(traits::ArchiveInfo { file_count: 2, total_uncompressed_bytes: 0, archive_path: self.name.clone() })
    }
    fn handle_http2_request(
        &self,
        _m: &str,
        _s: &str,
        _b: &[u8],
    ) -> anyhow::Result<(u16, Vec<(String, String)>, Vec<u8>)> {
        Ok((404, Vec::new(), b"not found".to_vec()))
    }
}

#[test]
fn tools3_scan_uses_warehouse_advisories_when_configured() {
    // A unique temp catalog root (skade is single-writer per catalog).
    let nanos = SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_nanos();
    let root = std::env::temp_dir().join(format!("holger-ui-wh-scan-{nanos}"));
    std::fs::create_dir_all(&root).unwrap();

    // Seed the warehouse with an advisory for cargo serde 1.0.0 (High → Block),
    // then DROP the handle so the scan can re-open the catalog (single-writer).
    {
        let wh = Warehouse::open(&root).unwrap();
        wh.ensure_modgunn_tables().unwrap();
        wh.append_advisories(
            "demo-rev-2026",
            &[Advisory::basic(
                "RUSTSEC-DEMO-0001",
                "cargo",
                "serde",
                vec!["1.0.0".into()],
                Severity::High,
                "demo: serde 1.0.0 flagged from the warehouse",
            )],
        )
        .unwrap();
    }

    // Point the UI's scanner at that warehouse.
    std::env::set_var("HOLGER_WAREHOUSE", &root);

    let repo: Arc<dyn RepositoryBackendTrait> = Arc::new(MemRepo::new("rust-dev"));
    let routes = FastRoutes::new(vec![("rust-dev".to_string(), repo)]);
    let holger: Arc<dyn HolgerObject> = Arc::new(LocalHolger::new(routes));
    let mut ui = UiData::new(holger).expect("runtime");

    ui.run_security_scan("rust-dev", "rust");
    let s = &ui.security;

    std::env::remove_var("HOLGER_WAREHOUSE");

    assert!(s.error.is_none(), "scan error: {:?}", s.error);
    assert!(s.loaded, "scan populated the view");
    assert!(
        s.source.starts_with("warehouse rev 'demo-rev-2026'"),
        "advisories sourced from the warehouse, got: {:?}",
        s.source
    );
    assert_eq!(s.scanned, 2, "both crates scanned");
    assert_eq!(s.block, 1, "serde 1.0.0 blocked by the warehouse advisory");
    assert_eq!(s.decision, "block");
    assert!(
        s.findings.iter().any(|f| f.name == "serde" && f.ids.iter().any(|i| i == "RUSTSEC-DEMO-0001")),
        "the serde finding carries the warehouse advisory id: {:?}",
        s.findings
    );

    std::fs::remove_dir_all(&root).ok();
}