use super::webhook_sig::*;
#[test]
fn rfc4231_tc1() {
let key = [0x0bu8; 20];
assert_eq!(
compute_hmac_hex(&key, b"Hi There"),
"b0344c61d8db38535ca8afceaf0bf12b881dc200c9833da726e9376c2e32cff7"
);
}
#[test]
fn rfc4231_tc2() {
assert_eq!(
compute_hmac_hex(b"Jefe", b"what do ya want for nothing?"),
"5bdcc146bf60754e6a042426089575c75a003f089d2739839dec58b964ec3843"
);
}
#[test]
fn rfc4231_tc3() {
let key = [0xaau8; 20];
let data = [0xddu8; 50];
assert_eq!(
compute_hmac_hex(&key, &data),
"773ea91e36800e46854db8ebd09181a72959098b3ef8c122d9635514ced565fe"
);
}
#[test]
fn rfc4231_tc6_key_longer_than_block() {
let key = [0xaau8; 131];
assert_eq!(
compute_hmac_hex(
&key,
b"Test Using Larger Than Block-Size Key - Hash Key First"
),
"60e431591ee0b67f0d8a26aacbf5b77f8e0bc6213728c5140546040f0ee37f54"
);
}
#[test]
fn hmac_different_data() {
let a = compute_hmac_hex(b"key", b"data-one");
let b = compute_hmac_hex(b"key", b"data-two");
assert_ne!(a, b);
}
#[test]
fn hmac_handles_empty_inputs() {
assert_eq!(
compute_hmac_hex(b"", b""),
"b613679a0814d9ec772f95d778c35fc5ff1697c493715653c6c712144292c5ad"
);
}
#[test]
fn verify_accepts_an_openssl_generated_signature() {
let sig = "5bdcc146bf60754e6a042426089575c75a003f089d2739839dec58b964ec3843";
assert!(verify_hex(b"Jefe", b"what do ya want for nothing?", sig));
}
#[test]
fn verify_is_hex_case_insensitive() {
let upper = "5BDCC146BF60754E6A042426089575C75A003F089D2739839DEC58B964EC3843";
assert!(verify_hex(b"Jefe", b"what do ya want for nothing?", upper));
}
#[test]
fn verify_rejects_wrong_secret_and_wrong_data() {
let sig = "5bdcc146bf60754e6a042426089575c75a003f089d2739839dec58b964ec3843";
assert!(!verify_hex(b"wrong", b"what do ya want for nothing?", sig));
assert!(!verify_hex(b"Jefe", b"tampered", sig));
}
#[test]
fn verify_rejects_malformed_hex() {
for bad in [
"",
"abc",
"zz2f679a0814d9ec772f95d778c35fc5ff1697c493715653c6c712144292c5ad",
"5bdcc146bf60754e6a042426089575c75a003f089d2739839dec58b964ec38",
"5bdcc146bf60754e6a042426089575c75a003f089d2739839dec58b964ec3843ff",
] {
assert!(!verify_hex(b"Jefe", b"data", bad), "accepted {bad:?}");
}
}
#[test]
fn verify_covers_non_utf8_bytes() {
let mut body: Vec<u8> = br#"{"a":"#.to_vec();
body.push(0xff);
body.push(b'}');
assert!(
std::str::from_utf8(&body).is_err(),
"fixture must be non-UTF-8"
);
let sig = compute_hmac_hex(b"s3cret", &body);
assert!(verify_hex(b"s3cret", &body, &sig));
let lossy = String::from_utf8_lossy(&body).into_owned();
assert!(!verify_hex(b"s3cret", lossy.as_bytes(), &sig));
}
#[test]
fn canonical_payload_is_newline_separated() {
let p = canonical_payload(1785350000, "POST", "/webhook", b"{\"a\":1}");
assert_eq!(
String::from_utf8(p).unwrap(),
"t=1785350000\nPOST\n/webhook\n{\"a\":1}"
);
}
#[test]
fn signature_does_not_transfer_between_paths() {
let secret = b"s3cret";
let body = b"{\"action\":\"go\"}";
let t = 1785350000;
let deploy = compute_hmac_hex(secret, &canonical_payload(t, "POST", "/hooks/deploy", body));
let destroy_payload = canonical_payload(t, "POST", "/hooks/destroy", body);
assert!(!verify_hex(secret, &destroy_payload, &deploy));
}
#[test]
fn signature_does_not_transfer_between_methods() {
let secret = b"s3cret";
let body = b"{}";
let t = 1785350000;
let post = compute_hmac_hex(secret, &canonical_payload(t, "POST", "/webhook", body));
let put_payload = canonical_payload(t, "PUT", "/webhook", body);
assert!(!verify_hex(secret, &put_payload, &post));
}
#[test]
fn dotted_path_cannot_shift_the_boundary() {
let a = canonical_payload(1, "POST", "/a.b", b"x");
let b = canonical_payload(1, "POST", "/a", b".b\nx");
assert_ne!(a, b);
}
#[test]
fn parse_signature_header_extracts_t_and_v1() {
let h = parse_forjar_signature("t=1785350000,v1=deadbeef");
assert_eq!(h.timestamp, Some(1785350000));
assert_eq!(h.v1, vec!["deadbeef".to_string()]);
assert!(h.has_v1());
}
#[test]
fn parse_signature_header_keeps_every_v1() {
let h = parse_forjar_signature("t=1,v1=aaa,v1=bbb");
assert_eq!(h.v1, vec!["aaa".to_string(), "bbb".to_string()]);
}
#[test]
fn parse_signature_header_without_v1_is_not_signed() {
let h = parse_forjar_signature("t=1,v2=future,junk");
assert!(!h.has_v1());
assert_eq!(h.timestamp, Some(1));
}
#[test]
fn parse_signature_header_tolerates_whitespace_and_bad_t() {
let h = parse_forjar_signature(" t = 12 , v1 = abc ");
assert_eq!(h.timestamp, Some(12));
assert_eq!(h.v1, vec!["abc".to_string()]);
assert_eq!(parse_forjar_signature("t=notanumber,v1=a").timestamp, None);
}
#[test]
fn parse_github_signature_strips_the_prefix() {
assert_eq!(
parse_github_signature("sha256=abc123").as_deref(),
Some("abc123")
);
assert!(parse_github_signature("sha1=abc123").is_none());
assert!(parse_github_signature("abc123").is_none());
}
#[test]
fn timestamp_freshness_window() {
let now = 1_000_000;
assert!(timestamp_is_fresh(now, now, 300));
assert!(timestamp_is_fresh(now - 300, now, 300));
assert!(!timestamp_is_fresh(now - 301, now, 300));
}
#[test]
fn timestamp_in_the_future_is_rejected() {
let now = 1_000_000;
assert!(timestamp_is_fresh(now + 300, now, 300));
assert!(!timestamp_is_fresh(now + 301, now, 300));
}
#[test]
fn replay_guard_admits_once() {
let mut g = ReplayGuard::new(300, 16);
assert!(g.admit("sig-a", 1000));
assert!(!g.admit("sig-a", 1000), "second admit must be refused");
assert!(g.admit("sig-b", 1000));
assert_eq!(g.len(), 2);
}
#[test]
fn replay_guard_expires_outside_the_window() {
let mut g = ReplayGuard::new(300, 16);
assert!(g.admit("sig-a", 1000));
assert!(g.admit("sig-b", 1400));
assert!(!g.seen_contains("sig-a"));
}
#[test]
fn replay_guard_is_bounded() {
let mut g = ReplayGuard::new(300, 8);
for i in 0..100 {
assert!(g.admit(&format!("sig-{i}"), 1000));
}
assert!(g.len() <= 8, "grew to {}", g.len());
}
#[test]
fn replay_guard_starts_empty() {
let g = ReplayGuard::new(300, 4);
assert!(g.is_empty());
assert_eq!(g.len(), 0);
}