use std::collections::HashMap;
use qubit_redact::MaskPolicy;
use qubit_redact::Redact;
use qubit_redact::RedactionCompletion;
use qubit_redact::RedactionPolicy;
use qubit_redact::Redactor;
use qubit_redact::Sensitivity;
use qubit_value::MultiValues;
use qubit_value::NamedMultiValues;
use qubit_value::NamedValue;
use qubit_value::Value;
use qubit_value::ValueContainer;
use serde_json::json;
fn redacted_text<T: Redact>(value: &T, policy: &RedactionPolicy) -> String {
Redactor::new(policy.clone())
.redact_text(value)
.into_complete_text()
.expect("test output must be complete")
.into_string()
}
fn sensitive_policy_with_nodes(field: &str, max_nodes: usize) -> RedactionPolicy {
RedactionPolicy::builder()
.fields(|fields| {
fields.raise(field, Sensitivity::Secret);
})
.expect("the test field rule should be valid")
.limits(|limits_builder| {
limits_builder
.max_nodes(max_nodes)
.max_collection_items(1024)
.max_depth(32);
})
.expect("the domain limits should be valid")
.build()
.expect("policy should build")
}
#[test]
fn test_value_redacted_view_masks_sensitive_string_map_entries() {
let value = Value::StringMap(HashMap::from([
("api_key".to_owned(), "raw-secret".to_owned()),
("label".to_owned(), "visible".to_owned()),
]));
let policy = RedactionPolicy::builder()
.fields(|fields| {
fields.raise("api_key", Sensitivity::Secret);
})
.expect("the test builder input should be valid")
.build()
.expect("policy should build");
let output = redacted_text(&value, &policy);
assert!(!output.contains("raw-secret"));
assert!(output.contains("visible"));
}
#[test]
fn test_multi_values_json_recursively_masks_sensitive_entries() {
let values = MultiValues::Json(vec![json!({
"profile": {
"password": "raw-json-secret",
"attempts": 3,
},
"label": "visible",
})]);
let policy = RedactionPolicy::builder()
.fields(|fields| {
fields.raise("password", Sensitivity::Secret);
})
.expect("the test field rule should be valid")
.build()
.expect("policy should build");
let output = redacted_text(&values, &policy);
assert!(!output.contains("raw-json-secret"), "{output}");
assert!(output.contains("visible"), "{output}");
}
#[test]
fn test_multi_values_json_masks_sensitive_non_string_entries() {
let values = MultiValues::Json(vec![
json!({"password": 12345}),
json!({"password": true, "label": "visible"}),
]);
let policy = RedactionPolicy::builder()
.fields(|fields| {
fields.raise("password", Sensitivity::Secret);
})
.expect("the test field rule should be valid")
.build()
.expect("policy should build");
let output = redacted_text(&values, &policy);
assert!(!output.contains("12345"), "{output}");
assert!(!output.contains("true"), "{output}");
assert!(output.contains("visible"), "{output}");
}
#[test]
fn test_multi_values_empty_json_collection_is_preserved() {
let values = MultiValues::Json(Vec::new());
let output = redacted_text(&values, &RedactionPolicy::standard());
assert_eq!(output, "[]");
}
#[test]
fn test_multi_values_json_stops_before_unadmitted_items() {
let values = MultiValues::Json(vec![json!("first"), json!("second")]);
let policy = RedactionPolicy::builder()
.limits(|limits| {
limits.max_nodes(64).max_collection_items(1).max_depth(8);
})
.expect("the test limits should be valid")
.build()
.expect("policy should build");
let result = Redactor::new(policy).redact_text(&values);
let output = result.text().as_str();
assert_eq!(result.summary().completion(), RedactionCompletion::Truncated);
assert!(output.contains("first"), "{output}");
assert!(!output.contains("second"), "{output}");
assert!(output.contains("<truncated>"), "{output}");
}
#[test]
fn test_value_redacted_view_preserves_scalar_without_key_context() {
let value = Value::String("visible-without-key".to_owned());
let policy = RedactionPolicy::builder()
.fields(|fields| {
fields.raise("password", Sensitivity::Secret);
})
.expect("the test builder input should be valid")
.build()
.expect("policy should build");
let output = redacted_text(&value, &policy);
assert!(output.contains("visible-without-key"));
}
#[test]
fn test_named_value_masks_non_strings_with_configured_opaque_value() {
let value = NamedValue::new("token", Value::Int32(12345));
let policy = RedactionPolicy::builder()
.fields(|fields| {
fields
.disable_floor()
.raise("token", Sensitivity::Low)
.mask(Sensitivity::Low, MaskPolicy::preserve_edges(1, 1, "OPAQUE", 0));
})
.expect("the test policy should be valid")
.build()
.expect("policy should build");
assert!(redacted_text(&value, &policy).contains("OPAQUE"));
}
#[test]
fn test_named_value_redaction_uses_text_masking_for_sensitive_strings() {
let value = NamedValue::new("token", Value::String("secret-token".to_owned()));
let policy = RedactionPolicy::builder()
.fields(|fields| {
fields
.disable_floor()
.raise("token", Sensitivity::Low)
.mask(Sensitivity::Low, MaskPolicy::preserve_edges(1, 1, "MASK", 0));
})
.expect("the test policy should be valid")
.build()
.expect("policy should build");
let output = redacted_text(&value, &policy);
assert!(!output.contains("secret-token"));
assert!(!output.contains("secret-token"));
}
#[test]
fn test_named_multi_values_redaction_masks_sensitive_collections_as_opaque() {
let value = NamedMultiValues::new(
"tokens",
MultiValues::String(vec!["first-secret".to_owned(), "second-secret".to_owned()]),
);
let policy = RedactionPolicy::builder()
.fields(|fields| {
fields
.raise("tokens", Sensitivity::Low)
.mask(Sensitivity::Low, MaskPolicy::preserve_edges(1, 1, "OPAQUE", 0));
})
.expect("the test policy should be valid")
.build()
.expect("policy should build");
let output = redacted_text(&value, &policy);
assert!(!output.contains("first-secret"));
assert!(!output.contains("second-secret"));
assert!(output.contains("OPAQUE"));
}
#[test]
fn test_named_value_exact_wrapper_node_budget_is_complete() {
let value = NamedValue::new("token", Value::String("secret-token".to_owned()));
let policy = sensitive_policy_with_nodes("token", 5);
let output = redacted_text(&value, &policy);
assert!(!output.contains("secret-token"), "{output}");
assert!(!output.contains("<truncated>"), "{output}");
}
#[test]
fn test_named_value_one_less_wrapper_node_truncates() {
let value = NamedValue::new("token", Value::String("secret-token".to_owned()));
let policy = sensitive_policy_with_nodes("token", 4);
let output = redacted_text(&value, &policy);
assert!(!output.contains("secret-token"), "{output}");
assert!(!output.contains("secret-token"), "{output}");
}
#[test]
fn test_named_multi_values_exact_wrapper_node_budget_is_complete() {
let value = NamedMultiValues::new("tokens", MultiValues::String(vec!["first-secret".to_owned()]));
let policy = sensitive_policy_with_nodes("tokens", 5);
let output = redacted_text(&value, &policy);
assert!(!output.contains("first-secret"), "{output}");
assert!(!output.contains("<truncated>"), "{output}");
}
#[test]
fn test_named_values_classify_payload_by_business_name() {
let redactor = Redactor::standard();
let scalar = NamedValue::new("password", Value::String("raw-secret".to_owned()));
let collection = NamedMultiValues::new("password", MultiValues::String(vec!["raw-secret".to_owned()]));
for inspection in [redactor.inspect(&scalar), redactor.inspect(&collection)] {
assert_eq!(
inspection.expect("complete inspection").max_sensitivity(),
Some(Sensitivity::Secret)
);
}
assert!(
redactor
.redact_text(&scalar)
.text()
.as_str()
.contains("value: \"<redacted>\"")
);
assert!(
redactor
.redact_text(&collection)
.text()
.as_str()
.contains("value: \"<redacted>\"")
);
}
#[test]
fn test_named_values_preserve_public_payloads() {
let redactor = Redactor::standard();
let scalar = NamedValue::new("region", Value::String("eu-west".to_owned()));
let collection = NamedMultiValues::new("region", MultiValues::String(vec!["eu-west".to_owned()]));
assert!(redactor.redact_text(&scalar).text().as_str().contains("eu-west"));
assert!(redactor.redact_text(&collection).text().as_str().contains("eu-west"));
assert_eq!(redactor.inspect(&scalar).expect("inspection").max_sensitivity(), None);
assert_eq!(
redactor.inspect(&collection).expect("inspection").max_sensitivity(),
None
);
}
#[test]
fn test_named_value_recursively_redacts_json_under_public_business_name() {
let value = NamedValue::new(
"region",
Value::Json(json!({"password": "nested-secret", "label": "visible"})),
);
let output = Redactor::standard().redact_text(&value);
assert!(!output.text().as_str().contains("nested-secret"));
assert!(output.text().as_str().contains("visible"));
assert_eq!(
Redactor::standard()
.inspect(&value)
.expect("complete inspection")
.max_sensitivity(),
Some(Sensitivity::Secret)
);
}
#[test]
fn test_named_values_disabled_policy_restores_payloads() {
let redactor = Redactor::new(RedactionPolicy::disabled());
let scalar = NamedValue::new("password", Value::String("raw-secret".to_owned()));
let collection = NamedMultiValues::new("password", MultiValues::String(vec!["raw-secret".to_owned()]));
assert!(redactor.redact_text(&scalar).text().as_str().contains("raw-secret"));
assert!(redactor.redact_text(&collection).text().as_str().contains("raw-secret"));
assert_eq!(redactor.inspect(&scalar).expect("inspection").max_sensitivity(), None);
assert_eq!(
redactor.inspect(&collection).expect("inspection").max_sensitivity(),
None
);
}
#[test]
fn test_string_map_collection_redacts_keys_and_preserves_public_values() {
let map = HashMap::from([
("password".to_owned(), "collection-secret".to_owned()),
("region".to_owned(), "eu-west".to_owned()),
]);
let value = ValueContainer::Collection(MultiValues::StringMap(vec![map]));
let redactor = Redactor::standard();
let output = redactor.redact_text(&value);
assert!(!output.text().as_str().contains("collection-secret"));
assert!(output.text().as_str().contains("eu-west"));
assert_eq!(
redactor.inspect(&value).expect("complete inspection").max_sensitivity(),
Some(Sensitivity::Secret)
);
let disabled = Redactor::new(RedactionPolicy::disabled()).redact_text(&value);
assert!(disabled.text().as_str().contains("collection-secret"));
}
#[test]
fn test_string_map_collection_stops_before_unadmitted_maps() {
let values = MultiValues::StringMap(vec![
HashMap::new(),
HashMap::from([("region".to_owned(), "must-not-be-visited".to_owned())]),
]);
let policy = RedactionPolicy::builder()
.limits(|limits| {
limits.max_collection_items(1);
})
.expect("valid limits")
.build()
.expect("valid policy");
let output = Redactor::new(policy).redact_text(&values);
assert_eq!(output.summary().completion(), RedactionCompletion::Truncated);
assert!(!output.text().as_str().contains("must-not-be-visited"));
}