mirror of
https://github.com/rustdesk/rustdesk.git
synced 2026-11-04 00:33:12 +00:00
test: remove tests that do not pin user-visible behavior (#16539)
* test: remove tests that do not pin user-visible behavior Apply the AGENTS.md test rules to the existing Rust and Flutter suites. A test is kept when it is the only coverage of user- or API-visible behavior, the regression test of a reported or fixed bug, a security boundary (one test per rejection class), or a check against an external reference. Tests that assert private state or the order of internal steps, pin review-round corner cases, or repeat what a kept test already covers are removed. Rust: 679 -> 298 tests. Dart: 118 -> 45 tests. Test-only helpers left without callers go with them (applyLastOnlineStateForTesting, terminalKeyboardRowWidth, debugBufferedInputCount, tokio's test-util dev feature), and cfg gates and visibility that existed only for the deleted tests are narrowed. Runtime behavior is unchanged. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WS1HTjp6UQuG43KEcQPq1R * test: keep tests that pin security and user-visible behavior Restore six tests that review showed are the only coverage of their behavior: - update metadata rejects an artifact file name with path components; - ICE candidates from another session are dropped; - KCP delivers every multi-segment frame queued before a close; - set_files rejects an empty name in a multi-file transfer; - a lopsided elongated cursor keeps its centre hotspot (#16242); - toggling Show hidden during a pending same-path read issues a new read. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WS1HTjp6UQuG43KEcQPq1R * test: keep offline update and key-exchange downgrade rejections Restore three tests that are the only coverage of their rejection class: - offline update metadata rejects a signed artifact with an http or credential-bearing URL; - offline verification rejects a renamed local file and a version that does not match the release; - a key-exchange pick lowered in transit leaves both sides unable to decrypt, so a v0 fallback after a failed v1 decrypt cannot pass unnoticed. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WS1HTjp6UQuG43KEcQPq1R * test: keep the decomposed-grapheme and reconnect-cancel tests Each catches a plausible wrong change that passes every kept test: mapping the first scalar under a locked Ctrl turns é into \x05, and a reconnect that does not fail pending reads leaves them hanging until the 30 s timeout. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WS1HTjp6UQuG43KEcQPq1R --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
76ab280ae9
commit
9997b7a3b8
94 files changed
+66
-9405
No files matched your search
-370
@@ -3173,55 +3173,6 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
// ThrottledInterval tick at the same time as tokio interval, if no sleeps
|
||||
#[allow(non_snake_case)]
|
||||
#[tokio::test]
|
||||
async fn test_RustDesk_interval() {
|
||||
let base_intervals = [interval_maker, interval_at_maker];
|
||||
for maker in base_intervals.into_iter() {
|
||||
let mut tokio_timer = maker();
|
||||
let mut tokio_times = Vec::new();
|
||||
let mut timer = rustdesk_interval(maker());
|
||||
let mut times = Vec::new();
|
||||
loop {
|
||||
tokio::select! {
|
||||
_ = timer.tick() => {
|
||||
if tokio_times.len() >= 10 && times.len() >= 10 {
|
||||
break;
|
||||
}
|
||||
times.push(get_timestamp_secs());
|
||||
}
|
||||
_ = tokio_timer.tick() => {
|
||||
if tokio_times.len() >= 10 && times.len() >= 10 {
|
||||
break;
|
||||
}
|
||||
tokio_times.push(get_timestamp_secs());
|
||||
}
|
||||
}
|
||||
}
|
||||
assert_eq!(times, tokio_times);
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_tokio_time_interval_sleep() {
|
||||
let mut timer = interval_maker();
|
||||
let mut times = Vec::new();
|
||||
sleep(Duration::from_secs(3)).await;
|
||||
loop {
|
||||
tokio::select! {
|
||||
_ = timer.tick() => {
|
||||
times.push(get_timestamp_secs());
|
||||
if times.len() == 5 {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
let times2: HashSet<u128> = HashSet::from_iter(times.clone());
|
||||
assert_eq!(times.len(), times2.len() + 3);
|
||||
}
|
||||
|
||||
// ThrottledInterval tick less times than tokio interval, if there're sleeps
|
||||
#[allow(non_snake_case)]
|
||||
#[tokio::test]
|
||||
@@ -3243,72 +3194,11 @@ mod tests {
|
||||
}
|
||||
// No multiple ticks in the `interval` time.
|
||||
// Values in "times" are unique and are less than normal tokio interval.
|
||||
// See previous test (test_tokio_time_interval_sleep) for comparison.
|
||||
let times2: HashSet<u128> = HashSet::from_iter(times.clone());
|
||||
assert_eq!(times.len(), times2.len(), "test: {}", i);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_duration_multiplication() {
|
||||
let dur = Duration::from_secs(1);
|
||||
|
||||
assert_eq!(dur * 2, Duration::from_secs(2));
|
||||
assert_eq!(
|
||||
Duration::from_secs_f64(dur.as_secs_f64() * 0.9),
|
||||
Duration::from_millis(900)
|
||||
);
|
||||
assert_eq!(
|
||||
Duration::from_secs_f64(dur.as_secs_f64() * 0.923),
|
||||
Duration::from_millis(923)
|
||||
);
|
||||
assert_eq!(
|
||||
Duration::from_secs_f64(dur.as_secs_f64() * 0.923 * 1e-3),
|
||||
Duration::from_micros(923)
|
||||
);
|
||||
assert_eq!(
|
||||
Duration::from_secs_f64(dur.as_secs_f64() * 0.923 * 1e-6),
|
||||
Duration::from_nanos(923)
|
||||
);
|
||||
assert_eq!(
|
||||
Duration::from_secs_f64(dur.as_secs_f64() * 0.923 * 1e-9),
|
||||
Duration::from_nanos(1)
|
||||
);
|
||||
assert_eq!(
|
||||
Duration::from_secs_f64(dur.as_secs_f64() * 0.5 * 1e-9),
|
||||
Duration::from_nanos(1)
|
||||
);
|
||||
assert_eq!(
|
||||
Duration::from_secs_f64(dur.as_secs_f64() * 0.499 * 1e-9),
|
||||
Duration::from_nanos(0)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_is_public() {
|
||||
// Test URLs containing "rustdesk.com/"
|
||||
assert!(is_public("https://rustdesk.com/"));
|
||||
assert!(is_public("https://www.rustdesk.com/"));
|
||||
assert!(is_public("https://api.rustdesk.com/v1"));
|
||||
assert!(is_public("https://API.RUSTDESK.COM/v1"));
|
||||
assert!(is_public("https://rustdesk.com/path"));
|
||||
|
||||
// Test URLs ending with "rustdesk.com"
|
||||
assert!(is_public("rustdesk.com"));
|
||||
assert!(is_public("https://rustdesk.com"));
|
||||
assert!(is_public("https://RustDesk.com"));
|
||||
assert!(is_public("http://www.rustdesk.com"));
|
||||
assert!(is_public("https://api.rustdesk.com"));
|
||||
|
||||
// Test non-public URLs
|
||||
assert!(!is_public("https://example.com"));
|
||||
assert!(!is_public("https://custom-server.com"));
|
||||
assert!(!is_public("http://192.168.1.1"));
|
||||
assert!(!is_public("localhost"));
|
||||
assert!(!is_public("https://rustdesk.computer.com"));
|
||||
assert!(!is_public("rustdesk.comhello.com"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_is_public_matches_rustdesk_root_domain() {
|
||||
assert!(is_public("rustdesk.com/"));
|
||||
@@ -3347,112 +3237,6 @@ mod tests {
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_should_throttle_log() {
|
||||
assert!(should_throttle_log("https://example.com/api/heartbeat"));
|
||||
assert!(should_throttle_log(
|
||||
"https://example.com/api/heartbeat?token=secret"
|
||||
));
|
||||
assert!(should_throttle_log("https://example.com/prefix/api/heartbeat"));
|
||||
assert!(!should_throttle_log("https://example.com/api/heartbeat2"));
|
||||
assert!(!should_throttle_log("https://example.com/api/sysinfo"));
|
||||
assert!(!should_throttle_log(
|
||||
"https://example.com/api/sysinfo?next=/api/heartbeat"
|
||||
));
|
||||
assert!(!should_throttle_log("not a url"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_get_tcp_proxy_addr_normalizes_bare_ipv6_host() {
|
||||
struct RestoreCustomRendezvousServer(String);
|
||||
|
||||
impl Drop for RestoreCustomRendezvousServer {
|
||||
fn drop(&mut self) {
|
||||
Config::set_option(
|
||||
keys::OPTION_CUSTOM_RENDEZVOUS_SERVER.to_string(),
|
||||
self.0.clone(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
let _restore = RestoreCustomRendezvousServer(Config::get_option(
|
||||
keys::OPTION_CUSTOM_RENDEZVOUS_SERVER,
|
||||
));
|
||||
Config::set_option(
|
||||
keys::OPTION_CUSTOM_RENDEZVOUS_SERVER.to_string(),
|
||||
"1:2".to_string(),
|
||||
);
|
||||
|
||||
assert_eq!(get_tcp_proxy_addr(), format!("[1:2]:{RENDEZVOUS_PORT}"));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_http_request_via_tcp_proxy_rejects_invalid_header_json() {
|
||||
let result = http_request_via_tcp_proxy("not a url", "get", None, "{").await;
|
||||
assert!(result.is_err());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_http_request_via_tcp_proxy_rejects_non_object_header_json() {
|
||||
let err = http_request_via_tcp_proxy("not a url", "get", None, "[]")
|
||||
.await
|
||||
.unwrap_err()
|
||||
.to_string();
|
||||
assert!(err.contains("HTTP header information parsing failed!"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_json_header_entries_preserves_single_content_type() {
|
||||
let headers = parse_json_header_entries(
|
||||
r#"{"Content-Type":"text/plain","Authorization":"Bearer token"}"#,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(
|
||||
headers
|
||||
.iter()
|
||||
.filter(|entry| entry.name.eq_ignore_ascii_case("Content-Type"))
|
||||
.count(),
|
||||
1
|
||||
);
|
||||
assert_eq!(
|
||||
headers
|
||||
.iter()
|
||||
.find(|entry| entry.name.eq_ignore_ascii_case("Content-Type"))
|
||||
.map(|entry| entry.value.as_str()),
|
||||
Some("text/plain")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_json_header_entries_does_not_add_default_content_type() {
|
||||
let headers = parse_json_header_entries(r#"{"Authorization":"Bearer token"}"#).unwrap();
|
||||
|
||||
assert!(!headers
|
||||
.iter()
|
||||
.any(|entry| entry.name.eq_ignore_ascii_case("Content-Type")));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_simple_header_respects_custom_content_type() {
|
||||
let headers = parse_simple_header("Content-Type: text/plain");
|
||||
|
||||
assert_eq!(
|
||||
headers
|
||||
.iter()
|
||||
.filter(|entry| entry.name.eq_ignore_ascii_case("Content-Type"))
|
||||
.count(),
|
||||
1
|
||||
);
|
||||
assert_eq!(
|
||||
headers
|
||||
.iter()
|
||||
.find(|entry| entry.name.eq_ignore_ascii_case("Content-Type"))
|
||||
.map(|entry| entry.value.as_str()),
|
||||
Some("text/plain")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_simple_header_preserves_non_content_type_header() {
|
||||
let headers = parse_simple_header("Authorization: Bearer token");
|
||||
@@ -3477,22 +3261,6 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_tcp_proxy_log_target_redacts_query_only() {
|
||||
assert_eq!(
|
||||
tcp_proxy_log_target("https://example.com/api/heartbeat?token=secret"),
|
||||
"https://example.com/api/heartbeat"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_tcp_proxy_log_target_brackets_ipv6_host_with_port() {
|
||||
assert_eq!(
|
||||
tcp_proxy_log_target("https://[2001:db8::1]:21114/api/heartbeat?token=secret"),
|
||||
"https://[2001:db8::1]:21114/api/heartbeat"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_http_proxy_response_to_json() {
|
||||
let mut resp = HttpProxyResponse {
|
||||
@@ -3521,38 +3289,6 @@ mod tests {
|
||||
assert!(err.contains("TCP proxy error: dial failed"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_mouse_event_constants_and_mask_layout() {
|
||||
use super::input::*;
|
||||
|
||||
// Verify MOUSE_TYPE constants are unique and within the mask range.
|
||||
let types = [
|
||||
MOUSE_TYPE_MOVE,
|
||||
MOUSE_TYPE_DOWN,
|
||||
MOUSE_TYPE_UP,
|
||||
MOUSE_TYPE_WHEEL,
|
||||
MOUSE_TYPE_TRACKPAD,
|
||||
MOUSE_TYPE_MOVE_RELATIVE,
|
||||
];
|
||||
|
||||
let mut seen = std::collections::HashSet::new();
|
||||
for t in types.iter() {
|
||||
assert!(seen.insert(*t), "Duplicate mouse type: {}", t);
|
||||
assert_eq!(
|
||||
*t & MOUSE_TYPE_MASK,
|
||||
*t,
|
||||
"Mouse type {} exceeds mask {}",
|
||||
t,
|
||||
MOUSE_TYPE_MASK
|
||||
);
|
||||
}
|
||||
|
||||
// The mask layout is: lower 3 bits for type, upper bits for buttons (shifted by 3).
|
||||
let combined_mask = MOUSE_TYPE_DOWN | ((MOUSE_BUTTON_LEFT | MOUSE_BUTTON_RIGHT) << 3);
|
||||
assert_eq!(combined_mask & MOUSE_TYPE_MASK, MOUSE_TYPE_DOWN);
|
||||
assert_eq!(combined_mask >> 3, MOUSE_BUTTON_LEFT | MOUSE_BUTTON_RIGHT);
|
||||
}
|
||||
|
||||
/// A stand-in rendezvous server on loopback: accepts one connection and hands it to `serve`.
|
||||
async fn rendezvous_stub<F, Fut>(serve: F) -> String
|
||||
where
|
||||
@@ -3623,105 +3359,6 @@ mod tests {
|
||||
assert!(!conn.is_secured());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_secure_tcp_required_refuses_a_closed_connection() {
|
||||
let (key, _) = server_key();
|
||||
let host = rendezvous_stub(|s| async move { drop(s) }).await;
|
||||
let mut conn = connect(&host).await;
|
||||
assert!(secure_tcp_required(&mut conn, &key).await.is_err());
|
||||
assert!(!conn.is_secured());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_secure_tcp_required_accepts_a_completed_exchange() {
|
||||
let (key, sk) = server_key();
|
||||
let host = rendezvous_stub(move |mut s| async move {
|
||||
let (eph_pk, eph_sk) = box_::gen_keypair();
|
||||
let mut msg = RendezvousMessage::new();
|
||||
msg.set_key_exchange(KeyExchange {
|
||||
keys: vec![sign::sign(&eph_pk.0, &sk).into()],
|
||||
..Default::default()
|
||||
});
|
||||
s.send(&msg).await.unwrap();
|
||||
// The client's reply must decode to a key with the ephemeral secret half.
|
||||
let reply = s.next_timeout(3000).await.unwrap().unwrap();
|
||||
let reply = RendezvousMessage::parse_from_bytes(&reply).unwrap();
|
||||
let Some(rendezvous_message::Union::KeyExchange(ex)) = reply.union else {
|
||||
panic!("expected the client's key exchange");
|
||||
};
|
||||
hbb_common::tcp::Encrypt::decode(&ex.keys[1], &ex.keys[0], &eph_sk).unwrap();
|
||||
})
|
||||
.await;
|
||||
let mut conn = connect(&host).await;
|
||||
secure_tcp_required(&mut conn, &key).await.unwrap();
|
||||
assert!(conn.is_secured());
|
||||
}
|
||||
|
||||
// The stand-in does what hbbs does at version 1: advertises it and splits the exchanged key
|
||||
// over the same transcript. Neither side's unit tests can catch a client that puts a
|
||||
// different byte string into the transcript than the server does; only a frame crossing
|
||||
// between the two can.
|
||||
#[tokio::test]
|
||||
async fn test_secure_tcp_version_1_keys_match_the_server_both_ways() {
|
||||
let (key, sk) = server_key();
|
||||
let host = rendezvous_stub(move |mut s| async move {
|
||||
let (eph_pk, eph_sk) = box_::gen_keypair();
|
||||
let mut msg = RendezvousMessage::new();
|
||||
msg.set_key_exchange(KeyExchange {
|
||||
keys: vec![sign::sign(&eph_pk.0, &sk).into()],
|
||||
version: 1,
|
||||
..Default::default()
|
||||
});
|
||||
s.send(&msg).await.unwrap();
|
||||
let reply = s.next_timeout(3000).await.unwrap().unwrap();
|
||||
let reply = RendezvousMessage::parse_from_bytes(&reply).unwrap();
|
||||
let Some(rendezvous_message::Union::KeyExchange(ex)) = reply.union else {
|
||||
panic!("expected the client's key exchange");
|
||||
};
|
||||
let shared =
|
||||
hbb_common::tcp::Encrypt::decode(&ex.keys[1], &ex.keys[0], &eph_sk).unwrap();
|
||||
s.set_key_split(
|
||||
shared,
|
||||
false,
|
||||
&hbb_common::tcp::KxTranscript {
|
||||
initiator_pk: &ex.keys[0],
|
||||
responder_pk: &eph_pk.0,
|
||||
advertised: 1,
|
||||
picked: ex.version,
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
// Answers with the request's serial, so the client learns from its own reply that
|
||||
// the request was read under the right key.
|
||||
let request = s.next_timeout(3000).await.unwrap().unwrap();
|
||||
let request = RendezvousMessage::parse_from_bytes(&request).unwrap();
|
||||
let Some(rendezvous_message::Union::TestNatRequest(nat)) = request.union else {
|
||||
panic!("expected the client's nat request");
|
||||
};
|
||||
let mut msg = RendezvousMessage::new();
|
||||
msg.set_test_nat_response(TestNatResponse {
|
||||
port: nat.serial,
|
||||
..Default::default()
|
||||
});
|
||||
s.send(&msg).await.unwrap();
|
||||
})
|
||||
.await;
|
||||
let mut conn = connect(&host).await;
|
||||
secure_tcp_required(&mut conn, &key).await.unwrap();
|
||||
let mut msg = RendezvousMessage::new();
|
||||
msg.set_test_nat_request(TestNatRequest {
|
||||
serial: 7,
|
||||
..Default::default()
|
||||
});
|
||||
conn.send(&msg).await.unwrap();
|
||||
let reply = conn.next_timeout(3000).await.unwrap().unwrap();
|
||||
let reply = RendezvousMessage::parse_from_bytes(&reply).unwrap();
|
||||
let Some(rendezvous_message::Union::TestNatResponse(nat)) = reply.union else {
|
||||
panic!("expected the server's nat response");
|
||||
};
|
||||
assert_eq!(nat.port, 7);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_secure_tcp_legacy_and_signed_servers_exchange_application_data() {
|
||||
for (advertised, signed) in [(0, false), (1, true), (3, true)] {
|
||||
@@ -3936,11 +3573,4 @@ mod tests {
|
||||
// non-WebRTC handshakes.
|
||||
assert_eq!(decode_id_pk(&signed, &pk).unwrap(), (id, their_pk));
|
||||
}
|
||||
|
||||
// The route probe is awaited on the connection path, so whatever it finds - an address, or
|
||||
// no IPv6 route on this machine - it finds without waiting on the network.
|
||||
#[tokio::test]
|
||||
async fn test_ipv6_route_probe_does_not_wait_on_the_network() {
|
||||
assert!(hbb_common::timeout(1_000, test_bind_ipv6()).await.is_ok());
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user