runx-receipts 0.6.19

Pure Rust receipt model, canonicalization, and verification parity for runx.
Documentation
use runx_contracts::{Reference, ReferenceType};

use super::{
    ReceiptTreeConfig, SliceReceiptResolver, validate_receipt_tree_with_resolver,
    verify_receipt_tree, verify_receipt_tree_with_resolver,
};
use crate::ReceiptFindingCode;

use super::test_support::{
    AmbiguousResolver, ResolverErrorResolver, SUCCESS_RECEIPT, assert_finding, child,
    child_refs_mut, fixture, reference,
};

#[test]
fn slice_adapter_accepts_only_typed_receipt_uri() -> Result<(), serde_json::Error> {
    let mut root = fixture(SUCCESS_RECEIPT)?;
    let child = child("hrn_rcpt_child_1")?;

    child_refs_mut(&mut root)[0].uri = "hrn_rcpt_child_1".to_owned().into();
    let verification = verify_receipt_tree(&root, std::slice::from_ref(&child));
    assert_finding(
        &verification,
        ReceiptFindingCode::ChildReceiptRefMalformed,
        "lineage.children[0]",
    );

    child_refs_mut(&mut root)[0].uri = "runx:receipt:hrn_rcpt_child_1".to_owned().into();
    assert!(verify_receipt_tree(&root, &[child]).valid);
    Ok(())
}

#[test]
fn malformed_and_wrong_namespace_refs_are_stable_findings() -> Result<(), serde_json::Error> {
    let mut root = fixture(SUCCESS_RECEIPT)?;
    let child = child("hrn_rcpt_child_1")?;

    child_refs_mut(&mut root)[0].uri = "runx:graph_receipt:hrn_rcpt_child_1".to_owned().into();
    let verification = verify_receipt_tree(&root, std::slice::from_ref(&child));
    assert_finding(
        &verification,
        ReceiptFindingCode::ChildReceiptRefMalformed,
        "lineage.children[0]",
    );

    child_refs_mut(&mut root)[0].uri = ":hrn_rcpt_child_1".to_owned().into();
    let verification = verify_receipt_tree(&root, &[child]);
    assert_finding(
        &verification,
        ReceiptFindingCode::ChildReceiptRefMalformed,
        "lineage.children[0]",
    );
    Ok(())
}

#[test]
fn suffix_only_refs_are_malformed_not_aliases() -> Result<(), serde_json::Error> {
    let mut root = fixture(SUCCESS_RECEIPT)?;
    child_refs_mut(&mut root)[0].uri = "child_1".to_owned().into();
    let child = child("hrn_rcpt_child_1")?;

    let verification = verify_receipt_tree(&root, &[child]);

    assert_finding(
        &verification,
        ReceiptFindingCode::ChildReceiptRefMalformed,
        "lineage.children[0]",
    );
    Ok(())
}

#[test]
fn duplicate_ids_make_slice_resolution_ambiguous() -> Result<(), serde_json::Error> {
    let root = fixture(SUCCESS_RECEIPT)?;
    let first = child("hrn_rcpt_child_1")?;
    let second = child("hrn_rcpt_child_1")?;

    let verification = verify_receipt_tree(&root, &[first, second]);

    assert_finding(
        &verification,
        ReceiptFindingCode::DuplicateChildReceipt,
        "children[1].id",
    );
    assert_finding(
        &verification,
        ReceiptFindingCode::ChildReceiptAmbiguous,
        "lineage.children[0]",
    );
    Ok(())
}

#[test]
fn resolver_ambiguous_result_is_a_stable_finding() -> Result<(), serde_json::Error> {
    let root = fixture(SUCCESS_RECEIPT)?;

    let verification =
        verify_receipt_tree_with_resolver(&root, &AmbiguousResolver, ReceiptTreeConfig::default());

    assert_finding(
        &verification,
        ReceiptFindingCode::ChildReceiptAmbiguous,
        "lineage.children[0]",
    );
    Ok(())
}

#[test]
fn resolver_error_result_is_a_stable_finding() -> Result<(), serde_json::Error> {
    let root = fixture(SUCCESS_RECEIPT)?;

    let verification = verify_receipt_tree_with_resolver(
        &root,
        &ResolverErrorResolver,
        ReceiptTreeConfig::default(),
    );

    assert_finding(
        &verification,
        ReceiptFindingCode::ChildReceiptResolverError,
        "lineage.children[0]",
    );
    Ok(())
}

