depup-cli 0.2.1

Check dependency versions across Maven and npm ecosystems
use console::style;

use crate::model::{
    AuditResult, CheckResult, CommandResult, DependencyKind, Severity, UpdateResult,
};

use super::format::print_kind_legend;

pub fn check_summary(results: &[CheckResult]) {
    let total = results.len();
    let outdated = results.iter().filter(|r| r.is_outdated()).count();
    let skipped = results.iter().filter(|r| r.is_skipped()).count();
    let errors = results
        .iter()
        .filter(|r| r.error_message().is_some())
        .count();
    let current = total - outdated - skipped - errors;

    print!("{total} checked: ");
    print!("{}", style(format!("{current} current")).green());
    if outdated > 0 {
        print!(", {}", style(format!("{outdated} outdated")).yellow());
    }
    if skipped > 0 {
        print!(", {}", style(format!("{skipped} skipped")).dim());
    }
    if errors > 0 {
        print!(", {}", style(format!("{errors} errors")).red());
    }

    let kinds: Vec<DependencyKind> = results.iter().map(|r| r.kind()).collect();
    print_kind_legend(&kinds);
}

pub fn update_summary(results: &[UpdateResult]) {
    let total = results.len();
    let errors = results.iter().filter(|r| r.is_error()).count();

    print!("{total} updated");
    if errors > 0 {
        print!(", {}", style(format!("{errors} errors")).red());
    }

    let kinds: Vec<DependencyKind> = results.iter().map(|r| r.kind()).collect();
    print_kind_legend(&kinds);
}

pub fn audit_summary(results: &[AuditResult]) {
    let total = results.len();
    let vulnerable = results.iter().filter(|r| r.is_vulnerable()).count();
    let clean = total - vulnerable;

    print!("{total} audited: ");
    print!("{}", style(format!("{clean} clean")).green());
    if vulnerable > 0 {
        print!(", {}", style(format!("{vulnerable} vulnerable")).red());

        let all_vulns: Vec<Severity> = results
            .iter()
            .flat_map(|r| r.vulnerabilities.iter().map(|v| v.severity))
            .collect();
        let critical = all_vulns
            .iter()
            .filter(|s| **s == Severity::Critical)
            .count();
        let high = all_vulns.iter().filter(|s| **s == Severity::High).count();
        let medium = all_vulns.iter().filter(|s| **s == Severity::Medium).count();
        let low = all_vulns.iter().filter(|s| **s == Severity::Low).count();

        let mut parts = Vec::new();
        if critical > 0 {
            parts.push(format!("{critical} critical"));
        }
        if high > 0 {
            parts.push(format!("{high} high"));
        }
        if medium > 0 {
            parts.push(format!("{medium} medium"));
        }
        if low > 0 {
            parts.push(format!("{low} low"));
        }
        if !parts.is_empty() {
            print!(" ({})", parts.join(", "));
        }
    }

    let kinds: Vec<DependencyKind> = results.iter().map(|r| r.kind()).collect();
    print_kind_legend(&kinds);
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::model::{Dependency, Ecosystem, Vulnerability};

    fn make_check_result(artifact: &str, kind: DependencyKind, outdated: bool) -> CheckResult {
        CheckResult::checked(
            Dependency::new(Ecosystem::Maven, kind, artifact.into(), None, String::new()),
            "1.0.0".into(),
            "2.0.0".into(),
            outdated,
        )
    }

    fn make_update_result(artifact: &str, is_error: bool) -> UpdateResult {
        let check = CheckResult::checked(
            Dependency::new(
                Ecosystem::Maven,
                DependencyKind::Dependency,
                artifact.into(),
                None,
                String::new(),
            ),
            "1.0.0".into(),
            "2.0.0".into(),
            true,
        );
        if is_error {
            UpdateResult::error(&check, "write failed".into())
        } else {
            UpdateResult::updated(&check, "2.0.0".into())
        }
    }

    fn make_audit_result(vulns: Vec<Vulnerability>) -> AuditResult {
        AuditResult {
            dep: Dependency::new(
                Ecosystem::Maven,
                DependencyKind::Dependency,
                "org.example:lib".into(),
                None,
                String::new(),
            ),
            version: "1.0.0".into(),
            vulnerabilities: vulns,
        }
    }

    fn make_vuln(id: &str, severity: Severity) -> Vulnerability {
        Vulnerability {
            id: id.into(),
            aliases: Vec::new(),
            summary: String::new(),
            severity,
            url: None,
        }
    }

    #[test]
    fn check_summary_with_mixed_results_does_not_panic() {
        let results = vec![
            make_check_result("g:a", DependencyKind::Dependency, false),
            make_check_result("g:b", DependencyKind::Dependency, true),
            CheckResult::error(
                Dependency::new(
                    Ecosystem::Maven,
                    DependencyKind::Plugin,
                    "g:c".into(),
                    None,
                    String::new(),
                ),
                "1.0".into(),
                "timeout".into(),
            ),
        ];
        check_summary(&results);
    }

    #[test]
    fn update_summary_with_errors_does_not_panic() {
        let results = vec![
            make_update_result("g:a", false),
            make_update_result("g:b", true),
        ];
        update_summary(&results);
    }

    #[test]
    fn audit_summary_with_vulnerabilities_does_not_panic() {
        let results = vec![
            make_audit_result(vec![
                make_vuln("CVE-1", Severity::Critical),
                make_vuln("CVE-2", Severity::Low),
            ]),
            make_audit_result(Vec::new()),
        ];
        audit_summary(&results);
    }

    #[test]
    fn audit_summary_all_clean_does_not_panic() {
        let results = vec![make_audit_result(Vec::new()), make_audit_result(Vec::new())];
        audit_summary(&results);
    }

    #[test]
    fn check_summary_empty_does_not_panic() {
        check_summary(&[]);
    }

    #[test]
    fn update_summary_empty_does_not_panic() {
        update_summary(&[]);
    }

    #[test]
    fn audit_summary_empty_does_not_panic() {
        audit_summary(&[]);
    }
}