northroot-proof-engine 0.3.2

Core proof computation and validation library for PoSH (Proof of Shape) and PoX (Proof of Execution)
Documentation
//! Property tests for v0.2 engine
//!
//! These tests verify properties and edge cases for validation,
//! hash computation, and canonicalization.

use northroot_proof_engine::{
    validate_iso_time, validate_method_ref, validate_sha256_format, validate_span_commitment,
    validate_span_id, validate_trace_id, ArtifactRef, MethodRef, SpanCommitment, SpanStatus,
};

#[test]
fn test_validate_trace_id_edge_cases() {
    // Valid: 8 chars minimum
    assert!(validate_trace_id("tr_12345").is_ok());

    // Invalid: too short
    assert!(validate_trace_id("short").is_err());

    // Valid: alphanumeric + hyphens/underscores
    assert!(validate_trace_id("trace-abc_123").is_ok());

    // Invalid: special chars
    assert!(validate_trace_id("trace@abc").is_err());
}

#[test]
fn test_validate_span_id_edge_cases() {
    // Valid: non-empty
    assert!(validate_span_id("sp-1").is_ok());

    // Invalid: empty
    assert!(validate_span_id("").is_err());

    // Valid: alphanumeric + hyphens/underscores
    assert!(validate_span_id("span_abc-123").is_ok());
}

#[test]
fn test_validate_sha256_format_edge_cases() {
    // Valid format
    let valid = "sha256:0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef";
    assert!(validate_sha256_format(valid).is_ok());

    // Invalid: wrong prefix
    assert!(validate_sha256_format("sha512:abc").is_err());
    assert!(validate_sha256_format("invalid:abc").is_err());

    // Invalid: wrong length
    assert!(validate_sha256_format("sha256:short").is_err());
    assert!(validate_sha256_format(
        "sha256:0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0"
    )
    .is_err());

    // Invalid: uppercase
    let uppercase = "sha256:0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF";
    assert!(validate_sha256_format(uppercase).is_err());

    // Invalid: non-hex chars
    assert!(validate_sha256_format(
        "sha256:0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdeg"
    )
    .is_err());
}

#[test]
fn test_validate_iso_time_edge_cases() {
    // Valid RFC3339
    assert!(validate_iso_time("2025-11-06T12:00:00.000Z").is_ok());
    assert!(validate_iso_time("2025-11-06T12:00:00Z").is_ok());

    // Invalid formats
    assert!(validate_iso_time("invalid").is_err());
    assert!(validate_iso_time("2025-11-06").is_err()); // No time
    assert!(validate_iso_time("2025-11-06T12:00:00").is_err()); // No Z
}

#[test]
fn test_validate_method_ref_edge_cases() {
    // Valid
    let valid = MethodRef {
        method_id: "com.test/method".to_string(),
        version: "1.0.0".to_string(),
        method_shape_root:
            "sha256:0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef".to_string(),
        image_digest: "sha256:0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"
            .to_string(),
        method_fingerprint: "git:deadbeef".to_string(),
    };
    assert!(validate_method_ref(&valid).is_ok());

    // Invalid: empty method_id
    let mut invalid = valid.clone();
    invalid.method_id = String::new();
    assert!(validate_method_ref(&invalid).is_err());

    // Invalid: empty version
    let mut invalid = valid.clone();
    invalid.version = String::new();
    assert!(validate_method_ref(&invalid).is_err());

    // Invalid: bad hash format
    let mut invalid = valid.clone();
    invalid.method_shape_root = "invalid".to_string();
    assert!(validate_method_ref(&invalid).is_err());
}

#[test]
fn test_validate_span_commitment_edge_cases() {
    let base = SpanCommitment {
        schema_version: "proof.v2".to_string(),
        trace_id: "tr_01H1234567890".to_string(),
        span_id: "sp-0001".to_string(),
        method_ref: MethodRef {
            method_id: "com.test/method".to_string(),
            version: "1.0.0".to_string(),
            method_shape_root:
                "sha256:0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"
                    .to_string(),
            image_digest: "sha256:0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"
                .to_string(),
            method_fingerprint: "git:deadbeef".to_string(),
        },
        data_shape_hash: "sha256:1111111111111111111111111111111111111111111111111111111111111111"
            .to_string(),
        span_shape_hash: "sha256:2222222222222222222222222222222222222222222222222222222222222222"
            .to_string(),
        inputs: vec![ArtifactRef {
            name: "input.csv".to_string(),
            content_hash: "sha256:3333333333333333333333333333333333333333333333333333333333333333"
                .to_string(),
            uri: None,
        }],
        outputs: vec![ArtifactRef {
            name: "output.json".to_string(),
            content_hash: "sha256:4444444444444444444444444444444444444444444444444444444444444444"
                .to_string(),
            uri: None,
        }],
        start_time: "2025-11-06T12:00:00.000Z".to_string(),
        end_time: "2025-11-06T12:00:00.042Z".to_string(),
        status: SpanStatus::OK,
        meta: None,
    };

    // Valid
    assert!(validate_span_commitment(&base).is_ok());

    // Invalid: empty inputs
    let mut invalid = base.clone();
    invalid.inputs.clear();
    assert!(validate_span_commitment(&invalid).is_err());

    // Invalid: empty outputs
    let mut invalid = base.clone();
    invalid.outputs.clear();
    assert!(validate_span_commitment(&invalid).is_err());

    // Invalid: end_time < start_time
    let mut invalid = base.clone();
    invalid.start_time = "2025-11-06T12:00:00.042Z".to_string();
    invalid.end_time = "2025-11-06T12:00:00.000Z".to_string();
    assert!(validate_span_commitment(&invalid).is_err());

    // Invalid: empty input name
    let mut invalid = base.clone();
    invalid.inputs[0].name = String::new();
    assert!(validate_span_commitment(&invalid).is_err());
}

#[test]
fn test_hash_format_consistency() {
    use northroot_proof_engine::compute_sha256;

    let hash = compute_sha256("test");
    assert!(hash.starts_with("sha256:"));
    assert_eq!(hash.len(), 71); // "sha256:" (7) + 64 hex = 71
    assert!(hash
        .chars()
        .skip(7)
        .all(|c| c.is_ascii_hexdigit() && !c.is_ascii_uppercase()));
}