use std::sync::atomic::Ordering;
use std::sync::Arc;
use eggserve_core::ops::{
self, CompositeLogSink, Event, EventKind, Field, LogSink, Logger, NopLogSink, OpsCounters,
Severity,
};
#[test]
fn json_log_validity() {
let events = vec![
Event::new(Severity::Info, EventKind::ProcessStarting, "test"),
Event::new(Severity::Warn, EventKind::ClientDisconnect, "disconnected"),
Event::new(
Severity::Error,
EventKind::ListenerPersistentError,
"fatal error",
),
Event::new(Severity::Debug, EventKind::FileNotFound, "not found"),
];
for event in &events {
let json = ops::event_to_json(event);
let parsed: serde_json::Value = serde_json::from_str(&json)
.unwrap_or_else(|e| panic!("invalid JSON from event_to_json: {e}\njson: {json}"));
assert!(parsed.is_object(), "JSON must be an object: {json}");
assert_eq!(parsed["schema_version"], 1);
assert_eq!(parsed["severity"], event.severity.to_string());
assert_eq!(parsed["event"], event.event.to_string());
assert!(parsed["timestamp"].is_string());
assert!(parsed["message"].is_string());
}
}
#[test]
fn json_one_record_per_line() {
let event = Event::new(
Severity::Info,
EventKind::ProcessStarting,
"test\nwith\nnewlines",
);
let json = ops::event_to_json(&event);
assert!(!json.contains('\n'), "raw newline in JSON: {json}");
let _: serde_json::Value = serde_json::from_str(&json).unwrap();
}
#[test]
fn json_no_plain_text_banner() {
let event = Event::new(
Severity::Info,
EventKind::ProcessStarting,
"server starting",
);
let json = ops::event_to_json(&event);
assert!(json.starts_with('{'), "JSON must start with {{: {json}");
assert!(json.ends_with('}'), "JSON must end with }}: {json}");
}
#[test]
fn json_event_schema_version_always_one() {
let event = Event::new(Severity::Info, EventKind::ProcessStarting, "test");
let json = ops::event_to_json(&event);
let parsed: serde_json::Value = serde_json::from_str(&json).unwrap();
assert_eq!(parsed["schema_version"], 1);
}
#[test]
fn json_event_timestamp_format() {
let event = Event::new(Severity::Info, EventKind::ProcessStarting, "test");
let json = ops::event_to_json(&event);
let parsed: serde_json::Value = serde_json::from_str(&json).unwrap();
let ts = parsed["timestamp"].as_str().unwrap();
assert!(ts.ends_with('Z'), "timestamp must end with Z: {ts}");
assert_eq!(ts.len(), 24, "timestamp must be 24 chars: {ts}");
assert!(ts.contains('T'), "timestamp must contain T: {ts}");
}
#[test]
fn json_connection_id_and_request_seq() {
let event = Event::new(Severity::Info, EventKind::ConnectionAccepted, "accepted")
.connection_id(42)
.request_seq(7);
let json = ops::event_to_json(&event);
let parsed: serde_json::Value = serde_json::from_str(&json).unwrap();
assert_eq!(parsed["connection_id"], 42);
assert_eq!(parsed["request_seq"], 7);
}
#[test]
fn json_optional_fields_omitted_when_none() {
let event = Event::new(Severity::Info, EventKind::ProcessStarting, "test");
let json = ops::event_to_json(&event);
let parsed: serde_json::Value = serde_json::from_str(&json).unwrap();
assert!(parsed.get("connection_id").is_none());
assert!(parsed.get("request_seq").is_none());
}
#[test]
fn json_control_chars_escaped() {
let event = Event::new(
Severity::Info,
EventKind::ProcessStarting,
"line1\nline2\ttab\"quote\\slash",
);
let json = ops::event_to_json(&event);
let parsed: serde_json::Value = serde_json::from_str(&json).unwrap();
let msg = parsed["message"].as_str().unwrap();
assert!(msg.contains('\n'));
assert!(msg.contains('\t'));
assert!(msg.contains('"'));
assert!(msg.contains('\\'));
}
#[test]
fn json_fields_array() {
let event = Event::new(Severity::Info, EventKind::ProcessStarting, "test")
.field(Field::Str("key".into(), "value".into()))
.field(Field::Bool("flag".into(), true))
.field(Field::U64("count".into(), 42))
.field(Field::I64("signed".into(), -1));
let json = ops::event_to_json(&event);
let parsed: serde_json::Value = serde_json::from_str(&json).unwrap();
let fields = parsed["fields"].as_array().unwrap();
assert_eq!(fields.len(), 4);
assert_eq!(fields[0]["key"], "value");
assert_eq!(fields[1]["flag"], true);
assert_eq!(fields[2]["count"], 42);
assert_eq!(fields[3]["signed"], -1);
}
#[test]
fn json_numeric_types_preserved() {
let event = Event::new(Severity::Info, EventKind::ProcessStarting, "test")
.field(Field::U64("u".into(), 999))
.field(Field::I64("i".into(), -42));
let json = ops::event_to_json(&event);
let parsed: serde_json::Value = serde_json::from_str(&json).unwrap();
let fields = parsed["fields"].as_array().unwrap();
assert!(fields[0]["u"].is_number());
assert!(fields[1]["i"].is_number());
assert_eq!(fields[0]["u"], 999);
assert_eq!(fields[1]["i"], -42);
}
#[test]
fn text_log_sanitize_control_chars() {
assert_eq!(ops::sanitize_text_field("hello\r\nworld"), "helloworld");
assert_eq!(ops::sanitize_text_field("tab\there"), "tabhere");
assert_eq!(ops::sanitize_text_field("esc\x1B[31mred"), "esc[31mred");
assert_eq!(
ops::sanitize_text_field("null\x00byte\x7Fdel"),
"nullbytedel"
);
assert_eq!(ops::sanitize_text_field("normal text"), "normal text");
}
#[test]
fn text_log_sanitize_bidi_controls() {
let bidi = "hello\u{202A}world\u{202C}";
let sanitized = ops::sanitize_text_field(bidi);
assert!(!sanitized.contains('\u{202A}'));
assert!(!sanitized.contains('\u{202C}'));
}
#[test]
fn text_log_sanitize_long_input() {
let long = "a".repeat(10000);
let sanitized = ops::sanitize_text_field(&long);
assert!(sanitized.len() <= 515);
assert!(sanitized.ends_with('…'));
}
#[test]
fn sanitize_path_extracts_last_component() {
assert_eq!(ops::sanitize_path("/foo/bar/baz.txt"), "baz.txt");
assert_eq!(ops::sanitize_path("no/slash/here/"), "");
assert_eq!(ops::sanitize_path("only-one"), "only-one");
}
#[test]
fn sanitize_path_truncates_long_paths() {
let long_name = "a".repeat(200);
let result = ops::sanitize_path(&format!("/prefix/{}", long_name));
assert!(result.chars().count() <= 128);
assert!(result.ends_with('…'));
}
#[test]
fn sanitize_path_strips_control_chars() {
let result = ops::sanitize_path("/foo/bar\x1B[31m.txt");
assert!(!result.contains('\x1B'));
}
#[test]
fn injection_carriage_return_in_path() {
let result = ops::sanitize_path("/foo/bar\r\ninjection.txt");
assert!(!result.contains('\r'));
assert!(!result.contains('\n'));
}
#[test]
fn injection_null_byte_in_text() {
let result = ops::sanitize_text_field("hello\x00world");
assert_eq!(result, "helloworld");
}
#[test]
fn injection_escape_sequence_in_text() {
let result = ops::sanitize_text_field("hello\x1B[31mred\x1B[0m");
assert_eq!(result, "hello[31mred[0m");
assert!(!result.contains('\x1B'));
}
#[test]
fn injection_ansi_sgr_in_path() {
let result = ops::sanitize_path("/foo/\x1B[31mred.txt\x1B[0m");
assert!(!result.contains('\x1B'));
}
#[test]
fn no_absolute_path_in_sanitize_path_output() {
assert_eq!(ops::sanitize_path("/etc/passwd"), "passwd");
assert_eq!(ops::sanitize_path("/home/user/secret.txt"), "secret.txt");
}
#[test]
fn path_sanitize_strips_query_string() {
assert_eq!(ops::sanitize_path("/foo/bar.txt?secret=abc"), "bar.txt");
}
#[test]
fn all_event_kinds_have_names() {
let kinds = [
EventKind::ProcessStarting,
EventKind::RootInitialized,
EventKind::ListenerReady,
EventKind::ShutdownRequested,
EventKind::DrainingStarted,
EventKind::ForcedShutdownStarted,
EventKind::ShutdownComplete,
EventKind::ConnectionAccepted,
EventKind::ConnectionRejected,
EventKind::TlsHandshakeSuccess,
EventKind::TlsHandshakeFailure,
EventKind::TlsHandshakeTimeout,
EventKind::HeaderTimeout,
EventKind::BodyReadTimeout,
EventKind::ParserRejection,
EventKind::KeepAliveClosed,
EventKind::ConnectionTotalTimeout,
EventKind::ClientDisconnect,
EventKind::ConnectionPanic,
EventKind::RequestCompleted,
EventKind::FileNotFound,
EventKind::FileDenied,
EventKind::FileError,
EventKind::DotfileDenied,
EventKind::SymlinkDenied,
EventKind::RootEscapeDenied,
EventKind::BodyPolicyRejection,
EventKind::ServiceTimeout,
EventKind::ServiceError,
EventKind::DirectoryListingLimit,
EventKind::ListenerTransientError,
EventKind::ListenerPersistentError,
EventKind::ResourceExhaustion,
EventKind::BlockingWorkerSaturation,
EventKind::LogSinkFailure,
];
let mut names = std::collections::HashSet::new();
for kind in &kinds {
let name = kind.to_string();
assert!(!name.is_empty(), "event kind has no name");
assert!(
names.insert(name.clone()),
"duplicate event kind name: {}",
name
);
}
}
#[test]
fn severity_display() {
assert_eq!(Severity::Debug.to_string(), "DEBUG");
assert_eq!(Severity::Info.to_string(), "INFO");
assert_eq!(Severity::Warn.to_string(), "WARN");
assert_eq!(Severity::Error.to_string(), "ERROR");
}
#[test]
fn correlation_id_starts_at_one() {
let cid = ops::CorrelationId::new();
assert_eq!(cid.next(), 1);
}
#[test]
fn correlation_id_increments() {
let cid = ops::CorrelationId::new();
assert_eq!(cid.next(), 1);
assert_eq!(cid.next(), 2);
assert_eq!(cid.next(), 3);
}
#[test]
fn correlation_id_wraps_safely() {
let cid = ops::CorrelationId::new();
for _ in 0..1000 {
let _ = cid.next();
}
}
#[tokio::test]
async fn correlation_id_unique_concurrent() {
let cid = Arc::new(ops::CorrelationId::new());
let mut handles = Vec::new();
for _ in 0..10 {
let cid = cid.clone();
handles.push(tokio::spawn(async move {
let mut ids = Vec::new();
for _ in 0..100 {
ids.push(cid.next());
}
ids
}));
}
let mut all_ids = Vec::new();
for h in handles {
all_ids.extend(h.await.unwrap());
}
let set: std::collections::HashSet<u64> = all_ids.into_iter().collect();
assert_eq!(set.len(), 1000);
}
struct PanicSink;
impl LogSink for PanicSink {
fn emit(&self, _event: &Event) {
panic!("sink panic");
}
fn flush(&self) {
panic!("flush panic");
}
}
#[test]
fn composite_sink_catches_panic() {
let counters = eggserve_core::ops::global_counters();
let before = counters
.dropped_log_events
.load(std::sync::atomic::Ordering::Relaxed);
let composite = CompositeLogSink::new(vec![Box::new(PanicSink), Box::new(NopLogSink)]);
let event = Event::new(Severity::Info, EventKind::ProcessStarting, "test");
composite.emit(&event);
let after = counters
.dropped_log_events
.load(std::sync::atomic::Ordering::Relaxed);
assert!(
after > before,
"dropped_log_events should increment when a sink panics"
);
}
#[test]
fn log_failure_does_not_panic() {
let logger = Logger::global();
let event = Event::new(Severity::Info, EventKind::ProcessStarting, "test");
logger.emit(event);
let event2 = Event::new(Severity::Debug, EventKind::FileNotFound, "still works");
logger.emit(event2);
}
#[test]
fn backoff_interruptible_by_shutdown() {
let rt = tokio::runtime::Runtime::new().unwrap();
rt.block_on(async {
let (_tx, mut rx) = tokio::sync::broadcast::channel::<()>(1);
let err = std::io::Error::new(std::io::ErrorKind::Interrupted, "test");
let mut backoff_idx = 0usize;
let mut error_repeat_count = 0usize;
let mut last_error_kind = None;
let _ = _tx.send(());
let result = tokio::time::timeout(
std::time::Duration::from_secs(5),
classify_accept_error_for_test(
&err,
&mut rx,
&mut backoff_idx,
&mut error_repeat_count,
&mut last_error_kind,
),
)
.await;
assert!(
result.is_ok(),
"backoff should be interruptible by shutdown"
);
});
}
#[test]
fn backoff_resets_on_success() {
let mut backoff_idx = 0usize;
for _ in 0..10 {
backoff_idx = backoff_idx.saturating_add(1);
}
backoff_idx = 0;
assert_eq!(backoff_idx, 0);
}
async fn classify_accept_error_for_test(
e: &std::io::Error,
shutdown_rx: &mut tokio::sync::broadcast::Receiver<()>,
backoff_idx: &mut usize,
error_repeat_count: &mut usize,
last_error_kind: &mut Option<String>,
) -> bool {
use eggserve_core::ops::{Event, EventKind, Logger, Severity};
let err_str = e.to_string();
let kind = e.kind();
let (severity, event_kind, should_backoff, is_fatal) = match kind {
std::io::ErrorKind::Interrupted => (
Severity::Debug,
EventKind::ListenerTransientError,
true,
false,
),
_ => (
Severity::Error,
EventKind::ListenerPersistentError,
false,
true,
),
};
let current_kind = format!("{}", event_kind);
let is_same_kind = last_error_kind.as_deref() == Some(¤t_kind);
if is_same_kind {
*error_repeat_count += 1;
} else {
*error_repeat_count = 1;
*last_error_kind = Some(current_kind);
}
let should_emit = *error_repeat_count == 1 || (*error_repeat_count).is_multiple_of(10);
if should_emit {
let message = if *error_repeat_count > 1 {
format!(
"accept error ({} consecutive): {}",
error_repeat_count, err_str
)
} else {
format!("accept error: {}", err_str)
};
Logger::global().emit(Event::new(severity, event_kind, message).field(
eggserve_core::ops::Field::Str("error_kind".into(), format!("{:?}", kind)),
));
}
if should_backoff {
static BACKOFF_MS: [u64; 5] = [1, 2, 4, 8, 50];
let idx = (*backoff_idx).min(BACKOFF_MS.len() - 1);
*backoff_idx = backoff_idx.saturating_add(1);
let backoff = std::time::Duration::from_millis(BACKOFF_MS[idx]);
tokio::select! {
_ = tokio::time::sleep(backoff) => {}
_ = shutdown_rx.recv() => {}
}
}
is_fatal
}
#[test]
fn log_sink_failure_emitted_on_panic() {
use std::sync::{Arc, Mutex};
let events_emitted = Arc::new(Mutex::new(Vec::new()));
let events_clone = events_emitted.clone();
struct CountingSink {
events: Arc<Mutex<Vec<String>>>,
}
impl LogSink for CountingSink {
fn emit(&self, event: &Event) {
if let Ok(mut evts) = self.events.lock() {
evts.push(event.event.to_string());
}
}
fn flush(&self) {}
}
let composite = CompositeLogSink::new(vec![
Box::new(PanicSink),
Box::new(CountingSink {
events: events_clone,
}),
]);
let event = Event::new(Severity::Info, EventKind::ProcessStarting, "test");
composite.emit(&event);
let counters = eggserve_core::ops::global_counters();
let dropped = counters
.dropped_log_events
.load(std::sync::atomic::Ordering::Relaxed);
assert!(dropped > 0, "dropped_log_events should be incremented");
}
#[test]
fn ops_counters_snapshot() {
let counters = OpsCounters::new();
counters
.connections_accepted
.fetch_add(5, Ordering::Relaxed);
counters.bytes_sent.fetch_add(1024, Ordering::Relaxed);
counters.listener_errors.fetch_add(1, Ordering::Relaxed);
counters.active_connections.fetch_add(3, Ordering::Relaxed);
counters.active_file_streams.fetch_add(2, Ordering::Relaxed);
counters.parser_rejects.fetch_add(1, Ordering::Relaxed);
counters.header_timeouts.fetch_add(1, Ordering::Relaxed);
counters
.connection_total_timeouts
.fetch_add(1, Ordering::Relaxed);
counters.graceful_shutdowns.fetch_add(1, Ordering::Relaxed);
counters.forced_shutdowns.fetch_add(1, Ordering::Relaxed);
counters
.connections_rejected
.fetch_add(2, Ordering::Relaxed);
counters.dropped_log_events.fetch_add(1, Ordering::Relaxed);
let snap = counters.snapshot();
assert_eq!(snap.connections_accepted, 5);
assert_eq!(snap.bytes_sent, 1024);
assert_eq!(snap.listener_errors, 1);
assert_eq!(snap.active_connections, 3);
assert_eq!(snap.active_file_streams, 2);
assert_eq!(snap.parser_rejects, 1);
assert_eq!(snap.header_timeouts, 1);
assert_eq!(snap.connection_total_timeouts, 1);
assert_eq!(snap.graceful_shutdowns, 1);
assert_eq!(snap.forced_shutdowns, 1);
assert_eq!(snap.connections_rejected, 2);
assert_eq!(snap.dropped_log_events, 1);
}
#[test]
fn global_counters_returns_singleton() {
let c1 = ops::global_counters();
let c2 = ops::global_counters();
assert!(std::ptr::eq(c1, c2));
}
#[test]
fn event_builder_chain() {
let event = Event::new(Severity::Info, EventKind::ProcessStarting, "test")
.field(Field::Str("key".into(), "value".into()))
.field(Field::Bool("flag".into(), true))
.field(Field::U64("count".into(), 42))
.field(Field::I64("signed".into(), -1))
.connection_id(123)
.request_seq(456);
assert_eq!(event.schema_version, 1);
assert_eq!(event.severity, Severity::Info);
assert_eq!(event.event, EventKind::ProcessStarting);
assert_eq!(event.message, "test");
assert_eq!(event.connection_id, Some(123));
assert_eq!(event.request_seq, Some(456));
assert_eq!(event.fields.len(), 4);
}
#[test]
fn logger_try_init_does_not_panic() {
let _ = Logger::try_init(Box::new(NopLogSink));
let _ = Logger::try_init(Box::new(NopLogSink));
}
#[test]
fn logger_global_returns_something() {
let _ = Logger::global();
}
#[test]
fn event_schema_version() {
assert_eq!(ops::SCHEMA_VERSION, 1);
}
#[test]
fn streaming_error_events_exist() {
assert_eq!(EventKind::ClientDisconnect.to_string(), "client_disconnect");
assert_eq!(
EventKind::ConnectionTotalTimeout.to_string(),
"connection_total_timeout"
);
assert_eq!(EventKind::BodyReadTimeout.to_string(), "body_read_timeout");
assert_eq!(EventKind::ServiceTimeout.to_string(), "service_timeout");
assert_eq!(EventKind::HeaderTimeout.to_string(), "header_timeout");
}
#[test]
fn filesystem_denial_events_exist() {
assert_eq!(EventKind::FileNotFound.to_string(), "file_not_found");
assert_eq!(EventKind::FileDenied.to_string(), "file_denied");
assert_eq!(EventKind::DotfileDenied.to_string(), "dotfile_denied");
assert_eq!(EventKind::SymlinkDenied.to_string(), "symlink_denied");
assert_eq!(
EventKind::RootEscapeDenied.to_string(),
"root_escape_denied"
);
}
#[test]
fn lifecycle_events_exist() {
assert_eq!(EventKind::DrainingStarted.to_string(), "draining_started");
assert_eq!(
EventKind::ForcedShutdownStarted.to_string(),
"forced_shutdown_started"
);
assert_eq!(EventKind::ShutdownComplete.to_string(), "shutdown_complete");
}
#[test]
fn field_display() {
let f = Field::Str("key".into(), "value".into());
assert_eq!(format!("{}", f), "\"key\": \"value\"");
let f = Field::Bool("flag".into(), true);
assert_eq!(format!("{}", f), "\"flag\": true");
let f = Field::U64("count".into(), 42);
assert_eq!(format!("{}", f), "\"count\": 42");
let f = Field::I64("signed".into(), -1);
assert_eq!(format!("{}", f), "\"signed\": -1");
}
#[test]
fn field_display_escapes_strings() {
let f = Field::Str("key".into(), "value with \"quotes\" and \\backslash".into());
let display = format!("{}", f);
assert!(display.contains("\\\"quotes\\\""));
assert!(display.contains("\\\\backslash"));
}
#[test]
fn text_json_semantic_equivalence() {
let event = Event::new(Severity::Warn, EventKind::ClientDisconnect, "client gone")
.connection_id(99)
.request_seq(3)
.field(Field::Str("error_kind".into(), "BrokenPipe".into()));
let json = ops::event_to_json(&event);
let parsed: serde_json::Value = serde_json::from_str(&json).unwrap();
assert_eq!(parsed["severity"], "WARN");
assert_eq!(parsed["event"], "client_disconnect");
assert_eq!(parsed["message"], "client gone");
assert_eq!(parsed["connection_id"], 99);
assert_eq!(parsed["request_seq"], 3);
let fields = parsed["fields"].as_array().unwrap();
assert_eq!(fields[0]["error_kind"], "BrokenPipe");
let mut text_buf = Vec::new();
{
let sink = TextCaptureSink {
output: std::sync::Mutex::new(&mut text_buf),
};
sink.emit(&event);
}
let text = String::from_utf8(text_buf).unwrap();
assert!(text.contains("WARN"), "text must contain severity: {text}");
assert!(
text.contains("client_disconnect"),
"text must contain event kind: {text}"
);
assert!(
text.contains("client gone"),
"text must contain message: {text}"
);
}
struct TextCaptureSink<'a> {
output: std::sync::Mutex<&'a mut Vec<u8>>,
}
impl LogSink for TextCaptureSink<'_> {
fn emit(&self, event: &Event) {
let mut line = format!("[{}] {}: {}", event.severity, event.event, event.message);
if let Some(cid) = event.connection_id {
line.push_str(&format!(" conn={}", cid));
}
if let Some(seq) = event.request_seq {
line.push_str(&format!(" seq={}", seq));
}
for f in &event.fields {
line.push_str(&format!(" {}", f));
}
line.push('\n');
if let Ok(mut out) = self.output.lock() {
out.extend_from_slice(line.as_bytes());
}
}
fn flush(&self) {}
}
#[test]
fn backoff_rate_limits_repeated_errors() {
let mut error_repeat_count = 0usize;
let mut last_error_kind: Option<String> = None;
let mut emit_count = 0usize;
for _ in 0..25 {
let current_kind = "listener_persistent_error".to_string();
let is_same_kind = last_error_kind.as_deref() == Some(¤t_kind);
if is_same_kind {
error_repeat_count += 1;
} else {
error_repeat_count = 1;
last_error_kind = Some(current_kind);
}
let should_emit = error_repeat_count == 1 || error_repeat_count.is_multiple_of(10);
if should_emit {
emit_count += 1;
}
}
assert_eq!(
emit_count, 3,
"should emit at 1st, 10th, and 20th occurrence"
);
}
#[test]
fn backoff_duration_is_bounded() {
static BACKOFF_MS: [u64; 5] = [1, 2, 4, 8, 50];
let max_backoff = *BACKOFF_MS.iter().max().unwrap();
assert!(
max_backoff <= 100,
"max backoff must be ≤100ms, got {}ms",
max_backoff
);
assert_eq!(BACKOFF_MS.len(), 5);
for w in BACKOFF_MS.windows(2) {
assert!(w[0] <= w[1], "backoff must be non-decreasing");
}
}
#[test]
fn client_disconnect_uses_debug_severity() {
let event = Event::new(
Severity::Debug,
EventKind::ClientDisconnect,
"connection error: connection reset",
);
assert_eq!(event.severity, Severity::Debug);
assert_eq!(event.event, EventKind::ClientDisconnect);
assert_eq!(event.event.to_string(), "client_disconnect");
}
#[test]
fn connection_total_timeout_uses_warn_severity() {
let event = Event::new(
Severity::Warn,
EventKind::ConnectionTotalTimeout,
"connection total timeout",
);
assert_eq!(event.severity, Severity::Warn);
assert_eq!(event.event, EventKind::ConnectionTotalTimeout);
}
#[test]
fn file_stream_io_error_uses_warn_severity() {
let event = Event::new(
Severity::Warn,
EventKind::FileError,
"file stream I/O error: broken pipe",
);
assert_eq!(event.severity, Severity::Warn);
assert_eq!(event.event, EventKind::FileError);
assert_eq!(event.event.to_string(), "file_error");
}
#[test]
fn file_stream_error_event_is_valid_json() {
let event = Event::new(
Severity::Warn,
EventKind::FileError,
"file stream I/O error: seek failed",
)
.connection_id(42)
.request_seq(1);
let json = ops::event_to_json(&event);
let parsed: serde_json::Value = serde_json::from_str(&json).unwrap();
assert_eq!(parsed["severity"], "WARN");
assert_eq!(parsed["event"], "file_error");
assert_eq!(parsed["connection_id"], 42);
}
#[test]
fn header_values_not_leaked_in_event_json() {
let sensitive_headers = [
"Authorization",
"Cookie",
"Set-Cookie",
"X-Api-Key",
"Proxy-Authorization",
];
for header in &sensitive_headers {
let event = Event::new(
Severity::Debug,
EventKind::RequestCompleted,
"request completed",
)
.field(Field::Str("path".into(), format!("/foo/{}.txt", header)));
let json = ops::event_to_json(&event);
let parsed: serde_json::Value = serde_json::from_str(&json).unwrap();
let msg = parsed["message"].as_str().unwrap();
assert!(
!msg.contains(header),
"event message leaked header name: {msg}"
);
}
}
#[test]
fn sanitize_path_strips_query_parameters() {
assert_eq!(ops::sanitize_path("/page?token=secret123"), "page");
assert_eq!(ops::sanitize_path("/api/data?key=abc123"), "data");
assert_eq!(ops::sanitize_path("/foo/bar/baz?q=1&r=2"), "baz");
}
#[test]
fn library_core_has_no_print_macros_in_production_code() {
let core_src = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("src");
let production_files: Vec<std::path::PathBuf> = std::fs::read_dir(&core_src)
.unwrap()
.filter_map(|e| e.ok())
.filter(|e| {
let name = e.file_name().to_string_lossy().to_string();
name.ends_with(".rs") && !name.contains("test")
})
.map(|e| e.path())
.collect();
assert!(
!production_files.is_empty(),
"should find production source files in {}",
core_src.display()
);
for path in &production_files {
let content = std::fs::read_to_string(path).unwrap();
for (i, line) in content.lines().enumerate() {
let trimmed = line.trim();
if trimmed.starts_with("//") || trimmed.starts_with("/*") || trimmed.starts_with("*") {
continue;
}
let has_println = trimmed.contains("println!");
let has_eprintln = trimmed.contains("eprintln!");
assert!(
!(has_println && !has_eprintln),
"{}:{}: found println! in production code: {}",
path.display(),
i + 1,
trimmed
);
assert!(
!trimmed.contains("print!(\""),
"{}:{}: found print!(\"..\") in production code: {}",
path.display(),
i + 1,
trimmed
);
}
}
}
#[cfg(test)]
mod proptest_tests {
use super::*;
use proptest::prelude::*;
proptest! {
#[test]
fn event_to_json_never_panics(
severity in prop_oneof![
Just(Severity::Debug),
Just(Severity::Info),
Just(Severity::Warn),
Just(Severity::Error),
],
message in ".*",
field_key in "[a-z_]{0,50}",
field_val in ".*",
) {
let event = Event::new(severity, EventKind::ProcessStarting, message)
.field(Field::Str(field_key, field_val));
let json = ops::event_to_json(&event);
let _: serde_json::Value = serde_json::from_str(&json).unwrap();
}
#[test]
fn sanitize_text_never_panics(s in ".*") {
let _ = ops::sanitize_text_field(&s);
}
#[test]
fn sanitize_path_never_panics(s in ".*") {
let _ = ops::sanitize_path(&s);
}
#[test]
fn sanitize_text_strips_control_chars(s in "\x00-\x1f") {
let result = ops::sanitize_text_field(&s);
assert!(!result.chars().any(|c| (c as u32) < 0x20));
}
#[test]
fn sanitize_text_preserves_printable(s in "[\\x20-\\x7e]+") {
let result = ops::sanitize_text_field(&s);
assert_eq!(result, s);
}
#[test]
fn sanitize_path_always_returns_last_component(path in "/[a-z]{1,10}/[a-z]{1,10}/[a-z]{1,10}") {
let result = ops::sanitize_path(&path);
let last = path.rsplit('/').next().unwrap();
assert!(result.len() <= last.len().min(128));
}
#[test]
fn event_kind_display_is_snake_case(kind in prop_oneof![
Just(EventKind::ProcessStarting),
Just(EventKind::RootInitialized),
Just(EventKind::ListenerReady),
Just(EventKind::ShutdownRequested),
Just(EventKind::DrainingStarted),
Just(EventKind::ForcedShutdownStarted),
Just(EventKind::ShutdownComplete),
Just(EventKind::ConnectionAccepted),
Just(EventKind::ConnectionRejected),
Just(EventKind::ClientDisconnect),
Just(EventKind::ConnectionTotalTimeout),
Just(EventKind::BodyReadTimeout),
Just(EventKind::ServiceTimeout),
Just(EventKind::FileNotFound),
Just(EventKind::FileDenied),
Just(EventKind::FileError),
Just(EventKind::DotfileDenied),
Just(EventKind::SymlinkDenied),
Just(EventKind::RootEscapeDenied),
Just(EventKind::ListenerTransientError),
Just(EventKind::ListenerPersistentError),
Just(EventKind::ResourceExhaustion),
Just(EventKind::LogSinkFailure),
]) {
let name = kind.to_string();
assert!(!name.is_empty());
assert!(name.chars().all(|c| c.is_ascii_lowercase() || c == '_'));
}
#[test]
fn correlation_id_always_positive(count in 1u64..10000) {
let cid = ops::CorrelationId::new();
let mut last = 0;
for _ in 0..count {
let id = cid.next();
assert!(id > last);
last = id;
}
}
#[test]
fn ops_counters_snapshot_consistent(
accepted in 0u64..1000,
rejected in 0u64..100,
errors in 0u64..100,
) {
let counters = OpsCounters::new();
counters.connections_accepted.fetch_add(accepted, Ordering::Relaxed);
counters.connections_rejected.fetch_add(rejected, Ordering::Relaxed);
counters.listener_errors.fetch_add(errors, Ordering::Relaxed);
let snap = counters.snapshot();
assert_eq!(snap.connections_accepted, accepted);
assert_eq!(snap.connections_rejected, rejected);
assert_eq!(snap.listener_errors, errors);
}
}
}