Skip to main content

pamoja_loopback/
faulty.rs

1//! A transport decorator that injects send failures for degraded-link testing.
2
3use pamoja_core::{Error, Result, Transport};
4
5/// Wraps a [`Transport`] and fails a configurable number of upcoming sends.
6///
7/// This simulates an intermittent link so offline-first behavior can be proven
8/// rather than assumed: pair it with a store-and-forward drain and assert that
9/// every record still arrives, in order, once the link recovers.
10///
11/// # Examples
12///
13/// ```
14/// use pamoja_core::Transport;
15/// use pamoja_loopback::{Faulty, LoopbackBroker, LoopbackTransport};
16///
17/// # async fn run() -> pamoja_core::Result<()> {
18/// let broker = LoopbackBroker::new();
19/// let mut node = Faulty::new(LoopbackTransport::new(broker), 1);
20/// node.connect().await?;
21///
22/// // The first send fails, simulating a dropped link; the next succeeds.
23/// assert!(node.send("t", b"x").await.is_err());
24/// node.send("t", b"x").await?;
25/// # Ok(())
26/// # }
27/// ```
28pub struct Faulty<T> {
29    inner: T,
30    upcoming_failures: usize,
31}
32
33impl<T> Faulty<T> {
34    /// Wraps `inner`, failing its next `failures` sends before passing through.
35    ///
36    /// # Arguments
37    ///
38    /// * `inner` - the transport to decorate.
39    /// * `failures` - how many of the next [`send`](Transport::send) calls fail.
40    ///
41    /// # Returns
42    ///
43    /// A decorator that injects the requested failures, then delegates to `inner`.
44    pub fn new(inner: T, failures: usize) -> Self {
45        Self {
46            inner,
47            upcoming_failures: failures,
48        }
49    }
50
51    /// Arms the decorator to fail the next `count` sends.
52    ///
53    /// # Arguments
54    ///
55    /// * `count` - the number of upcoming sends to fail, simulating another link
56    ///   outage.
57    pub fn fail_next(&mut self, count: usize) {
58        self.upcoming_failures = count;
59    }
60}
61
62impl<T: Transport> Transport for Faulty<T> {
63    async fn connect(&mut self) -> Result<()> {
64        self.inner.connect().await
65    }
66
67    async fn send(&mut self, topic: &str, payload: &[u8]) -> Result<()> {
68        if self.upcoming_failures > 0 {
69            self.upcoming_failures -= 1;
70            return Err(Error::Transport("simulated link failure".to_owned()));
71        }
72        self.inner.send(topic, payload).await
73    }
74
75    async fn subscribe(&mut self, topic: &str) -> Result<()> {
76        self.inner.subscribe(topic).await
77    }
78}
79
80#[cfg(test)]
81mod tests {
82    use super::*;
83    use crate::{LoopbackBroker, LoopbackTransport};
84
85    #[tokio::test]
86    async fn fails_the_configured_sends_then_passes_through() {
87        let broker = LoopbackBroker::new();
88        let mut gateway = LoopbackTransport::new(broker.clone());
89        gateway.connect().await.expect("connect");
90        gateway.subscribe("#").await.expect("subscribe");
91
92        let mut node = Faulty::new(LoopbackTransport::new(broker), 2);
93        node.connect().await.expect("connect");
94
95        assert!(node.send("t", b"1").await.is_err());
96        assert!(node.send("t", b"2").await.is_err());
97        node.send("t", b"3")
98            .await
99            .expect("third send passes through");
100
101        // Only the delivered payload reaches the gateway.
102        let message = gateway.recv().await.expect("recv").expect("a message");
103        assert_eq!(message.payload, b"3");
104    }
105}