#![allow(
clippy::unwrap_used,
clippy::expect_used,
reason = "a fake for tests says what went wrong by failing the test"
)]
use std::collections::{HashSet, VecDeque};
use std::io;
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::{Arc, Mutex, MutexGuard, PoisonError};
use std::time::Duration;
use async_trait::async_trait;
use serde::Deserialize;
use tokio::sync::{mpsc, watch};
use crate::error::Error;
use crate::protocol::{Command, Incoming, Protocol};
use crate::transport::{Conn, DialOptions, Transport};
use crate::v1_json::{SUBPROTOCOL_V1_JSON, V1Json};
pub const WAIT: Duration = Duration::from_secs(2);
const SILENCE: Duration = Duration::from_millis(100);
#[derive(Clone)]
pub struct FakeTransport {
shared: Arc<TransportState>,
}
struct TransportState {
subprotocol: Mutex<Option<String>>,
write_buffer: Mutex<usize>,
dialed: mpsc::UnboundedSender<Arc<FakeConn>>,
accepting: tokio::sync::Mutex<mpsc::UnboundedReceiver<Arc<FakeConn>>>,
dial_errors: Mutex<VecDeque<String>>,
options: Mutex<Option<DialOptions>>,
}
impl FakeTransport {
pub fn new() -> FakeTransport {
let (dialed, accepting) = mpsc::unbounded_channel();
FakeTransport {
shared: Arc::new(TransportState {
subprotocol: Mutex::new(Some(SUBPROTOCOL_V1_JSON.to_string())),
write_buffer: Mutex::new(32),
dialed,
accepting: tokio::sync::Mutex::new(accepting),
dial_errors: Mutex::new(VecDeque::new()),
options: Mutex::new(None),
}),
}
}
pub fn speak(&self, subprotocol: Option<&str>) {
*lock(&self.shared.subprotocol) = subprotocol.map(str::to_string);
}
pub fn write_buffer(&self, commands: usize) {
*lock(&self.shared.write_buffer) = commands.max(1);
}
pub fn fail_next_dial(&self, reason: &str) {
lock(&self.shared.dial_errors).push_back(reason.to_string());
}
pub fn dialed_with(&self) -> DialOptions {
lock(&self.shared.options)
.clone()
.expect("the client never dialed")
}
pub async fn accept(&self) -> Arc<FakeConn> {
tokio::time::timeout(WAIT, self.shared.accepting.lock().await.recv())
.await
.expect("no connection was dialed")
.unwrap()
}
pub async fn refuse_dial(&self) {
let dialed = tokio::time::timeout(SILENCE, self.shared.accepting.lock().await.recv()).await;
if let Ok(Some(conn)) = dialed {
panic!(
"expected no connection, got one with subprotocol {:?}",
conn.subprotocol
);
}
}
}
impl Default for FakeTransport {
fn default() -> FakeTransport {
FakeTransport::new()
}
}
#[async_trait]
impl Transport for FakeTransport {
async fn dial(&self, _url: &str, options: DialOptions) -> Result<Box<dyn Conn>, Error> {
*lock(&self.shared.options) = Some(options);
if let Some(reason) = lock(&self.shared.dial_errors).pop_front() {
return Err(Error::transport(io::Error::new(
io::ErrorKind::ConnectionRefused,
reason,
)));
}
let conn = Arc::new(FakeConn::new(
lock(&self.shared.subprotocol).clone(),
*lock(&self.shared.write_buffer),
));
self.shared.dialed.send(Arc::clone(&conn)).unwrap();
Ok(Box::new(ClientEnd(conn)))
}
}
pub struct FakeConn {
subprotocol: Option<String>,
incoming: mpsc::Sender<Vec<u8>>,
reading: tokio::sync::Mutex<mpsc::Receiver<Vec<u8>>>,
outgoing: mpsc::Sender<Vec<u8>>,
sent: tokio::sync::Mutex<mpsc::Receiver<Vec<u8>>>,
writes: watch::Sender<usize>,
confirmed: Mutex<HashSet<String>>,
failing_writes: AtomicBool,
closed: watch::Sender<bool>,
}
impl FakeConn {
fn new(subprotocol: Option<String>, write_buffer: usize) -> FakeConn {
let (incoming, reading) = mpsc::channel(1);
let (outgoing, sent) = mpsc::channel(write_buffer);
FakeConn {
subprotocol,
incoming,
reading: tokio::sync::Mutex::new(reading),
outgoing,
sent: tokio::sync::Mutex::new(sent),
writes: watch::Sender::new(0),
confirmed: Mutex::new(HashSet::new()),
failing_writes: AtomicBool::new(false),
closed: watch::Sender::new(false),
}
}
pub async fn push(&self, frame: &str) {
let mut closed = self.closed.subscribe();
tokio::select! {
biased;
_ = closed.wait_for(|closed| *closed) => panic!("connection closed before {frame} could be sent"),
sent = tokio::time::timeout(WAIT, self.incoming.send(frame.as_bytes().to_vec())) => {
sent.unwrap_or_else(|_| panic!("client never read {frame}")).unwrap_or_else(|_| panic!("client hung up before reading {frame}"));
}
}
}
pub async fn welcome(&self) {
self.push(r#"{"type":"welcome"}"#).await;
}
pub async fn confirm(&self, identifier: &str) {
lock(&self.confirmed).insert(identifier.to_string());
self.push(&format!(
r#"{{"type":"confirm_subscription","identifier":{}}}"#,
quote(identifier)
))
.await;
}
pub async fn reject(&self, identifier: &str) {
lock(&self.confirmed).remove(identifier);
self.push(&format!(
r#"{{"type":"reject_subscription","identifier":{}}}"#,
quote(identifier)
))
.await;
}
pub async fn broadcast(&self, identifier: &str, message: &str) {
self.push(&format!(
r#"{{"identifier":{},"message":{message}}}"#,
quote(identifier)
))
.await;
}
pub fn fail_writes(&self) {
self.failing_writes.store(true, Ordering::SeqCst);
}
pub async fn writing(&self, count: usize) {
let mut writes = self.writes.subscribe();
tokio::time::timeout(WAIT, writes.wait_for(|writes| *writes >= count))
.await
.expect("the client wrote less than that")
.unwrap();
}
pub async fn sent(&self) -> Vec<u8> {
self.next_acted_on(WAIT).await.expect("client sent nothing")
}
async fn next_acted_on(&self, wait: Duration) -> Option<Vec<u8>> {
let mut sent = self.sent.lock().await;
loop {
match tokio::time::timeout(wait, sent.recv()).await {
Ok(Some(payload)) if self.acts_on(&payload) => break Some(payload),
Ok(Some(_ignored)) => {}
Ok(None) | Err(_) => break None,
}
}
}
fn acts_on(&self, payload: &[u8]) -> bool {
match serde_json::from_slice::<SentCommand>(payload) {
Ok(command) if command.command == "subscribe" => {
!lock(&self.confirmed).contains(&command.identifier)
}
Ok(command) if command.command == "unsubscribe" => {
lock(&self.confirmed).remove(&command.identifier);
true
}
_ => true,
}
}
pub async fn command(&self) -> SentCommand {
let payload = self.sent().await;
serde_json::from_slice(&payload).unwrap_or_else(|error| {
panic!(
"decoding command {}: {error}",
String::from_utf8_lossy(&payload)
)
})
}
pub async fn expect_command(&self, name: &str, identifier: &str) -> SentCommand {
let command = self.command().await;
assert_eq!(
name, command.command,
"expected {name} {identifier}, got {command:?}"
);
assert_eq!(
identifier, command.identifier,
"expected {name} {identifier}, got {command:?}"
);
command
}
pub async fn expect_silence(&self) {
if let Some(payload) = self.next_acted_on(SILENCE).await {
panic!(
"expected no command, got {}",
String::from_utf8_lossy(&payload)
);
}
}
pub fn close(&self) {
self.closed.send_replace(true);
}
}
struct ClientEnd(Arc<FakeConn>);
#[async_trait]
impl Conn for ClientEnd {
fn subprotocol(&self) -> Option<&str> {
self.0.subprotocol.as_deref()
}
async fn read(&self) -> Result<Vec<u8>, Error> {
let mut closed = self.0.closed.subscribe();
let mut reading = self.0.reading.lock().await;
tokio::select! {
biased;
_ = closed.wait_for(|closed| *closed) => Err(eof()),
payload = reading.recv() => payload.ok_or_else(eof),
}
}
async fn write(&self, payload: &[u8]) -> Result<(), Error> {
if self.0.failing_writes.load(Ordering::SeqCst) {
return Err(Error::transport(io::Error::new(
io::ErrorKind::TimedOut,
"the peer stopped reading",
)));
}
self.0.writes.send_modify(|writes| *writes += 1);
let mut closed = self.0.closed.subscribe();
tokio::select! {
biased;
_ = closed.wait_for(|closed| *closed) => Err(eof()),
sent = self.0.outgoing.send(payload.to_vec()) => sent.map_err(|_| eof()),
}
}
async fn close(&self) {
self.0.close();
}
}
fn lock<T>(mutex: &Mutex<T>) -> MutexGuard<'_, T> {
mutex.lock().unwrap_or_else(PoisonError::into_inner)
}
fn eof() -> Error {
Error::transport(io::Error::new(
io::ErrorKind::UnexpectedEof,
"connection closed",
))
}
#[derive(Debug, Deserialize)]
pub struct SentCommand {
pub command: String,
pub identifier: String,
pub data: Option<String>,
}
pub fn quote(value: &str) -> String {
serde_json::to_string(value).unwrap()
}
pub struct FakeProtocol {
pub subprotocol: String,
pub stamp: String,
}
impl FakeProtocol {
pub fn v2() -> FakeProtocol {
FakeProtocol {
subprotocol: "actioncable-v2-json".to_string(),
stamp: "v2:".to_string(),
}
}
}
impl Protocol for FakeProtocol {
fn subprotocol(&self) -> &str {
&self.subprotocol
}
fn encode(&self, command: &Command) -> Result<Vec<u8>, Error> {
let mut payload = self.stamp.as_bytes().to_vec();
payload.extend(V1Json.encode(command)?);
Ok(payload)
}
fn decode(&self, payload: &[u8]) -> Result<Incoming, Error> {
V1Json.decode(
payload
.strip_prefix(self.stamp.as_bytes())
.unwrap_or(payload),
)
}
}