use std::sync::Arc;
use pedant_types::{
AnalysisTier, AttestationContent, Capability, CapabilityDiff, CapabilityFinding,
CapabilityProfile, SourceLocation,
};
fn sample_finding(capability: Capability, file: &str, line: usize) -> CapabilityFinding {
CapabilityFinding {
capability,
location: SourceLocation {
file: Arc::from(file),
line,
column: 1,
},
evidence: Arc::from("test evidence"),
build_script: false,
}
}
#[test]
fn capability_serializes_to_snake_case() {
let json = serde_json::to_string(&Capability::FileRead).unwrap();
assert_eq!(json, "\"file_read\"");
let json = serde_json::to_string(&Capability::ProcessExec).unwrap();
assert_eq!(json, "\"process_exec\"");
let json = serde_json::to_string(&Capability::UnsafeCode).unwrap();
assert_eq!(json, "\"unsafe_code\"");
let json = serde_json::to_string(&Capability::SystemTime).unwrap();
assert_eq!(json, "\"system_time\"");
let json = serde_json::to_string(&Capability::ProcMacro).unwrap();
assert_eq!(json, "\"proc_macro\"");
}
#[test]
fn capability_round_trip() {
let variants = [
Capability::Network,
Capability::FileRead,
Capability::FileWrite,
Capability::ProcessExec,
Capability::EnvAccess,
Capability::UnsafeCode,
Capability::Ffi,
Capability::Crypto,
Capability::SystemTime,
Capability::ProcMacro,
];
for cap in variants {
let json = serde_json::to_string(&cap).unwrap();
let back: Capability = serde_json::from_str(&json).unwrap();
assert_eq!(cap, back);
}
}
#[test]
fn source_location_round_trip() {
let loc = SourceLocation {
file: Arc::from("src/main.rs"),
line: 42,
column: 5,
};
let json = serde_json::to_string(&loc).unwrap();
let back: SourceLocation = serde_json::from_str(&json).unwrap();
assert_eq!(loc, back);
}
#[test]
fn capability_finding_round_trip() {
let finding = sample_finding(Capability::Network, "src/lib.rs", 10);
let json = serde_json::to_string(&finding).unwrap();
let back: CapabilityFinding = serde_json::from_str(&json).unwrap();
assert_eq!(finding, back);
}
#[test]
fn profile_capabilities_deduplicates_and_sorts() {
let profile = CapabilityProfile {
findings: vec![
sample_finding(Capability::Network, "a.rs", 1),
sample_finding(Capability::FileRead, "b.rs", 2),
sample_finding(Capability::Network, "c.rs", 3),
]
.into_boxed_slice(),
};
let caps = profile.capabilities();
assert_eq!(
caps,
vec![Capability::Network, Capability::FileRead].into_boxed_slice()
);
}
#[test]
fn profile_findings_for_filters() {
let profile = CapabilityProfile {
findings: vec![
sample_finding(Capability::Network, "a.rs", 1),
sample_finding(Capability::FileRead, "b.rs", 2),
sample_finding(Capability::Network, "c.rs", 3),
]
.into_boxed_slice(),
};
assert_eq!(profile.findings_for(Capability::Network).count(), 2);
assert_eq!(profile.findings_for(Capability::FileRead).count(), 1);
assert_eq!(profile.findings_for(Capability::Crypto).count(), 0);
}
#[test]
fn empty_profile_round_trip() {
let profile = CapabilityProfile::default();
let json = serde_json::to_string(&profile).unwrap();
let back: CapabilityProfile = serde_json::from_str(&json).unwrap();
assert_eq!(profile, back);
assert!(back.capabilities().is_empty());
}
#[test]
fn attestation_round_trip() {
let attestation = AttestationContent {
spec_version: Box::from("1.0"),
source_hash: Box::from("abc123"),
crate_name: Box::from("my-crate"),
crate_version: Box::from("0.1.0"),
analysis_tier: AnalysisTier::Syntactic,
timestamp: 1_700_000_000,
profile: CapabilityProfile {
findings: vec![sample_finding(Capability::Ffi, "src/lib.rs", 5)].into_boxed_slice(),
},
};
let json = serde_json::to_string(&attestation).unwrap();
let back: AttestationContent = serde_json::from_str(&json).unwrap();
assert_eq!(attestation, back);
}
#[test]
fn analysis_tier_round_trip() {
for tier in [
AnalysisTier::Syntactic,
AnalysisTier::Semantic,
AnalysisTier::DataFlow,
] {
let json = serde_json::to_string(&tier).unwrap();
let back: AnalysisTier = serde_json::from_str(&json).unwrap();
assert_eq!(tier, back);
}
}
#[test]
fn diff_overlapping_profiles() {
let old = CapabilityProfile {
findings: vec![
sample_finding(Capability::Network, "a.rs", 1),
sample_finding(Capability::FileRead, "b.rs", 2),
]
.into_boxed_slice(),
};
let new = CapabilityProfile {
findings: vec![
sample_finding(Capability::Network, "a.rs", 1),
sample_finding(Capability::Crypto, "c.rs", 3),
]
.into_boxed_slice(),
};
let diff = CapabilityDiff::compute(&old, &new);
assert_eq!(diff.added.len(), 1);
assert_eq!(diff.added[0].capability, Capability::Crypto);
assert_eq!(diff.removed.len(), 1);
assert_eq!(diff.removed[0].capability, Capability::FileRead);
assert_eq!(&*diff.new_capabilities, &[Capability::Crypto]);
assert_eq!(&*diff.dropped_capabilities, &[Capability::FileRead]);
}
#[test]
fn diff_disjoint_profiles() {
let old = CapabilityProfile {
findings: vec![sample_finding(Capability::Network, "a.rs", 1)].into_boxed_slice(),
};
let new = CapabilityProfile {
findings: vec![sample_finding(Capability::FileWrite, "b.rs", 2)].into_boxed_slice(),
};
let diff = CapabilityDiff::compute(&old, &new);
assert_eq!(diff.added.len(), 1);
assert_eq!(diff.removed.len(), 1);
assert_eq!(&*diff.new_capabilities, &[Capability::FileWrite]);
assert_eq!(&*diff.dropped_capabilities, &[Capability::Network]);
}
#[test]
fn diff_empty_profiles() {
let empty = CapabilityProfile::default();
let diff = CapabilityDiff::compute(&empty, &empty);
assert!(diff.added.is_empty());
assert!(diff.removed.is_empty());
assert!(diff.new_capabilities.is_empty());
assert!(diff.dropped_capabilities.is_empty());
}
#[test]
fn diff_round_trip() {
let old = CapabilityProfile {
findings: vec![sample_finding(Capability::Network, "a.rs", 1)].into_boxed_slice(),
};
let new = CapabilityProfile {
findings: vec![sample_finding(Capability::Crypto, "b.rs", 2)].into_boxed_slice(),
};
let diff = CapabilityDiff::compute(&old, &new);
let json = serde_json::to_string(&diff).unwrap();
let back: CapabilityDiff = serde_json::from_str(&json).unwrap();
assert_eq!(diff, back);
}