initial commit
This commit is contained in:
commit
c260d7bab8
235 files changed
+49786
No files matched your search
@@ -0,0 +1,582 @@
|
||||
use rust_xmpp::federation::Federation;
|
||||
use std::{fs, io::BufReader, path::Path, process::Command};
|
||||
use tokio::{net::TcpListener, sync::mpsc};
|
||||
use tokio_rustls::rustls::{
|
||||
RootCertStore,
|
||||
pki_types::{CertificateDer, PrivateKeyDer},
|
||||
};
|
||||
fn openssl(dir: &Path, args: &[&str]) {
|
||||
let out = Command::new("openssl")
|
||||
.current_dir(dir)
|
||||
.args(args)
|
||||
.output()
|
||||
.unwrap();
|
||||
assert!(
|
||||
out.status.success(),
|
||||
"{}",
|
||||
String::from_utf8_lossy(&out.stderr)
|
||||
);
|
||||
}
|
||||
fn fixture(dir: &Path) {
|
||||
openssl(
|
||||
dir,
|
||||
&[
|
||||
"req",
|
||||
"-x509",
|
||||
"-newkey",
|
||||
"rsa:2048",
|
||||
"-nodes",
|
||||
"-keyout",
|
||||
"ca.key",
|
||||
"-out",
|
||||
"ca.pem",
|
||||
"-days",
|
||||
"1",
|
||||
"-subj",
|
||||
"/CN=Federation Test CA",
|
||||
"-addext",
|
||||
"basicConstraints=critical,CA:TRUE",
|
||||
],
|
||||
);
|
||||
for domain in ["alpha.test", "beta.test"] {
|
||||
let key = format!("{domain}.key");
|
||||
let csr = format!("{domain}.csr");
|
||||
let cert = format!("{domain}.pem");
|
||||
let ext = format!("{domain}.ext");
|
||||
fs::write(dir.join(&ext),format!("basicConstraints=critical,CA:FALSE\nkeyUsage=critical,digitalSignature,keyEncipherment\nextendedKeyUsage=serverAuth\nsubjectAltName=DNS:{domain}\n")).unwrap();
|
||||
openssl(
|
||||
dir,
|
||||
&[
|
||||
"req",
|
||||
"-newkey",
|
||||
"rsa:2048",
|
||||
"-nodes",
|
||||
"-keyout",
|
||||
&key,
|
||||
"-out",
|
||||
&csr,
|
||||
"-subj",
|
||||
&format!("/CN={domain}"),
|
||||
],
|
||||
);
|
||||
openssl(
|
||||
dir,
|
||||
&[
|
||||
"x509",
|
||||
"-req",
|
||||
"-in",
|
||||
&csr,
|
||||
"-CA",
|
||||
"ca.pem",
|
||||
"-CAkey",
|
||||
"ca.key",
|
||||
"-CAcreateserial",
|
||||
"-out",
|
||||
&cert,
|
||||
"-days",
|
||||
"1",
|
||||
"-extfile",
|
||||
&ext,
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
fn material(
|
||||
dir: &Path,
|
||||
domain: &str,
|
||||
) -> (
|
||||
Vec<CertificateDer<'static>>,
|
||||
PrivateKeyDer<'static>,
|
||||
RootCertStore,
|
||||
) {
|
||||
let certs = rustls_pemfile::certs(&mut BufReader::new(
|
||||
fs::File::open(dir.join(format!("{domain}.pem"))).unwrap(),
|
||||
))
|
||||
.collect::<Result<Vec<_>, _>>()
|
||||
.unwrap();
|
||||
let key = rustls_pemfile::private_key(&mut BufReader::new(
|
||||
fs::File::open(dir.join(format!("{domain}.key"))).unwrap(),
|
||||
))
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
let mut roots = RootCertStore::empty();
|
||||
for cert in rustls_pemfile::certs(&mut BufReader::new(
|
||||
fs::File::open(dir.join("ca.pem")).unwrap(),
|
||||
)) {
|
||||
roots.add(cert.unwrap()).unwrap();
|
||||
}
|
||||
(certs, key, roots)
|
||||
}
|
||||
#[tokio::test(flavor = "multi_thread")]
|
||||
async fn mutual_tls_external_delivery_and_certificate_identity_rejection() {
|
||||
let temp = tempfile::tempdir().unwrap();
|
||||
fixture(temp.path());
|
||||
let (a_cert, a_key, a_roots) = material(temp.path(), "alpha.test");
|
||||
let mut alice = Federation::new("alpha.test".into(), a_cert, a_key, a_roots).unwrap();
|
||||
let (b_cert, b_key, b_roots) = material(temp.path(), "beta.test");
|
||||
let bob = Federation::new("beta.test".into(), b_cert, b_key, b_roots).unwrap();
|
||||
let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
|
||||
let port = listener.local_addr().unwrap().port();
|
||||
alice
|
||||
.set_target("beta.test".into(), "127.0.0.1".into(), port)
|
||||
.unwrap();
|
||||
let (tx, mut rx) = mpsc::channel(8);
|
||||
let task = tokio::spawn(async move {
|
||||
loop {
|
||||
let (tcp, _) = listener.accept().await.unwrap();
|
||||
let bob = bob.clone();
|
||||
let tx = tx.clone();
|
||||
tokio::spawn(async move {
|
||||
let _ = bob.accept(tcp, tx).await;
|
||||
});
|
||||
}
|
||||
});
|
||||
let xml = "<message xmlns='jabber:client' from='alice@alpha.test/r' to='bob@beta.test' type='chat'><body>Hello</body><encrypted xmlns='urn:xmpp:omemo:2'>opaque</encrypted></message>";
|
||||
alice
|
||||
.route("alpha.test", "bob@beta.test", xml)
|
||||
.await
|
||||
.unwrap();
|
||||
let inbound = tokio::time::timeout(std::time::Duration::from_secs(3), rx.recv())
|
||||
.await
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
assert_eq!(inbound.from_domain, "alpha.test");
|
||||
assert!(inbound.xml.contains("jabber:server"));
|
||||
assert!(inbound.xml.contains("urn:xmpp:omemo:2"));
|
||||
// An operator routing override changes only the TCP endpoint: beta's trusted
|
||||
// certificate must never authenticate it as gamma.test.
|
||||
alice
|
||||
.set_target("gamma.test".into(), "127.0.0.1".into(), port)
|
||||
.unwrap();
|
||||
assert!(
|
||||
alice
|
||||
.route(
|
||||
"alpha.test",
|
||||
"bob@gamma.test",
|
||||
"<message from='alice@alpha.test' to='bob@gamma.test'/>",
|
||||
)
|
||||
.await
|
||||
.is_err()
|
||||
);
|
||||
assert!(rx.try_recv().is_err());
|
||||
// Domain matching, target lookup and certificate authentication use the same
|
||||
// case-insensitive DNS identity.
|
||||
alice
|
||||
.route(
|
||||
"ALPHA.TEST",
|
||||
"bob@BETA.TEST",
|
||||
"<message from='alice@ALPHA.TEST/r' to='bob@BETA.TEST' id='mixed-case'/>",
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
let received = tokio::time::timeout(std::time::Duration::from_secs(3), rx.recv())
|
||||
.await
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
assert!(received.xml.contains("mixed-case"));
|
||||
// Identity matching cannot compensate for an untrusted certificate chain.
|
||||
let (certs, key, _) = material(temp.path(), "alpha.test");
|
||||
let mut unrelated_roots = RootCertStore::empty();
|
||||
unrelated_roots.add(certs[0].clone()).unwrap();
|
||||
let mut untrusted = Federation::new("alpha.test".into(), certs, key, unrelated_roots).unwrap();
|
||||
untrusted
|
||||
.set_target("beta.test".into(), "127.0.0.1".into(), port)
|
||||
.unwrap();
|
||||
assert!(
|
||||
untrusted
|
||||
.route("alpha.test", "bob@beta.test", xml)
|
||||
.await
|
||||
.is_err()
|
||||
);
|
||||
assert!(rx.try_recv().is_err());
|
||||
// Owning a valid certificate for alpha.test never authorizes gamma.test.
|
||||
alice.add_local_domain("gamma.test".into()).unwrap();
|
||||
assert!(
|
||||
alice
|
||||
.route(
|
||||
"gamma.test",
|
||||
"bob@beta.test",
|
||||
"<message from='alice@gamma.test' to='bob@beta.test'><body>forged</body></message>"
|
||||
)
|
||||
.await
|
||||
.is_err()
|
||||
);
|
||||
assert!(rx.try_recv().is_err());
|
||||
assert!(
|
||||
alice
|
||||
.route(
|
||||
"alpha.test",
|
||||
"bob@beta.test",
|
||||
"<message from='admin@gamma.test' to='bob@beta.test'/>"
|
||||
)
|
||||
.await
|
||||
.is_err()
|
||||
);
|
||||
// Source-domain authentication cannot be reused to deliver to a third domain.
|
||||
assert!(
|
||||
alice
|
||||
.route(
|
||||
"alpha.test",
|
||||
"bob@beta.test",
|
||||
"<message from='alice@alpha.test' to='bob@gamma.test'/>"
|
||||
)
|
||||
.await
|
||||
.is_err()
|
||||
);
|
||||
task.abort();
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread")]
|
||||
async fn reuses_connections_serializes_writes_and_retires_idle_or_closed_peers() {
|
||||
use std::sync::{
|
||||
Arc, Mutex,
|
||||
atomic::{AtomicUsize, Ordering},
|
||||
};
|
||||
use std::time::Duration;
|
||||
let temp = tempfile::tempdir().unwrap();
|
||||
fixture(temp.path());
|
||||
let (c, k, r) = material(temp.path(), "alpha.test");
|
||||
let mut alice = Federation::new("alpha.test".into(), c, k, r)
|
||||
.unwrap()
|
||||
.with_outbound_idle_timeout(Duration::from_millis(300))
|
||||
.unwrap();
|
||||
let (c, k, r) = material(temp.path(), "beta.test");
|
||||
let bob = Federation::new("beta.test".into(), c, k, r).unwrap();
|
||||
let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
|
||||
alice
|
||||
.set_target(
|
||||
"beta.test".into(),
|
||||
"127.0.0.1".into(),
|
||||
listener.local_addr().unwrap().port(),
|
||||
)
|
||||
.unwrap();
|
||||
let accepted = Arc::new(AtomicUsize::new(0));
|
||||
let completed = Arc::new(AtomicUsize::new(0));
|
||||
let peers = Arc::new(Mutex::new(Vec::<tokio::task::AbortHandle>::new()));
|
||||
let (tx, mut rx) = mpsc::channel(64);
|
||||
let server = {
|
||||
let accepted = accepted.clone();
|
||||
let completed = completed.clone();
|
||||
let peers = peers.clone();
|
||||
tokio::spawn(async move {
|
||||
loop {
|
||||
let (tcp, _) = listener.accept().await.unwrap();
|
||||
accepted.fetch_add(1, Ordering::SeqCst);
|
||||
let bob = bob.clone();
|
||||
let tx = tx.clone();
|
||||
let completed = completed.clone();
|
||||
let handle = tokio::spawn(async move {
|
||||
let _ = bob.accept(tcp, tx).await;
|
||||
completed.fetch_add(1, Ordering::SeqCst);
|
||||
});
|
||||
peers.lock().unwrap().push(handle.abort_handle());
|
||||
}
|
||||
})
|
||||
};
|
||||
let stanza = |id: usize| {
|
||||
format!(
|
||||
"<message xmlns='jabber:client' from='alice@alpha.test/r' to='bob@beta.test' id='message-{id}'><body>payload-{id}</body></message>"
|
||||
)
|
||||
};
|
||||
for id in 0..3 {
|
||||
alice
|
||||
.route("alpha.test", "bob@beta.test", &stanza(id))
|
||||
.await
|
||||
.unwrap();
|
||||
let received = tokio::time::timeout(Duration::from_secs(2), rx.recv())
|
||||
.await
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
assert!(received.xml.contains(&format!("message-{id}")));
|
||||
}
|
||||
assert_eq!(
|
||||
accepted.load(Ordering::SeqCst),
|
||||
1,
|
||||
"sequential sends should reuse one authenticated TLS connection"
|
||||
);
|
||||
assert!(
|
||||
alice
|
||||
.route(
|
||||
"alpha.test",
|
||||
"bob@beta.test",
|
||||
"<message from='forged@gamma.test' to='bob@beta.test'/>"
|
||||
)
|
||||
.await
|
||||
.is_err()
|
||||
);
|
||||
assert_eq!(
|
||||
accepted.load(Ordering::SeqCst),
|
||||
1,
|
||||
"source checks apply before reuse"
|
||||
);
|
||||
let mut jobs = vec![];
|
||||
for id in 3..23 {
|
||||
let alice = alice.clone();
|
||||
let xml = stanza(id);
|
||||
jobs.push(tokio::spawn(async move {
|
||||
alice
|
||||
.route("alpha.test", "bob@beta.test", &xml)
|
||||
.await
|
||||
.unwrap()
|
||||
}));
|
||||
}
|
||||
for job in jobs {
|
||||
job.await.unwrap();
|
||||
}
|
||||
let mut ids = std::collections::BTreeSet::new();
|
||||
for _ in 3..23 {
|
||||
let received = tokio::time::timeout(Duration::from_secs(2), rx.recv())
|
||||
.await
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
let d = roxmltree::Document::parse(&received.xml).unwrap();
|
||||
ids.insert(d.root_element().attribute("id").unwrap().to_owned());
|
||||
}
|
||||
assert_eq!(ids.len(), 20);
|
||||
assert_eq!(
|
||||
accepted.load(Ordering::SeqCst),
|
||||
1,
|
||||
"concurrent clones share a per-pair connection"
|
||||
);
|
||||
tokio::time::sleep(Duration::from_millis(500)).await;
|
||||
assert!(
|
||||
completed.load(Ordering::SeqCst) >= 1,
|
||||
"idle connection must close without another route call"
|
||||
);
|
||||
alice
|
||||
.route("alpha.test", "bob@beta.test", &stanza(23))
|
||||
.await
|
||||
.unwrap();
|
||||
let received = tokio::time::timeout(Duration::from_secs(2), rx.recv())
|
||||
.await
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
assert!(received.xml.contains("message-23"));
|
||||
assert_eq!(accepted.load(Ordering::SeqCst), 2);
|
||||
peers.lock().unwrap().last().unwrap().abort();
|
||||
tokio::time::sleep(Duration::from_millis(100)).await;
|
||||
alice
|
||||
.route("alpha.test", "bob@beta.test", &stanza(24))
|
||||
.await
|
||||
.unwrap();
|
||||
let received = tokio::time::timeout(Duration::from_secs(2), rx.recv())
|
||||
.await
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
assert!(received.xml.contains("message-24"));
|
||||
assert_eq!(accepted.load(Ordering::SeqCst), 3);
|
||||
assert!(rx.try_recv().is_err(), "no automatic duplicate deliveries");
|
||||
drop(alice);
|
||||
server.abort();
|
||||
for peer in peers.lock().unwrap().iter() {
|
||||
peer.abort();
|
||||
}
|
||||
}
|
||||
|
||||
/// Native interop test, deliberately opt-in: needs a built Prosody source tree,
|
||||
/// Lua 5.4, LuaSocket, LuaSec, LuaExpat and LuaFileSystem.
|
||||
///
|
||||
/// PROSODY_SOURCE=/path/to/prosody-cafd74b9c5ea PROSODY_LUA=/path/to/lua5.4 \
|
||||
/// cargo test --test federation_tls native_prosody_tls_external_bidirectional -- --ignored --nocapture
|
||||
///
|
||||
/// Lua's LUA_PATH/LUA_CPATH and the platform library path are inherited. The
|
||||
/// probe plugin overrides TCP discovery only; both peers still authenticate the
|
||||
/// original alpha.test/beta.test identities and trust only the generated CA.
|
||||
#[tokio::test(flavor = "multi_thread")]
|
||||
#[ignore = "requires native Prosody cafd74b9c5ea and Lua TLS dependencies"]
|
||||
async fn native_prosody_tls_external_bidirectional() {
|
||||
use std::process::{Child, Stdio};
|
||||
use std::time::Duration;
|
||||
struct Native(Child);
|
||||
impl Drop for Native {
|
||||
fn drop(&mut self) {
|
||||
let _ = self.0.kill();
|
||||
let _ = self.0.wait();
|
||||
}
|
||||
}
|
||||
fn lua_path(path: &Path) -> String {
|
||||
format!(
|
||||
"'{}'",
|
||||
path.to_string_lossy()
|
||||
.replace('\\', "\\\\")
|
||||
.replace('\'', "\\'")
|
||||
)
|
||||
}
|
||||
let source = std::env::var_os("PROSODY_SOURCE")
|
||||
.map(std::path::PathBuf::from)
|
||||
.expect("set PROSODY_SOURCE to the built Prosody cafd74b9c5ea source tree");
|
||||
let source = source.canonicalize().unwrap();
|
||||
assert_eq!(
|
||||
fs::read_to_string(source.join("prosody.version"))
|
||||
.unwrap()
|
||||
.trim(),
|
||||
"cafd74b9c5ea"
|
||||
);
|
||||
let lua = std::env::var_os("PROSODY_LUA").unwrap_or_else(|| "lua5.4".into());
|
||||
let temp = tempfile::tempdir().unwrap();
|
||||
fixture(temp.path());
|
||||
// OpenSSL's TLS client validation requires clientAuth as well as serverAuth
|
||||
// for s2s certificates, unlike rustls's server-identity verifier.
|
||||
let ext = temp.path().join("alpha.test.ext");
|
||||
let extensions = fs::read_to_string(&ext).unwrap().replace(
|
||||
"extendedKeyUsage=serverAuth",
|
||||
"extendedKeyUsage=serverAuth,clientAuth",
|
||||
);
|
||||
fs::write(ext, extensions).unwrap();
|
||||
openssl(
|
||||
temp.path(),
|
||||
&[
|
||||
"x509",
|
||||
"-req",
|
||||
"-in",
|
||||
"alpha.test.csr",
|
||||
"-CA",
|
||||
"ca.pem",
|
||||
"-CAkey",
|
||||
"ca.key",
|
||||
"-CAcreateserial",
|
||||
"-out",
|
||||
"alpha.test.pem",
|
||||
"-days",
|
||||
"1",
|
||||
"-extfile",
|
||||
"alpha.test.ext",
|
||||
],
|
||||
);
|
||||
let (certs, key, roots) = material(temp.path(), "alpha.test");
|
||||
let mut rust = Federation::new("alpha.test".into(), certs, key, roots).unwrap();
|
||||
let rust_listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
|
||||
let rust_port = rust_listener.local_addr().unwrap().port();
|
||||
let reservation = std::net::TcpListener::bind("127.0.0.1:0").unwrap();
|
||||
let prosody_port = reservation.local_addr().unwrap().port();
|
||||
drop(reservation);
|
||||
rust.set_target("beta.test".into(), "127.0.0.1".into(), prosody_port)
|
||||
.unwrap();
|
||||
let plugins = temp.path().join("plugins");
|
||||
fs::create_dir(&plugins).unwrap();
|
||||
fs::create_dir(temp.path().join("data")).unwrap();
|
||||
let received_marker = temp.path().join("received");
|
||||
fs::write(plugins.join("mod_native_probe.lua"), format!(r#"
|
||||
local st = require "prosody.util.stanza";
|
||||
local basic = require "prosody.net.resolvers.basic";
|
||||
module:hook("s2sout-pre-connect", function(event)
|
||||
assert(event.session.to_host == "alpha.test");
|
||||
event.resolver = basic.new("127.0.0.1", {rust_port}, "tcp");
|
||||
end);
|
||||
module:hook("message/bare", function(event)
|
||||
local stanza = event.stanza;
|
||||
if stanza.attr.id ~= "rust-to-prosody" then return; end
|
||||
assert(event.origin.secure and event.origin.type == "s2sin");
|
||||
assert(event.origin.external_auth == "succeeded");
|
||||
assert(event.origin.cert_chain_status == "valid");
|
||||
assert(event.origin.cert_identity_status == "valid");
|
||||
local marker = assert(io.open({}, "w"));
|
||||
marker:write(stanza:get_child_text("body")); marker:close();
|
||||
module:send(st.message({{ from="bob@beta.test"; to="alice@alpha.test"; id="prosody-to-rust"; type="chat" }}):body("native-prosody-reply"));
|
||||
return true;
|
||||
end, 1000);
|
||||
"#, lua_path(&received_marker))).unwrap();
|
||||
let config = temp.path().join("prosody.cfg.lua");
|
||||
fs::write(
|
||||
&config,
|
||||
format!(
|
||||
r#"
|
||||
daemonize = false;
|
||||
run_as_root = true;
|
||||
interfaces = {{ "127.0.0.1" }};
|
||||
c2s_ports = {{}};
|
||||
s2s_ports = {{ {prosody_port} }};
|
||||
http_ports = {{}};
|
||||
https_ports = {{}};
|
||||
plugin_paths = {{ {} }};
|
||||
data_path = {};
|
||||
modules_enabled = {{ "tls"; "saslauth"; "native_probe" }};
|
||||
modules_disabled = {{ "dialback"; "s2s_bidi"; "offline"; "c2s" }};
|
||||
s2s_secure_auth = true;
|
||||
s2s_require_encryption = true;
|
||||
log = "*console";
|
||||
ssl = {{ key = {}; certificate = {}; cafile = {}; capath = false; }};
|
||||
VirtualHost "beta.test"
|
||||
authentication = "internal_hashed";
|
||||
"#,
|
||||
lua_path(&plugins),
|
||||
lua_path(&temp.path().join("data")),
|
||||
lua_path(&temp.path().join("beta.test.key")),
|
||||
lua_path(&temp.path().join("beta.test.pem")),
|
||||
lua_path(&temp.path().join("ca.pem"))
|
||||
),
|
||||
)
|
||||
.unwrap();
|
||||
let log_path = temp.path().join("prosody.log");
|
||||
let log = fs::File::create(&log_path).unwrap();
|
||||
let mut native = Native(
|
||||
Command::new(lua)
|
||||
.current_dir(&source)
|
||||
.env("PROSODY_SRCDIR", &source)
|
||||
.env("PROSODY_PLUGINDIR", source.join("plugins"))
|
||||
.arg(source.join("prosody"))
|
||||
.args(["-F", "--config"])
|
||||
.arg(&config)
|
||||
.stdout(Stdio::from(log.try_clone().unwrap()))
|
||||
.stderr(Stdio::from(log))
|
||||
.spawn()
|
||||
.expect("start native Prosody"),
|
||||
);
|
||||
let log_text = || fs::read_to_string(&log_path).unwrap_or_default();
|
||||
let startup = tokio::time::timeout(Duration::from_secs(10), async {
|
||||
loop {
|
||||
assert!(
|
||||
native.0.try_wait().unwrap().is_none(),
|
||||
"Prosody exited:\n{}",
|
||||
log_text()
|
||||
);
|
||||
if tokio::net::TcpStream::connect(("127.0.0.1", prosody_port))
|
||||
.await
|
||||
.is_ok()
|
||||
{
|
||||
break;
|
||||
}
|
||||
tokio::time::sleep(Duration::from_millis(30)).await;
|
||||
}
|
||||
})
|
||||
.await;
|
||||
assert!(startup.is_ok(), "Prosody did not listen:\n{}", log_text());
|
||||
let (tx, mut rx) = mpsc::channel(4);
|
||||
let receiving = {
|
||||
let rust = rust.clone();
|
||||
tokio::spawn(async move {
|
||||
let (tcp, _) = rust_listener.accept().await.unwrap();
|
||||
rust.accept(tcp, tx).await
|
||||
})
|
||||
};
|
||||
let outcome = rust.route("alpha.test", "bob@beta.test", "<message from='alice@alpha.test' to='bob@beta.test' id='rust-to-prosody' type='chat'><body>native-rust-probe</body></message>").await;
|
||||
assert!(
|
||||
outcome.is_ok(),
|
||||
"Rust -> Prosody: {outcome:?}\n{}",
|
||||
log_text()
|
||||
);
|
||||
let reply = tokio::time::timeout(Duration::from_secs(10), rx.recv()).await;
|
||||
assert!(
|
||||
matches!(&reply, Ok(Some(_))),
|
||||
"Prosody -> Rust: {reply:?}\n{}",
|
||||
log_text()
|
||||
);
|
||||
let reply = reply.unwrap().unwrap();
|
||||
assert_eq!(reply.from_domain, "beta.test");
|
||||
assert!(reply.xml.contains("prosody-to-rust"));
|
||||
assert!(reply.xml.contains("native-prosody-reply"));
|
||||
assert_eq!(
|
||||
fs::read_to_string(received_marker).unwrap(),
|
||||
"native-rust-probe"
|
||||
);
|
||||
let logs = log_text();
|
||||
assert!(
|
||||
logs.contains("Accepting SASL EXTERNAL identity from alpha.test"),
|
||||
"{logs}"
|
||||
);
|
||||
assert!(
|
||||
logs.contains("SASL EXTERNAL with alpha.test succeeded"),
|
||||
"{logs}"
|
||||
);
|
||||
receiving.abort();
|
||||
}
|
||||
Reference in new issue
Block a user