//! Integration coverage for the handshake and camera-selection contract. //! //! Scope: exercise the real client handshake against a local server and the //! server camera selection logic that feeds the handshake response. //! Targets: `client/src/handshake.rs`, `server/src/handshake.rs`, and //! `server/src/camera.rs`. //! Why: the handshake path is the narrow entrypoint that decides whether the //! client and server agree on output mode, so it deserves a centralized //! contract test. use lesavka_client::handshake::{PeerCaps, negotiate}; use serial_test::serial; use std::net::TcpListener; use std::time::Duration; use temp_env::with_var; use tokio::runtime::Runtime; use tokio::sync::oneshot; use tonic::transport::Server; async fn negotiate_against_local_server() -> PeerCaps { let listener = TcpListener::bind("127.0.0.1:0").expect("bind local handshake listener"); let addr = listener.local_addr().expect("listener addr"); drop(listener); let (shutdown_tx, shutdown_rx) = oneshot::channel::<()>(); let server = tokio::spawn(async move { Server::builder() .add_service(lesavka_server::handshake::HandshakeSvc::server()) .serve_with_shutdown(addr, async move { let _ = shutdown_rx.await; }) .await .expect("serve handshake server"); }); tokio::time::sleep(Duration::from_millis(50)).await; let caps = negotiate(&format!("http://{addr}")).await; let _ = shutdown_tx.send(()); let _ = server.await; caps } #[test] #[serial] fn handshake_returns_uvc_caps_with_explicit_dimensions_and_fps() { with_var("LESAVKA_CAM_OUTPUT", Some("uvc"), || { with_var("LESAVKA_UVC_WIDTH", Some("1024"), || { with_var("LESAVKA_UVC_HEIGHT", Some("576"), || { with_var("LESAVKA_UVC_FPS", Some("12"), || { let rt = Runtime::new().expect("create runtime"); let caps = rt.block_on(negotiate_against_local_server()); assert!(caps.camera); assert!(caps.microphone); assert_eq!(caps.camera_output, Some(String::from("uvc"))); assert_eq!(caps.camera_codec, Some(String::from("mjpeg"))); assert_eq!(caps.camera_width, Some(1024)); assert_eq!(caps.camera_height, Some(576)); assert_eq!(caps.camera_fps, Some(12)); }); }); }); }); } #[test] #[serial] fn handshake_uses_uvc_interval_when_fps_is_unset() { with_var("LESAVKA_CAM_OUTPUT", Some("uvc"), || { with_var("LESAVKA_UVC_WIDTH", Some("640"), || { with_var("LESAVKA_UVC_HEIGHT", Some("360"), || { with_var("LESAVKA_UVC_INTERVAL", Some("250000"), || { let rt = Runtime::new().expect("create runtime"); let caps = rt.block_on(negotiate_against_local_server()); assert_eq!(caps.camera_output, Some(String::from("uvc"))); assert_eq!(caps.camera_codec, Some(String::from("mjpeg"))); assert_eq!(caps.camera_width, Some(640)); assert_eq!(caps.camera_height, Some(360)); assert_eq!(caps.camera_fps, Some(40)); assert!(caps.camera); assert!(caps.microphone); }); }); }); }); } #[test] #[serial] fn handshake_returns_hdmi_caps_with_h264_codec() { with_var("LESAVKA_CAM_OUTPUT", Some("hdmi"), || { with_var("LESAVKA_DISABLE_UAC", Some("1"), || { let rt = Runtime::new().expect("create runtime"); let caps = rt.block_on(negotiate_against_local_server()); assert_eq!(caps.camera_output, Some(String::from("hdmi"))); assert_eq!(caps.camera_codec, Some(String::from("h264"))); assert_eq!(caps.camera_fps, Some(30)); assert!(!caps.microphone); assert!(caps.camera); assert!(matches!(caps.camera_width, Some(1280) | Some(1920))); assert!(matches!(caps.camera_height, Some(720) | Some(1080))); }); }); } #[test] #[serial] fn handshake_auto_mode_falls_back_to_a_valid_camera_configuration() { with_var("LESAVKA_CAM_OUTPUT", Some("auto"), || { let rt = Runtime::new().expect("create runtime"); let caps = rt.block_on(negotiate_against_local_server()); match caps.camera_output.as_deref() { Some("uvc") => { assert_eq!(caps.camera_codec.as_deref(), Some("mjpeg")); assert!(matches!(caps.camera_width, Some(1280) | Some(640))); assert!(matches!(caps.camera_height, Some(720) | Some(360))); } Some("hdmi") => { assert_eq!(caps.camera_codec.as_deref(), Some("h264")); assert!(matches!(caps.camera_width, Some(1280) | Some(1920))); assert!(matches!(caps.camera_height, Some(720) | Some(1080))); } other => panic!("unexpected camera output: {other:?}"), } assert!(caps.camera); }); }