use ash_core::headers::{ash_extract_headers, HeaderMapView};
use ash_core::{AshErrorCode, InternalReason, ash_validate_nonce, ash_validate_timestamp_format};
struct TestHeaders(Vec<(String, String)>);
impl HeaderMapView for TestHeaders {
fn get_all_ci(&self, name: &str) -> Vec<&str> {
let name_lower = name.to_ascii_lowercase();
self.0
.iter()
.filter(|(k, _)| k.to_ascii_lowercase() == name_lower)
.map(|(_, v)| v.as_str())
.collect()
}
}
fn full_valid_headers() -> TestHeaders {
TestHeaders(vec![
("x-ash-ts".into(), "1700000000".into()),
("x-ash-nonce".into(), "0123456789abcdef0123456789abcdef".into()),
("x-ash-body-hash".into(), "a".repeat(64)),
("x-ash-proof".into(), "b".repeat(64)),
])
}
#[test]
fn precedence_missing_ts_before_invalid_nonce() {
let h = TestHeaders(vec![
("x-ash-nonce".into(), "too_short".into()),
("x-ash-body-hash".into(), "a".repeat(64)),
("x-ash-proof".into(), "b".repeat(64)),
]);
let err = ash_extract_headers(&h).unwrap_err();
assert_eq!(err.code(), AshErrorCode::ValidationError);
assert_eq!(err.reason(), InternalReason::HdrMissing);
assert!(err.details().unwrap().get("header").unwrap().contains("ts"));
}
#[test]
fn precedence_missing_ts_before_missing_nonce() {
let h = TestHeaders(vec![
("x-ash-body-hash".into(), "a".repeat(64)),
("x-ash-proof".into(), "b".repeat(64)),
]);
let err = ash_extract_headers(&h).unwrap_err();
assert_eq!(err.reason(), InternalReason::HdrMissing);
assert!(err.details().unwrap().get("header").unwrap().contains("ts"));
}
#[test]
fn precedence_missing_nonce_before_missing_body_hash() {
let h = TestHeaders(vec![
("x-ash-ts".into(), "1700000000".into()),
("x-ash-proof".into(), "b".repeat(64)),
]);
let err = ash_extract_headers(&h).unwrap_err();
assert_eq!(err.reason(), InternalReason::HdrMissing);
assert!(err.details().unwrap().get("header").unwrap().contains("nonce"));
}
#[test]
fn precedence_missing_body_hash_before_missing_proof() {
let h = TestHeaders(vec![
("x-ash-ts".into(), "1700000000".into()),
("x-ash-nonce".into(), "0123456789abcdef0123456789abcdef".into()),
]);
let err = ash_extract_headers(&h).unwrap_err();
assert_eq!(err.reason(), InternalReason::HdrMissing);
assert!(err.details().unwrap().get("header").unwrap().contains("body-hash"));
}
#[test]
fn reason_hdr_missing_maps_to_validation_485() {
let h = TestHeaders(vec![
("x-ash-nonce".into(), "0123456789abcdef0123456789abcdef".into()),
("x-ash-body-hash".into(), "a".repeat(64)),
("x-ash-proof".into(), "b".repeat(64)),
]);
let err = ash_extract_headers(&h).unwrap_err();
assert_eq!(err.code(), AshErrorCode::ValidationError);
assert_eq!(err.http_status(), 485);
assert_eq!(err.reason(), InternalReason::HdrMissing);
}
#[test]
fn reason_hdr_multi_maps_to_validation_485() {
let h = TestHeaders(vec![
("x-ash-ts".into(), "1700000000".into()),
("x-ash-ts".into(), "1700000001".into()), ("x-ash-nonce".into(), "0123456789abcdef0123456789abcdef".into()),
("x-ash-body-hash".into(), "a".repeat(64)),
("x-ash-proof".into(), "b".repeat(64)),
]);
let err = ash_extract_headers(&h).unwrap_err();
assert_eq!(err.code(), AshErrorCode::ValidationError);
assert_eq!(err.http_status(), 485);
assert_eq!(err.reason(), InternalReason::HdrMultiValue);
}
#[test]
fn reason_hdr_invalid_chars_maps_to_validation_485() {
let h = TestHeaders(vec![
("x-ash-ts".into(), "1700\n000000".into()), ("x-ash-nonce".into(), "0123456789abcdef0123456789abcdef".into()),
("x-ash-body-hash".into(), "a".repeat(64)),
("x-ash-proof".into(), "b".repeat(64)),
]);
let err = ash_extract_headers(&h).unwrap_err();
assert_eq!(err.code(), AshErrorCode::ValidationError);
assert_eq!(err.http_status(), 485);
assert_eq!(err.reason(), InternalReason::HdrInvalidChars);
}
#[test]
fn reason_nonce_too_short_maps_to_validation_485() {
let err = ash_validate_nonce("abcdef").unwrap_err();
assert_eq!(err.code(), AshErrorCode::ValidationError);
assert_eq!(err.http_status(), 485);
assert_eq!(err.reason(), InternalReason::NonceTooShort);
}
#[test]
fn reason_nonce_too_long_maps_to_validation_485() {
let err = ash_validate_nonce(&"a".repeat(513)).unwrap_err();
assert_eq!(err.code(), AshErrorCode::ValidationError);
assert_eq!(err.http_status(), 485);
assert_eq!(err.reason(), InternalReason::NonceTooLong);
}
#[test]
fn reason_nonce_invalid_chars_maps_to_validation_485() {
let err = ash_validate_nonce(&format!("{}XY", "a".repeat(30))).unwrap_err();
assert_eq!(err.code(), AshErrorCode::ValidationError);
assert_eq!(err.http_status(), 485);
assert_eq!(err.reason(), InternalReason::NonceInvalidChars);
}
#[test]
fn reason_ts_parse_maps_to_timestamp_482() {
let err = ash_validate_timestamp_format("not_a_number").unwrap_err();
assert_eq!(err.code(), AshErrorCode::TimestampInvalid);
assert_eq!(err.http_status(), 482);
}
#[test]
fn reason_ts_leading_zeros_maps_to_timestamp_482() {
let err = ash_validate_timestamp_format("0123456789").unwrap_err();
assert_eq!(err.code(), AshErrorCode::TimestampInvalid);
assert_eq!(err.http_status(), 482);
}
#[test]
fn reason_ts_empty_maps_to_timestamp_482() {
let err = ash_validate_timestamp_format("").unwrap_err();
assert_eq!(err.code(), AshErrorCode::TimestampInvalid);
assert_eq!(err.http_status(), 482);
}
#[test]
fn headers_order_does_not_matter() {
let h1 = TestHeaders(vec![
("x-ash-ts".into(), "1700000000".into()),
("x-ash-nonce".into(), "0123456789abcdef0123456789abcdef".into()),
("x-ash-body-hash".into(), "a".repeat(64)),
("x-ash-proof".into(), "b".repeat(64)),
]);
let h2 = TestHeaders(vec![
("x-ash-proof".into(), "b".repeat(64)),
("x-ash-body-hash".into(), "a".repeat(64)),
("x-ash-nonce".into(), "0123456789abcdef0123456789abcdef".into()),
("x-ash-ts".into(), "1700000000".into()),
]);
let h3 = TestHeaders(vec![
("x-ash-body-hash".into(), "a".repeat(64)),
("x-ash-ts".into(), "1700000000".into()),
("x-ash-proof".into(), "b".repeat(64)),
("x-ash-nonce".into(), "0123456789abcdef0123456789abcdef".into()),
]);
let b1 = ash_extract_headers(&h1).unwrap();
let b2 = ash_extract_headers(&h2).unwrap();
let b3 = ash_extract_headers(&h3).unwrap();
assert_eq!(b1.ts, b2.ts);
assert_eq!(b2.ts, b3.ts);
assert_eq!(b1.nonce, b2.nonce);
assert_eq!(b2.nonce, b3.nonce);
assert_eq!(b1.body_hash, b2.body_hash);
assert_eq!(b2.body_hash, b3.body_hash);
assert_eq!(b1.proof, b2.proof);
assert_eq!(b2.proof, b3.proof);
}
#[test]
fn headers_mixed_case_all_orders() {
let h1 = TestHeaders(vec![
("X-ASH-TS".into(), "1700000000".into()),
("X-Ash-Nonce".into(), "0123456789abcdef0123456789abcdef".into()),
("x-ASH-BODY-HASH".into(), "a".repeat(64)),
("x-ash-PROOF".into(), "b".repeat(64)),
]);
let h2 = TestHeaders(vec![
("x-ash-proof".into(), "b".repeat(64)),
("x-ash-body-hash".into(), "a".repeat(64)),
("x-ash-nonce".into(), "0123456789abcdef0123456789abcdef".into()),
("x-ash-ts".into(), "1700000000".into()),
]);
let b1 = ash_extract_headers(&h1).unwrap();
let b2 = ash_extract_headers(&h2).unwrap();
assert_eq!(b1.ts, b2.ts);
assert_eq!(b1.nonce, b2.nonce);
}
#[test]
fn nonce_validator_matches_derive_behavior() {
let nonce = "0123456789abcdef0123456789abcdef";
assert!(ash_validate_nonce(nonce).is_ok());
assert!(ash_core::ash_derive_client_secret(nonce, "ctx_test", "POST|/api|").is_ok());
}
#[test]
fn nonce_validator_rejects_same_as_derive() {
let short_nonce = "abcdef";
assert!(ash_validate_nonce(short_nonce).is_err());
assert!(ash_core::ash_derive_client_secret(short_nonce, "ctx_test", "POST|/api|").is_err());
let bad_chars = "0123456789abcdef0123456789abcdXY";
assert!(ash_validate_nonce(bad_chars).is_err());
assert!(ash_core::ash_derive_client_secret(bad_chars, "ctx_test", "POST|/api|").is_err());
}