use rust_xmpp::{muc::Muc, protocol::ModuleResult};
fn send(m: &mut Muc, j: &str, x: &str) -> ModuleResult {
let d = roxmltree::Document::parse(x).unwrap();
m.handle(j, d.root_element()).unwrap().unwrap()
}
fn join(m: &mut Muc, j: &str, n: &str) -> ModuleResult {
send(
m,
j,
&format!(
""
),
)
}
fn unlock(m: &mut Muc) {
let r = send(
m,
"alice@test/a",
"",
);
assert!(r.replies[0].contains("type='result'"));
}
#[test]
fn join_lock_chat_leave() {
let t = tempfile::tempdir().unwrap();
let mut m = Muc::new(t.path().into(), "rooms.test".into());
let a = join(&mut m, "alice@test/a", "Alice");
assert!(a.replies[0].contains("201"));
assert!(join(&mut m, "bob@test/b", "Bob").replies[0].contains("item-not-found"));
unlock(&mut m);
let b = join(&mut m, "bob@test/b", "Bob");
assert!(b.replies.iter().any(|x| x.contains("Alice")));
assert!(!b.replies[0].contains("alice@test"));
let c = send(
&mut m,
"bob@test/b",
"Hello",
);
assert_eq!(c.deliveries.len(), 2);
assert!(c.deliveries[0].1.contains("Hello"));
assert_eq!(m.disconnect("bob@test/b").deliveries.len(), 1);
}
#[test]
fn authorization_and_conflict() {
let t = tempfile::tempdir().unwrap();
let mut m = Muc::new(t.path().into(), "rooms.test".into());
join(&mut m, "alice@test/a", "Alice");
unlock(&mut m);
assert!(join(&mut m, "bob@test/b", "Alice").replies[0].contains("conflict"));
join(&mut m, "bob@test/b", "Bob");
let r = send(
&mut m,
"bob@test/b",
" ",
);
assert!(r.replies[0].contains("forbidden"));
let r = send(
&mut m,
"alice@test/a",
" ",
);
assert!(r.replies[0].contains("conflict"));
let r = send(
&mut m,
"alice@test/a",
" ",
);
assert_eq!(r.deliveries.len(), 2);
assert!(join(&mut m, "bob@test/b", "Bob").replies[0].contains("forbidden"));
}
#[test]
fn persistence_roundtrip() {
let t = tempfile::tempdir().unwrap();
let mut m = Muc::new(t.path().into(), "rooms.test".into());
join(&mut m, "alice@test/a", "Alice");
let r = send(
&mut m,
"alice@test/a",
"1Комната",
);
assert!(r.replies[0].contains("result"));
drop(m);
let mut m = Muc::new(t.path().into(), "rooms.test".into());
let r = send(
&mut m,
"alice@test/a",
"",
);
assert!(r.replies[0].contains("Комната"));
join(&mut m, "alice@test/a", "Alice");
}
#[test]
fn archive_history_and_permissions() {
let t = tempfile::tempdir().unwrap();
let mut m = Muc::new(t.path().into(), "rooms.test".into());
m.set_defaults(serde_json::json!({"archiving":true,"persistent":true}));
join(&mut m, "alice@test/a", "Alice");
unlock(&mut m);
send(
&mut m,
"alice@test/a",
"Archived",
);
let query = "";
// Native public/open room archives are readable by unaffiliated users.
assert!(send(&mut m, "outsider@test/a", query).replies[0].contains("Archived"));
let r = send(&mut m, "alice@test/a", query);
assert!(r.replies[0].contains("Archived"));
assert!(r.replies.last().unwrap().contains("1",
);
assert!(send(&mut m, "outsider@test/a", query).replies[0].contains("forbidden"));
}
#[test]
fn nick_invite_destroy_and_local_creation() {
let t = tempfile::tempdir().unwrap();
let mut m = Muc::new(t.path().into(), "rooms.test".into());
m.set_local_domain("test".into());
assert!(join(&mut m, "external@else/a", "External").replies[0].contains("forbidden"));
join(&mut m, "alice@test/a", "Alice");
unlock(&mut m);
let renamed = join(&mut m, "alice@test/a", "NewName");
assert!(renamed.replies[0].contains("303"));
assert!(renamed.replies[0].contains("nick='NewName'"));
let inv = send(
&mut m,
"alice@test/a",
"Join us",
);
assert_eq!(inv.deliveries[0].0, "bob@test");
assert!(inv.deliveries[0].1.contains("Join us"));
let destroy = "";
assert!(send(&mut m, "bob@test/a", destroy).replies[0].contains("forbidden"));
assert_eq!(send(&mut m, "alice@test/a", destroy).deliveries.len(), 1);
assert!(join(&mut m, "bob@test/a", "Bob").replies[0].contains("201"));
}
#[test]
fn preserves_unknown_binary_mixed_room_fields() {
use rust_xmpp::storage::{self, LuaValue as V};
let t = tempfile::tempdir().unwrap();
let mut raw = V::table();
raw.set("_jid", "r@rooms.test".into());
raw.set("alice@test", "owner".into());
let mut data = V::table();
data.set("persistent", V::Bool(true));
data.set("salt", V::String(vec![0xff, 0x00, 0x80]));
let mut mixed = V::table();
mixed.set("extension", "original".into());
mixed.set_key(V::Integer(1), "first".into());
data.set("mixed", mixed.clone());
raw.set("_data", data);
storage::save(t.path(), "rooms.test", Some("r"), "config", &raw).unwrap();
let mut m = Muc::new(t.path().into(), "rooms.test".into());
join(&mut m, "alice@test/a", "Alice");
send(
&mut m,
"alice@test/a",
"New",
);
let saved = storage::load(t.path(), "rooms.test", Some("r"), "config")
.unwrap()
.unwrap();
let d = saved.get("_data").unwrap();
assert_eq!(d.get("salt"), Some(&V::String(vec![0xff, 0x00, 0x80])));
assert_eq!(d.get("mixed"), Some(&mixed));
assert_eq!(d.get("name").unwrap().as_str(), Some("New"));
}
#[test]
fn multiple_resources_share_nick_and_private_delivery() {
let t = tempfile::tempdir().unwrap();
let mut m = Muc::new(t.path().into(), "rooms.test".into());
join(&mut m, "alice@test/a", "Alice");
unlock(&mut m);
join(&mut m, "bob@test/a", "Bob");
let second = join(&mut m, "bob@test/b", "Bob");
assert!(!second.replies.iter().any(|s| s.contains("type='error'")));
let pm = send(
&mut m,
"alice@test/a",
"Private",
);
assert_eq!(pm.deliveries.len(), 2);
assert_eq!(m.disconnect("bob@test/a").deliveries.len(), 0);
assert_eq!(m.disconnect("bob@test/b").deliveries.len(), 1);
}
#[test]
fn iq_response_requires_expected_sender() {
let t = tempfile::tempdir().unwrap();
let mut m = Muc::new(t.path().into(), "rooms.test".into());
join(&mut m, "alice@test/a", "Alice");
unlock(&mut m);
join(&mut m, "bob@test/b", "Bob");
let request = send(
&mut m,
"alice@test/a",
"",
);
let packet = roxmltree::Document::parse(&request.deliveries[0].1).unwrap();
let id = packet.root_element().attribute("id").unwrap();
let answer = format!("");
assert!(
send(&mut m, "mallory@test/c", &answer)
.deliveries
.is_empty()
);
let response = send(&mut m, "bob@test/b", &answer);
assert_eq!(response.deliveries[0].0, "alice@test/a");
assert!(response.deliveries[0].1.contains("id='original'"));
}
#[test]
fn persistent_destroy_creates_prosody_tombstone() {
let t = tempfile::tempdir().unwrap();
let mut m = Muc::new(t.path().into(), "rooms.test".into());
m.set_defaults(serde_json::json!({"persistent":true}));
join(&mut m, "alice@test/a", "Alice");
unlock(&mut m);
send(
&mut m,
"alice@test/a",
"Closed",
);
drop(m);
let mut m = Muc::new(t.path().into(), "rooms.test".into());
assert!(join(&mut m, "bob@test/a", "Bob").replies[0].contains("gone"));
let saved = rust_xmpp::storage::load(t.path(), "rooms.test", Some("r"), "config")
.unwrap()
.unwrap();
assert_eq!(
saved.get("_data").unwrap().get("destroyed"),
Some(&rust_xmpp::storage::LuaValue::Bool(true))
);
}
#[test]
fn reserved_nickname_blocks_other_users() {
let t = tempfile::tempdir().unwrap();
let mut m = Muc::new(t.path().into(), "rooms.test".into());
m.set_defaults(serde_json::json!({"persistent":true}));
join(&mut m, "alice@test/a", "Alice");
unlock(&mut m);
let reg = "http://jabber.org/protocol/muc#registerReserved";
assert!(send(&mut m, "alice@test/a", reg).replies[0].contains("type='result'"));
assert!(join(&mut m, "bob@test/a", "Reserved").replies[0].contains("conflict"));
drop(m);
let mut m = Muc::new(t.path().into(), "rooms.test".into());
assert!(join(&mut m, "bob@test/a", "Reserved").replies[0].contains("conflict"));
assert!(!join(&mut m, "alice@test/a", "Reserved").replies[0].contains("type='error'"));
}
#[test]
fn occupant_id_is_stable_and_spoofed_ids_removed() {
let t = tempfile::tempdir().unwrap();
let mut m = Muc::new(t.path().into(), "rooms.test".into());
join(&mut m, "alice@test/a", "Alice");
unlock(&mut m);
let first = send(
&mut m,
"alice@test/a",
"Hi",
);
let d = roxmltree::Document::parse(&first.deliveries[0].1).unwrap();
let ids = d
.descendants()
.filter(|n| n.has_tag_name(("urn:xmpp:occupant-id:0", "occupant-id")))
.collect::>();
assert_eq!(ids.len(), 1);
let id = ids[0].attribute("id").unwrap().to_owned();
assert_ne!(id, "fake");
let next = join(&mut m, "alice@test/b", "Alice");
assert!(next.replies.iter().any(|p| p.contains(&id)));
}
#[test]
fn switching_to_members_only_removes_unaffiliated_users() {
let t = tempfile::tempdir().unwrap();
let mut m = Muc::new(t.path().into(), "rooms.test".into());
join(&mut m, "alice@test/a", "Alice");
unlock(&mut m);
join(&mut m, "bob@test/a", "Bob");
let r = send(
&mut m,
"alice@test/a",
"1",
);
assert!(
r.deliveries
.iter()
.any(|(j, p)| j == "bob@test/a" && p.contains("322"))
);
assert!(
send(
&mut m,
"bob@test/a",
"Still here?"
)
.replies[0]
.contains("not-acceptable")
);
}
#[test]
fn room_vcard_owner_only_and_avatar_presence() {
let t = tempfile::tempdir().unwrap();
let mut m = Muc::new(t.path().into(), "rooms.test".into());
join(&mut m, "alice@test/a", "Alice");
unlock(&mut m);
let set = "Test Roomimage/pngaGVsbG8=";
assert!(send(&mut m, "bob@test/a", set).replies[0].contains("forbidden"));
let result = send(&mut m, "alice@test/a", set);
assert!(result.replies[0].contains("result"));
assert!(
result
.deliveries
.iter()
.any(|(_, p)| p.contains("vcard-temp:x:update"))
);
let get = send(
&mut m,
"bob@test/a",
"",
);
assert!(get.replies[0].contains("Test Room"));
let joined = join(&mut m, "bob@test/a", "Bob");
assert!(
joined
.replies
.iter()
.any(|p| p.contains("aaf4c61ddcc5e8a2dabede0f3b482cd9aea9434d"))
);
}
#[test]
fn visitor_voice_request_cannot_spoof_identity_or_grant_itself_voice() {
let t = tempfile::tempdir().unwrap();
let mut m = Muc::new(t.path().into(), "rooms.test".into());
m.set_defaults(serde_json::json!({"moderated":true}));
join(&mut m, "alice@test/a", "Alice");
unlock(&mut m);
join(&mut m, "bob@test/a", "Bob");
let message = "Hello";
assert!(send(&mut m, "bob@test/a", message).replies[0].contains("forbidden"));
let request = "http://jabber.org/protocol/muc#requestmallory@test/a1moderator";
let forwarded = send(&mut m, "bob@test/a", request);
assert_eq!(forwarded.deliveries.len(), 1);
assert_eq!(forwarded.deliveries[0].0, "alice@test/a");
assert!(forwarded.deliveries[0].1.contains("bob@test/a"));
assert!(!forwarded.deliveries[0].1.contains("mallory"));
assert!(send(&mut m, "bob@test/a", message).replies[0].contains("forbidden"));
let grant = request.replace("mallory@test/a", "bob@test/a");
let result = send(&mut m, "alice@test/a", &grant);
assert_eq!(result.deliveries.len(), 2);
assert!(result.deliveries[0].1.contains("role='participant'"));
assert_eq!(send(&mut m, "bob@test/a", message).deliveries.len(), 2);
}
#[test]
fn maintenance_expires_idle_archives_and_tombstones() {
use rust_xmpp::archive::{self, Entry};
let t = tempfile::tempdir().unwrap();
let mut m = Muc::new(t.path().into(), "rooms.test".into());
m.set_defaults(serde_json::json!({"persistent":true,"archiving":true}));
m.set_archive_retention(Some(60.0));
join(&mut m, "alice@test/a", "Alice");
unlock(&mut m);
let now = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_secs_f64();
for (id, when) in [("old", now - 100.0), ("new", now - 10.0)] {
archive::append_new(
t.path(),
"rooms.test",
Some("r"),
"muc_log",
Entry {
id: id.into(),
when,
peer: "alice@test/a".into(),
xml:
"Stored"
.into(),
},
)
.unwrap();
}
drop(m);
let mut m = Muc::new(t.path().into(), "rooms.test".into());
m.set_archive_retention(Some(60.0));
m.maintenance(now).unwrap();
let entries = archive::load_store(t.path(), "rooms.test", Some("r"), "muc_log").unwrap();
assert_eq!(entries.len(), 1);
assert_eq!(entries[0].id, "new");
send(
&mut m,
"alice@test/a",
"",
);
m.maintenance(now + 32.0 * 86400.0).unwrap();
assert!(join(&mut m, "bob@test/a", "Bob").replies[0].contains("201"));
}
#[test]
fn restored_temporary_room_is_deleted_after_last_disconnect() {
use rust_xmpp::storage::{self, LuaValue as V};
let t = tempfile::tempdir().unwrap();
let raw = storage::parse(
b"return {_jid='r@rooms.test';_data={persistent=false};['alice@test']='owner'}",
)
.unwrap();
storage::save(t.path(), "rooms.test", Some("r"), "config", &raw).unwrap();
storage::save(t.path(), "rooms.test", Some("r"), "state", &V::table()).unwrap();
let mut index = V::table();
index.set("r@rooms.test", V::Bool(true));
storage::save(t.path(), "rooms.test", None, "persistent", &index).unwrap();
let mut m = Muc::new(t.path().into(), "rooms.test".into());
join(&mut m, "alice@test/a", "Alice");
m.disconnect("alice@test/a");
assert!(
storage::load(t.path(), "rooms.test", Some("r"), "config")
.unwrap()
.is_none()
);
assert!(
storage::load(t.path(), "rooms.test", Some("r"), "state")
.unwrap()
.is_none()
);
assert!(join(&mut m, "bob@test/a", "Bob").replies[0].contains("201"));
}
#[test]
fn configured_lock_timeout_expires_without_owner_submission() {
let t = tempfile::tempdir().unwrap();
let mut m = Muc::new(t.path().into(), "rooms.test".into());
m.set_component_options(serde_json::json!({"muc_room_lock_timeout":0}), false);
join(&mut m, "alice@test/a", "Alice");
assert!(!join(&mut m, "bob@test/a", "Bob").replies[0].contains("item-not-found"));
}
#[test]
fn native_muc_archive_type_and_anonymity_are_preserved() {
use rust_xmpp::archive::{self, Entry};
let t = tempfile::tempdir().unwrap();
let mut m = Muc::new(t.path().into(), "rooms.test".into());
m.set_defaults(serde_json::json!({"persistent":true,"archiving":true,"whois":"moderators"}));
join(&mut m, "alice@test/a", "Alice");
unlock(&mut m);
let now = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_secs_f64();
archive::append_new(t.path(),"rooms.test",Some("r"),"muc_log",Entry{id:"native".into(),when:now,peer:"messageNative old message ".into()}).unwrap();
let joined = join(&mut m, "bob@test/b", "Bob");
let historic = joined
.replies
.iter()
.find(|s| s.contains("Native old message"))
.unwrap();
assert!(!historic.contains("secret@example"));
let query = "urn:xmpp:mam:2secret@example/r";
let result = send(&mut m, "bob@test/b", query);
assert!(
result
.replies
.iter()
.any(|s| s.contains("Native old message"))
);
assert!(!result.replies.iter().any(|s| s.contains("secret@example")));
send(
&mut m,
"alice@test/a",
"New",
);
assert!(
archive::load_store(t.path(), "rooms.test", Some("r"), "muc_log")
.unwrap()
.iter()
.all(|e| e.peer == "message