use rust_xmpp::{ account_modules as a, storage::{self, LuaValue as V}, }; fn iq(root: &std::path::Path, s: &str) -> rust_xmpp::protocol::ModuleResult { let d = roxmltree::Document::parse(s).unwrap(); a::handle( root, "example.test", "alice@example.test/r", d.root_element(), ) .unwrap() .unwrap() } #[test] fn private_and_vcard_authorization() { let t = tempfile::tempdir().unwrap(); let r = iq( t.path(), "secret", ); assert!(r.replies[0].contains("forbidden")); assert!(!t.path().join("example%2etest").exists()); let r = iq( t.path(), "secret", ); assert!(r.replies[0].contains("result")); let r = iq( t.path(), "", ); assert!(r.replies[0].contains("secret")); let r = iq( t.path(), "Alice", ); assert!(r.replies[0].contains("forbidden")); } #[test] fn roster_preserves_subscription_and_metadata() { let t = tempfile::tempdir().unwrap(); let v=storage::parse(b"return { [false]={version=9;pending={['foo@example.test']=true}};['bob@example.test']={subscription='both';ask='subscribe';groups={Old=true}}}").unwrap(); storage::save(t.path(), "example.test", Some("alice"), "roster", &v).unwrap(); let r = iq( t.path(), "New", ); assert_eq!(r.deliveries.len(), 1); let v = storage::load(t.path(), "example.test", Some("alice"), "roster") .unwrap() .unwrap(); assert_eq!( v.get("bob@example.test") .unwrap() .get("subscription") .and_then(V::as_str), Some("both") ); assert_eq!( v.get_key(&V::Bool(false)).unwrap().get("version"), Some(&V::Integer(10)) ); assert!(v.get_key(&V::Bool(false)).unwrap().get("pending").is_some()); let r = iq( t.path(), "", ); assert!(!r.replies[0].contains("", ); assert!(a::is_blocked(t.path(), "example.test", "alice", "bob@example.test/device").unwrap()); assert!(!a::is_blocked(t.path(), "example.test", "alice", "carol@example.test").unwrap()); iq( t.path(), "", ); assert!(!a::is_blocked(t.path(), "example.test", "alice", "bob@example.test").unwrap()); } #[test] fn subscription_handshake_and_cancel() { let t = tempfile::tempdir().unwrap(); for u in ["alice", "bob"] { let mut v = V::table(); v.set("test", V::Bool(true)); storage::save(t.path(), "example.test", Some(u), "accounts", &v).unwrap(); } let p = roxmltree::Document::parse("").unwrap(); a::subscription( t.path(), "example.test", "alice@example.test/r", p.root_element(), ) .unwrap(); assert_eq!( a::pending_subscriptions(t.path(), "example.test", "bob") .unwrap() .len(), 1 ); let p = roxmltree::Document::parse("") .unwrap(); a::subscription( t.path(), "example.test", "bob@example.test/r", p.root_element(), ) .unwrap(); assert_eq!( a::subscriptions(t.path(), "example.test", "alice").unwrap(), vec!["bob@example.test"] ); assert_eq!( a::subscribers(t.path(), "example.test", "bob").unwrap(), vec!["alice@example.test"] ); assert!( a::pending_subscriptions(t.path(), "example.test", "bob") .unwrap() .is_empty() ); let p = roxmltree::Document::parse("").unwrap(); a::subscription( t.path(), "example.test", "alice@example.test/r", p.root_element(), ) .unwrap(); assert!( a::subscriptions(t.path(), "example.test", "alice") .unwrap() .is_empty() ); assert!( a::subscribers(t.path(), "example.test", "bob") .unwrap() .is_empty() ); } #[test] fn reads_timestamp_blocklist_and_keeps_metadata() { let t = tempfile::tempdir().unwrap(); let v = storage::parse(b"return {[false]={created=42};['bob@example.test']=1729000000}").unwrap(); storage::save(t.path(), "example.test", Some("alice"), "blocklist", &v).unwrap(); assert!(a::is_blocked(t.path(), "example.test", "alice", "bob@example.test/r").unwrap()); let r = iq( t.path(), "", ); assert!(r.replies[0].contains("bob@example.test")); iq( t.path(), "", ); let v = storage::load(t.path(), "example.test", Some("alice"), "blocklist") .unwrap() .unwrap(); assert_eq!( v.get_key(&V::Bool(false)).unwrap().get("created"), Some(&V::Integer(42)) ); } fn legacy(root: &std::path::Path, jid: &str, s: &str) -> rust_xmpp::protocol::ModuleResult { let user = jid.split('@').next().unwrap(); if storage::load(root, "example.test", Some(user), "accounts") .unwrap() .is_none() { let mut v = V::table(); v.set("test", V::Bool(true)); storage::save(root, "example.test", Some(user), "accounts", &v).unwrap(); } let d = roxmltree::Document::parse(s).unwrap(); a::handle_legacy(root, "example.test", jid, d.root_element()) .unwrap() .unwrap() } #[test] fn vcard_pep_conversion_and_avatar() { let t = tempfile::tempdir().unwrap(); let r = legacy( t.path(), "alice@example.test/r", "AliceTesta@example.testimage/pngaGVsbG8=", ); assert!(r.replies[0].contains("result")); let r = legacy( t.path(), "bob@example.test/r", "", ); assert!(r.replies[0].contains("Alice")); assert!(r.replies[0].contains("aGVsbG8=")); let r = legacy( t.path(), "alice@example.test/r", "", ); assert!(r.replies[0].contains("surname")); let p = roxmltree::Document::parse("").unwrap(); let s = a::avatar_presence( t.path(), "example.test", "alice@example.test/r", p.root_element(), ) .unwrap(); assert!(s.contains("aaf4c61ddcc5e8a2dabede0f3b482cd9aea9434d")); } #[test] fn bookmarks_private_pep_bidirectional_and_private() { let t = tempfile::tempdir().unwrap(); let r = legacy( t.path(), "alice@example.test/r", "Alicesecret", ); assert!(r.replies[0].contains("result")); let r = legacy( t.path(), "alice@example.test/r", "", ); assert!(r.replies[0].contains("secret")); let d=roxmltree::Document::parse("").unwrap(); let r = rust_xmpp::pubsub::handle( t.path(), "example.test", "bob@example.test/r", d.root_element(), ) .unwrap() .unwrap(); assert!(r.replies[0].contains("forbidden")); legacy( t.path(), "alice@example.test/r", "", ); let r = legacy( t.path(), "alice@example.test/r", "", ); assert!(!r.replies[0].contains("room@")); } #[test] fn inbound_subscription_requires_pending_to_grant_to() { let t = tempfile::tempdir().unwrap(); let mut v = V::table(); v.set("test", V::Bool(true)); storage::save(t.path(), "example.test", Some("alice"), "accounts", &v).unwrap(); let d = roxmltree::Document::parse("") .unwrap(); a::inbound_subscription( t.path(), "example.test", "evil@other.test", d.root_element(), ) .unwrap(); assert!( a::subscriptions(t.path(), "example.test", "alice") .unwrap() .is_empty() ); } #[test] fn legacy_migration_clears_source_after_success() { let t = tempfile::tempdir().unwrap(); let mut account = V::table(); account.set("test", V::Bool(true)); storage::save( t.path(), "example.test", Some("alice"), "accounts", &account, ) .unwrap(); let d = roxmltree::Document::parse("Old Alice").unwrap(); storage::save( t.path(), "example.test", Some("alice"), "vcard", &storage::stanza_value(d.root_element()), ) .unwrap(); let d=roxmltree::Document::parse("").unwrap(); let mut private = V::table(); private.set( "storage:storage:bookmarks", storage::stanza_value(d.root_element()), ); private.set("unrelated", "keep".into()); storage::save(t.path(), "example.test", Some("alice"), "private", &private).unwrap(); a::legacy_on_bind(t.path(), "example.test", "alice@example.test/r").unwrap(); assert!( storage::load(t.path(), "example.test", Some("alice"), "vcard") .unwrap() .is_none() ); let v = storage::load(t.path(), "example.test", Some("alice"), "private") .unwrap() .unwrap(); assert!(v.get("storage:storage:bookmarks").is_none()); assert_eq!(v.get("unrelated").and_then(V::as_str), Some("keep")); a::legacy_on_bind(t.path(), "example.test", "alice@example.test/r").unwrap(); let r = legacy( t.path(), "alice@example.test/r", "", ); assert!(r.replies[0].contains("Old Alice")); } #[test] fn domain_roster_and_block_jid_normalization() { let t = tempfile::tempdir().unwrap(); let r = iq( t.path(), "", ); assert!(r.replies[0].contains("result")); let d = storage::load(t.path(), "example.test", Some("alice"), "roster") .unwrap() .unwrap(); assert!(d.get("transport.example.test").is_some()); let r = iq( t.path(), "", ); assert!(a::is_blocked(t.path(), "example.test", "alice", "bob@example.test/Phone").unwrap()); assert!(r.deliveries[0].1.contains("bob@example.test/Phone")); } #[test] fn purge_account_exact_files_preserves_others_and_extras() { let t = tempfile::tempdir().unwrap(); let root = t.path(); for store in ["accounts", "roster", "pep_aabbcc", "custom_store"] { for user in ["alice", "bob"] { for ext in ["dat", "list", "lidx", "rappend"] { let p = storage::path(root, "example.test", Some(user), store).with_extension(ext); std::fs::create_dir_all(p.parent().unwrap()).unwrap(); std::fs::write(&p, b"fixture").unwrap(); } } } let extra = storage::path(root, "example.test", Some("alice"), "custom_store").with_extension("backup"); std::fs::write(&extra, b"keep").unwrap(); let hostwide = storage::path(root, "example.test", None, "hostwide"); std::fs::write(&hostwide, b"keep").unwrap(); a::remove_account(root, "example.test", "alice").unwrap(); for store in ["accounts", "roster", "pep_aabbcc", "custom_store"] { for ext in ["dat", "list", "lidx", "rappend"] { assert!( !storage::path(root, "example.test", Some("alice"), store) .with_extension(ext) .exists() ); assert!( storage::path(root, "example.test", Some("bob"), store) .with_extension(ext) .exists() ); } } assert!(extra.exists()); assert!(hostwide.exists()); a::remove_account(root, "example.test", "alice").unwrap(); } #[test] fn privacy_migration_only_default_deny_jids() { let t = tempfile::tempdir().unwrap(); let mut v = V::table(); v.set("test", V::Bool(true)); storage::save(t.path(), "example.test", Some("alice"), "accounts", &v).unwrap(); let old=storage::parse(b"return {default='main';lists={main={items={{type='jid';action='deny';value='BOB@EXAMPLE.TEST'};{type='jid';action='allow';value='carol@example.test'};{type='group';action='deny';value='Friends'}}};other={items={{type='jid';action='deny';value='other@example.test'}}}}}").unwrap(); storage::save(t.path(), "example.test", Some("alice"), "privacy", &old).unwrap(); assert!( a::load_blocklist(t.path(), "example.test", "alice", false) .unwrap() .entries() .is_empty() ); assert!(a::is_blocked(t.path(), "example.test", "alice", "bob@example.test/r").unwrap()); assert!(!a::is_blocked(t.path(), "example.test", "alice", "carol@example.test").unwrap()); assert!(!a::is_blocked(t.path(), "example.test", "alice", "other@example.test").unwrap()); let b = a::load_blocklist(t.path(), "example.test", "alice", true).unwrap(); assert_eq!( b.get_key(&V::Bool(false)) .unwrap() .get("migrated") .and_then(V::as_str), Some("privacy") ); assert!( storage::load(t.path(), "example.test", Some("alice"), "privacy") .unwrap() .is_some() ); } #[cfg(unix)] #[test] fn account_purge_refuses_symlink_store_without_touching_other_files() { let t = tempfile::tempdir().unwrap(); let other = tempfile::tempdir().unwrap(); let mut v = V::table(); v.set("test", V::Bool(true)); storage::save(t.path(), "example.test", Some("alice"), "accounts", &v).unwrap(); std::fs::write(other.path().join("alice.dat"), b"external").unwrap(); std::os::unix::fs::symlink(other.path(), t.path().join("example%2etest/linked")).unwrap(); assert!(a::remove_account(t.path(), "example.test", "alice").is_err()); assert!( storage::load(t.path(), "example.test", Some("alice"), "accounts") .unwrap() .is_some() ); assert!(other.path().join("alice.dat").exists()); } #[test] fn legacy_bookmarks_respect_host_limit_and_read_over_256() { let t = tempfile::tempdir().unwrap(); a::set_legacy_pep_limit(t.path(), "example.test", 500).unwrap(); legacy( t.path(), "alice@example.test/r", "", ); let meta = storage::load(t.path(), "example.test", Some("alice"), "pep") .unwrap() .unwrap(); assert_eq!( meta.get("urn:xmpp:bookmarks:1") .unwrap() .get("config") .unwrap() .get("max_items"), Some(&V::Integer(500)) ); let store = "pep_urn:xmpp:bookmarks:1"; let entries = (0..300) .map(|i| rust_xmpp::archive::Entry { id: format!("room{i}@conference.example.test"), when: 1700000000.0 + i as f64, peer: "alice@example.test".into(), xml: "".into(), }) .collect::>(); rust_xmpp::archive::save_store(t.path(), "example.test", Some("alice"), store, &entries) .unwrap(); let s = a::legacy_bookmarks(t.path(), "example.test", "alice@example.test/r").unwrap(); assert_eq!(s.matches("", "", "", ] { assert!(iq(t.path(), xml).replies[0].contains("bad-request")); } assert!( storage::load(t.path(), "example.test", Some("alice"), "roster") .unwrap() .is_none() ); let r = iq( t.path(), "", ); assert!(r.replies[0].contains("item-not-found")); assert!(r.deliveries.is_empty()); } #[test] fn broken_roster_is_not_overwritten() { let t = tempfile::tempdir().unwrap(); let p = storage::path(t.path(), "example.test", Some("alice"), "roster"); std::fs::create_dir_all(p.parent().unwrap()).unwrap(); let data=b"return {[false]={broken='read-failed';version=7};['bob@example.test']={subscription='both';groups={}}}"; std::fs::write(&p, data).unwrap(); let r = iq( t.path(), "", ); assert!(r.replies[0].contains("internal-server-error")); assert_eq!(std::fs::read(&p).unwrap(), data); std::fs::write(&p, b"this is corrupt").unwrap(); let r = iq( t.path(), "", ); assert!(r.replies[0].contains("internal-server-error")); assert_eq!(std::fs::read(&p).unwrap(), b"this is corrupt"); } #[test] fn roster_empty_name_and_foreign_group_ignored() { let t = tempfile::tempdir().unwrap(); let r = iq( t.path(), "", ); assert!(r.replies[0].contains("ver='1'")); iq( t.path(), "HiddenFriends", ); let v = storage::load(t.path(), "example.test", Some("alice"), "roster") .unwrap() .unwrap(); let item = v.get("bob@example.test").unwrap(); assert!(item.get("name").is_none()); let groups = item.get("groups").unwrap(); assert_eq!(groups.entries().len(), 1); assert!(groups.get("Friends").is_some()); } #[test] fn creation_and_purge_are_serialized() { use rust_xmpp::auth; use std::sync::{Arc, Barrier}; for _ in 0..5 { let t = tempfile::tempdir().unwrap(); auth::create(t.path(), "example.test", "alice", "old", "SHA-256").unwrap(); let mut v = V::table(); v.set("old", "private-data".into()); storage::save(t.path(), "example.test", Some("alice"), "private", &v).unwrap(); let barrier = Arc::new(Barrier::new(2)); std::thread::scope(|scope| { let b = barrier.clone(); let p = t.path(); let new = scope.spawn(move || { b.wait(); auth::create(p, "example.test", "alice", "new", "SHA-256") }); barrier.wait(); a::remove_account(t.path(), "example.test", "alice").unwrap(); let created = new.join().unwrap(); assert!( storage::load(t.path(), "example.test", Some("alice"), "private") .unwrap() .is_none() ); if created.is_ok() { assert!(auth::verify(t.path(), "example.test", "alice", "new", "SHA-256").unwrap()); } }); } } #[test] fn password_change_after_purge_never_recreates_account() { use rust_xmpp::auth; let t = tempfile::tempdir().unwrap(); auth::create(t.path(), "example.test", "alice", "old", "SHA-256").unwrap(); a::remove_account(t.path(), "example.test", "alice").unwrap(); assert!(auth::set_password(t.path(), "example.test", "alice", "new", "SHA-256").is_err()); assert!( storage::load(t.path(), "example.test", Some("alice"), "accounts") .unwrap() .is_none() ); } #[test] fn malformed_legacy_vcard_and_private_bookmarks_rejected() { let t = tempfile::tempdir().unwrap(); let r = legacy( t.path(), "alice@example.test/r", "Bad", ); assert!(r.replies[0].contains("bad-request")); assert!( storage::load(t.path(), "example.test", Some("alice"), "pep") .unwrap() .is_none() ); let r = legacy( t.path(), "alice@example.test/r", "", ); assert!(r.replies[0].contains("bad-request")); let r = iq( t.path(), "", ); assert!(r.replies[0].contains("bad-request")); assert!(!a::is_blocked(t.path(), "example.test", "alice", "bob@example.test").unwrap()); }