1use std::future::Future;
64use std::sync::Arc;
65use std::time::Duration;
66
67use rsip::prelude::UntypedHeader;
68use rsip::Header;
69use tokio::select;
70use tokio::sync::Notify;
71use tokio_util::sync::CancellationToken;
72use tracing::{debug, info, warn};
73
74type BoxError = Box<dyn std::error::Error + Send + Sync>;
75
76pub const MIN_SESSION_EXPIRES_SECS: u32 = 90;
80
81pub const DEFAULT_SESSION_EXPIRES_SECS: u32 = 1800;
84
85const MAX_EXPIRY_HEADROOM_SECS: u32 = 32;
88
89#[derive(Debug, Clone, Copy, PartialEq, Eq)]
92pub enum Refresher {
93 Uac,
95 Uas,
97}
98
99impl Refresher {
100 pub fn as_str(&self) -> &'static str {
102 match self {
103 Refresher::Uac => "uac",
104 Refresher::Uas => "uas",
105 }
106 }
107
108 fn parse(s: &str) -> Result<Self, String> {
109 if s.eq_ignore_ascii_case("uac") {
110 Ok(Refresher::Uac)
111 } else if s.eq_ignore_ascii_case("uas") {
112 Ok(Refresher::Uas)
113 } else {
114 Err(format!("invalid refresher value {s:?} (want uac|uas)"))
115 }
116 }
117}
118
119#[derive(Debug, Clone, Copy, PartialEq, Eq)]
122pub struct SessionExpires {
123 pub interval_secs: u32,
125 pub refresher: Option<Refresher>,
127}
128
129impl SessionExpires {
130 pub fn parse(value: &str) -> Result<Self, String> {
135 let mut parts = value.split(';');
136 let interval = parts.next().unwrap_or("").trim();
137 let interval_secs: u32 = interval
138 .parse()
139 .map_err(|e| format!("invalid Session-Expires interval {interval:?}: {e}"))?;
140 let mut refresher = None;
141 for param in parts {
142 if let Some((name, val)) = param.split_once('=') {
143 if name.trim().eq_ignore_ascii_case("refresher") {
144 refresher = Some(Refresher::parse(val.trim())?);
145 }
146 }
147 }
148 Ok(Self {
149 interval_secs,
150 refresher,
151 })
152 }
153
154 pub fn header_value(&self) -> String {
156 match self.refresher {
157 Some(r) => format!("{};refresher={}", self.interval_secs, r.as_str()),
158 None => self.interval_secs.to_string(),
159 }
160 }
161
162 pub fn header(&self) -> Header {
165 Header::Other("Session-Expires".into(), self.header_value())
166 }
167}
168
169pub fn supported_timer_header() -> Header {
172 Header::Supported("timer".into())
173}
174
175pub fn require_timer_header() -> Header {
178 Header::Require("timer".into())
179}
180
181fn other_header_value<'a>(headers: &'a rsip::Headers, names: &[&str]) -> Option<&'a str> {
184 headers.iter().find_map(|h| match h {
185 Header::Other(name, value) if names.iter().any(|n| name.eq_ignore_ascii_case(n)) => {
186 Some(value.as_str())
187 }
188 _ => None,
189 })
190}
191
192pub fn session_expires_in(headers: &rsip::Headers) -> Option<SessionExpires> {
196 let raw = other_header_value(headers, &["Session-Expires", "x"])?;
197 match SessionExpires::parse(raw) {
198 Ok(se) => Some(se),
199 Err(e) => {
200 warn!("ignoring malformed Session-Expires header: {e}");
201 None
202 }
203 }
204}
205
206pub fn min_se_in(headers: &rsip::Headers) -> Option<u32> {
209 let raw = other_header_value(headers, &["Min-SE"])?;
210 let interval = raw.split(';').next().unwrap_or("").trim();
211 match interval.parse() {
212 Ok(v) => Some(v),
213 Err(e) => {
214 warn!("ignoring malformed Min-SE header {raw:?}: {e}");
215 None
216 }
217 }
218}
219
220fn has_timer_token(value: &str) -> bool {
221 value
222 .split(',')
223 .any(|tag| tag.trim().eq_ignore_ascii_case("timer"))
224}
225
226pub fn supports_timer(headers: &rsip::Headers) -> bool {
229 headers.iter().any(|h| match h {
230 Header::Supported(s) => has_timer_token(s.value()),
231 Header::Other(name, value)
232 if name.eq_ignore_ascii_case("Supported") || name.eq_ignore_ascii_case("k") =>
233 {
234 has_timer_token(value)
235 }
236 _ => false,
237 })
238}
239
240#[derive(Debug, Clone, Copy, PartialEq, Eq)]
243pub struct SessionTimer {
244 pub interval_secs: u32,
246 pub we_are_refresher: bool,
249}
250
251impl SessionTimer {
252 pub fn refresh_after(&self) -> Duration {
255 Duration::from_secs(u64::from(self.interval_secs / 2))
256 }
257
258 pub fn expiry_after(&self) -> Duration {
262 let headroom = (self.interval_secs / 3).min(MAX_EXPIRY_HEADROOM_SECS);
263 Duration::from_secs(u64::from(self.interval_secs.saturating_sub(headroom)))
264 }
265}
266
267pub fn negotiate_uac(response_headers: &rsip::Headers) -> Option<SessionTimer> {
280 let se = session_expires_in(response_headers)?;
281 Some(SessionTimer {
282 interval_secs: se.interval_secs.max(MIN_SESSION_EXPIRES_SECS),
283 we_are_refresher: !matches!(se.refresher, Some(Refresher::Uas)),
284 })
285}
286
287#[derive(Debug, Clone, Copy, PartialEq, Eq)]
290pub struct UasSessionTimer {
291 pub timer: SessionTimer,
293 pub echo: SessionExpires,
296 pub require_timer: bool,
299}
300
301pub fn negotiate_uas(invite_headers: &rsip::Headers) -> Option<UasSessionTimer> {
316 let se = session_expires_in(invite_headers)?;
317 let floor = min_se_in(invite_headers)
318 .unwrap_or(0)
319 .max(MIN_SESSION_EXPIRES_SECS);
320 let interval_secs = se.interval_secs.max(floor);
321 let peer_supports = supports_timer(invite_headers);
322 let refresher = if peer_supports {
323 se.refresher.unwrap_or(Refresher::Uac)
324 } else {
325 Refresher::Uas
326 };
327 Some(UasSessionTimer {
328 timer: SessionTimer {
329 interval_secs,
330 we_are_refresher: refresher == Refresher::Uas,
331 },
332 echo: SessionExpires {
333 interval_secs,
334 refresher: Some(refresher),
335 },
336 require_timer: peer_supports,
337 })
338}
339
340pub trait SessionDialogOps {
346 fn refresh(
350 &self,
351 headers: Vec<Header>,
352 body: Option<Vec<u8>>,
353 ) -> impl Future<Output = Result<Option<rsip::Response>, BoxError>> + Send;
354
355 fn send_bye(&self) -> impl Future<Output = Result<(), BoxError>> + Send;
357}
358
359#[derive(Debug, Clone, Copy, PartialEq, Eq)]
361pub enum SessionTimerOutcome {
362 Cancelled,
365 Expired,
368 RefreshFailed,
371 DialogGone,
374}
375
376pub async fn session_timer_loop<D: SessionDialogOps>(
394 dialog: &D,
395 timer: SessionTimer,
396 refresh_body: Option<Vec<u8>>,
397 peer_refreshed: Arc<Notify>,
398 cancel: CancellationToken,
399) -> SessionTimerOutcome {
400 let mut interval_secs = timer.interval_secs.max(MIN_SESSION_EXPIRES_SECS);
401 if timer.we_are_refresher {
402 loop {
403 let current = SessionTimer {
404 interval_secs,
405 we_are_refresher: true,
406 };
407 select! {
408 _ = tokio::time::sleep(current.refresh_after()) => {}
409 _ = cancel.cancelled() => return SessionTimerOutcome::Cancelled,
410 }
411
412 let headers = vec![
415 supported_timer_header(),
416 SessionExpires {
417 interval_secs,
418 refresher: Some(Refresher::Uac),
419 }
420 .header(),
421 ];
422 match dialog.refresh(headers, refresh_body.clone()).await {
423 Ok(Some(resp)) if resp.status_code.kind() == rsip::StatusCodeKind::Successful => {
424 if let Some(granted) = session_expires_in(&resp.headers) {
427 interval_secs = granted.interval_secs.max(MIN_SESSION_EXPIRES_SECS);
428 }
429 debug!(interval_secs, "session refresh accepted");
430 }
431 Ok(Some(resp)) => {
432 warn!(
433 status = %resp.status_code,
434 "session refresh rejected; hanging up"
435 );
436 if let Err(e) = dialog.send_bye().await {
437 warn!("BYE after rejected refresh failed: {e}");
438 }
439 return SessionTimerOutcome::RefreshFailed;
440 }
441 Ok(None) => {
442 debug!("dialog no longer confirmed; session timer standing down");
443 return SessionTimerOutcome::DialogGone;
444 }
445 Err(e) => {
446 warn!("session refresh error: {e}; hanging up");
447 if let Err(e) = dialog.send_bye().await {
448 warn!("BYE after failed refresh failed: {e}");
449 }
450 return SessionTimerOutcome::RefreshFailed;
451 }
452 }
453 }
454 } else {
455 let current = SessionTimer {
456 interval_secs,
457 we_are_refresher: false,
458 };
459 loop {
460 select! {
461 _ = tokio::time::sleep(current.expiry_after()) => {
462 info!(
463 interval_secs,
464 "session lapsed without refresh; sending BYE"
465 );
466 if let Err(e) = dialog.send_bye().await {
467 warn!("BYE after session expiry failed: {e}");
468 }
469 return SessionTimerOutcome::Expired;
470 }
471 _ = peer_refreshed.notified() => {
472 debug!("peer refreshed session; watchdog deadline reset");
473 }
474 _ = cancel.cancelled() => return SessionTimerOutcome::Cancelled,
475 }
476 }
477 }
478}
479
480#[cfg(test)]
481mod tests {
482 use super::*;
483 use std::sync::Mutex;
484
485 #[test]
488 fn parse_bare_interval() {
489 let se = SessionExpires::parse("1800").unwrap();
490 assert_eq!(se.interval_secs, 1800);
491 assert_eq!(se.refresher, None);
492 }
493
494 #[test]
495 fn parse_with_refresher_param() {
496 let se = SessionExpires::parse("1800;refresher=uas").unwrap();
497 assert_eq!(se.interval_secs, 1800);
498 assert_eq!(se.refresher, Some(Refresher::Uas));
499 let se = SessionExpires::parse("90;refresher=uac").unwrap();
500 assert_eq!(se.refresher, Some(Refresher::Uac));
501 }
502
503 #[test]
504 fn parse_is_case_insensitive_and_whitespace_tolerant() {
505 let se = SessionExpires::parse(" 600 ; Refresher = UAS ").unwrap();
506 assert_eq!(se.interval_secs, 600);
507 assert_eq!(se.refresher, Some(Refresher::Uas));
508 }
509
510 #[test]
511 fn parse_ignores_unknown_params() {
512 let se = SessionExpires::parse("1800;foo=bar;refresher=uac;baz").unwrap();
513 assert_eq!(se.refresher, Some(Refresher::Uac));
514 }
515
516 #[test]
517 fn parse_rejects_garbage() {
518 assert!(SessionExpires::parse("").is_err());
519 assert!(SessionExpires::parse("soon").is_err());
520 assert!(SessionExpires::parse("1800;refresher=bogus").is_err());
521 assert!(SessionExpires::parse("-5").is_err());
522 }
523
524 #[test]
525 fn header_value_round_trips() {
526 for se in [
527 SessionExpires {
528 interval_secs: 1800,
529 refresher: None,
530 },
531 SessionExpires {
532 interval_secs: 90,
533 refresher: Some(Refresher::Uac),
534 },
535 SessionExpires {
536 interval_secs: 7200,
537 refresher: Some(Refresher::Uas),
538 },
539 ] {
540 let parsed = SessionExpires::parse(&se.header_value()).unwrap();
541 assert_eq!(parsed, se, "round-trip via {:?}", se.header_value());
542 }
543 }
544
545 #[test]
546 fn header_builds_untyped_session_expires() {
547 let h = SessionExpires {
548 interval_secs: 1800,
549 refresher: Some(Refresher::Uac),
550 }
551 .header();
552 assert_eq!(h.to_string(), "Session-Expires: 1800;refresher=uac");
553 }
554
555 fn headers(items: Vec<Header>) -> rsip::Headers {
558 let mut h = rsip::Headers::default();
559 for item in items {
560 h.push(item);
561 }
562 h
563 }
564
565 #[test]
566 fn session_expires_in_finds_header_case_insensitively() {
567 let h = headers(vec![Header::Other(
568 "session-expires".into(),
569 "600;refresher=uas".into(),
570 )]);
571 let se = session_expires_in(&h).unwrap();
572 assert_eq!(se.interval_secs, 600);
573 assert_eq!(se.refresher, Some(Refresher::Uas));
574 }
575
576 #[test]
577 fn session_expires_in_accepts_compact_form() {
578 let h = headers(vec![Header::Other("x".into(), "300".into())]);
580 assert_eq!(
581 session_expires_in(&h),
582 Some(SessionExpires {
583 interval_secs: 300,
584 refresher: None
585 })
586 );
587 }
588
589 #[test]
590 fn session_expires_in_absent_or_malformed_is_none() {
591 assert_eq!(session_expires_in(&headers(vec![])), None);
592 let h = headers(vec![Header::Other("Session-Expires".into(), "soon".into())]);
593 assert_eq!(session_expires_in(&h), None);
594 }
595
596 #[test]
597 fn min_se_in_parses_and_ignores_params() {
598 let h = headers(vec![Header::Other("Min-SE".into(), "120".into())]);
599 assert_eq!(min_se_in(&h), Some(120));
600 let h = headers(vec![Header::Other("min-se".into(), "240;lr".into())]);
601 assert_eq!(min_se_in(&h), Some(240));
602 assert_eq!(min_se_in(&headers(vec![])), None);
603 let h = headers(vec![Header::Other("Min-SE".into(), "never".into())]);
604 assert_eq!(min_se_in(&h), None);
605 }
606
607 #[test]
608 fn supports_timer_scans_typed_untyped_and_compact() {
609 assert!(supports_timer(&headers(vec![supported_timer_header()])));
610 assert!(supports_timer(&headers(vec![Header::Supported(
611 "100rel, timer".into()
612 )])));
613 assert!(supports_timer(&headers(vec![Header::Other(
614 "k".into(),
615 "timer".into()
616 )])));
617 assert!(supports_timer(&headers(vec![Header::Other(
618 "Supported".into(),
619 "TIMER".into()
620 )])));
621 assert!(!supports_timer(&headers(vec![])));
622 assert!(!supports_timer(&headers(vec![Header::Supported(
623 "100rel".into()
624 )])));
625 assert!(!supports_timer(&headers(vec![Header::Supported(
627 "timers".into()
628 )])));
629 }
630
631 #[test]
634 fn refresh_fires_at_half_the_interval() {
635 let t = SessionTimer {
636 interval_secs: 1800,
637 we_are_refresher: true,
638 };
639 assert_eq!(t.refresh_after(), Duration::from_secs(900));
640 let t = SessionTimer {
641 interval_secs: 90,
642 we_are_refresher: true,
643 };
644 assert_eq!(t.refresh_after(), Duration::from_secs(45));
645 }
646
647 #[test]
648 fn expiry_keeps_min_of_32s_or_a_third_headroom() {
649 let t = SessionTimer {
651 interval_secs: 1800,
652 we_are_refresher: false,
653 };
654 assert_eq!(t.expiry_after(), Duration::from_secs(1768));
655 let t = SessionTimer {
657 interval_secs: 90,
658 we_are_refresher: false,
659 };
660 assert_eq!(t.expiry_after(), Duration::from_secs(60));
661 }
662
663 #[test]
666 fn uac_no_session_expires_means_no_timer() {
667 assert_eq!(negotiate_uac(&headers(vec![])), None);
668 }
669
670 #[test]
671 fn uac_refresher_uas_means_peer_refreshes() {
672 let h = headers(vec![Header::Other(
673 "Session-Expires".into(),
674 "1800;refresher=uas".into(),
675 )]);
676 assert_eq!(
677 negotiate_uac(&h),
678 Some(SessionTimer {
679 interval_secs: 1800,
680 we_are_refresher: false
681 })
682 );
683 }
684
685 #[test]
686 fn uac_refresher_uac_or_missing_means_we_refresh() {
687 let h = headers(vec![Header::Other(
688 "Session-Expires".into(),
689 "600;refresher=uac".into(),
690 )]);
691 assert!(negotiate_uac(&h).unwrap().we_are_refresher);
692 let h = headers(vec![Header::Other("Session-Expires".into(), "600".into())]);
695 assert!(negotiate_uac(&h).unwrap().we_are_refresher);
696 }
697
698 #[test]
699 fn uac_floors_tiny_grants_at_90s() {
700 let h = headers(vec![Header::Other(
701 "Session-Expires".into(),
702 "20;refresher=uac".into(),
703 )]);
704 assert_eq!(negotiate_uac(&h).unwrap().interval_secs, 90);
705 }
706
707 fn invite_headers(session_expires: &str, min_se: Option<&str>, timer: bool) -> rsip::Headers {
710 let mut items = vec![Header::Other(
711 "Session-Expires".into(),
712 session_expires.into(),
713 )];
714 if let Some(m) = min_se {
715 items.push(Header::Other("Min-SE".into(), m.into()));
716 }
717 if timer {
718 items.push(supported_timer_header());
719 }
720 headers(items)
721 }
722
723 #[test]
724 fn uas_no_session_expires_means_no_timer() {
725 assert_eq!(
726 negotiate_uas(&headers(vec![supported_timer_header()])),
727 None
728 );
729 }
730
731 #[test]
732 fn uas_default_makes_supporting_peer_the_refresher() {
733 let uas = negotiate_uas(&invite_headers("1800", None, true)).unwrap();
734 assert_eq!(uas.timer.interval_secs, 1800);
735 assert!(!uas.timer.we_are_refresher, "peer (UAC) should refresh");
736 assert_eq!(
737 uas.echo,
738 SessionExpires {
739 interval_secs: 1800,
740 refresher: Some(Refresher::Uac)
741 }
742 );
743 assert!(uas.require_timer);
744 }
745
746 #[test]
747 fn uas_honors_requested_refresher_uas() {
748 let uas = negotiate_uas(&invite_headers("1800;refresher=uas", None, true)).unwrap();
749 assert!(uas.timer.we_are_refresher, "we (UAS) were asked to refresh");
750 assert_eq!(uas.echo.refresher, Some(Refresher::Uas));
751 }
752
753 #[test]
754 fn uas_without_peer_support_takes_refresher_role() {
755 let uas = negotiate_uas(&invite_headers("1800;refresher=uac", None, false)).unwrap();
759 assert!(uas.timer.we_are_refresher);
760 assert_eq!(uas.echo.refresher, Some(Refresher::Uas));
761 assert!(!uas.require_timer);
762 }
763
764 #[test]
765 fn uas_floors_interval_at_min_se_and_90s() {
766 let uas = negotiate_uas(&invite_headers("30", Some("120"), true)).unwrap();
768 assert_eq!(uas.timer.interval_secs, 120);
769 assert_eq!(uas.echo.interval_secs, 120);
770 let uas = negotiate_uas(&invite_headers("30", None, true)).unwrap();
772 assert_eq!(uas.timer.interval_secs, 90);
773 }
774
775 #[derive(Debug, Clone, PartialEq, Eq)]
778 enum Event {
779 Refresh { session_expires: String },
780 Bye,
781 }
782
783 struct MockDialog {
786 events: Mutex<Vec<(Duration, Event)>>,
787 refresh_replies: Mutex<Vec<Result<Option<rsip::Response>, String>>>,
788 started: tokio::time::Instant,
789 }
790
791 impl MockDialog {
792 fn new(refresh_replies: Vec<Result<Option<rsip::Response>, String>>) -> Self {
793 Self {
794 events: Mutex::new(Vec::new()),
795 refresh_replies: Mutex::new(refresh_replies),
796 started: tokio::time::Instant::now(),
797 }
798 }
799
800 fn events(&self) -> Vec<(Duration, Event)> {
801 self.events.lock().unwrap().clone()
802 }
803 }
804
805 fn response(code: u16, extra: Vec<Header>) -> rsip::Response {
806 rsip::Response {
807 status_code: rsip::StatusCode::from(code),
808 version: rsip::Version::V2,
809 headers: headers(extra),
810 body: Vec::new(),
811 }
812 }
813
814 impl SessionDialogOps for MockDialog {
815 async fn refresh(
816 &self,
817 hdrs: Vec<Header>,
818 _body: Option<Vec<u8>>,
819 ) -> Result<Option<rsip::Response>, BoxError> {
820 let se = hdrs
821 .iter()
822 .find_map(|h| match h {
823 Header::Other(name, value) if name == "Session-Expires" => Some(value.clone()),
824 _ => None,
825 })
826 .unwrap_or_default();
827 self.events.lock().unwrap().push((
828 self.started.elapsed(),
829 Event::Refresh {
830 session_expires: se,
831 },
832 ));
833 let reply = self.refresh_replies.lock().unwrap().remove(0);
834 reply.map_err(Into::into)
835 }
836
837 async fn send_bye(&self) -> Result<(), BoxError> {
838 self.events
839 .lock()
840 .unwrap()
841 .push((self.started.elapsed(), Event::Bye));
842 Ok(())
843 }
844 }
845
846 fn timer(interval_secs: u32, we_are_refresher: bool) -> SessionTimer {
847 SessionTimer {
848 interval_secs,
849 we_are_refresher,
850 }
851 }
852
853 #[tokio::test(start_paused = true)]
854 async fn refresher_sends_refresh_every_half_interval() {
855 let dialog = Arc::new(MockDialog::new(vec![
856 Ok(Some(response(200, vec![]))),
857 Ok(Some(response(200, vec![]))),
858 Ok(None), ]));
860 let cancel = CancellationToken::new();
861 let outcome = session_timer_loop(
862 &*dialog,
863 timer(180, true),
864 None,
865 Arc::new(Notify::new()),
866 cancel,
867 )
868 .await;
869 assert_eq!(outcome, SessionTimerOutcome::DialogGone);
870
871 let events = dialog.events();
872 assert_eq!(events.len(), 3);
873 assert_eq!(events[0].0, Duration::from_secs(90));
874 assert_eq!(events[1].0, Duration::from_secs(180));
875 assert_eq!(events[2].0, Duration::from_secs(270));
876 for (_, e) in &events {
877 assert_eq!(
878 e,
879 &Event::Refresh {
880 session_expires: "180;refresher=uac".into()
881 }
882 );
883 }
884 }
885
886 #[tokio::test(start_paused = true)]
887 async fn refresher_adopts_interval_granted_in_refresh_response() {
888 let regrant = response(
891 200,
892 vec![Header::Other(
893 "Session-Expires".into(),
894 "360;refresher=uac".into(),
895 )],
896 );
897 let dialog = Arc::new(MockDialog::new(vec![Ok(Some(regrant)), Ok(None)]));
898 let cancel = CancellationToken::new();
899 let outcome = session_timer_loop(
900 &*dialog,
901 timer(180, true),
902 None,
903 Arc::new(Notify::new()),
904 cancel,
905 )
906 .await;
907 assert_eq!(outcome, SessionTimerOutcome::DialogGone);
908
909 let events = dialog.events();
910 assert_eq!(events[0].0, Duration::from_secs(90), "first at 180/2");
911 assert_eq!(
912 events[1].0,
913 Duration::from_secs(90 + 180),
914 "second at 90 + 360/2 after the re-grant"
915 );
916 }
917
918 #[tokio::test(start_paused = true)]
919 async fn refresher_rejected_refresh_sends_bye() {
920 let dialog = Arc::new(MockDialog::new(vec![Ok(Some(response(481, vec![])))]));
921 let cancel = CancellationToken::new();
922 let outcome = session_timer_loop(
923 &*dialog,
924 timer(180, true),
925 None,
926 Arc::new(Notify::new()),
927 cancel,
928 )
929 .await;
930 assert_eq!(outcome, SessionTimerOutcome::RefreshFailed);
931 let events = dialog.events();
932 assert!(matches!(events[0].1, Event::Refresh { .. }));
933 assert_eq!(events[1].1, Event::Bye);
934 }
935
936 #[tokio::test(start_paused = true)]
937 async fn refresher_transport_error_sends_bye() {
938 let dialog = Arc::new(MockDialog::new(vec![Err("socket closed".into())]));
939 let cancel = CancellationToken::new();
940 let outcome = session_timer_loop(
941 &*dialog,
942 timer(180, true),
943 None,
944 Arc::new(Notify::new()),
945 cancel,
946 )
947 .await;
948 assert_eq!(outcome, SessionTimerOutcome::RefreshFailed);
949 assert_eq!(dialog.events().last().unwrap().1, Event::Bye);
950 }
951
952 #[tokio::test(start_paused = true)]
953 async fn refresher_cancellation_wins_before_first_refresh() {
954 let dialog = Arc::new(MockDialog::new(vec![]));
955 let cancel = CancellationToken::new();
956 cancel.cancel();
957 let outcome = session_timer_loop(
958 &*dialog,
959 timer(180, true),
960 None,
961 Arc::new(Notify::new()),
962 cancel,
963 )
964 .await;
965 assert_eq!(outcome, SessionTimerOutcome::Cancelled);
966 assert!(dialog.events().is_empty(), "no refresh, no BYE");
967 }
968
969 #[tokio::test(start_paused = true)]
970 async fn watchdog_sends_bye_when_session_lapses() {
971 let dialog = Arc::new(MockDialog::new(vec![]));
972 let cancel = CancellationToken::new();
973 let outcome = session_timer_loop(
974 &*dialog,
975 timer(90, false),
976 None,
977 Arc::new(Notify::new()),
978 cancel,
979 )
980 .await;
981 assert_eq!(outcome, SessionTimerOutcome::Expired);
982 let events = dialog.events();
983 assert_eq!(events.len(), 1);
984 assert_eq!(events[0], (Duration::from_secs(60), Event::Bye));
986 }
987
988 #[tokio::test(start_paused = true)]
989 async fn watchdog_resets_deadline_on_peer_refresh() {
990 let dialog = Arc::new(MockDialog::new(vec![]));
991 let cancel = CancellationToken::new();
992 let refreshed = Arc::new(Notify::new());
993
994 let loop_task = tokio::spawn({
995 let dialog = dialog.clone();
996 let refreshed = refreshed.clone();
997 let cancel = cancel.clone();
998 async move { session_timer_loop(&*dialog, timer(90, false), None, refreshed, cancel).await }
999 });
1000
1001 tokio::time::sleep(Duration::from_secs(59)).await;
1003 refreshed.notify_one();
1004 tokio::task::yield_now().await;
1005 tokio::time::sleep(Duration::from_secs(30)).await;
1007 assert!(dialog.events().is_empty(), "deadline should have reset");
1008 let outcome = loop_task.await.unwrap();
1010 assert_eq!(outcome, SessionTimerOutcome::Expired);
1011 assert_eq!(
1012 dialog.events(),
1013 vec![(Duration::from_secs(119), Event::Bye)]
1014 );
1015 }
1016
1017 #[tokio::test(start_paused = true)]
1018 async fn watchdog_cancellation_stands_down_without_bye() {
1019 let dialog = Arc::new(MockDialog::new(vec![]));
1020 let cancel = CancellationToken::new();
1021 let loop_task = tokio::spawn({
1022 let dialog = dialog.clone();
1023 let cancel = cancel.clone();
1024 async move {
1025 session_timer_loop(
1026 &*dialog,
1027 timer(90, false),
1028 None,
1029 Arc::new(Notify::new()),
1030 cancel,
1031 )
1032 .await
1033 }
1034 });
1035 tokio::time::sleep(Duration::from_secs(10)).await;
1036 cancel.cancel();
1037 assert_eq!(loop_task.await.unwrap(), SessionTimerOutcome::Cancelled);
1038 assert!(dialog.events().is_empty());
1039 }
1040}