mod common;
use common::ffi_helpers::{consume_result, into_c_string};
use sbom_tools::ffi::{
SbomToolsErrorCode, SbomToolsScoringProfile, sbom_tools_abi_version_json,
sbom_tools_detect_format_json, sbom_tools_diff_sboms_json, sbom_tools_parse_sbom_path_json,
sbom_tools_parse_sbom_str_json, sbom_tools_score_sbom_json, sbom_tools_string_result_free,
};
use std::ffi::CStr;
use std::path::Path;
const FIXTURES_DIR: &str = concat!(env!("CARGO_MANIFEST_DIR"), "/tests/fixtures");
fn fixture_path(name: &str) -> std::path::PathBuf {
Path::new(FIXTURES_DIR).join(name)
}
#[test]
fn conformance_all_seven_abi_functions_callable() {
let version =
consume_result(sbom_tools_abi_version_json()).expect("abi_version should be callable");
assert!(!version.is_empty());
let fixture = fixture_path("cyclonedx/minimal.cdx.json");
let content = std::fs::read_to_string(&fixture).expect("fixture should exist");
let content_c = into_c_string(&content);
let detected = consume_result(sbom_tools_detect_format_json(content_c.as_ptr()))
.expect("detect_format should be callable");
assert!(!detected.is_empty());
let path_c = into_c_string(fixture.to_string_lossy().as_ref());
let parsed = consume_result(sbom_tools_parse_sbom_path_json(path_c.as_ptr()))
.expect("parse_sbom_path should be callable");
assert!(!parsed.is_empty());
let parsed = consume_result(sbom_tools_parse_sbom_str_json(content_c.as_ptr()))
.expect("parse_sbom_str should be callable");
assert!(!parsed.is_empty());
let parsed_c = into_c_string(&parsed);
let diff = consume_result(sbom_tools_diff_sboms_json(
parsed_c.as_ptr(),
parsed_c.as_ptr(),
))
.expect("diff_sboms should be callable");
assert!(!diff.is_empty());
let score = consume_result(sbom_tools_score_sbom_json(
parsed_c.as_ptr(),
SbomToolsScoringProfile::Standard,
))
.expect("score_sbom should be callable");
assert!(!score.is_empty());
}
#[test]
fn conformance_all_six_scoring_profiles_return_valid_json() {
let fixture = fixture_path("demo-new.cdx.json");
let content = std::fs::read_to_string(&fixture).expect("fixture should exist");
let content_c = into_c_string(&content);
let parsed = consume_result(sbom_tools_parse_sbom_str_json(content_c.as_ptr()))
.expect("parse should succeed");
let parsed_c = into_c_string(&parsed);
let profiles = vec![
SbomToolsScoringProfile::Minimal,
SbomToolsScoringProfile::Standard,
SbomToolsScoringProfile::Security,
SbomToolsScoringProfile::LicenseCompliance,
SbomToolsScoringProfile::Cra,
SbomToolsScoringProfile::Comprehensive,
];
for profile in profiles {
let score_json = consume_result(sbom_tools_score_sbom_json(parsed_c.as_ptr(), profile))
.expect("each profile should score successfully");
let value: serde_json::Value =
serde_json::from_str(&score_json).expect("score response must be valid JSON");
assert!(
value["overall_score"].as_f64().unwrap_or(-1.0) > 0.0,
"score must be positive for profile {:?}",
profile
);
}
}
#[test]
fn conformance_all_error_codes_are_reachable() {
let unknown_format = into_c_string("this is not any known SBOM format");
let (code, msg) = consume_result(sbom_tools_parse_sbom_str_json(unknown_format.as_ptr()))
.expect_err("unknown format should fail");
assert_eq!(
code,
SbomToolsErrorCode::Unsupported,
"Unsupported error code should be reachable"
);
assert!(!msg.is_empty(), "Unsupported error should have message");
let invalid = into_c_string("{}");
let (code, msg) = consume_result(sbom_tools_diff_sboms_json(
invalid.as_ptr(),
invalid.as_ptr(),
))
.expect_err("empty JSON should fail validation");
assert_eq!(
code,
SbomToolsErrorCode::Validation,
"Validation error code should be reachable"
);
assert!(!msg.is_empty(), "Validation error should have message");
let nonexistent = into_c_string("/nonexistent/path-that-must-not-exist-abc123.json");
let (code, msg) = consume_result(sbom_tools_parse_sbom_path_json(nonexistent.as_ptr()))
.expect_err("nonexistent path should fail with IO error");
assert_eq!(
code,
SbomToolsErrorCode::Io,
"IO error code should be reachable"
);
assert!(!msg.is_empty(), "IO error should have message");
}
#[test]
fn conformance_abi_version_is_stable_at_1() {
let payload =
consume_result(sbom_tools_abi_version_json()).expect("abi_version should succeed");
let value: serde_json::Value =
serde_json::from_str(&payload).expect("abi_version JSON is valid");
assert_eq!(
value["abi_version"], "1",
"ABI version is the stability contract; must remain at 1"
);
}
#[test]
fn conformance_detect_format_returns_null_json_for_unknown_content() {
let unknown = into_c_string("hello world");
let result = consume_result(sbom_tools_detect_format_json(unknown.as_ptr()))
.expect("detect_format should succeed even for unknown input");
let value: serde_json::Value =
serde_json::from_str(&result).expect("detect_format always returns valid JSON");
if !value.is_null() {
assert!(
value.is_object() || value.is_null(),
"result should be JSON object or null"
);
}
}
#[test]
fn conformance_required_keys_present_for_all_operations() {
let contract_path = fixture_path("abi/contract_required_keys.json");
if !contract_path.exists() {
eprintln!("Warning: contract_required_keys.json not found; skipping key validation");
return;
}
let contract = std::fs::read_to_string(&contract_path).expect("contract should load");
let contract: serde_json::Value =
serde_json::from_str(&contract).expect("contract should be valid JSON");
let fixture = fixture_path("demo-new.cdx.json");
let content = std::fs::read_to_string(&fixture).expect("fixture should exist");
let content_c = into_c_string(&content);
let parsed = consume_result(sbom_tools_parse_sbom_str_json(content_c.as_ptr()))
.expect("parse should succeed");
let _parsed_c = into_c_string(&parsed);
if let Some(keys) = contract.get("abi_version_json").and_then(|v| v.as_array()) {
let result = consume_result(sbom_tools_abi_version_json()).expect("abi_version ok");
let value: serde_json::Value = serde_json::from_str(&result).expect("abi_version is JSON");
for key in keys {
let key_str = key.as_str().expect("key should be string");
assert!(
value.get(key_str).is_some(),
"abi_version must contain required key '{}'",
key_str
);
}
}
if let Some(keys) = contract
.get("parse_sbom_str_json")
.and_then(|v| v.as_array())
{
let value: serde_json::Value = serde_json::from_str(&parsed).expect("parsed is JSON");
for key in keys {
let key_str = key.as_str().expect("key should be string");
assert!(
value.get(key_str).is_some(),
"parse_sbom_str must contain required key '{}'",
key_str
);
}
}
}
#[test]
fn conformance_c_header_contains_all_function_signatures() {
let header_path =
fixture_path("../../../bindings/swift/Sources/CSbomTools/include/sbom_tools.h");
if !header_path.exists() {
eprintln!("Warning: header file not found at {:?}", header_path);
return;
}
let header = std::fs::read_to_string(&header_path).expect("header should load");
let expected_sigs = vec![
"sbom_tools_abi_version_json",
"sbom_tools_detect_format_json",
"sbom_tools_parse_sbom_path_json",
"sbom_tools_parse_sbom_str_json",
"sbom_tools_diff_sboms_json",
"sbom_tools_score_sbom_json",
"sbom_tools_string_result_free",
];
for sig in expected_sigs {
assert!(
header.contains(sig),
"header must declare function '{}'",
sig
);
}
}
#[test]
fn conformance_result_struct_has_null_error_on_success() {
let result = sbom_tools_abi_version_json();
assert_eq!(
result.error_code,
SbomToolsErrorCode::Ok,
"abi_version should succeed"
);
assert!(
result.error_message.is_null(),
"successful result must have null error_message, not garbage"
);
assert!(
!result.data.is_null(),
"successful result must have non-null data"
);
sbom_tools_string_result_free(result);
}
#[test]
fn conformance_result_struct_has_null_data_on_error() {
let garbage = into_c_string("zzz not sbom zzz");
let result = sbom_tools_parse_sbom_str_json(garbage.as_ptr());
assert_ne!(
result.error_code,
SbomToolsErrorCode::Ok,
"garbage should fail"
);
assert!(
result.data.is_null(),
"error result must have null data, not garbage"
);
assert!(
!result.error_message.is_null(),
"error result must have non-null error_message"
);
sbom_tools_string_result_free(result);
}
#[test]
fn conformance_error_messages_are_valid_utf8() {
let garbage = into_c_string("zzz not sbom zzz");
let result = sbom_tools_parse_sbom_str_json(garbage.as_ptr());
assert_ne!(result.error_code, SbomToolsErrorCode::Ok);
let error_str = unsafe { CStr::from_ptr(result.error_message) };
let utf8_result = error_str.to_str();
assert!(
utf8_result.is_ok(),
"error message must be valid UTF-8, not lossy substitution"
);
sbom_tools_string_result_free(result);
let invalid = into_c_string("{}");
let result = sbom_tools_diff_sboms_json(invalid.as_ptr(), invalid.as_ptr());
assert_ne!(result.error_code, SbomToolsErrorCode::Ok);
let error_str = unsafe { CStr::from_ptr(result.error_message) };
let utf8_result = error_str.to_str();
assert!(
utf8_result.is_ok(),
"validation error message must be valid UTF-8"
);
sbom_tools_string_result_free(result);
let nonexistent = into_c_string("/nonexistent/path-abc123.json");
let result = sbom_tools_parse_sbom_path_json(nonexistent.as_ptr());
assert_ne!(result.error_code, SbomToolsErrorCode::Ok);
let error_str = unsafe { CStr::from_ptr(result.error_message) };
let utf8_result = error_str.to_str();
assert!(utf8_result.is_ok(), "IO error message must be valid UTF-8");
sbom_tools_string_result_free(result);
}