use std::collections::HashSet;
use std::time::Duration;
use keyhog_verifier::oob::{redact_interactsh_error, InteractshError};
#[test]
fn keygen_error_redacts_to_exact_message() {
let err = InteractshError::KeyGen("rng exhausted".to_string());
assert_eq!(
redact_interactsh_error(&err),
"interactsh keypair generation failed: rng exhausted"
);
}
#[test]
fn keyencode_error_redacts_to_exact_message() {
let err = InteractshError::KeyEncode("der write failed".to_string());
assert_eq!(
redact_interactsh_error(&err),
"interactsh public-key encoding failed: der write failed"
);
}
#[test]
fn register_http_status_failure_redacts_to_exact_message() {
let err = InteractshError::Register {
status: 401,
body: "unauthorized".to_string(),
};
assert_eq!(
redact_interactsh_error(&err),
"interactsh register failed (HTTP 401): unauthorized"
);
}
#[test]
fn deregister_http_status_failure_redacts_to_exact_message() {
let err = InteractshError::Deregister {
status: 500,
body: "cleanup rejected".to_string(),
};
assert_eq!(
redact_interactsh_error(&err),
"interactsh deregister failed (HTTP 500): cleanup rejected"
);
}
#[test]
fn poll_http_status_failure_redacts_to_exact_message() {
let err = InteractshError::Poll {
status: 404,
body: "not found".to_string(),
};
assert_eq!(
redact_interactsh_error(&err),
"interactsh poll failed (HTTP 404): not found"
);
}
#[test]
fn bad_response_error_redacts_to_exact_message() {
let err = InteractshError::BadResponse("data present but aes_key missing".to_string());
assert_eq!(
redact_interactsh_error(&err),
"interactsh response shape unexpected: data present but aes_key missing"
);
}
#[test]
fn blocked_collector_error_redacts_to_exact_message() {
let err = InteractshError::BlockedCollector("169.254.169.254".to_string());
assert_eq!(
redact_interactsh_error(&err),
"interactsh collector host blocked by SSRF guard: 169.254.169.254"
);
}
#[test]
fn aes_unwrap_error_redacts_to_exact_message() {
let err = InteractshError::AesUnwrap("rsa-oaep: decryption error".to_string());
assert_eq!(
redact_interactsh_error(&err),
"interactsh AES key unwrap failed: rsa-oaep: decryption error"
);
}
#[test]
fn decrypt_error_redacts_to_exact_message() {
let err = InteractshError::Decrypt("cfb init: bad iv length".to_string());
assert_eq!(
redact_interactsh_error(&err),
"interactsh interaction decrypt failed: cfb init: bad iv length"
);
}
#[test]
fn timeout_error_renders_subsecond_duration_exactly() {
let err = InteractshError::Timeout(Duration::from_millis(1500));
assert_eq!(
redact_interactsh_error(&err),
"interactsh request timed out after 1.5s"
);
}
#[test]
fn timeout_error_renders_millisecond_and_zero_boundaries_exactly() {
let millis = InteractshError::Timeout(Duration::from_millis(250));
assert_eq!(
redact_interactsh_error(&millis),
"interactsh request timed out after 250ms"
);
let zero = InteractshError::Timeout(Duration::ZERO);
assert_eq!(
redact_interactsh_error(&zero),
"interactsh request timed out after 0ns"
);
}
#[test]
fn register_deregister_poll_stay_phase_distinct_at_same_status_and_body() {
let register = redact_interactsh_error(&InteractshError::Register {
status: 503,
body: "same".to_string(),
});
let deregister = redact_interactsh_error(&InteractshError::Deregister {
status: 503,
body: "same".to_string(),
});
let poll = redact_interactsh_error(&InteractshError::Poll {
status: 503,
body: "same".to_string(),
});
assert_eq!(register, "interactsh register failed (HTTP 503): same");
assert_eq!(deregister, "interactsh deregister failed (HTTP 503): same");
assert_eq!(poll, "interactsh poll failed (HTTP 503): same");
let set: HashSet<&String> = [®ister, &deregister, &poll].into_iter().collect();
assert_eq!(set.len(), 3, "phase word must keep the three distinct");
}
#[test]
fn all_ten_non_transport_variants_redact_pairwise_distinct() {
let reasons = [
redact_interactsh_error(&InteractshError::KeyGen("x".to_string())),
redact_interactsh_error(&InteractshError::KeyEncode("x".to_string())),
redact_interactsh_error(&InteractshError::Register {
status: 400,
body: "x".to_string(),
}),
redact_interactsh_error(&InteractshError::Deregister {
status: 400,
body: "x".to_string(),
}),
redact_interactsh_error(&InteractshError::Poll {
status: 400,
body: "x".to_string(),
}),
redact_interactsh_error(&InteractshError::BadResponse("x".to_string())),
redact_interactsh_error(&InteractshError::BlockedCollector("x".to_string())),
redact_interactsh_error(&InteractshError::AesUnwrap("x".to_string())),
redact_interactsh_error(&InteractshError::Decrypt("x".to_string())),
redact_interactsh_error(&InteractshError::Timeout(Duration::from_secs(1))),
];
let unique: HashSet<&String> = reasons.iter().collect();
assert_eq!(
unique.len(),
10,
"all 10 non-transport error classifications must be pairwise distinct"
);
}
#[test]
fn redaction_is_transport_scoped_non_transport_payload_passes_through_verbatim() {
let payload = "https://oast.fun/poll?id=abc&secret=deadbeef";
let err = InteractshError::BlockedCollector(payload.to_string());
assert_eq!(
redact_interactsh_error(&err),
format!("interactsh collector host blocked by SSRF guard: {payload}"),
);
}
#[test]
fn error_body_with_format_tokens_and_newlines_is_preserved_verbatim() {
let hostile = "{}{0}{status}\nline2\t{{escaped}}";
let err = InteractshError::Poll {
status: 418,
body: hostile.to_string(),
};
assert_eq!(
redact_interactsh_error(&err),
format!("interactsh poll failed (HTTP 418): {hostile}"),
);
}
#[test]
fn status_and_body_numeric_boundaries_render_exactly() {
let zero = InteractshError::Register {
status: 0,
body: String::new(),
};
assert_eq!(
redact_interactsh_error(&zero),
"interactsh register failed (HTTP 0): "
);
let max = InteractshError::Deregister {
status: u16::MAX,
body: "edge".to_string(),
};
assert_eq!(
redact_interactsh_error(&max),
"interactsh deregister failed (HTTP 65535): edge"
);
}