use aex_core::wire::{
is_within_clock_skew, registration_challenge_bytes, rotate_key_challenge_bytes,
transfer_intent_bytes, transfer_receipt_bytes, MAX_CLOCK_SKEW_SECS, MAX_NONCE_LEN,
MIN_NONCE_LEN,
};
use aex_core::{Endpoint, EndpointHealth, HealthStatus};
use proptest::prelude::*;
#[derive(Debug, Clone, Copy)]
enum Outcome {
Success,
Failure,
}
fn arb_outcome() -> impl Strategy<Value = Outcome> {
prop_oneof![Just(Outcome::Success), Just(Outcome::Failure)]
}
fn apply(h: EndpointHealth, o: Outcome, now: i64) -> EndpointHealth {
match o {
Outcome::Success => h.on_probe_success(now),
Outcome::Failure => h.on_probe_failure(now),
}
}
proptest! {
#[test]
fn health_counters_stay_bounded(outcomes in proptest::collection::vec(arb_outcome(), 0..200)) {
let mut h = EndpointHealth::fresh_healthy(0);
for (i, o) in outcomes.iter().enumerate() {
h = apply(h, *o, 1 + i as i64);
prop_assert!(h.consecutive_fails <= EndpointHealth::FAIL_THRESHOLD);
prop_assert!(h.consecutive_successes <= EndpointHealth::SUCCESS_THRESHOLD);
}
}
#[test]
fn three_consecutive_failures_flip_unhealthy(start_unix in any::<i64>().prop_filter(
"avoid overflow when adding 3",
|&v| v < i64::MAX - 3,
)) {
let mut h = EndpointHealth::fresh_healthy(start_unix);
for i in 1..=3 {
h = h.on_probe_failure(start_unix + i);
}
prop_assert_eq!(h.status, HealthStatus::Unhealthy);
}
#[test]
fn two_consecutive_successes_heal_unhealthy(start_unix in any::<i64>().prop_filter(
"avoid overflow when adding 2",
|&v| v < i64::MAX - 2,
)) {
let mut h = EndpointHealth {
status: HealthStatus::Unhealthy,
consecutive_fails: 0,
consecutive_successes: 0,
last_probe_unix: Some(start_unix),
};
h = h.on_probe_success(start_unix + 1);
h = h.on_probe_success(start_unix + 2);
prop_assert_eq!(h.status, HealthStatus::Healthy);
}
#[test]
fn success_always_resets_consecutive_fails(
failures in 0u8..=10,
) {
let mut h = EndpointHealth::fresh_healthy(0);
for i in 0..failures {
h = h.on_probe_failure(1 + i as i64);
}
h = h.on_probe_success(100);
prop_assert_eq!(h.consecutive_fails, 0);
}
#[test]
fn last_probe_unix_tracks_most_recent_call(outcomes in proptest::collection::vec(arb_outcome(), 1..20)) {
let mut h = EndpointHealth::fresh_healthy(0);
let final_ts = 1000 + outcomes.len() as i64;
for (i, o) in outcomes.iter().enumerate() {
h = apply(h, *o, 1000 + i as i64);
}
prop_assert_eq!(
h.last_probe_unix,
Some(final_ts - 1),
"last probe of a {}-outcome fold",
outcomes.len()
);
}
}
fn arb_ascii_token(min: usize, max: usize) -> impl Strategy<Value = String> {
let re = format!("[a-zA-Z0-9_:\\-./@]{{{},{}}}", min, max);
proptest::string::string_regex(&re).expect("ascii token regex compiles")
}
proptest! {
#[test]
fn endpoint_serde_roundtrip(
kind in arb_ascii_token(1, 32),
url in arb_ascii_token(1, 128),
priority in any::<i32>(),
hint in proptest::option::of(any::<i64>()),
include_health in any::<bool>(),
status_variant in 0u8..2,
fails in 0u8..=EndpointHealth::FAIL_THRESHOLD,
successes in 0u8..=EndpointHealth::SUCCESS_THRESHOLD,
last_probe in proptest::option::of(any::<i64>()),
) {
let health = if include_health {
Some(EndpointHealth {
status: if status_variant == 0 {
HealthStatus::Healthy
} else {
HealthStatus::Unhealthy
},
consecutive_fails: fails,
consecutive_successes: successes,
last_probe_unix: last_probe,
})
} else {
None
};
let ep = Endpoint {
kind,
url,
priority,
health_hint_unix: hint,
health,
};
let json = serde_json::to_string(&ep).unwrap();
let back: Endpoint = serde_json::from_str(&json).unwrap();
prop_assert_eq!(ep, back);
}
}
proptest! {
#[test]
fn clock_skew_never_panics(now in any::<i64>(), issued_at in any::<i64>()) {
let _ = is_within_clock_skew(now, issued_at);
}
#[test]
fn inside_window_is_accepted(
now in (i64::MIN / 2)..(i64::MAX / 2),
delta in -MAX_CLOCK_SKEW_SECS..=MAX_CLOCK_SKEW_SECS,
) {
prop_assert!(is_within_clock_skew(now, now + delta));
}
#[test]
fn outside_window_is_rejected(
now in (i64::MIN / 2)..(i64::MAX / 2),
delta_over in (MAX_CLOCK_SKEW_SECS + 1)..10_000,
) {
prop_assert!(!is_within_clock_skew(now, now + delta_over));
prop_assert!(!is_within_clock_skew(now, now - delta_over));
}
}
fn arb_ascii_label() -> impl Strategy<Value = String> {
proptest::string::string_regex("[a-zA-Z0-9_-]{1,64}").expect("label regex compiles")
}
fn arb_nonce() -> impl Strategy<Value = String> {
let re = format!("[0-9a-f]{{{},{}}}", MIN_NONCE_LEN, MAX_NONCE_LEN);
proptest::string::string_regex(&re).expect("nonce regex compiles")
}
fn arb_pubkey_hex() -> impl Strategy<Value = String> {
proptest::string::string_regex("[0-9a-f]{64}").expect("pubkey regex compiles")
}
proptest! {
#[test]
fn registration_bytes_deterministic(
pub_hex in arb_pubkey_hex(),
org in arb_ascii_label(),
name in arb_ascii_label(),
nonce in arb_nonce(),
ts in any::<i64>(),
) {
let a = registration_challenge_bytes(&pub_hex, &org, &name, &nonce, ts).unwrap();
let b = registration_challenge_bytes(&pub_hex, &org, &name, &nonce, ts).unwrap();
prop_assert_eq!(a, b);
}
#[test]
fn registration_rejects_newline_in_fields(
org_prefix in "[a-zA-Z]{1,10}",
org_suffix in "[a-zA-Z]{1,10}",
) {
let org = format!("{org_prefix}\n{org_suffix}");
let result = registration_challenge_bytes(
"aa",
&org,
"alice",
"0123456789abcdef0123456789abcdef",
0,
);
prop_assert!(result.is_err());
}
#[test]
fn rotate_key_bytes_deterministic_and_distinct(
agent in arb_ascii_token(5, 64),
old_pub in arb_pubkey_hex(),
new_pub in arb_pubkey_hex(),
nonce in arb_nonce(),
ts in any::<i64>(),
) {
if old_pub == new_pub {
let err = rotate_key_challenge_bytes(&agent, &old_pub, &new_pub, &nonce, ts);
prop_assert!(err.is_err());
} else {
let a = rotate_key_challenge_bytes(&agent, &old_pub, &new_pub, &nonce, ts).unwrap();
let b = rotate_key_challenge_bytes(&agent, &old_pub, &new_pub, &nonce, ts).unwrap();
prop_assert_eq!(a, b);
}
}
#[test]
fn transfer_intent_bytes_deterministic(
sender in arb_ascii_token(5, 64),
recipient in arb_ascii_token(1, 128),
size in any::<u64>(),
mime in "[a-zA-Z0-9/_.-]{0,64}",
filename in "[a-zA-Z0-9_.-]{0,64}",
nonce in arb_nonce(),
ts in any::<i64>(),
) {
let a = transfer_intent_bytes(&sender, &recipient, size, &mime, &filename, &nonce, ts).unwrap();
let b = transfer_intent_bytes(&sender, &recipient, size, &mime, &filename, &nonce, ts).unwrap();
prop_assert_eq!(a, b);
}
#[test]
fn transfer_receipt_rejects_unknown_action(
recipient in arb_ascii_token(5, 64),
transfer in arb_ascii_token(3, 64),
action in "[a-zA-Z]{3,10}".prop_filter(
"skip valid actions",
|s| !["download", "ack", "inbox", "request_ticket"].contains(&s.as_str()),
),
nonce in arb_nonce(),
ts in any::<i64>(),
) {
let result = transfer_receipt_bytes(&recipient, &transfer, &action, &nonce, ts);
prop_assert!(result.is_err());
}
}