Proof of concept mechanical port of ircnet/ircd to Rust as part of a bit about C being insecure for network services
0

Configure Feed

Select the types of activity you want to include in your feed.

ircd.rs / leveva / src / command / testutil.rs
14 kB 353 lines
1//! Shared test fixtures for the command-handler unit tests: an in-memory 2//! [`ServerContext`], a registered `alice` [`Registered`] client, dispatch 3//! drivers, and registry/channel seeding helpers. `#[cfg(test)]` only. 4 5use std::sync::Arc; 6 7use tokio::sync::mpsc::{unbounded_channel, UnboundedReceiver}; 8 9use crate::command::*; 10use crate::config::Config; 11use crate::registry::{Envelope, Mailbox}; 12 13/// A throwaway mailbox sender (receiver dropped) for claims we never deliver to. 14pub(crate) fn mb() -> Mailbox { 15 unbounded_channel().0 16} 17 18/// A stable UID per test nick, so a nick's registry claim and its channel 19/// membership agree on the same identity (mirroring what the real generator does). 20pub(crate) fn uid_for(nick: &str) -> Uid { 21 let suffix = match nick { 22 "alice" => "AAAAA", 23 "bob" => "AAAAB", 24 "carol" => "AAAAC", 25 "alice2" | "alicia" | "Alice" => "AAAAA", // a rename keeps alice's UID 26 // Any other nick gets a deterministic, fold-stable 5-char base-36 suffix derived 27 // from its case-folded bytes (FNV-1a). Lets multi-target/cap tests claim arbitrary 28 // nicks (t1..t6, a..f) without hand-maintaining a fixture for each. 29 other => return derive_uid(other), 30 }; 31 Uid::try_from(format!("0ABC{suffix}").as_str()).unwrap() 32} 33 34/// Deterministic `0ABC`+5-char-base36 UID for an arbitrary test nick (case-insensitive, 35/// so a fold-equal nick maps to the same identity, matching the real generator's intent). 36fn derive_uid(nick: &str) -> Uid { 37 const ALPHABET: &[u8; 36] = b"0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ"; 38 let mut h: u64 = 0xcbf29ce484222325; 39 for b in nick.bytes() { 40 h ^= b.to_ascii_lowercase() as u64; 41 h = h.wrapping_mul(0x100000001b3); 42 } 43 let mut suffix = [0u8; 5]; 44 for slot in suffix.iter_mut() { 45 *slot = ALPHABET[(h % 36) as usize]; 46 h /= 36; 47 } 48 Uid::try_from(format!("0ABC{}", std::str::from_utf8(&suffix).unwrap()).as_str()).unwrap() 49} 50 51/// Link a fake peer `0XYZ` (registered in both the network mirror and the [`PeerLinks`] 52/// routing table); returns the receiver so a test can read what we relay to that peer. 53/// Mirrors the `link_peer` helper in the `s2s::relay` tests. 54pub(crate) fn link_observed_peer(ctx: &ServerContext) -> UnboundedReceiver<Envelope> { 55 use crate::ident::{ServerName, Sid}; 56 use crate::s2s::network::RemoteServer; 57 use crate::s2s::PeerLink; 58 let (link, rx) = PeerLink::new( 59 Sid::try_from("0XYZ").unwrap(), 60 ServerName::try_from("peer.test").unwrap(), 61 ); 62 ctx.net.insert_server(RemoteServer { 63 sid: link.sid.clone(), 64 name: link.name.clone(), 65 description: "Peer".into(), 66 hopcount: 1, 67 uplink: link.sid.clone(), 68 parent: ctx.uids.sid().clone(), 69 }); 70 ctx.peers 71 .link(link.sid.clone(), link.mailbox_tx.clone(), link.caps); 72 rx 73} 74 75/// Like [`link_observed_peer`] but the peer advertised `message-tags` in its `CAPAB`, so leveva 76/// tags its wire (used by the message-ids S2S-propagation tests, slice 130). 77pub(crate) fn link_observed_tag_peer(ctx: &ServerContext) -> UnboundedReceiver<Envelope> { 78 use crate::ident::{ServerName, Sid}; 79 use crate::s2s::caps::PeerCaps; 80 use crate::s2s::network::RemoteServer; 81 use crate::s2s::PeerLink; 82 let (mut link, rx) = PeerLink::new( 83 Sid::try_from("0XYZ").unwrap(), 84 ServerName::try_from("peer.test").unwrap(), 85 ); 86 link.caps = PeerCaps { 87 message_tags: true, 88 ..Default::default() 89 }; 90 ctx.net.insert_server(RemoteServer { 91 sid: link.sid.clone(), 92 name: link.name.clone(), 93 description: "Peer".into(), 94 hopcount: 1, 95 uplink: link.sid.clone(), 96 parent: ctx.uids.sid().clone(), 97 }); 98 ctx.peers 99 .link(link.sid.clone(), link.mailbox_tx.clone(), link.caps); 100 rx 101} 102 103/// Seed a remote server into the network mirror with an explicit **tree parent** — for the 104/// `LINKS`/`MAP` topology tests. `sid`/`parent` are 4-char SID strings, `hop` the hopcount; 105/// the description is `"<name> info"`. `uplink` is set to `sid` (inert here — topology keys 106/// on `parent`, not `uplink`). 107pub(crate) fn seed_server(ctx: &ServerContext, sid: &str, name: &str, parent: &str, hop: u16) { 108 use crate::ident::{ServerName, Sid}; 109 use crate::s2s::network::RemoteServer; 110 ctx.net.insert_server(RemoteServer { 111 sid: Sid::try_from(sid).unwrap(), 112 name: ServerName::try_from(name).unwrap(), 113 description: format!("{name} info"), 114 hopcount: hop, 115 uplink: Sid::try_from(sid).unwrap(), 116 parent: Sid::try_from(parent).unwrap(), 117 }); 118} 119 120/// Claim `nick` (with its fixture UID) and an observable mailbox; returns the 121/// receiver so a test can assert what was delivered to that client. 122pub(crate) fn claim_observed(ctx: &ServerContext, nick: &str) -> UnboundedReceiver<Envelope> { 123 let (tx, rx) = unbounded_channel(); 124 ctx.registry 125 .try_claim(&uid_for(nick), nick, "u", "h", "real", tx) 126 .expect("free nick"); 127 rx 128} 129 130/// Like [`claim_observed`] but with an explicit `host` (so host-mask `$#` tests can 131/// register users with realistic dotted hosts). Returns the observable mailbox receiver. 132pub(crate) fn claim_with_host( 133 ctx: &ServerContext, 134 nick: &str, 135 host: &str, 136) -> UnboundedReceiver<Envelope> { 137 let (tx, rx) = unbounded_channel(); 138 ctx.registry 139 .try_claim(&uid_for(nick), nick, "u", host, "real", tx) 140 .expect("free nick"); 141 rx 142} 143 144pub(crate) fn ctx() -> Arc<ServerContext> { 145 let cfg = Config::from_kdl( 146 r#" 147 server { name "leveva.test"; description "T"; sid "0ABC" } 148 admin { name "A"; email "a@test"; network "TestNet" } 149 class "c" { ping-freq 90; max-links 10; sendq 1000 } 150 // The shared unit-test scenarios were authored against modeless channels 151 // (a non-op sets TOPIC, an outsider PRIVMSGs in, …); pin off the +nt default. 152 options { default-channel-modes "+" } 153 "#, 154 ) 155 .unwrap(); 156 ServerContext::from_config(&cfg, "FIXED".to_string(), None) 157} 158 159/// A context whose `default-channel-modes` is `spec` (e.g. `"+nt"`); everything else 160/// mirrors [`ctx`]. Used by the default-channel-modes slice to drive a server that 161/// stamps freshly-created channels. 162pub(crate) fn ctx_with_default_channel_modes(spec: &str) -> Arc<ServerContext> { 163 let cfg = Config::from_kdl(&format!( 164 r#" 165 server {{ name "leveva.test"; description "T"; sid "0ABC" }} 166 admin {{ name "A"; email "a@test"; network "TestNet" }} 167 class "c" {{ ping-freq 90; max-links 10; sendq 1000 }} 168 options {{ default-channel-modes "{spec}" }} 169 "# 170 )) 171 .unwrap(); 172 ServerContext::from_config(&cfg, "FIXED".to_string(), None) 173} 174 175/// A context with an explicit [`Admin`] block — used by the `ADMIN` tests to reach the 176/// empty-block `423` path (unreachable through `from_config`, which requires a non-empty 177/// `name`/`email`). Mirrors the `ctx()` fixture for every other field. 178pub(crate) fn ctx_with_admin(admin: crate::config::Admin) -> Arc<ServerContext> { 179 use crate::channel::Channels; 180 use crate::ident::Sid; 181 use crate::registry::Registry; 182 use crate::server::Counters; 183 use crate::uid::UidGenerator; 184 use crate::whowas::WhowasHistory; 185 Arc::new(ServerContext { 186 name: "leveva.test".to_string(), 187 description: "T".to_string(), 188 network: admin.network.clone(), 189 version: "leveva-0.0.0", 190 created: "FIXED".to_string(), 191 motd: None, 192 counters: Counters::default(), 193 registry: Registry::new(), 194 channels: Channels::new(), 195 whowas: WhowasHistory::default(), 196 uids: UidGenerator::new(Sid::try_from("0ABC").unwrap()), 197 default_user_modes: 0, 198 default_channel_modes: 0, 199 operators: Vec::new(), 200 stats_conf: crate::server::StatsConf::default(), 201 klines: crate::kline::KlineStore::new(), 202 dlines: crate::dline::DlineStore::new(), 203 net: crate::s2s::Network::new(), 204 peers: crate::s2s::PeerLinks::new(), 205 auth: crate::server::AuthConfig::default(), 206 admin, 207 monitors: crate::monitor::Monitors::new(), 208 conn_limits: crate::connlimit::ConnLimits::new(), 209 linking: crate::link::LinkingSet::new(), 210 knock_throttle: crate::knock_throttle::KnockThrottle::new(), 211 knock_user_throttle: crate::knock_throttle::KnockThrottle::new(), 212 resvs: crate::resv::ResvStore::new(), 213 history: crate::history::History::disabled(), 214 }) 215} 216 217/// A context whose `server.description` is set to `desc` (everything else mirrors 218/// [`ctx`]). Used by the `LINKS` tests to reach the empty-description `(Unknown Location)` 219/// branch. 220pub(crate) fn ctx_with_description(desc: &str) -> Arc<ServerContext> { 221 use crate::channel::Channels; 222 use crate::ident::Sid; 223 use crate::registry::Registry; 224 use crate::server::Counters; 225 use crate::uid::UidGenerator; 226 use crate::whowas::WhowasHistory; 227 Arc::new(ServerContext { 228 name: "leveva.test".to_string(), 229 description: desc.to_string(), 230 network: "TestNet".to_string(), 231 version: "leveva-0.0.0", 232 created: "FIXED".to_string(), 233 motd: None, 234 counters: Counters::default(), 235 registry: Registry::new(), 236 channels: Channels::new(), 237 whowas: WhowasHistory::default(), 238 uids: UidGenerator::new(Sid::try_from("0ABC").unwrap()), 239 default_user_modes: 0, 240 default_channel_modes: 0, 241 operators: Vec::new(), 242 stats_conf: crate::server::StatsConf::default(), 243 klines: crate::kline::KlineStore::new(), 244 dlines: crate::dline::DlineStore::new(), 245 net: crate::s2s::Network::new(), 246 peers: crate::s2s::PeerLinks::new(), 247 auth: crate::server::AuthConfig::default(), 248 admin: crate::config::Admin::default(), 249 monitors: crate::monitor::Monitors::new(), 250 conn_limits: crate::connlimit::ConnLimits::new(), 251 linking: crate::link::LinkingSet::new(), 252 knock_throttle: crate::knock_throttle::KnockThrottle::new(), 253 knock_user_throttle: crate::knock_throttle::KnockThrottle::new(), 254 resvs: crate::resv::ResvStore::new(), 255 history: crate::history::History::disabled(), 256 }) 257} 258 259pub(crate) fn ctx_with_motd(lines: Vec<String>) -> Arc<ServerContext> { 260 let cfg = Config::from_kdl( 261 r#" 262 server { name "leveva.test"; description "T"; sid "0ABC" } 263 admin { name "A"; email "a@test"; network "TestNet" } 264 class "c" { ping-freq 90; max-links 10; sendq 1000 } 265 "#, 266 ) 267 .unwrap(); 268 ServerContext::from_config(&cfg, "FIXED".to_string(), Some(lines)) 269} 270 271pub(crate) fn client() -> Registered { 272 Registered { 273 uid: uid_for("alice"), 274 nick: "alice".to_string(), 275 user: "alice".to_string(), 276 host: "127.0.0.1".to_string(), 277 realname: "Alice Realname".to_string(), 278 modes: 0, 279 privileges: Vec::new(), 280 caps: Default::default(), 281 } 282} 283 284pub(crate) fn run(ctx: &ServerContext, line: &str) -> Vec<Message> { 285 dispatch(&mut client(), &Message::parse(line).unwrap(), ctx) 286} 287 288/// Like [`run`] but the sender is `nick` (its `uid_for(nick)` identity), so a test can drive a 289/// command as a *different* client — e.g. to show a per-user gate distinguishes knockers. 290pub(crate) fn run_as(ctx: &ServerContext, nick: &str, line: &str) -> Vec<Message> { 291 let mut c = client(); 292 c.uid = uid_for(nick); 293 c.nick = nick.to_string(); 294 c.user = nick.to_string(); 295 dispatch(&mut c, &Message::parse(line).unwrap(), ctx) 296} 297 298/// Like [`run`] but the sender holds operator status (`+o`), so operator-gated paths 299/// (mask targets, WALLOPS, KILL, …) are exercised. 300pub(crate) fn run_oper(ctx: &ServerContext, line: &str) -> Vec<Message> { 301 let mut c = client(); 302 c.modes |= crate::mode::UserMode::Oper.bit(); 303 dispatch(&mut c, &Message::parse(line).unwrap(), ctx) 304} 305 306pub(crate) fn codes(ms: &[Message]) -> Vec<&str> { 307 ms.iter().map(|m| m.command()).collect() 308} 309 310/// Parse the next delivered wire line out of a mailbox receiver (panics on a control 311/// frame — use [`ejected`] for those). 312pub(crate) fn delivered(rx: &mut UnboundedReceiver<Envelope>) -> Message { 313 match rx.try_recv().expect("a frame was delivered") { 314 Envelope::Wire(bytes) => { 315 Message::parse(&String::from_utf8_lossy(&bytes)).expect("delivered line parses") 316 } 317 other => panic!("expected a wire frame, got {other:?}"), 318 } 319} 320 321/// The next raw wire frame (as a `String`, CRLF included) from a mailbox receiver — 322/// for asserting exact relayed bytes (e.g. a peer's `:<sid> MODE #chan +nt`). 323pub(crate) fn raw(rx: &mut UnboundedReceiver<Envelope>) -> String { 324 match rx.try_recv().expect("a frame was delivered") { 325 Envelope::Wire(bytes) => String::from_utf8(bytes).expect("utf8 wire frame"), 326 other => panic!("expected a wire frame, got {other:?}"), 327 } 328} 329 330/// Pull the next [`Envelope::Eject`] (a `KILL`) off a mailbox receiver, returning its 331/// raw wire bytes (the kill notice + ERROR) and the QUIT reason relayed to co-members. 332pub(crate) fn ejected(rx: &mut UnboundedReceiver<Envelope>) -> (Vec<u8>, String) { 333 match rx.try_recv().expect("a frame was delivered") { 334 Envelope::Eject { wire, reason } => (wire, reason), 335 other => panic!("expected an eject frame, got {other:?}"), 336 } 337} 338 339/// Claim `nick` in the registry and place its (fixture) UID into `chan`, returning 340/// the channel-creating side-effect (the first member becomes a chanop). 341pub(crate) fn claim_and_join(ctx: &ServerContext, nick: &str, chan: &str) { 342 let _ = claim_observed(ctx, nick); 343 ctx.channels.join(chan, &uid_for(nick), 0); 344} 345 346/// Force a channel flag on, acting as `actor` (must be a chanop of `chan`). 347pub(crate) fn force_flag(ctx: &ServerContext, chan: &str, actor: &str, mode: ChanMode) { 348 ctx.channels.apply_modes( 349 chan, 350 &uid_for(actor), 351 &[ModeChange::Flag { add: true, mode }], 352 ); 353}