Proof of concept mechanical port of ircnet/ircd to Rust as part of a bit about C being insecure for network services
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}