Compare commits

...

12 commits

Author SHA1 Message Date
ad5ece8ad5
TestComponent: assert closer to callsite
Attempts to assert closer to callsite to make it easier to debug. This
requires that we also pay attention to remaining items in the
expect_buffer. This check is done on Drop.

Signed-off-by: Maxime “pep” Buquet <pep@bouah.net>
2022-09-23 17:55:10 +02:00
e338bff920 CI: initial tests
Signed-off-by: Maxime “pep” Buquet <pep@bouah.net>
2022-09-22 18:40:50 +02:00
db7237069c tests: Split into submodules
Signed-off-by: Maxime “pep” Buquet <pep@bouah.net>
2022-09-22 18:40:50 +02:00
7a687df552 Support Multi-Session Nicks joins
Signed-off-by: Maxime “pep” Buquet <pep@bouah.net>
2022-09-22 18:40:50 +02:00
919f3cf754 room: split test_broadcast_presence
Signed-off-by: Maxime “pep” Buquet <pep@bouah.net>
2022-09-22 18:40:50 +02:00
4ec1aa42b4 Room::broadcast_presence: change parameters again
Signed-off-by: Maxime “pep” Buquet <pep@bouah.net>
2022-09-22 18:40:50 +02:00
e7d31e41dc Expect: add description alongside callback
Signed-off-by: Maxime “pep” Buquet <pep@bouah.net>
2022-09-22 18:40:50 +02:00
562aadb488
Room: simplify add_session; abstract away send_subject
Signed-off-by: Maxime “pep” Buquet <pep@bouah.net>
2022-09-18 14:07:53 +02:00
44bec0e232
Add test_presence_resync
Signed-off-by: Maxime “pep” Buquet <pep@bouah.net>
2022-09-18 12:12:27 +02:00
c884f38dc2
Add broadcast_presence method
Also add bits of the next commit because there are too many changes now
and I failed to properly dissociate with add -p :(

Signed-off-by: Maxime “pep” Buquet <pep@bouah.net>
2022-09-18 11:45:04 +02:00
7554ff4d7c
component: add TestElement to have a custom Debug for Element
Signed-off-by: Maxime “pep” Buquet <pep@bouah.net>
2022-09-17 22:17:26 +02:00
ef03c1b032
Return SessionAlreadyExists error in Occupant::add_session
Signed-off-by: Maxime “pep” Buquet <pep@bouah.net>
2022-09-17 16:53:33 +02:00
9 changed files with 965 additions and 272 deletions

11
.woodpecker.yml Normal file
View file

@ -0,0 +1,11 @@
---
pipeline:
lint:
image: rustlang/rust:nightly-alpine
commands:
- cargo fmt --check
test:
image: rustlang/rust:nightly-alpine
commands:
- RUST_BACKTRACE=1 cargo test

View file

@ -13,13 +13,13 @@ file.
### XMPP ### XMPP
- [ ] Join - [x] Join
* [x] Single session * [x] Normal sessions
* [ ] Multiple sessions non-MSN * [x] MSN
* [ ] MSN
- [ ] Presence - [ ] Presence
* [ ] Probes * [x] Updates
* [ ] Resync * [x] Resync
* [ ] Probes (storing updates to answer probes)
- [ ] Iq - [ ] Iq
* [ ] Ping answers * [ ] Ping answers
* [ ] Ping probes? * [ ] Ping probes?

View file

@ -12,6 +12,7 @@ use std::marker::Send;
use std::ops::{Deref, DerefMut}; use std::ops::{Deref, DerefMut};
use std::pin::Pin; use std::pin::Pin;
use std::task::Context; use std::task::Context;
use std::thread;
use async_trait::async_trait; use async_trait::async_trait;
use futures::{task::Poll, Stream}; use futures::{task::Poll, Stream};
@ -78,10 +79,14 @@ impl Component {
} }
enum Expect { enum Expect {
Element(Element), /// Simple Element
Iq(Box<dyn FnOnce(Iq) + Send + 'static>), Element(TestElement),
Presence(Box<dyn FnOnce(Presence) + Send + 'static>), /// Callback taking an Iq, with a description alongside
Message(Box<dyn FnOnce(Message) + Send + 'static>), Iq(Box<dyn FnOnce(Iq) + Send + 'static>, String),
/// Callback taking a Presence, with a description alongside
Presence(Box<dyn FnOnce(Presence) + Send + 'static>, String),
/// Callback taking a Message, with a description alongside
Message(Box<dyn FnOnce(Message) + Send + 'static>, String),
} }
impl fmt::Debug for Expect { impl fmt::Debug for Expect {
@ -89,75 +94,138 @@ impl fmt::Debug for Expect {
write!(f, "Expect::")?; write!(f, "Expect::")?;
match self { match self {
Expect::Element(el) => write!(f, "Element({:?})", String::from(el)), Expect::Element(el) => write!(f, "Element({:?})", String::from(el)),
Expect::Iq(_) => write!(f, "Iq(<cb>)"), Expect::Iq(_, desc) => write!(f, "Iq(<cb>, {})", desc),
Expect::Message(_) => write!(f, "Message(<cb>)"), Expect::Message(_, desc) => write!(f, "Message(<cb>, {})", desc),
Expect::Presence(_) => write!(f, "Presence(<cb>)"), Expect::Presence(_, desc) => write!(f, "Presence(<cb>, {})", desc),
} }
} }
} }
#[derive(Clone, Eq, PartialEq)]
pub struct TestElement(pub Element);
impl Deref for TestElement {
type Target = Element;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl fmt::Debug for TestElement {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", String::from(&self.0))
}
}
impl fmt::Display for TestElement {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", String::from(&self.0))
}
}
impl From<&TestElement> for String {
fn from(elem: &TestElement) -> Self {
format!("{}", elem)
}
}
impl From<Element> for TestElement {
fn from(elem: Element) -> Self {
Self(elem)
}
}
impl From<TestElement> for Element {
fn from(elem: TestElement) -> Self {
elem.0
}
}
impl From<Iq> for TestElement {
fn from(elem: Iq) -> Self {
Self(Element::from(elem))
}
}
impl From<Presence> for TestElement {
fn from(elem: Presence) -> Self {
Self(Element::from(elem))
}
}
impl From<Message> for TestElement {
fn from(elem: Message) -> Self {
Self(Element::from(elem))
}
}
#[derive(Debug)] #[derive(Debug)]
pub struct TestComponent { pub struct TestComponent {
in_buffer: VecDeque<Element>, in_buffer: VecDeque<TestElement>,
out_buffer: VecDeque<Element>,
expect_buffer: VecDeque<Expect>, expect_buffer: VecDeque<Expect>,
} }
impl TestComponent { impl TestComponent {
pub fn new(in_buffer: Vec<Element>) -> Self { pub fn new(in_buffer: Vec<Element>) -> Self {
TestComponent { TestComponent {
in_buffer: VecDeque::from(in_buffer), in_buffer: VecDeque::from(
out_buffer: VecDeque::new(), in_buffer
.into_iter()
.map(|el| TestElement(el))
.collect::<Vec<_>>(),
),
expect_buffer: VecDeque::new(), expect_buffer: VecDeque::new(),
} }
} }
/// Adds elements to be expected, in the order they're being added /// Adds elements to be expected, in the order they're being added
pub fn expect<E: Into<Element>>(&mut self, el: E) { pub fn expect<E: Into<TestElement>>(&mut self, el: E) {
self.expect_buffer.push_back(Expect::Element(el.into())) self.expect_buffer.push_back(Expect::Element(el.into()))
} }
pub fn expect_iq<F: FnOnce(Iq) + Send + 'static>(&mut self, callback: F) { pub fn expect_iq<F: FnOnce(Iq) + Send + 'static, S: Into<String>>(
self.expect_buffer.push_back(Expect::Iq(Box::new(callback))) &mut self,
} callback: F,
desc: S,
pub fn expect_message<F: FnOnce(Message) + Send + 'static>(&mut self, callback: F) { ) {
self.expect_buffer self.expect_buffer
.push_back(Expect::Message(Box::new(callback))) .push_back(Expect::Iq(Box::new(callback), desc.into()))
} }
pub fn expect_presence<F: FnOnce(Presence) + Send + 'static>(&mut self, callback: F) { pub fn expect_message<F: FnOnce(Message) + Send + 'static, S: Into<String>>(
&mut self,
callback: F,
desc: S,
) {
self.expect_buffer self.expect_buffer
.push_back(Expect::Presence(Box::new(callback))) .push_back(Expect::Message(Box::new(callback), desc.into()))
} }
/// Asserts expected output and actual output are the same pub fn expect_presence<F: FnOnce(Presence) + Send + 'static, S: Into<String>>(
pub fn assert(&mut self) { &mut self,
loop { callback: F,
let out = self.out_buffer.pop_front(); desc: S,
let expected = self.expect_buffer.pop_front(); ) {
self.expect_buffer
.push_back(Expect::Presence(Box::new(callback), desc.into()))
}
match (out, expected) { fn _send_stanza<E: Into<TestElement> + Send>(&mut self, el: E) -> Result<(), Error> {
(None, None) => break, let out: TestElement = el.into();
(Some(out), Some(expected)) => match expected { let expected = self.expect_buffer.pop_front();
Expect::Element(el) => assert_eq!(String::from(&el), String::from(&out)),
Expect::Iq(cb) => cb(Iq::try_from(out).unwrap()), match expected {
Expect::Message(cb) => cb(Message::try_from(out).unwrap()), Some(expected) => match expected {
Expect::Presence(cb) => cb(Presence::try_from(out).unwrap()), Expect::Element(el) => assert_eq!(String::from(&el), String::from(&out)),
}, Expect::Iq(cb, _) => cb(Iq::try_from(out.0).unwrap()),
(Some(out), None) => panic!("Missing matching expected element: {:?}", out), Expect::Message(cb, _) => cb(Message::try_from(out.0).unwrap()),
(None, Some(expected)) => match expected { Expect::Presence(cb, _) => cb(Presence::try_from(out.0).unwrap()),
Expect::Element(el) => panic!("Missing matching sent element: {:?}", el), },
Expect::Iq(_) => panic!("Missing matching sent iq"), None => panic!("Missing matching expected element: {:?}", out),
Expect::Message(_) => panic!("Missing matching sent message"),
Expect::Presence(_) => panic!("Missing matching sent presence"),
},
}
} }
}
fn _send_stanza<E: Into<Element> + Send>(&mut self, el: E) -> Result<(), Error> { Ok(())
Ok(self.out_buffer.push_back(el.into()))
} }
} }
@ -166,23 +234,34 @@ impl Stream for TestComponent {
fn poll_next(mut self: Pin<&mut Self>, _cx: &mut Context) -> Poll<Option<Self::Item>> { fn poll_next(mut self: Pin<&mut Self>, _cx: &mut Context) -> Poll<Option<Self::Item>> {
while self.in_buffer.len() > 0 { while self.in_buffer.len() > 0 {
return Poll::Ready(self.in_buffer.pop_front()); return Poll::Ready(self.in_buffer.pop_front().map(|el| el.0));
} }
Poll::Ready(None) Poll::Ready(None)
} }
} }
impl Drop for TestComponent {
fn drop(&mut self) {
// Don't assert if we're already panicking. Rustc displays a huge backtrace when "panicked
// while panicking" even when nobody asks for it (RUST_BACKTRACE unset). Let the error
// appear if there isn't any other error.
if ! thread::panicking() {
assert_eq!(self.expect_buffer.len(), 0, "Remaining expected elements in the buffer");
}
}
}
#[async_trait] #[async_trait]
impl ComponentTrait for TestComponent { impl ComponentTrait for TestComponent {
async fn send_stanza<E: Into<Element> + Send>(&mut self, el: E) -> Result<(), Error> { async fn send_stanza<E: Into<Element> + Send>(&mut self, el: E) -> Result<(), Error> {
self._send_stanza(el) self._send_stanza(el.into())
} }
} }
#[async_trait] #[async_trait]
impl ComponentTrait for &mut TestComponent { impl ComponentTrait for &mut TestComponent {
async fn send_stanza<E: Into<Element> + Send>(&mut self, el: E) -> Result<(), Error> { async fn send_stanza<E: Into<Element> + Send>(&mut self, el: E) -> Result<(), Error> {
self._send_stanza(el) self._send_stanza(el.into())
} }
} }

