Files
rust-xmpp/tests/pubsub_persistence.rs
2026-10-20 15:59:55 +07:00

952 lines
34 KiB
Rust

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!(
"<iq type='{typ}' id='x' to='{to}'><pubsub xmlns='http://jabber.org/protocol/pubsub'>{content}</pubsub></iq>"
)
}
#[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",
"<publish node='urn:xmpp:omemo:2:devices'><item id='current'><devices xmlns='urn:xmpp:omemo:2' xmlns:ex='urn:extension'><device id='123' ex:label='phone'/></devices></item></publish>",
);
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",
"<items node='urn:xmpp:omemo:2:devices'/>",
);
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",
"<create node='test'/><configure><x xmlns='jabber:x:data' type='submit'><field var='FORM_TYPE'><value>http://jabber.org/protocol/pubsub#node_config</value></field><field var='pubsub#access_model'><value>open</value></field></x></configure>",
);
assert!(request(d.path(), "alice@example.test/r", &create).contains("type='result'"));
let sub = iq(
"set",
"alice@example.test",
"<subscribe node='test' jid='bob@example.test/r'/>",
);
assert!(request(d.path(), "bob@example.test/r", &sub).contains("subscribed"));
let p = iq(
"set",
"alice@example.test",
"<publish node='test'><item id='a'><data xmlns='urn:test'>abc</data></item></publish>",
);
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", "<items node='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",
"<publish node='test'><item id='a'><x xmlns='urn:test'/></item></publish>",
);
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", "<items node='test'/>")
)
.contains("type='result'")
);
assert!(
request(
d.path(),
"bob@example.test/r",
&iq(
"set",
"alice@example.test",
"<subscribe node='test' jid='eve@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", "<create node='test'/>"),
);
let x = "<iq type='set' to='alice@example.test' id='x'><pubsub xmlns='http://jabber.org/protocol/pubsub#owner'><delete node='test'/></pubsub></iq>";
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", "<items node='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", "<items node='urn:old'/>"),
);
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","<create node='n'/><configure><x xmlns='jabber:x:data' type='submit'><field var='pubsub#max_items'><value>10000</value></field></x></configure>")).contains("type='result'"));
let set = "<iq type='set' to='pubsub.example.test'><pubsub xmlns='http://jabber.org/protocol/pubsub#owner'><affiliations node='n'><affiliation jid='bob@example.test' affiliation='publisher'/></affiliations></pubsub></iq>";
assert!(run("alice@example.test/r", set).contains("type='result'"));
let publish = iq(
"set",
"pubsub.example.test",
"<publish node='n'><item id='a'><x xmlns='urn:test'/></item></publish>",
);
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",
"<publish node='urn:test'><item id='one'><x xmlns='urn:test'>hello</x></item></publish>",
);
request(d.path(), "alice@example.test/r", &publish);
let presence=roxmltree::Document::parse("<presence><c xmlns='http://jabber.org/protocol/caps' hash='sha-1' node='urn:client' ver='abc'/></presence>").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!(
"<iq type='result' id='{id}'><query xmlns='http://jabber.org/protocol/disco#info'><feature var='urn:test+notify'/></query></iq>"
);
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",
"<create node='n'/><configure><x xmlns='jabber:x:data' type='submit'><field var='pubsub#access_model'><value>open</value></field></x></configure>",
),
);
let sub = iq(
"set",
"alice@example.test",
"<subscribe node='n' jid='bob@example.test/r'/><options><x xmlns='jabber:x:data' type='submit'><field var='FORM_TYPE'><value>http://jabber.org/protocol/pubsub#subscribe_options</value></field><field var='pubsub#deliver'><value>0</value></field></x></options>",
);
assert!(request(d.path(), "bob@example.test/r", &sub).contains("subscribed"));
let get = iq(
"get",
"alice@example.test",
"<options node='n' jid='bob@example.test/r'/>",
);
assert!(request(d.path(), "bob@example.test/r", &get).contains("<value>0</value>"));
assert!(request(d.path(), "eve@example.test/r", &get).contains("forbidden"));
let pubxml = iq(
"set",
"alice@example.test",
"<publish node='n'><item id='x'><x xmlns='urn:test'/></item></publish>",
);
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",
"<create node='n'/><configure><x xmlns='jabber:x:data' type='submit'><field var='pubsub#persist_items'><value>0</value></field></x></configure>",
);
assert!(request(d.path(), "alice@example.test/r", &create).contains("type='result'"));
let p = iq(
"set",
"alice@example.test",
"<publish node='n'><item id='a'><x xmlns='urn:test'>memory-only</x></item></publish>",
);
assert!(request(d.path(), "alice@example.test/r", &p).contains("type='result'"));
let get = iq("get", "alice@example.test", "<items node='n'/>");
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 = "<iq type='set' to='alice@example.test'><pubsub xmlns='http://jabber.org/protocol/pubsub#owner'><configure node='n'><x xmlns='jabber:x:data' type='submit'><field var='pubsub#persist_items'><value>1</value></field></x></configure></pubsub></iq>";
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("<presence><c xmlns='http://jabber.org/protocol/caps' hash='sha-1' node='client' ver='v'/></presence>").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!(
"<iq type='result' id='{id}'><query xmlns='http://jabber.org/protocol/disco#info'><feature var='storage:bookmarks+notify'/></query></iq>"
);
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",
"<publish node='urn:xmpp:bookmarks:1'><item id='room@conference.example.test'><conference xmlns='urn:xmpp:bookmarks:1' name='Room' autojoin='true'><nick>Alice</nick></conference></item></publish>",
);
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("<nick>Alice</nick>"));
}
#[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",
"<create node='urn:test'/><configure><x xmlns='jabber:x:data' type='submit'><field var='pubsub#access_model'><value>open</value></field></x></configure>",
);
request(d.path(), "alice@example.test/r", &create);
let p = iq(
"set",
"alice@example.test",
"<publish node='urn:test'><item id='x'><x xmlns='urn:test'>remote payload</x></item></publish>",
);
request(d.path(), "alice@example.test/r", &p);
let presence=roxmltree::Document::parse("<presence from='bob@remote.test/r' to='alice@example.test'><c xmlns='http://jabber.org/protocol/caps' node='client' ver='v' hash='sha-1'/></presence>").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!(
"<iq type='result' id='{id}'><query xmlns='http://jabber.org/protocol/disco#info'><feature var='urn:test+notify'/></query></iq>"
);
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", "<create node='n'/>"));
for i in 0..22 {
run(iq(
"set",
"pubsub.example.test",
&format!("<publish node='n'><item id='{i}'><x xmlns='urn:test'/></item></publish>"),
));
}
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!(
"<presence from='bob@remote.test/r' to='{owner}@example.test'><c xmlns='http://jabber.org/protocol/caps' ver='v'/></presence>"
);
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(
"<presence from='bob@remote.test' to='alice@example.test' type='unavailable'/>",
)
.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!(
"<iq type='result' id='{id}'><query xmlns='http://jabber.org/protocol/disco#info'><feature var='urn:test+notify'/></query></iq>"
);
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"),
"<publish node='urn:test'><item id='x'><x xmlns='urn:test'/></item></publish>",
);
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",
"<secret xmlns='urn:test'>payload-secret</secret>",
),
(
"urn:xmpp:bookmarks:1",
"storage:bookmarks",
"<conference xmlns='urn:xmpp:bookmarks:1' name='payload-secret'/>",
),
] {
let d = tempfile::tempdir().unwrap();
account(d.path(), "alice");
let create = iq(
"set",
"alice@example.test",
&format!(
"<create node='{node}'/><configure><x xmlns='jabber:x:data' type='submit'><field var='pubsub#include_payload'><value>0</value></field></x></configure>"
),
);
assert!(request(d.path(), "alice@example.test/r", &create).contains("type='result'"));
let publish = iq(
"set",
"alice@example.test",
&format!(
"<publish node='{node}'><item id='room@conference.example.test'>{payload}</item></publish>"
),
);
assert!(request(d.path(), "alice@example.test/r", &publish).contains("type='result'"));
let subscribe = iq(
"set",
"alice@example.test",
&format!("<subscribe node='{node}' jid='alice@example.test/subscriber'/>"),
);
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("<item id='room@conference.example.test'/>"),
"{:?}",
result.deliveries
);
assert!(!result.deliveries[0].1.contains("payload-secret"));
let presence = roxmltree::Document::parse(
"<presence><c xmlns='http://jabber.org/protocol/caps' ver='v'/></presence>",
)
.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!(
"<iq type='result' id='{id}'><query xmlns='http://jabber.org/protocol/disco#info'><feature var='{advertised}+notify'/></query></iq>"
);
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!("<items node='{node}'/>"),
);
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!(
"<publish node='{node}'><item id='current'><devices xmlns='urn:xmpp:omemo:2'>{value}</devices></item></publish><publish-options><x xmlns='jabber:x:data' type='submit'><field var='FORM_TYPE'><value>http://jabber.org/protocol/pubsub#publish-options</value></field><field var='pubsub#access_model'><value>{access}</value></field></x></publish-options>"
),
)
};
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!("<items node='{node}'/>"),
);
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",
"<create node='n'/><configure><x xmlns='jabber:x:data' type='submit'><field var='pubsub#access_model'><value>open</value></field></x></configure>",
);
assert!(request(d.path(), "alice@example.test/r", &create).contains("type='result'"));
let subscribe = iq(
"set",
"alice@example.test",
"<subscribe node='n' jid='bob@example.test/blocked'/>",
);
assert!(request(d.path(), "bob@example.test/blocked", &subscribe).contains("subscribed"));
let publish = iq(
"set",
"alice@example.test",
"<publish node='n'><item id='one'><x xmlns='urn:test'>secret</x></item></publish>",
);
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", "<items node='n'/>");
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!(
"<publish node='urn:test:devices'><item id='current'><devices xmlns='urn:test:devices'/></item></publish><publish-options><x xmlns='jabber:x:data' type='submit'><field var='FORM_TYPE'><value>http://jabber.org/protocol/pubsub#publish-options</value></field><field var='pubsub#max_items'><value>{maximum}</value></field></x></publish-options>"
),
);
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,
"<create node='n'/><configure><x xmlns='jabber:x:data' type='submit'><field var='pubsub#access_model'><value>open</value></field><field var='pubsub#deliver_notifications'><value>0</value></field><field var='pubsub#deliver_payloads'><value>0</value></field><field var='pubsub#notify_retract'><value>1</value></field></x></configure>",
);
assert!(run("alice@example.test/r", &create).replies[0].contains("type='result'"));
run(
"bob@example.test/r",
&iq(
"set",
target,
"<subscribe node='n' jid='bob@example.test/r'/>",
),
);
let publish = iq(
"set",
target,
"<publish node='n'><item id='one'><x xmlns='urn:test'>secret</x></item></publish>",
);
let retract = iq(
"set",
target,
"<retract node='n' notify='true'><item id='one'/></retract>",
);
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("<items node='n'><retract id='one'/></items>")
);
let configure = format!(
"<iq type='set' to='{target}'><pubsub xmlns='http://jabber.org/protocol/pubsub#owner'><configure node='n'><x xmlns='jabber:x:data' type='submit'><field var='pubsub#deliver_notifications'><value>1</value></field><field var='pubsub#notify_retract'><value>0</value></field></x></configure></pubsub></iq>"
);
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!(
"<iq type='get' to='{target}'><pubsub xmlns='http://jabber.org/protocol/pubsub#owner'><configure node='n'/></pubsub></iq>"
);
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"));
}
}