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 message_router;
|
||||||
|
mod pending_answer;
|
||||||
mod reconnect;
|
mod reconnect;
|
||||||
|
|
||||||
use std::{cell::RefCell, rc::Rc};
|
use std::{cell::RefCell, rc::Rc};
|
||||||
@ -6,10 +8,12 @@ use std::{cell::RefCell, rc::Rc};
|
|||||||
use crate::{
|
use crate::{
|
||||||
config::Config, constants::DEFAULT_SIGNALING_URL, models::SignalingMessage, utils::MediaManager,
|
config::Config, constants::DEFAULT_SIGNALING_URL, models::SignalingMessage, utils::MediaManager,
|
||||||
};
|
};
|
||||||
|
use call_flow::{build_offer_message, OfferBuildError};
|
||||||
use dioxus::prelude::*;
|
use dioxus::prelude::*;
|
||||||
use futures::StreamExt;
|
use futures::StreamExt;
|
||||||
use gloo_timers::future::TimeoutFuture;
|
use gloo_timers::future::TimeoutFuture;
|
||||||
use message_router::{Directive as MessageDirective, MessageRouter, RouterState};
|
use message_router::{Directive as MessageDirective, MessageRouter, RouterState};
|
||||||
|
use pending_answer::{resolve_pending_answer, PendingResolution};
|
||||||
use reconnect::{DisconnectReason, ReconnectController};
|
use reconnect::{DisconnectReason, ReconnectController};
|
||||||
use wasm_bindgen::{prelude::Closure, JsCast, JsValue};
|
use wasm_bindgen::{prelude::Closure, JsCast, JsValue};
|
||||||
use wasm_bindgen_futures::spawn_local;
|
use wasm_bindgen_futures::spawn_local;
|
||||||
@ -654,14 +658,20 @@ pub fn SignalingProvider(props: SignalingProviderProps) -> Element {
|
|||||||
let initiator_connection = initiator_connection.clone();
|
let initiator_connection = initiator_connection.clone();
|
||||||
let last_error_signal = last_error.clone();
|
let last_error_signal = last_error.clone();
|
||||||
use_effect(move || {
|
use_effect(move || {
|
||||||
if let Some(answer_sdp) = pending.read().clone() {
|
let pending_value = pending.read().clone();
|
||||||
if let Some(pc) = initiator_connection.read().as_ref() {
|
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 pc_clone = pc.clone();
|
||||||
let answer_clone = answer_sdp.clone();
|
|
||||||
let mut pending_signal = pending.clone();
|
|
||||||
let err_signal = last_error_signal.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 {
|
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!(
|
Ok(_) => log::info!(
|
||||||
"✅ Gepufferte Answer erfolgreich gesetzt auf Initiator-PC"
|
"✅ Gepufferte Answer erfolgreich gesetzt auf Initiator-PC"
|
||||||
),
|
),
|
||||||
@ -676,6 +686,9 @@ pub fn SignalingProvider(props: SignalingProviderProps) -> Element {
|
|||||||
}
|
}
|
||||||
pending_signal.set(None);
|
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 {
|
match MediaManager::create_offer(&pc).await {
|
||||||
Ok(offer_sdp) => {
|
Ok(offer_sdp) => {
|
||||||
if let Some(socket) = websocket_signal.read().as_ref() {
|
if let Some(socket) = websocket_signal.read().as_ref() {
|
||||||
let msg = SignalingMessage {
|
let from_id = peer_id_signal.read().clone();
|
||||||
from: peer_id_signal.read().clone(),
|
let to_id = remote_id_signal.read().clone();
|
||||||
to: remote_id_signal.read().clone(),
|
|
||||||
msg_type: "offer".to_string(),
|
|
||||||
data: offer_sdp,
|
|
||||||
};
|
|
||||||
|
|
||||||
if let Ok(json) = serde_json::to_string(&msg) {
|
match build_offer_message(&from_id, &to_id, &offer_sdp) {
|
||||||
let _ = socket.send_with_str(&json);
|
Ok(msg) => {
|
||||||
log::info!(
|
if let Ok(json) = serde_json::to_string(&msg) {
|
||||||
"Offer dispatched to {}",
|
let target = msg.to.clone();
|
||||||
remote_id_signal.read().as_str()
|
let _ = socket.send_with_str(&json);
|
||||||
);
|
log::info!("Offer dispatched to {}", target);
|
||||||
let mut in_call_writer = in_call_signal.clone();
|
let mut in_call_writer = in_call_signal.clone();
|
||||||
in_call_writer.set(true);
|
in_call_writer.set(true);
|
||||||
error_signal.set(None);
|
error_signal.set(None);
|
||||||
} else {
|
} else {
|
||||||
error_signal.set(Some("Failed to encode offer".to_string()));
|
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 {
|
} else {
|
||||||
error_signal.set(Some(
|
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