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", "rredo"] {
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", "rredo"] {
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());
}