use keyhog_scanner::testing::unicode_hardening::{
contains_evasion, detect_unicode_attacks, full_normalize, normalize_homoglyphs, EvasionKind,
};
use std::borrow::Cow;
fn assert_mark_stripped_from_token(mark: char, label: &str) {
let text = format!("ghp_ab{mark}cd");
let normalized = normalize_homoglyphs(&text);
assert!(
normalized.contains("ghp_abcd"),
"{label} (U+{:04X}) must be stripped so the token reassembles; got {normalized:?}",
mark as u32
);
assert!(
!normalized.contains(mark),
"{label} (U+{:04X}) must not survive normalization; got {normalized:?}",
mark as u32
);
}
#[test]
fn original_diacritical_block_still_stripped() {
assert_mark_stripped_from_token('\u{0300}', "combining grave (orig block)");
assert_mark_stripped_from_token('\u{036F}', "orig block end");
}
#[test]
fn diacritical_extended_block_stripped() {
assert_mark_stripped_from_token('\u{1AB0}', "Combining Diacritical Marks Extended");
}
#[test]
fn diacritical_supplement_block_stripped() {
assert_mark_stripped_from_token('\u{1DC0}', "Combining Diacritical Marks Supplement");
}
#[test]
fn for_symbols_block_stripped() {
assert_mark_stripped_from_token('\u{20D0}', "Combining Marks for Symbols");
}
#[test]
fn enclosing_mark_me_category_stripped() {
assert_mark_stripped_from_token('\u{20DD}', "combining enclosing circle (Me)");
}
#[test]
fn half_marks_block_stripped() {
assert_mark_stripped_from_token('\u{FE20}', "Combining Half Marks");
}
#[test]
fn cyrillic_combining_titlo_stripped() {
assert_mark_stripped_from_token('\u{0483}', "combining Cyrillic titlo");
}
#[test]
fn cyrillic_millions_me_category_stripped() {
assert_mark_stripped_from_token('\u{0489}', "combining Cyrillic millions (Me)");
}
#[test]
fn hebrew_combining_accent_stripped() {
assert_mark_stripped_from_token('\u{0591}', "Hebrew accent etnahta");
}
#[test]
fn arabic_combining_fathatan_stripped() {
assert_mark_stripped_from_token('\u{064B}', "Arabic fathatan");
}
#[test]
fn devanagari_combining_udatta_stripped() {
assert_mark_stripped_from_token('\u{0951}', "Devanagari stress sign udatta");
}
#[test]
fn extended_mark_between_prefix_chars_reassembles_ghp() {
let normalized = normalize_homoglyphs("g\u{1DC0}hp_deadbeefcafe");
assert!(
normalized.starts_with("ghp_"),
"prefix-interior combining mark must be stripped; got {normalized:?}"
);
}
#[test]
fn spliced_mark_lets_aws_key_reassemble() {
let normalized = normalize_homoglyphs("AKIA\u{20DD}QYLPMN5HFIQR7BBB");
assert!(
normalized.contains("AKIAQYLPMN5HFIQR7BBB"),
"combining mark after AKIA must be stripped; got {normalized:?}"
);
}
#[test]
fn marks_from_multiple_blocks_in_one_token_all_stripped() {
let normalized = normalize_homoglyphs("g\u{1DC0}h\u{20DD}p\u{FE20}_secret");
assert_eq!(
normalized.as_ref(),
"ghp_secret",
"marks from three different blocks must all be stripped; got {normalized:?}"
);
}
#[test]
fn leading_combining_mark_does_not_panic_and_is_dropped() {
let normalized = normalize_homoglyphs("\u{1DC0}ghp_token");
assert_eq!(normalized.as_ref(), "ghp_token");
}
#[test]
fn string_of_only_combining_marks_normalizes_to_empty() {
let normalized = normalize_homoglyphs("\u{1DC0}\u{20DD}\u{FE20}");
assert_eq!(normalized.as_ref(), "");
}
#[test]
fn trailing_extended_mark_dropped() {
let normalized = normalize_homoglyphs("ghp_token\u{1AB0}");
assert_eq!(normalized.as_ref(), "ghp_token");
}
#[test]
fn full_normalize_strips_supplement_mark_nfc_cannot_compose() {
let normalized = full_normalize("ghp_g\u{1DC0}h");
assert!(
normalized.contains("ghp_gh") && !normalized.contains('\u{1DC0}'),
"full_normalize must strip the supplement mark NFC leaves behind; got {normalized:?}"
);
}
#[test]
fn detect_flags_supplement_mark_as_decomposed() {
let attacks = detect_unicode_attacks("ghp_a\u{1DC0}b");
assert!(
attacks
.iter()
.any(|a| a.kind == EvasionKind::Decomposed && a.char == '\u{1DC0}'),
"supplement combining mark must be reported as Decomposed evasion; got {attacks:?}"
);
}
#[test]
fn detect_flags_half_mark_as_decomposed() {
let attacks = detect_unicode_attacks("ghp_a\u{FE20}b");
assert!(
attacks.iter().any(|a| a.kind == EvasionKind::Decomposed),
"half-mark must be reported as evasion; got {attacks:?}"
);
}
#[test]
fn contains_evasion_true_for_extended_supplement_mark() {
assert!(contains_evasion("ghp_a\u{1DC0}b"));
assert!(contains_evasion("ghp_a\u{FE20}b"));
}
#[test]
fn precomposed_letter_n_tilde_is_kept() {
let normalized = normalize_homoglyphs("ma\u{00F1}ana_token");
assert!(
normalized.contains('\u{00F1}'),
"precomposed ñ (a letter) must not be stripped; got {normalized:?}"
);
}
#[test]
fn cjk_ideograph_is_kept() {
let normalized = normalize_homoglyphs("token_\u{4E00}_value");
assert!(
normalized.contains('\u{4E00}'),
"CJK ideograph must be kept; got {normalized:?}"
);
}
#[test]
fn pure_ascii_stays_borrowed_and_identical() {
let normalized = normalize_homoglyphs("ghp_abcdef0123456789");
assert!(
matches!(normalized, Cow::Borrowed(_)),
"pure-ASCII must not allocate"
);
assert_eq!(normalized.as_ref(), "ghp_abcdef0123456789");
}
#[test]
fn ascii_credential_with_no_marks_is_unchanged() {
let normalized = normalize_homoglyphs("AKIAQYLPMN5HFIQR7BBB");
assert_eq!(normalized.as_ref(), "AKIAQYLPMN5HFIQR7BBB");
}
#[test]
fn precomposed_accent_letter_not_flagged_as_evasion() {
assert!(!contains_evasion("cafe_\u{00E9}_token"));
}