From 4d4b357cbccfdbf746d838676fabc4d4bc868c7a Mon Sep 17 00:00:00 2001 From: ghost Date: Tue, 4 Nov 2025 17:44:20 +0100 Subject: [PATCH] test(signaling): validate offer builder and pending answers --- src/services/signaling.rs | 62 +++++++++++++++--------- src/services/signaling/call_flow.rs | 45 +++++++++++++++++ src/services/signaling/pending_answer.rs | 39 +++++++++++++++ 3 files changed, 124 insertions(+), 22 deletions(-) create mode 100644 src/services/signaling/call_flow.rs create mode 100644 src/services/signaling/pending_answer.rs diff --git a/src/services/signaling.rs b/src/services/signaling.rs index 71325db..54ac11d 100644 --- a/src/services/signaling.rs +++ b/src/services/signaling.rs @@ -1,4 +1,6 @@ +mod call_flow; mod message_router; +mod pending_answer; mod reconnect; use std::{cell::RefCell, rc::Rc}; @@ -6,10 +8,12 @@ use std::{cell::RefCell, rc::Rc}; use crate::{ config::Config, constants::DEFAULT_SIGNALING_URL, models::SignalingMessage, utils::MediaManager, }; +use call_flow::{build_offer_message, OfferBuildError}; use dioxus::prelude::*; use futures::StreamExt; use gloo_timers::future::TimeoutFuture; use message_router::{Directive as MessageDirective, MessageRouter, RouterState}; +use pending_answer::{resolve_pending_answer, PendingResolution}; use reconnect::{DisconnectReason, ReconnectController}; use wasm_bindgen::{prelude::Closure, JsCast, JsValue}; use wasm_bindgen_futures::spawn_local; @@ -654,14 +658,20 @@ pub fn SignalingProvider(props: SignalingProviderProps) -> Element { let initiator_connection = initiator_connection.clone(); let last_error_signal = last_error.clone(); use_effect(move || { - if let Some(answer_sdp) = pending.read().clone() { - if let Some(pc) = initiator_connection.read().as_ref() { + let pending_value = pending.read().clone(); + let initiator_pc = initiator_connection.read().as_ref().cloned(); + + if let PendingResolution::Apply(answer_sdp) = + resolve_pending_answer(pending_value.clone(), initiator_pc.is_some()) + { + if let Some(pc) = initiator_pc { let pc_clone = pc.clone(); - let answer_clone = answer_sdp.clone(); - let mut pending_signal = pending.clone(); let err_signal = last_error_signal.clone(); + let mut clear_pending = pending.clone(); + clear_pending.set(None); + let mut pending_signal = pending.clone(); spawn_local(async move { - match MediaManager::handle_answer(&pc_clone, &answer_clone).await { + match MediaManager::handle_answer(&pc_clone, &answer_sdp).await { Ok(_) => log::info!( "✅ Gepufferte Answer erfolgreich gesetzt auf Initiator-PC" ), @@ -676,6 +686,9 @@ pub fn SignalingProvider(props: SignalingProviderProps) -> Element { } pending_signal.set(None); }); + } else { + let mut pending_signal = pending.clone(); + pending_signal.set(Some(answer_sdp)); } } }); @@ -883,24 +896,29 @@ pub fn SignalingProvider(props: SignalingProviderProps) -> Element { match MediaManager::create_offer(&pc).await { Ok(offer_sdp) => { if let Some(socket) = websocket_signal.read().as_ref() { - let msg = SignalingMessage { - from: peer_id_signal.read().clone(), - to: remote_id_signal.read().clone(), - msg_type: "offer".to_string(), - data: offer_sdp, - }; + let from_id = peer_id_signal.read().clone(); + let to_id = remote_id_signal.read().clone(); - if let Ok(json) = serde_json::to_string(&msg) { - let _ = socket.send_with_str(&json); - log::info!( - "Offer dispatched to {}", - remote_id_signal.read().as_str() - ); - let mut in_call_writer = in_call_signal.clone(); - in_call_writer.set(true); - error_signal.set(None); - } else { - error_signal.set(Some("Failed to encode offer".to_string())); + match build_offer_message(&from_id, &to_id, &offer_sdp) { + Ok(msg) => { + if let Ok(json) = serde_json::to_string(&msg) { + let target = msg.to.clone(); + let _ = socket.send_with_str(&json); + log::info!("Offer dispatched to {}", target); + let mut in_call_writer = in_call_signal.clone(); + in_call_writer.set(true); + error_signal.set(None); + } else { + error_signal + .set(Some("Failed to encode offer".to_string())); + } + } + Err(OfferBuildError::EmptyRemoteId) => { + log::warn!( + "Remote ID missing after sanitizing – cannot dispatch offer" + ); + error_signal.set(Some("Target ID fehlt".to_string())); + } } } else { error_signal.set(Some( diff --git a/src/services/signaling/call_flow.rs b/src/services/signaling/call_flow.rs new file mode 100644 index 0000000..ca3b7c9 --- /dev/null +++ b/src/services/signaling/call_flow.rs @@ -0,0 +1,45 @@ +use crate::models::SignalingMessage; + +#[derive(Debug, PartialEq, Eq)] +pub enum OfferBuildError { + EmptyRemoteId, +} + +/// Builds the signaling message for an SDP offer, ensuring fields are sanitized. +pub fn build_offer_message( + peer_id: &str, + remote_id: &str, + offer_sdp: &str, +) -> Result { + let trimmed_remote = remote_id.trim(); + if trimmed_remote.is_empty() { + return Err(OfferBuildError::EmptyRemoteId); + } + + Ok(SignalingMessage { + from: peer_id.trim().to_string(), + to: trimmed_remote.to_string(), + msg_type: "offer".to_string(), + data: offer_sdp.to_string(), + }) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn trims_ids_and_sets_offer_type() { + let msg = build_offer_message(" peer-1 ", " remote-2 ", "sdp").unwrap(); + assert_eq!(msg.from, "peer-1"); + assert_eq!(msg.to, "remote-2"); + assert_eq!(msg.msg_type, "offer"); + assert_eq!(msg.data, "sdp"); + } + + #[test] + fn rejects_empty_remote_id() { + let err = build_offer_message("peer", " ", "sdp").unwrap_err(); + assert_eq!(err, OfferBuildError::EmptyRemoteId); + } +} diff --git a/src/services/signaling/pending_answer.rs b/src/services/signaling/pending_answer.rs new file mode 100644 index 0000000..31ae9aa --- /dev/null +++ b/src/services/signaling/pending_answer.rs @@ -0,0 +1,39 @@ +#[derive(Debug, PartialEq, Eq)] +pub enum PendingResolution { + None, + Apply(String), +} + +/// Determines what to do with a buffered answer based on initiator availability. +pub fn resolve_pending_answer( + pending: Option, + initiator_present: bool, +) -> PendingResolution { + match (pending, initiator_present) { + (Some(answer), true) => PendingResolution::Apply(answer), + _ => PendingResolution::None, + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn apply_when_initiator_present() { + let resolution = resolve_pending_answer(Some("answer".into()), true); + assert_eq!(resolution, PendingResolution::Apply("answer".into())); + } + + #[test] + fn keep_buffer_when_initiator_missing() { + let resolution = resolve_pending_answer(Some("answer".into()), false); + assert_eq!(resolution, PendingResolution::None); + } + + #[test] + fn nothing_when_no_pending_answer() { + let resolution = resolve_pending_answer(None, true); + assert_eq!(resolution, PendingResolution::None); + } +}