use ash_core::{
ash_build_proof, ash_verify_proof, ash_derive_client_secret,
ash_canonicalize_json, ash_canonicalize_query,
ash_hash_body, ash_validate_timestamp,
ash_extract_scoped_fields, ash_extract_scoped_fields_strict,
};
mod error_messages {
use super::*;
#[test]
fn test_meaningful_error_for_empty_nonce() {
let result = ash_derive_client_secret("", "ctx", "GET|/|");
assert!(result.is_err());
let error = result.unwrap_err();
let msg = error.to_string().to_lowercase();
assert!(msg.contains("nonce") || msg.contains("empty") || msg.contains("required"),
"Error should mention nonce: {}", error);
}
#[test]
fn test_meaningful_error_for_invalid_nonce_format() {
let result = ash_derive_client_secret("not-hex-characters!!", "ctx", "GET|/|");
assert!(result.is_err());
let error = result.unwrap_err();
let msg = error.to_string().to_lowercase();
assert!(msg.contains("nonce") || msg.contains("hex") || msg.contains("invalid"),
"Error should mention nonce format: {}", error);
}
#[test]
fn test_meaningful_error_for_short_nonce() {
let result = ash_derive_client_secret("abc123", "ctx", "GET|/|");
assert!(result.is_err());
let error = result.unwrap_err();
let msg = error.to_string().to_lowercase();
assert!(msg.contains("nonce") || msg.contains("length") || msg.contains("short") || msg.contains("32"),
"Error should mention nonce length: {}", error);
}
#[test]
fn test_meaningful_error_for_empty_context_id() {
let nonce = "a".repeat(64);
let result = ash_derive_client_secret(&nonce, "", "GET|/|");
assert!(result.is_err());
let error = result.unwrap_err();
let msg = error.to_string().to_lowercase();
assert!(msg.contains("context") || msg.contains("empty") || msg.contains("required"),
"Error should mention context_id: {}", error);
}
#[test]
fn test_meaningful_error_for_empty_binding() {
let nonce = "a".repeat(64);
let result = ash_derive_client_secret(&nonce, "ctx", "");
assert!(result.is_err());
let error = result.unwrap_err();
let msg = error.to_string().to_lowercase();
assert!(msg.contains("binding") || msg.contains("empty") || msg.contains("required"),
"Error should mention binding: {}", error);
}
#[test]
fn test_meaningful_error_for_invalid_json() {
let result = ash_canonicalize_json("not json");
assert!(result.is_err());
let error = result.unwrap_err();
let msg = error.to_string().to_lowercase();
assert!(msg.contains("json") || msg.contains("parse") || msg.contains("invalid") || msg.contains("syntax"),
"Error should mention JSON: {}", error);
}
#[test]
fn test_meaningful_error_for_invalid_timestamp() {
let result = ash_validate_timestamp("not-a-number", 300, 60);
assert!(result.is_err());
let error = result.unwrap_err();
let msg = error.to_string().to_lowercase();
assert!(msg.contains("timestamp") || msg.contains("invalid") || msg.contains("format"),
"Error should mention timestamp: {}", error);
}
#[test]
fn test_meaningful_error_for_expired_timestamp() {
let old_ts = (chrono::Utc::now().timestamp() - 3600).to_string();
let result = ash_validate_timestamp(&old_ts, 300, 60);
assert!(result.is_err());
let error = result.unwrap_err();
let msg = error.to_string().to_lowercase();
assert!(msg.contains("expired") || msg.contains("old") || msg.contains("past"),
"Error should mention expiration: {}", error);
}
#[test]
fn test_meaningful_error_for_future_timestamp() {
let future_ts = (chrono::Utc::now().timestamp() + 3600).to_string();
let result = ash_validate_timestamp(&future_ts, 300, 60);
assert!(result.is_err());
let error = result.unwrap_err();
let msg = error.to_string().to_lowercase();
assert!(msg.contains("future") || msg.contains("ahead"),
"Error should mention future timestamp: {}", error);
}
}
mod resource_limits {
use super::*;
#[test]
fn test_meaningful_error_for_oversized_json() {
let large_data = "x".repeat(11 * 1024 * 1024);
let large_json = format!(r#"{{"data":"{}"}}"#, large_data);
let result = ash_canonicalize_json(&large_json);
assert!(result.is_err());
let error = result.unwrap_err();
let msg = error.to_string().to_lowercase();
assert!(msg.contains("size") || msg.contains("large") || msg.contains("maximum") || msg.contains("exceed"),
"Error should mention size: {}", error);
}
#[test]
fn test_meaningful_error_for_deeply_nested_json() {
let mut deep_json = String::from("1");
for _ in 0..100 {
deep_json = format!(r#"{{"a":{}}}"#, deep_json);
}
let result = ash_canonicalize_json(&deep_json);
assert!(result.is_err());
let error = result.unwrap_err();
let msg = error.to_string().to_lowercase();
assert!(msg.contains("depth") || msg.contains("nested") || msg.contains("deep") || msg.contains("recursion"),
"Error should mention nesting depth: {}", error);
}
#[test]
fn test_meaningful_error_for_oversized_nonce() {
let long_nonce = "a".repeat(513); let result = ash_derive_client_secret(&long_nonce, "ctx", "GET|/|");
assert!(result.is_err());
let error = result.unwrap_err();
let msg = error.to_string().to_lowercase();
assert!(msg.contains("nonce") || msg.contains("length") || msg.contains("long") || msg.contains("maximum"),
"Error should mention nonce length: {}", error);
}
}
mod scope_errors {
use super::*;
#[test]
fn test_dangerous_keys_handling() {
let payload = serde_json::json!({"a": 1});
let result = ash_extract_scoped_fields(&payload, &["__proto__"]);
assert!(result.is_ok());
let extracted = result.unwrap();
assert!(extracted.get("__proto__").is_none());
}
#[test]
fn test_error_for_missing_field_strict() {
let payload = serde_json::json!({"a": 1});
let result = ash_extract_scoped_fields_strict(&payload, &["b"], true);
assert!(result.is_err());
let error = result.unwrap_err();
let msg = error.to_string().to_lowercase();
assert!(msg.contains("missing") || msg.contains("not found") || msg.contains("field") || msg.contains("path"),
"Error should mention missing field: {}", error);
}
#[test]
fn test_error_for_invalid_array_index() {
let payload = serde_json::json!({"items": [1, 2]});
let result = ash_extract_scoped_fields(&payload, &["items[999999999]"]);
if let Ok(extracted) = result {
let is_empty = extracted.as_object().map(|o| o.is_empty()).unwrap_or(true);
let has_key = extracted.get("items[999999999]").is_some();
assert!(is_empty || !has_key);
}
}
}
mod no_leakage {
use super::*;
#[test]
fn test_no_nonce_in_error() {
let secret_nonce = format!("secret_nonce_{}", "a".repeat(51));
let result = ash_derive_client_secret(&secret_nonce, "ctx", "GET|/|");
if let Err(e) = result {
let msg = e.to_string();
assert!(!msg.contains("secret_nonce_"),
"Error should not contain nonce value");
}
}
#[test]
fn test_no_secret_in_error() {
let nonce = "a".repeat(64);
let secret = ash_derive_client_secret(&nonce, "ctx_test", "POST|/api|").unwrap();
let result = ash_build_proof(&secret, "invalid", "POST|/api|", "invalid-hash");
if let Err(e) = result {
let msg = e.to_string();
assert!(!msg.contains(&secret),
"Error should not contain client secret");
}
}
#[test]
fn test_verification_failure_no_expected_proof() {
let nonce = "a".repeat(64);
let context_id = "ctx_test";
let binding = "POST|/api/test|";
let timestamp = chrono::Utc::now().timestamp().to_string();
let body_hash = "b".repeat(64);
let wrong_proof = "c".repeat(64);
let result = ash_verify_proof(&nonce, context_id, binding, ×tamp, &body_hash, &wrong_proof);
assert!(result.is_ok());
assert!(!result.unwrap());
}
}
mod graceful_handling {
use super::*;
#[test]
fn test_verification_returns_false_not_throw() {
let nonce = "a".repeat(64);
let context_id = "ctx_test";
let binding = "POST|/api/test|";
let timestamp = chrono::Utc::now().timestamp().to_string();
let body_hash = "b".repeat(64);
let invalid_proof = "x".repeat(64);
let result = ash_verify_proof(&nonce, context_id, binding, ×tamp, &body_hash, &invalid_proof);
match result {
Ok(valid) => assert!(!valid),
Err(_) => {} }
}
#[test]
fn test_empty_body_hash() {
let hash = ash_hash_body("");
assert_eq!(hash.len(), 64);
}
#[test]
fn test_empty_query_canonicalization() {
let result = ash_canonicalize_query("").unwrap();
assert_eq!(result, "");
}
#[test]
fn test_empty_json_canonicalization() {
let result = ash_canonicalize_json("{}").unwrap();
assert_eq!(result, "{}");
}
}
mod recovery {
use super::*;
#[test]
fn test_recover_after_multiple_failures() {
for _ in 0..10 {
let _ = ash_canonicalize_json("invalid json");
}
let result = ash_canonicalize_json(r#"{"valid":"json"}"#);
assert!(result.is_ok());
assert_eq!(result.unwrap(), r#"{"valid":"json"}"#);
}
#[test]
fn test_recover_after_verification_failures() {
let nonce = "a".repeat(64);
let context_id = "ctx_test";
let binding = "POST|/api/test|";
let timestamp = chrono::Utc::now().timestamp().to_string();
let body_hash = "b".repeat(64);
for _ in 0..10 {
let _ = ash_verify_proof(&nonce, context_id, binding, ×tamp, &body_hash, &"c".repeat(64));
}
let secret = ash_derive_client_secret(&nonce, context_id, binding).unwrap();
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);
}
}
mod error_codes {
use super::*;
#[test]
fn test_error_code_accessible() {
let result = ash_derive_client_secret("", "ctx", "GET|/|");
if let Err(e) = result {
let _ = e.code();
}
}
#[test]
fn test_error_display() {
let result = ash_derive_client_secret("", "ctx", "GET|/|");
if let Err(e) = result {
let msg = format!("{}", e);
assert!(!msg.is_empty());
}
}
}