use crate::client::Network;
use crate::utils::Address;
use super::*;
use crate::live::fixtures::mainnet;
use crate::live::frame::Frame;
use crate::live::request::{PING, Post, post, subscribe, unsubscribe};
use crate::live::subscription::{BadSubscription, Subscription};
fn trades(coin: &str) -> Subscription {
Subscription::Trades { coin: coin.to_owned() }
}
fn fast_book(coin: &str) -> Subscription {
Subscription::L2Book { coin: coin.to_owned(), n_sig_figs: None, mantissa: None, fast: true }
}
fn someone(n: u8) -> Address {
let mut bytes = [0u8; 20];
bytes[19] = n;
Address::from_bytes(bytes)
}
fn open(at: u64) -> Session {
let mut session = Session::new(Network::Mainnet);
let _ = session.opened(at);
session
}
fn answer(id: u64) -> String {
format!(
r#"{{"channel":"post","data":{{"id":{id},"response":{{"type":"info","payload":{{"type":"l2Book","data":null}}}}}}}}"#
)
}
#[test]
fn a_session_names_its_own_network_s_socket() {
for network in Network::ALL {
let session = Session::new(network);
assert_eq!((session.network(), session.url()), (network, network.ws_url()));
assert!(!session.is_open());
}
}
#[test]
fn subscriptions_wait_while_closed_and_all_go_out_in_order_on_open() {
let mut session = Session::new(Network::Mainnet);
assert_eq!(session.subscribe(trades("BTC")), Ok(None));
assert_eq!(session.subscribe(fast_book("SOL")), Ok(None));
assert_eq!(session.subscribe(trades("BTC")), Ok(None), "wanted once");
let all = vec![subscribe(&trades("BTC")), subscribe(&fast_book("SOL"))];
assert_eq!(session.opened(1_000), all);
assert!(session.is_open());
let _ = session.closed(2_000, 0);
assert!(!session.is_open());
assert_eq!(session.opened(3_000), all);
}
#[test]
fn while_open_a_new_subscription_goes_out_at_once_and_a_repeat_does_not() {
let mut session = open(0);
assert_eq!(session.subscribe(trades("BTC")), Ok(Some(subscribe(&trades("BTC")))));
assert_eq!(session.subscribe(trades("BTC")), Ok(None));
assert_eq!(session.wanted(), [trades("BTC")]);
}
#[test]
fn unsubscribing_sends_the_same_object_the_subscribe_did() {
let mut session = open(0);
let _ = session.subscribe(fast_book("SOL"));
let _ = session.subscribe(trades("BTC"));
let stop = session.unsubscribe(&fast_book("SOL")).unwrap();
assert_eq!(stop, unsubscribe(&fast_book("SOL")));
assert!(stop.contains(r#""fast":true"#));
assert_eq!(session.unsubscribe(&fast_book("SOL")), None, "no longer wanted");
assert_eq!(session.wanted(), [trades("BTC")]);
let _ = session.closed(10, 0);
assert_eq!(session.unsubscribe(&trades("BTC")), None, "nothing to send while closed");
assert!(session.opened(20).is_empty(), "and nothing comes back on open");
}
#[test]
fn a_quiet_socket_is_pinged_after_thirty_seconds_and_its_pong_keeps_it() {
let mut session = open(1_000);
assert_eq!(session.tick(30_999), Tick::Wait);
assert_eq!(session.tick(31_000), Tick::Ping);
assert_eq!(session.tick(31_500), Tick::Wait, "one ping, not one a tick");
assert_eq!(session.received(31_200, &mainnet("pong")), Ok(Frame::Pong));
assert_eq!(session.tick(61_199), Tick::Wait);
assert_eq!(session.tick(61_200), Tick::Ping, "thirty seconds after the pong");
assert_eq!(PING, r#"{"method":"ping"}"#);
}
#[test]
fn a_busy_socket_is_never_pinged() {
let mut session = open(0);
for at in (10_000..600_000).step_by(10_000) {
assert!(session.received(at, &mainnet("bbo")).is_ok());
assert_eq!(session.tick(at + 9_999), Tick::Wait, "{at}");
}
}
#[test]
fn sixty_seconds_of_nothing_pong_included_is_a_dead_socket() {
let mut session = open(0);
assert_eq!(session.tick(30_000), Tick::Ping);
assert_eq!(session.tick(59_999), Tick::Wait);
assert_eq!(session.tick(60_000), Tick::Reconnect);
assert_eq!(session.tick(90_000), Tick::Reconnect, "until the socket is closed");
let _ = session.closed(90_001, 0);
assert_eq!(session.tick(200_000), Tick::Wait, "a closed socket waits for its reconnect");
let mut session = open(0);
assert!(session.received(50_000, "Websocket connection established.").is_err());
assert_eq!(session.tick(109_999), Tick::Ping);
assert_eq!(session.tick(109_999), Tick::Wait);
assert_eq!(session.tick(110_000), Tick::Reconnect);
}
#[test]
fn due_says_when_a_tick_next_has_work() {
let mut session = Session::new(Network::Mainnet);
assert_eq!(session.due(), None, "closed");
let _ = session.opened(1_000);
assert_eq!(session.due(), Some(31_000));
assert_eq!(session.tick(31_000), Tick::Ping);
assert_eq!(session.due(), Some(61_000), "the silence limit and the next ping coincide");
let _ = session.received(40_000, &mainnet("pong"));
assert_eq!(session.due(), Some(70_000));
}
#[test]
fn reconnect_delays_double_from_a_quarter_second_to_twenty_and_never_stop() {
let mut session = Session::new(Network::Testnet);
let plain: Vec<u64> = (0..10).map(|at| session.closed(at, 0).after_ms).collect();
assert_eq!(plain, [250, 500, 1_000, 2_000, 4_000, 8_000, 16_000, 20_000, 20_000, 20_000]);
for random in [1, 62, 63, 64, 0xdead_beef, u64::MAX] {
let first = Session::new(Network::Testnet).closed(0, random).after_ms;
assert_eq!(first, 250 + random % 63, "{random}");
}
let mut session = Session::new(Network::Mainnet);
let mut random = 0x9e37_79b9_7f4a_7c15_u64;
for attempt in 0..10_000u64 {
random = random.wrapping_mul(6_364_136_223_846_793_005).wrapping_add(1_442_695_040_888_963_407);
let after = session.closed(attempt, random).after_ms;
assert!((250..=25_000).contains(&after), "{attempt}: {after}");
if attempt >= 7 {
assert!(after >= 20_000, "{attempt}: {after}");
}
}
}
#[test]
fn a_socket_that_held_starts_the_ladder_over_and_one_dropped_at_once_does_not() {
let mut session = Session::new(Network::Mainnet);
let mut at = 0;
for expected in [250, 500, 1_000, 2_000] {
let _ = session.opened(at);
assert!(session.received(at + 200, &mainnet("ack.allMids")).is_ok());
assert_eq!(session.closed(at + 600, 0).after_ms, expected);
at += 10_000;
}
let _ = session.opened(at);
let _ = session.received(at + 29_000, &mainnet("bbo"));
assert_eq!(session.closed(at + 30_000, 0).after_ms, 250);
assert_eq!(session.closed(at + 30_500, 0).after_ms, 500, "a failed connect climbs again");
let _ = session.opened(at + 40_000);
assert_eq!(session.closed(at + 95_000, 0).after_ms, 1_000);
}
#[test]
fn the_venue_s_limits_are_refused_before_anything_is_sent() {
assert_eq!(Limits::VENUE, Limits { subscriptions: 1_000, users: 10, posts: 100 });
assert_eq!(Limits::default(), Limits::VENUE);
let mut session = open(0);
for n in 0..1_000 {
assert!(matches!(session.subscribe(trades(&format!("C{n}"))), Ok(Some(_))), "{n}");
}
assert_eq!(session.subscribe(trades("MORE")), Err(Refused::Subscriptions { limit: 1_000 }));
assert_eq!(session.wanted().len(), 1_000);
assert_eq!(session.subscribe(trades("C0")), Ok(None), "one already kept is no more");
let _ = session.unsubscribe(&trades("C0"));
assert!(matches!(session.subscribe(trades("MORE")), Ok(Some(_))), "room again");
let mut session = open(0);
for n in 1..=10 {
assert!(session.subscribe(Subscription::UserFills { user: someone(n), aggregate_by_time: false }).is_ok());
assert!(session.subscribe(Subscription::OrderUpdates { user: someone(n) }).is_ok());
}
assert_eq!(session.users(), 10);
assert_eq!(session.subscribe(Subscription::UserEvents { user: someone(11) }), Err(Refused::Users { limit: 10 }));
assert!(session.subscribe(Subscription::UserEvents { user: someone(3) }).is_ok(), "a user already followed");
assert!(session.subscribe(trades("BTC")).is_ok(), "market data names no user");
let _ = session.unsubscribe(&Subscription::UserFills { user: someone(1), aggregate_by_time: false });
assert_eq!(session.subscribe(Subscription::UserEvents { user: someone(11) }), Err(Refused::Users { limit: 10 }));
let _ = session.unsubscribe(&Subscription::OrderUpdates { user: someone(1) });
assert_eq!(session.users(), 9);
assert!(session.subscribe(Subscription::UserEvents { user: someone(11) }).is_ok(), "the slot came free");
}
#[test]
fn a_bad_subscription_is_refused_before_it_is_kept() {
let mut session = open(0);
assert_eq!(session.subscribe(trades("")), Err(Refused::Bad(BadSubscription::EmptyCoin)));
let lumped = Subscription::L2Book { coin: "BTC".to_owned(), n_sig_figs: Some(4), mantissa: Some(2), fast: false };
assert_eq!(session.subscribe(lumped), Err(Refused::Bad(BadSubscription::Mantissa(2))));
assert!(session.wanted().is_empty());
}
#[test]
fn smaller_limits_leave_room_for_other_sockets_on_the_same_ip() {
let mut session = Session::new(Network::Mainnet).with_limits(Limits { subscriptions: 2, users: 1, posts: 1 });
let _ = session.opened(0);
assert!(session.subscribe(Subscription::OrderUpdates { user: someone(1) }).is_ok());
assert_eq!(session.subscribe(Subscription::OrderUpdates { user: someone(2) }), Err(Refused::Users { limit: 1 }));
assert!(session.subscribe(trades("BTC")).is_ok());
assert_eq!(session.subscribe(trades("ETH")), Err(Refused::Subscriptions { limit: 2 }));
let read = Post::info(r#"{"type":"allMids"}"#).unwrap();
assert!(session.post(&read).is_ok());
assert_eq!(session.post(&read), Err(Refused::Posts { limit: 1 }));
}
#[test]
fn posts_wait_for_their_answer_by_id_and_a_dropped_socket_reports_them_unanswered() {
let read = Post::info(r#"{"type":"l2Book","coin":"ETH","nSigFigs":5,"mantissa":null}"#).unwrap();
let mut session = Session::new(Network::Mainnet);
assert_eq!(session.post(&read), Err(Refused::Closed), "never held to send later");
let _ = session.opened(0);
let (one, text) = session.post(&read).unwrap();
assert_eq!((one, text.as_str()), (1, post(1, &read).as_str()));
let (two, _) = session.post(&Post::info(r#"{"type":"noSuchRead"}"#).unwrap()).unwrap();
let (three, _) = session.post(&read).unwrap();
assert_eq!((two, three), (2, 3));
assert_eq!(session.waiting(), [1, 2, 3]);
assert!(matches!(session.received(10, &mainnet("post.info.l2Book")), Ok(Frame::Post(ref reply)) if reply.id == 1));
assert!(matches!(session.received(11, &mainnet("error.post")), Ok(Frame::Error(_))));
assert_eq!(session.waiting(), [3]);
assert_eq!(session.closed(20, 0).unanswered, [3]);
assert!(session.waiting().is_empty());
let _ = session.opened(30);
assert_eq!(session.post(&read).map(|(id, _)| id), Ok(4), "an id is never reused");
}
#[test]
fn a_hundred_posts_in_flight_is_the_most() {
let mut session = open(0);
let read = Post::info(r#"{"type":"allMids"}"#).unwrap();
for n in 1..=100 {
assert_eq!(session.post(&read).map(|(id, _)| id), Ok(n));
}
assert_eq!(session.post(&read), Err(Refused::Posts { limit: 100 }));
assert!(session.received(5, &answer(40)).is_ok());
assert_eq!(session.post(&read).map(|(id, _)| id), Ok(101));
}