fix(tests): break the irtt-client/irtt-server dev-dependency cycle
irtt-client and irtt-server mutually dev-depended on each other for cross-crate integration tests. That cycle blocks `cargo publish` for either crate whenever the other doesn't already exist on the registry at the required version, which is exactly the situation for a crate's first-ever release (irtt-server has never been published before). Introduce a new `irtt-integration-tests` crate (publish = false, no library target) to own tests whose actual subject is the interaction between two or more irtt-* production crates: crate-local behavior -> crate's own tests/ cross-crate/system behavior -> irtt-integration-tests Move every test that imports both irtt_client and irtt_server there: the client-side support harness (hmac, large_packets, protocol_options, fill_negotiation) and the server-side real-socket tests that drive a real AsyncClient against a real Server (async_client, negotiation_policy, fill_wire_bytes, traffic_class, wildcard_source). server_set.rs stays in irtt-server/tests since it only uses irtt_proto as test machinery to drive irtt-server directly, which is a normal one-directional test dependency, not a peer relationship. irtt-server's dev-dependency on irtt-client is dropped entirely now that nothing under crates/irtt-server/tests needs it, which fully breaks the cycle from that side. irtt-client keeps a one-directional dev-dependency on irtt-server: a handful of white-box unit tests in src/async_client/tests.rs assert on private AsyncClient internals (socket, remote, test_hooks) while also needing a real in-tree server for blocking/async equivalence checks, so they can't move to an external crate without losing that access. Their InTreeServer helper is duplicated locally rather than shared, matching the existing precedent of irtt-cli's own tests/support/in_tree_server.rs.
This commit is contained in:
19 files changed
+106
-5
No files matched your search
@@ -0,0 +1,309 @@
|
||||
mod support;
|
||||
|
||||
use std::time::Duration;
|
||||
|
||||
use irtt_client::{
|
||||
Client, ClientConfig, ClientError, ClientEvent, OpenOutcome, SocketConfig, WarningKind,
|
||||
};
|
||||
use irtt_proto::TimestampFields;
|
||||
|
||||
use support::{
|
||||
config_for_params, default_params, params_for_modes, run_one_probe_with_config,
|
||||
standard_timestamps, start_bad_hmac_echo_reply_server, start_hmac_close_server,
|
||||
start_hmac_required_open_drop_server, BackendPeer, ServerObservation, TOKEN,
|
||||
};
|
||||
|
||||
use crate::support::test_echo_packet_len;
|
||||
|
||||
#[test]
|
||||
fn hmac_open_success_negotiates_without_warnings() {
|
||||
let key = b"compat-secret".to_vec();
|
||||
let params = default_params();
|
||||
let config_params = params.clone();
|
||||
let config_key = key.clone();
|
||||
|
||||
let run = run_one_probe_with_config(
|
||||
params.clone(),
|
||||
TimestampFields::default(),
|
||||
Some(key),
|
||||
|addr| ClientConfig {
|
||||
hmac_key: Some(config_key),
|
||||
..config_for_params(addr, &config_params)
|
||||
},
|
||||
);
|
||||
|
||||
assert_eq!(run.negotiated.params, params);
|
||||
match &run.observations[0] {
|
||||
ServerObservation::Open { params: got, hmac } => {
|
||||
assert_eq!(got, ¶ms);
|
||||
assert!(*hmac);
|
||||
}
|
||||
other => panic!("expected open observation, got {other:?}"),
|
||||
}
|
||||
assert_no_warning_or_error(&[run.sent, run.reply]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn hmac_echo_success_verifies_request_and_accepts_reply() {
|
||||
let key = b"compat-secret".to_vec();
|
||||
let params = default_params();
|
||||
let config_params = params.clone();
|
||||
let config_key = key.clone();
|
||||
|
||||
let run = run_one_probe_with_config(params.clone(), standard_timestamps(), Some(key), |addr| {
|
||||
ClientConfig {
|
||||
hmac_key: Some(config_key),
|
||||
..config_for_params(addr, &config_params)
|
||||
}
|
||||
});
|
||||
|
||||
match &run.observations[1] {
|
||||
ServerObservation::Echo {
|
||||
len,
|
||||
hmac,
|
||||
token,
|
||||
sequence,
|
||||
} => {
|
||||
assert_eq!(*len, test_echo_packet_len(true, ¶ms));
|
||||
assert!(*hmac);
|
||||
assert_eq!(*token, TOKEN);
|
||||
assert_eq!(*sequence, 0);
|
||||
}
|
||||
other => panic!("expected echo observation, got {other:?}"),
|
||||
}
|
||||
|
||||
match &run.reply {
|
||||
ClientEvent::EchoReply {
|
||||
seq,
|
||||
bytes,
|
||||
server_timing,
|
||||
..
|
||||
} => {
|
||||
assert_eq!(*seq, 0);
|
||||
assert_eq!(*bytes, test_echo_packet_len(true, ¶ms));
|
||||
assert!(server_timing.is_some());
|
||||
}
|
||||
other => panic!("expected EchoReply, got {other:?}"),
|
||||
}
|
||||
assert_no_duplicate_late_or_warning(&[run.reply]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn hmac_close_success_sends_authenticated_close_and_closes_session() {
|
||||
let key = b"compat-secret".to_vec();
|
||||
let params = default_params();
|
||||
let server = start_hmac_close_server(params.clone(), key.clone());
|
||||
let mut config = config_for_params(server.addr, ¶ms);
|
||||
config.hmac_key = Some(key);
|
||||
|
||||
let mut client = Client::connect(config).unwrap();
|
||||
let outcome = client.open().unwrap();
|
||||
assert_started(outcome, ¶ms);
|
||||
|
||||
let events = client.close().unwrap();
|
||||
assert!(matches!(
|
||||
events.as_slice(),
|
||||
[ClientEvent::SessionClosed { token: TOKEN, .. }]
|
||||
));
|
||||
assert!(matches!(
|
||||
client.send_probe(),
|
||||
Err(ClientError::AlreadyClosed)
|
||||
));
|
||||
|
||||
let observations = server.observations(2);
|
||||
match &observations[1] {
|
||||
ServerObservation::Close { hmac, token } => {
|
||||
assert!(*hmac);
|
||||
assert_eq!(*token, TOKEN);
|
||||
}
|
||||
other => panic!("expected close observation, got {other:?}"),
|
||||
}
|
||||
server.join();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn hmac_required_server_rejects_missing_or_wrong_client_key() {
|
||||
// Both rejections leave the client timing out, but they are different
|
||||
// failures on the wire: a client with no key presents no HMAC field at
|
||||
// all, while a client with the wrong key presents one that does not
|
||||
// authenticate.
|
||||
for (hmac_key, expected_hmac_flag, expected_bad_hmac) in [
|
||||
(None, false, false),
|
||||
(Some(b"wrong-secret".to_vec()), true, true),
|
||||
] {
|
||||
let server_key = b"compat-secret".to_vec();
|
||||
let server = start_hmac_required_open_drop_server(server_key, Duration::from_millis(250));
|
||||
let mut config = config_for_params(server.addr, &default_params());
|
||||
config.open_timeouts = vec![Duration::from_millis(200)];
|
||||
config.hmac_key = hmac_key;
|
||||
|
||||
let mut client = Client::connect(config).unwrap();
|
||||
assert!(matches!(client.open(), Err(ClientError::OpenTimeout)));
|
||||
|
||||
let observations = server.observations(1);
|
||||
assert_eq!(
|
||||
observations.as_slice(),
|
||||
[ServerObservation::RejectedHmac {
|
||||
hmac: expected_hmac_flag,
|
||||
bad_hmac: expected_bad_hmac,
|
||||
}]
|
||||
);
|
||||
server.join();
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn bad_hmac_echo_reply_is_rejected_without_echo_reply_event() {
|
||||
let key = b"compat-secret".to_vec();
|
||||
let params = default_params();
|
||||
let server = start_bad_hmac_echo_reply_server(params.clone(), key.clone());
|
||||
let mut config = config_for_params(server.addr, ¶ms);
|
||||
config.hmac_key = Some(key);
|
||||
config.socket_config = SocketConfig {
|
||||
recv_timeout: Some(Duration::from_millis(500)),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let mut client = Client::connect(config).unwrap();
|
||||
let outcome = client.open().unwrap();
|
||||
assert_started(outcome, ¶ms);
|
||||
|
||||
let sent = client.send_probe().unwrap();
|
||||
assert_eq!(sent.len(), 1);
|
||||
let events = client.recv_once().unwrap();
|
||||
assert!(matches!(
|
||||
events.as_slice(),
|
||||
[ClientEvent::Warning {
|
||||
kind: WarningKind::MalformedOrUnrelatedPacket,
|
||||
..
|
||||
}]
|
||||
));
|
||||
assert!(!events
|
||||
.iter()
|
||||
.any(|event| matches!(event, ClientEvent::EchoReply { .. })));
|
||||
|
||||
let observations = server.observations(2);
|
||||
assert!(matches!(
|
||||
observations.as_slice(),
|
||||
[
|
||||
ServerObservation::Open { hmac: true, .. },
|
||||
ServerObservation::Echo {
|
||||
hmac: true,
|
||||
token: TOKEN,
|
||||
sequence: 0,
|
||||
..
|
||||
}
|
||||
]
|
||||
));
|
||||
server.join();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn hmac_open_reply_with_bad_hmac_is_ignored_until_timeout() {
|
||||
let key = b"compat-secret".to_vec();
|
||||
let wrong_key = b"wrong-secret".to_vec();
|
||||
let params = default_params();
|
||||
let server = support::start_bad_hmac_open_reply_server(params.clone(), key.clone(), wrong_key);
|
||||
let mut config = config_for_params(server.addr, ¶ms);
|
||||
config.hmac_key = Some(key);
|
||||
|
||||
let mut client = Client::connect(config).unwrap();
|
||||
assert!(matches!(client.open(), Err(ClientError::OpenTimeout)));
|
||||
server.join();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn non_hmac_client_open_does_not_set_hmac_flag() {
|
||||
let params = params_for_modes(
|
||||
irtt_proto::ReceivedStats::None,
|
||||
irtt_proto::StampAt::None,
|
||||
irtt_proto::Clock::Both,
|
||||
);
|
||||
let server = support::start_open_server(params.clone(), None);
|
||||
let mut config = config_for_params(server.addr, ¶ms);
|
||||
config.hmac_key = None;
|
||||
|
||||
let mut client = Client::connect(config).unwrap();
|
||||
let outcome = client.open().unwrap();
|
||||
assert_started(outcome, ¶ms);
|
||||
|
||||
let observations = server.observations(1);
|
||||
assert!(matches!(
|
||||
observations.as_slice(),
|
||||
[ServerObservation::Open { hmac: false, .. }]
|
||||
));
|
||||
server.join();
|
||||
}
|
||||
|
||||
fn assert_started(outcome: OpenOutcome, expected: &irtt_proto::Params) {
|
||||
match outcome {
|
||||
OpenOutcome::Started {
|
||||
token,
|
||||
negotiated,
|
||||
event:
|
||||
ClientEvent::SessionStarted {
|
||||
token: event_token,
|
||||
negotiated: event_negotiated,
|
||||
..
|
||||
},
|
||||
..
|
||||
} => {
|
||||
assert_eq!(token, TOKEN);
|
||||
assert_eq!(event_token, TOKEN);
|
||||
assert_eq!(negotiated.params, *expected);
|
||||
assert_eq!(event_negotiated.params, *expected);
|
||||
}
|
||||
other => panic!("expected started open outcome, got {other:?}"),
|
||||
}
|
||||
}
|
||||
|
||||
fn assert_no_warning_or_error(events: &[ClientEvent]) {
|
||||
assert!(!events.iter().any(|event| {
|
||||
matches!(
|
||||
event,
|
||||
ClientEvent::Warning { .. }
|
||||
| ClientEvent::DuplicateReply { .. }
|
||||
| ClientEvent::LateReply { .. }
|
||||
)
|
||||
}));
|
||||
}
|
||||
|
||||
fn assert_no_duplicate_late_or_warning(events: &[ClientEvent]) {
|
||||
assert_no_warning_or_error(events);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn backend_hmac_correct_key_succeeds() {
|
||||
let key = b"compat-secret".to_vec();
|
||||
let params = default_params();
|
||||
let peer = BackendPeer::start_open_echo(Some(key.clone()));
|
||||
let mut config = config_for_params(peer.addr(), ¶ms);
|
||||
config.hmac_key = Some(key);
|
||||
|
||||
let mut client = Client::connect(config).unwrap();
|
||||
let outcome = client.open().unwrap();
|
||||
assert!(matches!(outcome, OpenOutcome::Started { .. }));
|
||||
|
||||
let sent = client.send_probe().unwrap();
|
||||
assert_eq!(sent.len(), 1);
|
||||
|
||||
let events = client.recv_once().unwrap();
|
||||
assert_eq!(events.len(), 1);
|
||||
assert!(matches!(events[0], ClientEvent::EchoReply { .. }));
|
||||
|
||||
client.close().unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn backend_hmac_required_rejects_missing_or_wrong_client_key() {
|
||||
for hmac_key in [None, Some(b"wrong-secret".to_vec())] {
|
||||
let server_key = b"compat-secret".to_vec();
|
||||
let peer = BackendPeer::start_hmac_required(server_key);
|
||||
let mut config = config_for_params(peer.addr(), &default_params());
|
||||
config.open_timeouts = vec![Duration::from_millis(200)];
|
||||
config.hmac_key = hmac_key;
|
||||
|
||||
let mut client = Client::connect(config).unwrap();
|
||||
assert!(matches!(client.open(), Err(ClientError::OpenTimeout)));
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user