#![allow(dead_code)]
use std::time::Duration;
use polyoxide_ws::{
test_server::{Script as Shared, ScriptedServer as Server},
tungstenite::Message,
};
use serde_json::{json, Value};
use tokio::time::Instant;
#[derive(Debug, Clone, Default)]
pub struct Script {
pub pushes: Vec<String>,
pub push_every: Option<Duration>,
pub close_after: bool,
pub close_code: Option<(u16, String)>,
pub drop_after: Option<Duration>,
pub go_silent: bool,
pub ping_every: Option<Duration>,
pub refuse: Vec<(String, i64, String)>,
pub ignore_later_requests: bool,
pub hold_answer: Option<(u64, String)>,
pub reject_handshake: bool,
pub refuse_upgrade: Option<(u16, Vec<(String, String)>)>,
}
impl Script {
fn shared(&self) -> Shared {
if self.reject_handshake {
return Shared::reject();
}
if let Some((status, headers)) = &self.refuse_upgrade {
return headers
.iter()
.fold(Shared::refuse_upgrade(*status), |shared, (name, value)| {
shared.with_header(name, value)
});
}
let pushes: Vec<&str> = self.pushes.iter().map(String::as_str).collect();
let mut shared = Shared::frames(&pushes).after_first_frame();
if let Some(every) = self.push_every {
shared = shared.push_every(every);
}
if self.close_after {
shared = shared.then_close();
}
if let Some((code, reason)) = &self.close_code {
shared = shared.close_with(*code, reason);
}
if let Some(after) = self.drop_after {
shared = shared.drop_after(after);
}
if self.go_silent {
shared = shared.go_silent();
}
if let Some(every) = self.ping_every {
shared = shared.ping_every(every);
}
shared
}
}
#[derive(Debug, Clone)]
pub struct Received {
pub connection: usize,
pub at: Instant,
pub request: Value,
}
pub struct ScriptedServer {
pub url: String,
inner: Server,
}
impl ScriptedServer {
pub async fn start(scripts: Vec<Script>) -> Self {
let shared = scripts.iter().map(Script::shared).collect();
let inner = Server::start_answering(shared, move |index| {
let script = scripts
.get(index)
.or_else(|| scripts.last())
.cloned()
.unwrap_or_default();
answer(script)
})
.await;
Self {
url: inner.url.clone(),
inner,
}
}
pub fn connection_count(&self) -> usize {
self.inner.connection_count()
}
pub fn accepted_at(&self) -> Vec<Instant> {
self.inner.accepted_at()
}
pub fn paths(&self) -> Vec<String> {
self.inner.paths()
}
pub fn received(&self) -> Vec<Received> {
self.inner
.received()
.into_iter()
.map(|r| Received {
connection: r.connection,
at: r.at,
request: serde_json::from_str(&r.text)
.unwrap_or_else(|err| panic!("a request that is not JSON ({err}): {}", r.text)),
})
.collect()
}
pub fn requests(&self) -> Vec<Value> {
self.received().into_iter().map(|r| r.request).collect()
}
pub fn ping_count(&self) -> usize {
self.inner.ping_count()
}
pub fn close_count(&self) -> usize {
self.inner.close_count()
}
pub fn close_reply_count(&self) -> usize {
self.inner.close_reply_count()
}
pub fn ended_count(&self) -> usize {
self.inner.ended_count()
}
pub async fn wait_for(&self, label: &str, predicate: impl Fn(&Self) -> bool) {
self.inner.wait_for(label, |_| predicate(self)).await;
}
}
fn answer(script: Script) -> impl FnMut(&str) -> Vec<Message> + Send {
let mut streams: Vec<String> = Vec::new();
let mut answered = 0usize;
let mut held: Option<Value> = None;
move |text| {
let Ok(request) = serde_json::from_str::<Value>(text) else {
return Vec::new();
};
if answered > 0 && script.ignore_later_requests {
return Vec::new();
}
let reply = reply(&script, &request, &mut streams);
let mut frames = Vec::new();
if let Some((held_id, before)) = &script.hold_answer {
if request.get("id").and_then(Value::as_u64) == Some(*held_id) {
held = Some(reply);
return Vec::new();
}
if let Some(late) = held.take() {
frames.push(Message::Text(before.clone().into()));
frames.push(Message::Text(late.to_string().into()));
}
}
frames.push(Message::Text(reply.to_string().into()));
answered += 1;
frames
}
}
fn reply(script: &Script, request: &Value, streams: &mut Vec<String>) -> Value {
let id = request.get("id").cloned().unwrap_or(Value::Null);
let params: Vec<String> = request["params"]
.as_array()
.map(|names| {
names
.iter()
.filter_map(|n| n.as_str().map(str::to_owned))
.collect()
})
.unwrap_or_default();
if let Some((_, code, msg)) = script
.refuse
.iter()
.find(|(name, _, _)| params.contains(name))
{
return json!({ "error": { "code": code, "msg": msg }, "id": id });
}
match request["method"].as_str() {
Some("SUBSCRIBE") => {
for name in params {
if !streams.contains(&name) {
streams.push(name);
}
}
json!({ "result": null, "id": id })
}
Some("UNSUBSCRIBE") => {
streams.retain(|name| !params.contains(name));
json!({ "result": null, "id": id })
}
Some("LIST_SUBSCRIPTIONS") => json!({ "result": streams, "id": id }),
_ => json!({ "error": { "code": 2, "msg": "Invalid request" }, "id": id }),
}
}