marionette/
lib.rs

1//! Minimal scripted MQTT v5 "broker" for driving protocol edge cases that a
2//! real broker will never produce (unsolicited packets, withheld CONNACKs,
3//! stalled sockets, exotic CONNACK properties).
4//!
5//! This is not a conforming broker: each test scripts the exact packets to
6//! read and write. Packets are encoded/decoded with `finmqtt-core`, so tests
7//! can construct and inspect them with the regular packet types.
8
9#![allow(
10    clippy::expect_used,
11    clippy::panic,
12    reason = "this is a test helper crate"
13)]
14
15use std::io::{self, ErrorKind};
16use std::time::Duration;
17
18use finmqtt_core::codec::{GrowBuffer, PacketReader};
19use finmqtt_core::packet::{Ack, Connack, Packet, ReasonCode, Suback};
20use finmqtt_core::property::{AckProperties, ConnackPropertiesBuf, Property, SubackProperties};
21use finmqtt_core::wire::{FixedHeader, PacketId, PacketType};
22use tokio::io::{AsyncReadExt, AsyncWriteExt};
23use tokio::net::{TcpListener, TcpStream};
24use tokio::time::timeout;
25
26/// Default deadline for blocking-style helpers ([`Broker::accept`],
27/// [`Conn::read_packet`]).
28const DEADLINE: Duration = Duration::from_secs(10);
29
30/// A TCP listener that hands out scripted MQTT connections.
31pub struct Broker {
32    listener: TcpListener,
33}
34
35impl Broker {
36    /// Binds to an ephemeral IPv6 port.
37    ///
38    /// Listening on `[::]` accepts IPv6 clients and, on the usual dual-stack
39    /// configuration, IPv4-mapped clients (e.g. `127.0.0.1`) too. On a rare
40    /// IPv6-only host, clients simply connect over IPv6.
41    ///
42    /// # Panics
43    ///
44    /// Panics if binding fails.
45    pub async fn bind() -> Self {
46        let listener = TcpListener::bind("[::]:0")
47            .await
48            .expect("failed to bind fake broker");
49        Self { listener }
50    }
51
52    /// The TCP port the fake broker is listening on.
53    ///
54    /// # Panics
55    ///
56    /// Panics if the listener's local address cannot be queried.
57    #[must_use]
58    pub fn port(&self) -> u16 {
59        self.listener
60            .local_addr()
61            .expect("failed to get local addr")
62            .port()
63    }
64
65    /// The loopback host a client should connect to, matching the bound
66    /// address family. Pair it with [`port`](Self::port) so test code never
67    /// has to assume IPv4 or dual-stack.
68    #[must_use]
69    pub fn host(&self) -> &'static str {
70        if self
71            .listener
72            .local_addr()
73            .map_or(true, |addr| addr.is_ipv6())
74        {
75            "::1"
76        } else {
77            "127.0.0.1"
78        }
79    }
80
81    /// Accepts the next client connection.
82    ///
83    /// # Panics
84    ///
85    /// Panics if no client connects within 10 s.
86    pub async fn accept(&self) -> Conn {
87        self.try_accept(DEADLINE)
88            .await
89            .expect("timed out waiting for a client connection")
90    }
91
92    /// Accepts the next client connection, returning `None` if no client
93    /// connects within `dur`.
94    ///
95    /// # Panics
96    ///
97    /// Panics on listener I/O errors.
98    pub async fn try_accept(&self, dur: Duration) -> Option<Conn> {
99        match timeout(dur, self.listener.accept()).await {
100            Ok(Ok((stream, _addr))) => Some(Conn {
101                stream,
102                reader: PacketReader::with_capacity(8 * 1024),
103            }),
104            Ok(Err(e)) => panic!("fake broker accept failed: {e}"),
105            Err(_elapsed) => None,
106        }
107    }
108}
109
110/// A single scripted broker-side connection.
111pub struct Conn {
112    stream: TcpStream,
113    reader: PacketReader<GrowBuffer>,
114}
115
116/// A packet received from the client: fixed header plus body bytes.
117///
118/// Use [`RecvPacket::decode`] to view it as a [`Packet`].
119pub struct RecvPacket {
120    pub header: FixedHeader,
121    pub body: Vec<u8>,
122}
123
124impl RecvPacket {
125    /// Decodes the packet, panicking on malformed data.
126    ///
127    /// # Panics
128    ///
129    /// Panics if the body cannot be decoded for the header's packet type.
130    #[must_use]
131    pub fn decode(&self) -> Packet<'_> {
132        Packet::decode(self.header, &self.body)
133            .unwrap_or_else(|e| panic!("client sent malformed {:?}: {e:?}", self.header))
134    }
135
136    /// The packet type from the fixed header.
137    #[must_use]
138    pub const fn packet_type(&self) -> PacketType {
139        self.header.packet_type
140    }
141}
142
143impl Conn {
144    /// Reads the next packet from the client.
145    ///
146    /// # Panics
147    ///
148    /// Panics if no packet arrives within 10 s or the connection closes.
149    pub async fn read_packet(&mut self) -> RecvPacket {
150        let result = timeout(DEADLINE, self.read_packet_or_eof())
151            .await
152            .expect("timed out waiting for a packet from the client");
153        result.expect("client closed the connection while a packet was expected")
154    }
155
156    /// Reads the next packet, returning `None` on timeout or if the client
157    /// closed the connection cleanly.
158    ///
159    /// # Panics
160    ///
161    /// Panics on I/O errors or if the connection drops mid-packet.
162    pub async fn try_read_packet(&mut self, dur: Duration) -> Option<RecvPacket> {
163        timeout(dur, self.read_packet_or_eof())
164            .await
165            .unwrap_or_default()
166    }
167
168    /// Reads one packet; `None` on a clean EOF before the next packet starts.
169    async fn read_packet_or_eof(&mut self) -> Option<RecvPacket> {
170        let mut chunk = [0u8; 1024];
171        loop {
172            match self.reader.peek() {
173                Ok(Some((header, total))) => {
174                    let body = self.reader.body(header, total).to_vec();
175                    self.reader.consume(total);
176                    return Some(RecvPacket { header, body });
177                }
178                Ok(None) => {}
179                Err(e) => panic!("client sent a malformed packet header: {e:?}"),
180            }
181            match self.stream.read(&mut chunk).await {
182                Ok(0) => {
183                    assert!(
184                        self.reader.buffered_len() == 0,
185                        "connection dropped mid packet"
186                    );
187                    return None;
188                }
189                Ok(n) => self
190                    .reader
191                    .feed(&chunk[..n])
192                    .expect("GrowBuffer never reports a capacity limit"),
193                Err(e) if e.kind() == ErrorKind::UnexpectedEof => return None,
194                Err(e) => panic!("fake broker read failed: {e}"),
195            }
196        }
197    }
198
199    /// Encodes and writes a packet to the client.
200    ///
201    /// # Panics
202    ///
203    /// Panics if encoding or the socket write fails.
204    pub async fn write_packet(&mut self, pkt: &Packet<'_>) {
205        let mut buf = vec![0u8; pkt.encoded_len().expect("packet too large to encode")];
206        let n = pkt.encode(&mut buf).expect("failed to encode packet");
207        self.write_raw(&buf[..n]).await;
208    }
209
210    /// Writes raw bytes to the client, bypassing packet encoding.
211    ///
212    /// Useful for hand-crafted or deliberately malformed wire data, e.g. a
213    /// fixed header that announces a body which is never sent.
214    ///
215    /// # Panics
216    ///
217    /// Panics if the socket write fails.
218    pub async fn write_raw(&mut self, bytes: &[u8]) {
219        self.try_write_raw(bytes)
220            .await
221            .expect("fake broker write failed");
222    }
223
224    /// Writes raw bytes to the client, returning any socket error instead of
225    /// panicking.
226    ///
227    /// Use this when the client is expected to apply backpressure or close
228    /// the connection, so the write failing (or never returning) is part of
229    /// what the test observes.
230    ///
231    /// # Errors
232    ///
233    /// Returns the underlying I/O error if the socket write fails.
234    pub async fn try_write_raw(&mut self, bytes: &[u8]) -> io::Result<()> {
235        self.stream.write_all(bytes).await
236    }
237
238    /// Reads the next packet and asserts it is a CONNECT.
239    ///
240    /// # Panics
241    ///
242    /// Panics if the next packet is not a CONNECT.
243    pub async fn expect_connect(&mut self) -> RecvPacket {
244        let pkt = self.read_packet().await;
245        assert_eq!(
246            pkt.packet_type(),
247            PacketType::Connect,
248            "expected CONNECT as the first packet"
249        );
250        pkt
251    }
252
253    /// Sends a successful CONNACK with the given properties.
254    ///
255    /// # Panics
256    ///
257    /// Panics if a property is not valid for CONNACK.
258    pub async fn send_connack(&mut self, session_present: bool, props: &[Property<'_>]) {
259        let buf = ConnackPropertiesBuf::try_from(props).expect("invalid CONNACK property");
260        self.write_packet(&Packet::Connack(Connack {
261            session_present,
262            reason_code: ReasonCode::Success,
263            properties: buf.as_properties(),
264        }))
265        .await;
266    }
267
268    /// Performs the broker side of the connection handshake: reads CONNECT,
269    /// replies with a successful CONNACK (`session_present = false`) carrying
270    /// the given properties.
271    pub async fn handshake(&mut self, props: &[Property<'_>]) {
272        self.expect_connect().await;
273        self.send_connack(false, props).await;
274    }
275
276    /// Acknowledges a `QoS` 1 PUBLISH with a successful PUBACK.
277    pub async fn send_puback(&mut self, packet_id: PacketId) {
278        self.write_packet(&Packet::Puback(Ack {
279            packet_id,
280            reason_code: ReasonCode::Success,
281            properties: AckProperties::EMPTY,
282        }))
283        .await;
284    }
285
286    /// Acknowledges a SUBSCRIBE with one reason code per requested filter.
287    pub async fn send_suback(&mut self, packet_id: PacketId, reason_codes: &[ReasonCode]) {
288        self.write_packet(&Packet::Suback(Suback {
289            packet_id,
290            properties: SubackProperties::EMPTY,
291            reason_codes,
292        }))
293        .await;
294    }
295
296    /// Reads packets until a PUBLISH arrives, transparently answering
297    /// PINGREQ with PINGRESP. Returns the PUBLISH packet.
298    ///
299    /// # Panics
300    ///
301    /// Panics if a packet other than PUBLISH or PINGREQ arrives, or if no
302    /// PUBLISH arrives within 10 s.
303    pub async fn expect_publish(&mut self) -> RecvPacket {
304        self.expect_packet(PacketType::Publish).await
305    }
306
307    /// Reads packets until a SUBSCRIBE arrives, transparently answering
308    /// PINGREQ with PINGRESP. Returns the SUBSCRIBE packet.
309    ///
310    /// # Panics
311    ///
312    /// Panics if a packet other than SUBSCRIBE or PINGREQ arrives, or if no
313    /// SUBSCRIBE arrives within 10 s.
314    pub async fn expect_subscribe(&mut self) -> RecvPacket {
315        self.expect_packet(PacketType::Subscribe).await
316    }
317
318    /// Reads packets until one of type `want` arrives, transparently
319    /// answering PINGREQ with PINGRESP.
320    ///
321    /// # Panics
322    ///
323    /// Panics if a packet other than `want` or PINGREQ arrives, or if no
324    /// matching packet arrives within 10 s.
325    async fn expect_packet(&mut self, want: PacketType) -> RecvPacket {
326        loop {
327            let pkt = self.read_packet().await;
328            let got = pkt.packet_type();
329            if got == want {
330                return pkt;
331            }
332            match got {
333                PacketType::Pingreq => self.write_packet(&Packet::Pingresp).await,
334                other => panic!("expected {want:?}, got {other:?}"),
335            }
336        }
337    }
338}