//! Dropping Muc without checkpoint_live models an abrupt process loss: only
//! checkpoints committed before returning the accepted stanza can survive.
use rust_xmpp::{muc::Muc, protocol::ModuleResult, storage};
use serde_json::json;
const HOST: &str = "rooms.example.org";
const ROOM: &str = "crash@rooms.example.org";
const A: &str = "alice@example.org/desktop";
const B: &str = "bob@example.org/phone";
fn send(m: &mut Muc, jid: &str, packet: &str) -> ModuleResult {
let doc = roxmltree::Document::parse(packet).unwrap();
m.handle(jid, doc.root_element()).unwrap().unwrap()
}
fn muc(root: &std::path::Path) -> Muc {
let mut m = Muc::new(root.into(), HOST.into());
m.set_component_options(json!({"muc_room_locking":false,"max_history_messages":3,"muc_room_default_history_length":3}), false);
m
}
fn join(m: &mut Muc, jid: &str, nick: &str) -> ModuleResult {
send(
m,
jid,
&format!(
""
),
)
}
fn has_self(out: &ModuleResult) -> bool {
out.replies.iter().any(|p| p.contains("code='110'"))
}
#[test]
fn temporary_membership_and_full_bounded_history_survive_ungraceful_loss() {
let root = tempfile::tempdir().unwrap();
let mut first = muc(root.path());
assert!(has_self(&join(&mut first, A, "Alice")));
assert!(has_self(&join(&mut first, B, "Bob")));
for text in ["old-zero", "recent-one", "recent-two", "recent-three"] {
let out = send(
&mut first,
A,
&format!("{text}"),
);
assert!(!out.deliveries.is_empty());
}
drop(first);
let mut next = muc(root.path());
next.prepare_recovery().unwrap();
assert!(
next.restore_for_session("alice@example.org/other")
.unwrap()
.replies
.is_empty()
);
let a = next.restore_for_session(A).unwrap();
assert!(has_self(&a));
// A dormant Bob is never an available routing target before authenticating.
let out = send(
&mut next,
A,
&format!("after-restart"),
);
assert!(out.deliveries.iter().all(|(jid, _)| jid != B));
let b = next.restore_for_session(B).unwrap();
let history = b.replies.join("");
assert!(has_self(&b));
assert!(
!history.contains("old-zero") && !history.contains("recent-one"),
"{history}"
);
assert!(
history.contains("recent-two")
&& history.contains("recent-three")
&& history.contains("after-restart")
);
assert!(history.contains("urn:xmpp:delay"));
drop(next);
let mut third = muc(root.path());
third.prepare_recovery().unwrap();
assert!(has_self(&third.restore_for_session(A).unwrap()));
assert!(has_self(&third.restore_for_session(B).unwrap()));
}
#[test]
fn acknowledged_kick_and_room_destruction_cannot_resurrect_membership() {
let root = tempfile::tempdir().unwrap();
let mut first = muc(root.path());
join(&mut first, A, "Alice");
join(&mut first, B, "Bob");
let kicked = send(
&mut first,
A,
&format!(
" "
),
);
assert!(kicked.replies.iter().any(|p| p.contains("type='result'")));
drop(first);
let mut next = muc(root.path());
next.prepare_recovery().unwrap();
assert!(next.restore_for_session(B).unwrap().replies.is_empty());
assert!(has_self(&next.restore_for_session(A).unwrap()));
let destroyed = send(
&mut next,
A,
&format!(
""
),
);
assert!(
destroyed
.replies
.iter()
.any(|p| p.contains("type='result'"))
);
drop(next);
let mut third = muc(root.path());
third.prepare_recovery().unwrap();
assert!(third.restore_for_session(A).unwrap().replies.is_empty());
assert!(third.restore_for_session(B).unwrap().replies.is_empty());
}
#[test]
fn dormant_deadline_stays_fixed_when_another_occupant_recovers_and_sends() {
let root = tempfile::tempdir().unwrap();
let mut first = muc(root.path());
join(&mut first, A, "Alice");
join(&mut first, B, "Bob");
drop(first);
let initial = storage::load(root.path(), HOST, None, "rust_muc_checkpoint")
.unwrap()
.unwrap();
let initial_at = initial
.get("rooms")
.unwrap()
.get(ROOM)
.unwrap()
.get("saved_at")
.unwrap()
.as_i64()
.unwrap();
let mut next = muc(root.path());
next.prepare_recovery().unwrap();
next.restore_for_session(A).unwrap();
send(
&mut next,
A,
&format!("traffic"),
);
drop(next);
storage::update(root.path(), HOST, None, "rust_muc_checkpoint", |v| {
let mut rooms = v.get("rooms").unwrap().clone();
let mut room = rooms.get(ROOM).unwrap().clone();
assert_eq!(room.get("saved_at").unwrap().as_i64().unwrap(), initial_at);
room.set("saved_at", storage::LuaValue::Integer(initial_at - 301));
rooms.set(ROOM, room);
v.set("rooms", rooms);
Ok(())
})
.unwrap();
let mut third = muc(root.path());
third.prepare_recovery().unwrap();
assert!(third.restore_for_session(B).unwrap().replies.is_empty());
}
#[test]
fn durable_disconnect_removes_last_temporary_room_without_shutdown_export() {
let root = tempfile::tempdir().unwrap();
let mut first = muc(root.path());
join(&mut first, A, "Alice");
first.disconnect_durable(A).unwrap();
drop(first);
let mut next = muc(root.path());
next.prepare_recovery().unwrap();
assert!(next.restore_for_session(A).unwrap().replies.is_empty());
}
#[test]
fn journal_failure_returns_no_routing_and_rolls_back_role_mutation() {
let root = tempfile::tempdir().unwrap();
let mut first = muc(root.path());
join(&mut first, A, "Alice");
join(&mut first, B, "Bob");
let path = storage::path(root.path(), HOST, None, "rust_muc_checkpoint");
let checkpoint = std::fs::read(&path).unwrap();
std::fs::remove_file(&path).unwrap();
std::fs::create_dir(&path).unwrap();
let packet = format!(
" "
);
let doc = roxmltree::Document::parse(&packet).unwrap();
assert!(first.handle(A, doc.root_element()).is_err());
std::fs::remove_dir(&path).unwrap();
storage::atomic_write(&path, &checkpoint).unwrap();
let out = send(
&mut first,
B,
&format!("still-participant"),
);
assert!(
out.deliveries
.iter()
.any(|(jid, p)| jid == A && p.contains("still-participant"))
);
}
#[test]
fn safe_presence_checkpoint_sanitization_is_idempotent() {
use rust_xmpp::muc_recovery::sanitize_presence_payload;
let payload = "awayReady & waitingsecret";
let safe = sanitize_presence_payload(payload);
assert_eq!(
safe,
"awayReady & waiting"
);
assert_eq!(sanitize_presence_payload(&safe), safe);
}
#[test]
fn foreign_stanzas_do_not_initialize_or_rewrite_muc_checkpoint() {
let root = tempfile::tempdir().unwrap();
let mut m = muc(root.path());
let packet = "direct";
let doc = roxmltree::Document::parse(packet).unwrap();
assert!(m.handle(A, doc.root_element()).unwrap().is_none());
let path = storage::path(root.path(), HOST, None, "rust_muc_checkpoint");
assert!(!path.exists());
join(&mut m, A, "Alice");
let checkpoint = std::fs::read(&path).unwrap();
// A sentinel distinguishes a write from an identical journal serialization.
let sentinel = [checkpoint.as_slice(), b"-- untouched\n"].concat();
storage::atomic_write(&path, &sentinel).unwrap();
assert!(m.handle(A, doc.root_element()).unwrap().is_none());
assert_eq!(std::fs::read(&path).unwrap(), sentinel);
}