1use std::collections::HashMap;
18use std::sync::Mutex;
19use std::sync::OnceLock;
20use std::sync::mpsc;
21
22use url::Url;
23
24use crate::Listener;
25use crate::Result;
26use crate::Transport;
27use crate::error::ClosedSnafu;
28use crate::error::MalformedUrlSnafu;
29use crate::error::MockDropSnafu;
30use crate::registry::TransportKind;
31
32static CHANNELS: OnceLock<Mutex<HashMap<String, mpsc::Sender<MockHandshake>>>> = OnceLock::new();
36
37static CONFIGS: OnceLock<Mutex<HashMap<String, MockConfig>>> = OnceLock::new();
41
42fn channels() -> &'static Mutex<HashMap<String, mpsc::Sender<MockHandshake>>> {
43 CHANNELS.get_or_init(|| Mutex::new(HashMap::new()))
44}
45
46fn configs() -> &'static Mutex<HashMap<String, MockConfig>> {
47 CONFIGS.get_or_init(|| Mutex::new(HashMap::new()))
48}
49
50#[derive(Clone, Debug, Default)]
52pub struct MockConfig {
53 pub drop_all: bool,
56 pub tamper: bool,
59}
60
61impl MockConfig {
62 pub fn install(name: &str, cfg: MockConfig) {
65 configs()
66 .lock()
67 .expect("mock config table poisoned")
68 .insert(name.to_string(), cfg);
69 }
70}
71
72struct MockHandshake {
73 connector_to_peer: mpsc::Receiver<Vec<u8>>,
74 peer_to_connector: mpsc::Sender<Vec<u8>>,
75}
76
77fn channel_name(url: &Url) -> Result<String> {
78 let name = url.host_str().unwrap_or("");
79 if name.is_empty() {
80 return MalformedUrlSnafu {
81 scheme: "mock",
82 url: url.to_string(),
83 reason: "missing channel name (use mock://<name>)",
84 }
85 .fail();
86 }
87 Ok(name.to_string())
88}
89
90fn config_for(name: &str) -> MockConfig {
91 configs()
92 .lock()
93 .expect("mock config table poisoned")
94 .get(name)
95 .cloned()
96 .unwrap_or_default()
97}
98
99pub struct MockTransport {
101 tx: Option<mpsc::Sender<Vec<u8>>>,
102 rx: Option<mpsc::Receiver<Vec<u8>>>,
103 name: String,
104 cfg: MockConfig,
105 pending: Vec<u8>,
106 pub sent_log: Vec<Vec<u8>>,
108 pub recv_log: Vec<Vec<u8>>,
110}
111
112impl MockTransport {
113 fn new(
114 tx: mpsc::Sender<Vec<u8>>,
115 rx: mpsc::Receiver<Vec<u8>>,
116 name: String,
117 cfg: MockConfig,
118 ) -> Self {
119 Self {
120 tx: Some(tx),
121 rx: Some(rx),
122 name,
123 cfg,
124 pending: Vec::new(),
125 sent_log: Vec::new(),
126 recv_log: Vec::new(),
127 }
128 }
129
130 pub fn name(&self) -> &str {
132 &self.name
133 }
134}
135
136impl Transport for MockTransport {
137 fn send(&mut self, data: &[u8]) -> Result<()> {
138 self.sent_log.push(data.to_vec());
139 if self.cfg.drop_all {
140 let tx = match &self.tx {
145 Some(tx) => tx,
146 None => return ClosedSnafu.fail(),
147 };
148 return tx.send(Vec::new()).map_err(|_| ClosedSnafu.build());
149 }
150 let mut payload = data.to_vec();
151 if self.cfg.tamper {
152 for byte in &mut payload {
153 *byte ^= 0xFF;
154 }
155 }
156 let tx = match &self.tx {
157 Some(tx) => tx,
158 None => return ClosedSnafu.fail(),
159 };
160 tx.send(payload).map_err(|_| ClosedSnafu.build())
161 }
162
163 fn recv(&mut self, buf: &mut [u8]) -> Result<usize> {
164 if self.pending.is_empty() {
165 let rx = match &self.rx {
166 Some(rx) => rx,
167 None => return ClosedSnafu.fail(),
168 };
169 let msg = rx.recv().map_err(|_| ClosedSnafu.build())?;
170 if self.cfg.drop_all {
171 return MockDropSnafu.fail();
172 }
173 self.recv_log.push(msg.clone());
174 self.pending = msg;
175 }
176 let n = std::cmp::min(self.pending.len(), buf.len());
177 buf[..n].copy_from_slice(&self.pending[..n]);
178 self.pending.drain(..n);
179 Ok(n)
180 }
181
182 fn close(&mut self) -> Result<()> {
183 self.tx = None;
184 self.rx = None;
185 self.pending.clear();
186 Ok(())
187 }
188}
189
190pub struct MockListener {
191 name: String,
192 inbox: Option<mpsc::Receiver<MockHandshake>>,
193}
194
195impl MockListener {
196 fn new(name: String, inbox: mpsc::Receiver<MockHandshake>) -> Self {
197 Self {
198 name,
199 inbox: Some(inbox),
200 }
201 }
202}
203
204impl Listener for MockListener {
205 fn accept(&mut self) -> Result<Box<dyn Transport>> {
206 let inbox = match &self.inbox {
207 Some(rx) => rx,
208 None => return ClosedSnafu.fail(),
209 };
210 let handshake = inbox.recv().map_err(|_| ClosedSnafu.build())?;
211 let cfg = config_for(&self.name);
212 Ok(Box::new(MockTransport::new(
213 handshake.peer_to_connector,
214 handshake.connector_to_peer,
215 self.name.clone(),
216 cfg,
217 )))
218 }
219}
220
221impl Drop for MockListener {
222 fn drop(&mut self) {
223 if let Ok(mut table) = channels().lock() {
224 table.remove(&self.name);
225 }
226 }
227}
228
229pub struct MockKind;
230
231impl TransportKind for MockKind {
232 fn schemes(&self) -> &'static [&'static str] {
233 &["mock"]
234 }
235
236 fn connect(&self, url: &Url) -> Result<Box<dyn Transport>> {
237 let name = channel_name(url)?;
238 let tx = {
239 let mut table = channels().lock().expect("mock channel table poisoned");
240 match table.remove(&name) {
241 Some(sender) => sender,
242 None => {
243 return MalformedUrlSnafu {
244 scheme: "mock",
245 url: url.to_string(),
246 reason: "no listener registered for this channel name",
247 }
248 .fail();
249 }
250 }
251 };
252 let (c2p_tx, c2p_rx) = mpsc::channel::<Vec<u8>>();
253 let (p2c_tx, p2c_rx) = mpsc::channel::<Vec<u8>>();
254 tx.send(MockHandshake {
255 connector_to_peer: c2p_rx,
256 peer_to_connector: p2c_tx,
257 })
258 .map_err(|_| ClosedSnafu.build())?;
259 let cfg = config_for(&name);
260 Ok(Box::new(MockTransport::new(c2p_tx, p2c_rx, name, cfg)))
261 }
262
263 fn listen(&self, url: &Url) -> Result<Box<dyn Listener>> {
264 let name = channel_name(url)?;
265 let (handshake_tx, handshake_rx) = mpsc::channel::<MockHandshake>();
266 let mut table = channels().lock().expect("mock channel table poisoned");
267 if table.contains_key(&name) {
268 return MalformedUrlSnafu {
269 scheme: "mock",
270 url: url.to_string(),
271 reason: "channel name already in use",
272 }
273 .fail();
274 }
275 table.insert(name.clone(), handshake_tx);
276 Ok(Box::new(MockListener::new(name, handshake_rx)))
277 }
278}
279
280crate::register_transport!(MockKind);
281
282#[cfg(test)]
283mod tests {
284 use super::*;
285
286 fn unique_name(tag: &str) -> String {
287 use std::sync::atomic::AtomicU64;
288 use std::sync::atomic::Ordering;
289 static SEQ: AtomicU64 = AtomicU64::new(0);
290 let n = SEQ.fetch_add(1, Ordering::Relaxed);
291 format!("mock-{tag}-{n}")
292 }
293
294 fn rendezvous(tag: &str) -> (Box<dyn Transport>, Box<dyn Transport>) {
295 let name = unique_name(tag);
296 let url = format!("mock://{name}");
297 let mut listener = crate::listen(&url).unwrap();
298 let client = crate::connect(&url).unwrap();
299 let server = listener.accept().unwrap();
300 (client, server)
301 }
302
303 #[test]
304 fn round_trip_preserves_order_and_payloads() {
305 let (mut client, mut server) = rendezvous("rt");
306
307 client.send(b"alpha").unwrap();
308 client.send(b"beta").unwrap();
309
310 let mut buf = [0u8; 16];
311 let n = server.recv(&mut buf).unwrap();
312 assert_eq!(&buf[..n], b"alpha");
313 let n = server.recv(&mut buf).unwrap();
314 assert_eq!(&buf[..n], b"beta");
315 }
316
317 #[test]
318 fn logs_record_traffic_for_directly_built_pair() {
319 let name = unique_name("log");
321 let cfg = MockConfig::default();
322 let (c2p_tx, c2p_rx) = mpsc::channel::<Vec<u8>>();
323 let (p2c_tx, p2c_rx) = mpsc::channel::<Vec<u8>>();
324 let mut client = MockTransport::new(c2p_tx, p2c_rx, name.clone(), cfg.clone());
325 let mut server = MockTransport::new(p2c_tx, c2p_rx, name, cfg);
326
327 client.send(b"one").unwrap();
328 client.send(b"two").unwrap();
329
330 let mut buf = [0u8; 16];
331 let _ = server.recv(&mut buf).unwrap();
332 let _ = server.recv(&mut buf).unwrap();
333
334 assert_eq!(client.sent_log.len(), 2);
335 assert_eq!(client.sent_log[0], b"one".to_vec());
336 assert_eq!(client.sent_log[1], b"two".to_vec());
337 assert_eq!(server.recv_log.len(), 2);
338 assert_eq!(server.recv_log[0], b"one".to_vec());
339 assert_eq!(server.recv_log[1], b"two".to_vec());
340 }
341
342 #[test]
343 fn drop_config_silences_receiver() {
344 let name = unique_name("drop");
345 MockConfig::install(
346 &name,
347 MockConfig {
348 drop_all: true,
349 tamper: false,
350 },
351 );
352 let url = format!("mock://{name}");
353 let mut listener = crate::listen(&url).unwrap();
354 let mut client = crate::connect(&url).unwrap();
355 let mut server = listener.accept().unwrap();
356
357 client.send(b"lost").unwrap();
360 let mut buf = [0u8; 16];
361 let err = server.recv(&mut buf).unwrap_err();
362 assert!(matches!(err, crate::error::Error::MockDrop { .. }));
363 }
364
365 #[test]
366 fn tamper_config_corrupts_payload() {
367 let name = unique_name("tamper");
368 MockConfig::install(
369 &name,
370 MockConfig {
371 drop_all: false,
372 tamper: true,
373 },
374 );
375 let url = format!("mock://{name}");
376 let mut listener = crate::listen(&url).unwrap();
377 let mut client = crate::connect(&url).unwrap();
378 let mut server = listener.accept().unwrap();
379
380 client.send(b"ABCDEFGH").unwrap();
381 let mut buf = [0u8; 16];
382 let n = server.recv(&mut buf).unwrap();
383 assert_eq!(n, 8);
384 let expected: Vec<u8> = b"ABCDEFGH".iter().map(|b| b ^ 0xFF).collect();
386 assert_eq!(&buf[..n], &expected[..]);
387 }
388
389 #[test]
390 fn default_config_passes_payloads_through_unchanged() {
391 let (mut client, mut server) = rendezvous("passthrough");
392 client.send(b"plain").unwrap();
393 let mut buf = [0u8; 16];
394 let n = server.recv(&mut buf).unwrap();
395 assert_eq!(&buf[..n], b"plain");
396 }
397}