allow-core 0.1.11

Core types and matching primitives for cargo-allow source exception policies.
Documentation
use super::{CargoAllowError, CargoAllowErrorKind};
use std::path::Path;

#[test]
fn cargo_allow_error_fmt_return_value_observer() {
    let err = CargoAllowError::new("policy validation failed");
    assert_eq!(format!("{err}"), "policy validation failed");
    assert_eq!(err.to_string(), "policy validation failed");
}

#[test]
fn new_defaults_to_unknown_kind() {
    let err = CargoAllowError::new("something went wrong");
    assert_eq!(err.kind(), CargoAllowErrorKind::Unknown);
    assert_eq!(err.message(), "something went wrong");
}

#[test]
fn with_kind_sets_structured_kind() {
    let err = CargoAllowError::with_kind(CargoAllowErrorKind::InvalidPolicy, "missing owner");
    assert_eq!(err.kind(), CargoAllowErrorKind::InvalidPolicy);
    assert_eq!(err.message(), "missing owner");
}

#[test]
fn kind_renders_as_stable_lowercase_str() {
    assert_eq!(CargoAllowErrorKind::Usage.as_str(), "usage");
    assert_eq!(
        CargoAllowErrorKind::InvalidConfig.as_str(),
        "invalid_config"
    );
    assert_eq!(
        CargoAllowErrorKind::InvalidPolicy.as_str(),
        "invalid_policy"
    );
    assert_eq!(
        CargoAllowErrorKind::PolicyViolation.as_str(),
        "policy_violation"
    );
    assert_eq!(CargoAllowErrorKind::Artifact.as_str(), "artifact");
    assert_eq!(CargoAllowErrorKind::Unknown.as_str(), "unknown");
}

#[test]
fn every_error_kind_has_a_unique_stable_code() {
    let codes = CargoAllowErrorKind::ALL
        .iter()
        .map(|kind| kind.code())
        .collect::<std::collections::BTreeSet<_>>();

    assert_eq!(codes.len(), CargoAllowErrorKind::ALL.len());
    assert_eq!(CargoAllowErrorKind::Usage.code(), "E0001_USAGE");
    assert_eq!(
        CargoAllowErrorKind::InvalidPolicy.code(),
        "E0003_INVALID_POLICY"
    );
    assert_eq!(CargoAllowErrorKind::Unknown.code(), "E0009_UNKNOWN");
}

#[test]
fn error_exposes_the_code_of_its_kind() {
    let error = CargoAllowError::with_kind(CargoAllowErrorKind::Artifact, "write failed");

    assert_eq!(error.code(), "E0007_ARTIFACT");
}

#[test]
fn error_exposes_structured_diagnostic_details() -> Result<(), String> {
    let error = CargoAllowError::with_kind(CargoAllowErrorKind::InvalidPolicy, "missing owner")
        .with_diagnostic(super::CargoAllowDiagnostic::error(
            "E0003_INVALID_POLICY",
            "policy_validation",
            Some("allow-1"),
            Some("owner"),
            "allow-1 missing owner",
        ));

    let diagnostic = error
        .diagnostics()
        .first()
        .ok_or_else(|| "diagnostic should be present".to_string())?;
    assert_eq!(diagnostic.code, "E0003_INVALID_POLICY");
    assert_eq!(diagnostic.entry_id.as_deref(), Some("allow-1"));
    assert_eq!(diagnostic.field.as_deref(), Some("owner"));
    Ok(())
}

#[test]
fn toml_span_preserves_path_and_one_based_line_column() -> Result<(), String> {
    let error = CargoAllowError::with_kind(CargoAllowErrorKind::InvalidPolicy, "invalid TOML")
        .with_toml_span(
            Some(Path::new("policy/allow.toml")),
            "policy = \"cargo-allow\"\nowner = [",
            Some(29..30),
        );
    let location = error
        .location()
        .ok_or_else(|| "TOML span should produce a structured location".to_string())?;

    assert_eq!(location.path.as_deref(), Some("policy/allow.toml"));
    assert_eq!(location.line, 2);
    assert!(location.column > 0);
    Ok(())
}

#[test]
fn display_includes_cause_chain() {
    let inner = std::io::Error::new(std::io::ErrorKind::NotFound, "file missing");
    let err = CargoAllowError::with_kind(CargoAllowErrorKind::Artifact, "failed to write receipt")
        .with_cause(&inner);
    let rendered = format!("{err}");
    assert!(rendered.contains("failed to write receipt"));
    assert!(rendered.contains("caused by:"));
    assert!(rendered.contains("file missing"));
}

#[test]
fn error_source_walks_attached_causes() {
    use std::error::Error as _;

    let inner = std::io::Error::new(std::io::ErrorKind::NotFound, "file missing");
    let mid = std::io::Error::other("git failed");
    let err = CargoAllowError::with_kind(CargoAllowErrorKind::Inventory, "failed to read revision")
        .with_cause(&mid)
        .with_cause(&inner);

    let first = err
        .source()
        .unwrap_or_else(|| std::panic::panic_any("expected first cause"));
    assert_eq!(first.to_string(), "git failed");
    let second = first
        .source()
        .unwrap_or_else(|| std::panic::panic_any("expected nested cause"));
    assert_eq!(second.to_string(), "file missing");
    assert!(second.source().is_none());
}

#[test]
fn from_io_error_exposes_source_without_duplicate_display_cause() {
    use std::error::Error as _;

    let io_err = std::io::Error::new(std::io::ErrorKind::NotFound, "no such file");
    let err: CargoAllowError = io_err.into();
    assert_eq!(err.message(), "no such file");
    assert!(err.causes().is_empty());
    let source = err
        .source()
        .unwrap_or_else(|| std::panic::panic_any("expected io source"));
    assert_eq!(source.to_string(), "no such file");
    assert!(!format!("{err}").contains("caused by:"));
}

#[test]
fn partialeq_compares_kind_and_message_not_causes() {
    let a = CargoAllowError::with_kind(CargoAllowErrorKind::Scan, "read error");
    let b = CargoAllowError::with_kind(CargoAllowErrorKind::Scan, "read error");
    // Same kind + message -> equal even if causes differ.
    let a_with_cause = a.with_cause(&std::io::Error::other("boom"));
    assert_eq!(a_with_cause, b);
    // Different kind -> not equal.
    let c = CargoAllowError::with_kind(CargoAllowErrorKind::Unknown, "read error");
    assert_ne!(b, c);
}

#[test]
fn from_io_error_preserves_kind_and_message() {
    let io_err = std::io::Error::new(std::io::ErrorKind::NotFound, "no such file");
    let err: CargoAllowError = io_err.into();
    assert_eq!(err.kind(), CargoAllowErrorKind::InvalidConfig);
    assert!(err.message().contains("no such file"));
}

#[test]
fn from_io_permission_denied_maps_to_inventory_kind() {
    let io_err = std::io::Error::new(std::io::ErrorKind::PermissionDenied, "denied");
    let err: CargoAllowError = io_err.into();
    assert_eq!(err.kind(), CargoAllowErrorKind::Inventory);
}