refactor(lib): split the server actor's value types and tests out
server_actor.rs had grown past twelve hundred lines, a fifth of them tests and a hundred the value types it hands out. The types now live in server_actor/types.rs and the tests in server_actor/tests.rs, as the client's already do, and the room and user traffic in handle_message moves to its own dispatch step beside the wishlist one. No behaviour changes; the public paths are re-exported as before.
This commit is contained in:
@@ -0,0 +1,210 @@
|
||||
use super::*;
|
||||
|
||||
fn code_of(message: &Message) -> u32 {
|
||||
u32::from_le_bytes(message.get_data()[0..4].try_into().unwrap())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_timed_out_connect_parks_the_actor_in_disconnected() {
|
||||
let mut actor = parked_actor(1);
|
||||
actor.connection_state = ConnectionState::Connecting {
|
||||
since: Instant::now().checked_sub(Duration::from_secs(21)).unwrap(),
|
||||
};
|
||||
|
||||
actor.tick();
|
||||
|
||||
assert!(
|
||||
matches!(actor.connection_state, ConnectionState::Disconnected),
|
||||
"a timed-out connect must leave Connecting"
|
||||
);
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
#[test]
|
||||
fn the_server_socket_keeps_itself_alive() {
|
||||
let listener = std::net::TcpListener::bind("127.0.0.1:0").unwrap();
|
||||
let mut actor = parked_actor(listener.local_addr().unwrap().port());
|
||||
|
||||
assert!(actor.initiate_connection());
|
||||
|
||||
assert!(
|
||||
crate::utils::keepalive::keepalive_enabled(
|
||||
actor.stream.as_ref().unwrap()
|
||||
),
|
||||
"an idle NAT mapping must be kept alive and a dead path noticed"
|
||||
);
|
||||
}
|
||||
|
||||
fn parked_actor(port: u16) -> ServerActor {
|
||||
let (tx, _rx) = std::sync::mpsc::channel();
|
||||
ServerActor::new(
|
||||
PeerAddress::new("127.0.0.1".to_string(), port),
|
||||
tx,
|
||||
0,
|
||||
false,
|
||||
0,
|
||||
0,
|
||||
)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn dropping_the_connection_forgets_the_dispatcher_and_the_verdict() {
|
||||
let mut actor = parked_actor(1);
|
||||
actor.initialize_dispatcher();
|
||||
actor.connection_state = ConnectionState::Connected;
|
||||
actor.handle_login_status(true);
|
||||
|
||||
actor.disconnect_with_error();
|
||||
|
||||
assert_eq!(actor.session.loss(), Some(SessionLoss::Disconnected));
|
||||
assert!(
|
||||
actor.dispatcher_sender.is_none(),
|
||||
"a login queued now must wait in the buffer, not in a channel \
|
||||
the next connection replaces"
|
||||
);
|
||||
assert_eq!(
|
||||
actor.context.read().unwrap().logged_in,
|
||||
None,
|
||||
"the old verdict must not answer the next login"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn disconnect_clears_a_partial_frame_so_the_next_session_reframes_clean() {
|
||||
use std::io::Write;
|
||||
|
||||
let listener = std::net::TcpListener::bind("127.0.0.1:0").unwrap();
|
||||
let addr = listener.local_addr().unwrap();
|
||||
let client = TcpStream::connect(addr).unwrap();
|
||||
let (mut server_side, _) = listener.accept().unwrap();
|
||||
let partial_frame = [10u8, 0, 0, 0, 1, 2, 3];
|
||||
server_side.write_all(&partial_frame).unwrap();
|
||||
server_side.flush().unwrap();
|
||||
client.set_nonblocking(true).unwrap();
|
||||
|
||||
let mut actor = parked_actor(addr.port());
|
||||
actor.stream = Some(client);
|
||||
actor.connection_state = ConnectionState::Connected;
|
||||
for _ in 0..50 {
|
||||
actor.process_read();
|
||||
if actor.reader.buffer_len() > 0 {
|
||||
break;
|
||||
}
|
||||
std::thread::sleep(Duration::from_millis(10));
|
||||
}
|
||||
assert!(
|
||||
actor.reader.buffer_len() > 0,
|
||||
"an incomplete frame should be waiting for its rest"
|
||||
);
|
||||
|
||||
actor.disconnect();
|
||||
|
||||
assert_eq!(
|
||||
actor.reader.buffer_len(),
|
||||
0,
|
||||
"a stale partial frame carried across a reconnect reads the next \
|
||||
session's bytes as a length prefix and wedges recovery"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_login_on_a_parked_actor_dials_again_and_queues_the_login() {
|
||||
let listener = std::net::TcpListener::bind("127.0.0.1:0").unwrap();
|
||||
let mut actor = parked_actor(listener.local_addr().unwrap().port());
|
||||
actor.session.record(SessionLoss::Disconnected);
|
||||
let (response, _verdict) = std::sync::mpsc::channel();
|
||||
|
||||
actor.handle_login(
|
||||
"u".into(),
|
||||
"p".into(),
|
||||
ClientVersion::default(),
|
||||
response,
|
||||
);
|
||||
|
||||
assert!(
|
||||
matches!(actor.connection_state, ConnectionState::Connecting { .. }),
|
||||
"a parked actor must dial again on login"
|
||||
);
|
||||
assert_eq!(
|
||||
actor.queued_messages.len(),
|
||||
1,
|
||||
"the login waits for the connection to come up"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_parked_actor_handles_login_rather_than_queueing_it() {
|
||||
let listener = std::net::TcpListener::bind("127.0.0.1:0").unwrap();
|
||||
let mut actor = parked_actor(listener.local_addr().unwrap().port());
|
||||
actor.session.record(SessionLoss::Disconnected);
|
||||
let (response, _verdict) = std::sync::mpsc::channel();
|
||||
|
||||
actor.handle_message(ServerMessage::Login {
|
||||
username: "u".into(),
|
||||
password: "p".into(),
|
||||
version: ClientVersion::default(),
|
||||
response,
|
||||
});
|
||||
|
||||
assert!(
|
||||
matches!(actor.connection_state, ConnectionState::Connecting { .. }),
|
||||
"a parked actor must act on a login, not park it in the queue"
|
||||
);
|
||||
assert!(
|
||||
actor
|
||||
.queued_messages
|
||||
.iter()
|
||||
.all(|m| !matches!(m, ServerMessage::Login { .. })),
|
||||
"the login must not be sitting unhandled in the queue"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_login_with_nobody_listening_is_refused_at_once() {
|
||||
let mut actor = parked_actor(1);
|
||||
let (response, verdict) = std::sync::mpsc::channel();
|
||||
|
||||
actor.handle_login(
|
||||
"u".into(),
|
||||
"p".into(),
|
||||
ClientVersion::default(),
|
||||
response,
|
||||
);
|
||||
|
||||
assert!(
|
||||
matches!(
|
||||
verdict.recv_timeout(Duration::from_secs(5)),
|
||||
Ok(Err(SoulseekRs::NotConnected))
|
||||
),
|
||||
"a refused dial must not make the caller wait out the verdict \
|
||||
timeout"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_successful_login_marks_the_session_live_again() {
|
||||
let mut actor = parked_actor(1);
|
||||
actor.session.record(SessionLoss::Disconnected);
|
||||
|
||||
actor.handle_login_status(true);
|
||||
|
||||
assert_eq!(actor.session.loss(), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn post_login_messages_carry_counts_and_conditional_wait_port() {
|
||||
let messages = post_login_messages(true, 4321, 3, 7);
|
||||
let codes: Vec<u32> = messages.iter().map(code_of).collect();
|
||||
// SharedFolders, HaveNoParent, SetStatus, SetWaitPort.
|
||||
assert_eq!(codes, vec![35, 71, 28, 2]);
|
||||
|
||||
// The SharedFolders message (code 35) carries the real counts.
|
||||
let shared = messages[0].get_data();
|
||||
assert_eq!(u32::from_le_bytes(shared[4..8].try_into().unwrap()), 3);
|
||||
assert_eq!(u32::from_le_bytes(shared[8..12].try_into().unwrap()), 7);
|
||||
|
||||
// Not listening omits SetWaitPort (code 2).
|
||||
let no_listen = post_login_messages(false, 4321, 3, 7);
|
||||
let codes: Vec<u32> = no_listen.iter().map(code_of).collect();
|
||||
assert_eq!(codes, vec![35, 71, 28]);
|
||||
}
|
||||
@@ -0,0 +1,99 @@
|
||||
//! The values the server actor hands out: where a peer listens, a private
|
||||
//! message, and the login context.
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct PeerAddress {
|
||||
host: String,
|
||||
port: u16,
|
||||
}
|
||||
|
||||
impl PeerAddress {
|
||||
#[must_use]
|
||||
pub const fn new(host: String, port: u16) -> Self {
|
||||
Self { host, port }
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn get_host(&self) -> &str {
|
||||
&self.host
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub const fn get_port(&self) -> u16 {
|
||||
self.port
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Display for PeerAddress {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
|
||||
write!(f, "{}:{}", self.host, self.port)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
pub struct Context {
|
||||
pub logged_in: Option<bool>,
|
||||
}
|
||||
|
||||
impl Context {
|
||||
#[must_use]
|
||||
pub fn new() -> Self {
|
||||
Self::default()
|
||||
}
|
||||
}
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct UserMessage {
|
||||
id: u32,
|
||||
timestamp: u32,
|
||||
username: String,
|
||||
message: String,
|
||||
new_message: bool,
|
||||
}
|
||||
impl UserMessage {
|
||||
#[must_use]
|
||||
pub const fn new(
|
||||
id: u32,
|
||||
timestamp: u32,
|
||||
username: String,
|
||||
message: String,
|
||||
new_message: bool,
|
||||
) -> Self {
|
||||
Self {
|
||||
id,
|
||||
timestamp,
|
||||
username,
|
||||
message,
|
||||
new_message,
|
||||
}
|
||||
}
|
||||
/// The server-assigned id of this message (used to acknowledge it).
|
||||
#[must_use]
|
||||
pub const fn id(&self) -> u32 {
|
||||
self.id
|
||||
}
|
||||
|
||||
/// Unix timestamp the server recorded for this message.
|
||||
#[must_use]
|
||||
pub const fn timestamp(&self) -> u32 {
|
||||
self.timestamp
|
||||
}
|
||||
|
||||
/// The username of the sender.
|
||||
#[must_use]
|
||||
pub fn username(&self) -> &str {
|
||||
&self.username
|
||||
}
|
||||
|
||||
/// The message body.
|
||||
#[must_use]
|
||||
pub fn message(&self) -> &str {
|
||||
&self.message
|
||||
}
|
||||
|
||||
/// Whether the server flagged this as freshly delivered (as opposed to a
|
||||
/// message replayed because it was queued while the recipient was offline).
|
||||
#[must_use]
|
||||
pub const fn is_new(&self) -> bool {
|
||||
self.new_message
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user