use super::*;
use appcore_contracts::ApplicationId;
use appcore_dnt::{inspect_header, KeyId, SecretKey, StaticDntKeyProvider};
use std::sync::Arc;
fn event(verbosity: Verbosity) -> LogEvent {
LogEvent::new(10, Severity::Info, verbosity, "sync", "token=abc hello")
.path("path", "/work/data/a.db")
.secret("api_key", "abc")
}
#[test]
fn verbosity_filters_independently_from_severity() {
let ring = Arc::new(RingBufferSink::new(4, 4096).unwrap());
let dispatcher = LogDispatcher::new(LogPolicy::new(Verbosity::V4), vec![ring.clone()]);
dispatcher.emit(event(Verbosity::V5));
dispatcher.emit(LogEvent::new(
11,
Severity::Critical,
Verbosity::V1,
"sync",
"down",
));
assert_eq!(ring.snapshot().len(), 1);
assert_eq!(dispatcher.stats().filtered, 1);
}
#[test]
fn safe_policy_redacts_secrets_and_aliases_paths() {
let mut policy = LogPolicy::default();
policy.paths.app_root = Some("/work".to_string());
let sanitized = policy.sanitize(&event(Verbosity::V4));
assert_eq!(sanitized.message, "<REDACTED> hello");
assert_eq!(sanitized.fields[0].value, "<APP_ROOT>/data/a.db");
assert_eq!(sanitized.fields[1].value, "<REDACTED>");
}
#[test]
fn safe_policy_redacts_credentials_embedded_in_text() {
let policy = LogPolicy::default();
let sanitized = policy.sanitize(&LogEvent::new(
10,
Severity::Warn,
Verbosity::V3,
"network",
"url=https://user:password@example.invalid token=abc cookie:session",
));
assert!(!sanitized.message.contains("password"));
assert!(!sanitized.message.contains("token=abc"));
assert!(!sanitized.message.contains("session"));
}
#[test]
fn safe_policy_aliases_home_and_hides_unknown_local_paths() {
let mut policy = LogPolicy::default();
policy.paths.home = Some("/Users/dan".to_string());
let home = policy.sanitize(
&LogEvent::new(10, Severity::Info, Verbosity::V4, "storage", "opened")
.path("path", "/Users/dan/.local/share/app/data.db"),
);
let unknown = policy.sanitize(
&LogEvent::new(11, Severity::Info, Verbosity::V4, "storage", "opened")
.path("path", "/srv/private/app/data.db"),
);
assert_eq!(home.fields[0].value, "<HOME>/.local/share/app/data.db");
assert_eq!(unknown.fields[0].value, "<LOCAL_PATH>");
}
#[test]
fn ring_buffer_enforces_its_capacity() {
let ring = RingBufferSink::new(1, 4096).unwrap();
ring.emit(&event(Verbosity::V4)).unwrap();
ring.emit(&LogEvent::new(
11,
Severity::Info,
Verbosity::V4,
"sync",
"next",
))
.unwrap();
assert_eq!(ring.snapshot().len(), 1);
assert_eq!(ring.snapshot()[0].message, "next");
}
#[test]
fn sensitive_sink_writes_only_authenticated_dnt() {
let path = std::env::temp_dir().join(format!("appcore-log-{}.dnt", std::process::id()));
let key_id = KeyId::new("log-key").unwrap();
let provider = StaticDntKeyProvider::new().with_key(key_id.clone(), SecretKey::new([7; 32]));
let sink = SensitiveDntSink::new(
SensitiveDntSinkConfig {
path: path.clone(),
application_id: ApplicationId::new("log-app").unwrap(),
key_id,
max_bytes: 4096,
max_events: 4,
retention: 1,
},
provider,
)
.unwrap();
sink.emit(&event(Verbosity::V9)).unwrap();
sink.emit(&LogEvent::new(
11,
Severity::Error,
Verbosity::V2,
"security",
"rotated snapshot",
))
.unwrap();
let bytes = std::fs::read(&path).unwrap();
assert!(inspect_header(&bytes).is_ok());
assert!(!bytes.windows(3).any(|value| value == b"abc"));
assert!(path.with_extension("dnt.1").is_file());
std::fs::remove_file(&path).unwrap();
std::fs::remove_file(path.with_extension("dnt.1")).unwrap();
}
#[test]
fn sensitive_policy_never_delivers_to_an_ordinary_sink() {
let ring = Arc::new(RingBufferSink::new(4, 4096).unwrap());
let mut policy = LogPolicy::default();
policy.sensitivity = Sensitivity::Sensitive;
let dispatcher = LogDispatcher::new(policy, vec![ring.clone()]);
dispatcher.emit(LogEvent::new(
12,
Severity::Error,
Verbosity::V2,
"security",
"credential diagnostic",
));
assert!(ring.snapshot().is_empty());
assert_eq!(dispatcher.stats().sink_failures, 1);
}
#[test]
fn component_override_does_not_change_the_global_threshold() {
let ring = Arc::new(RingBufferSink::new(4, 4096).unwrap());
let mut policy = LogPolicy::new(Verbosity::V4);
policy.set_component("sync", Verbosity::V9);
let dispatcher = LogDispatcher::new(policy, vec![ring.clone()]);
dispatcher.emit(LogEvent::new(
13,
Severity::Debug,
Verbosity::V8,
"sync",
"accepted",
));
dispatcher.emit(LogEvent::new(
14,
Severity::Debug,
Verbosity::V8,
"storage",
"filtered",
));
assert_eq!(ring.snapshot().len(), 1);
assert_eq!(dispatcher.stats().filtered, 1);
}
#[test]
fn parent_component_override_applies_to_descendants() {
let ring = Arc::new(RingBufferSink::new(4, 4096).unwrap());
let mut policy = LogPolicy::new(Verbosity::V4);
policy.set_component("sync", Verbosity::V8);
let dispatcher = LogDispatcher::new(policy, vec![ring.clone()]);
dispatcher.emit(LogEvent::new(
15,
Severity::Debug,
Verbosity::V7,
"sync.transport",
"accepted",
));
assert_eq!(ring.snapshot().len(), 1);
}
#[test]
fn oversized_event_is_rejected_before_sink_delivery() {
let ring = Arc::new(RingBufferSink::new(4, 8192).unwrap());
let dispatcher = LogDispatcher::new(LogPolicy::default(), vec![ring.clone()]);
dispatcher.emit(LogEvent::new(
16,
Severity::Info,
Verbosity::V4,
"application",
"x".repeat(MAX_LOG_TEXT_BYTES + 1),
));
assert!(ring.snapshot().is_empty());
assert_eq!(dispatcher.stats().invalid, 1);
}
#[test]
fn injected_clock_produces_a_deterministic_timestamp() {
let ring = Arc::new(RingBufferSink::new(4, 4096).unwrap());
let dispatcher = LogDispatcher::new(LogPolicy::default(), vec![ring.clone()]);
dispatcher
.event_now(&FixedLogClock::new(42), "application")
.info("clocked");
assert_eq!(ring.snapshot()[0].timestamp_ms, 42);
}
#[derive(Debug)]
struct FailingSink;
impl LogSink for FailingSink {
fn emit(&self, _: &LogEvent) -> Result<(), LogError> {
Err(LogError::Io)
}
fn name(&self) -> &'static str {
"test_failure"
}
}
#[test]
fn sink_failures_are_accounted_per_sink_without_recursion() {
let dispatcher = LogDispatcher::new(LogPolicy::default(), vec![Arc::new(FailingSink)]);
dispatcher.emit(LogEvent::new(
17,
Severity::Warn,
Verbosity::V3,
"application",
"controlled failure",
));
assert_eq!(dispatcher.stats().sink_failures, 1);
assert_eq!(dispatcher.sink_stats()[0].name, "test_failure");
assert_eq!(dispatcher.sink_stats()[0].failures, 1);
}
#[test]
fn concurrent_emission_preserves_ring_bounds() {
let ring = Arc::new(RingBufferSink::new(16, 16 * 1024).unwrap());
let dispatcher = Arc::new(LogDispatcher::new(LogPolicy::default(), vec![ring.clone()]));
let workers = (0..4)
.map(|worker| {
let dispatcher = Arc::clone(&dispatcher);
std::thread::spawn(move || {
for sequence in 0..16 {
dispatcher.emit(LogEvent::new(
worker * 100 + sequence,
Severity::Info,
Verbosity::V4,
"concurrent",
"bounded",
));
}
})
})
.collect::<Vec<_>>();
for worker in workers {
worker.join().unwrap();
}
assert!(ring.snapshot().len() <= 16);
}
#[test]
fn fluent_builder_reuses_defaults_with_a_per_event_override() {
let ring = Arc::new(RingBufferSink::new(4, 4096).unwrap());
let mut policy = LogPolicy::new(Verbosity::V4);
policy.set_component("sync", Verbosity::V8);
let dispatcher = LogDispatcher::new(policy, vec![ring.clone()]);
let log = dispatcher.event(15, "application").component("sync");
log.info("default event");
log.verbosity(7).info("deep event");
assert_eq!(ring.snapshot().len(), 2);
assert_eq!(ring.snapshot()[0].component, "sync");
assert_eq!(ring.snapshot()[0].verbosity, Verbosity::V4);
assert_eq!(ring.snapshot()[1].verbosity, Verbosity::V7);
}
#[test]
fn checked_verbosity_rejects_invalid_configuration() {
let dispatcher = LogDispatcher::new(LogPolicy::default(), Vec::new());
assert!(matches!(
dispatcher.event(18, "application").try_verbosity(0),
Err(VerbosityError)
));
assert!(dispatcher.event(18, "application").try_verbosity(9).is_ok());
}
struct PanicMessage;
impl From<PanicMessage> for String {
fn from(_: PanicMessage) -> Self {
panic!("disabled logging formatted a message")
}
}
#[test]
fn disabled_dispatcher_does_not_convert_the_message() {
let logger = LoggerConfig {
output: LogOutputMode::Disabled,
..LoggerConfig::default()
}
.build()
.unwrap();
logger.dispatcher().event(19, "disabled").info(PanicMessage);
}