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:
RustDeskandClaude Opus 5.5 authored and GitHub committed 2026-10-14 14:45:22 +08:00
1 parent 76ab280ae9
commit 9997b7a3b8
94 files changed
+66 -9405

No files matched your search

-370
View File
@@ -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());
}
}