mod classification_mutation {
use provide_telemetry::{
classify_key, clear_classification_rules, register_classification_rule,
ClassificationPolicy, ClassificationRule, DataClass,
};
use std::sync::{mpsc, Mutex, OnceLock};
use std::time::Duration;
static CLS_LOCK: OnceLock<Mutex<()>> = OnceLock::new();
fn cls_lock() -> &'static Mutex<()> {
CLS_LOCK.get_or_init(|| Mutex::new(()))
}
#[test]
fn glob_trailing_wildcard_matches_prefix() {
let _guard = cls_lock().lock().expect("cls lock");
clear_classification_rules();
register_classification_rule(ClassificationRule::new("email*", DataClass::Pii));
assert_eq!(
classify_key("email_address").as_deref(),
Some("PII"),
"trailing wildcard must match key that starts with the prefix"
);
}
#[test]
fn glob_trailing_wildcard_does_not_match_suffix_only() {
let _guard = cls_lock().lock().expect("cls lock");
clear_classification_rules();
register_classification_rule(ClassificationRule::new("email*", DataClass::Pii));
assert_eq!(
classify_key("address_email"),
None,
"trailing wildcard must not match key that has prefix only as suffix"
);
}
#[test]
fn glob_trailing_wildcard_matches_exact_prefix() {
let _guard = cls_lock().lock().expect("cls lock");
clear_classification_rules();
register_classification_rule(ClassificationRule::new("user*", DataClass::Internal));
assert_eq!(
classify_key("user").as_deref(),
Some("INTERNAL"),
"trailing wildcard must match a key that equals the prefix exactly"
);
assert_eq!(
classify_key("user_id").as_deref(),
Some("INTERNAL"),
"trailing wildcard must match key with additional chars after prefix"
);
}
#[test]
fn glob_internal_wildcard_matches_middle_segment() {
let _guard = cls_lock().lock().expect("cls lock");
clear_classification_rules();
register_classification_rule(ClassificationRule::new("user*_id", DataClass::Pii));
assert_eq!(
classify_key("user_id").as_deref(),
Some("PII"),
"mid-pattern wildcard must match with zero chars consumed"
);
assert_eq!(
classify_key("user_123_id").as_deref(),
Some("PII"),
"mid-pattern wildcard must match with non-empty run of chars"
);
assert_eq!(
classify_key("user_id_extra"),
None,
"mid-pattern wildcard must not match when suffix does not align"
);
}
#[test]
fn glob_leading_wildcard_matches_suffix() {
let _guard = cls_lock().lock().expect("cls lock");
clear_classification_rules();
register_classification_rule(ClassificationRule::new("*_id", DataClass::Internal));
assert_eq!(
classify_key("user_id").as_deref(),
Some("INTERNAL"),
"leading wildcard must match key ending with suffix"
);
assert_eq!(
classify_key("order_123_id").as_deref(),
Some("INTERNAL"),
"leading wildcard must match key with arbitrary prefix"
);
assert_eq!(
classify_key("user_name"),
None,
"leading wildcard must not match key without required suffix"
);
}
#[test]
fn glob_multiple_wildcards_match_complex_patterns() {
let _guard = cls_lock().lock().expect("cls lock");
clear_classification_rules();
register_classification_rule(ClassificationRule::new("user*id*", DataClass::Pii));
assert_eq!(
classify_key("user_id").as_deref(),
Some("PII"),
"multiple wildcards must match key with both segments present"
);
assert_eq!(
classify_key("user_foo_id_bar").as_deref(),
Some("PII"),
"multiple wildcards must match key with extra chars in both positions"
);
assert_eq!(
classify_key("userid").as_deref(),
Some("PII"),
"multiple wildcards must match when wildcard spans zero chars"
);
}
#[test]
fn glob_consecutive_wildcards_collapse_like_single_wildcard() {
let _guard = cls_lock().lock().expect("cls lock");
clear_classification_rules();
register_classification_rule(ClassificationRule::new("user**id", DataClass::Pii));
assert_eq!(
classify_key("userid").as_deref(),
Some("PII"),
"consecutive wildcards must still match zero consumed chars"
);
assert_eq!(
classify_key("user_42id").as_deref(),
Some("PII"),
"consecutive wildcards must still match non-empty runs"
);
assert_eq!(
classify_key("user_42name"),
None,
"consecutive wildcards must still honor the literal suffix"
);
}
#[test]
fn glob_consecutive_wildcards_returns_promptly() {
let _guard = cls_lock().lock().expect("cls lock");
clear_classification_rules();
register_classification_rule(ClassificationRule::new("user**id", DataClass::Pii));
let (tx, rx) = mpsc::channel();
std::thread::spawn(move || {
let result = classify_key("userid");
let _ = tx.send(result);
});
let result = rx
.recv_timeout(Duration::from_millis(250))
.expect("consecutive-wildcard matcher must complete promptly");
assert_eq!(
result.as_deref(),
Some("PII"),
"consecutive wildcards must not hang the matcher"
);
}
#[test]
fn exact_pattern_does_not_match_different_key() {
let _guard = cls_lock().lock().expect("cls lock");
clear_classification_rules();
register_classification_rule(ClassificationRule::new("password", DataClass::Secret));
assert_eq!(
classify_key("passwords"),
None,
"exact pattern must not match a key with extra trailing characters"
);
assert_eq!(
classify_key("my_password"),
None,
"exact pattern must not match a key with extra leading characters"
);
}
#[test]
fn exact_pattern_matches_identical_key() {
let _guard = cls_lock().lock().expect("cls lock");
clear_classification_rules();
register_classification_rule(ClassificationRule::new("ssn", DataClass::Pii));
assert_eq!(
classify_key("ssn").as_deref(),
Some("PII"),
"exact pattern must match the identical key"
);
}
#[test]
fn classify_key_returns_first_matching_rule() {
let _guard = cls_lock().lock().expect("cls lock");
clear_classification_rules();
register_classification_rule(ClassificationRule::new("email*", DataClass::Pii));
register_classification_rule(ClassificationRule::new("email*", DataClass::Phi));
assert_eq!(
classify_key("email_address").as_deref(),
Some("PII"),
"first registered matching rule wins"
);
}
#[test]
fn data_class_as_str_all_variants() {
assert_eq!(DataClass::Public.as_str(), "PUBLIC");
assert_eq!(DataClass::Internal.as_str(), "INTERNAL");
assert_eq!(DataClass::Pii.as_str(), "PII");
assert_eq!(DataClass::Phi.as_str(), "PHI");
assert_eq!(DataClass::Pci.as_str(), "PCI");
assert_eq!(DataClass::Secret.as_str(), "SECRET"); }
#[test]
fn classify_key_uses_exact_labels_for_all_data_classes() {
let _guard = cls_lock().lock().expect("cls lock");
let cases = [
("public_field", DataClass::Public, "PUBLIC"),
("internal_field", DataClass::Internal, "INTERNAL"),
("pii_field", DataClass::Pii, "PII"),
("phi_field", DataClass::Phi, "PHI"),
("pci_field", DataClass::Pci, "PCI"),
("secret_field", DataClass::Secret, "SECRET"), ];
for (key, classification, expected) in cases {
clear_classification_rules();
register_classification_rule(ClassificationRule::new(key, classification));
assert_eq!(
classify_key(key).as_deref(),
Some(expected),
"classify_key must preserve the canonical DataClass label"
);
}
clear_classification_rules();
}
#[test]
fn classification_policy_lookup_action_treats_public_as_distinct_from_unknown() {
let policy = ClassificationPolicy {
public: "hash".to_string(),
internal: "drop".to_string(),
pii: "redact".to_string(),
phi: "drop".to_string(),
pci: "hash".to_string(),
secret: "drop".to_string(), };
assert_eq!(policy.lookup_action("PUBLIC"), "hash");
assert_eq!(policy.lookup_action("UNKNOWN"), "pass");
}
#[test]
fn classify_key_returns_none_with_no_rules() {
let _guard = cls_lock().lock().expect("cls lock");
clear_classification_rules();
assert_eq!(classify_key("anything"), None);
}
}