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")); } }