2024-02-18 16:46:29 +00:00
|
|
|
use std::io::ErrorKind;
|
2023-10-09 11:35:41 +00:00
|
|
|
use std::time::Duration;
|
|
|
|
|
2023-10-13 14:30:31 +00:00
|
|
|
use anyhow::{anyhow, Result};
|
2024-04-21 21:00:44 +00:00
|
|
|
use chrono::{DateTime, SecondsFormat};
|
2023-10-09 11:35:41 +00:00
|
|
|
use prometheus::Registry as MetricsRegistry;
|
2023-10-13 14:30:31 +00:00
|
|
|
use tokio::io::{AsyncReadExt, AsyncWriteExt, BufReader};
|
2023-10-09 11:35:41 +00:00
|
|
|
use tokio::net::tcp::{ReadHalf, WriteHalf};
|
|
|
|
use tokio::net::TcpStream;
|
|
|
|
|
2024-05-10 20:44:24 +00:00
|
|
|
use lavina_core::clustering::{ClusterConfig, ClusterMetadata};
|
2024-04-21 21:00:44 +00:00
|
|
|
use lavina_core::player::{JoinResult, PlayerId, SendMessageResult};
|
2023-10-09 11:35:41 +00:00
|
|
|
use lavina_core::repo::{Storage, StorageConfig};
|
2024-04-21 21:00:44 +00:00
|
|
|
use lavina_core::room::RoomId;
|
2024-04-21 17:45:50 +00:00
|
|
|
use lavina_core::LavinaCore;
|
2024-03-15 00:54:55 +00:00
|
|
|
use projection_irc::APP_VERSION;
|
2023-10-13 14:30:31 +00:00
|
|
|
use projection_irc::{launch, read_irc_message, RunningServer, ServerConfig};
|
2024-04-21 21:00:44 +00:00
|
|
|
|
2023-10-09 11:35:41 +00:00
|
|
|
struct TestScope<'a> {
|
|
|
|
reader: BufReader<ReadHalf<'a>>,
|
|
|
|
writer: WriteHalf<'a>,
|
|
|
|
buffer: Vec<u8>,
|
|
|
|
pub timeout: Duration,
|
|
|
|
}
|
|
|
|
|
|
|
|
impl<'a> TestScope<'a> {
|
|
|
|
fn new(stream: &mut TcpStream) -> TestScope<'_> {
|
|
|
|
let (reader, writer) = stream.split();
|
|
|
|
let reader = BufReader::new(reader);
|
|
|
|
let buffer = vec![];
|
2024-04-23 16:31:00 +00:00
|
|
|
let timeout = Duration::from_millis(1000);
|
2023-10-09 11:35:41 +00:00
|
|
|
TestScope {
|
|
|
|
reader,
|
|
|
|
writer,
|
|
|
|
buffer,
|
|
|
|
timeout,
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
async fn send(&mut self, str: &(impl AsRef<str> + ?Sized)) -> Result<()> {
|
|
|
|
self.writer.write_all(str.as_ref().as_bytes()).await?;
|
|
|
|
self.writer.write_all(b"\r\n").await?;
|
|
|
|
self.writer.flush().await?;
|
|
|
|
Ok(())
|
|
|
|
}
|
|
|
|
|
|
|
|
async fn expect(&mut self, str: &str) -> Result<()> {
|
2023-12-08 02:47:09 +00:00
|
|
|
tracing::debug!("Expecting {}", str);
|
2023-10-09 11:35:41 +00:00
|
|
|
let len = tokio::time::timeout(self.timeout, read_irc_message(&mut self.reader, &mut self.buffer)).await??;
|
|
|
|
assert_eq!(std::str::from_utf8(&self.buffer[..len - 2])?, str);
|
|
|
|
self.buffer.clear();
|
|
|
|
Ok(())
|
|
|
|
}
|
2023-10-13 14:30:31 +00:00
|
|
|
|
2024-04-06 22:34:11 +00:00
|
|
|
async fn expect_that(&mut self, validate: impl FnOnce(&str) -> bool) -> Result<()> {
|
|
|
|
let len = tokio::time::timeout(self.timeout, read_irc_message(&mut self.reader, &mut self.buffer)).await??;
|
|
|
|
let msg = std::str::from_utf8(&self.buffer[..len - 2])?;
|
|
|
|
if !validate(msg) {
|
|
|
|
return Err(anyhow!("unexpected message: {:?}", msg));
|
|
|
|
}
|
|
|
|
self.buffer.clear();
|
|
|
|
Ok(())
|
|
|
|
}
|
|
|
|
|
|
|
|
async fn expect_server_introduction(&mut self, nick: &str) -> Result<()> {
|
|
|
|
self.expect(&format!(":testserver 001 {nick} :Welcome to testserver Server")).await?;
|
|
|
|
self.expect(&format!(":testserver 002 {nick} :Welcome to testserver Server")).await?;
|
|
|
|
self.expect(&format!(":testserver 003 {nick} :Welcome to testserver Server")).await?;
|
|
|
|
self.expect(&format!(
|
|
|
|
":testserver 004 {nick} testserver {APP_VERSION} r CFILPQbcefgijklmnopqrstvz"
|
|
|
|
))
|
|
|
|
.await?;
|
|
|
|
self.expect(&format!(
|
|
|
|
":testserver 005 {nick} CHANTYPES=# :are supported by this server"
|
|
|
|
))
|
|
|
|
.await?;
|
|
|
|
Ok(())
|
|
|
|
}
|
|
|
|
|
2023-10-13 14:30:31 +00:00
|
|
|
async fn expect_eof(&mut self) -> Result<()> {
|
|
|
|
let mut buf = [0; 1];
|
|
|
|
let len = tokio::time::timeout(self.timeout, self.reader.read(&mut buf)).await??;
|
|
|
|
if len != 0 {
|
|
|
|
return Err(anyhow!("not a eof"));
|
|
|
|
}
|
|
|
|
Ok(())
|
|
|
|
}
|
|
|
|
|
|
|
|
async fn expect_nothing(&mut self) -> Result<()> {
|
|
|
|
let mut buf = [0; 1];
|
|
|
|
match tokio::time::timeout(self.timeout, self.reader.read(&mut buf)).await {
|
|
|
|
Ok(res) => Err(anyhow!("received something: {:?}", res)),
|
|
|
|
Err(_) => Ok(()),
|
|
|
|
}
|
|
|
|
}
|
2024-04-21 21:00:44 +00:00
|
|
|
|
|
|
|
async fn expect_cap_ls(&mut self) -> Result<()> {
|
2024-06-01 11:20:59 +00:00
|
|
|
self.expect(":testserver CAP * LS :sasl=PLAIN server-time draft/chathistory").await?;
|
2024-04-21 21:00:44 +00:00
|
|
|
Ok(())
|
|
|
|
}
|
2023-10-13 14:30:31 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
struct TestServer {
|
2024-04-21 17:45:50 +00:00
|
|
|
core: LavinaCore,
|
2023-10-13 14:30:31 +00:00
|
|
|
server: RunningServer,
|
|
|
|
}
|
2024-05-28 19:13:47 +00:00
|
|
|
|
2023-10-13 14:30:31 +00:00
|
|
|
impl TestServer {
|
|
|
|
async fn start() -> Result<TestServer> {
|
2023-12-08 02:47:09 +00:00
|
|
|
let _ = tracing_subscriber::fmt::try_init();
|
2023-10-13 14:30:31 +00:00
|
|
|
let config = ServerConfig {
|
|
|
|
listen_on: "127.0.0.1:0".parse().unwrap(),
|
|
|
|
server_name: "testserver".into(),
|
|
|
|
};
|
2024-05-13 14:32:45 +00:00
|
|
|
let mut metrics = MetricsRegistry::new();
|
2024-05-10 21:50:34 +00:00
|
|
|
let storage = Storage::open(StorageConfig {
|
2023-10-13 14:30:31 +00:00
|
|
|
db_path: ":memory:".into(),
|
|
|
|
})
|
|
|
|
.await?;
|
2024-05-10 20:44:24 +00:00
|
|
|
let cluster_config = ClusterConfig {
|
|
|
|
addresses: vec![],
|
|
|
|
metadata: ClusterMetadata {
|
|
|
|
node_id: 0,
|
|
|
|
main_owner: 0,
|
|
|
|
rooms: Default::default(),
|
|
|
|
},
|
|
|
|
};
|
2024-05-13 14:32:45 +00:00
|
|
|
let core = LavinaCore::new(&mut metrics, cluster_config, storage).await?;
|
2024-05-03 22:37:49 +00:00
|
|
|
let server = launch(config, core.clone(), metrics.clone()).await.unwrap();
|
2024-05-13 14:32:45 +00:00
|
|
|
Ok(TestServer { core, server })
|
2024-04-15 09:06:10 +00:00
|
|
|
}
|
|
|
|
|
2024-04-21 17:45:50 +00:00
|
|
|
async fn reboot(self) -> Result<TestServer> {
|
2024-04-15 09:06:10 +00:00
|
|
|
let config = ServerConfig {
|
|
|
|
listen_on: "127.0.0.1:0".parse().unwrap(),
|
|
|
|
server_name: "testserver".into(),
|
|
|
|
};
|
2024-05-10 20:44:24 +00:00
|
|
|
let cluster_config = ClusterConfig {
|
|
|
|
addresses: vec![],
|
|
|
|
metadata: ClusterMetadata {
|
|
|
|
node_id: 0,
|
|
|
|
main_owner: 0,
|
|
|
|
rooms: Default::default(),
|
|
|
|
},
|
|
|
|
};
|
2024-05-13 14:32:45 +00:00
|
|
|
let TestServer { core, server } = self;
|
2024-04-15 09:06:10 +00:00
|
|
|
server.terminate().await?;
|
2024-05-13 14:32:45 +00:00
|
|
|
let storage = core.shutdown().await;
|
|
|
|
let mut metrics = MetricsRegistry::new();
|
|
|
|
let core = LavinaCore::new(&mut metrics, cluster_config, storage).await?;
|
2024-05-03 22:37:49 +00:00
|
|
|
let server = launch(config, core.clone(), metrics.clone()).await.unwrap();
|
2024-05-13 14:32:45 +00:00
|
|
|
Ok(TestServer { core, server })
|
2023-10-13 14:30:31 +00:00
|
|
|
}
|
2024-04-23 17:14:46 +00:00
|
|
|
|
2024-05-13 14:32:45 +00:00
|
|
|
async fn shutdown(self) {
|
|
|
|
let _ = self.server.terminate().await;
|
|
|
|
let storage = self.core.shutdown().await;
|
|
|
|
let _ = storage.close().await;
|
2024-04-23 17:14:46 +00:00
|
|
|
}
|
2023-10-09 11:35:41 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
#[tokio::test]
|
|
|
|
async fn scenario_basic() -> Result<()> {
|
2024-05-10 21:50:34 +00:00
|
|
|
let server = TestServer::start().await?;
|
2023-10-09 11:35:41 +00:00
|
|
|
|
|
|
|
// test scenario
|
|
|
|
|
2024-05-13 14:32:45 +00:00
|
|
|
server.core.create_player(&PlayerId::from("tester")?).await?;
|
|
|
|
server.core.set_password("tester", "password").await?;
|
2023-10-09 11:35:41 +00:00
|
|
|
|
2023-10-13 14:30:31 +00:00
|
|
|
let mut stream = TcpStream::connect(server.server.addr).await?;
|
2023-10-09 11:35:41 +00:00
|
|
|
let mut s = TestScope::new(&mut stream);
|
|
|
|
|
|
|
|
s.send("PASS password").await?;
|
|
|
|
s.send("NICK tester").await?;
|
|
|
|
s.send("USER UserName 0 * :Real Name").await?;
|
2024-04-06 22:34:11 +00:00
|
|
|
s.expect_server_introduction("tester").await?;
|
2023-12-08 02:47:09 +00:00
|
|
|
s.expect_nothing().await?;
|
|
|
|
s.send("QUIT :Leaving").await?;
|
|
|
|
s.expect(":testserver ERROR :Leaving the server").await?;
|
|
|
|
s.expect_eof().await?;
|
|
|
|
|
|
|
|
stream.shutdown().await?;
|
|
|
|
|
|
|
|
// wrap up
|
|
|
|
|
2024-05-13 14:32:45 +00:00
|
|
|
server.shutdown().await;
|
2023-12-08 02:47:09 +00:00
|
|
|
Ok(())
|
|
|
|
}
|
|
|
|
|
2024-05-28 19:13:47 +00:00
|
|
|
#[tokio::test]
|
|
|
|
async fn scenario_basic_with_chathistory() -> Result<()> {
|
|
|
|
let server = TestServer::start().await?;
|
|
|
|
|
|
|
|
// test scenario
|
|
|
|
|
|
|
|
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);
|
|
|
|
|
|
|
|
s.send("NICK tester").await?;
|
2024-05-30 22:22:34 +00:00
|
|
|
s.send("CAP REQ :draft/chathistory").await?;
|
|
|
|
s.send("USER UserName 0 * :Real Name").await?;
|
|
|
|
s.send("PASS password").await?;
|
2024-05-28 19:13:47 +00:00
|
|
|
s.send("USER UserName 0 * :Real Name").await?;
|
2024-05-30 22:22:34 +00:00
|
|
|
s.expect(":testserver CAP tester ACK :draft/chathistory").await?;
|
|
|
|
s.send("CAP END").await?;
|
|
|
|
|
2024-05-28 19:13:47 +00:00
|
|
|
s.expect_server_introduction("tester").await?;
|
|
|
|
s.expect_nothing().await?;
|
|
|
|
|
|
|
|
s.send("JOIN #test").await?;
|
|
|
|
s.expect(":tester JOIN #test").await?;
|
|
|
|
s.expect(":testserver 332 tester #test :New room").await?;
|
|
|
|
s.expect(":testserver 353 tester = #test :tester").await?;
|
|
|
|
s.expect(":testserver 366 tester #test :End of /NAMES list").await?;
|
2024-05-30 22:22:34 +00:00
|
|
|
|
2024-05-28 19:24:42 +00:00
|
|
|
s.send("PRIVMSG #test :Message1").await?;
|
|
|
|
s.send("PRIVMSG #test :Message2").await?;
|
|
|
|
s.send("PRIVMSG #test :Message3").await?;
|
|
|
|
s.send("PRIVMSG #test :Message4").await?;
|
|
|
|
|
|
|
|
s.send("CHATHISTORY LATEST #test * 1").await?;
|
|
|
|
s.expect(":tester PRIVMSG #test :Message4").await?;
|
|
|
|
|
|
|
|
s.send("CHATHISTORY LATEST #test * 3").await?;
|
|
|
|
s.expect(":tester PRIVMSG #test :Message2").await?;
|
|
|
|
s.expect(":tester PRIVMSG #test :Message3").await?;
|
|
|
|
s.expect(":tester PRIVMSG #test :Message4").await?;
|
|
|
|
s.expect_nothing().await?;
|
2024-05-28 19:13:47 +00:00
|
|
|
|
|
|
|
s.send("QUIT :Leaving").await?;
|
|
|
|
s.expect(":testserver ERROR :Leaving the server").await?;
|
|
|
|
s.expect_eof().await?;
|
|
|
|
|
|
|
|
stream.shutdown().await?;
|
|
|
|
|
|
|
|
// wrap up
|
|
|
|
|
|
|
|
server.shutdown().await;
|
|
|
|
Ok(())
|
|
|
|
}
|
|
|
|
|
2024-04-15 09:06:10 +00:00
|
|
|
#[tokio::test]
|
|
|
|
async fn scenario_join_and_reboot() -> Result<()> {
|
2024-05-10 21:50:34 +00:00
|
|
|
let server = TestServer::start().await?;
|
2024-04-15 09:06:10 +00:00
|
|
|
|
|
|
|
// test scenario
|
|
|
|
|
2024-05-13 14:32:45 +00:00
|
|
|
server.core.create_player(&PlayerId::from("tester")?).await?;
|
|
|
|
server.core.set_password("tester", "password").await?;
|
2024-04-15 09:06:10 +00:00
|
|
|
|
|
|
|
let mut stream = TcpStream::connect(server.server.addr).await?;
|
|
|
|
let mut s = TestScope::new(&mut stream);
|
|
|
|
|
|
|
|
// Open a connection and join a channel
|
|
|
|
|
|
|
|
s.send("PASS password").await?;
|
|
|
|
s.send("NICK tester").await?;
|
|
|
|
s.send("USER UserName 0 * :Real Name").await?;
|
|
|
|
s.expect_server_introduction("tester").await?;
|
|
|
|
s.expect_nothing().await?;
|
|
|
|
s.send("JOIN #test").await?;
|
|
|
|
s.expect(":tester JOIN #test").await?;
|
|
|
|
s.expect(":testserver 332 tester #test :New room").await?;
|
|
|
|
s.expect(":testserver 353 tester = #test :tester").await?;
|
|
|
|
s.expect(":testserver 366 tester #test :End of /NAMES list").await?;
|
|
|
|
s.send("PRIVMSG #test :Hello").await?;
|
|
|
|
s.send("QUIT :Leaving").await?;
|
|
|
|
s.expect(":testserver ERROR :Leaving the server").await?;
|
|
|
|
s.expect_eof().await?;
|
|
|
|
stream.shutdown().await?;
|
|
|
|
|
|
|
|
// Open a new connection and expect to be force-joined to the channel
|
|
|
|
|
|
|
|
let mut stream = TcpStream::connect(server.server.addr).await?;
|
|
|
|
let mut s = TestScope::new(&mut stream);
|
|
|
|
|
|
|
|
async fn test(s: &mut TestScope<'_>) -> Result<()> {
|
|
|
|
s.send("PASS password").await?;
|
|
|
|
s.send("NICK tester").await?;
|
|
|
|
s.send("USER UserName 0 * :Real Name").await?;
|
|
|
|
s.expect_server_introduction("tester").await?;
|
|
|
|
s.expect(":tester JOIN #test").await?;
|
|
|
|
s.expect(":testserver 332 tester #test :New room").await?;
|
|
|
|
s.expect(":testserver 353 tester = #test :tester").await?;
|
|
|
|
s.expect(":testserver 366 tester #test :End of /NAMES list").await?;
|
|
|
|
s.send("QUIT :Leaving").await?;
|
|
|
|
s.expect(":testserver ERROR :Leaving the server").await?;
|
|
|
|
s.expect_eof().await?;
|
|
|
|
Ok(())
|
|
|
|
}
|
|
|
|
test(&mut s).await?;
|
|
|
|
stream.shutdown().await?;
|
|
|
|
|
|
|
|
// Reboot the server
|
|
|
|
|
|
|
|
let server = server.reboot().await?;
|
|
|
|
|
|
|
|
// Open a new connection and expect to be force-joined to the channel
|
|
|
|
|
|
|
|
let mut stream = TcpStream::connect(server.server.addr).await?;
|
|
|
|
let mut s = TestScope::new(&mut stream);
|
|
|
|
|
|
|
|
test(&mut s).await?;
|
|
|
|
stream.shutdown().await?;
|
|
|
|
|
|
|
|
// wrap up
|
|
|
|
|
2024-05-13 14:32:45 +00:00
|
|
|
server.shutdown().await;
|
2024-04-15 09:06:10 +00:00
|
|
|
Ok(())
|
|
|
|
}
|
|
|
|
|
2024-04-06 22:34:11 +00:00
|
|
|
#[tokio::test]
|
|
|
|
async fn scenario_force_join_msg() -> Result<()> {
|
2024-05-10 21:50:34 +00:00
|
|
|
let server = TestServer::start().await?;
|
2024-04-06 22:34:11 +00:00
|
|
|
|
|
|
|
// test scenario
|
|
|
|
|
2024-05-13 14:32:45 +00:00
|
|
|
server.core.create_player(&PlayerId::from("tester")?).await?;
|
|
|
|
server.core.set_password("tester", "password").await?;
|
2024-04-06 22:34:11 +00:00
|
|
|
|
|
|
|
let mut stream1 = TcpStream::connect(server.server.addr).await?;
|
|
|
|
let mut s1 = TestScope::new(&mut stream1);
|
|
|
|
let mut stream2 = TcpStream::connect(server.server.addr).await?;
|
|
|
|
let mut s2 = TestScope::new(&mut stream2);
|
|
|
|
|
|
|
|
s1.send("PASS password").await?;
|
|
|
|
s1.send("NICK tester").await?;
|
|
|
|
s1.send("USER UserName 0 * :Real Name").await?;
|
|
|
|
s1.expect_server_introduction("tester").await?;
|
|
|
|
s1.expect_nothing().await?;
|
|
|
|
|
|
|
|
s2.send("PASS password").await?;
|
|
|
|
s2.send("NICK tester").await?;
|
|
|
|
s2.send("USER UserName 0 * :Real Name").await?;
|
|
|
|
s2.expect_server_introduction("tester").await?;
|
|
|
|
s2.expect_nothing().await?;
|
|
|
|
|
|
|
|
// We join the channel from the first connection
|
|
|
|
s1.send("JOIN #test").await?;
|
|
|
|
s1.expect(":tester JOIN #test").await?;
|
|
|
|
s1.expect(":testserver 332 tester #test :New room").await?;
|
|
|
|
s1.expect(":testserver 353 tester = #test :tester").await?;
|
|
|
|
s1.expect(":testserver 366 tester #test :End of /NAMES list").await?;
|
|
|
|
|
|
|
|
// And the second connection should receive the JOIN message (forced JOIN)
|
|
|
|
s2.expect(":tester JOIN #test").await?;
|
|
|
|
s2.expect(":testserver 332 tester #test :New room").await?;
|
|
|
|
s2.expect(":testserver 353 tester = #test :tester").await?;
|
|
|
|
s2.expect(":testserver 366 tester #test :End of /NAMES list").await?;
|
|
|
|
|
|
|
|
// We send a message to the channel from the second connection
|
|
|
|
s2.send("PRIVMSG #test :Hello").await?;
|
|
|
|
// We should not receive an acknowledgement from the server
|
|
|
|
s2.expect_nothing().await?;
|
|
|
|
// But we should receive this message from the first connection
|
|
|
|
s1.expect(":tester PRIVMSG #test :Hello").await?;
|
|
|
|
|
|
|
|
s1.send("QUIT :Leaving").await?;
|
|
|
|
s1.expect(":testserver ERROR :Leaving the server").await?;
|
|
|
|
s1.expect_eof().await?;
|
|
|
|
|
|
|
|
// Closing a connection does not kick you from the channel on a different connection
|
|
|
|
s2.expect_nothing().await?;
|
|
|
|
|
|
|
|
s2.send("QUIT :Leaving").await?;
|
|
|
|
s2.expect(":testserver ERROR :Leaving the server").await?;
|
|
|
|
s2.expect_eof().await?;
|
|
|
|
|
|
|
|
stream1.shutdown().await?;
|
|
|
|
stream2.shutdown().await?;
|
|
|
|
|
|
|
|
// wrap up
|
|
|
|
|
2024-05-13 14:32:45 +00:00
|
|
|
server.shutdown().await;
|
2024-04-06 22:34:11 +00:00
|
|
|
Ok(())
|
|
|
|
}
|
|
|
|
|
|
|
|
#[tokio::test]
|
|
|
|
async fn scenario_two_users() -> Result<()> {
|
2024-05-10 21:50:34 +00:00
|
|
|
let server = TestServer::start().await?;
|
2024-04-06 22:34:11 +00:00
|
|
|
|
|
|
|
// test scenario
|
|
|
|
|
2024-05-13 14:32:45 +00:00
|
|
|
server.core.create_player(&PlayerId::from("tester1")?).await?;
|
|
|
|
server.core.set_password("tester1", "password").await?;
|
|
|
|
server.core.create_player(&PlayerId::from("tester2")?).await?;
|
|
|
|
server.core.set_password("tester2", "password").await?;
|
2024-04-06 22:34:11 +00:00
|
|
|
|
|
|
|
let mut stream1 = TcpStream::connect(server.server.addr).await?;
|
|
|
|
let mut s1 = TestScope::new(&mut stream1);
|
|
|
|
let mut stream2 = TcpStream::connect(server.server.addr).await?;
|
|
|
|
let mut s2 = TestScope::new(&mut stream2);
|
|
|
|
|
|
|
|
s1.send("PASS password").await?;
|
|
|
|
s1.send("NICK tester1").await?;
|
|
|
|
s1.send("USER UserName 0 * :Real Name").await?;
|
|
|
|
s1.expect_server_introduction("tester1").await?;
|
|
|
|
s1.expect_nothing().await?;
|
|
|
|
|
|
|
|
s2.send("PASS password").await?;
|
|
|
|
s2.send("NICK tester2").await?;
|
|
|
|
s2.send("USER UserName 0 * :Real Name").await?;
|
|
|
|
s2.expect_server_introduction("tester2").await?;
|
|
|
|
s2.expect_nothing().await?;
|
|
|
|
|
|
|
|
// Join the channel from the first user
|
|
|
|
s1.send("JOIN #test").await?;
|
|
|
|
s1.expect(":tester1 JOIN #test").await?;
|
|
|
|
s1.expect(":testserver 332 tester1 #test :New room").await?;
|
|
|
|
s1.expect(":testserver 353 tester1 = #test :tester1").await?;
|
|
|
|
s1.expect(":testserver 366 tester1 #test :End of /NAMES list").await?;
|
|
|
|
// Then join the channel from the second user
|
|
|
|
s2.send("JOIN #test").await?;
|
|
|
|
s2.expect(":tester2 JOIN #test").await?;
|
|
|
|
s2.expect(":testserver 332 tester2 #test :New room").await?;
|
|
|
|
s2.expect_that(|msg| {
|
|
|
|
msg == ":testserver 353 tester2 = #test :tester1 tester2"
|
|
|
|
|| msg == ":testserver 353 tester2 = #test :tester2 tester1"
|
|
|
|
})
|
|
|
|
.await?;
|
|
|
|
s2.expect(":testserver 366 tester2 #test :End of /NAMES list").await?;
|
|
|
|
// The first user should receive the JOIN message from the second user
|
|
|
|
s1.expect(":tester2 JOIN #test").await?;
|
|
|
|
s1.expect_nothing().await?;
|
|
|
|
s2.expect_nothing().await?;
|
|
|
|
// Send a message from the second user
|
|
|
|
s2.send("PRIVMSG #test :Hello").await?;
|
|
|
|
// The first user should receive the message
|
|
|
|
s1.expect(":tester2 PRIVMSG #test :Hello").await?;
|
|
|
|
// Leave the channel from the first user
|
2024-04-06 23:01:24 +00:00
|
|
|
s1.send("PART #test").await?;
|
|
|
|
s1.expect(":tester1 PART #test").await?;
|
2024-04-06 22:34:11 +00:00
|
|
|
// The second user should receive the PART message
|
2024-04-06 23:01:24 +00:00
|
|
|
s2.expect(":tester1 PART #test").await?;
|
2024-04-23 17:14:46 +00:00
|
|
|
|
2024-05-05 17:21:40 +00:00
|
|
|
s1.send("WHOIS tester2").await?;
|
|
|
|
s1.expect(":testserver 318 tester1 tester2 :End of /WHOIS list").await?;
|
|
|
|
|
2024-04-23 17:14:46 +00:00
|
|
|
stream1.shutdown().await?;
|
2024-05-05 17:21:40 +00:00
|
|
|
s2.send("WHOIS tester3").await?;
|
|
|
|
s2.expect(":testserver 401 tester2 tester3 :No such nick/channel").await?;
|
|
|
|
s2.expect(":testserver 318 tester2 tester3 :End of /WHOIS list").await?;
|
|
|
|
|
2024-04-23 17:14:46 +00:00
|
|
|
stream2.shutdown().await?;
|
|
|
|
|
2024-05-13 14:32:45 +00:00
|
|
|
server.shutdown().await;
|
2024-04-06 22:34:11 +00:00
|
|
|
Ok(())
|
|
|
|
}
|
|
|
|
|
2023-12-08 02:47:09 +00:00
|
|
|
/*
|
|
|
|
IRC SASL doc: https://ircv3.net/specs/extensions/sasl-3.1.html
|
|
|
|
AUTHENTICATE doc: https://modern.ircdocs.horse/#authenticate-message
|
|
|
|
*/
|
|
|
|
#[tokio::test]
|
|
|
|
async fn scenario_cap_full_negotiation() -> Result<()> {
|
2024-05-10 21:50:34 +00:00
|
|
|
let server = TestServer::start().await?;
|
2023-12-08 02:47:09 +00:00
|
|
|
|
|
|
|
// test scenario
|
|
|
|
|
2024-05-13 14:32:45 +00:00
|
|
|
server.core.create_player(&PlayerId::from("tester")?).await?;
|
|
|
|
server.core.set_password("tester", "password").await?;
|
2023-12-08 02:47:09 +00:00
|
|
|
|
|
|
|
let mut stream = TcpStream::connect(server.server.addr).await?;
|
|
|
|
let mut s = TestScope::new(&mut stream);
|
|
|
|
|
|
|
|
s.send("CAP LS 302").await?;
|
|
|
|
s.send("NICK tester").await?;
|
|
|
|
s.send("USER UserName 0 * :Real Name").await?;
|
2024-04-21 21:00:44 +00:00
|
|
|
s.expect_cap_ls().await?;
|
2023-12-08 02:47:09 +00:00
|
|
|
s.send("CAP REQ :sasl").await?;
|
|
|
|
s.expect(":testserver CAP tester ACK :sasl").await?;
|
|
|
|
s.send("AUTHENTICATE PLAIN").await?;
|
|
|
|
s.expect(":testserver AUTHENTICATE +").await?;
|
|
|
|
s.send("AUTHENTICATE dGVzdGVyAHRlc3RlcgBwYXNzd29yZA==").await?; // base64-encoded 'tester\x00tester\x00password'
|
|
|
|
s.expect(":testserver 900 tester tester tester :You are now logged in as tester").await?;
|
|
|
|
s.expect(":testserver 903 tester :SASL authentication successful").await?;
|
|
|
|
|
|
|
|
s.send("CAP END").await?;
|
|
|
|
|
2024-04-06 22:34:11 +00:00
|
|
|
s.expect_server_introduction("tester").await?;
|
2023-12-08 02:47:09 +00:00
|
|
|
s.expect_nothing().await?;
|
|
|
|
s.send("QUIT :Leaving").await?;
|
|
|
|
s.expect(":testserver ERROR :Leaving the server").await?;
|
|
|
|
s.expect_eof().await?;
|
|
|
|
|
|
|
|
stream.shutdown().await?;
|
|
|
|
|
|
|
|
// wrap up
|
|
|
|
|
2024-05-13 14:32:45 +00:00
|
|
|
server.shutdown().await;
|
2023-12-08 02:47:09 +00:00
|
|
|
Ok(())
|
|
|
|
}
|
|
|
|
|
2024-04-16 11:35:14 +00:00
|
|
|
#[tokio::test]
|
|
|
|
async fn scenario_cap_full_negotiation_nick_last() -> Result<()> {
|
2024-05-10 21:50:34 +00:00
|
|
|
let server = TestServer::start().await?;
|
2024-04-16 11:35:14 +00:00
|
|
|
|
|
|
|
// test scenario
|
|
|
|
|
2024-05-13 14:32:45 +00:00
|
|
|
server.core.create_player(&PlayerId::from("tester")?).await?;
|
|
|
|
server.core.set_password("tester", "password").await?;
|
2024-04-16 11:35:14 +00:00
|
|
|
|
|
|
|
let mut stream = TcpStream::connect(server.server.addr).await?;
|
|
|
|
let mut s = TestScope::new(&mut stream);
|
|
|
|
|
|
|
|
s.send("CAP LS 302").await?;
|
2024-04-21 21:00:44 +00:00
|
|
|
s.expect_cap_ls().await?;
|
2024-04-16 11:35:14 +00:00
|
|
|
s.send("CAP REQ :sasl").await?;
|
|
|
|
s.expect(":testserver CAP * ACK :sasl").await?;
|
|
|
|
s.send("AUTHENTICATE PLAIN").await?;
|
|
|
|
s.expect(":testserver AUTHENTICATE +").await?;
|
|
|
|
s.send("AUTHENTICATE dGVzdGVyAHRlc3RlcgBwYXNzd29yZA==").await?; // base64-encoded 'tester\x00tester\x00password'
|
|
|
|
s.expect(":testserver 900 tester tester tester :You are now logged in as tester").await?;
|
|
|
|
s.expect(":testserver 903 tester :SASL authentication successful").await?;
|
|
|
|
s.send("CAP END").await?;
|
|
|
|
s.send("USER UserName 0 * :Real Name").await?;
|
|
|
|
s.send("NICK tester").await?;
|
|
|
|
|
|
|
|
s.expect_server_introduction("tester").await?;
|
|
|
|
s.expect_nothing().await?;
|
|
|
|
s.send("QUIT :Leaving").await?;
|
|
|
|
s.expect(":testserver ERROR :Leaving the server").await?;
|
|
|
|
s.expect_eof().await?;
|
|
|
|
|
|
|
|
stream.shutdown().await?;
|
|
|
|
|
|
|
|
// wrap up
|
|
|
|
|
2024-05-13 14:32:45 +00:00
|
|
|
server.shutdown().await;
|
2024-04-16 11:35:14 +00:00
|
|
|
Ok(())
|
|
|
|
}
|
|
|
|
|
2023-12-08 02:47:09 +00:00
|
|
|
#[tokio::test]
|
|
|
|
async fn scenario_cap_short_negotiation() -> Result<()> {
|
2024-05-10 21:50:34 +00:00
|
|
|
let server = TestServer::start().await?;
|
2023-12-08 02:47:09 +00:00
|
|
|
|
|
|
|
// test scenario
|
|
|
|
|
2024-05-13 14:32:45 +00:00
|
|
|
server.core.create_player(&PlayerId::from("tester")?).await?;
|
|
|
|
server.core.set_password("tester", "password").await?;
|
2023-12-08 02:47:09 +00:00
|
|
|
|
|
|
|
let mut stream = TcpStream::connect(server.server.addr).await?;
|
|
|
|
let mut s = TestScope::new(&mut stream);
|
|
|
|
|
|
|
|
s.send("NICK tester").await?;
|
|
|
|
s.send("CAP REQ :sasl").await?;
|
|
|
|
s.send("USER UserName 0 * :Real Name").await?;
|
|
|
|
s.expect(":testserver CAP tester ACK :sasl").await?;
|
|
|
|
s.send("AUTHENTICATE PLAIN").await?;
|
|
|
|
s.expect(":testserver AUTHENTICATE +").await?;
|
|
|
|
s.send("AUTHENTICATE dGVzdGVyAHRlc3RlcgBwYXNzd29yZA==").await?; // base64-encoded 'tester\x00tester\x00password'
|
|
|
|
s.expect(":testserver 900 tester tester tester :You are now logged in as tester").await?;
|
|
|
|
s.expect(":testserver 903 tester :SASL authentication successful").await?;
|
|
|
|
|
|
|
|
s.send("CAP END").await?;
|
|
|
|
|
2024-04-06 22:34:11 +00:00
|
|
|
s.expect_server_introduction("tester").await?;
|
2023-10-13 14:30:31 +00:00
|
|
|
s.expect_nothing().await?;
|
|
|
|
s.send("QUIT :Leaving").await?;
|
|
|
|
s.expect(":testserver ERROR :Leaving the server").await?;
|
|
|
|
s.expect_eof().await?;
|
2023-10-09 11:35:41 +00:00
|
|
|
|
|
|
|
stream.shutdown().await?;
|
|
|
|
|
|
|
|
// wrap up
|
|
|
|
|
2024-05-13 14:32:45 +00:00
|
|
|
server.shutdown().await;
|
2023-10-09 11:35:41 +00:00
|
|
|
Ok(())
|
|
|
|
}
|
2024-02-06 23:08:14 +00:00
|
|
|
|
|
|
|
#[tokio::test]
|
|
|
|
async fn scenario_cap_sasl_fail() -> Result<()> {
|
2024-05-10 21:50:34 +00:00
|
|
|
let server = TestServer::start().await?;
|
2024-02-06 23:08:14 +00:00
|
|
|
|
|
|
|
// test scenario
|
|
|
|
|
2024-05-13 14:32:45 +00:00
|
|
|
server.core.create_player(&PlayerId::from("tester")?).await?;
|
|
|
|
server.core.set_password("tester", "password").await?;
|
2024-02-06 23:08:14 +00:00
|
|
|
|
|
|
|
let mut stream = TcpStream::connect(server.server.addr).await?;
|
|
|
|
let mut s = TestScope::new(&mut stream);
|
|
|
|
|
|
|
|
s.send("CAP LS 302").await?;
|
|
|
|
s.send("NICK tester").await?;
|
|
|
|
s.send("USER UserName 0 * :Real Name").await?;
|
2024-04-21 21:00:44 +00:00
|
|
|
s.expect_cap_ls().await?;
|
2024-02-06 23:08:14 +00:00
|
|
|
s.send("CAP REQ :sasl").await?;
|
|
|
|
s.expect(":testserver CAP tester ACK :sasl").await?;
|
|
|
|
s.send("AUTHENTICATE SHA256").await?;
|
|
|
|
s.expect(":testserver 904 tester :Unsupported mechanism").await?;
|
|
|
|
s.send("AUTHENTICATE PLAIN").await?;
|
|
|
|
s.expect(":testserver AUTHENTICATE +").await?;
|
|
|
|
s.send("AUTHENTICATE dGVzdGVyAHRlc3RlcgBwYXNzd29yZDE=").await?;
|
|
|
|
s.expect(":testserver 904 tester :Bad credentials").await?;
|
|
|
|
s.send("AUTHENTICATE dGVzdGVyAHRlc3RlcgBwYXNzd29yZA==").await?; // base64-encoded 'tester\x00tester\x00password'
|
|
|
|
s.expect(":testserver 900 tester tester tester :You are now logged in as tester").await?;
|
|
|
|
s.expect(":testserver 903 tester :SASL authentication successful").await?;
|
|
|
|
|
|
|
|
s.send("CAP END").await?;
|
|
|
|
|
2024-04-06 22:34:11 +00:00
|
|
|
s.expect_server_introduction("tester").await?;
|
2024-02-06 23:08:14 +00:00
|
|
|
s.expect_nothing().await?;
|
|
|
|
s.send("QUIT :Leaving").await?;
|
|
|
|
s.expect(":testserver ERROR :Leaving the server").await?;
|
|
|
|
s.expect_eof().await?;
|
|
|
|
|
|
|
|
stream.shutdown().await?;
|
|
|
|
|
|
|
|
// wrap up
|
|
|
|
|
2024-05-13 14:32:45 +00:00
|
|
|
server.shutdown().await;
|
2024-02-06 23:08:14 +00:00
|
|
|
Ok(())
|
|
|
|
}
|
2024-02-18 16:46:29 +00:00
|
|
|
|
|
|
|
#[tokio::test]
|
|
|
|
async fn terminate_socket_scenario() -> Result<()> {
|
2024-05-10 21:50:34 +00:00
|
|
|
let server = TestServer::start().await?;
|
2024-02-18 16:46:29 +00:00
|
|
|
|
|
|
|
// test scenario
|
|
|
|
|
2024-05-13 14:32:45 +00:00
|
|
|
server.core.create_player(&PlayerId::from("tester")?).await?;
|
|
|
|
server.core.set_password("tester", "password").await?;
|
2024-02-18 16:46:29 +00:00
|
|
|
|
|
|
|
let mut stream = TcpStream::connect(server.server.addr).await?;
|
|
|
|
let mut s = TestScope::new(&mut stream);
|
|
|
|
|
|
|
|
s.send("NICK tester").await?;
|
|
|
|
s.send("CAP REQ :sasl").await?;
|
|
|
|
s.send("USER UserName 0 * :Real Name").await?;
|
|
|
|
s.expect(":testserver CAP tester ACK :sasl").await?;
|
|
|
|
s.send("AUTHENTICATE PLAIN").await?;
|
|
|
|
s.expect(":testserver AUTHENTICATE +").await?;
|
|
|
|
|
2024-05-13 14:32:45 +00:00
|
|
|
server.shutdown().await;
|
2024-02-18 16:46:29 +00:00
|
|
|
assert_eq!(stream.read_u8().await.unwrap_err().kind(), ErrorKind::UnexpectedEof);
|
|
|
|
|
|
|
|
Ok(())
|
|
|
|
}
|
2024-04-21 21:00:44 +00:00
|
|
|
|
|
|
|
#[tokio::test]
|
|
|
|
async fn server_time_capability() -> Result<()> {
|
2024-05-10 21:50:34 +00:00
|
|
|
let server = TestServer::start().await?;
|
2024-04-21 21:00:44 +00:00
|
|
|
|
|
|
|
// test scenario
|
|
|
|
|
2024-05-13 14:32:45 +00:00
|
|
|
server.core.create_player(&PlayerId::from("tester")?).await?;
|
|
|
|
server.core.set_password("tester", "password").await?;
|
2024-04-21 21:00:44 +00:00
|
|
|
|
|
|
|
let mut stream = TcpStream::connect(server.server.addr).await?;
|
|
|
|
let mut s = TestScope::new(&mut stream);
|
|
|
|
|
|
|
|
s.send("CAP LS 302").await?;
|
|
|
|
s.send("NICK tester").await?;
|
|
|
|
s.send("USER UserName 0 * :Real Name").await?;
|
|
|
|
s.expect_cap_ls().await?;
|
|
|
|
s.send("CAP REQ :sasl server-time").await?;
|
|
|
|
s.expect(":testserver CAP tester ACK :sasl server-time").await?;
|
|
|
|
s.send("AUTHENTICATE PLAIN").await?;
|
|
|
|
s.expect(":testserver AUTHENTICATE +").await?;
|
|
|
|
s.send("AUTHENTICATE dGVzdGVyAHRlc3RlcgBwYXNzd29yZA==").await?; // base64-encoded 'tester\x00tester\x00password'
|
|
|
|
s.expect(":testserver 900 tester tester tester :You are now logged in as tester").await?;
|
|
|
|
s.expect(":testserver 903 tester :SASL authentication successful").await?;
|
|
|
|
s.send("CAP END").await?;
|
|
|
|
s.expect_server_introduction("tester").await?;
|
|
|
|
s.expect_nothing().await?;
|
|
|
|
s.send("JOIN #test").await?;
|
|
|
|
s.expect(":tester JOIN #test").await?;
|
|
|
|
s.expect(":testserver 332 tester #test :New room").await?;
|
|
|
|
s.expect(":testserver 353 tester = #test :tester").await?;
|
|
|
|
s.expect(":testserver 366 tester #test :End of /NAMES list").await?;
|
|
|
|
|
2024-05-13 14:32:45 +00:00
|
|
|
server.core.create_player(&PlayerId::from("some_guy")?).await?;
|
|
|
|
let mut conn = server.core.connect_to_player(&PlayerId::from("some_guy").unwrap()).await;
|
2024-05-26 11:20:26 +00:00
|
|
|
let res = conn.join_room(RoomId::try_from("test").unwrap()).await?;
|
2024-04-21 21:00:44 +00:00
|
|
|
let JoinResult::Success(_) = res else {
|
|
|
|
panic!("Failed to join room");
|
|
|
|
};
|
|
|
|
|
|
|
|
s.expect(":some_guy JOIN #test").await?;
|
|
|
|
|
2024-05-26 11:20:26 +00:00
|
|
|
let SendMessageResult::Success(res) = conn.send_message(RoomId::try_from("test").unwrap(), "Hello".into()).await?
|
2024-04-21 21:00:44 +00:00
|
|
|
else {
|
|
|
|
panic!("Failed to send message");
|
|
|
|
};
|
|
|
|
s.expect(&format!(
|
|
|
|
"@time={} :some_guy PRIVMSG #test :Hello",
|
|
|
|
res.to_rfc3339_opts(SecondsFormat::Millis, true)
|
|
|
|
))
|
|
|
|
.await?;
|
|
|
|
|
|
|
|
// formatting check
|
|
|
|
assert_eq!(
|
|
|
|
DateTime::parse_from_rfc3339(&"2024-01-01T10:00:32.123Z").unwrap().to_rfc3339_opts(SecondsFormat::Millis, true),
|
|
|
|
"2024-01-01T10:00:32.123Z"
|
|
|
|
);
|
|
|
|
|
|
|
|
s.send("QUIT :Leaving").await?;
|
|
|
|
s.expect(":testserver ERROR :Leaving the server").await?;
|
|
|
|
s.expect_eof().await?;
|
|
|
|
|
|
|
|
stream.shutdown().await?;
|
|
|
|
|
|
|
|
// wrap up
|
|
|
|
|
2024-05-13 14:32:45 +00:00
|
|
|
server.shutdown().await;
|
2024-04-21 21:00:44 +00:00
|
|
|
Ok(())
|
|
|
|
}
|
2024-04-23 16:26:40 +00:00
|
|
|
|
|
|
|
#[tokio::test]
|
|
|
|
async fn scenario_two_players_dialog() -> Result<()> {
|
2024-05-10 21:50:34 +00:00
|
|
|
let server = TestServer::start().await?;
|
2024-04-23 16:26:40 +00:00
|
|
|
|
|
|
|
// test scenario
|
|
|
|
|
2024-05-13 14:32:45 +00:00
|
|
|
server.core.create_player(&PlayerId::from("tester1")?).await?;
|
|
|
|
server.core.set_password("tester1", "password").await?;
|
|
|
|
server.core.create_player(&PlayerId::from("tester2")?).await?;
|
|
|
|
server.core.set_password("tester2", "password").await?;
|
2024-04-23 16:26:40 +00:00
|
|
|
|
|
|
|
let mut stream1 = TcpStream::connect(server.server.addr).await?;
|
|
|
|
let mut s1 = TestScope::new(&mut stream1);
|
|
|
|
|
|
|
|
let mut stream2 = TcpStream::connect(server.server.addr).await?;
|
|
|
|
let mut s2 = TestScope::new(&mut stream2);
|
|
|
|
|
|
|
|
s1.send("CAP LS 302").await?;
|
|
|
|
s1.send("NICK tester1").await?;
|
|
|
|
s1.send("USER UserName 0 * :Real Name").await?;
|
|
|
|
s1.expect_cap_ls().await?;
|
|
|
|
s1.send("CAP REQ :sasl").await?;
|
|
|
|
s1.expect(":testserver CAP tester1 ACK :sasl").await?;
|
|
|
|
s1.send("AUTHENTICATE PLAIN").await?;
|
|
|
|
s1.expect(":testserver AUTHENTICATE +").await?;
|
|
|
|
s1.send("AUTHENTICATE dGVzdGVyMQB0ZXN0ZXIxAHBhc3N3b3Jk").await?; // base64-encoded 'tester1\x00tester1\x00password'
|
|
|
|
s1.expect(":testserver 900 tester1 tester1 tester1 :You are now logged in as tester1").await?;
|
|
|
|
s1.expect(":testserver 903 tester1 :SASL authentication successful").await?;
|
|
|
|
s1.send("CAP END").await?;
|
|
|
|
s1.expect_server_introduction("tester1").await?;
|
|
|
|
s1.expect_nothing().await?;
|
|
|
|
|
|
|
|
s2.send("CAP LS 302").await?;
|
|
|
|
s2.send("NICK tester2").await?;
|
|
|
|
s2.send("USER UserName 0 * :Real Name").await?;
|
|
|
|
s2.expect_cap_ls().await?;
|
|
|
|
s2.send("CAP REQ :sasl").await?;
|
|
|
|
s2.expect(":testserver CAP tester2 ACK :sasl").await?;
|
|
|
|
s2.send("AUTHENTICATE PLAIN").await?;
|
|
|
|
s2.expect(":testserver AUTHENTICATE +").await?;
|
|
|
|
s2.send("AUTHENTICATE dGVzdGVyMgB0ZXN0ZXIyAHBhc3N3b3Jk").await?; // base64-encoded 'tester2\x00tester2\x00password'
|
|
|
|
s2.expect(":testserver 900 tester2 tester2 tester2 :You are now logged in as tester2").await?;
|
|
|
|
s2.expect(":testserver 903 tester2 :SASL authentication successful").await?;
|
|
|
|
s2.send("CAP END").await?;
|
|
|
|
s2.expect_server_introduction("tester2").await?;
|
|
|
|
s2.expect_nothing().await?;
|
|
|
|
|
|
|
|
s1.send("PRIVMSG tester2 :Henlo! How are you?").await?;
|
|
|
|
s1.expect_nothing().await?;
|
|
|
|
s2.expect(":tester1 PRIVMSG tester2 :Henlo! How are you?").await?;
|
|
|
|
s2.expect_nothing().await?;
|
|
|
|
|
|
|
|
s2.send("PRIVMSG tester1 good").await?;
|
|
|
|
s2.expect_nothing().await?;
|
|
|
|
s1.expect(":tester2 PRIVMSG tester1 :good").await?;
|
|
|
|
s1.expect_nothing().await?;
|
|
|
|
|
2024-04-23 17:14:46 +00:00
|
|
|
stream1.shutdown().await?;
|
|
|
|
stream2.shutdown().await?;
|
|
|
|
|
2024-05-13 14:32:45 +00:00
|
|
|
server.shutdown().await;
|
2024-04-23 17:14:46 +00:00
|
|
|
|
2024-04-23 16:26:40 +00:00
|
|
|
Ok(())
|
|
|
|
}
|