test(signaling): validate offer builder and pending answers

This commit is contained in:
ghost 2025-11-04 17:44:20 +01:00
parent 2bdf4789bd
commit 4d4b357cbc
3 changed files with 124 additions and 22 deletions

View File

@ -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(

View File

@ -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<SignalingMessage, OfferBuildError> {
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);
}
}

View File

@ -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<String>,
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);
}
}