Restructure into a library crate with modular bot and irc
Split the single-binary crate into a reusable `rustbot` library (src/lib.rs)
plus a thin supervisor binary. The monolithic irc.rs and bot.rs become module
folders:
irc/ message (wire parsing), connection (TCP/TLS transport), encoding
bot/ mod (state + event loop), caps (IRCv3 cap negotiation + SASL),
commands (message routing + command table)
config Config + layered loading, moved out of main.rs and bot.rs
main.rs is now just the per-network connect/reconnect supervisor over the
library API. Unit tests move out of the source files into tests/ integration
tests (config.rs, protocol.rs) that drive the public surface — prefix parsing
is now covered through Message::parse, and config layering through the new
public config::parse_str. Behavior is unchanged; no new dependencies.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
e7e805cb87
commit
eb3b8fec40
16 changed files with 1189 additions and 1116 deletions
55
tests/config.rs
Normal file
55
tests/config.rs
Normal file
|
|
@ -0,0 +1,55 @@
|
|||
//! Integration tests for config layering and `[network]` sections, exercising
|
||||
//! the public `rustbot::config::parse_str` (no filesystem or environment).
|
||||
|
||||
use rustbot::config::parse_str;
|
||||
|
||||
#[test]
|
||||
fn flat_config_is_a_single_network() {
|
||||
let cfgs = parse_str("server = irc.example.org\nnick = flatbot\nchannels = #a #b\n");
|
||||
assert_eq!(cfgs.len(), 1);
|
||||
assert_eq!(cfgs[0].server, "irc.example.org");
|
||||
assert_eq!(cfgs[0].nick, "flatbot");
|
||||
assert_eq!(cfgs[0].channels, vec!["#a", "#b"]);
|
||||
// The label defaults to the server host.
|
||||
assert_eq!(cfgs[0].name, "irc.example.org");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn sections_inherit_top_level_defaults_and_override() {
|
||||
let cfgs = parse_str(
|
||||
"nick = shared\nchannels = #lobby\n\
|
||||
[libera]\nserver = irc.libera.chat\ntls = true\n\
|
||||
[oftc]\nserver = irc.oftc.net\nnick = other\nchannels = #oftc\n",
|
||||
);
|
||||
assert_eq!(cfgs.len(), 2);
|
||||
|
||||
let libera = &cfgs[0];
|
||||
assert_eq!(libera.name, "libera");
|
||||
assert_eq!(libera.server, "irc.libera.chat");
|
||||
assert_eq!(libera.nick, "shared"); // inherited
|
||||
assert_eq!(libera.channels, vec!["#lobby"]); // inherited
|
||||
assert!(libera.tls);
|
||||
assert_eq!(libera.port, 6697); // TLS-port convention applied per network
|
||||
|
||||
let oftc = &cfgs[1];
|
||||
assert_eq!(oftc.name, "oftc");
|
||||
assert_eq!(oftc.server, "irc.oftc.net");
|
||||
assert_eq!(oftc.nick, "other"); // overridden
|
||||
assert_eq!(oftc.channels, vec!["#oftc"]); // overridden
|
||||
assert!(!oftc.tls); // default
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_section_may_rename_itself() {
|
||||
let cfgs = parse_str("[net]\nname = pretty\nserver = irc.example.org\n");
|
||||
assert_eq!(cfgs.len(), 1);
|
||||
assert_eq!(cfgs[0].name, "pretty");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn comments_and_blanks_are_ignored() {
|
||||
// A lone commented/blank file yields the built-in default single network.
|
||||
let cfgs = parse_str("# comment\n; also\n\n \n");
|
||||
assert_eq!(cfgs.len(), 1);
|
||||
assert_eq!(cfgs[0].server, "irc.tchatou.fr"); // built-in default
|
||||
}
|
||||
80
tests/protocol.rs
Normal file
80
tests/protocol.rs
Normal file
|
|
@ -0,0 +1,80 @@
|
|||
//! Integration tests for the IRC protocol layer, exercising the public
|
||||
//! `rustbot::irc` API: message parsing, IRCv3 tags, base64, and server-time.
|
||||
|
||||
use rustbot::irc::{base64_encode, parse_server_time, Message};
|
||||
|
||||
#[test]
|
||||
fn parses_privmsg_with_prefix() {
|
||||
let m = Message::parse(":alice!a@host PRIVMSG #chan :hi there\r\n").unwrap();
|
||||
assert_eq!(m.command, "PRIVMSG");
|
||||
assert_eq!(m.nick(), Some("alice"));
|
||||
assert_eq!(m.param(0), Some("#chan"));
|
||||
assert_eq!(m.trailing(), Some("hi there"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parses_ping_without_prefix() {
|
||||
let m = Message::parse("PING :LAG1234").unwrap();
|
||||
assert_eq!(m.command, "PING");
|
||||
assert!(m.prefix.is_none());
|
||||
assert_eq!(m.trailing(), Some("LAG1234"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parses_and_unescapes_tags() {
|
||||
let m = Message::parse("@id=123;key=a\\sb\\:c :n!u@h PRIVMSG #c :yo").unwrap();
|
||||
assert_eq!(m.tag("id"), Some("123"));
|
||||
assert_eq!(m.tag("key"), Some("a b;c"));
|
||||
assert_eq!(m.command, "PRIVMSG");
|
||||
assert_eq!(m.trailing(), Some("yo"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parses_numeric_reply() {
|
||||
let m = Message::parse(":srv 001 me :Welcome to the network").unwrap();
|
||||
assert_eq!(m.command, "001");
|
||||
assert_eq!(m.param(0), Some("me"));
|
||||
assert_eq!(m.trailing(), Some("Welcome to the network"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn full_prefix_is_split() {
|
||||
// Prefix parsing is exercised through the public `Message::parse`.
|
||||
let m = Message::parse(":nick!user@host.example PRIVMSG #c :hi").unwrap();
|
||||
let p = m.prefix.expect("prefix present");
|
||||
assert_eq!(p.nick.as_deref(), Some("nick"));
|
||||
assert_eq!(p.user.as_deref(), Some("user"));
|
||||
assert_eq!(p.host.as_deref(), Some("host.example"));
|
||||
assert_eq!(p.name(), "nick");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn blank_line_is_none() {
|
||||
assert!(Message::parse(" \r\n").is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn base64_matches_known_vectors() {
|
||||
assert_eq!(base64_encode(b""), "");
|
||||
assert_eq!(base64_encode(b"f"), "Zg==");
|
||||
assert_eq!(base64_encode(b"fo"), "Zm8=");
|
||||
assert_eq!(base64_encode(b"foobar"), "Zm9vYmFy");
|
||||
// A SASL PLAIN payload: authzid="" authcid="me" passwd="pw"
|
||||
assert_eq!(base64_encode(b"\0me\0pw"), "AG1lAHB3");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn server_time_parses_to_unix_seconds() {
|
||||
assert_eq!(parse_server_time("1970-01-01T00:00:00.000Z"), Some(0));
|
||||
assert_eq!(parse_server_time("2020-01-01T00:00:00Z"), Some(1_577_836_800));
|
||||
assert_eq!(parse_server_time("2021-01-01T00:00:00.123Z"), Some(1_609_459_200));
|
||||
assert_eq!(parse_server_time("not-a-timestamp"), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reads_message_tags_via_accessor() {
|
||||
let m = Message::parse("@time=2021-01-01T00:00:00Z;batch=42 :s PRIVMSG #c :hi").unwrap();
|
||||
assert_eq!(m.tag("batch"), Some("42"));
|
||||
assert_eq!(m.tag("time"), Some("2021-01-01T00:00:00Z"));
|
||||
assert_eq!(m.tag("missing"), None);
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue