lesavka/testing/tests/server_main_binary_contract.rs

221 lines
7.7 KiB
Rust
Raw Normal View History

//! Integration coverage for server binary startup and RPC guards.
//!
//! Scope: include sanitized `server/src/main.rs` and execute startup/runtime
//! error branches directly so llvm-cov attributes lines to the entrypoint file.
//! Targets: `server/src/main.rs`.
//! Why: subprocess-only coverage does not reliably move binary file coverage.
#[allow(warnings)]
mod server_main_binary {
include!(env!("LESAVKA_SERVER_MAIN_SRC"));
use lesavka_common::lesavka::relay_client::RelayClient;
use serial_test::serial;
use temp_env::with_var;
use tempfile::tempdir;
fn build_handler_for_tests_with_modes(
kb_writable: bool,
ms_writable: bool,
) -> (tempfile::TempDir, Handler) {
let dir = tempdir().expect("tempdir");
let kb_path = dir.path().join("hidg0.bin");
let ms_path = dir.path().join("hidg1.bin");
std::fs::write(&kb_path, []).expect("create kb file");
std::fs::write(&ms_path, []).expect("create ms file");
let kb_std = std::fs::OpenOptions::new()
.read(true)
.write(kb_writable)
.create(kb_writable)
.truncate(kb_writable)
.open(&kb_path)
.expect("open kb");
let ms_std = std::fs::OpenOptions::new()
.read(true)
.write(ms_writable)
.create(ms_writable)
.truncate(ms_writable)
.open(&ms_path)
.expect("open ms");
let kb = tokio::fs::File::from_std(kb_std);
let ms = tokio::fs::File::from_std(ms_std);
(
dir,
Handler {
kb: std::sync::Arc::new(tokio::sync::Mutex::new(kb)),
ms: std::sync::Arc::new(tokio::sync::Mutex::new(ms)),
gadget: UsbGadget::new("lesavka"),
did_cycle: std::sync::Arc::new(std::sync::atomic::AtomicBool::new(false)),
camera_rt: std::sync::Arc::new(CameraRuntime::new()),
capture_power: CapturePowerManager::new(),
},
)
}
fn build_handler_for_tests() -> (tempfile::TempDir, Handler) {
build_handler_for_tests_with_modes(true, true)
}
async fn connect_with_retry(addr: std::net::SocketAddr) -> tonic::transport::Channel {
let endpoint = tonic::transport::Endpoint::from_shared(format!("http://{addr}"))
.expect("endpoint")
.tcp_nodelay(true);
for _ in 0..40 {
if let Ok(channel) = endpoint.clone().connect().await {
return channel;
}
tokio::time::sleep(std::time::Duration::from_millis(25)).await;
}
panic!("failed to connect to local tonic server");
}
#[test]
#[serial]
fn main_returns_error_without_hid_nodes() {
with_var("LESAVKA_DISABLE_UVC", Some("1"), || {
with_var("LESAVKA_ALLOW_GADGET_CYCLE", None::<&str>, || {
let _ = std::panic::catch_unwind(main);
});
});
}
#[test]
#[serial]
fn main_covers_external_uvc_helper_branch_before_failing_without_hid_nodes() {
with_var("LESAVKA_DISABLE_UVC", None::<&str>, || {
with_var("LESAVKA_UVC_EXTERNAL", Some("1"), || {
with_var("LESAVKA_ALLOW_GADGET_CYCLE", None::<&str>, || {
let _ = std::panic::catch_unwind(main);
});
});
});
}
#[test]
#[serial]
fn main_spawns_uvc_supervisor_branch_before_failing_without_hid_nodes() {
with_var("LESAVKA_DISABLE_UVC", None::<&str>, || {
with_var("LESAVKA_UVC_EXTERNAL", None::<&str>, || {
with_var(
"LESAVKA_UVC_CTRL_BIN",
Some("/definitely/missing/uvc-helper"),
|| {
with_var("LESAVKA_ALLOW_GADGET_CYCLE", Some("1"), || {
let _ = std::panic::catch_unwind(main);
});
},
);
});
});
}
#[test]
#[serial]
fn handler_new_fails_fast_without_hid_endpoints() {
with_var("LESAVKA_ALLOW_GADGET_CYCLE", None::<&str>, || {
let rt = tokio::runtime::Runtime::new().expect("runtime");
let result = rt.block_on(Handler::new(UsbGadget::new("lesavka")));
let err = match result {
Ok(_) => panic!("missing hid nodes should fail startup"),
Err(err) => err,
};
let msg = err.to_string();
assert!(msg.contains("/dev/hidg0") || msg.contains("No such file"));
});
}
#[test]
#[serial]
fn handler_new_attempts_cycle_when_explicitly_enabled() {
with_var("LESAVKA_ALLOW_GADGET_CYCLE", Some("1"), || {
let rt = tokio::runtime::Runtime::new().expect("runtime");
let result = rt.block_on(Handler::new(UsbGadget::new("lesavka")));
assert!(
result.is_err(),
"startup should still fail without hid endpoints even after cycle attempt"
);
});
}
#[test]
#[serial]
fn capture_video_rejects_invalid_monitor_id() {
let (_dir, handler) = build_handler_for_tests();
let rt = tokio::runtime::Runtime::new().expect("runtime");
let result = rt.block_on(async {
handler
.capture_video(tonic::Request::new(MonitorRequest {
id: 9,
max_bitrate: 4_000,
requested_width: 0,
requested_height: 0,
requested_fps: 0,
2026-04-17 11:51:19 -03:00
prefer_reencode: false,
}))
.await
});
let err = match result {
Ok(_) => panic!("invalid monitor id must be rejected"),
Err(err) => err,
};
assert_eq!(err.code(), tonic::Code::InvalidArgument);
}
#[test]
#[serial]
fn paste_text_rejects_plaintext_requests() {
let (_dir, handler) = build_handler_for_tests();
let req = PasteRequest {
nonce: vec![],
data: vec![],
encrypted: false,
};
let rt = tokio::runtime::Runtime::new().expect("runtime");
let result = rt.block_on(async { handler.paste_text(tonic::Request::new(req)).await });
let err = match result {
Ok(_) => panic!("plaintext paste request should be rejected"),
Err(err) => err,
};
assert_eq!(err.code(), tonic::Code::Unauthenticated);
}
#[test]
#[serial]
fn reset_usb_returns_internal_status_when_cycle_fails() {
let (_dir, handler) = build_handler_for_tests();
let rt = tokio::runtime::Runtime::new().expect("runtime");
let result = rt.block_on(async { handler.reset_usb(tonic::Request::new(Empty {})).await });
let err = match result {
Ok(_) => panic!("cycle should fail without gadget sysfs"),
Err(err) => err,
};
assert_eq!(err.code(), tonic::Code::Internal);
}
#[test]
#[serial]
fn capture_audio_returns_internal_status_when_sink_is_missing() {
let (_dir, handler) = build_handler_for_tests();
let req = MonitorRequest {
id: 0,
max_bitrate: 0,
requested_width: 0,
requested_height: 0,
requested_fps: 0,
2026-04-17 11:51:19 -03:00
prefer_reencode: false,
};
let rt = tokio::runtime::Runtime::new().expect("runtime");
let result = rt.block_on(async { handler.capture_audio(tonic::Request::new(req)).await });
let err = match result {
Ok(_) => panic!("missing ALSA source should fail"),
Err(err) => err,
};
assert_eq!(err.code(), tonic::Code::Internal);
}
}