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, Message, Receive, 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. Connecting,
10/// subscribing, and receiving pass straight through to the wrapped transport;
11/// only sends are failed.
12///
13/// # Examples
14///
15/// ```
16/// use pamoja_core::Transport;
17/// use pamoja_loopback::{Faulty, LoopbackBroker, LoopbackTransport};
18///
19/// # async fn run() -> pamoja_core::Result<()> {
20/// let broker = LoopbackBroker::new();
21/// let mut node = Faulty::new(LoopbackTransport::new(broker), 1);
22/// node.connect().await?;
23///
24/// // The first send fails, simulating a dropped link; the next succeeds.
25/// assert!(node.send("t", b"x").await.is_err());
26/// node.send("t", b"x").await?;
27/// # Ok(())
28/// # }
29/// ```
30pub struct Faulty<T> {
31    inner: T,
32    upcoming_failures: usize,
33}
34
35impl<T> Faulty<T> {
36    /// Wraps `inner`, failing its next `failures` sends before passing through.
37    ///
38    /// # Arguments
39    ///
40    /// * `inner` - the transport to decorate.
41    /// * `failures` - how many of the next [`send`](Transport::send) calls fail.
42    ///
43    /// # Returns
44    ///
45    /// A decorator that injects the requested failures, then delegates to `inner`.
46    pub fn new(inner: T, failures: usize) -> Self {
47        Self {
48            inner,
49            upcoming_failures: failures,
50        }
51    }
52
53    /// Arms the decorator to fail the next `count` sends.
54    ///
55    /// # Arguments
56    ///
57    /// * `count` - the number of upcoming sends to fail, simulating another link
58    ///   outage.
59    pub fn fail_next(&mut self, count: usize) {
60        self.upcoming_failures = count;
61    }
62}
63
64impl<T: Transport + Send> Transport for Faulty<T> {
65    async fn connect(&mut self) -> Result<()> {
66        self.inner.connect().await
67    }
68
69    async fn send(&mut self, topic: &str, payload: &[u8]) -> Result<()> {
70        if self.upcoming_failures > 0 {
71            self.upcoming_failures -= 1;
72            return Err(Error::Transport("simulated link failure".to_owned()));
73        }
74        self.inner.send(topic, payload).await
75    }
76
77    async fn subscribe(&mut self, topic: &str) -> Result<()> {
78        self.inner.subscribe(topic).await
79    }
80}
81
82impl<T: Receive + Send> Receive for Faulty<T> {
83    async fn recv(&mut self) -> Result<Option<Message>> {
84        self.inner.recv().await
85    }
86}
87
88#[cfg(test)]
89mod tests {
90    use super::*;
91    use crate::{LoopbackBroker, LoopbackTransport};
92
93    #[tokio::test]
94    async fn fails_the_configured_sends_then_passes_through() {
95        let broker = LoopbackBroker::new();
96        let mut gateway = LoopbackTransport::new(broker.clone());
97        gateway.connect().await.expect("connect");
98        gateway.subscribe("#").await.expect("subscribe");
99
100        let mut node = Faulty::new(LoopbackTransport::new(broker), 2);
101        node.connect().await.expect("connect");
102
103        assert!(node.send("t", b"1").await.is_err());
104        assert!(node.send("t", b"2").await.is_err());
105        node.send("t", b"3")
106            .await
107            .expect("third send passes through");
108
109        // Only the delivered payload reaches the gateway.
110        let message = gateway.recv().await.expect("recv").expect("a message");
111        assert_eq!(message.payload, b"3");
112    }
113}