use rust_xmpp::{auth, config::Config, push::Push, storage}; fn config() -> Config { serde_json::from_value(serde_json::json!({"global":{"modules_enabled":["cloud_notify"],"push_notification_with_body":false,"push_notification_with_sender":true,"push_notification_important_body":"New message"},"hosts":{"example.test":{"options":{}}}})).unwrap() } #[test] fn registrations_notifications_and_authenticated_errors() { let dir = tempfile::tempdir().unwrap(); auth::create(dir.path(), "example.test", "alice", "secret", "SHA-256").unwrap(); let mut push = Push::default(); let enable=roxmltree::Document::parse("token").unwrap(); assert!( push.handle( dir.path(), "example.test", "alice@example.test/mobile", enable.root_element(), 50 ) .unwrap() .unwrap() .replies[0] .contains("type='result'") ); let data = storage::load(dir.path(), "example.test", Some("alice"), "cloud_notify") .unwrap() .unwrap(); assert!(data.get("push.testprivate text").unwrap(); for (i, sender) in ["forged.test", "push.test", "push.test"].iter().enumerate() { let notifications = push .notify( dir.path(), "example.test", "alice", message.root_element(), &config(), ) .unwrap(); assert_eq!(notifications.len(), 1); let xml = ¬ifications[0].1; assert!(!xml.contains("private text")); assert!(xml.contains("New message") && xml.contains("token")); let d = roxmltree::Document::parse(xml).unwrap(); let id = d.root_element().attribute("id").unwrap(); let reply = format!(""); let d = roxmltree::Document::parse(&reply).unwrap(); assert_eq!( push.result(dir.path(), "example.test", d.root_element(), 2) .unwrap(), i > 0 ); } assert!( storage::load(dir.path(), "example.test", Some("alice"), "cloud_notify") .unwrap() .is_none() ); } #[test] fn foreign_account_cannot_register() { let dir = tempfile::tempdir().unwrap(); let mut push = Push::default(); let d=roxmltree::Document::parse("").unwrap(); let r = push .handle( dir.path(), "example.test", "alice@example.test/r", d.root_element(), 50, ) .unwrap() .unwrap(); assert!(r.replies[0].contains("forbidden")); } #[test] fn stable_client_registration_and_targeted_notification() { use rust_xmpp::push::client_device; let dir = tempfile::tempdir().unwrap(); let mut push = Push::default(); for (node, resource, client) in [ ("phone", "old", Some("stable-phone")), ("tablet", "tablet", None), ] { let xml = format!( "" ); let document = roxmltree::Document::parse(&xml).unwrap(); push.handle_with_client( dir.path(), "example.test", &format!("alice@example.test/{resource}"), document.root_element(), 50, client, ) .unwrap(); } let table = storage::load(dir.path(), "example.test", Some("alice"), "cloud_notify") .unwrap() .unwrap(); let phone = table.get("push.testhello").unwrap(); let sent = push .notify_for_identifier( dir.path(), "example.test", "alice", message.root_element(), &config(), Some("push.test", ) .unwrap(); push.handle( dir.path(), "example.test", "alice@example.test/r", enable.root_element(), 5, ) .unwrap(); let messages = vec![ "not important".into(), "".into(), "important".into(), "second important".into(), ]; let result = push .notify_queue( dir.path(), "example.test", "alice", &messages, &config(), "push.testTopictopic text", false, ), ("", true), ( "", false, ), ( "", true, ), ( "outgoing", false, ), ] { let doc = roxmltree::Document::parse(xml).unwrap(); assert_eq!( rust_xmpp::push::is_important(doc.root_element()), expected, "{xml}" ); } } #[test] fn reenable_cancels_old_errors_and_failure_counts_are_per_account() { let dir = tempfile::tempdir().unwrap(); let mut push = Push::default(); let enable = roxmltree::Document::parse( "", ) .unwrap(); for user in ["alice", "bob"] { push.handle( dir.path(), "example.test", &format!("{user}@example.test/r"), enable.root_element(), 5, ) .unwrap(); } let message = roxmltree::Document::parse("hello").unwrap(); let fail = |push: &mut Push, user: &str| { let out = push .notify( dir.path(), "example.test", user, message.root_element(), &config(), ) .unwrap(); let doc = roxmltree::Document::parse(&out[0].1).unwrap(); let id = doc.root_element().attribute("id").unwrap(); let error = format!(""); let doc = roxmltree::Document::parse(&error).unwrap(); push.result(dir.path(), "example.test", doc.root_element(), 2) .unwrap(); }; fail(&mut push, "alice"); fail(&mut push, "bob"); assert!(push.take_removed().is_empty()); fail(&mut push, "alice"); assert_eq!( push.take_removed(), vec![("alice".into(), "push.test"); push.handle( dir.path(), "example.test", "bob@example.test/r", enable.root_element(), 5, ) .unwrap(); let doc = roxmltree::Document::parse(&error).unwrap(); assert!( !push .result(dir.path(), "example.test", doc.root_element(), 1) .unwrap() ); assert!(push.take_removed().is_empty()); } #[test] fn push_summary_never_copies_foreign_namespace_body() { let dir = tempfile::tempdir().unwrap(); let mut push = Push::default(); let enable = roxmltree::Document::parse( "", ) .unwrap(); push.handle( dir.path(), "example.test", "alice@example.test/r", enable.root_element(), 5, ) .unwrap(); let mut settings = config(); settings.global.insert( "push_notification_with_body".into(), serde_json::json!(true), ); for (message, expected) in [ ( "extension-secret", None, ), ( "nested-secret", None, ), ( "extension-secretvisible text", Some("visible text"), ), ( "nested-secret", None, ), ( "visible text", Some("visible text"), ), ( "extension-secretvisible text", Some("visible text"), ), ] { let doc = roxmltree::Document::parse(message).unwrap(); let notifications = push .notify( dir.path(), "example.test", "alice", doc.root_element(), &settings, ) .unwrap(); assert_eq!(notifications.len(), 1); let xml = ¬ifications[0].1; assert!( !xml.contains("extension-secret") && !xml.contains("nested-secret"), "foreign body leaked: {xml}" ); let result = roxmltree::Document::parse(xml).unwrap(); let summary = result.descendants().find(|n| { n.has_tag_name(("jabber:x:data", "field")) && n.attribute("var") == Some("last-message-body") }); let body = summary .and_then(|n| { n.children() .find(|n| n.has_tag_name(("jabber:x:data", "value"))) }) .and_then(|n| n.text()); assert_eq!(body, expected); assert_eq!( rust_xmpp::push::is_important(doc.root_element()), expected.is_some() ); } } #[test] #[ignore = "requires PROSODY_REFERENCE_SOURCE and PROSODY_REFERENCE_LUA"] fn native_push_body_lookup_excludes_extensions() { use std::io::Write; use std::process::{Command, Stdio}; let source = std::env::var("PROSODY_REFERENCE_SOURCE").unwrap(); let mut lua = Command::new(std::env::var("PROSODY_REFERENCE_LUA").unwrap()) .args(["-", &source]) .stdin(Stdio::piped()) .stdout(Stdio::piped()) .stderr(Stdio::piped()) .spawn() .unwrap(); lua.stdin.take().unwrap().write_all(br#" local source=assert(arg[1]); package.path=source..'/?.lua;'..package.path; package.cpath=source..'/?.so;'..package.cpath; dofile(source..'/loader.lua'); local st=require 'prosody.util.stanza'; -- mod_cloud_notify uses this real helper for notification bodies and importance. local foreign=st.message({xmlns='jabber:client'}):tag('body',{xmlns='urn:private'}):text('extension-secret'):reset(); assert(foreign:get_child_text('body') == nil); local mixed=foreign:tag('body'):text('visible text'):reset(); assert(mixed:get_child_text('body') == 'visible text'); local nested=st.message({xmlns='jabber:client'}):tag('private',{xmlns='urn:private'}):tag('body'):text('nested-secret'):reset(); assert(nested:get_child_text('body') == nil); local embedded=st.message({xmlns='jabber:client'}):tag('body'):tag('private',{xmlns='urn:private'}):text('nested-secret'):reset(); assert(embedded:get_child_text('body') == nil); "#).unwrap(); let output = lua.wait_with_output().unwrap(); assert!( output.status.success(), "{}", String::from_utf8_lossy(&output.stderr) ); }