//! Process-independent durable SM slots; network SIGKILL coverage lives in sm_restart_network.py. use rust_xmpp::sessions::{ConflictPolicy, Hub}; use std::time::{Duration, Instant}; use tokio::sync::mpsc; #[test] fn restart_restores_owner_presence_and_only_unacknowledged_output() { let dir = tempfile::tempdir().unwrap(); let hub = Hub::persistent(dir.path()).unwrap(); let (tx, mut rx) = mpsc::channel(8); let session = hub .bind("a@x/mobile", "original", tx, Duration::from_secs(60)) .unwrap(); let token; { let mut state = session.lock().unwrap(); state.enable(true).unwrap(); token = state.resume_id.clone().unwrap(); state.available = true; state.priority = 3; state.presence = Some("away".into()); state.client_id = Some("mobile-client".into()); state.csi.set_inactive(true); state.mark_handled().unwrap(); state.transmit("first".into()).unwrap(); state.transmit("second".into()).unwrap(); state.acknowledge(1).unwrap(); } assert_eq!(rx.try_recv().unwrap(), "first"); drop(hub); let recovered = Hub::persistent(dir.path()).unwrap(); let state = recovered.get("a@x/mobile").unwrap(); { let state = state.lock().unwrap(); assert!(state.sender.is_none()); assert!(state.available); assert_eq!(state.priority, 3); assert_eq!(state.handled, 1); assert_eq!(state.client_id.as_deref(), Some("mobile-client")); assert_eq!( state.pending.iter().cloned().collect::>(), ["second"] ); } let (tx, _rx) = mpsc::channel(8); assert!( recovered .resume("attacker@x", &token, 1, "bad", tx.clone()) .is_err() ); assert!( recovered .resume("a@x", &token, 3, "bad", tx.clone()) .is_err() ); let (resumed, replay) = recovered .resume("a@x", &token, 1, "replacement", tx) .unwrap(); assert!(replay[0].contains("h='1'")); assert_eq!(replay[1], "second"); assert!(state.lock().unwrap().acknowledge(2).is_err()); resumed.lock().unwrap().acknowledge(2).unwrap(); drop(recovered); let recovered = Hub::persistent(dir.path()).unwrap(); assert!( recovered .get("a@x/mobile") .unwrap() .lock() .unwrap() .pending .is_empty() ); } #[test] fn disconnected_deadline_is_fixed_across_recovery() { let dir = tempfile::tempdir().unwrap(); let hub = Hub::persistent(dir.path()).unwrap(); let (tx, _rx) = mpsc::channel(8); let session = hub.bind("a@x/r", "c", tx, Duration::from_secs(60)).unwrap(); session.lock().unwrap().enable(true).unwrap(); hub.disconnect("c", false); let deadline = session.lock().unwrap().expires.unwrap(); hub.disconnect("c", false); assert_eq!(session.lock().unwrap().expires.unwrap(), deadline); drop(hub); let recovered = Hub::persistent(dir.path()).unwrap(); let recovered_deadline = recovered .get("a@x/r") .unwrap() .lock() .unwrap() .expires .unwrap(); assert!(recovered_deadline <= deadline + Duration::from_millis(2)); assert!(recovered_deadline >= deadline - Duration::from_millis(5)); } #[test] fn graceful_close_conflict_account_purge_and_expiration_remove_tokens() { for action in ["close", "conflict", "purge", "expire"] { let dir = tempfile::tempdir().unwrap(); let hub = Hub::persistent(dir.path()).unwrap(); let (tx, _rx) = mpsc::channel(8); let session = hub .bind("a@x/r", "c", tx.clone(), Duration::from_secs(60)) .unwrap(); session.lock().unwrap().enable(true).unwrap(); let token = session.lock().unwrap().resume_id.clone().unwrap(); match action { "close" => { hub.disconnect("c", true); } "purge" => hub.kick_owner("a@x"), "conflict" => { hub.bind_resolving( "a@x/r", "new", tx.clone(), Duration::from_secs(60), ConflictPolicy::KickOld, ) .unwrap(); } _ => { session.lock().unwrap().expires = Some(Instant::now()); hub.expired(); } } drop(hub); let recovered = Hub::persistent(dir.path()).unwrap(); assert!( recovered.resume("a@x", &token, 0, "after", tx).is_err(), "{action}" ); } } #[test] fn store_has_one_process_owner_and_enforces_output_byte_bound() { let dir = tempfile::tempdir().unwrap(); let hub = Hub::persistent(dir.path()).unwrap(); assert!(Hub::persistent(dir.path()).is_err()); let (tx, mut rx) = mpsc::channel(8); let session = hub.bind("a@x/r", "c", tx, Duration::from_secs(60)).unwrap(); let mut state = session.lock().unwrap(); state.enable(true).unwrap(); assert!(state.transmit("x".repeat(8 * 1024 * 1024 + 1)).is_err()); // Fits the payload limit but exceeds the serialized slot limit: persistence // failure must also roll counters back and keep bytes off the transport. assert!(state.transmit("x".repeat(8 * 1024 * 1024)).is_err()); assert_eq!(state.sent, 0); assert!(state.pending.is_empty()); assert!(rx.try_recv().is_err()); } #[test] fn shutdown_freeze_retires_old_transport_before_cleanup() { let dir = tempfile::tempdir().unwrap(); let hub = Hub::persistent(dir.path()).unwrap(); let (tx, _rx) = mpsc::channel(8); let old = hub.bind("a@x/r", "c", tx, Duration::from_secs(60)).unwrap(); old.lock().unwrap().enable(true).unwrap(); old.lock().unwrap().transmit("pending".into()).unwrap(); let token = old.lock().unwrap().resume_id.clone().unwrap(); hub.freeze().unwrap(); assert!(old.lock().unwrap().acknowledge(1).is_err()); assert!(old.lock().unwrap().transmit("stale".into()).is_err()); assert!(hub.disconnect("c", true).is_empty()); drop(hub); let hub = Hub::persistent(dir.path()).unwrap(); let (tx, _rx) = mpsc::channel(8); let (_, replay) = hub.resume("a@x", &token, 0, "fresh", tx).unwrap(); assert_eq!(replay[1], "pending"); } #[test] fn durable_counters_wrap_without_accepting_stale_acknowledgements() { let dir = tempfile::tempdir().unwrap(); let hub = Hub::persistent(dir.path()).unwrap(); let (tx, _rx) = mpsc::channel(8); let session = hub.bind("a@x/r", "c", tx, Duration::from_secs(60)).unwrap(); { let mut state = session.lock().unwrap(); state.enable(true).unwrap(); state.sent = u32::MAX; state.acked = u32::MAX; state.handled = u32::MAX; state.mark_handled().unwrap(); state.transmit("wrapped".into()).unwrap(); } drop(hub); let hub = Hub::persistent(dir.path()).unwrap(); let session = hub.get("a@x/r").unwrap(); let mut state = session.lock().unwrap(); assert_eq!(state.handled, 0); assert_eq!(state.sent, 0); assert!(state.acknowledge(u32::MAX - 1).is_err()); state.acknowledge(0).unwrap(); assert!(state.pending.is_empty()); } #[test] #[cfg(unix)] fn durable_tokens_and_private_pending_output_have_owner_only_permissions() { use std::os::unix::fs::PermissionsExt; let dir = tempfile::tempdir().unwrap(); let hub = Hub::persistent(dir.path()).unwrap(); let (tx, _rx) = mpsc::channel(8); let session = hub.bind("a@x/r", "c", tx, Duration::from_secs(60)).unwrap(); session.lock().unwrap().enable(true).unwrap(); assert_eq!( std::fs::metadata(dir.path()).unwrap().permissions().mode() & 0o777, 0o700 ); for entry in std::fs::read_dir(dir.path()).unwrap() { assert_eq!( entry.unwrap().metadata().unwrap().permissions().mode() & 0o777, 0o600 ); } } #[test] fn recovery_discards_owned_crash_temps_and_keeps_last_committed_slot() { let dir = tempfile::tempdir().unwrap(); let hub = Hub::persistent(dir.path()).unwrap(); let (tx, _rx) = mpsc::channel(8); let session = hub.bind("a@x/r", "c", tx, Duration::from_secs(60)).unwrap(); let token; { let mut state = session.lock().unwrap(); state.enable(true).unwrap(); state.transmit("committed".into()).unwrap(); token = state.resume_id.clone().unwrap(); } drop(hub); let interrupted = dir.path().join(format!("{token}.tmp")); let orphan = dir.path().join(format!("{}.tmp", uuid::Uuid::new_v4())); let unknown = dir.path().join("notes.tmp"); let unknown_dir = dir.path().join(format!("{}.tmp", uuid::Uuid::new_v4())); std::fs::write(&interrupted, b"{\"pending\": [\"uncommitted").unwrap(); std::fs::write(&orphan, b"incomplete").unwrap(); std::fs::write(&unknown, b"preserve").unwrap(); std::fs::create_dir(&unknown_dir).unwrap(); let recovered = Hub::persistent(dir.path()).unwrap(); assert!(!interrupted.exists()); assert!(!orphan.exists()); assert!(unknown.exists()); assert!(unknown_dir.is_dir()); let session = recovered.get("a@x/r").unwrap(); let state = session.lock().unwrap(); assert_eq!(state.sent, 1); assert_eq!( state.pending.iter().cloned().collect::>(), ["committed"] ); } #[test] fn corrupt_committed_slot_fails_recovery_closed() { let dir = tempfile::tempdir().unwrap(); std::fs::write( dir.path().join(format!("{}.json", uuid::Uuid::new_v4())), b"{incomplete", ) .unwrap(); assert!(Hub::persistent(dir.path()).is_err()); } #[test] fn independent_hubs_with_identical_connection_names_keep_their_own_slots() { let first_dir = tempfile::tempdir().unwrap(); let second_dir = tempfile::tempdir().unwrap(); let first = Hub::persistent(first_dir.path()).unwrap(); let second = Hub::persistent(second_dir.path()).unwrap(); let (tx, _rx) = mpsc::channel(8); let a = first .bind("a@x/r", "c", tx.clone(), Duration::from_secs(60)) .unwrap(); let b = second .bind("a@x/r", "c", tx, Duration::from_secs(60)) .unwrap(); a.lock().unwrap().enable(true).unwrap(); b.lock().unwrap().enable(true).unwrap(); a.lock().unwrap().transmit("first hub".into()).unwrap(); b.lock().unwrap().transmit("second hub".into()).unwrap(); first.disconnect("c", true); drop(first); drop(second); let first = Hub::persistent(first_dir.path()).unwrap(); let second = Hub::persistent(second_dir.path()).unwrap(); assert!(first.get("a@x/r").is_none()); let b = second.get("a@x/r").unwrap(); assert_eq!( b.lock().unwrap().pending.front().map(String::as_str), Some("second hub") ); } #[test] fn session_reference_does_not_keep_store_locked_or_allow_unpersisted_writes() { let dir = tempfile::tempdir().unwrap(); let hub = Hub::persistent(dir.path()).unwrap(); let (tx, _rx) = mpsc::channel(8); let old = hub.bind("a@x/r", "c", tx, Duration::from_secs(60)).unwrap(); old.lock().unwrap().enable(true).unwrap(); drop(hub); let recovered = Hub::persistent(dir.path()).unwrap(); assert!(old.lock().unwrap().checkpoint().is_err()); assert!(old.lock().unwrap().transmit("orphaned".into()).is_err()); assert_eq!(old.lock().unwrap().sent, 0); assert!( recovered .get("a@x/r") .unwrap() .lock() .unwrap() .pending .is_empty() ); }