use ash_core::{
ash_build_proof, ash_verify_proof, ash_derive_client_secret,
ash_build_proof_scoped, ash_verify_proof_scoped,
ash_build_proof_unified, ash_verify_proof_unified,
ash_hash_body, ash_canonicalize_json,
ASH_SDK_VERSION, ASH_VERSION_PREFIX,
};
mod protocol_version {
use super::*;
#[test]
fn test_version_constant_exists() {
assert!(!ASH_SDK_VERSION.is_empty(), "SDK version should be defined");
assert!(!ASH_VERSION_PREFIX.is_empty(), "Version prefix should be defined");
}
#[test]
fn test_version_format() {
let version = ASH_SDK_VERSION;
let parts: Vec<&str> = version.split('.').collect();
assert!(parts.len() >= 2, "Version should have at least major.minor");
}
}
mod v21_compatibility {
use super::*;
#[test]
fn test_v21_proof_format() {
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();
assert_eq!(proof.len(), 64);
assert!(proof.chars().all(|c| c.is_ascii_hexdigit()));
}
#[test]
fn test_v21_verification_works() {
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();
let valid = ash_verify_proof(&nonce, context_id, binding, timestamp, &body_hash, &proof).unwrap();
assert!(valid, "v2.1 proof should verify");
}
#[test]
fn test_v21_deterministic_across_calls() {
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 proof1 = ash_build_proof(&secret, timestamp, binding, &body_hash).unwrap();
let proof2 = ash_build_proof(&secret, timestamp, binding, &body_hash).unwrap();
assert_eq!(proof1, proof2, "v2.1 proofs should be deterministic");
}
}
mod v22_scoped_compatibility {
use super::*;
#[test]
fn test_v22_scoped_proof_format() {
let nonce = "a".repeat(64);
let context_id = "ctx_test";
let binding = "POST|/api/test|";
let timestamp = "1700000000";
let payload = r#"{"amount":100,"memo":"test"}"#;
let scope = vec!["amount"];
let secret = ash_derive_client_secret(&nonce, context_id, binding).unwrap();
let (proof, scope_hash) = ash_build_proof_scoped(&secret, timestamp, binding, payload, &scope).unwrap();
assert_eq!(proof.len(), 64);
assert_eq!(scope_hash.len(), 64);
}
#[test]
fn test_v22_scoped_verification_works() {
let nonce = "a".repeat(64);
let context_id = "ctx_test";
let binding = "POST|/api/test|";
let timestamp = "1700000000";
let payload = r#"{"amount":100,"memo":"test"}"#;
let scope = vec!["amount"];
let secret = ash_derive_client_secret(&nonce, context_id, binding).unwrap();
let (proof, scope_hash) = ash_build_proof_scoped(&secret, timestamp, binding, payload, &scope).unwrap();
let valid = ash_verify_proof_scoped(&nonce, context_id, binding, timestamp, payload, &scope, &scope_hash, &proof).unwrap();
assert!(valid, "v2.2 scoped proof should verify");
}
#[test]
fn test_v22_empty_scope_works() {
let nonce = "a".repeat(64);
let context_id = "ctx_test";
let binding = "POST|/api/test|";
let timestamp = "1700000000";
let payload = r#"{"amount":100}"#;
let empty_scope: Vec<&str> = vec![];
let secret = ash_derive_client_secret(&nonce, context_id, binding).unwrap();
let (proof, scope_hash) = ash_build_proof_scoped(&secret, timestamp, binding, payload, &empty_scope).unwrap();
assert!(scope_hash.is_empty() || scope_hash.len() == 64);
let valid = ash_verify_proof_scoped(&nonce, context_id, binding, timestamp, payload, &empty_scope, &scope_hash, &proof).unwrap();
assert!(valid);
}
}
mod v23_unified_compatibility {
use super::*;
#[test]
fn test_v23_unified_format() {
let nonce = "a".repeat(64);
let context_id = "ctx_test";
let binding = "POST|/api/test|";
let timestamp = "1700000000";
let payload = r#"{"amount":100}"#;
let scope = vec!["amount"];
let secret = ash_derive_client_secret(&nonce, context_id, binding).unwrap();
let result = ash_build_proof_unified(&secret, timestamp, binding, payload, &scope, None).unwrap();
assert_eq!(result.proof.len(), 64);
assert!(result.scope_hash.len() == 64 || result.scope_hash.is_empty());
assert!(result.chain_hash.is_empty());
}
#[test]
fn test_v23_unified_verification_works() {
let nonce = "a".repeat(64);
let context_id = "ctx_test";
let binding = "POST|/api/test|";
let timestamp = "1700000000";
let payload = r#"{"amount":100}"#;
let scope = vec!["amount"];
let secret = ash_derive_client_secret(&nonce, context_id, binding).unwrap();
let result = ash_build_proof_unified(&secret, timestamp, binding, payload, &scope, None).unwrap();
let valid = ash_verify_proof_unified(
&nonce, context_id, binding, timestamp, payload,
&result.proof, &scope, &result.scope_hash,
None, &result.chain_hash
).unwrap();
assert!(valid);
}
#[test]
fn test_v23_chaining_works() {
let nonce = "a".repeat(64);
let context_id = "ctx_test";
let timestamp = "1700000000";
let binding1 = "POST|/api/step1|";
let secret1 = ash_derive_client_secret(&nonce, context_id, binding1).unwrap();
let result1 = ash_build_proof_unified(&secret1, timestamp, binding1, "{}", &[], None).unwrap();
let binding2 = "POST|/api/step2|";
let secret2 = ash_derive_client_secret(&nonce, context_id, binding2).unwrap();
let result2 = ash_build_proof_unified(&secret2, timestamp, binding2, "{}", &[], Some(&result1.proof)).unwrap();
assert!(!result2.chain_hash.is_empty());
let valid = ash_verify_proof_unified(
&nonce, context_id, binding2, timestamp, "{}",
&result2.proof, &[], &result2.scope_hash,
Some(&result1.proof), &result2.chain_hash
).unwrap();
assert!(valid);
}
#[test]
fn test_v23_no_scope_no_chain() {
let nonce = "a".repeat(64);
let context_id = "ctx_test";
let binding = "POST|/api/test|";
let timestamp = "1700000000";
let payload = r#"{}"#;
let empty_scope: Vec<&str> = vec![];
let secret = ash_derive_client_secret(&nonce, context_id, binding).unwrap();
let result = ash_build_proof_unified(&secret, timestamp, binding, payload, &empty_scope, None).unwrap();
assert!(result.scope_hash.is_empty());
assert!(result.chain_hash.is_empty());
let valid = ash_verify_proof_unified(
&nonce, context_id, binding, timestamp, payload,
&result.proof, &empty_scope, &result.scope_hash,
None, &result.chain_hash
).unwrap();
assert!(valid);
}
}
mod backward_compatibility {
use super::*;
#[test]
fn test_v21_inputs_work_with_v23() {
let nonce = "a".repeat(64);
let context_id = "ctx_test";
let binding = "POST|/api/test|";
let timestamp = "1700000000";
let body = "{}";
let body_hash = ash_hash_body(body);
let secret = ash_derive_client_secret(&nonce, context_id, binding).unwrap();
let v21_proof = ash_build_proof(&secret, timestamp, binding, &body_hash).unwrap();
let v23_result = ash_build_proof_unified(&secret, timestamp, binding, body, &[], None).unwrap();
let v21_valid = ash_verify_proof(&nonce, context_id, binding, timestamp, &body_hash, &v21_proof).unwrap();
assert!(v21_valid);
let v23_valid = ash_verify_proof_unified(
&nonce, context_id, binding, timestamp, body,
&v23_result.proof, &[], &v23_result.scope_hash,
None, &v23_result.chain_hash
).unwrap();
assert!(v23_valid);
}
#[test]
fn test_hash_function_unchanged() {
let body = "test content";
let hash = ash_hash_body(body);
assert_eq!(hash.len(), 64);
assert!(hash.chars().all(|c| c.is_ascii_hexdigit()));
}
#[test]
fn test_canonicalization_unchanged() {
let json = r#"{"z":1,"a":2}"#;
let canonical = ash_canonicalize_json(json).unwrap();
assert_eq!(canonical, r#"{"a":2,"z":1}"#);
}
}
mod feature_detection {
use super::*;
#[test]
fn test_v21_functions_exported() {
let nonce = "a".repeat(64);
let _ = ash_derive_client_secret(&nonce, "ctx", "GET|/|");
let _ = ash_build_proof;
let _ = ash_verify_proof;
let _ = ash_hash_body;
}
#[test]
fn test_v22_functions_exported() {
let _ = ash_build_proof_scoped;
let _ = ash_verify_proof_scoped;
}
#[test]
fn test_v23_functions_exported() {
let _ = ash_build_proof_unified;
let _ = ash_verify_proof_unified;
}
}
mod migration {
use super::*;
#[test]
fn test_v21_to_v22_migration() {
let nonce = "a".repeat(64);
let context_id = "ctx_test";
let binding = "POST|/api/payment|";
let timestamp = "1700000000";
let payload = r#"{"amount":100,"memo":"test"}"#;
let secret = ash_derive_client_secret(&nonce, context_id, binding).unwrap();
let body_hash = ash_hash_body(payload);
let v21_proof = ash_build_proof(&secret, timestamp, binding, &body_hash).unwrap();
assert!(ash_verify_proof(&nonce, context_id, binding, timestamp, &body_hash, &v21_proof).unwrap());
let scope = vec!["amount"];
let (v22_proof, scope_hash) = ash_build_proof_scoped(&secret, timestamp, binding, payload, &scope).unwrap();
assert!(ash_verify_proof_scoped(&nonce, context_id, binding, timestamp, payload, &scope, &scope_hash, &v22_proof).unwrap());
}
#[test]
fn test_v22_to_v23_migration() {
let nonce = "a".repeat(64);
let context_id = "ctx_test";
let binding = "POST|/api/payment|";
let timestamp = "1700000000";
let payload = r#"{"amount":100}"#;
let scope = vec!["amount"];
let secret = ash_derive_client_secret(&nonce, context_id, binding).unwrap();
let (v22_proof, v22_scope_hash) = ash_build_proof_scoped(&secret, timestamp, binding, payload, &scope).unwrap();
let v23_result = ash_build_proof_unified(&secret, timestamp, binding, payload, &scope, None).unwrap();
assert_eq!(v22_scope_hash, v23_result.scope_hash);
assert!(ash_verify_proof_scoped(&nonce, context_id, binding, timestamp, payload, &scope, &v22_scope_hash, &v22_proof).unwrap());
assert!(ash_verify_proof_unified(&nonce, context_id, binding, timestamp, payload, &v23_result.proof, &scope, &v23_result.scope_hash, None, &v23_result.chain_hash).unwrap());
}
}