use std::sync::Arc;
use std::time::{Duration, Instant};
use tokio::io::{AsyncBufReadExt, AsyncWriteExt, BufReader};
use tokio::net::TcpListener;
use tokio::sync::mpsc;
use ircbot::make_messages;
#[test]
fn test_make_messages_short_text() {
let lines = make_messages("PRIVMSG #chan :", "hello world", "");
assert_eq!(lines, vec!["PRIVMSG #chan :hello world\r\n"]);
}
#[test]
fn test_make_messages_empty_text() {
let lines = make_messages("PRIVMSG #chan :", "", "");
assert_eq!(lines, vec!["PRIVMSG #chan :\r\n"]);
}
#[test]
fn test_make_messages_long_text_splits() {
let text = "a".repeat(1_000);
let header = "PRIVMSG #chan :";
let lines = make_messages(header, &text, "");
assert!(
lines.len() >= 3,
"expected ≥ 3 lines for 1 000-char text, got {}",
lines.len()
);
for (i, line) in lines.iter().enumerate() {
assert!(
line.len() <= 512,
"line {i} is {} bytes (> 512)",
line.len()
);
}
let recovered: String = lines
.iter()
.map(|l| l.strip_prefix(header).unwrap().trim_end_matches("\r\n"))
.collect::<Vec<_>>()
.join("");
assert_eq!(recovered, text);
}
#[test]
fn test_make_messages_prefers_word_boundary() {
let text = format!("{} {}", "a".repeat(400), "b".repeat(200));
let header = "PRIVMSG #chan :";
let lines = make_messages(header, &text, "");
assert_eq!(lines.len(), 2, "expected 2 lines, got {}", lines.len());
let first = lines[0]
.strip_prefix(header)
.unwrap()
.trim_end_matches("\r\n");
assert!(
first.ends_with('a'),
"first chunk should end at the word boundary (all 'a's), got: {first:?}"
);
}
#[test]
fn test_make_messages_ctcp_action_suffix() {
let header = "PRIVMSG #chan :\x01ACTION ";
let suffix = "\x01";
let text = "x".repeat(1_000);
let lines = make_messages(header, &text, suffix);
for (i, line) in lines.iter().enumerate() {
assert!(
line.len() <= 512,
"ACTION line {i} is {} bytes (> 512)",
line.len()
);
assert!(
line.ends_with("\x01\r\n"),
"ACTION line {i} must end with \\x01\\r\\n"
);
}
}
#[test]
fn test_make_messages_utf8_boundary() {
let header = "PRIVMSG #chan :";
let text = "é".repeat(300);
let lines = make_messages(header, &text, "");
for (i, line) in lines.iter().enumerate() {
assert!(
line.len() <= 512,
"line {i} is {} bytes (> 512)",
line.len()
);
assert!(
std::str::from_utf8(line.as_bytes()).is_ok(),
"line {i} is not valid UTF-8"
);
}
}
#[test]
fn test_make_messages_zero_available_bytes() {
let header = "X".repeat(509);
let suffix = "S";
let lines = make_messages(&header, "some text", suffix);
assert_eq!(lines.len(), 1);
assert_eq!(lines[0], format!("{header}{suffix}\r\n"));
}
#[test]
fn test_make_messages_exact_fit() {
let header = "PRIVMSG #chan :";
let overhead = header.len() + 2; let available = 510 - overhead; let text = "a".repeat(available);
let lines = make_messages(header, &text, "");
assert_eq!(lines.len(), 1);
assert_eq!(lines[0], format!("{header}{text}\r\n"));
}
#[tokio::test]
async fn test_flood_control_rate_limits_messages() {
use ircbot::State;
let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
let addr = listener.local_addr().unwrap().to_string();
let (ts_tx, mut ts_rx) = mpsc::unbounded_channel::<Instant>();
tokio::spawn(async move {
let (sock, _) = listener.accept().await.unwrap();
let (read_half, mut write_half) = sock.into_split();
let _ = write_half
.write_all(b":server 001 testbot :Welcome\r\n")
.await;
let mut reader = BufReader::new(read_half).lines();
while let Ok(Some(line)) = reader.next_line().await {
if line.starts_with("PRIVMSG") {
let _ = ts_tx.send(Instant::now());
}
}
});
let state = State::connect("testbot".to_string(), &addr, vec![])
.await
.expect("failed to connect to mock server")
.with_flood_control(2, Duration::from_millis(200));
use ircbot::{handler::HandlerEntry, handler::Trigger, BoxFuture, Context};
let handler: HandlerEntry<()> = HandlerEntry {
trigger: Trigger::Event {
event: "001".to_string(),
target: None,
regex: None,
},
handler: Box::new(|_bot: Arc<()>, ctx: Context| -> BoxFuture<ircbot::Result> {
Box::pin(async move {
for i in 0..6_u32 {
ctx.say(format!("message {i}"))?;
}
Ok(())
})
}),
};
let bot = Arc::new(());
let bot_task = tokio::spawn(ircbot::internal::run_bot(bot, state, vec![handler]));
let mut timestamps = Vec::new();
let collect = tokio::time::timeout(Duration::from_secs(10), async {
while timestamps.len() < 6 {
if let Some(ts) = ts_rx.recv().await {
timestamps.push(ts);
}
}
});
collect
.await
.expect("did not receive all 6 messages within 10 s");
bot_task.abort();
let elapsed = timestamps
.last()
.unwrap()
.duration_since(*timestamps.first().unwrap());
assert!(
elapsed >= Duration::from_millis(700), "flood control did not delay messages enough: elapsed = {elapsed:?}"
);
}