use std::io::ErrorKind; use std::net::SocketAddr; use std::time::Duration; use anyhow::{anyhow, Result}; use prometheus::Registry as MetricsRegistry; use tokio::io::{AsyncReadExt, AsyncWriteExt, BufReader}; use tokio::net::tcp::{ReadHalf, WriteHalf}; use tokio::net::TcpStream; use lavina_core::repo::{Storage, StorageConfig}; use lavina_core::{player::PlayerRegistry, room::RoomRegistry}; use projection_irc::{launch, read_irc_message, RunningServer, ServerConfig}; struct TestScope<'a> { reader: BufReader>, writer: WriteHalf<'a>, buffer: Vec, 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![]; let timeout = Duration::from_millis(100); TestScope { reader, writer, buffer, timeout, } } async fn send(&mut self, str: &(impl AsRef + ?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<()> { tracing::debug!("Expecting {}", str); 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(()) } 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(()), } } } struct TestServer { metrics: MetricsRegistry, storage: Storage, rooms: RoomRegistry, players: PlayerRegistry, server: RunningServer, } impl TestServer { async fn start() -> Result { let _ = tracing_subscriber::fmt::try_init(); let config = ServerConfig { listen_on: "127.0.0.1:0".parse().unwrap(), server_name: "testserver".into(), }; let mut metrics = MetricsRegistry::new(); let mut storage = Storage::open(StorageConfig { db_path: ":memory:".into(), }) .await?; let rooms = RoomRegistry::new(&mut metrics, storage.clone()).unwrap(); let players = PlayerRegistry::empty(rooms.clone(), &mut metrics).unwrap(); let server = launch(config, players.clone(), rooms.clone(), metrics.clone(), storage.clone()).await.unwrap(); Ok(TestServer { metrics, storage, rooms, players, server, }) } } #[tokio::test] async fn scenario_basic() -> Result<()> { let mut server = TestServer::start().await?; // test scenario server.storage.create_user("tester").await?; server.storage.set_password("tester", "password").await?; let mut stream = TcpStream::connect(server.server.addr).await?; 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?; s.expect(":testserver 001 tester :Welcome to testserver Server").await?; s.expect(":testserver 002 tester :Welcome to testserver Server").await?; s.expect(":testserver 003 tester :Welcome to testserver Server").await?; s.expect(":testserver 004 tester testserver lavina_0.0.2-dev r CFILPQbcefgijklmnopqrstvz").await?; s.expect(":testserver 005 tester CHANTYPES=# :are supported by this server").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 server.server.terminate().await?; Ok(()) } /* 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<()> { let mut server = TestServer::start().await?; // test scenario server.storage.create_user("tester").await?; server.storage.set_password("tester", "password").await?; 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(":testserver CAP * LS :sasl=PLAIN").await?; 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?; s.expect(":testserver 001 tester :Welcome to testserver Server").await?; s.expect(":testserver 002 tester :Welcome to testserver Server").await?; s.expect(":testserver 003 tester :Welcome to testserver Server").await?; s.expect(":testserver 004 tester testserver lavina_0.0.2-dev r CFILPQbcefgijklmnopqrstvz").await?; s.expect(":testserver 005 tester CHANTYPES=# :are supported by this server").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 server.server.terminate().await?; Ok(()) } #[tokio::test] async fn scenario_cap_short_negotiation() -> Result<()> { let mut server = TestServer::start().await?; // test scenario server.storage.create_user("tester").await?; server.storage.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?; 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?; s.expect(":testserver 001 tester :Welcome to testserver Server").await?; s.expect(":testserver 002 tester :Welcome to testserver Server").await?; s.expect(":testserver 003 tester :Welcome to testserver Server").await?; s.expect(":testserver 004 tester testserver lavina_0.0.2-dev r CFILPQbcefgijklmnopqrstvz").await?; s.expect(":testserver 005 tester CHANTYPES=# :are supported by this server").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 server.server.terminate().await?; Ok(()) } #[tokio::test] async fn scenario_cap_sasl_fail() -> Result<()> { let mut server = TestServer::start().await?; // test scenario server.storage.create_user("tester").await?; server.storage.set_password("tester", "password").await?; 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(":testserver CAP * LS :sasl=PLAIN").await?; 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?; s.expect(":testserver 001 tester :Welcome to testserver Server").await?; s.expect(":testserver 002 tester :Welcome to testserver Server").await?; s.expect(":testserver 003 tester :Welcome to testserver Server").await?; s.expect(":testserver 004 tester testserver lavina_0.0.2-dev r CFILPQbcefgijklmnopqrstvz").await?; s.expect(":testserver 005 tester CHANTYPES=# :are supported by this server").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 server.server.terminate().await?; Ok(()) } #[tokio::test] async fn terminate_socket_scenario() -> Result<()> { let mut server = TestServer::start().await?; let address: SocketAddr = ("127.0.0.1:0".parse().unwrap()); // test scenario server.storage.create_user("tester").await?; server.storage.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?; 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?; server.server.terminate().await?; assert_eq!(stream.read_u8().await.unwrap_err().kind(), ErrorKind::UnexpectedEof); Ok(()) }