use rust_xmpp::{
pubsub,
storage::{self, LuaValue as V},
};
use std::path::Path;
fn account(root: &Path, user: &str) {
let mut data = V::table();
data.set("password", "test-only".into());
storage::save(root, "example.test", Some(user), "accounts", &data).unwrap();
}
fn request(root: &Path, actor: &str, body: &str) -> String {
let doc = roxmltree::Document::parse(body).unwrap();
pubsub::handle(root, "example.test", actor, doc.root_element())
.unwrap()
.unwrap()
.replies
.join("")
}
fn iq(typ: &str, to: &str, content: &str) -> String {
format!(
"{content}"
)
}
#[test]
fn pep_persists_namespaces_and_enforces_owner() {
let d = tempfile::tempdir().unwrap();
account(d.path(), "alice");
account(d.path(), "bob");
let publish = iq(
"set",
"alice@example.test",
" ",
);
assert!(request(d.path(), "bob@example.test/r", &publish).contains("forbidden"));
assert!(request(d.path(), "alice@example.test/r", &publish).contains("type='result'"));
let get = iq(
"get",
"alice@example.test",
"",
);
assert!(request(d.path(), "bob@example.test/r", &get).contains("forbidden"));
let response = request(d.path(), "alice@example.test/r", &get);
let doc = roxmltree::Document::parse(&response).unwrap();
let device = doc
.descendants()
.find(|n| n.has_tag_name(("urn:xmpp:omemo:2", "device")))
.unwrap();
assert_eq!(device.attribute(("urn:extension", "label")), Some("phone"));
let nodes = storage::load(d.path(), "example.test", Some("alice"), "pep")
.unwrap()
.unwrap();
assert!(nodes.get("urn:xmpp:omemo:2:devices").is_some());
}
#[test]
fn open_model_and_notifications() {
let d = tempfile::tempdir().unwrap();
account(d.path(), "alice");
account(d.path(), "bob");
let create = iq(
"set",
"alice@example.test",
"http://jabber.org/protocol/pubsub#node_configopen",
);
assert!(request(d.path(), "alice@example.test/r", &create).contains("type='result'"));
let sub = iq(
"set",
"alice@example.test",
"",
);
assert!(request(d.path(), "bob@example.test/r", &sub).contains("subscribed"));
let p = iq(
"set",
"alice@example.test",
"- abc
",
);
let doc = roxmltree::Document::parse(&p).unwrap();
let result = pubsub::handle(
d.path(),
"example.test",
"alice@example.test/r",
doc.root_element(),
)
.unwrap()
.unwrap();
assert_eq!(result.deliveries.len(), 1);
assert!(
result
.deliveries
.iter()
.any(|d| d.0 == "bob@example.test/r")
);
assert!(
request(
d.path(),
"bob@example.test/r",
&iq("get", "alice@example.test", "")
)
.contains("abc")
);
}
#[test]
fn roster_presence_access_and_foreign_subscription_denied() {
let d = tempfile::tempdir().unwrap();
account(d.path(), "alice");
let p = iq(
"set",
"alice@example.test",
" ",
);
request(d.path(), "alice@example.test/r", &p);
let mut roster = V::table();
let mut contact = V::table();
contact.set("subscription", "from".into());
roster.set("bob@example.test", contact);
storage::save(d.path(), "example.test", Some("alice"), "roster", &roster).unwrap();
assert!(
request(
d.path(),
"bob@example.test/r",
&iq("get", "alice@example.test", "")
)
.contains("type='result'")
);
assert!(
request(
d.path(),
"bob@example.test/r",
&iq(
"set",
"alice@example.test",
""
)
)
.contains("forbidden")
);
}
#[test]
fn configure_requires_owner() {
let d = tempfile::tempdir().unwrap();
account(d.path(), "alice");
request(
d.path(),
"alice@example.test/r",
&iq("set", "alice@example.test", ""),
);
let x = "";
assert!(request(d.path(), "bob@example.test/r", x).contains("forbidden"));
assert!(request(d.path(), "alice@example.test/r", x).contains("type='result'"));
assert!(
request(
d.path(),
"alice@example.test/r",
&iq("get", "alice@example.test", "")
)
.contains("item-not-found")
);
}
#[test]
fn reads_prosody_legacy_fixture() {
let d = tempfile::tempdir().unwrap();
account(d.path(), "alice");
let p = storage::path(d.path(), "example.test", Some("alice"), "pep");
storage::atomic_write(&p,b"return { ['urn:old']={name='urn:old';config={access_model='open';max_items=1};subscribers={};affiliations={['alice@example.test']='owner'}}};").unwrap();
let archive = storage::path(d.path(), "example.test", Some("alice"), "pep_urn:old")
.with_extension("list");
storage::atomic_write(&archive,b"item({name='payload';attr={xmlns='urn:old';stamp='2020-01-01T00:00:00Z'};[1]='persisted';key='current';when=1577836800;with='alice@example.test'});\n").unwrap();
let out = request(
d.path(),
"bob@example.test/r",
&iq("get", "alice@example.test", ""),
);
assert!(out.contains("persisted"), "{out}");
let doc = roxmltree::Document::parse(&out).unwrap();
assert!(
doc.descendants()
.any(|n| n.has_tag_name(("urn:old", "payload")))
);
}
#[test]
fn configurable_limits_component_creation_and_publisher() {
let d = tempfile::tempdir().unwrap();
let options = pubsub::Options {
max_items: 10000,
restrict_node_creation: false,
autocreate_on_publish: false,
};
let run = |actor: &str, xml: &str| {
let doc = roxmltree::Document::parse(xml).unwrap();
pubsub::handle_component_with_options(
d.path(),
"pubsub.example.test",
actor,
&[],
doc.root_element(),
&options,
)
.unwrap()
.unwrap()
.replies
.join("")
};
assert!(run("alice@example.test/r",&iq("set","pubsub.example.test","10000")).contains("type='result'"));
let set = "";
assert!(run("alice@example.test/r", set).contains("type='result'"));
let publish = iq(
"set",
"pubsub.example.test",
" ",
);
assert!(run("bob@example.test/r", &publish).contains("type='result'"));
assert!(run("eve@example.test/r", &publish).contains("forbidden"));
}
#[test]
fn capability_discovery_replays_and_filters_notifications() {
let d = tempfile::tempdir().unwrap();
account(d.path(), "alice");
account(d.path(), "bob");
let mut a = V::table();
let mut c = V::table();
c.set("subscription", "from".into());
a.set("bob@example.test", c);
storage::save(d.path(), "example.test", Some("alice"), "roster", &a).unwrap();
let mut b = V::table();
let mut c = V::table();
c.set("subscription", "to".into());
b.set("alice@example.test", c);
storage::save(d.path(), "example.test", Some("bob"), "roster", &b).unwrap();
let publish = iq(
"set",
"alice@example.test",
"- hello
",
);
request(d.path(), "alice@example.test/r", &publish);
let presence=roxmltree::Document::parse("").unwrap();
let query = pubsub::on_presence(
d.path(),
"example.test",
"bob@example.test/r",
presence.root_element(),
)
.unwrap();
assert_eq!(query.deliveries.len(), 1);
let querydoc = roxmltree::Document::parse(&query.deliveries[0].1).unwrap();
let id = querydoc.root_element().attribute("id").unwrap();
let response = format!(
""
);
let doc = roxmltree::Document::parse(&response).unwrap();
assert!(
pubsub::on_disco_result(
d.path(),
"example.test",
"eve@example.test/r",
doc.root_element()
)
.unwrap()
.is_none()
);
let replay = pubsub::on_disco_result(
d.path(),
"example.test",
"bob@example.test/r",
doc.root_element(),
)
.unwrap()
.unwrap();
assert_eq!(replay.deliveries.len(), 1);
assert!(replay.deliveries[0].1.contains("hello"));
let doc = roxmltree::Document::parse(&publish).unwrap();
let events = pubsub::handle(
d.path(),
"example.test",
"alice@example.test/r",
doc.root_element(),
)
.unwrap()
.unwrap();
assert!(
events
.deliveries
.iter()
.any(|d| d.0 == "bob@example.test/r")
);
storage::save(
d.path(),
"example.test",
Some("alice"),
"roster",
&V::table(),
)
.unwrap();
let events = pubsub::handle(
d.path(),
"example.test",
"alice@example.test/r",
doc.root_element(),
)
.unwrap()
.unwrap();
assert!(
!events
.deliveries
.iter()
.any(|d| d.0 == "bob@example.test/r")
);
}
#[test]
fn subscription_options_disable_events_and_require_subscriber() {
let d = tempfile::tempdir().unwrap();
account(d.path(), "alice");
request(
d.path(),
"alice@example.test/r",
&iq(
"set",
"alice@example.test",
"open",
),
);
let sub = iq(
"set",
"alice@example.test",
"http://jabber.org/protocol/pubsub#subscribe_options0",
);
assert!(request(d.path(), "bob@example.test/r", &sub).contains("subscribed"));
let get = iq(
"get",
"alice@example.test",
"",
);
assert!(request(d.path(), "bob@example.test/r", &get).contains("0"));
assert!(request(d.path(), "eve@example.test/r", &get).contains("forbidden"));
let pubxml = iq(
"set",
"alice@example.test",
" ",
);
let doc = roxmltree::Document::parse(&pubxml).unwrap();
let result = pubsub::handle(
d.path(),
"example.test",
"alice@example.test/r",
doc.root_element(),
)
.unwrap()
.unwrap();
assert!(
!result
.deliveries
.iter()
.any(|d| d.0 == "bob@example.test/r")
);
}
#[test]
fn nonpersistent_items_stay_in_memory_and_can_be_made_persistent() {
let d = tempfile::tempdir().unwrap();
account(d.path(), "alice");
let create = iq(
"set",
"alice@example.test",
"0",
);
assert!(request(d.path(), "alice@example.test/r", &create).contains("type='result'"));
let p = iq(
"set",
"alice@example.test",
"- memory-only
",
);
assert!(request(d.path(), "alice@example.test/r", &p).contains("type='result'"));
let get = iq("get", "alice@example.test", "");
assert!(request(d.path(), "alice@example.test/r", &get).contains("memory-only"));
assert!(
rust_xmpp::archive::load_store(d.path(), "example.test", Some("alice"), "pep_n")
.unwrap()
.is_empty()
);
let config = "1";
assert!(request(d.path(), "alice@example.test/r", config).contains("type='result'"));
assert_eq!(
rust_xmpp::archive::load_store(d.path(), "example.test", Some("alice"), "pep_n")
.unwrap()
.len(),
1
);
}
#[test]
fn modern_bookmarks_notify_legacy_capability() {
let d = tempfile::tempdir().unwrap();
account(d.path(), "alice");
let presence=roxmltree::Document::parse("").unwrap();
let q = pubsub::on_presence(
d.path(),
"example.test",
"alice@example.test/old",
presence.root_element(),
)
.unwrap();
let qdoc = roxmltree::Document::parse(&q.deliveries[0].1).unwrap();
let id = qdoc.root_element().attribute("id").unwrap();
let answer = format!(
""
);
let doc = roxmltree::Document::parse(&answer).unwrap();
pubsub::on_disco_result(
d.path(),
"example.test",
"alice@example.test/old",
doc.root_element(),
)
.unwrap();
let publish = iq(
"set",
"alice@example.test",
"- Alice
",
);
let doc = roxmltree::Document::parse(&publish).unwrap();
let events = pubsub::handle(
d.path(),
"example.test",
"alice@example.test/new",
doc.root_element(),
)
.unwrap()
.unwrap();
let legacy = events
.deliveries
.iter()
.find(|d| d.0 == "alice@example.test/old")
.unwrap();
assert!(legacy.1.contains("storage:bookmarks"));
assert!(legacy.1.contains("jid='room@conference.example.test'"));
assert!(legacy.1.contains("Alice"));
}
#[test]
fn remote_caps_replay_and_open_node_privacy_revoke() {
let d = tempfile::tempdir().unwrap();
account(d.path(), "alice");
let create = iq(
"set",
"alice@example.test",
"open",
);
request(d.path(), "alice@example.test/r", &create);
let p = iq(
"set",
"alice@example.test",
"- remote payload
",
);
request(d.path(), "alice@example.test/r", &p);
let presence=roxmltree::Document::parse("").unwrap();
assert!(
pubsub::on_remote_presence(
d.path(),
"example.test",
"bob@remote.test/r",
presence.root_element()
)
.unwrap()
.deliveries
.is_empty()
);
let mut roster = V::table();
let mut c = V::table();
c.set("subscription", "from".into());
roster.set("bob@remote.test", c);
storage::save(d.path(), "example.test", Some("alice"), "roster", &roster).unwrap();
let q = pubsub::on_remote_presence(
d.path(),
"example.test",
"bob@remote.test/r",
presence.root_element(),
)
.unwrap();
assert_eq!(q.deliveries.len(), 1);
let doc = roxmltree::Document::parse(&q.deliveries[0].1).unwrap();
let id = doc.root_element().attribute("id").unwrap();
let answer = format!(
""
);
let doc = roxmltree::Document::parse(&answer).unwrap();
assert!(
pubsub::on_disco_result(
d.path(),
"example.test",
"mallory@remote.test/r",
doc.root_element()
)
.unwrap()
.is_none()
);
let result = pubsub::on_disco_result(
d.path(),
"example.test",
"bob@remote.test/r",
doc.root_element(),
)
.unwrap()
.unwrap();
assert_eq!(result.deliveries.len(), 1);
assert!(result.deliveries[0].1.contains("remote payload"));
let doc = roxmltree::Document::parse(&p).unwrap();
let result = pubsub::handle(
d.path(),
"example.test",
"alice@example.test/r",
doc.root_element(),
)
.unwrap()
.unwrap();
assert!(result.deliveries.iter().any(|e| e.0 == "bob@remote.test/r"));
storage::save(
d.path(),
"example.test",
Some("alice"),
"roster",
&V::table(),
)
.unwrap();
let result = pubsub::handle(
d.path(),
"example.test",
"alice@example.test/r",
doc.root_element(),
)
.unwrap()
.unwrap();
assert!(!result.deliveries.iter().any(|e| e.0 == "bob@remote.test/r"));
}
#[test]
fn component_default_retention_matches_prosody_twenty_items() {
let d = tempfile::tempdir().unwrap();
let options = pubsub::Options {
restrict_node_creation: false,
..Default::default()
};
let run = |xml: String| {
let doc = roxmltree::Document::parse(&xml).unwrap();
pubsub::handle_component_with_options(
d.path(),
"pubsub.example.test",
"alice@example.test/r",
&[],
doc.root_element(),
&options,
)
.unwrap()
.unwrap()
};
run(iq("set", "pubsub.example.test", ""));
for i in 0..22 {
run(iq(
"set",
"pubsub.example.test",
&format!(" "),
));
}
let entries =
rust_xmpp::archive::load_store(d.path(), "pubsub.example.test", None, "pubsub_n").unwrap();
assert_eq!(entries.len(), 20);
assert_eq!(entries[0].id, "2");
}
#[test]
fn remote_unavailable_is_scoped_to_addressed_owner_and_cancels_pending() {
let d = tempfile::tempdir().unwrap();
for owner in ["alice", "carol"] {
account(d.path(), owner);
let mut roster = V::table();
let mut c = V::table();
c.set("subscription", "from".into());
roster.set("bob@remote.test", c);
storage::save(d.path(), "example.test", Some(owner), "roster", &roster).unwrap();
}
let mut ids = vec![];
for owner in ["alice", "carol"] {
let p = format!(
""
);
let doc = roxmltree::Document::parse(&p).unwrap();
let q = pubsub::on_remote_presence(
d.path(),
"example.test",
"bob@remote.test/r",
doc.root_element(),
)
.unwrap();
let qdoc = roxmltree::Document::parse(&q.deliveries[0].1).unwrap();
ids.push(qdoc.root_element().attribute("id").unwrap().to_owned());
}
let unavailable = roxmltree::Document::parse(
"",
)
.unwrap();
pubsub::on_remote_presence(
d.path(),
"example.test",
"bob@remote.test",
unavailable.root_element(),
)
.unwrap();
for (i, id) in ids.iter().enumerate() {
let answer = format!(
""
);
let doc = roxmltree::Document::parse(&answer).unwrap();
let result = pubsub::on_disco_result(
d.path(),
"example.test",
"bob@remote.test/r",
doc.root_element(),
)
.unwrap();
assert_eq!(result.is_some(), i == 1);
}
for owner in ["alice", "carol"] {
let p = iq(
"set",
&format!("{owner}@example.test"),
" ",
);
let doc = roxmltree::Document::parse(&p).unwrap();
let r = pubsub::handle(
d.path(),
"example.test",
&format!("{owner}@example.test/r"),
doc.root_element(),
)
.unwrap()
.unwrap();
assert_eq!(
r.deliveries.iter().any(|e| e.0 == "bob@remote.test/r"),
owner == "carol"
);
}
}
#[test]
fn payload_policy_applies_to_subscription_and_caps_replays() {
for (node, advertised, payload) in [
(
"urn:test:private",
"urn:test:private",
"payload-secret",
),
(
"urn:xmpp:bookmarks:1",
"storage:bookmarks",
"",
),
] {
let d = tempfile::tempdir().unwrap();
account(d.path(), "alice");
let create = iq(
"set",
"alice@example.test",
&format!(
"0"
),
);
assert!(request(d.path(), "alice@example.test/r", &create).contains("type='result'"));
let publish = iq(
"set",
"alice@example.test",
&format!(
"- {payload}
"
),
);
assert!(request(d.path(), "alice@example.test/r", &publish).contains("type='result'"));
let subscribe = iq(
"set",
"alice@example.test",
&format!(""),
);
let doc = roxmltree::Document::parse(&subscribe).unwrap();
let result = pubsub::handle(
d.path(),
"example.test",
"alice@example.test/r",
doc.root_element(),
)
.unwrap()
.unwrap();
assert_eq!(result.deliveries.len(), 1);
assert!(
result.deliveries[0]
.1
.contains(" "),
"{:?}",
result.deliveries
);
assert!(!result.deliveries[0].1.contains("payload-secret"));
let presence = roxmltree::Document::parse(
"",
)
.unwrap();
let query = pubsub::on_presence(
d.path(),
"example.test",
"alice@example.test/caps",
presence.root_element(),
)
.unwrap();
let query_doc = roxmltree::Document::parse(&query.deliveries[0].1).unwrap();
let id = query_doc.root_element().attribute("id").unwrap();
let answer = format!(
""
);
let doc = roxmltree::Document::parse(&answer).unwrap();
let result = pubsub::on_disco_result(
d.path(),
"example.test",
"alice@example.test/caps",
doc.root_element(),
)
.unwrap()
.unwrap();
assert_eq!(result.deliveries.len(), 1);
assert!(
!result.deliveries[0].1.contains("payload-secret"),
"{:?}",
result.deliveries
);
let doc = roxmltree::Document::parse(&publish).unwrap();
let result = pubsub::handle(
d.path(),
"example.test",
"alice@example.test/r",
doc.root_element(),
)
.unwrap()
.unwrap();
assert_eq!(result.deliveries.len(), 2);
assert!(
result
.deliveries
.iter()
.all(|(_, xml)| !xml.contains("payload-secret"))
);
// Omitting notification payload does not discard the stored item.
let get = iq(
"get",
"alice@example.test",
&format!(""),
);
assert!(request(d.path(), "alice@example.test/r", &get).contains("payload-secret"));
}
}
#[test]
fn publish_options_conflict_is_recoverable_and_preserves_item() {
let d = tempfile::tempdir().unwrap();
account(d.path(), "alice");
let node = "urn:xmpp:omemo:2:devices";
let publish = |value: &str, access: &str| {
iq(
"set",
"alice@example.test",
&format!(
"- {value}
http://jabber.org/protocol/pubsub#publish-options{access}"
),
)
};
assert!(
request(
d.path(),
"alice@example.test/r",
&publish("original", "presence")
)
.contains("type='result'")
);
let reply = request(
d.path(),
"alice@example.test/r",
&publish("replacement", "open"),
);
let doc = roxmltree::Document::parse(&reply).unwrap();
assert!(
doc.descendants()
.any(|n| n.has_tag_name(("urn:ietf:params:xml:ns:xmpp-stanzas", "conflict")))
);
assert!(doc.descendants().any(|n| n.has_tag_name((
"http://jabber.org/protocol/pubsub#errors",
"precondition-not-met"
))));
let get = iq(
"get",
"alice@example.test",
&format!(""),
);
let reply = request(d.path(), "alice@example.test/r", &get);
assert!(reply.contains("original"));
assert!(!reply.contains("replacement"));
}
#[test]
fn full_jid_block_applies_to_reads_subscription_replays_and_events() {
let d = tempfile::tempdir().unwrap();
account(d.path(), "alice");
account(d.path(), "bob");
let create = iq(
"set",
"alice@example.test",
"open",
);
assert!(request(d.path(), "alice@example.test/r", &create).contains("type='result'"));
let subscribe = iq(
"set",
"alice@example.test",
"",
);
assert!(request(d.path(), "bob@example.test/blocked", &subscribe).contains("subscribed"));
let publish = iq(
"set",
"alice@example.test",
"- secret
",
);
request(d.path(), "alice@example.test/r", &publish);
let mut blocked = V::table();
blocked.set("bob@example.test/blocked", V::Bool(true));
storage::save(
d.path(),
"example.test",
Some("alice"),
"blocklist",
&blocked,
)
.unwrap();
let get = iq("get", "alice@example.test", "");
assert!(request(d.path(), "bob@example.test/blocked", &get).contains("forbidden"));
assert!(request(d.path(), "bob@example.test/allowed", &get).contains("secret"));
// A different resource must not cause a replay to the blocked resource.
assert!(request(d.path(), "bob@example.test/allowed", &subscribe).contains("forbidden"));
let doc = roxmltree::Document::parse(&publish).unwrap();
let result = pubsub::handle(
d.path(),
"example.test",
"alice@example.test/r",
doc.root_element(),
)
.unwrap()
.unwrap();
assert!(result.deliveries.is_empty(), "{:?}", result.deliveries);
}
#[test]
fn native_string_max_items_satisfies_numeric_publish_precondition() {
for maximum in ["1", "max"] {
let d = tempfile::tempdir().unwrap();
account(d.path(), "alice");
let mut config = V::table();
config.set("max_items", maximum.into());
let mut node = V::table();
node.set("name", "urn:test:devices".into());
node.set("config", config);
let mut metadata = V::table();
metadata.set("urn:test:devices", node);
storage::save(d.path(), "example.test", Some("alice"), "pep", &metadata).unwrap();
let publish = iq(
"set",
"alice@example.test",
&format!(
" http://jabber.org/protocol/pubsub#publish-options{maximum}"
),
);
let result = request(d.path(), "alice@example.test/r", &publish);
assert!(result.contains("type='result'"), "{result}");
}
}
#[test]
fn native_notification_fields_and_retract_policy_are_independent() {
for component in [false, true] {
let d = tempfile::tempdir().unwrap();
account(d.path(), "alice");
let target = if component {
"pubsub.example.test"
} else {
"alice@example.test"
};
let options = pubsub::Options {
restrict_node_creation: false,
..Default::default()
};
let run = |actor: &str, xml: &str| {
let doc = roxmltree::Document::parse(xml).unwrap();
if component {
pubsub::handle_component_with_options(
d.path(),
target,
actor,
&[],
doc.root_element(),
&options,
)
} else {
pubsub::handle(d.path(), "example.test", actor, doc.root_element())
}
.unwrap()
.unwrap()
};
let create = iq(
"set",
target,
"open001",
);
assert!(run("alice@example.test/r", &create).replies[0].contains("type='result'"));
run(
"bob@example.test/r",
&iq(
"set",
target,
"",
),
);
let publish = iq(
"set",
target,
"- secret
",
);
let retract = iq(
"set",
target,
" ",
);
assert!(run("alice@example.test/r", &publish).deliveries.is_empty());
let result = run("alice@example.test/r", &retract);
assert_eq!(result.deliveries.len(), 1, "{:?}", result);
assert!(
result.deliveries[0]
.1
.contains("")
);
let configure = format!(
"10"
);
run("alice@example.test/r", &configure);
let result = run("alice@example.test/r", &publish);
assert_eq!(result.deliveries.len(), 1);
assert!(!result.deliveries[0].1.contains("secret"));
assert!(run("alice@example.test/r", &retract).deliveries.is_empty());
let get = format!(
""
);
let result = run("alice@example.test/r", &get);
assert!(result.replies[0].contains("pubsub#deliver_notifications"));
assert!(result.replies[0].contains("pubsub#deliver_payloads"));
assert!(!result.replies[0].contains("pubsub#notify_items"));
assert!(!result.replies[0].contains("pubsub#include_payload"));
}
}