//! 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 serial_test::serial; use temp_env::with_var; use tempfile::tempdir; fn build_handler_for_tests() -> (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 rt = tokio::runtime::Runtime::new().expect("runtime"); let kb = rt .block_on(async { tokio::fs::OpenOptions::new() .create(true) .truncate(true) .write(true) .open(&kb_path) .await }) .expect("open kb"); let ms = rt .block_on(async { tokio::fs::OpenOptions::new() .create(true) .truncate(true) .write(true) .open(&ms_path) .await }) .expect("open ms"); ( 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()), }, ) } #[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 result = main(); assert!(result.is_err(), "startup should fail without /dev/hidg* endpoints"); }); }); } #[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 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, })) .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); } }