use rust_xmpp::logging::{Level, Logger};
use serde_json::json;
#[test]
fn filters_appends_escapes_and_redacts() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("events.log");
std::fs::write(&path, "existing\n").unwrap();
let logger = Logger::from_config(Some(&json!({"info":path.to_str().unwrap()}))).unwrap();
logger.event(Level::Debug, "test", None, "hidden", &[]);
logger.event(
Level::Info,
"test\nmodule",
Some("c1"),
"connected",
&[
("password", "supersecret"),
("access_token", "token-secret"),
("raw_xml", "secret"),
("peer", "a\n\r\u{1b}\"b"),
],
);
let text = std::fs::read_to_string(&path).unwrap();
assert!(text.starts_with("existing\n"));
assert_eq!(text.lines().count(), 2);
assert!(!text.contains("hidden"));
assert!(!text.contains("supersecret"));
assert!(!text.contains("token-secret"));
assert!(!text.contains(""));
assert!(text.contains("[REDACTED]"));
assert!(text.contains("test\\nmodule"));
assert!(text.contains("\\u001b"));
assert!(text.contains("level=info"));
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
assert_eq!(
std::fs::metadata(path).unwrap().permissions().mode() & 0o777,
0o600
);
}
}
#[test]
fn json_records_and_bounded_ranges() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("debug.log");
let config = json!([{"levels":{"min":"debug","max":"info"},"to":"file","filename":path}]);
let logger = Logger::from_config_with_format(Some(&config), Some("json")).unwrap();
logger.event(Level::Error, "net", None, "excluded", &[]);
logger.event(
Level::Debug,
"net",
Some("42"),
"accepted",
&[("client_proof", "secret"), ("peer", "localhost")],
);
let text = std::fs::read_to_string(path).unwrap();
let record: serde_json::Value = serde_json::from_str(text.trim()).unwrap();
assert_eq!(record["level"], "debug");
assert_eq!(record["connection"], "42");
assert_eq!(record["fields"][0]["value"], "[REDACTED]");
let timestamp = record["timestamp"].as_str().unwrap();
assert!(timestamp.ends_with('Z'));
assert_eq!(timestamp.len(), 24);
chrono::DateTime::parse_from_rfc3339(timestamp).unwrap();
}
#[test]
fn cloned_loggers_serialize_records() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("threads.log");
let logger = Logger::from_config(Some(&json!({"debug":path}))).unwrap();
let handles: Vec<_> = (0..4)
.map(|_| {
let logger = logger.clone();
std::thread::spawn(move || {
for _ in 0..40 {
logger.event(
Level::Info,
"worker",
None,
"complete",
&[("text", "x".repeat(4096).as_str())],
);
}
})
})
.collect();
for handle in handles {
handle.join().unwrap();
}
let text = std::fs::read_to_string(path).unwrap();
assert_eq!(text.lines().count(), 160);
assert!(
text.lines()
.all(|line| line.starts_with("timestamp=") && line.ends_with('"'))
);
}
#[test]
fn rejects_malformed_configuration() {
for config in [
json!(42),
json!({"bogus":"*console"}),
json!({"info":42}),
json!("*unknown"),
json!([{"to":"file"}]),
json!([{"to":"console","levels":{"min":"error","max":"debug"}}]),
] {
assert!(
Logger::from_config(Some(&config)).is_err(),
"accepted {config}"
);
}
assert!(Logger::from_config(None).is_ok());
assert!(Logger::from_config(Some(&json!("debug"))).is_ok());
assert!(Logger::from_config(Some(&json!("*console"))).is_ok());
assert!(Logger::from_config_with_format(None, Some("invalid")).is_err());
}
#[test]
fn explicit_sink_levels_select_exactly_and_deduplicate() {
let dir = tempfile::tempdir().unwrap();
for (name, selection, expected) in [
("single", json!("warn"), vec!["warn"]),
(
"set",
json!(["debug", "error", "debug"]),
vec!["debug", "error"],
),
(
"all",
serde_json::Value::Null,
vec!["debug", "info", "warn", "error"],
),
] {
let path = dir.path().join(name);
let mut config = json!({"to":"file", "filename":path});
if !selection.is_null() {
config["levels"] = selection;
}
let logger = Logger::from_config_with_format(Some(&config), Some("json")).unwrap();
for level in [Level::Debug, Level::Info, Level::Warn, Level::Error] {
logger.event(level, "test", None, "selection", &[]);
}
let text = std::fs::read_to_string(path).unwrap();
let actual: Vec = text
.lines()
.map(|line| {
let record: serde_json::Value = serde_json::from_str(line).unwrap();
record["level"].as_str().unwrap().to_owned()
})
.collect();
assert_eq!(actual, expected, "selection {name}");
}
}
#[test]
fn native_console_shorthands_are_accepted() {
assert!(Logger::from_config(Some(&json!("*console"))).is_ok());
assert!(Logger::from_config(Some(&json!(["*console"]))).is_ok());
assert!(Logger::from_config(Some(&json!(["*unknown"]))).is_err());
}
#[test]
fn overlapping_shorthand_destinations_receive_each_record_once() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("shared.log");
let config = json!({"debug":path,"info":path,"error":path});
let logger = Logger::from_config_with_format(Some(&config), Some("json")).unwrap();
for level in [Level::Debug, Level::Info, Level::Warn, Level::Error] {
logger.event(level, "test", None, "selection", &[]);
}
let text = std::fs::read_to_string(path).unwrap();
let levels: Vec = text
.lines()
.map(|line| {
let record: serde_json::Value = serde_json::from_str(line).unwrap();
record["level"].as_str().unwrap().to_owned()
})
.collect();
assert_eq!(levels, ["debug", "info", "warn", "error"]);
}
#[test]
fn shared_sink_unions_levels_and_preserves_distinct_destinations() {
let dir = tempfile::tempdir().unwrap();
let shared = dir.path().join("shared.log");
let other = dir.path().join("other.log");
let config = json!([
{"to":"file","filename":shared,"levels":["debug","error"]},
{"to":"file","filename":shared,"levels":["warn","error"]},
{"to":"file","filename":other,"levels":{"min":"info","max":"error"}}
]);
let logger = Logger::from_config_with_format(Some(&config), Some("json")).unwrap();
for level in [Level::Debug, Level::Info, Level::Warn, Level::Error] {
logger.event(level, "test", None, "selection", &[]);
}
for (path, expected) in [
(shared, vec!["debug", "warn", "error"]),
(other, vec!["info", "warn", "error"]),
] {
let text = std::fs::read_to_string(path).unwrap();
let levels: Vec = text
.lines()
.map(|line| {
let record: serde_json::Value = serde_json::from_str(line).unwrap();
record["level"].as_str().unwrap().to_owned()
})
.collect();
assert_eq!(levels, expected);
}
}
#[test]
fn reopen_rotated_files_updates_clones() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("events.log");
let rotated = dir.path().join("events.log.1");
let logger = Logger::from_config(Some(&json!({"info":path}))).unwrap();
let clone = logger.clone();
logger.event(Level::Info, "test", None, "before_rotation", &[]);
std::fs::rename(&path, &rotated).unwrap();
logger.reopen().unwrap();
clone.event(Level::Info, "test", None, "after_rotation", &[]);
let old = std::fs::read_to_string(rotated).unwrap();
let new = std::fs::read_to_string(path).unwrap();
assert!(old.contains("before_rotation"));
assert!(!old.contains("after_rotation"));
assert!(new.contains("after_rotation"));
assert!(!new.contains("before_rotation"));
}
#[test]
fn failed_reopen_keeps_all_original_handles() {
let dir = tempfile::tempdir().unwrap();
let first = dir.path().join("first.log");
let second = dir.path().join("second.log");
let first_old = dir.path().join("first.log.1");
let second_old = dir.path().join("second.log.1");
let logger = Logger::from_config(Some(&json!([
{"to":"file","filename":first},
{"to":"file","filename":second}
])))
.unwrap();
std::fs::rename(&first, &first_old).unwrap();
std::fs::rename(&second, &second_old).unwrap();
std::fs::create_dir(&second).unwrap();
assert!(logger.reopen().is_err());
logger.event(Level::Info, "test", None, "retained_handles", &[]);
assert!(
std::fs::read_to_string(first_old)
.unwrap()
.contains("retained_handles")
);
assert!(
std::fs::read_to_string(second_old)
.unwrap()
.contains("retained_handles")
);
assert!(std::fs::read_to_string(first).unwrap().is_empty());
}