Skip to main content

actioncable/
test_support.rs

1//! In-memory connections a test can play the server on, and a made-up protocol that stamps
2//! everything it encodes so a test can tell which protocol the client settled on. Behind the
3//! `test-support` feature, for this crate's own tests and for the tests of anything built on
4//! it.
5//!
6//! Everything here panics rather than returning an error: a test that can't have what it
7//! asked for has nothing left to assert.
8
9#![allow(
10    clippy::unwrap_used,
11    clippy::expect_used,
12    reason = "a fake for tests says what went wrong by failing the test"
13)]
14
15use std::collections::{HashSet, VecDeque};
16use std::io;
17use std::sync::atomic::{AtomicBool, Ordering};
18use std::sync::{Arc, Mutex, MutexGuard, PoisonError};
19use std::time::Duration;
20
21use async_trait::async_trait;
22use serde::Deserialize;
23use tokio::sync::{mpsc, watch};
24
25use crate::error::Error;
26use crate::protocol::{Command, Incoming, Protocol};
27use crate::transport::{Conn, DialOptions, Transport};
28use crate::v1_json::{SUBPROTOCOL_V1_JSON, V1Json};
29
30/// How long a test will hang around for something that should already have happened.
31pub const WAIT: Duration = Duration::from_secs(2);
32
33/// How long a test waits to be sure nothing is coming.
34const SILENCE: Duration = Duration::from_millis(100);
35
36/// A transport whose connections a test accepts and plays the server on.
37#[derive(Clone)]
38pub struct FakeTransport {
39    shared: Arc<TransportState>,
40}
41
42struct TransportState {
43    subprotocol: Mutex<Option<String>>,
44    write_buffer: Mutex<usize>,
45    dialed: mpsc::UnboundedSender<Arc<FakeConn>>,
46    accepting: tokio::sync::Mutex<mpsc::UnboundedReceiver<Arc<FakeConn>>>,
47    dial_errors: Mutex<VecDeque<String>>,
48    options: Mutex<Option<DialOptions>>,
49}
50
51impl FakeTransport {
52    /// A transport that answers every dial with a connection speaking
53    /// `actioncable-v1-json`.
54    pub fn new() -> FakeTransport {
55        let (dialed, accepting) = mpsc::unbounded_channel();
56        FakeTransport {
57            shared: Arc::new(TransportState {
58                subprotocol: Mutex::new(Some(SUBPROTOCOL_V1_JSON.to_string())),
59                write_buffer: Mutex::new(32),
60                dialed,
61                accepting: tokio::sync::Mutex::new(accepting),
62                dial_errors: Mutex::new(VecDeque::new()),
63                options: Mutex::new(None),
64            }),
65        }
66    }
67
68    /// What the fake server names as the negotiated subprotocol from now on.
69    pub fn speak(&self, subprotocol: Option<&str>) {
70        *lock(&self.shared.subprotocol) = subprotocol.map(str::to_string);
71    }
72
73    /// How many commands a connection takes before a write waits on the test to read them.
74    /// One, the smallest a channel can be, is how a test holds the client mid-write.
75    pub fn write_buffer(&self, commands: usize) {
76        *lock(&self.shared.write_buffer) = commands.max(1);
77    }
78
79    /// Turns the next dial down, as a refused connection would.
80    pub fn fail_next_dial(&self, reason: &str) {
81        lock(&self.shared.dial_errors).push_back(reason.to_string());
82    }
83
84    /// The options the client last dialed with.
85    pub fn dialed_with(&self) -> DialOptions {
86        lock(&self.shared.options)
87            .clone()
88            .expect("the client never dialed")
89    }
90
91    /// The next connection the client opened, to play the server on.
92    pub async fn accept(&self) -> Arc<FakeConn> {
93        tokio::time::timeout(WAIT, self.shared.accepting.lock().await.recv())
94            .await
95            .expect("no connection was dialed")
96            .unwrap()
97    }
98
99    /// Fails when the client dials at all in the next little while.
100    pub async fn refuse_dial(&self) {
101        let dialed = tokio::time::timeout(SILENCE, self.shared.accepting.lock().await.recv()).await;
102        if let Ok(Some(conn)) = dialed {
103            panic!(
104                "expected no connection, got one with subprotocol {:?}",
105                conn.subprotocol
106            );
107        }
108    }
109}
110
111impl Default for FakeTransport {
112    fn default() -> FakeTransport {
113        FakeTransport::new()
114    }
115}
116
117#[async_trait]
118impl Transport for FakeTransport {
119    async fn dial(&self, _url: &str, options: DialOptions) -> Result<Box<dyn Conn>, Error> {
120        *lock(&self.shared.options) = Some(options);
121        if let Some(reason) = lock(&self.shared.dial_errors).pop_front() {
122            return Err(Error::transport(io::Error::new(
123                io::ErrorKind::ConnectionRefused,
124                reason,
125            )));
126        }
127
128        let conn = Arc::new(FakeConn::new(
129            lock(&self.shared.subprotocol).clone(),
130            *lock(&self.shared.write_buffer),
131        ));
132        self.shared.dialed.send(Arc::clone(&conn)).unwrap();
133        Ok(Box::new(ClientEnd(conn)))
134    }
135}
136
137/// The server's end of one connection. Like Rails it keeps the identifiers it has confirmed
138/// and ignores a second subscribe for one of them, so a test reads off the wire exactly what
139/// a Rails server would act on, in the order the test acts on it.
140pub struct FakeConn {
141    subprotocol: Option<String>,
142    incoming: mpsc::Sender<Vec<u8>>,
143    reading: tokio::sync::Mutex<mpsc::Receiver<Vec<u8>>>,
144    outgoing: mpsc::Sender<Vec<u8>>,
145    sent: tokio::sync::Mutex<mpsc::Receiver<Vec<u8>>>,
146    /// Counts the writes that have begun, so a test can tell the client is stuck in one
147    /// before anyone has read what it wrote.
148    writes: watch::Sender<usize>,
149    confirmed: Mutex<HashSet<String>>,
150    failing_writes: AtomicBool,
151    closed: watch::Sender<bool>,
152}
153
154impl FakeConn {
155    fn new(subprotocol: Option<String>, write_buffer: usize) -> FakeConn {
156        let (incoming, reading) = mpsc::channel(1);
157        let (outgoing, sent) = mpsc::channel(write_buffer);
158        FakeConn {
159            subprotocol,
160            incoming,
161            reading: tokio::sync::Mutex::new(reading),
162            outgoing,
163            sent: tokio::sync::Mutex::new(sent),
164            writes: watch::Sender::new(0),
165            confirmed: Mutex::new(HashSet::new()),
166            failing_writes: AtomicBool::new(false),
167            closed: watch::Sender::new(false),
168        }
169    }
170
171    /// Plays a server frame to the client.
172    pub async fn push(&self, frame: &str) {
173        let mut closed = self.closed.subscribe();
174        tokio::select! {
175            biased;
176            _ = closed.wait_for(|closed| *closed) => panic!("connection closed before {frame} could be sent"),
177            sent = tokio::time::timeout(WAIT, self.incoming.send(frame.as_bytes().to_vec())) => {
178                sent.unwrap_or_else(|_| panic!("client never read {frame}")).unwrap_or_else(|_| panic!("client hung up before reading {frame}"));
179            }
180        }
181    }
182
183    /// Plays the welcome that makes the connection usable.
184    pub async fn welcome(&self) {
185        self.push(r#"{"type":"welcome"}"#).await;
186    }
187
188    /// Confirms a subscription, and remembers it the way Rails does.
189    pub async fn confirm(&self, identifier: &str) {
190        lock(&self.confirmed).insert(identifier.to_string());
191        self.push(&format!(
192            r#"{{"type":"confirm_subscription","identifier":{}}}"#,
193            quote(identifier)
194        ))
195        .await;
196    }
197
198    /// Turns a subscription down, which also forgets it: the client is free to try again.
199    pub async fn reject(&self, identifier: &str) {
200        lock(&self.confirmed).remove(identifier);
201        self.push(&format!(
202            r#"{{"type":"reject_subscription","identifier":{}}}"#,
203            quote(identifier)
204        ))
205        .await;
206    }
207
208    /// Plays a channel message to the client, as the server broadcasts one to `identifier`.
209    pub async fn broadcast(&self, identifier: &str, message: &str) {
210        self.push(&format!(
211            r#"{{"identifier":{},"message":{message}}}"#,
212            quote(identifier)
213        ))
214        .await;
215    }
216
217    /// Every write from now on fails, the way a socket the peer has stopped reading does
218    /// once the write timeout passes.
219    pub fn fail_writes(&self) {
220        self.failing_writes.store(true, Ordering::SeqCst);
221    }
222
223    /// Waits until `count` writes have begun. With a write buffer smaller than that, the
224    /// last of them is still waiting for the test to read what came before it.
225    pub async fn writing(&self, count: usize) {
226        let mut writes = self.writes.subscribe();
227        tokio::time::timeout(WAIT, writes.wait_for(|writes| *writes >= count))
228            .await
229            .expect("the client wrote less than that")
230            .unwrap();
231    }
232
233    /// The next payload the client wrote that the server acts on, exactly as it went out.
234    pub async fn sent(&self) -> Vec<u8> {
235        self.next_acted_on(WAIT).await.expect("client sent nothing")
236    }
237
238    /// Reads what the client wrote until something Rails would act on: a second subscribe
239    /// for an identifier already confirmed is skipped, the way Rails skips it, and an
240    /// unsubscribe makes room for the next subscribe.
241    async fn next_acted_on(&self, wait: Duration) -> Option<Vec<u8>> {
242        let mut sent = self.sent.lock().await;
243        loop {
244            match tokio::time::timeout(wait, sent.recv()).await {
245                Ok(Some(payload)) if self.acts_on(&payload) => break Some(payload),
246                Ok(Some(_ignored)) => {}
247                Ok(None) | Err(_) => break None,
248            }
249        }
250    }
251
252    fn acts_on(&self, payload: &[u8]) -> bool {
253        match serde_json::from_slice::<SentCommand>(payload) {
254            Ok(command) if command.command == "subscribe" => {
255                !lock(&self.confirmed).contains(&command.identifier)
256            }
257            Ok(command) if command.command == "unsubscribe" => {
258                lock(&self.confirmed).remove(&command.identifier);
259                true
260            }
261            _ => true,
262        }
263    }
264
265    /// The next command the client sent, decoded.
266    pub async fn command(&self) -> SentCommand {
267        let payload = self.sent().await;
268        serde_json::from_slice(&payload).unwrap_or_else(|error| {
269            panic!(
270                "decoding command {}: {error}",
271                String::from_utf8_lossy(&payload)
272            )
273        })
274    }
275
276    /// The next command, which has to be the one named. Taking a subscribe is not
277    /// answering it: until [`confirm`](FakeConn::confirm), the server is as good as having
278    /// dropped it on the floor and the client's guarantor will send it again.
279    pub async fn expect_command(&self, name: &str, identifier: &str) -> SentCommand {
280        let command = self.command().await;
281        assert_eq!(
282            name, command.command,
283            "expected {name} {identifier}, got {command:?}"
284        );
285        assert_eq!(
286            identifier, command.identifier,
287            "expected {name} {identifier}, got {command:?}"
288        );
289        command
290    }
291
292    /// Fails when the client sends anything in the next little while.
293    pub async fn expect_silence(&self) {
294        if let Some(payload) = self.next_acted_on(SILENCE).await {
295            panic!(
296                "expected no command, got {}",
297                String::from_utf8_lossy(&payload)
298            );
299        }
300    }
301
302    /// Hangs up from the server's side.
303    pub fn close(&self) {
304        self.closed.send_replace(true);
305    }
306}
307
308struct ClientEnd(Arc<FakeConn>);
309
310#[async_trait]
311impl Conn for ClientEnd {
312    fn subprotocol(&self) -> Option<&str> {
313        self.0.subprotocol.as_deref()
314    }
315
316    async fn read(&self) -> Result<Vec<u8>, Error> {
317        let mut closed = self.0.closed.subscribe();
318        let mut reading = self.0.reading.lock().await;
319        tokio::select! {
320            biased;
321            _ = closed.wait_for(|closed| *closed) => Err(eof()),
322            payload = reading.recv() => payload.ok_or_else(eof),
323        }
324    }
325
326    async fn write(&self, payload: &[u8]) -> Result<(), Error> {
327        if self.0.failing_writes.load(Ordering::SeqCst) {
328            return Err(Error::transport(io::Error::new(
329                io::ErrorKind::TimedOut,
330                "the peer stopped reading",
331            )));
332        }
333        self.0.writes.send_modify(|writes| *writes += 1);
334
335        let mut closed = self.0.closed.subscribe();
336        tokio::select! {
337            biased;
338            _ = closed.wait_for(|closed| *closed) => Err(eof()),
339            sent = self.0.outgoing.send(payload.to_vec()) => sent.map_err(|_| eof()),
340        }
341    }
342
343    async fn close(&self) {
344        self.0.close();
345    }
346}
347
348fn lock<T>(mutex: &Mutex<T>) -> MutexGuard<'_, T> {
349    mutex.lock().unwrap_or_else(PoisonError::into_inner)
350}
351
352fn eof() -> Error {
353    Error::transport(io::Error::new(
354        io::ErrorKind::UnexpectedEof,
355        "connection closed",
356    ))
357}
358
359/// A command as the client wrote it.
360#[derive(Debug, Deserialize)]
361pub struct SentCommand {
362    /// The verb: `subscribe`, `unsubscribe` or `message`.
363    pub command: String,
364    /// The identifier it is about.
365    pub identifier: String,
366    /// The payload, on a `message`.
367    pub data: Option<String>,
368}
369
370/// `value` as a JSON string literal, for building frames by hand.
371pub fn quote(value: &str) -> String {
372    serde_json::to_string(value).unwrap()
373}
374
375/// Speaks a made-up subprotocol and stamps everything it encodes, so a test can tell which
376/// protocol the client settled on.
377pub struct FakeProtocol {
378    /// The subprotocol it negotiates under.
379    pub subprotocol: String,
380    /// What it puts in front of every payload.
381    pub stamp: String,
382}
383
384impl FakeProtocol {
385    /// A stand-in for a second protocol version, `actioncable-v2-json`.
386    pub fn v2() -> FakeProtocol {
387        FakeProtocol {
388            subprotocol: "actioncable-v2-json".to_string(),
389            stamp: "v2:".to_string(),
390        }
391    }
392}
393
394impl Protocol for FakeProtocol {
395    fn subprotocol(&self) -> &str {
396        &self.subprotocol
397    }
398
399    fn encode(&self, command: &Command) -> Result<Vec<u8>, Error> {
400        let mut payload = self.stamp.as_bytes().to_vec();
401        payload.extend(V1Json.encode(command)?);
402        Ok(payload)
403    }
404
405    fn decode(&self, payload: &[u8]) -> Result<Incoming, Error> {
406        V1Json.decode(
407            payload
408                .strip_prefix(self.stamp.as_bytes())
409                .unwrap_or(payload),
410        )
411    }
412}