use std::error::Error as _;
use rocketmq_error::fields;
use rocketmq_error::Error;
use rocketmq_error::ErrorContext;
use rocketmq_error::FieldValueRef;
use rocketmq_error::RecoveryHint;
use rocketmq_error::Sensitive;
use rocketmq_error::AUTH_OPERATION_FAILED;
use rocketmq_error::CORE_INTERNAL_FAILURE;
use rocketmq_error::OBSERVABILITY_INITIALIZATION_FAILED;
use rocketmq_error::OBSERVABILITY_LOG_FILTER_INVALID;
use rocketmq_error::PROTOCOL_BODY_INVALID;
use rocketmq_error::PROTOCOL_HEADER_INVALID;
use rocketmq_error::ROUTE_TOPIC_NOT_FOUND;
#[test]
fn sensitive_display_and_debug_are_redacted() {
let secret = Sensitive::new("secret-value");
assert_eq!(secret.expose_secret(), &"secret-value");
assert_eq!(secret.to_string(), "<redacted>");
assert!(!format!("{secret:?}").contains("secret-value"));
}
#[test]
fn error_context_redacts_sensitive_fields() {
let context = ErrorContext::new()
.with_text(fields::TOPIC, "TopicA")
.with_secret_presence(fields::CREDENTIALS_PRESENT)
.with_secret_presence(fields::SOURCE_PRESENT);
let public = context.public_fields().collect::<Vec<_>>();
assert_eq!(public.len(), 1);
assert_eq!(public[0].value(), FieldValueRef::Text("TopicA"));
assert_eq!(
context.to_string(),
"topic=TopicA, credentials_present=<redacted>, source_present=<redacted>"
);
}
#[test]
fn canonical_error_exposes_only_catalog_message_and_safe_context() {
let route = Error::new(&ROUTE_TOPIC_NOT_FOUND).with_context(ErrorContext::new().with_text(fields::TOPIC, "TopicA"));
assert_eq!(
route.public_view().expect("valid view").message(),
"Topic route was not found"
);
let internal = Error::caused_by(&CORE_INTERNAL_FAILURE, std::io::Error::other("password=plain-text")).with_context(
ErrorContext::new()
.with_text(fields::OPERATION_DIAGNOSTIC, "run internal operation")
.with_secret_presence(fields::SOURCE_PRESENT),
);
let view = internal.public_view().expect("valid view");
assert_eq!(view.message(), "Internal error");
assert_eq!(view.fields().count(), 0);
assert!(!format!("{view:?}").contains("plain-text"));
assert_eq!(internal.recovery_hint(), RecoveryHint::OperatorAction);
}
#[test]
fn observability_context_never_contains_sensitive_details() {
let init = Error::new(&OBSERVABILITY_INITIALIZATION_FAILED).with_context(
ErrorContext::new()
.with_text(fields::OBSERVABILITY_SIGNAL, "metrics")
.with_secret_presence(fields::REASON_PRESENT),
);
let filter = Error::new(&OBSERVABILITY_LOG_FILTER_INVALID).with_context(
ErrorContext::new()
.with_secret_presence(fields::FILTER_PRESENT)
.with_secret_presence(fields::ERROR_PRESENT),
);
for error in [init, filter] {
assert!(error.context().public_fields().next().is_none());
assert!(!error.context().to_string().contains("secret"));
}
}
#[test]
fn request_boundary_errors_preserve_typed_source_chains() {
let cases = [
(&PROTOCOL_BODY_INVALID, "decode checkpoint", "body-secret"),
(&PROTOCOL_HEADER_INVALID, "decode maintenance header", "header-secret"),
(&AUTH_OPERATION_FAILED, "authorize checkpoint", "auth-secret"),
];
for (descriptor, operation, secret) in cases {
let error = Error::caused_by(descriptor, std::io::Error::other(secret)).with_context(
ErrorContext::new()
.with_text(fields::OPERATION_DIAGNOSTIC, operation)
.with_secret_presence(fields::SOURCE_PRESENT),
);
assert!(error
.source()
.and_then(|source| source.downcast_ref::<std::io::Error>())
.is_some());
assert!(!format!("{:?}", error.public_view().expect("valid view")).contains(secret));
}
}