#[test]
fn strict_mode_rejects_mismatched_parent_link() -> Result<(), serde_json::Error> {
    let root = fixture(SUCCESS_RECEIPT)?;
    let mut child = child("hrn_rcpt_child_1")?;
    child.lineage.get_or_insert_with(Default::default).parent =
        Some(reference(ReferenceType::Receipt, "other"));

    let verification = verify_receipt_tree_with_resolver(
        &root,
        &SliceReceiptResolver {
            children: std::slice::from_ref(&child),
        },
        ReceiptTreeConfig {
            require_parent_links: true,
            ..ReceiptTreeConfig::default()
        },
    );

    assert_finding(
        &verification,
        ReceiptFindingCode::ChildReceiptParentMismatch,
        "lineage.children[0].lineage.parent",
    );
    Ok(())
}

#[test]
fn strict_mode_requires_present_parent_link() -> Result<(), serde_json::Error> {
    let root = fixture(SUCCESS_RECEIPT)?;
    let child = child("hrn_rcpt_child_1")?;

    let verification = verify_receipt_tree_with_resolver(
        &root,
        &SliceReceiptResolver {
            children: std::slice::from_ref(&child),
        },
        ReceiptTreeConfig {
            require_parent_links: true,
            ..ReceiptTreeConfig::default()
        },
    );

    assert_finding(
        &verification,
        ReceiptFindingCode::ChildReceiptParentMismatch,
        "lineage.children[0].lineage.parent",
    );
    Ok(())
}

#[test]
fn depth_limit_blocks_hostile_nested_tree() -> Result<(), serde_json::Error> {
    let root = fixture(SUCCESS_RECEIPT)?;
    let mut child_receipt = child("hrn_rcpt_child_1")?;
    child_refs_mut(&mut child_receipt).push(reference(ReferenceType::Receipt, "grandchild"));
    let grandchild = child("grandchild")?;

    let verification = verify_receipt_tree_with_resolver(
        &root,
        &SliceReceiptResolver {
            children: &[child_receipt, grandchild],
        },
        ReceiptTreeConfig {
            max_depth: 1,
            ..ReceiptTreeConfig::default()
        },
    );

    assert_finding(
        &verification,
        ReceiptFindingCode::ChildReceiptDepthLimit,
        "children[0].lineage.children[0]",
    );
    Ok(())
}

#[test]
fn breadth_limit_blocks_hostile_fanout() -> Result<(), serde_json::Error> {
    let mut root = fixture(SUCCESS_RECEIPT)?;
    child_refs_mut(&mut root).push(reference(ReferenceType::Receipt, "second"));
    let first = child("hrn_rcpt_child_1")?;
    let second = child("second")?;

    let verification = verify_receipt_tree_with_resolver(
        &root,
        &SliceReceiptResolver {
            children: &[first, second],
        },
        ReceiptTreeConfig {
            max_breadth: 1,
            ..ReceiptTreeConfig::default()
        },
    );

    assert_finding(
        &verification,
        ReceiptFindingCode::ChildReceiptBreadthLimit,
        "lineage.children",
    );
    Ok(())
}

#[test]
fn positive_nested_tree_verifies() -> Result<(), serde_json::Error> {
    let root = fixture(SUCCESS_RECEIPT)?;
    let mut child_receipt = child("hrn_rcpt_child_1")?;
    child_refs_mut(&mut child_receipt).push(reference(ReferenceType::Receipt, "grandchild"));
    let grandchild = child("grandchild")?;

    assert!(verify_receipt_tree(&root, &[child_receipt, grandchild]).valid);
    Ok(())
}

#[test]
fn positive_fanout_tree_verifies() -> Result<(), serde_json::Error> {
    let mut root = fixture(SUCCESS_RECEIPT)?;
    child_refs_mut(&mut root).push(reference(ReferenceType::Receipt, "second"));
    let first = child("hrn_rcpt_child_1")?;
    let second = child("second")?;

    assert!(verify_receipt_tree(&root, &[first, second]).valid);
    Ok(())
}

#[test]
fn strict_parent_links_can_verify_cleanly() -> Result<(), serde_json::Error> {
    let root = fixture(SUCCESS_RECEIPT)?;
    let mut child = child("hrn_rcpt_child_1")?;
    child.lineage.get_or_insert_with(Default::default).parent =
        Some(Reference::runx(ReferenceType::Receipt, &root.id));

    assert!(
        validate_receipt_tree_with_resolver(
            &root,
            &SliceReceiptResolver {
                children: std::slice::from_ref(&child),
            },
            ReceiptTreeConfig {
                require_parent_links: true,
                ..ReceiptTreeConfig::default()
            },
        )
        .is_ok()
    );
    Ok(())
}