use crate::logging::redact::{REDACTED, contains_token, scrub};
const FAKE_TOKEN: &str = "bot1234567890:AAFakeTokenValueForTestingOnly12345";
fn leaking_log_line() -> String {
format!(
"Telegram: rich details edit failed for mid=MessageId(16685): error sending \
request for url (https://api.telegram.org/{FAKE_TOKEN}/editMessageText) \
— falling back to HTML"
)
}
#[test]
fn a_formatted_transport_error_carries_no_token() {
let line = leaking_log_line();
let scrubbed = scrub(&line);
assert!(
!contains_token(&scrubbed),
"token survived redaction: {scrubbed}"
);
assert!(
!scrubbed.contains("AAFakeTokenValueForTestingOnly12345"),
"the secret itself must be gone: {scrubbed}"
);
}
#[test]
fn the_bot_id_survives_so_the_line_stays_diagnosable() {
let line = leaking_log_line();
let scrubbed = scrub(&line);
assert!(scrubbed.contains("bot1234567890"), "{scrubbed}");
assert!(scrubbed.contains(REDACTED), "{scrubbed}");
assert!(scrubbed.contains("MessageId(16685)"));
assert!(scrubbed.contains("api.telegram.org"));
assert!(scrubbed.contains("editMessageText"));
}
#[test]
fn every_token_in_a_line_is_redacted() {
let two = format!("first {FAKE_TOKEN}/sendMessage then {FAKE_TOKEN}/editMessageText");
let scrubbed = scrub(&two);
assert!(!contains_token(&scrubbed), "{scrubbed}");
assert_eq!(scrubbed.matches(REDACTED).count(), 2, "{scrubbed}");
}
#[test]
fn an_ordinary_line_is_borrowed_untouched() {
let line = "Telegram send ok: kind=rich_edit chat=-100123 thread=None msg=42";
assert!(matches!(scrub(line), std::borrow::Cow::Borrowed(_)));
assert_eq!(scrub(line), line);
}
#[test]
fn prose_mentioning_a_bot_is_not_mangled() {
for line in [
"the bot is typing",
"bot1:ok",
"robot:starting",
"bot 1234567890: something happened",
] {
assert_eq!(scrub(line), line, "should be untouched: {line}");
}
}
#[test]
fn the_writer_reports_the_input_length_after_shrinking_a_line() {
use std::io::Write;
use tracing_subscriber::fmt::MakeWriter;
#[derive(Clone, Default)]
struct Captured(std::sync::Arc<std::sync::Mutex<Vec<u8>>>);
impl Write for Captured {
fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
self.0.lock().unwrap().extend_from_slice(buf);
Ok(buf.len())
}
fn flush(&mut self) -> std::io::Result<()> {
Ok(())
}
}
impl<'a> MakeWriter<'a> for Captured {
type Writer = Captured;
fn make_writer(&'a self) -> Self::Writer {
self.clone()
}
}
let sink = Captured::default();
let made = crate::logging::logger::ScrubbingWriter(sink.clone());
let line = leaking_log_line();
let written = made.make_writer().write(line.as_bytes()).unwrap();
assert_eq!(
written,
line.len(),
"must report the caller's length so write_all does not resend the tail"
);
let out = sink.0.lock().unwrap().clone();
let out = String::from_utf8(out).unwrap();
assert!(!contains_token(&out), "token reached the sink: {out}");
assert!(out.len() < line.len(), "redacted output should be shorter");
}