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}