diff --git a/crates/projection-irc/tests/lib.rs b/crates/projection-irc/tests/lib.rs index c3efd2a..240a067 100644 --- a/crates/projection-irc/tests/lib.rs +++ b/crates/projection-irc/tests/lib.rs @@ -48,6 +48,31 @@ impl<'a> TestScope<'a> { Ok(()) } + 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(()) + } + 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??; @@ -113,18 +138,7 @@ async fn scenario_basic() -> Result<()> { 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( - format!( - ":testserver 004 tester testserver {} r CFILPQbcefgijklmnopqrstvz", - &APP_VERSION - ) - .as_str(), - ) - .await?; - s.expect(":testserver 005 tester CHANTYPES=# :are supported by this server").await?; + s.expect_server_introduction("tester").await?; s.expect_nothing().await?; s.send("QUIT :Leaving").await?; s.expect(":testserver ERROR :Leaving the server").await?; @@ -138,6 +152,133 @@ async fn scenario_basic() -> Result<()> { Ok(()) } +#[tokio::test] +async fn scenario_force_join_msg() -> Result<()> { + let mut server = TestServer::start().await?; + + // test scenario + + server.storage.create_user("tester").await?; + server.storage.set_password("tester", "password").await?; + + 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 + + server.server.terminate().await?; + Ok(()) +} + +#[tokio::test] +async fn scenario_two_users() -> Result<()> { + let mut server = TestServer::start().await?; + + // test scenario + + server.storage.create_user("tester1").await?; + server.storage.set_password("tester1", "password").await?; + server.storage.create_user("tester2").await?; + server.storage.set_password("tester2", "password").await?; + + 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 + // TODO implement irc PART command + // s1.send("PART #test").await?; + // s1.expect(":tester1 PART #test").await?; + // The second user should receive the PART message + // s2.expect(":tester1 PART #test").await?; + Ok(()) +} + /* IRC SASL doc: https://ircv3.net/specs/extensions/sasl-3.1.html AUTHENTICATE doc: https://modern.ircdocs.horse/#authenticate-message @@ -168,18 +309,7 @@ async fn scenario_cap_full_negotiation() -> Result<()> { 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( - format!( - ":testserver 004 tester testserver {} r CFILPQbcefgijklmnopqrstvz", - &APP_VERSION - ) - .as_str(), - ) - .await?; - s.expect(":testserver 005 tester CHANTYPES=# :are supported by this server").await?; + s.expect_server_introduction("tester").await?; s.expect_nothing().await?; s.send("QUIT :Leaving").await?; s.expect(":testserver ERROR :Leaving the server").await?; @@ -217,18 +347,7 @@ async fn scenario_cap_short_negotiation() -> Result<()> { 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( - format!( - ":testserver 004 tester testserver {} r CFILPQbcefgijklmnopqrstvz", - &APP_VERSION - ) - .as_str(), - ) - .await?; - s.expect(":testserver 005 tester CHANTYPES=# :are supported by this server").await?; + s.expect_server_introduction("tester").await?; s.expect_nothing().await?; s.send("QUIT :Leaving").await?; s.expect(":testserver ERROR :Leaving the server").await?; @@ -272,18 +391,7 @@ async fn scenario_cap_sasl_fail() -> Result<()> { 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( - format!( - ":testserver 004 tester testserver {} r CFILPQbcefgijklmnopqrstvz", - &APP_VERSION - ) - .as_str(), - ) - .await?; - s.expect(":testserver 005 tester CHANTYPES=# :are supported by this server").await?; + s.expect_server_introduction("tester").await?; s.expect_nothing().await?; s.send("QUIT :Leaving").await?; s.expect(":testserver ERROR :Leaving the server").await?; diff --git a/crates/proto-irc/src/server.rs b/crates/proto-irc/src/server.rs index 6e1bd66..c751e23 100644 --- a/crates/proto-irc/src/server.rs +++ b/crates/proto-irc/src/server.rs @@ -317,10 +317,15 @@ impl ServerMessageBody { writer.write_all(b" = ").await?; chan.write_async(writer).await?; writer.write_all(b" :").await?; - for member in members { + { + let member = &members.head; writer.write_all(member.prefix.to_string().as_bytes()).await?; writer.write_all(member.nick.as_bytes()).await?; + } + for member in &members.tail { writer.write_all(b" ").await?; + writer.write_all(member.prefix.to_string().as_bytes()).await?; + writer.write_all(member.nick.as_bytes()).await?; } } ServerMessageBody::N366NamesReplyEnd { client, chan } => {