253 lines
9.2 KiB
Rust
253 lines
9.2 KiB
Rust
//! End-to-end session tests: full connect → sessionInit handshake → online,
|
|
//! against a self-signed TLS server that answers the handshake.
|
|
|
|
use std::sync::Arc;
|
|
use std::time::Duration;
|
|
|
|
use kolibri_net::protocol::{codec, framing::PacketReceiver, opcodes, packet::cmd};
|
|
use kolibri_net::{ClientConfig, HandshakeConfig, Session, SessionConfig, SessionState, UserAgent};
|
|
use tokio::io::{AsyncReadExt, AsyncWriteExt};
|
|
use tokio::net::TcpListener;
|
|
use tokio::time::timeout;
|
|
use tokio_rustls::TlsAcceptor;
|
|
|
|
fn server_config() -> Arc<rustls::ServerConfig> {
|
|
let certified = rcgen::generate_simple_self_signed(vec!["localhost".to_string()]).unwrap();
|
|
let cert_der = certified.cert.der().clone();
|
|
let key_der = rustls::pki_types::PrivatePkcs8KeyDer::from(certified.key_pair.serialize_der());
|
|
let provider = Arc::new(rustls::crypto::ring::default_provider());
|
|
let config = rustls::ServerConfig::builder_with_provider(provider)
|
|
.with_safe_default_protocol_versions()
|
|
.unwrap()
|
|
.with_no_client_auth()
|
|
.with_single_cert(
|
|
vec![cert_der],
|
|
rustls::pki_types::PrivateKeyDer::Pkcs8(key_der),
|
|
)
|
|
.unwrap();
|
|
Arc::new(config)
|
|
}
|
|
|
|
/// Handshake response payload: {callsSeed: 7, device_name: "Rusty"}.
|
|
fn handshake_response() -> Vec<u8> {
|
|
let value = rmpv::Value::Map(vec![
|
|
(rmpv::Value::from("callsSeed"), rmpv::Value::from(7i64)),
|
|
(rmpv::Value::from("device_name"), rmpv::Value::from("Rusty")),
|
|
]);
|
|
let mut out = Vec::new();
|
|
rmpv::encode::write_value(&mut out, &value).unwrap();
|
|
out
|
|
}
|
|
|
|
/// Server: answer sessionInit with the handshake payload; echo any other
|
|
/// request; silently accept pings (no matching waiter on the client).
|
|
async fn run_server(listener: TcpListener, acceptor: TlsAcceptor) {
|
|
let (tcp, _) = listener.accept().await.unwrap();
|
|
let mut tls = acceptor.accept(tcp).await.unwrap();
|
|
|
|
let mut receiver = PacketReceiver::new();
|
|
let mut buf = vec![0u8; 16 * 1024];
|
|
loop {
|
|
let n = match tls.read(&mut buf).await {
|
|
Ok(0) | Err(_) => break,
|
|
Ok(n) => n,
|
|
};
|
|
let packets = receiver.feed(&buf[..n]).unwrap();
|
|
for raw in packets {
|
|
let req = codec::decode(&raw).unwrap();
|
|
match req.opcode {
|
|
opcodes::SESSION_INIT => {
|
|
let resp =
|
|
codec::encode_with_cmd(cmd::OK, req.opcode, &handshake_response(), req.seq);
|
|
tls.write_all(&resp).await.unwrap();
|
|
tls.flush().await.unwrap();
|
|
}
|
|
opcodes::PING => {}
|
|
_ => {
|
|
let resp = codec::encode_with_cmd(cmd::OK, req.opcode, &req.payload, req.seq);
|
|
tls.write_all(&resp).await.unwrap();
|
|
tls.flush().await.unwrap();
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
fn handshake_config() -> HandshakeConfig {
|
|
HandshakeConfig {
|
|
instance_id: "inst-123".to_string(),
|
|
device_id: "dev-abc".to_string(),
|
|
client_session_id: 42,
|
|
user_agent: UserAgent {
|
|
device_type: "ANDROID".to_string(),
|
|
app_version: "1.0.0".to_string(),
|
|
os_version: "Android 14".to_string(),
|
|
timezone: "Europe/Moscow".to_string(),
|
|
screen: "420dpi 420dpi 1080x2340".to_string(),
|
|
push_device_type: "GCM".to_string(),
|
|
arch: "arm64-v8a".to_string(),
|
|
locale: "ru".to_string(),
|
|
build_number: 100,
|
|
device_name: "Pixel".to_string(),
|
|
device_locale: "ru".to_string(),
|
|
is_pwa: None,
|
|
header_user_agent: None,
|
|
},
|
|
}
|
|
}
|
|
|
|
async fn start_session(auto_reconnect: bool) -> Session {
|
|
let acceptor = TlsAcceptor::from(server_config());
|
|
let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
|
|
let addr = listener.local_addr().unwrap();
|
|
tokio::spawn(run_server(listener, acceptor));
|
|
|
|
let client = ClientConfig::new("127.0.0.1", addr.port()).insecure(true);
|
|
let mut config = SessionConfig::new(client, handshake_config());
|
|
config.auto_reconnect = auto_reconnect;
|
|
config.ping_interval = Duration::from_millis(200);
|
|
Session::new(config)
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn connect_performs_handshake_and_goes_online() {
|
|
let session = start_session(false).await;
|
|
|
|
let info = timeout(Duration::from_secs(5), session.connect())
|
|
.await
|
|
.expect("connect timed out")
|
|
.expect("handshake failed");
|
|
|
|
assert_eq!(info.calls_seed, Some(7));
|
|
assert_eq!(info.device_name.as_deref(), Some("Rusty"));
|
|
assert_eq!(session.state(), SessionState::Online);
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn request_routes_through_session() {
|
|
let session = start_session(false).await;
|
|
session.connect().await.unwrap();
|
|
|
|
let payload = {
|
|
let value = rmpv::Value::Map(vec![(rmpv::Value::from("q"), rmpv::Value::from("hi"))]);
|
|
let mut out = Vec::new();
|
|
rmpv::encode::write_value(&mut out, &value).unwrap();
|
|
out
|
|
};
|
|
let resp = session
|
|
.request(opcodes::CHATS_LIST, &payload)
|
|
.await
|
|
.unwrap();
|
|
assert!(resp.is_ok());
|
|
assert_eq!(resp.payload, payload);
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn state_transitions_reach_online() {
|
|
let session = start_session(false).await;
|
|
let mut states = session.subscribe_state();
|
|
|
|
session.connect().await.unwrap();
|
|
|
|
// Drain observed states; the terminal one must be Online.
|
|
let mut last = *states.borrow_and_update();
|
|
while states.has_changed().unwrap_or(false) {
|
|
last = *states.borrow_and_update();
|
|
}
|
|
assert_eq!(last, SessionState::Online);
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn keepalive_ping_is_sent() {
|
|
// ping_interval is 200ms; if the session stays online for >0.5s without the
|
|
// server closing on an unexpected packet, pings are being accepted.
|
|
let session = start_session(false).await;
|
|
session.connect().await.unwrap();
|
|
tokio::time::sleep(Duration::from_millis(600)).await;
|
|
assert_eq!(session.state(), SessionState::Online);
|
|
}
|
|
|
|
/// Server that drops the first connection right after the handshake, then serves
|
|
/// the second connection normally — to exercise auto-reconnect.
|
|
async fn run_flaky_server(listener: TcpListener, acceptor: TlsAcceptor) {
|
|
// First connection: answer the handshake, then drop the socket.
|
|
if let Ok((tcp, _)) = listener.accept().await {
|
|
if let Ok(mut tls) = acceptor.accept(tcp).await {
|
|
let mut receiver = PacketReceiver::new();
|
|
let mut buf = vec![0u8; 16 * 1024];
|
|
'first: loop {
|
|
let n = match tls.read(&mut buf).await {
|
|
Ok(0) | Err(_) => break,
|
|
Ok(n) => n,
|
|
};
|
|
for raw in receiver.feed(&buf[..n]).unwrap() {
|
|
let req = codec::decode(&raw).unwrap();
|
|
if req.opcode == opcodes::SESSION_INIT {
|
|
let resp = codec::encode_with_cmd(
|
|
cmd::OK,
|
|
req.opcode,
|
|
&handshake_response(),
|
|
req.seq,
|
|
);
|
|
tls.write_all(&resp).await.unwrap();
|
|
tls.flush().await.unwrap();
|
|
break 'first; // drop the connection
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
// Second connection: serve normally.
|
|
run_server(listener, acceptor).await;
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn auto_reconnects_after_drop() {
|
|
let acceptor = TlsAcceptor::from(server_config());
|
|
let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
|
|
let addr = listener.local_addr().unwrap();
|
|
tokio::spawn(run_flaky_server(listener, acceptor));
|
|
|
|
let client = ClientConfig::new("127.0.0.1", addr.port()).insecure(true);
|
|
let mut config = SessionConfig::new(client, handshake_config());
|
|
config.auto_reconnect = true;
|
|
config.ping_interval = Duration::from_millis(150);
|
|
let session = Session::new(config);
|
|
|
|
let mut states = session.subscribe_state();
|
|
// First handshake succeeds on conn1 (which the server then drops). The drop
|
|
// may be detected before this returns, so we don't assert Online here.
|
|
session.connect().await.unwrap();
|
|
|
|
// Observe: … Disconnected (drop) → … → Online again (reconnect on conn2).
|
|
let mut saw_disconnected = false;
|
|
let mut reconnected = false;
|
|
let deadline = Duration::from_secs(8);
|
|
let result = timeout(deadline, async {
|
|
while states.changed().await.is_ok() {
|
|
match *states.borrow_and_update() {
|
|
SessionState::Disconnected => saw_disconnected = true,
|
|
SessionState::Online if saw_disconnected => {
|
|
reconnected = true;
|
|
break;
|
|
}
|
|
_ => {}
|
|
}
|
|
}
|
|
})
|
|
.await;
|
|
|
|
assert!(result.is_ok(), "did not reconnect within {deadline:?}");
|
|
assert!(reconnected, "session did not return to Online after drop");
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn disconnect_stops_session() {
|
|
let session = start_session(false).await;
|
|
session.connect().await.unwrap();
|
|
session.disconnect();
|
|
assert_eq!(session.state(), SessionState::Disconnected);
|
|
let err = session.request(opcodes::PING, &[]).await;
|
|
assert!(err.is_err());
|
|
}
|