allow-core 0.2.0-rc.1

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 with_kind_preserving_metadata_reclassifies_without_loss() -> Result<(), String> {
    let reclassified = structured_error().with_kind_preserving_metadata(CargoAllowErrorKind::Usage);

    assert_eq!(reclassified.kind(), CargoAllowErrorKind::Usage);
    assert_eq!(reclassified.message(), "missing owner");
    let location = reclassified
        .location()
        .ok_or_else(|| "reclassification should preserve location".to_string())?;
    assert_eq!(location.path.as_deref(), Some("legacy/policy.toml"));
    assert_eq!(reclassified.diagnostics().len(), 1);
    assert_eq!(
        reclassified.causes(),
        &[String::from("underlying parse failure")]
    );
    Ok(())
}

fn structured_error() -> CargoAllowError {
    let cause = std::io::Error::other("underlying parse failure");

    CargoAllowError::with_kind(CargoAllowErrorKind::InvalidPolicy, "missing owner")
        .with_toml_span(
            Some(Path::new("legacy/policy.toml")),
            "policy = \"unsafe-allowlist\"\nowner = [",
            Some(36..37),
        )
        .with_diagnostic(super::CargoAllowDiagnostic::error(
            "E0003_INVALID_POLICY",
            "policy_validation",
            Some("allow-1"),
            Some("owner"),
            "allow-1 missing owner",
        ))
        .with_cause(&cause)
}

#[test]
fn message_prefix_preserves_structured_error_metadata() -> Result<(), String> {
    let prefixed = structured_error().with_message_prefix("legacy file `policy.toml`: ");

    assert_eq!(
        prefixed.message(),
        "legacy file `policy.toml`: missing owner"
    );
    assert_eq!(prefixed.kind(), CargoAllowErrorKind::InvalidPolicy);
    let location = prefixed
        .location()
        .ok_or_else(|| "message prefix should preserve location".to_string())?;
    assert_eq!(location.path.as_deref(), Some("legacy/policy.toml"));
    assert_eq!(location.line, 2);
    assert_eq!(location.column, 9);
    assert_eq!(prefixed.diagnostics().len(), 1);
    let diagnostic = prefixed
        .diagnostics()
        .first()
        .ok_or_else(|| "message prefix should preserve diagnostics".to_string())?;
    assert_eq!(diagnostic.code, "E0003_INVALID_POLICY");
    assert_eq!(diagnostic.field.as_deref(), Some("owner"));
    assert_eq!(diagnostic.message, "allow-1 missing owner");
    assert_eq!(
        prefixed.causes(),
        &[String::from("underlying parse failure")]
    );
    Ok(())
}

#[test]
fn message_suffix_preserves_structured_error_metadata() -> Result<(), String> {
    let suffixed = structured_error().with_message_suffix("; regenerate the plan");

    assert_eq!(suffixed.message(), "missing owner; regenerate the plan");
    assert_eq!(suffixed.kind(), CargoAllowErrorKind::InvalidPolicy);
    let location = suffixed
        .location()
        .ok_or_else(|| "message suffix should preserve location".to_string())?;
    assert_eq!(location.path.as_deref(), Some("legacy/policy.toml"));
    assert_eq!(suffixed.diagnostics().len(), 1);
    assert_eq!(
        suffixed.causes(),
        &[String::from("underlying parse failure")]
    );
    Ok(())
}

#[test]
fn empty_message_prefix_leaves_error_unchanged() -> Result<(), String> {
    let unchanged = structured_error().with_message_prefix("");

    assert_eq!(unchanged.message(), "missing owner");
    assert_eq!(unchanged.kind(), CargoAllowErrorKind::InvalidPolicy);
    let location = unchanged
        .location()
        .ok_or_else(|| "empty prefix should preserve location".to_string())?;
    assert_eq!(location.path.as_deref(), Some("legacy/policy.toml"));
    assert_eq!(location.column, 9);
    assert_eq!(unchanged.diagnostics().len(), 1);
    assert_eq!(
        unchanged.causes(),
        &[String::from("underlying parse failure")]
    );
    Ok(())
}

#[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 newly_classified_runtime_kinds_have_append_only_codes() {
    assert_eq!(CargoAllowErrorKind::Unsupported.code(), "E0010_UNSUPPORTED");
    assert_eq!(
        CargoAllowErrorKind::InstrumentFailure.code(),
        "E0011_INSTRUMENT_FAILURE"
    );
    assert_eq!(CargoAllowErrorKind::Internal.code(), "E0008_INTERNAL");
    assert_eq!(CargoAllowErrorKind::Unknown.code(), "E0009_UNKNOWN");
}

#[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 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);
}