test(signaling): validate offer builder and pending answers
This commit is contained in:
parent
2bdf4789bd
commit
4d4b357cbc
@ -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(
|
||||
|
||||
45
src/services/signaling/call_flow.rs
Normal file
45
src/services/signaling/call_flow.rs
Normal 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);
|
||||
}
|
||||
}
|
||||
39
src/services/signaling/pending_answer.rs
Normal file
39
src/services/signaling/pending_answer.rs
Normal 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);
|
||||
}
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user