View file

@ -24,6 +24,7 @@ pub enum Error {
MismatchJids(Jid, Jid), MismatchJids(Jid, Jid),
NickAlreadyAssigned(String), NickAlreadyAssigned(String),
NonexistantSession(FullJid), NonexistantSession(FullJid),
SessionAlreadyExists(FullJid),
XMPPError(TokioXMPPError), XMPPError(TokioXMPPError),
} }
@ -35,6 +36,7 @@ impl fmt::Display for Error {
Error::MismatchJids(jid1, jid2) => write!(f, "Mismatch Jids: {}, {}", jid1, jid2), Error::MismatchJids(jid1, jid2) => write!(f, "Mismatch Jids: {}, {}", jid1, jid2),
Error::NickAlreadyAssigned(err) => write!(f, "Nickname already assigned: {}", err), Error::NickAlreadyAssigned(err) => write!(f, "Nickname already assigned: {}", err),
Error::NonexistantSession(err) => write!(f, "Session doesn't exist: {}", err), Error::NonexistantSession(err) => write!(f, "Session doesn't exist: {}", err),
Error::SessionAlreadyExists(err) => write!(f, "Session already exist: {}", err),
Error::XMPPError(err) => write!(f, "XMPP error: {}", err), Error::XMPPError(err) => write!(f, "XMPP error: {}", err),
} }
} }

View file

