//! 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_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 = tokio::fs::File::from_std( std::fs::OpenOptions::new() .read(true) .write(kb_writable) .create(kb_writable) .truncate(kb_writable) .open(&kb_path) .expect("open kb"), ); let ms = tokio::fs::File::from_std( std::fs::OpenOptions::new() .read(true) .write(ms_writable) .create(ms_writable) .truncate(ms_writable) .open(&ms_path) .expect("open ms"), ); let handler = with_var("LESAVKA_CAPTURE_POWER_UNIT", Some("none"), || Handler { kb: std::sync::Arc::new(tokio::sync::Mutex::new(Some(kb))), ms: std::sync::Arc::new(tokio::sync::Mutex::new(Some(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(), eye_hubs: std::sync::Arc::new( tokio::sync::Mutex::new(std::collections::HashMap::new()), ), }); (dir, handler) } fn build_handler_for_tests() -> (tempfile::TempDir, Handler) { build_handler_for_tests_with_modes(true, true) } #[test] #[serial] fn reopen_hid_tolerates_missing_hid_endpoints() { let (_dir, handler) = build_handler_for_tests(); let missing_dir = tempdir().expect("missing hid dir"); let hid_dir = missing_dir.path().join("missing"); with_var( "LESAVKA_HID_DIR", Some(hid_dir.to_string_lossy().to_string()), || { let rt = tokio::runtime::Runtime::new().expect("runtime"); let result = rt.block_on(handler.reopen_hid()); assert!( result.is_ok(), "reopen_hid should keep the server alive while HID endpoints are absent" ); let endpoints = rt.block_on(async { ( handler.kb.lock().await.is_none(), handler.ms.lock().await.is_none(), ) }); assert_eq!(endpoints, (true, true)); }, ); } #[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, requested_width: 0, requested_height: 0, requested_fps: 0, source_id: None, })) .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 capture_video_right_eye_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: 1, max_bitrate: 3_000, requested_width: 0, requested_height: 0, requested_fps: 0, source_id: None, })) .await }); let err = match result { Ok(_) => panic!("missing right-eye camera device should fail"), Err(err) => err, }; assert_eq!(err.code(), tonic::Code::Internal); } #[test] #[cfg(coverage)] #[serial] fn capture_video_returns_stream_when_coverage_source_is_overridden() { let (_dir, handler) = build_handler_for_tests(); let rt = tokio::runtime::Runtime::new().expect("runtime"); with_var( "LESAVKA_TEST_VIDEO_SOURCE", Some("/dev/lesavka_l_eye"), || { let mut stream = rt .block_on(async { handler .capture_video(tonic::Request::new(MonitorRequest { id: 0, max_bitrate: 3_000, requested_width: 0, requested_height: 0, requested_fps: 0, source_id: None, })) .await }) .expect("coverage video stream should succeed") .into_inner(); let packet = rt .block_on(async { stream.next().await }) .expect("stream item") .expect("packet"); assert_eq!(packet.id, 0); assert!(!packet.data.is_empty()); drop(stream); rt.block_on(async { tokio::time::sleep(std::time::Duration::from_millis(50)).await; }); }, ); } #[test] #[cfg(coverage)] #[serial] fn mirrored_capture_requests_share_one_source_hub_but_keep_logical_ids() { let (_dir, handler) = build_handler_for_tests(); let rt = tokio::runtime::Runtime::new().expect("runtime"); with_var( "LESAVKA_TEST_VIDEO_SOURCE", Some("/dev/lesavka_r_eye"), || { let (left_packet, right_packet, hub_count) = rt.block_on(async { let mut left = handler .capture_video(tonic::Request::new(MonitorRequest { id: 0, max_bitrate: 3_000, requested_width: 1920, requested_height: 1080, requested_fps: 60, source_id: Some(1), })) .await .expect("left stream") .into_inner(); let mut right = handler .capture_video(tonic::Request::new(MonitorRequest { id: 1, max_bitrate: 3_000, requested_width: 1920, requested_height: 1080, requested_fps: 60, source_id: Some(1), })) .await .expect("right stream") .into_inner(); let left_packet = left.next().await.expect("left item").expect("left packet"); let right_packet = right .next() .await .expect("right item") .expect("right packet"); drop(left); drop(right); tokio::time::sleep(std::time::Duration::from_millis(50)).await; let hub_count = handler.eye_hub_count().await; (left_packet, right_packet, hub_count) }); assert_eq!(left_packet.id, 0); assert_eq!(right_packet.id, 1); assert!(!left_packet.data.is_empty()); assert!(!right_packet.data.is_empty()); assert_eq!(hub_count, 1); }, ); } #[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 paste_text_returns_structured_error_when_hid_write_fails() { let (_dir, handler) = build_handler_for_tests_with_modes(false, true); 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 structured reply") .into_inner(); assert!(!reply.ok); assert!(!reply.error.is_empty()); }); }, ); } #[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, requested_width: 0, requested_height: 0, requested_fps: 0, source_id: None, }; 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); } #[test] #[cfg(coverage)] #[serial] fn capture_power_rpcs_surface_stub_snapshot_and_manual_modes() { let (_dir, handler) = build_handler_for_tests(); let rt = tokio::runtime::Runtime::new().expect("runtime"); let snapshot = rt .block_on(async { handler .get_capture_power(tonic::Request::new(Empty {})) .await }) .expect("capture power snapshot") .into_inner(); assert!(snapshot.available); assert!(!snapshot.enabled); assert_eq!(snapshot.mode, "auto"); let forced_on = rt .block_on(async { handler .set_capture_power(tonic::Request::new(SetCapturePowerRequest { enabled: true, command: CapturePowerCommand::ForceOn as i32, })) .await }) .expect("force capture power on") .into_inner(); assert!(forced_on.available); assert!(forced_on.enabled); assert_eq!(forced_on.mode, "forced-on"); let forced_off = rt .block_on(async { handler .set_capture_power(tonic::Request::new(SetCapturePowerRequest { enabled: false, command: CapturePowerCommand::ForceOff as i32, })) .await }) .expect("force capture power off") .into_inner(); assert!(forced_off.available); assert!(!forced_off.enabled); assert_eq!(forced_off.mode, "forced-off"); let auto = rt .block_on(async { handler .set_capture_power(tonic::Request::new(SetCapturePowerRequest { enabled: false, command: CapturePowerCommand::Auto as i32, })) .await }) .expect("return capture power to auto") .into_inner(); assert!(auto.available); assert!(!auto.enabled); assert_eq!(auto.mode, "auto"); let legacy_fallback = rt .block_on(async { handler .set_capture_power(tonic::Request::new(SetCapturePowerRequest { enabled: true, command: CapturePowerCommand::Unspecified as i32, })) .await }) .expect("legacy bool fallback") .into_inner(); assert!(legacy_fallback.available); assert!(legacy_fallback.enabled); assert_eq!(legacy_fallback.mode, "forced-on"); } #[test] #[cfg(coverage)] #[serial] fn reset_usb_tolerates_missing_hid_after_successful_cycle() { let dir = tempdir().expect("tempdir"); std::fs::write(dir.path().join("hidg0.bin"), "").expect("create kb file"); std::fs::write(dir.path().join("hidg1.bin"), "").expect("create ms file"); std::fs::create_dir_all(dir.path().join("sys/class/udc/fake-ctrl.usb")) .expect("create udc dir"); std::fs::create_dir_all(dir.path().join("cfg/lesavka")).expect("create cfg dir"); std::fs::write( dir.path().join("sys/class/udc/fake-ctrl.usb/state"), "configured\n", ) .expect("write state"); std::fs::write(dir.path().join("cfg/lesavka/UDC"), "fake-ctrl.usb\n").expect("write udc"); let kb = tokio::fs::File::from_std( std::fs::OpenOptions::new() .read(true) .write(true) .open(dir.path().join("hidg0.bin")) .expect("open kb"), ); let ms = tokio::fs::File::from_std( std::fs::OpenOptions::new() .read(true) .write(true) .open(dir.path().join("hidg1.bin")) .expect("open ms"), ); with_var( "LESAVKA_GADGET_SYSFS_ROOT", Some(dir.path().join("sys").to_string_lossy().to_string()), || { with_var( "LESAVKA_GADGET_CONFIGFS_ROOT", Some(dir.path().join("cfg").to_string_lossy().to_string()), || { let handler = Handler { kb: std::sync::Arc::new(tokio::sync::Mutex::new(Some(kb))), ms: std::sync::Arc::new(tokio::sync::Mutex::new(Some(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(), eye_hubs: std::sync::Arc::new(tokio::sync::Mutex::new( std::collections::HashMap::new(), )), }; with_var( "LESAVKA_HID_DIR", Some(dir.path().join("missing").to_string_lossy().to_string()), || { let rt = tokio::runtime::Runtime::new().expect("runtime"); let reply = rt .block_on(async { handler.reset_usb(tonic::Request::new(Empty {})).await }) .expect("missing HID should not fail USB reset") .into_inner(); assert!(reply.ok); }, ); }, ); }, ); } }