use std::io::{self, Cursor, Read, Write}; use std::sync::{Arc, Mutex}; use rustbot::bot::Bot; use rustbot::config::Config; use rustbot::irc::Connection; struct Mock { input: Cursor>, output: Arc>>, } impl Read for Mock { fn read(&mut self, buf: &mut [u8]) -> io::Result { self.input.read(buf) } } impl Write for Mock { fn write(&mut self, buf: &[u8]) -> io::Result { self.output.lock().unwrap().extend_from_slice(buf); Ok(buf.len()) } fn flush(&mut self) -> io::Result<()> { Ok(()) } } fn run(server: &str) -> String { let output = Arc::new(Mutex::new(Vec::new())); let mock = Mock { input: Cursor::new(server.as_bytes().to_vec()), output: output.clone(), }; let conn = Connection::from_stream(mock); let mut bot = Bot::new(Config::default(), conn); let _ = bot.run(); let bytes = output.lock().unwrap().clone(); String::from_utf8_lossy(&bytes).into_owned() } #[test] fn sends_registration() { let out = run(""); assert!(out.contains("CAP LS"), "{out}"); assert!(out.contains("NICK rubot"), "{out}"); assert!(out.contains("USER rubot"), "{out}"); } #[test] fn answers_ping() { let out = run("PING :tok123\r\n"); assert!(out.contains("PONG :tok123"), "{out}"); } #[test] fn joins_configured_channels_on_welcome() { let out = run(":srv 001 rubot :welcome\r\n"); assert!(out.contains("JOIN #devs"), "{out}"); } #[test] fn appends_underscore_on_nick_in_use() { let out = run(":srv 433 * rubot :nickname is already in use\r\n"); assert!(out.contains("NICK rubot_"), "{out}"); } #[test] fn dispatches_command_to_module() { let out = run(":alice!a@h PRIVMSG #chan :!ping\r\n"); assert!(out.contains("PRIVMSG #chan :pong"), "{out}"); } #[test] fn ignores_own_echoed_message() { let out = run(":rubot!r@h PRIVMSG #chan :!ping\r\n"); assert!(!out.contains(":pong"), "{out}"); } #[test] fn rehash_applies_channel_nick_and_prefix() { let output = Arc::new(Mutex::new(Vec::new())); let mock = Mock { input: Cursor::new(Vec::new()), output: output.clone(), }; let conn = Connection::from_stream(mock); let cfg = Config { name: "testnet".to_string(), channels: vec!["#a".to_string(), "#keep".to_string()], ..Config::default() }; let mut bot = Bot::new(cfg, conn); let new = Config { name: "testnet".to_string(), channels: vec!["#keep".to_string(), "#b".to_string()], command_prefix: ">".to_string(), nick: "rubot2".to_string(), ..Config::default() }; let summary = bot.apply_config(new).unwrap(); let out = String::from_utf8_lossy(&output.lock().unwrap()).into_owned(); assert!(out.contains("JOIN #b"), "{out}"); assert!(out.contains("PART #a"), "{out}"); assert!(!out.contains("PART #keep"), "{out}"); assert!(out.contains("NICK rubot2"), "{out}"); assert!( summary.contains("+#b") && summary.contains("-#a"), "{summary}" ); assert!( summary.contains("prefix") && summary.contains("nick"), "{summary}" ); } #[test] fn rehash_command_requires_admin() { // no admin configured -> ignored (default prefix is !) let out = run(":alice!a@h PRIVMSG #chan :!rehash\r\n"); assert!( !out.contains("rehash:"), "unauthorized rehash replied: {out}" ); }