@ -175,7 +175,7 @@ async fn handle_presence<C: ComponentTrait>(
).into() ).into()
]); ]);
if let Some(mut room) = rooms.remove(&roomjid) { if let Some(mut room) = rooms.remove(&roomjid) {
match room.remove_session(component, realjid).await { match room.remove_session(component, realjid, participant.resource.clone()).await {
Ok(()) => (), Ok(()) => (),
Err(Error::NonexistantSession(_)) => { Err(Error::NonexistantSession(_)) => {
component.send_stanza(error).await.unwrap(); component.send_stanza(error).await.unwrap();

View file

@ -16,7 +16,7 @@
use crate::component::ComponentTrait; use crate::component::ComponentTrait;
use crate::error::Error; use crate::error::Error;
use std::collections::HashMap; use std::collections::BTreeMap;
use std::iter::IntoIterator; use std::iter::IntoIterator;
use chrono::{FixedOffset, Utc}; use chrono::{FixedOffset, Utc};
@ -34,11 +34,25 @@ use xmpp_parsers::{
}; };
pub type Nick = String; pub type Nick = String;
type Session = FullJid;
#[derive(Debug)] #[derive(Debug, PartialEq)]
pub enum BroadcastPresence {
/// Resource joined the room. It needs to know about all other participants, and other
/// participants needs to know about it.
Join,
/// Resource lost sync. It needs to know about all other participants.
Resync,
/// Resource change status (becomes busy, etc.), tell all other participants.
Update,
/// Resource leaves. It only needs confirmation that it leaves. Tell all other participants.
Leave,
}
#[derive(Debug, PartialEq)]
pub struct Room { pub struct Room {
pub jid: BareJid, pub jid: BareJid,
pub occupants: HashMap<BareJid, Occupant>, pub occupants: BTreeMap<Nick, Occupant>,
// TODO: Subject struct. // TODO: Subject struct.
// TODO: Store subject lang // TODO: Store subject lang
pub subject: Option<(String, Occupant, DateTime)>, pub subject: Option<(String, Occupant, DateTime)>,
@ -48,107 +62,235 @@ impl Room {
pub fn new(jid: BareJid) -> Self { pub fn new(jid: BareJid) -> Self {
Room { Room {
jid, jid,
occupants: HashMap::new(), occupants: BTreeMap::new(),
subject: None, subject: None,
} }
} }
pub async fn broadcast_presence<C: ComponentTrait>(
&self,
component: &mut C,
own_occupant: &Occupant,
own_session: &Session,
mode: BroadcastPresence,
) -> Result<(), Error> {
let leave = mode == BroadcastPresence::Leave;
// All participants to new participant
let presence_to_new = Presence::new(
if leave { PresenceType::Unavailable } else { PresenceType::None }
)
.with_to(own_session.clone())
.with_payloads(vec![MucUser {
status: Vec::new(),
items: vec![MucItem::new(Affiliation::Owner, Role::Moderator)],
}
.into()]);
// New participant to all other sessions
let presence_to_old = Presence::new(
if leave { PresenceType::Unavailable } else { PresenceType::None }
)
.with_from(Jid::Full(own_occupant.participant.clone()))
.with_payloads(vec![MucUser {
status: Vec::new(),
items: vec![
MucItem::new(
Affiliation::Owner,
if leave { Role::None } else { Role::Moderator },
)
],
}
.into()]);
let sync = match mode {
BroadcastPresence::Join |
BroadcastPresence::Resync => true,
_ => false,
};
let update = match mode {
BroadcastPresence::Join |
BroadcastPresence::Update => true,
_ => false,
};
for (_, other) in self.occupants.iter() {
if own_occupant.nick == other.nick {
continue;
}
if sync { // Send presences from others to participant.
let presence = presence_to_new.clone()
.with_from(Jid::Full(other.participant.clone()));
component.send_stanza(presence).await?;
}
if update || leave { // Send presence from participant to others.
for session in other.iter() {
// Skip sending if it's us.
if session == own_session {
continue;
}
let presence = presence_to_old.clone()
.with_to(Jid::Full(session.clone()));
component.send_stanza(presence).await?;
}
}
}
// MucItems to be sent to sessions of this occupant
let self_items = own_occupant.iter().map(|session| {
MucItem {
affiliation: Affiliation::Owner,
role: if leave { Role::None } else { Role::Moderator },
jid: Some(session.clone()),
nick: None,
actor: None,
continue_: None,
reason: None,
}
}).collect::<Vec<MucItem>>();
// Multi-Session Nick: For this occupant, include all sessions all with item@jid discovered
// so they can identify each other as being the same account under the same nick.
let session_presence = Presence::new(
if leave { PresenceType::Unavailable } else { PresenceType::None }
)
.with_from(Jid::Full(own_occupant.participant.clone()));
for session in own_occupant.iter() {
if session == own_session {
continue;
}
let presence = session_presence.clone()
.with_to(Jid::Full(session.clone()))
.with_payloads(vec![MucUser {
status: vec![],
items: self_items.clone(),
}.into()]);
component.send_stanza(presence).await?;
}
// Send self-presence
if sync || leave {
// New participant to all other sessions
let self_presence = Presence::new(
if leave { PresenceType::Unavailable } else { PresenceType::None }
)
.with_from(Jid::Full(own_occupant.participant.clone()))
.with_to(own_session.clone())
.with_payloads(vec![MucUser {
status: if leave { vec![
MucStatus::SelfPresence,
] } else {
vec![
MucStatus::SelfPresence,
MucStatus::AssignedNick,
] },
items: if leave {
vec![MucItem::new(Affiliation::Owner, Role::None)]
} else {
self_items
},
}
.into()]);
component.send_stanza(self_presence).await?;
}
Ok(())
}
pub async fn send_subject<C: ComponentTrait>(
&mut self,
component: &mut C,
realjid: Session,
occupant: Occupant,
) -> Result<(), Error> {
debug!("Sending subject!");
if self.subject.is_none() {
let subject = String::from("");
let setter = occupant;
let stamp = DateTime(Utc::now().with_timezone(&FixedOffset::east(0)));
self.subject = Some((subject, setter, stamp));
}
let mut subject = Message::new(Some(Jid::Full(realjid)));
subject.from = Some(Jid::Full(
self.subject.as_ref().unwrap().1.participant.clone(),
));
subject.subjects.insert(
String::from("en"),
Subject(self.subject.as_ref().unwrap().0.clone()),
);
subject.type_ = MessageType::Groupchat;
subject.payloads = vec![Delay {
from: Some(Jid::Bare(self.jid.clone())),
stamp: self.subject.as_ref().unwrap().2.clone(),
data: None,
}
.into()];
component.send_stanza(subject).await
}
pub async fn add_session<C: ComponentTrait>( pub async fn add_session<C: ComponentTrait>(
&mut self, &mut self,
component: &mut C, component: &mut C,
realjid: FullJid, realjid: Session,
nick: Nick, new_nick: Nick,
) -> Result<(), Error> { ) -> Result<(), Error> {
let bare = BareJid::from(realjid.clone()); // Ensure nick isn't already assigned
if let Some(occupant) = self.occupants.get_mut(&bare) { let _ = self.occupants.iter().try_for_each(|(nick, occupant)| {
occupant.add_session(realjid)?; let new_nick = new_nick.as_str();
} else { if new_nick == nick && occupant.real != BareJid::from(realjid.clone()) {
debug!("{} is joining {}", realjid, self.jid); return Err(Error::NickAlreadyAssigned(String::from(new_nick)));
let new_occupant = Occupant::new(&self, realjid.clone(), nick.clone());
// Ensure nick isn't already assigned
let _ = self.occupants.iter().try_for_each(|(_, occupant)| {
let nick = nick.clone();
if occupant.nick == nick {
return Err(Error::NickAlreadyAssigned(nick));
}
Ok(())
})?;
// Send occupants
debug!("Sending occupants for {}", realjid);
// Other participants to new participant
let presence_to_new = Presence::new(PresenceType::None)
// New occupant with a single session
.with_to(new_occupant.sessions[0].clone())
.with_payloads(vec![MucUser {
status: Vec::new(),
items: vec![MucItem::new(Affiliation::Owner, Role::Moderator)],
}
.into()]);
// New participant to other participants
let presence_to_old = Presence::new(PresenceType::None)
.with_from(Jid::Full(new_occupant.participant.clone()))
.with_payloads(vec![MucUser {
status: Vec::new(),
items: vec![MucItem::new(Affiliation::Owner, Role::Moderator)],
}
.into()]);
for (_, occupant) in self.occupants.iter() {
component
.send_stanza(
presence_to_new
.clone()
.with_from(occupant.participant.clone()),
)
.await?;
for session in occupant.iter() {
component
.send_stanza(presence_to_old.clone().with_to(session.clone()))
.await?;
}
} }
Ok(())
})?;
// Add into occupants let mode: Option<BroadcastPresence> = {
let _ = self.occupants.insert(bare.clone(), new_occupant.clone()); if let Some(occupant) = self.occupants.get_mut(&new_nick) {
match occupant.add_session(realjid.clone()) {
// Self-presence Ok(_) => {
debug!("Sending self-presence for {}", realjid); Some(BroadcastPresence::Join)
let participant: FullJid = self.jid.clone().with_resource(nick); },
let status = vec![MucStatus::SelfPresence, MucStatus::AssignedNick]; Err(Error::SessionAlreadyExists(_)) => {
let items = vec![MucItem::new(Affiliation::Owner, Role::Moderator)]; Some(BroadcastPresence::Resync)
let self_presence = Presence::new(PresenceType::None) },
.with_from(participant.clone()) Err(err) => return Err(err),
.with_to(realjid.clone()) }
.with_payloads(vec![MucUser { status, items }.into()]); } else {
component.send_stanza(self_presence).await?; Some(BroadcastPresence::Join)
// Send subject
debug!("Sending subject!");
if self.subject.is_none() {
let subject = String::from("");
let setter = new_occupant;
let stamp = DateTime(Utc::now().with_timezone(&FixedOffset::east(0)));
self.subject = Some((subject, setter, stamp));
} }
};
let mut subject = Message::new(Some(Jid::Full(realjid))); if ! self.occupants.contains_key(&new_nick) {
subject.from = Some(Jid::Full( let _ = self.occupants.insert(
self.subject.as_ref().unwrap().1.participant.clone(), new_nick.clone(),
)); Occupant::new(&self, realjid.clone(), new_nick.clone()),
subject.subjects.insert(
String::from("en"),
Subject(self.subject.as_ref().unwrap().0.clone()),
); );
subject.type_ = MessageType::Groupchat; }
subject.payloads = vec![Delay { let occupant = self.occupants.get(&new_nick).unwrap();
from: Some(Jid::Bare(self.jid.clone())),
stamp: self.subject.as_ref().unwrap().2.clone(), match mode {
data: None, Some(BroadcastPresence::Resync) => {
} self.broadcast_presence(
.into()]; component,
component.send_stanza(subject).await?; &occupant,
&realjid,
BroadcastPresence::Resync,
).await?;
},
Some(BroadcastPresence::Join) => {
debug!("{} is joining {}", realjid, self.jid);
self.broadcast_presence(
component,
&occupant,
&realjid,
BroadcastPresence::Join,
).await?;
self.send_subject(component, realjid, occupant.clone()).await?;
},
_ => (),
} }
Ok(()) Ok(())
@ -157,56 +299,34 @@ impl Room {
pub async fn remove_session<C: ComponentTrait>( pub async fn remove_session<C: ComponentTrait>(
&mut self, &mut self,
component: &mut C, component: &mut C,
realjid: FullJid, realjid: Session,
nick: Nick,
) -> Result<(), Error> { ) -> Result<(), Error> {
let bare = BareJid::from(realjid.clone());
// If occupant doesn't exist, ignore. // If occupant doesn't exist, ignore.
if let Some(mut occupant) = self.occupants.remove(&bare) { if let Some(mut occupant) = self.occupants.remove(&nick) {
let self_presence = Presence::new(PresenceType::Unavailable) self.broadcast_presence(
.with_from(occupant.participant.clone()) component,
.with_to(realjid.clone()) &occupant,
.with_payloads(vec![MucUser { &realjid,
status: vec![MucStatus::SelfPresence], BroadcastPresence::Leave,
items: vec![MucItem::new(Affiliation::Owner, Role::None)], ).await?;
}
.into()]);
component.send_stanza(self_presence).await?;
occupant.remove_session(realjid)?; occupant.remove_session(realjid)?;
} else {
let presence = Presence::new(PresenceType::Unavailable) // TODO: Error
.with_from(occupant.participant.clone())
.with_payloads(vec![MucUser {
status: Vec::new(),
items: vec![MucItem::new(Affiliation::Owner, Role::None)],
}
.into()]);
// Add remaining occupant sessions in the occupant pool
if occupant.sessions.len() > 0 {
self.occupants.insert(bare, occupant);
}
for (_, occupant) in self.occupants.iter() {
for session in occupant.iter() {
let presence = presence.clone().with_to(Jid::Full(session.clone()));
component.send_stanza(presence).await?;
}
}
} }
Ok(()) Ok(())
} }
} }
#[derive(Debug, Clone)] #[derive(Debug, Clone, PartialEq)]
pub struct Occupant { pub struct Occupant {
/// Public Jid for the Occupant /// Public Jid for the Occupant
real: BareJid, pub real: BareJid,
participant: FullJid, pub participant: FullJid,
nick: Nick, pub nick: Nick,
sessions: Vec<FullJid>, pub sessions: Vec<FullJid>,
} }
impl Occupant { impl Occupant {
@ -224,6 +344,13 @@ impl Occupant {
return Err(Error::MismatchJids(Jid::from(self.real.clone()), Jid::from(real.clone()))); return Err(Error::MismatchJids(Jid::from(self.real.clone()), Jid::from(real.clone())));
} }
for session in &self.sessions {
if &real == session {
return Err(Error::SessionAlreadyExists(real))
}
}
self.sessions.push(real);
Ok(()) Ok(())
} }
@ -263,14 +390,293 @@ impl Occupant {
mod tests { mod tests {
use super::*; use super::*;
use std::str::FromStr; use std::str::FromStr;
use xmpp_parsers::{BareJid, FullJid}; use crate::component::TestComponent;
use xmpp_parsers::{
BareJid, Element,
presence::{Presence, Type as PresenceType},
muc::{
MucUser,
user::{Affiliation, Role, Item as MucItem, Status as MucStatus},
},
};
#[test] #[tokio::test]
fn occupant_ignore_dup_session() { async fn test_broadcast_presence_resync() {
let room = Room::new(BareJid::from_str("room@muc").unwrap()); let roomjid = BareJid::from_str("room@muc").unwrap();
let real = FullJid::from_str("foo@bar/meh").unwrap(); let realjid1 = FullJid::from_str("foo@bar/qxx").unwrap();
let mut occupant = Occupant::new(&room, real.clone(), String::from("nick")); let participant1 = roomjid.clone().with_resource(String::from("nick1"));
occupant.add_session(real.clone()).unwrap(); let realjid2 = FullJid::from_str("qxx@foo/bar").unwrap();
assert_eq!(occupant.iter().count(), 1); let participant2 = roomjid.clone().with_resource(String::from("nick2"));
let realjid3 = FullJid::from_str("bar@qxx/foo").unwrap();
let participant3 = roomjid.clone().with_resource(String::from("nick3"));
let mut room = Room::new(roomjid.clone());
room.occupants = BTreeMap::new();
room.occupants.insert(
participant1.resource.clone(), Occupant::new(&room, realjid1.clone(), String::from("nick1")),
);
room.occupants.insert(
participant2.resource.clone(), Occupant::new(&room, realjid2.clone(), String::from("nick2")),
);
let occupant3 = Occupant::new(&room, realjid3.clone(), String::from("nick3"));
room.occupants.insert(participant3.resource.clone(), occupant3.clone());
let self_item = MucItem::try_from(
r#"<item xmlns="http://jabber.org/protocol/muc#user" affiliation="owner" role="moderator" jid="bar@qxx/foo"/>"#
.parse::<Element>()
.unwrap()
).unwrap();
// BroadcastPresence::Resync
let mut component = TestComponent::new(vec![]);
component.expect(
Presence::new(PresenceType::None)
.with_from(participant1.clone())
.with_to(realjid3.clone())
.with_payloads(vec![
MucUser {
status: vec![],
items: vec![MucItem::new(Affiliation::Owner, Role::Moderator)],
}.into()
])
);
component.expect(
Presence::new(PresenceType::None)
.with_from(participant2.clone())
.with_to(realjid3.clone())
.with_payloads(vec![
MucUser {
status: vec![],
items: vec![MucItem::new(Affiliation::Owner, Role::Moderator)],
}.into()
])
);
component.expect(
Presence::new(PresenceType::None)
.with_from(participant3.clone())
.with_to(realjid3.clone())
.with_payloads(vec![
MucUser {
status: vec![MucStatus::SelfPresence, MucStatus::AssignedNick],
items: vec![self_item.clone()],
}.into()
])
);
room.broadcast_presence(&mut component, &occupant3, &realjid3, BroadcastPresence::Resync).await.unwrap();
}
#[tokio::test]
async fn test_broadcast_presence_update() {
let roomjid = BareJid::from_str("room@muc").unwrap();
let realjid1 = FullJid::from_str("foo@bar/qxx").unwrap();
let participant1 = roomjid.clone().with_resource(String::from("nick1"));
let realjid2 = FullJid::from_str("qxx@foo/bar").unwrap();
let participant2 = roomjid.clone().with_resource(String::from("nick2"));
let realjid3 = FullJid::from_str("bar@qxx/foo").unwrap();
let participant3 = roomjid.clone().with_resource(String::from("nick3"));
let mut room = Room::new(roomjid.clone());
room.occupants = BTreeMap::new();
room.occupants.insert(
participant1.resource.clone(), Occupant::new(&room, realjid1.clone(), String::from("nick1")),
);
room.occupants.insert(
participant2.resource.clone(), Occupant::new(&room, realjid2.clone(), String::from("nick2")),
);
let occupant3 = Occupant::new(&room, realjid3.clone(), String::from("nick3"));
room.occupants.insert(participant3.resource.clone(), occupant3.clone());
// BroadcastPresence::Update
let mut component = TestComponent::new(vec![]);
component.expect(
Presence::new(PresenceType::None)
.with_from(participant3.clone())
.with_to(realjid1.clone())
.with_payloads(vec![
MucUser {
status: vec![],
items: vec![MucItem::new(Affiliation::Owner, Role::Moderator)],
}.into()
])
);
component.expect(
Presence::new(PresenceType::None)
.with_from(participant3.clone())
.with_to(realjid2.clone())
.with_payloads(vec![
MucUser {
status: vec![],
items: vec![MucItem::new(Affiliation::Owner, Role::Moderator)],
}.into()
])
);
room.broadcast_presence(&mut component, &occupant3, &realjid3, BroadcastPresence::Update).await.unwrap();
}
#[tokio::test]
async fn test_broadcast_presence_join() {
let roomjid = BareJid::from_str("room@muc").unwrap();
let realjid1 = FullJid::from_str("foo@bar/qxx").unwrap();
let participant1 = roomjid.clone().with_resource(String::from("nick1"));
let realjid2 = FullJid::from_str("qxx@foo/bar").unwrap();
let participant2 = roomjid.clone().with_resource(String::from("nick2"));
let realjid3 = FullJid::from_str("bar@qxx/foo").unwrap();
let participant3 = roomjid.clone().with_resource(String::from("nick3"));
let mut room = Room::new(roomjid.clone());
room.occupants = BTreeMap::new();
room.occupants.insert(
participant1.resource.clone(), Occupant::new(&room, realjid1.clone(), String::from("nick1")),
);
room.occupants.insert(
participant2.resource.clone(), Occupant::new(&room, realjid2.clone(), String::from("nick2")),
);
let occupant3 = Occupant::new(&room, realjid3.clone(), String::from("nick3"));
room.occupants.insert(participant3.resource.clone(), occupant3.clone());
let self_item = MucItem::try_from(
r#"<item xmlns="http://jabber.org/protocol/muc#user" affiliation="owner" role="moderator" jid="bar@qxx/foo"/>"#
.parse::<Element>()
.unwrap()
).unwrap();
// BroadcastPresence::Join
let mut component = TestComponent::new(vec![]);
component.expect(
Presence::new(PresenceType::None)
.with_from(participant1.clone())
.with_to(realjid3.clone())
.with_payloads(vec![
MucUser {
status: vec![],
items: vec![MucItem::new(Affiliation::Owner, Role::Moderator)],
}.into()
])
);
component.expect(
Presence::new(PresenceType::None)
.with_from(participant3.clone())
.with_to(realjid1.clone())
.with_payloads(vec![
MucUser {
status: vec![],
items: vec![MucItem::new(Affiliation::Owner, Role::Moderator)],
}.into()
])
);
component.expect(
Presence::new(PresenceType::None)
.with_from(participant2.clone())
.with_to(realjid3.clone())
.with_payloads(vec![
MucUser {
status: vec![],
items: vec![MucItem::new(Affiliation::Owner, Role::Moderator)],
}.into()
])
);
component.expect(
Presence::new(PresenceType::None)
.with_from(participant3.clone())
.with_to(realjid2.clone())
.with_payloads(vec![
MucUser {
status: vec![],
items: vec![MucItem::new(Affiliation::Owner, Role::Moderator)],
}.into()
])
);
component.expect(
Presence::new(PresenceType::None)
.with_from(participant3.clone())
.with_to(realjid3.clone())
.with_payloads(vec![
MucUser {
status: vec![MucStatus::SelfPresence, MucStatus::AssignedNick],
items: vec![self_item],
}.into()
])
);
room.broadcast_presence(&mut component, &occupant3, &realjid3, BroadcastPresence::Join).await.unwrap();
}
#[tokio::test]
async fn test_broadcast_presence_leave() {
let roomjid = BareJid::from_str("room@muc").unwrap();
let realjid1 = FullJid::from_str("foo@bar/qxx").unwrap();
let participant1 = roomjid.clone().with_resource(String::from("nick1"));
let realjid2 = FullJid::from_str("qxx@foo/bar").unwrap();
let participant2 = roomjid.clone().with_resource(String::from("nick2"));
let realjid3 = FullJid::from_str("bar@qxx/foo").unwrap();
let participant3 = roomjid.clone().with_resource(String::from("nick3"));
let mut room = Room::new(roomjid.clone());
room.occupants = BTreeMap::new();
room.occupants.insert(
participant1.resource.clone(), Occupant::new(&room, realjid1.clone(), String::from("nick1")),
);
room.occupants.insert(
participant2.resource.clone(), Occupant::new(&room, realjid2.clone(), String::from("nick2")),
);
let occupant3 = Occupant::new(&room, realjid3.clone(), String::from("nick3"));
room.occupants.insert(participant3.resource.clone(), occupant3.clone());
// BroadcastPresence::Leave
let mut component = TestComponent::new(vec![]);
component.expect(
Presence::new(PresenceType::Unavailable)
.with_from(participant3.clone())
.with_to(realjid1.clone())
.with_payloads(vec![
MucUser {
status: vec![],
items: vec![MucItem::new(Affiliation::Owner, Role::None)],
}.into()
])
);
component.expect(
Presence::new(PresenceType::Unavailable)
.with_from(participant3.clone())
.with_to(realjid2.clone())
.with_payloads(vec![
MucUser {
status: vec![],
items: vec![MucItem::new(Affiliation::Owner, Role::None)],
}.into()
])
);
component.expect(
Presence::new(PresenceType::Unavailable)
.with_from(participant3.clone())
.with_to(realjid3.clone())
.with_payloads(vec![
MucUser {
status: vec![MucStatus::SelfPresence],
items: vec![MucItem::new(Affiliation::Owner, Role::None)],
}.into()
])
);
room.broadcast_presence(&mut component, &occupant3, &realjid3, BroadcastPresence::Leave).await.unwrap();
} }
} }

64
src/tests/iq.rs Normal file
View file

@ -0,0 +1,64 @@
//
// This program is free software: you can redistribute it and/or modify it
// under the terms of the GNU Affero General Public License as published by the
// Free Software Foundation, either version 3 of the License, or (at your
// option) any later version.
//
// This program is distributed in the hope that it will be useful, but WITHOUT
// ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
// FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License
// for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
use crate::component::TestComponent;
use crate::handlers::handle_stanza;
use crate::room::Room;
use std::collections::HashMap;
use std::str::FromStr;
use lazy_static::lazy_static;
use xmpp_parsers::{
iq::{Iq, IqType},
stanza_error::{DefinedCondition, ErrorType, StanzaError},
BareJid, Element, FullJid, Jid,
};
lazy_static! {
static ref COMPONENT_JID: BareJid = BareJid::from_str("muc.component").unwrap();
}
#[tokio::test]
async fn test_iq_unimplemented() {
let from = Jid::Full(FullJid::from_str("foo@bar/qxx").unwrap());
let to = Jid::Bare(COMPONENT_JID.clone());
let disco: Element = Iq {
from: Some(from.clone()),
to: Some(to.clone()),
id: String::from("disco"),
payload: IqType::Get(Element::builder("x", "urn:example:unimplemented").build()),
}
.into();
let reply: Element = Iq::from_error(
"disco",
StanzaError::new(
ErrorType::Cancel,
DefinedCondition::ServiceUnavailable,
"en",
"No handler defined for this kind of iq.",
),
)
.with_from(to)
.with_to(from)
.into();
let mut component = TestComponent::new(vec![disco]);
let mut rooms: HashMap<BareJid, Room> = HashMap::new();
component.expect(reply);
handle_stanza(&mut component, &mut rooms).await.unwrap();
}

20
src/tests/mod.rs Normal file
View file

@ -0,0 +1,20 @@
// Copyright (C) 2022-2099 The crate authors.
//
// This program is free software: you can redistribute it and/or modify it
// under the terms of the GNU Affero General Public License as published by the
// Free Software Foundation, either version 3 of the License, or (at your
// option) any later version.
//
// This program is distributed in the hope that it will be useful, but WITHOUT
// ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
// FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License
// for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
#[cfg(test)]
mod iq;
#[cfg(test)]
mod presence;

View file

@ -23,7 +23,6 @@ use std::str::FromStr;
use lazy_static::lazy_static; use lazy_static::lazy_static;
use xmpp_parsers::{ use xmpp_parsers::{
delay::Delay, delay::Delay,
iq::{Iq, IqType},
message::{Message, MessageType, Subject as MessageSubject}, message::{Message, MessageType, Subject as MessageSubject},
muc::{ muc::{
user::{Affiliation, Item as MucItem, Role, Status as MucStatus}, user::{Affiliation, Item as MucItem, Role, Status as MucStatus},
@ -38,40 +37,6 @@ lazy_static! {
static ref COMPONENT_JID: BareJid = BareJid::from_str("muc.component").unwrap(); static ref COMPONENT_JID: BareJid = BareJid::from_str("muc.component").unwrap();
} }
#[tokio::test]
async fn test_iq_unimplemented() {
let from = Jid::Full(FullJid::from_str("foo@bar/qxx").unwrap());
let to = Jid::Bare(COMPONENT_JID.clone());
let disco: Element = Iq {
from: Some(from.clone()),
to: Some(to.clone()),
id: String::from("disco"),
payload: IqType::Get(Element::builder("x", "urn:example:unimplemented").build()),
}
.into();
let reply: Element = Iq::from_error(
"disco",
StanzaError::new(
ErrorType::Cancel,
DefinedCondition::ServiceUnavailable,
"en",
"No handler defined for this kind of iq.",
),
)
.with_from(to)
.with_to(from)
.into();
let mut component = TestComponent::new(vec![disco]);
let mut rooms: HashMap<BareJid, Room> = HashMap::new();
component.expect(reply);
handle_stanza(&mut component, &mut rooms).await.unwrap();
component.assert();
}
#[tokio::test] #[tokio::test]
async fn test_join_presence_empty_room() { async fn test_join_presence_empty_room() {
let from = FullJid::from_str("foo@bar/qxx").unwrap(); let from = FullJid::from_str("foo@bar/qxx").unwrap();
@ -101,7 +66,11 @@ async fn test_join_presence_empty_room() {
.with_to(Jid::Full(from.clone())) .with_to(Jid::Full(from.clone()))
.with_payloads(vec![MucUser { .with_payloads(vec![MucUser {
status: vec![MucStatus::SelfPresence, MucStatus::AssignedNick], status: vec![MucStatus::SelfPresence, MucStatus::AssignedNick],
items: vec![MucItem::new(Affiliation::Owner, Role::Moderator)], items: vec![{
let mut item = MucItem::new(Affiliation::Owner, Role::Moderator);
item.jid = Some(from.clone());
item
}],
} }
.into()]), .into()]),
); );
@ -134,10 +103,9 @@ async fn test_join_presence_empty_room() {
String::from(&expected), String::from(&expected),
String::from(&Into::<Element>::into(out)) String::from(&Into::<Element>::into(out))
); );
}); }, "Room subject to participant1");
handle_stanza(&mut component, &mut rooms).await.unwrap(); handle_stanza(&mut component, &mut rooms).await.unwrap();
component.assert();
assert_eq!(rooms.len(), 1); assert_eq!(rooms.len(), 1);
match rooms.get(&roomjid) { match rooms.get(&roomjid) {
@ -174,10 +142,8 @@ async fn test_join_presence_nick_already_assigned() {
let mut component = TestComponent::new(vec![join1, join2]); let mut component = TestComponent::new(vec![join1, join2]);
let mut rooms: HashMap<BareJid, Room> = HashMap::new(); let mut rooms: HashMap<BareJid, Room> = HashMap::new();
// Ignore self-presence for first participant component.expect_presence(|_| (), "Self-presence for first participant");
component.expect_presence(|_| ()); component.expect_message(|_| (), "Subject for first participant");
// Ignore message subject for first participant
component.expect_message(|_| ());
component.expect( component.expect(
Presence::new(PresenceType::Error) Presence::new(PresenceType::Error)
@ -193,7 +159,6 @@ async fn test_join_presence_nick_already_assigned() {
); );
handle_stanza(&mut component, &mut rooms).await.unwrap(); handle_stanza(&mut component, &mut rooms).await.unwrap();
component.assert();
match rooms.get(&roomjid) { match rooms.get(&roomjid) {
Some(room) => assert_eq!(room.occupants.len(), 1), Some(room) => assert_eq!(room.occupants.len(), 1),
@ -224,10 +189,8 @@ async fn test_join_presence_existing_room() {
let mut component = TestComponent::new(vec![join1, join2]); let mut component = TestComponent::new(vec![join1, join2]);
let mut rooms: HashMap<BareJid, Room> = HashMap::new(); let mut rooms: HashMap<BareJid, Room> = HashMap::new();
// Ignore self-presence for first participant component.expect_presence(|_| (), "Self-presence for participant1");
component.expect_presence(|_| ()); component.expect_message(|_| (), "Subject for participant1");
// Ignore message subject for first participant
component.expect_message(|_| ());
// Participant1 presence for participant2 // Participant1 presence for participant2
component.expect( component.expect(
@ -260,12 +223,15 @@ async fn test_join_presence_existing_room() {
.with_to(Jid::Full(realjid2.clone())) .with_to(Jid::Full(realjid2.clone()))
.with_payloads(vec![MucUser { .with_payloads(vec![MucUser {
status: vec![MucStatus::SelfPresence, MucStatus::AssignedNick], status: vec![MucStatus::SelfPresence, MucStatus::AssignedNick],
items: vec![MucItem::new(Affiliation::Owner, Role::Moderator)], items: vec![{
let mut item = MucItem::new(Affiliation::Owner, Role::Moderator);
item.jid = Some(realjid2.clone());
item
}],
} }
.into()]), .into()]),
); );
// Subject for participant2
component.expect_message(|el| { component.expect_message(|el| {
let mut subjects = BTreeMap::new(); let mut subjects = BTreeMap::new();
subjects.insert(String::from("en"), MessageSubject::from_str("").unwrap()); subjects.insert(String::from("en"), MessageSubject::from_str("").unwrap());
@ -292,10 +258,9 @@ async fn test_join_presence_existing_room() {
String::from(&expected), String::from(&expected),
String::from(&Into::<Element>::into(out)) String::from(&Into::<Element>::into(out))
); );
}); }, "Subject for participant2");
handle_stanza(&mut component, &mut rooms).await.unwrap(); handle_stanza(&mut component, &mut rooms).await.unwrap();
component.assert();
match rooms.get(&roomjid) { match rooms.get(&roomjid) {
Some(room) => assert_eq!(room.occupants.len(), 2), Some(room) => assert_eq!(room.occupants.len(), 2),
@ -303,6 +268,73 @@ async fn test_join_presence_existing_room() {
} }
} }
#[tokio::test]
async fn test_presence_resync() {
let realjid1 = FullJid::from_str("foo@bar/qxx").unwrap();
let realjid2 = FullJid::from_str("qxx@bar/foo").unwrap();
let roomjid = COMPONENT_JID.clone().with_node("room");
let participant1 = roomjid.clone().with_resource("nick1");
let participant2 = roomjid.clone().with_resource("nick2");
let join1: Element = Presence::new(PresenceType::None)
.with_from(Jid::Full(realjid1.clone()))
.with_to(Jid::Full(participant1.clone()))
.with_payloads(vec![Muc::new().into()])
.into();
let join2: Element = Presence::new(PresenceType::None)
.with_from(Jid::Full(realjid2.clone()))
.with_to(Jid::Full(participant2.clone()))
.with_payloads(vec![Muc::new().into()])
.into();
let mut component = TestComponent::new(vec![join1.clone(), join2, join1]);
let mut rooms: HashMap<BareJid, Room> = HashMap::new();
component.expect_presence(|_| (), "Self-presence for participant1");
component.expect_message(|_| (), "Subject for participant1");
component.expect_presence(|_| (), "Participant1 presence for participant2");
component.expect_presence(|_| (), "Participant2 presence for participant1");
component.expect_presence(|_| (), "Self-presence for participant2");
component.expect_message(|_| (), "Subject for participant2");
// Resync: Participant2 presence for participant1
component.expect(
Presence::new(PresenceType::None)
.with_from(Jid::Full(participant2.clone()))
.with_to(Jid::Full(realjid1.clone()))
.with_payloads(vec![MucUser {
status: Vec::new(),
items: vec![MucItem::new(Affiliation::Owner, Role::Moderator)],
}
.into()]),
);
// Resync: Participant1 self-presence
component.expect(
Presence::new(PresenceType::None)
.with_from(Jid::Full(participant1))
.with_to(Jid::Full(realjid1.clone()))
.with_payloads(vec![MucUser {
status: vec![MucStatus::SelfPresence, MucStatus::AssignedNick],
items: vec![{
let mut item = MucItem::new(Affiliation::Owner, Role::Moderator);
item.jid = Some(realjid1.clone());
item
}],
}
.into()]),
);
handle_stanza(&mut component, &mut rooms).await.unwrap();
match rooms.get(&roomjid) {
Some(room) => assert_eq!(room.occupants.len(), 2),
None => panic!(),
}
}
#[tokio::test] #[tokio::test]
async fn test_leave_non_existing_room() { async fn test_leave_non_existing_room() {
let realjid1 = FullJid::from_str("foo@bar/qxx").unwrap(); let realjid1 = FullJid::from_str("foo@bar/qxx").unwrap();
@ -319,7 +351,6 @@ async fn test_leave_non_existing_room() {
let mut rooms: HashMap<BareJid, Room> = HashMap::new(); let mut rooms: HashMap<BareJid, Room> = HashMap::new();
handle_stanza(&mut component, &mut rooms).await.unwrap(); handle_stanza(&mut component, &mut rooms).await.unwrap();
// The leave should be ignored, there should be no output at all. // The leave should be ignored, there should be no output at all.
component.assert();
} }
#[tokio::test] #[tokio::test]
@ -343,10 +374,8 @@ async fn test_leave_last_participant() {
let mut component = TestComponent::new(vec![join1, leave1]); let mut component = TestComponent::new(vec![join1, leave1]);
let mut rooms: HashMap<BareJid, Room> = HashMap::new(); let mut rooms: HashMap<BareJid, Room> = HashMap::new();
// Ignore self-presence for participant1 component.expect_presence(|_| (), "Self-presence for participant1");
component.expect_presence(|_| ()); component.expect_message(|_| (), "Subject for participant1");
// Ignore subject message for participant1
component.expect_message(|_| ());
component.expect( component.expect(
Presence::new(PresenceType::Unavailable) Presence::new(PresenceType::Unavailable)
@ -361,7 +390,6 @@ async fn test_leave_last_participant() {
handle_stanza(&mut component, &mut rooms).await.unwrap(); handle_stanza(&mut component, &mut rooms).await.unwrap();
component.assert();
assert_eq!(rooms.len(), 0); assert_eq!(rooms.len(), 0);
} }
@ -394,29 +422,12 @@ async fn test_leave_room_not_last() {
let mut component = TestComponent::new(vec![join1, join2, leave2]); let mut component = TestComponent::new(vec![join1, join2, leave2]);
let mut rooms: HashMap<BareJid, Room> = HashMap::new(); let mut rooms: HashMap<BareJid, Room> = HashMap::new();
// Ignore self-presence for participant1 component.expect_presence(|_| (), "Self-presence for participant1");
component.expect_presence(|_| ()); component.expect_message(|_| (), "Subject message for participant1");
// Ignore subject message for participant1 component.expect_presence(|_| (), "Participant1 presence for participant2");
component.expect_message(|_| ()); component.expect_presence(|_| (), "Self-presence for participant2");
// Ignore participant1 presence for participant2 component.expect_presence(|_| (), "Participant2 presence for participant1");
component.expect_presence(|_| ()); component.expect_message(|_| (), "Subject message for participant2");
// Ignore self-presence for participant2
component.expect_presence(|_| ());
// Ignore participant2 presence for participant1
component.expect_presence(|_| ());
// Ignore subject message for participant2
component.expect_message(|_| ());
component.expect(
Presence::new(PresenceType::Unavailable)
.with_from(Jid::Full(participant2.clone()))
.with_to(Jid::Full(realjid2.clone()))
.with_payloads(vec![MucUser {
status: vec![MucStatus::SelfPresence],
items: vec![MucItem::new(Affiliation::Owner, Role::None)],
}
.into()]),
);
component.expect( component.expect(
Presence::new(PresenceType::Unavailable) Presence::new(PresenceType::Unavailable)
@ -429,11 +440,111 @@ async fn test_leave_room_not_last() {
.into()]), .into()]),
); );
component.expect(
Presence::new(PresenceType::Unavailable)
.with_from(Jid::Full(participant2.clone()))
.with_to(Jid::Full(realjid2.clone()))
.with_payloads(vec![MucUser {
status: vec![MucStatus::SelfPresence],
items: vec![MucItem::new(Affiliation::Owner, Role::None)],
}
.into()]),
);
handle_stanza(&mut component, &mut rooms).await.unwrap(); handle_stanza(&mut component, &mut rooms).await.unwrap();
component.assert();
assert_eq!(rooms.len(), 1); assert_eq!(rooms.len(), 1);
match rooms.get(&roomjid) { match rooms.get(&roomjid) {
Some(room) => assert_eq!(room.occupants.len(), 1), Some(room) => assert_eq!(room.occupants.len(), 1),
None => panic!(), None => panic!(),
} }
} }
#[tokio::test]
async fn test_join_msn() {
let barejid = BareJid::from_str("foo@bar").unwrap();
let realjid1 = barejid.clone().with_resource("qxx");
let realjid2 = barejid.clone().with_resource("hah");
let roomjid = COMPONENT_JID.clone().with_node("room");
let participant1 = roomjid.clone().with_resource("nick1");
let join1: Element = Presence::new(PresenceType::None)
.with_from(Jid::Full(realjid1.clone()))
.with_to(Jid::Full(participant1.clone()))
.with_payloads(vec![Muc::new().into()])
.into();
let join2: Element = Presence::new(PresenceType::None)
.with_from(Jid::Full(realjid2.clone()))
.with_to(Jid::Full(participant1.clone()))
.with_payloads(vec![Muc::new().into()])
.into();
let mut component = TestComponent::new(vec![join1, join2]);
let mut rooms: HashMap<BareJid, Room> = HashMap::new();
component.expect(
Presence::new(PresenceType::None)
.with_from(Jid::Full(participant1.clone()))
.with_to(Jid::Full(realjid1.clone()))
.with_payloads(vec![MucUser {
status: vec![MucStatus::SelfPresence, MucStatus::AssignedNick],
items: vec![{
let mut item = MucItem::new(Affiliation::Owner, Role::Moderator);
item.jid = Some(realjid1.clone());
item
}],
}.into()])
);
component.expect_message(|_| (), "Subject message for participant1");
// New session joins
// Participant1 presence for participant2
component.expect(
Presence::new(PresenceType::None)
.with_from(Jid::Full(participant1.clone()))
.with_to(Jid::Full(realjid1.clone()))
.with_payloads(vec![MucUser {
status: Vec::new(),
items: {
let item = MucItem::new(Affiliation::Owner, Role::Moderator);
let mut item1 = item.clone();
item1.jid = Some(realjid1.clone());
let mut item2 = item.clone();
item2.jid = Some(realjid2.clone());
vec![item1, item2]
},
}.into()])
);
// Self-presence for participant2
component.expect(
Presence::new(PresenceType::None)
.with_from(Jid::Full(participant1.clone()))
.with_to(Jid::Full(realjid2.clone()))
.with_payloads(vec![MucUser {
status: vec![MucStatus::SelfPresence, MucStatus::AssignedNick],
items: {
let item = MucItem::new(Affiliation::Owner, Role::Moderator);
let mut item1 = item.clone();
item1.jid = Some(realjid1.clone());
let mut item2 = item.clone();
item2.jid = Some(realjid2.clone());
vec![item1, item2]
},
}.into()])
);
component.expect_message(|_| (), "Subject message for participant2");
handle_stanza(&mut component, &mut rooms).await.unwrap();
assert_eq!(rooms.len(), 1);
match rooms.get(&roomjid) {
Some(room) => {
assert_eq!(room.occupants.len(), 1);
assert_eq!(room.occupants.get(&participant1.resource).unwrap().sessions.len(), 2);
},
None => panic!(),
}
}