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

369 lines
11 KiB
Rust

use roxmltree::Document;
use rust_xmpp::{
account_modules, preauth,
storage::{self, LuaValue as V},
};
use tempfile::tempdir;
fn account(root: &std::path::Path, user: &str) {
let mut data = V::table();
data.set("password", "test".into());
storage::save(root, "example.org", Some(user), "accounts", &data).unwrap();
}
#[test]
fn invitations_expire_are_single_use_and_owner_scoped() {
let d = tempdir().unwrap();
account(d.path(), "alice");
account(d.path(), "other");
let invite =
preauth::create_invite(d.path(), "example.org", "alice@example.org", None, 10, 100)
.unwrap();
assert_eq!(invite.token.len(), 32);
assert!(
!preauth::consume(
d.path(),
"example.org",
"other@example.org",
"bob@remote.test",
&invite.token,
105
)
.unwrap()
);
assert!(
preauth::consume(
d.path(),
"example.org",
"alice@example.org",
"bob@remote.test/mobile",
&invite.token,
109
)
.unwrap()
);
assert!(
!preauth::consume(
d.path(),
"example.org",
"alice@example.org",
"bob@remote.test",
&invite.token,
109
)
.unwrap()
);
let expired =
preauth::create_invite(d.path(), "example.org", "alice@example.org", None, 10, 100)
.unwrap();
assert!(
!preauth::consume(
d.path(),
"example.org",
"alice@example.org",
"bob@remote.test",
&expired.token,
110
)
.unwrap()
);
}
#[test]
fn identity_scope_does_not_burn_on_wrong_sender_or_leak_to_prosody() {
let d = tempdir().unwrap();
account(d.path(), "alice");
let invite = preauth::create_invite(
d.path(),
"example.org",
"alice@example.org",
Some("Bob@remote.test"),
10,
100,
)
.unwrap();
assert!(
storage::load(d.path(), "example.org", Some("alice"), "invite_token")
.unwrap()
.is_none()
);
assert!(
!preauth::consume(
d.path(),
"example.org",
"alice@example.org",
"eve@remote.test",
&invite.token,
101
)
.unwrap()
);
assert!(
preauth::consume(
d.path(),
"example.org",
"alice@example.org",
"bob@remote.test",
&invite.token,
101
)
.unwrap()
);
}
#[test]
fn prosody_numeric_expiry_is_consumed_and_other_entries_preserved() {
let d = tempdir().unwrap();
account(d.path(), "alice");
let mut tokens = V::table();
tokens.set("legacy", V::Number(200.0));
tokens.set("unrelated", V::Integer(300));
storage::save(
d.path(),
"example.org",
Some("alice"),
"invite_token",
&tokens,
)
.unwrap();
assert!(
preauth::consume(
d.path(),
"example.org",
"alice@example.org",
"bob@remote.test",
"legacy",
100
)
.unwrap()
);
let data = storage::load(d.path(), "example.org", Some("alice"), "invite_token")
.unwrap()
.unwrap();
assert!(data.get("legacy").is_none());
assert_eq!(data.get("unrelated"), Some(&V::Integer(300)));
}
#[test]
fn duplicate_or_wrong_namespace_tokens_do_not_qualify() {
for xml in [
"<presence type='subscribe'><preauth xmlns='wrong' token='x'/></presence>",
"<presence type='subscribed'><preauth xmlns='urn:xmpp:pars:0' token='x'/></presence>",
"<presence type='subscribe'><preauth xmlns='urn:xmpp:pars:0' token='x'/><preauth xmlns='urn:xmpp:pars:0' token='y'/></presence>",
] {
let doc = Document::parse(xml).unwrap();
assert!(preauth::token_from_presence(doc.root_element()).is_none());
}
}
#[test]
fn standard_adhoc_command_returns_contact_uri() {
let d = tempdir().unwrap();
account(d.path(), "alice");
let doc = Document::parse("<iq type='set' id='i' to='example.org'><command xmlns='http://jabber.org/protocol/commands' node='urn:xmpp:invite#invite' action='execute'/></iq>").unwrap();
let result = preauth::handle_iq(
d.path(),
"example.org",
"alice@example.org/phone",
doc.root_element(),
)
.unwrap()
.unwrap();
let answer = Document::parse(&result.replies[0]).unwrap();
assert_eq!(answer.root_element().attribute("type"), Some("result"));
assert!(result.replies[0].contains("xmpp:alice@example.org?roster;preauth="));
assert!(result.replies[0].contains("var='expire'"));
assert!(!result.replies[0].contains("ibr=y"));
}
#[test]
fn unknown_client_token_is_preserved_on_remote_and_pending_delivery() {
let d = tempdir().unwrap();
account(d.path(), "alice");
let doc = Document::parse("<presence xmlns='jabber:client' to='bob@remote.test' type='subscribe'><preauth xmlns='urn:xmpp:pars:0' token='client-token'/></presence>").unwrap();
let result = account_modules::subscription(
d.path(),
"example.org",
"alice@example.org/phone",
doc.root_element(),
)
.unwrap()
.unwrap();
let forwarded = result
.deliveries
.iter()
.find(|(jid, _)| jid == "bob@remote.test")
.unwrap();
assert!(forwarded.1.contains("client-token"));
let doc = Document::parse("<presence xmlns='jabber:client' to='alice@example.org' type='subscribe'><preauth xmlns='urn:xmpp:pars:0' token='unknown-token'/></presence>").unwrap();
let result = account_modules::inbound_subscription(
d.path(),
"example.org",
"bob@remote.test",
doc.root_element(),
)
.unwrap()
.unwrap();
assert!(
result
.deliveries
.iter()
.any(|(jid, xml)| jid == "alice@example.org" && xml.contains("unknown-token"))
);
let pending = account_modules::pending_subscriptions(d.path(), "example.org", "alice").unwrap();
assert!(pending.iter().any(|xml| xml.contains("unknown-token")));
}
#[test]
fn valid_token_autoapproves_and_sends_reciprocal_request() {
let d = tempdir().unwrap();
account(d.path(), "alice");
let invite = preauth::create_invite(
d.path(),
"example.org",
"alice@example.org",
None,
60,
chrono::Utc::now().timestamp(),
)
.unwrap();
let xml = format!(
"<presence xmlns='jabber:client' to='alice@example.org' type='subscribe'><preauth xmlns='urn:xmpp:pars:0' token='{}'/></presence>",
invite.token
);
let doc = Document::parse(&xml).unwrap();
let result = account_modules::inbound_subscription(
d.path(),
"example.org",
"bob@remote.test",
doc.root_element(),
)
.unwrap()
.unwrap();
for kind in ["subscribed", "subscribe"] {
assert!(result.deliveries.iter().any(|(jid, xml)| {
jid == "bob@remote.test"
&& Document::parse(xml)
.unwrap()
.root_element()
.attribute("type")
== Some(kind)
}));
}
let roster = storage::load(d.path(), "example.org", Some("alice"), "roster")
.unwrap()
.unwrap();
let entry = roster.get("bob@remote.test").unwrap();
assert_eq!(entry.get("subscription").and_then(V::as_str), Some("from"));
assert_eq!(entry.get("ask").and_then(V::as_str), Some("subscribe"));
assert!(
account_modules::pending_subscriptions(d.path(), "example.org", "alice")
.unwrap()
.is_empty()
);
}
#[test]
fn concurrent_redemption_has_exactly_one_winner() {
let d = tempdir().unwrap();
account(d.path(), "alice");
let invite =
preauth::create_invite(d.path(), "example.org", "alice@example.org", None, 60, 100)
.unwrap();
let winners = std::thread::scope(|scope| {
let tasks: Vec<_> = (0..8)
.map(|_| {
let token = &invite.token;
let root = d.path();
scope.spawn(move || {
preauth::consume(
root,
"example.org",
"alice@example.org",
"bob@remote.test",
token,
101,
)
.unwrap()
})
})
.collect();
tasks
.into_iter()
.map(|t| t.join().unwrap())
.filter(|accepted| *accepted)
.count()
});
assert_eq!(winners, 1);
}
#[test]
fn local_invitation_updates_both_rosters_without_granting_reverse_subscription() {
let d = tempdir().unwrap();
account(d.path(), "alice");
account(d.path(), "bob");
let invite = preauth::create_invite(
d.path(),
"example.org",
"alice@example.org",
None,
60,
chrono::Utc::now().timestamp(),
)
.unwrap();
let xml = format!(
"<presence xmlns='jabber:client' to='alice@example.org' type='subscribe'><preauth xmlns='urn:xmpp:pars:0' token='{}'/></presence>",
invite.token
);
let doc = Document::parse(&xml).unwrap();
let result = account_modules::subscription(
d.path(),
"example.org",
"bob@example.org/mobile",
doc.root_element(),
)
.unwrap()
.unwrap();
assert!(result.deliveries.iter().any(|(jid, xml)| {
jid == "bob@example.org"
&& Document::parse(xml)
.unwrap()
.root_element()
.attribute("type")
== Some("subscribe")
}));
for (user, contact, subscription) in [
("alice", "bob@example.org", "from"),
("bob", "alice@example.org", "to"),
] {
let data = storage::load(d.path(), "example.org", Some(user), "roster")
.unwrap()
.unwrap();
assert_eq!(
data.get(contact)
.unwrap()
.get("subscription")
.and_then(V::as_str),
Some(subscription)
);
}
}
#[test]
fn unknown_tokens_do_not_create_invite_stores_and_disabled_accounts_cannot_issue() {
let d = tempdir().unwrap();
account(d.path(), "alice");
assert!(
!preauth::consume(
d.path(),
"example.org",
"alice@example.org",
"bob@remote.test",
"unknown",
100
)
.unwrap()
);
assert!(!storage::path(d.path(), "example.org", Some("alice"), "invite_token").exists());
assert!(!storage::path(d.path(), "example.org", Some("alice"), "preauth_scoped").exists());
storage::update(d.path(), "example.org", Some("alice"), "accounts", |v| {
v.set("disabled", V::Bool(true));
Ok(())
})
.unwrap();
assert!(
preauth::create_invite(d.path(), "example.org", "alice@example.org", None, 10, 100)
.is_err()
);
}