use ash_core::{
ash_build_proof, ash_verify_proof, ash_derive_client_secret,
ash_canonicalize_json, ash_canonicalize_query,
ash_hash_body, ash_timing_safe_equal,
ash_build_proof_scoped,
};
use std::collections::HashSet;
mod hash_properties {
use super::*;
#[test]
fn test_hash_determinism() {
let long_input = "x".repeat(10000);
let inputs = [
"",
"a",
"hello world",
r#"{"key":"value"}"#,
long_input.as_str(),
];
for input in inputs {
let hash1 = ash_hash_body(input);
let hash2 = ash_hash_body(input);
assert_eq!(hash1, hash2, "Hash should be deterministic for: {}", input);
}
}
#[test]
fn test_hash_avalanche_effect() {
let base = "test input string";
let base_hash = ash_hash_body(base);
let variations = [
"Test input string", "test input string ", "test input strings", "test input strinG", ];
for var in variations {
let var_hash = ash_hash_body(var);
let differences: usize = base_hash.chars()
.zip(var_hash.chars())
.filter(|(a, b)| a != b)
.count();
assert!(differences > 20, "Avalanche effect not seen for: {}", var);
}
}
#[test]
fn test_hash_length_consistency() {
let inputs = vec![
"".to_string(),
"a".to_string(),
"ab".to_string(),
"abc".to_string(),
"x".repeat(100),
"x".repeat(1000),
"x".repeat(10000),
];
for input in &inputs {
let hash = ash_hash_body(input);
assert_eq!(hash.len(), 64, "Hash length should always be 64");
}
}
#[test]
fn test_hash_uniqueness() {
let inputs: Vec<String> = (0..1000).map(|i| format!("input_{}", i)).collect();
let hashes: HashSet<String> = inputs.iter().map(|s| ash_hash_body(s)).collect();
assert_eq!(hashes.len(), inputs.len(), "All hashes should be unique");
}
#[test]
fn test_hash_hex_format() {
for i in 0..100 {
let hash = ash_hash_body(&format!("test_{}", i));
assert!(hash.chars().all(|c| c.is_ascii_hexdigit() && !c.is_uppercase()));
}
}
}
mod proof_properties {
use super::*;
#[test]
fn test_proof_soundness() {
let nonce = "a".repeat(64);
let context_id = "ctx_test";
let binding = "POST|/api/test|";
let timestamp = "1700000000";
let body_hash = "b".repeat(64);
let secret = ash_derive_client_secret(&nonce, context_id, binding).unwrap();
let proof = ash_build_proof(&secret, timestamp, binding, &body_hash).unwrap();
for _ in 0..100 {
let valid = ash_verify_proof(&nonce, context_id, binding, timestamp, &body_hash, &proof).unwrap();
assert!(valid, "Valid proof should always verify");
}
}
#[test]
fn test_proof_completeness() {
let nonce = "a".repeat(64);
let context_id = "ctx_test";
let binding = "POST|/api/test|";
let timestamp = "1700000000";
let body_hash = "b".repeat(64);
let secret = ash_derive_client_secret(&nonce, context_id, binding).unwrap();
let correct_proof = ash_build_proof(&secret, timestamp, binding, &body_hash).unwrap();
for i in 0..100 {
let wrong_proof = format!("{:064x}", i);
if wrong_proof != correct_proof {
let result = ash_verify_proof(&nonce, context_id, binding, timestamp, &body_hash, &wrong_proof);
match result {
Ok(valid) => assert!(!valid, "Wrong proof should not verify"),
Err(_) => {} }
}
}
}
#[test]
fn test_proof_determinism() {
let nonce = "a".repeat(64);
let context_id = "ctx_test";
let binding = "POST|/api/test|";
let timestamp = "1700000000";
let body_hash = "b".repeat(64);
let secret = ash_derive_client_secret(&nonce, context_id, binding).unwrap();
let proofs: HashSet<String> = (0..100)
.map(|_| ash_build_proof(&secret, timestamp, binding, &body_hash).unwrap())
.collect();
assert_eq!(proofs.len(), 1, "Same inputs should produce same proof");
}
#[test]
fn test_proof_sensitivity_to_timestamp() {
let nonce = "a".repeat(64);
let binding = "POST|/api|";
let body_hash = "b".repeat(64);
let secret = ash_derive_client_secret(&nonce, "ctx", binding).unwrap();
let proofs: HashSet<String> = (0..100)
.map(|i| {
let ts = format!("{}", 1700000000 + i);
ash_build_proof(&secret, &ts, binding, &body_hash).unwrap()
})
.collect();
assert_eq!(proofs.len(), 100, "Different timestamps should produce different proofs");
}
#[test]
fn test_proof_sensitivity_to_body() {
let nonce = "a".repeat(64);
let binding = "POST|/api|";
let timestamp = "1700000000";
let secret = ash_derive_client_secret(&nonce, "ctx", binding).unwrap();
let proofs: HashSet<String> = (0..100)
.map(|i| {
let body_hash = ash_hash_body(&format!("body_{}", i));
ash_build_proof(&secret, timestamp, binding, &body_hash).unwrap()
})
.collect();
assert_eq!(proofs.len(), 100, "Different bodies should produce different proofs");
}
}
mod canonicalization_properties {
use super::*;
#[test]
fn test_json_idempotence() {
let inputs = [
r#"{"a":1}"#,
r#"{"z":1,"a":2}"#,
r#"{"nested":{"z":1,"a":2}}"#,
r#"{"arr":[3,1,4]}"#,
r#"{"mixed":{"arr":[1,2],"val":true}}"#,
];
for input in inputs {
let once = ash_canonicalize_json(input).unwrap();
let twice = ash_canonicalize_json(&once).unwrap();
assert_eq!(once, twice, "Canonicalization should be idempotent for: {}", input);
}
}
#[test]
fn test_json_determinism() {
let input = r#"{"z":1,"a":2,"m":{"b":3,"a":4}}"#;
let results: HashSet<String> = (0..100)
.map(|_| ash_canonicalize_json(input).unwrap())
.collect();
assert_eq!(results.len(), 1, "Canonicalization should be deterministic");
}
#[test]
fn test_json_semantic_preservation() {
let input = r#"{"z":1,"a":[3,1,4],"m":{"x":true,"y":null}}"#;
let canonical = ash_canonicalize_json(input).unwrap();
let original: serde_json::Value = serde_json::from_str(input).unwrap();
let canonicalized: serde_json::Value = serde_json::from_str(&canonical).unwrap();
assert_eq!(original, canonicalized, "Semantics should be preserved");
}
#[test]
fn test_query_idempotence() {
let inputs = [
"a=1&b=2",
"z=3&a=1&m=2",
"key=value",
"a=1&a=2&a=3",
];
for input in inputs {
let once = ash_canonicalize_query(input).unwrap();
let twice = ash_canonicalize_query(&once).unwrap();
assert_eq!(once, twice, "Query canonicalization should be idempotent");
}
}
#[test]
fn test_query_determinism() {
let input = "z=3&a=1&m=2&b=4";
let results: HashSet<String> = (0..100)
.map(|_| ash_canonicalize_query(input).unwrap())
.collect();
assert_eq!(results.len(), 1, "Query canonicalization should be deterministic");
}
#[test]
fn test_query_order_independence() {
let orderings = [
"a=1&b=2&c=3",
"b=2&a=1&c=3",
"c=3&a=1&b=2",
"c=3&b=2&a=1",
];
let results: HashSet<String> = orderings
.iter()
.map(|q| ash_canonicalize_query(q).unwrap())
.collect();
assert_eq!(results.len(), 1, "Different orderings should produce same output");
}
}
mod scope_properties {
use super::*;
#[test]
fn test_scope_order_independence() {
let nonce = "a".repeat(64);
let context_id = "ctx_test";
let binding = "POST|/api|";
let timestamp = "1700000000";
let payload = r#"{"a":1,"b":2,"c":3}"#;
let secret = ash_derive_client_secret(&nonce, context_id, binding).unwrap();
let scope1 = vec!["a", "b", "c"];
let scope2 = vec!["c", "b", "a"];
let scope3 = vec!["b", "a", "c"];
let result1 = ash_build_proof_scoped(&secret, timestamp, binding, payload, &scope1).unwrap();
let result2 = ash_build_proof_scoped(&secret, timestamp, binding, payload, &scope2).unwrap();
let result3 = ash_build_proof_scoped(&secret, timestamp, binding, payload, &scope3).unwrap();
assert_eq!(result1, result2, "Scope order should not affect result");
assert_eq!(result2, result3, "Scope order should not affect result");
}
#[test]
fn test_scope_deduplication() {
let nonce = "a".repeat(64);
let context_id = "ctx_test";
let binding = "POST|/api|";
let timestamp = "1700000000";
let payload = r#"{"a":1,"b":2}"#;
let secret = ash_derive_client_secret(&nonce, context_id, binding).unwrap();
let scope_single = vec!["a"];
let scope_dup = vec!["a", "a", "a"];
let result_single = ash_build_proof_scoped(&secret, timestamp, binding, payload, &scope_single).unwrap();
let result_dup = ash_build_proof_scoped(&secret, timestamp, binding, payload, &scope_dup).unwrap();
assert_eq!(result_single, result_dup, "Duplicates should be deduplicated");
}
#[test]
fn test_empty_scope_consistency() {
let nonce = "a".repeat(64);
let context_id = "ctx_test";
let binding = "POST|/api|";
let timestamp = "1700000000";
let payload = r#"{"a":1}"#;
let secret = ash_derive_client_secret(&nonce, context_id, binding).unwrap();
let empty_scope: Vec<&str> = vec![];
let results: HashSet<(String, String)> = (0..100)
.map(|_| ash_build_proof_scoped(&secret, timestamp, binding, payload, &empty_scope).unwrap())
.collect();
assert_eq!(results.len(), 1, "Empty scope should produce consistent result");
}
}
mod timing_properties {
use super::*;
#[test]
fn test_equality_reflexivity() {
let values = [
b"".to_vec(),
b"a".to_vec(),
b"abc".to_vec(),
b"x".repeat(100),
];
for v in values {
assert!(ash_timing_safe_equal(&v, &v), "Reflexivity failed");
}
}
#[test]
fn test_equality_symmetry() {
let pairs = [
(b"abc".to_vec(), b"abc".to_vec()),
(b"abc".to_vec(), b"abd".to_vec()),
(b"short".to_vec(), b"longer".to_vec()),
];
for (a, b) in pairs {
let ab = ash_timing_safe_equal(&a, &b);
let ba = ash_timing_safe_equal(&b, &a);
assert_eq!(ab, ba, "Symmetry failed for {:?} and {:?}", a, b);
}
}
#[test]
fn test_equality_transitivity() {
let a = b"test";
let b = b"test";
let c = b"test";
assert!(ash_timing_safe_equal(a, b));
assert!(ash_timing_safe_equal(b, c));
assert!(ash_timing_safe_equal(a, c), "Transitivity failed");
}
#[test]
fn test_inequality_for_different() {
let pairs: Vec<(&[u8], &[u8])> = vec![
(b"abc", b"abd"),
(b"abc", b"ABC"),
(b"abc", b"abcd"),
(b"", b"a"),
];
for (a, b) in pairs {
assert!(!ash_timing_safe_equal(a, b), "Should be unequal: {:?} vs {:?}", a, b);
}
}
}
mod mathematical_invariants {
use super::*;
#[test]
fn test_secret_derivation_is_function() {
let nonce = "a".repeat(64);
let context_id = "ctx_test";
let binding = "POST|/api|";
let secrets: HashSet<String> = (0..100)
.map(|_| ash_derive_client_secret(&nonce, context_id, binding).unwrap())
.collect();
assert_eq!(secrets.len(), 1, "Secret derivation should be a function");
}
#[test]
fn test_hash_is_function() {
let input = "test content";
let hashes: HashSet<String> = (0..100)
.map(|_| ash_hash_body(input))
.collect();
assert_eq!(hashes.len(), 1, "Hash should be a function");
}
#[test]
fn test_proof_build_verify_inverse() {
let nonce = "a".repeat(64);
let context_id = "ctx_test";
let binding = "POST|/api|";
let body_hash = "b".repeat(64);
let secret = ash_derive_client_secret(&nonce, context_id, binding).unwrap();
for i in 0..100 {
let timestamp = format!("{}", 1700000000 + i);
let proof = ash_build_proof(&secret, ×tamp, binding, &body_hash).unwrap();
let valid = ash_verify_proof(&nonce, context_id, binding, ×tamp, &body_hash, &proof).unwrap();
assert!(valid, "Build and verify should be inverses");
}
}
}