forked from lavina/lavina
1
0
Fork 0
lavina/crates/projection-xmpp/tests/lib.rs

477 lines
22 KiB
Rust
Raw Normal View History

use std::io::ErrorKind;
use std::str::from_utf8;
use std::sync::Arc;
use std::time::Duration;
use anyhow::Result;
use assert_matches::*;
use prometheus::Registry as MetricsRegistry;
use quick_xml::events::Event;
use quick_xml::name::LocalName;
use quick_xml::NsReader;
use tokio::io::{AsyncBufReadExt, AsyncReadExt, AsyncWriteExt, BufReader};
use tokio::io::{ReadHalf as GenericReadHalf, WriteHalf as GenericWriteHalf};
use tokio::net::tcp::{ReadHalf, WriteHalf};
use tokio::net::TcpStream;
use tokio_rustls::client::TlsStream;
use tokio_rustls::rustls::client::ServerCertVerifier;
use tokio_rustls::rustls::{ClientConfig, ServerName};
use tokio_rustls::TlsConnector;
use lavina_core::clustering::{ClusterConfig, ClusterMetadata};
core: separate the model from the logic implementation (#66) This separates the core in two layers – the model objects and the `LavinaCore` service. Service is responsible for implementing the application logic and exposing it as core's public API to projections, while the model objects will be independent of each other and responsible only for managing and owning in-memory data. The model objects include: 1. `Storage` – the open connection to the SQLite DB. 2. `PlayerRegistry` – creates, stores refs to, and stops player actors. 3. `RoomRegistry` – manages active rooms. 4. `DialogRegistry` – manages active dialogs. 5. `Broadcasting` – manages subscriptions of players to rooms on remote cluster nodes. 6. `LavinaClient` – manages HTTP connections to remote cluster nodes. 7. `ClusterMetadata` – read-only configuration of the cluster metadata, i.e. allocation of entities to nodes. As a result: 1. Model objects will be fully independent of each other, e.g. it's no longer necessary to provide a `Storage` to all registries, or to provide `PlayerRegistry` and `DialogRegistry` to each other. 2. Model objects will no longer be `Arc`-wrapped; instead the whole `Services` object will be `Arc`ed and provided to projections. 3. The public API of `lavina-core` will be properly delimited by the APIs of `LavinaCore`, `PlayerConnection` and so on. 4. `LavinaCore` and `PlayerConnection` will also contain APIs of all features, unlike it was previously with `RoomRegistry` and `DialogRegistry`. This is unfortunate, but it could be improved in future. Reviewed-on: https://git.vilunov.me/lavina/lavina/pulls/66
2024-05-13 14:32:45 +00:00
use lavina_core::player::PlayerId;
use lavina_core::repo::{Storage, StorageConfig};
use lavina_core::LavinaCore;
use projection_xmpp::{launch, RunningServer, ServerConfig};
fn element_name<'a>(local_name: &LocalName<'a>) -> &'a str {
from_utf8(local_name.into_inner()).unwrap()
}
pub async fn read_irc_message(reader: &mut BufReader<ReadHalf<'_>>, buf: &mut Vec<u8>) -> Result<usize> {
let mut size = 0;
let res = reader.read_until(b'\n', buf).await?;
size += res;
return Ok(size);
}
struct TestScope<'a> {
reader: NsReader<BufReader<ReadHalf<'a>>>,
writer: WriteHalf<'a>,
buffer: Vec<u8>,
}
impl<'a> TestScope<'a> {
fn new(stream: &mut TcpStream) -> TestScope<'_> {
let (reader, writer) = stream.split();
let reader = NsReader::from_reader(BufReader::new(reader));
let buffer = vec![];
TestScope { reader, writer, buffer }
}
async fn send(&mut self, str: &str) -> Result<()> {
self.writer.write_all(str.as_bytes()).await?;
self.writer.flush().await?;
Ok(())
}
async fn next_xml_event(&mut self) -> Result<Event<'_>> {
self.buffer.clear();
let event = self.reader.read_event_into_async(&mut self.buffer).await?;
Ok(event)
}
async fn expect_starttls_required(&mut self) -> Result<()> {
assert_matches!(self.next_xml_event().await?, Event::Start(b) => assert_eq!(element_name(&b.local_name()), "features"));
assert_matches!(self.next_xml_event().await?, Event::Start(b) => assert_eq!(element_name(&b.local_name()), "starttls"));
assert_matches!(self.next_xml_event().await?, Event::Empty(b) => assert_eq!(element_name(&b.local_name()), "required"));
assert_matches!(self.next_xml_event().await?, Event::End(b) => assert_eq!(element_name(&b.local_name()), "starttls"));
assert_matches!(self.next_xml_event().await?, Event::End(b) => assert_eq!(element_name(&b.local_name()), "features"));
Ok(())
}
}
struct TestScopeTls<'a> {
reader: NsReader<BufReader<GenericReadHalf<&'a mut TlsStream<TcpStream>>>>,
writer: GenericWriteHalf<&'a mut TlsStream<TcpStream>>,
buffer: Vec<u8>,
pub timeout: Duration,
}
impl<'a> TestScopeTls<'a> {
fn new(stream: &'a mut TlsStream<TcpStream>, buffer: Vec<u8>) -> TestScopeTls<'a> {
let (reader, writer) = tokio::io::split(stream);
let reader = NsReader::from_reader(BufReader::new(reader));
let timeout = Duration::from_millis(500);
TestScopeTls {
reader,
writer,
buffer,
timeout,
}
}
async fn send(&mut self, str: &str) -> Result<()> {
self.writer.write_all(str.as_bytes()).await?;
self.writer.flush().await?;
Ok(())
}
async fn expect_auth_mechanisms(&mut self) -> Result<()> {
assert_matches!(self.next_xml_event().await?, Event::Start(b) => assert_eq!(element_name(&b.local_name()), "features"));
assert_matches!(self.next_xml_event().await?, Event::Start(b) => assert_eq!(element_name(&b.local_name()), "mechanisms"));
assert_matches!(self.next_xml_event().await?, Event::Start(b) => assert_eq!(element_name(&b.local_name()), "mechanism"));
assert_matches!(self.next_xml_event().await?, Event::Text(b) => assert_eq!(&*b, b"PLAIN"));
assert_matches!(self.next_xml_event().await?, Event::End(b) => assert_eq!(element_name(&b.local_name()), "mechanism"));
assert_matches!(self.next_xml_event().await?, Event::End(b) => assert_eq!(element_name(&b.local_name()), "mechanisms"));
assert_matches!(self.next_xml_event().await?, Event::End(b) => assert_eq!(element_name(&b.local_name()), "features"));
Ok(())
}
async fn expect_bind_feature(&mut self) -> Result<()> {
assert_matches!(self.next_xml_event().await?, Event::Start(b) => assert_eq!(element_name(&b.local_name()), "features"));
assert_matches!(self.next_xml_event().await?, Event::Empty(b) => assert_eq!(element_name(&b.local_name()), "bind"));
assert_matches!(self.next_xml_event().await?, Event::End(b) => assert_eq!(element_name(&b.local_name()), "features"));
Ok(())
}
async fn next_xml_event(&mut self) -> Result<Event<'_>> {
self.buffer.clear();
let event = self.reader.read_event_into_async(&mut self.buffer);
let event = tokio::time::timeout(self.timeout, event).await??;
Ok(event)
}
}
struct IgnoreCertVerification;
impl ServerCertVerifier for IgnoreCertVerification {
fn verify_server_cert(
&self,
_end_entity: &tokio_rustls::rustls::Certificate,
_intermediates: &[tokio_rustls::rustls::Certificate],
_server_name: &ServerName,
_scts: &mut dyn Iterator<Item = &[u8]>,
_ocsp_response: &[u8],
_now: std::time::SystemTime,
) -> std::result::Result<tokio_rustls::rustls::client::ServerCertVerified, tokio_rustls::rustls::Error> {
Ok(tokio_rustls::rustls::client::ServerCertVerified::assertion())
}
}
struct TestServer {
core: LavinaCore,
server: RunningServer,
}
impl TestServer {
async fn start() -> Result<TestServer> {
let _ = tracing_subscriber::fmt::try_init();
let config = ServerConfig {
listen_on: "127.0.0.1:0".parse().unwrap(),
cert: "tests/certs/xmpp.pem".parse().unwrap(),
key: "tests/certs/xmpp.key".parse().unwrap(),
hostname: "localhost".into(),
};
core: separate the model from the logic implementation (#66) This separates the core in two layers – the model objects and the `LavinaCore` service. Service is responsible for implementing the application logic and exposing it as core's public API to projections, while the model objects will be independent of each other and responsible only for managing and owning in-memory data. The model objects include: 1. `Storage` – the open connection to the SQLite DB. 2. `PlayerRegistry` – creates, stores refs to, and stops player actors. 3. `RoomRegistry` – manages active rooms. 4. `DialogRegistry` – manages active dialogs. 5. `Broadcasting` – manages subscriptions of players to rooms on remote cluster nodes. 6. `LavinaClient` – manages HTTP connections to remote cluster nodes. 7. `ClusterMetadata` – read-only configuration of the cluster metadata, i.e. allocation of entities to nodes. As a result: 1. Model objects will be fully independent of each other, e.g. it's no longer necessary to provide a `Storage` to all registries, or to provide `PlayerRegistry` and `DialogRegistry` to each other. 2. Model objects will no longer be `Arc`-wrapped; instead the whole `Services` object will be `Arc`ed and provided to projections. 3. The public API of `lavina-core` will be properly delimited by the APIs of `LavinaCore`, `PlayerConnection` and so on. 4. `LavinaCore` and `PlayerConnection` will also contain APIs of all features, unlike it was previously with `RoomRegistry` and `DialogRegistry`. This is unfortunate, but it could be improved in future. Reviewed-on: https://git.vilunov.me/lavina/lavina/pulls/66
2024-05-13 14:32:45 +00:00
let mut metrics = MetricsRegistry::new();
let storage = Storage::open(StorageConfig {
db_path: ":memory:".into(),
})
.await?;
let cluster_config = ClusterConfig {
addresses: vec![],
metadata: ClusterMetadata {
node_id: 0,
main_owner: 0,
rooms: Default::default(),
},
};
core: separate the model from the logic implementation (#66) This separates the core in two layers – the model objects and the `LavinaCore` service. Service is responsible for implementing the application logic and exposing it as core's public API to projections, while the model objects will be independent of each other and responsible only for managing and owning in-memory data. The model objects include: 1. `Storage` – the open connection to the SQLite DB. 2. `PlayerRegistry` – creates, stores refs to, and stops player actors. 3. `RoomRegistry` – manages active rooms. 4. `DialogRegistry` – manages active dialogs. 5. `Broadcasting` – manages subscriptions of players to rooms on remote cluster nodes. 6. `LavinaClient` – manages HTTP connections to remote cluster nodes. 7. `ClusterMetadata` – read-only configuration of the cluster metadata, i.e. allocation of entities to nodes. As a result: 1. Model objects will be fully independent of each other, e.g. it's no longer necessary to provide a `Storage` to all registries, or to provide `PlayerRegistry` and `DialogRegistry` to each other. 2. Model objects will no longer be `Arc`-wrapped; instead the whole `Services` object will be `Arc`ed and provided to projections. 3. The public API of `lavina-core` will be properly delimited by the APIs of `LavinaCore`, `PlayerConnection` and so on. 4. `LavinaCore` and `PlayerConnection` will also contain APIs of all features, unlike it was previously with `RoomRegistry` and `DialogRegistry`. This is unfortunate, but it could be improved in future. Reviewed-on: https://git.vilunov.me/lavina/lavina/pulls/66
2024-05-13 14:32:45 +00:00
let core = LavinaCore::new(&mut metrics, cluster_config, storage).await?;
let server = launch(config, core.clone(), metrics.clone()).await.unwrap();
core: separate the model from the logic implementation (#66) This separates the core in two layers – the model objects and the `LavinaCore` service. Service is responsible for implementing the application logic and exposing it as core's public API to projections, while the model objects will be independent of each other and responsible only for managing and owning in-memory data. The model objects include: 1. `Storage` – the open connection to the SQLite DB. 2. `PlayerRegistry` – creates, stores refs to, and stops player actors. 3. `RoomRegistry` – manages active rooms. 4. `DialogRegistry` – manages active dialogs. 5. `Broadcasting` – manages subscriptions of players to rooms on remote cluster nodes. 6. `LavinaClient` – manages HTTP connections to remote cluster nodes. 7. `ClusterMetadata` – read-only configuration of the cluster metadata, i.e. allocation of entities to nodes. As a result: 1. Model objects will be fully independent of each other, e.g. it's no longer necessary to provide a `Storage` to all registries, or to provide `PlayerRegistry` and `DialogRegistry` to each other. 2. Model objects will no longer be `Arc`-wrapped; instead the whole `Services` object will be `Arc`ed and provided to projections. 3. The public API of `lavina-core` will be properly delimited by the APIs of `LavinaCore`, `PlayerConnection` and so on. 4. `LavinaCore` and `PlayerConnection` will also contain APIs of all features, unlike it was previously with `RoomRegistry` and `DialogRegistry`. This is unfortunate, but it could be improved in future. Reviewed-on: https://git.vilunov.me/lavina/lavina/pulls/66
2024-05-13 14:32:45 +00:00
Ok(TestServer { core, server })
}
async fn shutdown(self) -> Result<()> {
self.server.terminate().await?;
core: separate the model from the logic implementation (#66) This separates the core in two layers – the model objects and the `LavinaCore` service. Service is responsible for implementing the application logic and exposing it as core's public API to projections, while the model objects will be independent of each other and responsible only for managing and owning in-memory data. The model objects include: 1. `Storage` – the open connection to the SQLite DB. 2. `PlayerRegistry` – creates, stores refs to, and stops player actors. 3. `RoomRegistry` – manages active rooms. 4. `DialogRegistry` – manages active dialogs. 5. `Broadcasting` – manages subscriptions of players to rooms on remote cluster nodes. 6. `LavinaClient` – manages HTTP connections to remote cluster nodes. 7. `ClusterMetadata` – read-only configuration of the cluster metadata, i.e. allocation of entities to nodes. As a result: 1. Model objects will be fully independent of each other, e.g. it's no longer necessary to provide a `Storage` to all registries, or to provide `PlayerRegistry` and `DialogRegistry` to each other. 2. Model objects will no longer be `Arc`-wrapped; instead the whole `Services` object will be `Arc`ed and provided to projections. 3. The public API of `lavina-core` will be properly delimited by the APIs of `LavinaCore`, `PlayerConnection` and so on. 4. `LavinaCore` and `PlayerConnection` will also contain APIs of all features, unlike it was previously with `RoomRegistry` and `DialogRegistry`. This is unfortunate, but it could be improved in future. Reviewed-on: https://git.vilunov.me/lavina/lavina/pulls/66
2024-05-13 14:32:45 +00:00
let storage = self.core.shutdown().await;
storage.close().await;
Ok(())
}
}
#[tokio::test]
async fn scenario_basic() -> Result<()> {
let server = TestServer::start().await?;
// test scenario
core: separate the model from the logic implementation (#66) This separates the core in two layers – the model objects and the `LavinaCore` service. Service is responsible for implementing the application logic and exposing it as core's public API to projections, while the model objects will be independent of each other and responsible only for managing and owning in-memory data. The model objects include: 1. `Storage` – the open connection to the SQLite DB. 2. `PlayerRegistry` – creates, stores refs to, and stops player actors. 3. `RoomRegistry` – manages active rooms. 4. `DialogRegistry` – manages active dialogs. 5. `Broadcasting` – manages subscriptions of players to rooms on remote cluster nodes. 6. `LavinaClient` – manages HTTP connections to remote cluster nodes. 7. `ClusterMetadata` – read-only configuration of the cluster metadata, i.e. allocation of entities to nodes. As a result: 1. Model objects will be fully independent of each other, e.g. it's no longer necessary to provide a `Storage` to all registries, or to provide `PlayerRegistry` and `DialogRegistry` to each other. 2. Model objects will no longer be `Arc`-wrapped; instead the whole `Services` object will be `Arc`ed and provided to projections. 3. The public API of `lavina-core` will be properly delimited by the APIs of `LavinaCore`, `PlayerConnection` and so on. 4. `LavinaCore` and `PlayerConnection` will also contain APIs of all features, unlike it was previously with `RoomRegistry` and `DialogRegistry`. This is unfortunate, but it could be improved in future. Reviewed-on: https://git.vilunov.me/lavina/lavina/pulls/66
2024-05-13 14:32:45 +00:00
server.core.create_player(&PlayerId::from("tester")?).await?;
server.core.set_password("tester", "password").await?;
let mut stream = TcpStream::connect(server.server.addr).await?;
let mut s = TestScope::new(&mut stream);
tracing::info!("TCP connection established");
s.send(r#"<?xml version="1.0"?>"#).await?;
s.send(r#"<stream:stream xmlns:stream="http://etherx.jabber.org/streams" to="127.0.0.1" xml:lang="en" xmlns:xml="http://www.w3.org/XML/1998/namespace" xmlns="jabber:client" version="1.0">"#).await?;
assert_matches!(s.next_xml_event().await?, Event::Decl(_) => {});
assert_matches!(s.next_xml_event().await?, Event::Start(b) => assert_eq!(element_name(&b.local_name()), "stream"));
s.expect_starttls_required().await?;
s.send(r#"<starttls/>"#).await?;
assert_matches!(s.next_xml_event().await?, Event::Empty(b) => assert_eq!(element_name(&b.local_name()), "proceed"));
let buffer = s.buffer;
tracing::info!("TLS feature negotiation complete");
let connector = TlsConnector::from(Arc::new(
ClientConfig::builder()
.with_safe_defaults()
.with_custom_certificate_verifier(Arc::new(IgnoreCertVerification))
.with_no_client_auth(),
));
tracing::info!("Initiating TLS connection...");
let mut stream = connector.connect(ServerName::IpAddress(server.server.addr.ip()), stream).await?;
tracing::info!("TLS connection established");
let mut s = TestScopeTls::new(&mut stream, buffer);
s.send(r#"<?xml version="1.0"?>"#).await?;
s.send(r#"<stream:stream xmlns:stream="http://etherx.jabber.org/streams" to="127.0.0.1" xml:lang="en" xmlns:xml="http://www.w3.org/XML/1998/namespace" xmlns="jabber:client" version="1.0">"#).await?;
assert_matches!(s.next_xml_event().await?, Event::Decl(_) => {});
assert_matches!(s.next_xml_event().await?, Event::Start(b) => assert_eq!(element_name(&b.local_name()), "stream"));
s.expect_auth_mechanisms().await?;
// base64-encoded "\x00tester\x00password"
s.send(r#"<auth xmlns="urn:ietf:params:xml:ns:xmpp-sasl" mechanism="PLAIN">AHRlc3RlcgBwYXNzd29yZA==</auth>"#)
.await?;
assert_matches!(s.next_xml_event().await?, Event::Empty(b) => assert_eq!(element_name(&b.local_name()), "success"));
s.send(r#"<?xml version="1.0"?>"#).await?;
s.send(r#"<stream:stream xmlns:stream="http://etherx.jabber.org/streams" to="127.0.0.1" xml:lang="en" xmlns:xml="http://www.w3.org/XML/1998/namespace" xmlns="jabber:client" version="1.0">"#).await?;
assert_matches!(s.next_xml_event().await?, Event::Decl(_) => {});
assert_matches!(s.next_xml_event().await?, Event::Start(b) => assert_eq!(element_name(&b.local_name()), "stream"));
s.expect_bind_feature().await?;
s.send(r#"<iq id="bind_1" type="set"><bind xmlns="urn:ietf:params:xml:ns:xmpp-bind"><resource>kek</resource></bind></iq>"#).await?;
assert_matches!(s.next_xml_event().await?, Event::Start(b) => assert_eq!(element_name(&b.local_name()), "iq"));
assert_matches!(s.next_xml_event().await?, Event::Start(b) => assert_eq!(element_name(&b.local_name()), "bind"));
assert_matches!(s.next_xml_event().await?, Event::Start(b) => assert_eq!(element_name(&b.local_name()), "jid"));
assert_matches!(s.next_xml_event().await?, Event::Text(b) => assert_eq!(&*b, b"tester@localhost/tester"));
assert_matches!(s.next_xml_event().await?, Event::End(b) => assert_eq!(element_name(&b.local_name()), "jid"));
assert_matches!(s.next_xml_event().await?, Event::End(b) => assert_eq!(element_name(&b.local_name()), "bind"));
assert_matches!(s.next_xml_event().await?, Event::End(b) => assert_eq!(element_name(&b.local_name()), "iq"));
s.send(r#"<presence xmlns="jabber:client" type="unavailable"><status>Logged out</status></presence>"#).await?;
stream.shutdown().await?;
// wrap up
server.shutdown().await?;
Ok(())
}
#[tokio::test]
async fn scenario_wrong_password() -> Result<()> {
let server = TestServer::start().await?;
// test scenario
core: separate the model from the logic implementation (#66) This separates the core in two layers – the model objects and the `LavinaCore` service. Service is responsible for implementing the application logic and exposing it as core's public API to projections, while the model objects will be independent of each other and responsible only for managing and owning in-memory data. The model objects include: 1. `Storage` – the open connection to the SQLite DB. 2. `PlayerRegistry` – creates, stores refs to, and stops player actors. 3. `RoomRegistry` – manages active rooms. 4. `DialogRegistry` – manages active dialogs. 5. `Broadcasting` – manages subscriptions of players to rooms on remote cluster nodes. 6. `LavinaClient` – manages HTTP connections to remote cluster nodes. 7. `ClusterMetadata` – read-only configuration of the cluster metadata, i.e. allocation of entities to nodes. As a result: 1. Model objects will be fully independent of each other, e.g. it's no longer necessary to provide a `Storage` to all registries, or to provide `PlayerRegistry` and `DialogRegistry` to each other. 2. Model objects will no longer be `Arc`-wrapped; instead the whole `Services` object will be `Arc`ed and provided to projections. 3. The public API of `lavina-core` will be properly delimited by the APIs of `LavinaCore`, `PlayerConnection` and so on. 4. `LavinaCore` and `PlayerConnection` will also contain APIs of all features, unlike it was previously with `RoomRegistry` and `DialogRegistry`. This is unfortunate, but it could be improved in future. Reviewed-on: https://git.vilunov.me/lavina/lavina/pulls/66
2024-05-13 14:32:45 +00:00
server.core.create_player(&PlayerId::from("tester")?).await?;
server.core.set_password("tester", "password").await?;
let mut stream = TcpStream::connect(server.server.addr).await?;
let mut s = TestScope::new(&mut stream);
tracing::info!("TCP connection established");
s.send(r#"<?xml version="1.0"?>"#).await?;
s.send(r#"<stream:stream xmlns:stream="http://etherx.jabber.org/streams" to="127.0.0.1" xml:lang="en" xmlns:xml="http://www.w3.org/XML/1998/namespace" xmlns="jabber:client" version="1.0">"#).await?;
assert_matches!(s.next_xml_event().await?, Event::Decl(_) => {});
assert_matches!(s.next_xml_event().await?, Event::Start(b) => assert_eq!(element_name(&b.local_name()), "stream"));
s.expect_starttls_required().await?;
s.send(r#"<starttls/>"#).await?;
assert_matches!(s.next_xml_event().await?, Event::Empty(b) => assert_eq!(element_name(&b.local_name()), "proceed"));
let buffer = s.buffer;
tracing::info!("TLS feature negotiation complete");
let connector = TlsConnector::from(Arc::new(
ClientConfig::builder()
.with_safe_defaults()
.with_custom_certificate_verifier(Arc::new(IgnoreCertVerification))
.with_no_client_auth(),
));
tracing::info!("Initiating TLS connection...");
let mut stream = connector.connect(ServerName::IpAddress(server.server.addr.ip()), stream).await?;
tracing::info!("TLS connection established");
let mut s = TestScopeTls::new(&mut stream, buffer);
s.send(r#"<?xml version="1.0"?>"#).await?;
s.send(r#"<stream:stream xmlns:stream="http://etherx.jabber.org/streams" to="127.0.0.1" xml:lang="en" xmlns:xml="http://www.w3.org/XML/1998/namespace" xmlns="jabber:client" version="1.0">"#).await?;
assert_matches!(s.next_xml_event().await?, Event::Decl(_) => {});
assert_matches!(s.next_xml_event().await?, Event::Start(b) => assert_eq!(element_name(&b.local_name()), "stream"));
s.expect_auth_mechanisms().await?;
// base64-encoded "\x00tester\x00password2"
s.send(r#"<auth xmlns="urn:ietf:params:xml:ns:xmpp-sasl" mechanism="PLAIN">AHRlc3RlcgBwYXNzd29yZDI=</auth>"#)
.await?;
assert_matches!(s.next_xml_event().await?, Event::Start(b) => assert_eq!(element_name(&b.local_name()), "failure"));
assert_matches!(s.next_xml_event().await?, Event::Empty(b) => assert_eq!(element_name(&b.local_name()), "not-authorized"));
assert_matches!(s.next_xml_event().await?, Event::End(b) => assert_eq!(element_name(&b.local_name()), "failure"));
2024-04-29 17:13:32 +00:00
let _ = stream.shutdown().await;
// wrap up
server.shutdown().await?;
Ok(())
}
#[tokio::test]
async fn scenario_basic_without_headers() -> Result<()> {
let server = TestServer::start().await?;
// test scenario
core: separate the model from the logic implementation (#66) This separates the core in two layers – the model objects and the `LavinaCore` service. Service is responsible for implementing the application logic and exposing it as core's public API to projections, while the model objects will be independent of each other and responsible only for managing and owning in-memory data. The model objects include: 1. `Storage` – the open connection to the SQLite DB. 2. `PlayerRegistry` – creates, stores refs to, and stops player actors. 3. `RoomRegistry` – manages active rooms. 4. `DialogRegistry` – manages active dialogs. 5. `Broadcasting` – manages subscriptions of players to rooms on remote cluster nodes. 6. `LavinaClient` – manages HTTP connections to remote cluster nodes. 7. `ClusterMetadata` – read-only configuration of the cluster metadata, i.e. allocation of entities to nodes. As a result: 1. Model objects will be fully independent of each other, e.g. it's no longer necessary to provide a `Storage` to all registries, or to provide `PlayerRegistry` and `DialogRegistry` to each other. 2. Model objects will no longer be `Arc`-wrapped; instead the whole `Services` object will be `Arc`ed and provided to projections. 3. The public API of `lavina-core` will be properly delimited by the APIs of `LavinaCore`, `PlayerConnection` and so on. 4. `LavinaCore` and `PlayerConnection` will also contain APIs of all features, unlike it was previously with `RoomRegistry` and `DialogRegistry`. This is unfortunate, but it could be improved in future. Reviewed-on: https://git.vilunov.me/lavina/lavina/pulls/66
2024-05-13 14:32:45 +00:00
server.core.create_player(&PlayerId::from("tester")?).await?;
server.core.set_password("tester", "password").await?;
let mut stream = TcpStream::connect(server.server.addr).await?;
let mut s = TestScope::new(&mut stream);
tracing::info!("TCP connection established");
s.send(r#"<stream:stream xmlns:stream="http://etherx.jabber.org/streams" to="127.0.0.1" xml:lang="en" xmlns:xml="http://www.w3.org/XML/1998/namespace" xmlns="jabber:client" version="1.0">"#).await?;
assert_matches!(s.next_xml_event().await?, Event::Decl(_) => {});
assert_matches!(s.next_xml_event().await?, Event::Start(b) => assert_eq!(element_name(&b.local_name()), "stream"));
s.expect_starttls_required().await?;
s.send(r#"<starttls/>"#).await?;
assert_matches!(s.next_xml_event().await?, Event::Empty(b) => assert_eq!(element_name(&b.local_name()), "proceed"));
let buffer = s.buffer;
tracing::info!("TLS feature negotiation complete");
let connector = TlsConnector::from(Arc::new(
ClientConfig::builder()
.with_safe_defaults()
.with_custom_certificate_verifier(Arc::new(IgnoreCertVerification))
.with_no_client_auth(),
));
tracing::info!("Initiating TLS connection...");
let mut stream = connector.connect(ServerName::IpAddress(server.server.addr.ip()), stream).await?;
tracing::info!("TLS connection established");
let mut s = TestScopeTls::new(&mut stream, buffer);
s.send(r#"<stream:stream xmlns:stream="http://etherx.jabber.org/streams" to="127.0.0.1" xml:lang="en" xmlns:xml="http://www.w3.org/XML/1998/namespace" xmlns="jabber:client" version="1.0">"#).await?;
assert_matches!(s.next_xml_event().await?, Event::Decl(_) => {});
assert_matches!(s.next_xml_event().await?, Event::Start(b) => assert_eq!(element_name(&b.local_name()), "stream"));
stream.shutdown().await?;
// wrap up
server.shutdown().await?;
Ok(())
}
#[tokio::test]
async fn terminate_socket() -> Result<()> {
let server = TestServer::start().await?;
// test scenario
core: separate the model from the logic implementation (#66) This separates the core in two layers – the model objects and the `LavinaCore` service. Service is responsible for implementing the application logic and exposing it as core's public API to projections, while the model objects will be independent of each other and responsible only for managing and owning in-memory data. The model objects include: 1. `Storage` – the open connection to the SQLite DB. 2. `PlayerRegistry` – creates, stores refs to, and stops player actors. 3. `RoomRegistry` – manages active rooms. 4. `DialogRegistry` – manages active dialogs. 5. `Broadcasting` – manages subscriptions of players to rooms on remote cluster nodes. 6. `LavinaClient` – manages HTTP connections to remote cluster nodes. 7. `ClusterMetadata` – read-only configuration of the cluster metadata, i.e. allocation of entities to nodes. As a result: 1. Model objects will be fully independent of each other, e.g. it's no longer necessary to provide a `Storage` to all registries, or to provide `PlayerRegistry` and `DialogRegistry` to each other. 2. Model objects will no longer be `Arc`-wrapped; instead the whole `Services` object will be `Arc`ed and provided to projections. 3. The public API of `lavina-core` will be properly delimited by the APIs of `LavinaCore`, `PlayerConnection` and so on. 4. `LavinaCore` and `PlayerConnection` will also contain APIs of all features, unlike it was previously with `RoomRegistry` and `DialogRegistry`. This is unfortunate, but it could be improved in future. Reviewed-on: https://git.vilunov.me/lavina/lavina/pulls/66
2024-05-13 14:32:45 +00:00
server.core.create_player(&PlayerId::from("tester")?).await?;
server.core.set_password("tester", "password").await?;
let mut stream = TcpStream::connect(server.server.addr).await?;
let mut s = TestScope::new(&mut stream);
tracing::info!("TCP connection established");
s.send(r#"<?xml version="1.0"?>"#).await?;
s.send(r#"<stream:stream xmlns:stream="http://etherx.jabber.org/streams" to="127.0.0.1" xml:lang="en" xmlns:xml="http://www.w3.org/XML/1998/namespace" xmlns="jabber:client" version="1.0">"#).await?;
assert_matches!(s.next_xml_event().await?, Event::Decl(_) => {});
assert_matches!(s.next_xml_event().await?, Event::Start(b) => assert_eq!(element_name(&b.local_name()), "stream"));
s.expect_starttls_required().await?;
s.send(r#"<starttls/>"#).await?;
assert_matches!(s.next_xml_event().await?, Event::Empty(b) => assert_eq!(element_name(&b.local_name()), "proceed"));
let connector = TlsConnector::from(Arc::new(
ClientConfig::builder()
.with_safe_defaults()
.with_custom_certificate_verifier(Arc::new(IgnoreCertVerification))
.with_no_client_auth(),
));
tracing::info!("Initiating TLS connection...");
let mut stream = connector.connect(ServerName::IpAddress(server.server.addr.ip()), stream).await?;
tracing::info!("TLS connection established");
server.shutdown().await?;
assert_eq!(stream.read_u8().await.unwrap_err().kind(), ErrorKind::UnexpectedEof);
Ok(())
}
#[tokio::test]
async fn test_message_archive_request() -> Result<()> {
let server = TestServer::start().await?;
// test scenario
core: separate the model from the logic implementation (#66) This separates the core in two layers – the model objects and the `LavinaCore` service. Service is responsible for implementing the application logic and exposing it as core's public API to projections, while the model objects will be independent of each other and responsible only for managing and owning in-memory data. The model objects include: 1. `Storage` – the open connection to the SQLite DB. 2. `PlayerRegistry` – creates, stores refs to, and stops player actors. 3. `RoomRegistry` – manages active rooms. 4. `DialogRegistry` – manages active dialogs. 5. `Broadcasting` – manages subscriptions of players to rooms on remote cluster nodes. 6. `LavinaClient` – manages HTTP connections to remote cluster nodes. 7. `ClusterMetadata` – read-only configuration of the cluster metadata, i.e. allocation of entities to nodes. As a result: 1. Model objects will be fully independent of each other, e.g. it's no longer necessary to provide a `Storage` to all registries, or to provide `PlayerRegistry` and `DialogRegistry` to each other. 2. Model objects will no longer be `Arc`-wrapped; instead the whole `Services` object will be `Arc`ed and provided to projections. 3. The public API of `lavina-core` will be properly delimited by the APIs of `LavinaCore`, `PlayerConnection` and so on. 4. `LavinaCore` and `PlayerConnection` will also contain APIs of all features, unlike it was previously with `RoomRegistry` and `DialogRegistry`. This is unfortunate, but it could be improved in future. Reviewed-on: https://git.vilunov.me/lavina/lavina/pulls/66
2024-05-13 14:32:45 +00:00
server.core.create_player(&PlayerId::from("tester")?).await?;
server.core.set_password("tester", "password").await?;
let mut stream = TcpStream::connect(server.server.addr).await?;
let mut s = TestScope::new(&mut stream);
tracing::info!("TCP connection established");
s.send(r#"<?xml version="1.0"?>"#).await?;
s.send(r#"<stream:stream xmlns:stream="http://etherx.jabber.org/streams" to="127.0.0.1" xml:lang="en" xmlns:xml="http://www.w3.org/XML/1998/namespace" xmlns="jabber:client" version="1.0">"#).await?;
assert_matches!(s.next_xml_event().await?, Event::Decl(_) => {});
assert_matches!(s.next_xml_event().await?, Event::Start(b) => assert_eq!(element_name(&b.local_name()), "stream"));
s.expect_starttls_required().await?;
s.send(r#"<starttls/>"#).await?;
assert_matches!(s.next_xml_event().await?, Event::Empty(b) => assert_eq!(element_name(&b.local_name()), "proceed"));
let buffer = s.buffer;
tracing::info!("TLS feature negotiation complete");
let connector = TlsConnector::from(Arc::new(
ClientConfig::builder()
.with_safe_defaults()
.with_custom_certificate_verifier(Arc::new(IgnoreCertVerification))
.with_no_client_auth(),
));
tracing::info!("Initiating TLS connection...");
let mut stream = connector.connect(ServerName::IpAddress(server.server.addr.ip()), stream).await?;
tracing::info!("TLS connection established");
let mut s = TestScopeTls::new(&mut stream, buffer);
s.send(r#"<?xml version="1.0"?>"#).await?;
s.send(r#"<stream:stream xmlns:stream="http://etherx.jabber.org/streams" to="127.0.0.1" xml:lang="en" xmlns:xml="http://www.w3.org/XML/1998/namespace" xmlns="jabber:client" version="1.0">"#).await?;
assert_matches!(s.next_xml_event().await?, Event::Decl(_) => {});
assert_matches!(s.next_xml_event().await?, Event::Start(b) => assert_eq!(element_name(&b.local_name()), "stream"));
s.expect_auth_mechanisms().await?;
// base64-encoded "\x00tester\x00password"
s.send(r#"<auth xmlns="urn:ietf:params:xml:ns:xmpp-sasl" mechanism="PLAIN">AHRlc3RlcgBwYXNzd29yZA==</auth>"#)
.await?;
assert_matches!(s.next_xml_event().await?, Event::Empty(b) => assert_eq!(element_name(&b.local_name()), "success"));
s.send(r#"<?xml version="1.0"?>"#).await?;
s.send(r#"<stream:stream xmlns:stream="http://etherx.jabber.org/streams" to="127.0.0.1" xml:lang="en" xmlns:xml="http://www.w3.org/XML/1998/namespace" xmlns="jabber:client" version="1.0">"#).await?;
assert_matches!(s.next_xml_event().await?, Event::Decl(_) => {});
assert_matches!(s.next_xml_event().await?, Event::Start(b) => assert_eq!(element_name(&b.local_name()), "stream"));
s.expect_bind_feature().await?;
s.send(r#"<iq id="bind_1" type="set"><bind xmlns="urn:ietf:params:xml:ns:xmpp-bind"><resource>kek</resource></bind></iq>"#).await?;
assert_matches!(s.next_xml_event().await?, Event::Start(b) => assert_eq!(element_name(&b.local_name()), "iq"));
assert_matches!(s.next_xml_event().await?, Event::Start(b) => assert_eq!(element_name(&b.local_name()), "bind"));
assert_matches!(s.next_xml_event().await?, Event::Start(b) => assert_eq!(element_name(&b.local_name()), "jid"));
assert_matches!(s.next_xml_event().await?, Event::Text(b) => assert_eq!(&*b, b"tester@localhost/tester"));
assert_matches!(s.next_xml_event().await?, Event::End(b) => assert_eq!(element_name(&b.local_name()), "jid"));
assert_matches!(s.next_xml_event().await?, Event::End(b) => assert_eq!(element_name(&b.local_name()), "bind"));
assert_matches!(s.next_xml_event().await?, Event::End(b) => assert_eq!(element_name(&b.local_name()), "iq"));
s.send(r#"<iq type='get' id='juliet1'><query xmlns='urn:xmpp:mam:2' queryid='f27'/></iq>"#).await?;
assert_matches!(s.next_xml_event().await?, Event::Start(b) => {
assert_eq!(element_name(&b.local_name()), "iq")
});
assert_matches!(s.next_xml_event().await?, Event::Start(b) => {
assert_eq!(element_name(&b.local_name()), "fin")
});
assert_matches!(s.next_xml_event().await?, Event::Start(b) => {
assert_eq!(element_name(&b.local_name()), "set")
});
assert_matches!(s.next_xml_event().await?, Event::Start(b) => {
assert_eq!(element_name(&b.local_name()), "count")
});
assert_matches!(s.next_xml_event().await?, Event::Text(b) => {
assert_eq!(&*b, b"0")
});
s.send(r#"<presence xmlns="jabber:client" type="unavailable"><status>Logged out</status></presence>"#).await?;
stream.shutdown().await?;
// wrap up
server.shutdown().await?;
Ok(())
}