use logdive_core::{Indexer, LogEntry, execute, parse_query};
fn make_entry(ts: &str, level: &str, message: &str) -> LogEntry {
let raw = format!(r#"{{"timestamp":"{ts}","level":"{level}","message":"{message}"}}"#);
let mut e = LogEntry::new(raw);
e.timestamp = Some(ts.to_string());
e.level = Some(level.to_string());
e.message = Some(message.to_string());
e
}
#[test]
fn field_with_single_quote_is_parse_error() {
assert!(parse_query("serv'ice=x").is_err());
}
#[test]
fn field_with_semicolon_is_parse_error() {
assert!(parse_query("a;b=x").is_err());
}
#[test]
fn field_with_or_injection_payload_is_parse_error() {
assert!(parse_query("service' OR 1=1--=x").is_err());
}
#[test]
fn field_with_unicode_non_ascii_is_parse_error() {
assert!(parse_query("svc\u{2019}=x").is_err());
}
#[test]
fn value_injection_does_not_drop_table() {
let mut idx = Indexer::open_in_memory().unwrap();
idx.insert_batch(&[make_entry("2026-04-20T10:00:00Z", "error", "real row")])
.unwrap();
let ast = parse_query(r#"level="'; DROP TABLE log_entries--""#).unwrap();
let results = execute(&ast, idx.connection(), None).unwrap();
assert!(
results.is_empty(),
"adversarial value must not match the real row"
);
let stats = idx.stats().unwrap();
assert_eq!(stats.entries, 1, "original row must still be present");
}
#[test]
fn value_injection_yields_zero_results_not_all_rows() {
let mut idx = Indexer::open_in_memory().unwrap();
idx.insert_batch(&[
make_entry("2026-04-20T10:00:00Z", "info", "a"),
make_entry("2026-04-20T10:00:01Z", "error", "b"),
])
.unwrap();
let ast = parse_query(r#"level="1=1 OR 1=1""#).unwrap();
let results = execute(&ast, idx.connection(), None).unwrap();
assert!(
results.is_empty(),
"injection payload must not widen the result set"
);
}
#[test]
fn like_underscore_in_contains_is_literal() {
let mut idx = Indexer::open_in_memory().unwrap();
idx.insert_batch(&[
make_entry("2026-04-20T10:00:00Z", "info", "warn_threshold"),
make_entry("2026-04-20T10:00:01Z", "info", "warnXthreshold"),
])
.unwrap();
let ast = parse_query(r#"message contains "warn_threshold""#).unwrap();
let results = execute(&ast, idx.connection(), None).unwrap();
assert_eq!(results.len(), 1);
assert!(results[0].message.as_deref().unwrap().contains('_'));
}
#[test]
fn like_backslash_in_contains_is_literal() {
let mut idx = Indexer::open_in_memory().unwrap();
idx.insert_batch(&[
make_entry("2026-04-20T10:00:00Z", "info", r"path\to\file"),
make_entry("2026-04-20T10:00:01Z", "info", "unrelated message"),
])
.unwrap();
let ast = parse_query(r#"message contains "path\to""#).unwrap();
let results = execute(&ast, idx.connection(), None).unwrap();
assert_eq!(results.len(), 1);
assert!(results[0].message.as_deref().unwrap().contains('\\'));
}
#[test]
fn deep_or_1000_disjuncts_does_not_overflow() {
let query: String = (0..1000)
.map(|i| format!("level=v{i}"))
.collect::<Vec<_>>()
.join(" OR ");
let ast = parse_query(&query).expect("1000-disjunct query must parse");
let idx = Indexer::open_in_memory().unwrap();
let _ = execute(&ast, idx.connection(), None);
}
#[test]
fn long_line_10mb_ingested_gracefully() {
let mut idx = Indexer::open_in_memory().unwrap();
let big = "x".repeat(10 * 1024 * 1024);
let mut e = LogEntry::new(big.clone());
e.timestamp = Some("2026-04-20T10:00:00Z".to_string());
e.message = Some(big);
let _ = idx.insert_batch(&[e]); }