//! Integration coverage for server main RPC handler branches. //! //! Scope: include `server/src/main.rs` and exercise additional RPC paths that //! are awkward to hit from process-level tests. //! Targets: `server/src/main.rs`. //! Why: keep handler-side error/reply behavior stable without HID hardware. #[allow(warnings)] mod server_main_rpc { 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 kb = tokio::fs::File::from_std( std::fs::OpenOptions::new() .read(true) .write(true) .open(&kb_path) .expect("open kb"), ); let ms = tokio::fs::File::from_std( std::fs::OpenOptions::new() .read(true) .write(true) .open(&ms_path) .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()), capture_power: CapturePowerManager::new(), }, ) } #[test] #[serial] fn reopen_hid_returns_error_without_hid_endpoints() { let (_dir, handler) = build_handler_for_tests(); let rt = tokio::runtime::Runtime::new().expect("runtime"); let result = rt.block_on(handler.reopen_hid()); assert!(result.is_err(), "reopen_hid should fail without /dev/hidg*"); } #[test] #[serial] fn capture_video_valid_monitor_surfaces_internal_error_without_device() { 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: 0, max_bitrate: 3_000, })) .await }); let err = match result { Ok(_) => panic!("missing camera device should fail"), Err(err) => err, }; assert_eq!(err.code(), tonic::Code::Internal); } #[test] #[serial] fn paste_text_accepts_encrypted_payload_and_returns_reply() { let (_dir, handler) = build_handler_for_tests(); with_var( "LESAVKA_PASTE_KEY", Some("hex:00112233445566778899aabbccddeeff00112233445566778899aabbccddeeff"), || { with_var("LESAVKA_PASTE_DELAY_MS", Some("0"), || { let req = lesavka_client::paste::build_paste_request("hello").expect("build request"); let rt = tokio::runtime::Runtime::new().expect("runtime"); let reply = rt .block_on(async { handler.paste_text(tonic::Request::new(req)).await }) .expect("paste rpc should return reply") .into_inner(); assert!( reply.ok || !reply.error.is_empty(), "paste path should execute and return a structured reply" ); }); }, ); } #[test] #[serial] fn capture_audio_accepts_secondary_monitor_id_and_fails_internally_without_sink() { let (_dir, handler) = build_handler_for_tests(); let req = MonitorRequest { id: 1, max_bitrate: 0, }; 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); } }