forked from lavina/lavina
				
			Compare commits
	
		
			5 Commits
		
	
	
		
			45bd3f3819
			...
			32e228b4d3
		
	
	| Author | SHA1 | Date | 
|---|---|---|
|  | 32e228b4d3 | |
|  | 1996c3d6c7 | |
|  | 99be4fca68 | |
|  | fdb48f3c47 | |
|  | 9f3a97a1e7 | 
|  | @ -4,7 +4,7 @@ use quick_xml::events::Event; | |||
| 
 | ||||
| use lavina_core::room::{RoomId, RoomRegistry}; | ||||
| use proto_xmpp::bind::{BindResponse, Jid, Name, Server}; | ||||
| use proto_xmpp::client::{Iq, IqError, IqErrorType, IqType}; | ||||
| use proto_xmpp::client::{Iq, IqError, IqErrorType, IqType, Message, MessageType}; | ||||
| use proto_xmpp::disco::{Feature, Identity, InfoQuery, Item, ItemQuery}; | ||||
| use proto_xmpp::roster::RosterQuery; | ||||
| use proto_xmpp::session::Session; | ||||
|  | @ -12,7 +12,7 @@ use proto_xmpp::session::Session; | |||
| use crate::proto::IqClientBody; | ||||
| use crate::XmppConnection; | ||||
| 
 | ||||
| use proto_xmpp::xml::ToXml; | ||||
| use proto_xmpp::xml::{Ignore, ToXml}; | ||||
| 
 | ||||
| impl<'a> XmppConnection<'a> { | ||||
|     pub async fn handle_iq(&self, output: &mut Vec<Event<'static>>, iq: Iq<IqClientBody>) { | ||||
|  | @ -87,6 +87,9 @@ impl<'a> XmppConnection<'a> { | |||
|                 }; | ||||
|                 req.serialize(output); | ||||
|             } | ||||
|             IqClientBody::MessageArchiveRequest(request) => { | ||||
|                 // let response = self.mam().await?;
 | ||||
|             } | ||||
|             _ => { | ||||
|                 let req = Iq { | ||||
|                     from: None, | ||||
|  | @ -220,4 +223,22 @@ impl<'a> XmppConnection<'a> { | |||
|         }; | ||||
|         ItemQuery { item } | ||||
|     } | ||||
| 
 | ||||
|     async fn mam(&self) -> Result<Message<Ignore>, ()> { | ||||
|         // return Ok(());
 | ||||
|         Ok(Message::<Ignore> { | ||||
|             from: None, | ||||
|             id: Some("aacea".to_string()), | ||||
|             to: Some(Jid { | ||||
|                 name: Some(Name("nikita".into())), | ||||
|                 server: Server("vlnv.dev".into()), | ||||
|                 resource: None, | ||||
|             }), | ||||
|             r#type: MessageType::Chat, | ||||
|             lang: None, | ||||
|             subject: Some("daa".into()), | ||||
|             body: "bbb".into(), | ||||
|             custom: vec![Ignore], | ||||
|         }) | ||||
|     } | ||||
| } | ||||
|  |  | |||
|  | @ -7,6 +7,7 @@ use lavina_core::prelude::*; | |||
| use proto_xmpp::bind::BindRequest; | ||||
| use proto_xmpp::client::{Iq, Message, Presence}; | ||||
| use proto_xmpp::disco::{InfoQuery, ItemQuery}; | ||||
| use proto_xmpp::mam::MessageArchiveRequest; | ||||
| use proto_xmpp::roster::RosterQuery; | ||||
| use proto_xmpp::session::Session; | ||||
| use proto_xmpp::xml::*; | ||||
|  | @ -18,6 +19,7 @@ pub enum IqClientBody { | |||
|     Roster(RosterQuery), | ||||
|     DiscoInfo(InfoQuery), | ||||
|     DiscoItem(ItemQuery), | ||||
|     MessageArchiveRequest(MessageArchiveRequest), | ||||
|     Unknown(Ignore), | ||||
| } | ||||
| 
 | ||||
|  | @ -38,6 +40,7 @@ impl FromXml for IqClientBody { | |||
|                 RosterQuery, | ||||
|                 InfoQuery, | ||||
|                 ItemQuery, | ||||
|                 MessageArchiveRequest, | ||||
|                 { | ||||
|                     delegate_parsing!(Ignore, namespace, event).into() | ||||
|                 } | ||||
|  |  | |||
|  | @ -1,4 +1,5 @@ | |||
| use std::io::ErrorKind; | ||||
| use std::str::from_utf8; | ||||
| use std::sync::Arc; | ||||
| use std::time::Duration; | ||||
| 
 | ||||
|  | @ -6,6 +7,7 @@ use anyhow::Result; | |||
| use assert_matches::*; | ||||
| use prometheus::Registry as MetricsRegistry; | ||||
| use quick_xml::events::Event; | ||||
| use quick_xml::name::LocalName; | ||||
| use quick_xml::NsReader; | ||||
| use tokio::io::{AsyncBufReadExt, AsyncReadExt, AsyncWriteExt, BufReader}; | ||||
| use tokio::io::{ReadHalf as GenericReadHalf, WriteHalf as GenericWriteHalf}; | ||||
|  | @ -22,6 +24,10 @@ use lavina_core::room::RoomRegistry; | |||
| use projection_xmpp::{launch, RunningServer, ServerConfig}; | ||||
| use proto_xmpp::xml::{Continuation, FromXml, Parser}; | ||||
| 
 | ||||
| fn element_name<'a>(local_name: &LocalName<'a>) -> &'a str { | ||||
|     from_utf8(local_name.into_inner()).unwrap() | ||||
| } | ||||
| 
 | ||||
| pub async fn read_irc_message(reader: &mut BufReader<ReadHalf<'_>>, buf: &mut Vec<u8>) -> Result<usize> { | ||||
|     let mut size = 0; | ||||
|     let res = reader.read_until(b'\n', buf).await?; | ||||
|  | @ -107,6 +113,7 @@ impl<'a> TestScopeTls<'a> { | |||
| } | ||||
| 
 | ||||
| struct IgnoreCertVerification; | ||||
| 
 | ||||
| impl ServerCertVerifier for IgnoreCertVerification { | ||||
|     fn verify_server_cert( | ||||
|         &self, | ||||
|  | @ -128,6 +135,7 @@ struct TestServer { | |||
|     players: PlayerRegistry, | ||||
|     server: RunningServer, | ||||
| } | ||||
| 
 | ||||
| impl TestServer { | ||||
|     async fn start() -> Result<TestServer> { | ||||
|         let _ = tracing_subscriber::fmt::try_init(); | ||||
|  | @ -300,3 +308,88 @@ async fn terminate_socket() -> Result<()> { | |||
| 
 | ||||
|     Ok(()) | ||||
| } | ||||
| 
 | ||||
| #[tokio::test] | ||||
| async fn test_message_archive_request() -> 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); | ||||
|     tracing::info!("TCP connection established"); | ||||
| 
 | ||||
|     s.send(r#"<?xml version="1.0"?>"#).await?; | ||||
|     s.send(r#"<stream:stream xmlns:stream="http://etherx.jabber.org/streams" to="127.0.0.1" xml:lang="en" xmlns:xml="http://www.w3.org/XML/1998/namespace" xmlns="jabber:client" version="1.0">"#).await?; | ||||
|     assert_matches!(s.next_xml_event().await?, Event::Decl(_) => {}); | ||||
|     assert_matches!(s.next_xml_event().await?, Event::Start(b) => assert_eq!(b.local_name().into_inner(), b"stream")); | ||||
|     assert_matches!(s.next_xml_event().await?, Event::Start(b) => assert_eq!(b.local_name().into_inner(), b"features")); | ||||
|     assert_matches!(s.next_xml_event().await?, Event::Start(b) => assert_eq!(b.local_name().into_inner(), b"starttls")); | ||||
|     assert_matches!(s.next_xml_event().await?, Event::Empty(b) => assert_eq!(b.local_name().into_inner(), b"required")); | ||||
|     assert_matches!(s.next_xml_event().await?, Event::End(b) => assert_eq!(b.local_name().into_inner(), b"starttls")); | ||||
|     assert_matches!(s.next_xml_event().await?, Event::End(b) => assert_eq!(b.local_name().into_inner(), b"features")); | ||||
|     s.send(r#"<starttls/>"#).await?; | ||||
|     assert_matches!(s.next_xml_event().await?, Event::Empty(b) => assert_eq!(b.local_name().into_inner(), b"proceed")); | ||||
|     let buffer = s.buffer; | ||||
|     tracing::info!("TLS feature negotiation complete"); | ||||
| 
 | ||||
|     let connector = TlsConnector::from(Arc::new( | ||||
|         ClientConfig::builder() | ||||
|             .with_safe_defaults() | ||||
|             .with_custom_certificate_verifier(Arc::new(IgnoreCertVerification)) | ||||
|             .with_no_client_auth(), | ||||
|     )); | ||||
|     tracing::info!("Initiating TLS connection..."); | ||||
|     let mut stream = connector.connect(ServerName::IpAddress(server.server.addr.ip()), stream).await?; | ||||
|     tracing::info!("TLS connection established"); | ||||
| 
 | ||||
|     let mut s = TestScopeTls::new(&mut stream, buffer); | ||||
| 
 | ||||
|     s.send(r#"<?xml version="1.0"?>"#).await?; | ||||
|     s.send(r#"<stream:stream xmlns:stream="http://etherx.jabber.org/streams" to="127.0.0.1" xml:lang="en" xmlns:xml="http://www.w3.org/XML/1998/namespace" xmlns="jabber:client" version="1.0">"#).await?; | ||||
|     assert_matches!(s.next_xml_event().await?, Event::Decl(_) => {}); | ||||
|     assert_matches!(s.next_xml_event().await?, Event::Start(b) => assert_eq!(b.local_name().into_inner(), b"stream")); | ||||
| 
 | ||||
|     assert_matches!(s.next_xml_event().await?, Event::Start(b) => { | ||||
|         let local_name: &str = from_utf8(b.local_name().into_inner()).unwrap(); | ||||
|         assert_eq!(local_name, "features") | ||||
|     }); | ||||
|     assert_matches!(s.next_xml_event().await?, Event::Start(b) => { | ||||
|         let local_name: &str = from_utf8(b.local_name().into_inner()).unwrap(); | ||||
|         assert_eq!(local_name, "mechanisms") | ||||
|     }); | ||||
|     assert_matches!(s.next_xml_event().await?, Event::Start(b) => { | ||||
|         let local_name: &str = from_utf8(b.local_name().into_inner()).unwrap(); | ||||
|         assert_eq!(local_name, "mechanism") | ||||
|     }); | ||||
|     assert_matches!(s.next_xml_event().await?, Event::Text(b) => { | ||||
|         // b.
 | ||||
|         // let local_name: &str = from_utf8(b.local_name().into_inner()).unwrap();
 | ||||
|         // assert_eq!(local_name, "mechanism")
 | ||||
|     }); | ||||
|     assert_matches!(s.next_xml_event().await?, Event::End(b) => { | ||||
|         // let local_name: &str = from_utf8(b.local_name().into_inner()).unwrap();
 | ||||
|         // assert_eq!(local_name, "mechanism")
 | ||||
|     }); | ||||
|     assert_matches!(s.next_xml_event().await?, Event::End(b) => { | ||||
|         let local_name: &str = from_utf8(b.local_name().into_inner()).unwrap(); | ||||
|         assert_eq!(local_name, "mechanisms") | ||||
|     }); | ||||
|     assert_matches!(s.next_xml_event().await?, Event::End(b) => { | ||||
|         assert_eq!(element_name(&b.local_name()), "features") | ||||
|     }); | ||||
|     // s.send(r#"<iq type='get' id='juliet1'><query xmlns='urn:xmpp:mam:tmp' queryid='f27'/></iq>"#).await?;
 | ||||
|     assert_matches!(s.next_xml_event().await?, Event::Start(b) => { | ||||
|         assert_eq!(element_name(&b.local_name()), "message") | ||||
|     }); | ||||
| 
 | ||||
|     stream.shutdown().await?; | ||||
| 
 | ||||
|     // wrap up
 | ||||
| 
 | ||||
|     server.server.terminate().await?; | ||||
|     Ok(()) | ||||
| } | ||||
|  |  | |||
|  | @ -74,8 +74,8 @@ impl Jid { | |||
| pub struct BindRequest(pub Resource); | ||||
| 
 | ||||
| impl FromXmlTag for BindRequest { | ||||
|     const NS: &'static str = XMLNS; | ||||
|     const NAME: &'static str = "bind"; | ||||
|     const NS: &'static str = XMLNS; | ||||
| } | ||||
| 
 | ||||
| impl FromXml for BindRequest { | ||||
|  |  | |||
|  | @ -1,8 +1,11 @@ | |||
| #![feature(coroutines, coroutine_trait, type_alias_impl_trait, impl_trait_in_assoc_type)] | ||||
| 
 | ||||
| extern crate core; | ||||
| 
 | ||||
| pub mod bind; | ||||
| pub mod client; | ||||
| pub mod disco; | ||||
| pub mod mam; | ||||
| pub mod muc; | ||||
| mod prelude; | ||||
| pub mod roster; | ||||
|  |  | |||
|  | @ -0,0 +1,196 @@ | |||
| use anyhow::{anyhow, Result}; | ||||
| use quick_xml::events::Event; | ||||
| use quick_xml::name::{Namespace, ResolveResult}; | ||||
| use std::io::Read; | ||||
| 
 | ||||
| use crate::xml::*; | ||||
| 
 | ||||
| pub const MAM_XMLNS: &'static str = "urn:xmpp:mam:2"; | ||||
| pub const DATA_XMLNS: &'static str = "jabber:x:data"; | ||||
| 
 | ||||
| #[derive(PartialEq, Eq, Debug, Clone)] | ||||
| pub struct MessageArchiveRequest { | ||||
|     pub x: Option<X>, | ||||
| } | ||||
| 
 | ||||
| #[derive(PartialEq, Eq, Debug, Clone)] | ||||
| pub struct X { | ||||
|     pub fields: Vec<Field>, | ||||
| } | ||||
| 
 | ||||
| #[derive(PartialEq, Eq, Debug, Clone)] | ||||
| pub struct Field { | ||||
|     pub values: Vec<String>, | ||||
| } | ||||
| 
 | ||||
| impl FromXmlTag for X { | ||||
|     const NAME: &'static str = "x"; | ||||
|     const NS: &'static str = DATA_XMLNS; | ||||
| } | ||||
| 
 | ||||
| impl FromXmlTag for MessageArchiveRequest { | ||||
|     const NAME: &'static str = "query"; | ||||
|     const NS: &'static str = MAM_XMLNS; | ||||
| } | ||||
| 
 | ||||
| impl FromXml for X { | ||||
|     type P = impl Parser<Output = Result<Self>>; | ||||
| 
 | ||||
|     fn parse() -> Self::P { | ||||
|         |(mut namespace, mut event): (ResolveResult<'static>, &'static Event<'static>)| -> Result<Self> { | ||||
|             println!("X::parse {:?}", event); | ||||
| 
 | ||||
|             let bytes = match event { | ||||
|                 Event::Start(bytes) if bytes.name().0 == X::NAME.as_bytes() => bytes, | ||||
|                 Event::Empty(bytes) if bytes.name().0 == X::NAME.as_bytes() => return Ok(X { fields: vec![] }), | ||||
|                 _ => return Err(anyhow!("Unexpected XML event: {event:?}")), | ||||
|             }; | ||||
|             let mut fields = vec![]; | ||||
|             loop { | ||||
|                 (namespace, event) = yield; | ||||
|                 match event { | ||||
|                     Event::Start(bytes) => { | ||||
|                         // start of <field>
 | ||||
|                         let mut values = vec![]; | ||||
|                         loop { | ||||
|                             (namespace, event) = yield; | ||||
|                             match event { | ||||
|                                 Event::Start(bytes) if bytes.name().0 == b"value" => { | ||||
|                                     // start of <value>
 | ||||
|                                 } | ||||
|                                 Event::End(bytes) if bytes.name().0 == b"field" => { | ||||
|                                     // end of </field>
 | ||||
|                                     break; | ||||
|                                 } | ||||
|                                 _ => return Err(anyhow!("Unexpected XML event: {event:?}")), | ||||
|                             } | ||||
|                             (namespace, event) = yield; | ||||
|                             let text: String = match event { | ||||
|                                 Event::Text(bytes) => { | ||||
|                                     // text inside <value></value>
 | ||||
|                                     String::from_utf8(bytes.to_vec())? | ||||
|                                 } | ||||
|                                 _ => return Err(anyhow!("Unexpected XML event: {event:?}")), | ||||
|                             }; | ||||
|                             (namespace, event) = yield; | ||||
|                             match event { | ||||
|                                 Event::End(bytes) if bytes.name().0 == b"value" => { | ||||
|                                     // end of </value>
 | ||||
|                                 } | ||||
|                                 _ => return Err(anyhow!("Unexpected XML event: {event:?}")), | ||||
|                             } | ||||
|                             values.push(text); | ||||
|                         } | ||||
|                         fields.push(Field { values }) | ||||
|                     } | ||||
|                     Event::End(bytes) if bytes.name().0 == X::NAME.as_bytes() => { | ||||
|                         // end of <x/>
 | ||||
|                         return Ok(X { fields }); | ||||
|                     } | ||||
|                     _ => return Err(anyhow!("Unexpected XML event: {event:?}")), | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| impl FromXml for MessageArchiveRequest { | ||||
|     type P = impl Parser<Output = Result<Self>>; | ||||
| 
 | ||||
|     fn parse() -> Self::P { | ||||
|         |(mut namespace, mut event): (ResolveResult<'static>, &'static Event<'static>)| -> Result<Self> { | ||||
|             println!("MessageArchiveRequest::parse {:?}", event); | ||||
| 
 | ||||
|             let bytes = match event { | ||||
|                 Event::Empty(_) => return Ok(MessageArchiveRequest { x: None }), | ||||
|                 Event::Start(bytes) => bytes, | ||||
|                 _ => return Err(anyhow!("Unexpected XML event: {event:?}")), | ||||
|             }; | ||||
|             if bytes.name().0 != MessageArchiveRequest::NAME.as_bytes() { | ||||
|                 return Err(anyhow!("Unexpected XML tag: {:?}", bytes.name())); | ||||
|             } | ||||
|             let ResolveResult::Bound(Namespace(ns)) = namespace else { | ||||
|                 return Err(anyhow!("No namespace provided")); | ||||
|             }; | ||||
|             if ns != MAM_XMLNS.as_bytes() { | ||||
|                 return Err(anyhow!("Incorrect namespace")); | ||||
|             } | ||||
|             (namespace, event) = yield; | ||||
|             match event { | ||||
|                 Event::End(bytes) if bytes.name().0 == MessageArchiveRequest::NAME.as_bytes() => { | ||||
|                     Ok(MessageArchiveRequest { x: None }) | ||||
|                 } | ||||
|                 Event::Start(bytes) | Event::Empty(bytes) if bytes.name().0 == X::NAME.as_bytes() => { | ||||
|                     let x = delegate_parsing!(X, namespace, event)?; | ||||
|                     Ok(MessageArchiveRequest { x: Some(x) }) | ||||
|                 } | ||||
|                 _ => Err(anyhow!("Unexpected XML event: {event:?}")), | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| impl MessageArchiveRequest {} | ||||
| 
 | ||||
| #[cfg(test)] | ||||
| mod tests { | ||||
|     use super::*; | ||||
|     use crate::bind::{Jid, Name, Server}; | ||||
|     use crate::client::{Iq, IqType}; | ||||
| 
 | ||||
|     #[test] | ||||
|     fn test_parse_archive_query() { | ||||
|         let input = r#"<iq to='pubsub.shakespeare.lit' type='set' id='juliet1'><query xmlns='urn:xmpp:mam:2' queryid='f28'/></iq>"#; | ||||
| 
 | ||||
|         let result: Iq<MessageArchiveRequest> = parse(input).unwrap(); | ||||
|         assert_eq!( | ||||
|             result, | ||||
|             Iq { | ||||
|                 from: None, | ||||
|                 id: "juliet1".to_string(), | ||||
|                 to: Option::from(Jid { | ||||
|                     name: None, | ||||
|                     server: Server("pubsub.shakespeare.lit".into()), | ||||
|                     resource: None, | ||||
|                 }), | ||||
|                 r#type: IqType::Set, | ||||
|                 body: MessageArchiveRequest { x: None }, | ||||
|             } | ||||
|         ); | ||||
|     } | ||||
| 
 | ||||
|     #[test] | ||||
|     fn test_parse_query_messages_from_jid() { | ||||
|         let input = r#"<iq type='set' id='juliet1'><query xmlns='urn:xmpp:mam:2'><x xmlns='jabber:x:data' type='submit'><field var='FORM_TYPE' type='hidden'><value>value1</value></field><field var='with'><value>juliet@capulet.lit</value></field></x></query></iq>"#; | ||||
| 
 | ||||
|         let result: Iq<MessageArchiveRequest> = parse(input).unwrap(); | ||||
|         assert_eq!( | ||||
|             result, | ||||
|             Iq { | ||||
|                 from: None, | ||||
|                 id: "juliet1".to_string(), | ||||
|                 to: None, | ||||
|                 r#type: IqType::Set, | ||||
|                 body: MessageArchiveRequest { | ||||
|                     x: Some(X { | ||||
|                         fields: vec![ | ||||
|                             Field { | ||||
|                                 values: vec!["value1".to_string()], | ||||
|                             }, | ||||
|                             Field { | ||||
|                                 values: vec!["juliet@capulet.lit".to_string()], | ||||
|                             }, | ||||
|                         ] | ||||
|                     }) | ||||
|                 }, | ||||
|             } | ||||
|         ); | ||||
|     } | ||||
| 
 | ||||
|     #[test] | ||||
|     fn test_parse_query_messages_from_jid_with_unclosed_tag() { | ||||
|         let input = r#"<iq type='set' id='juliet1'><query xmlns='urn:xmpp:mam:2'><x xmlns='jabber:x:data' type='submit'><field var='FORM_TYPE' type='hidden'><value>value1</value></field><field var='with'><value>juliet@capulet.lit</value></field></query></iq>"#; | ||||
| 
 | ||||
|         assert!(parse::<Iq<MessageArchiveRequest>>(input).is_err()) | ||||
|     } | ||||
| } | ||||
|  | @ -2,7 +2,7 @@ use quick_xml::events::{BytesStart, Event}; | |||
| 
 | ||||
| use crate::xml::*; | ||||
| use anyhow::{anyhow, Result}; | ||||
| use quick_xml::name::ResolveResult; | ||||
| use quick_xml::name::{Namespace, ResolveResult}; | ||||
| 
 | ||||
| pub const XMLNS: &'static str = "jabber:iq:roster"; | ||||
| 
 | ||||
|  | @ -14,6 +14,9 @@ impl FromXml for RosterQuery { | |||
| 
 | ||||
|     fn parse() -> Self::P { | ||||
|         |(mut namespace, mut event): (ResolveResult<'static>, &'static Event<'static>)| -> Result<Self> { | ||||
|             let ResolveResult::Bound(Namespace(ns)) = namespace else { | ||||
|                 return Err(anyhow!("No namespace provided")); | ||||
|             }; | ||||
|             match event { | ||||
|                 Event::Start(_) => (), | ||||
|                 Event::Empty(_) => return Ok(RosterQuery), | ||||
|  | @ -38,3 +41,39 @@ impl ToXml for RosterQuery { | |||
|         events.push(Event::Empty(BytesStart::new(format!(r#"query xmlns="{}""#, XMLNS)))); | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| #[cfg(test)] | ||||
| mod tests { | ||||
|     use super::*; | ||||
|     use crate::bind::{Jid, Name, Resource, Server}; | ||||
|     use crate::client::{Iq, IqType}; | ||||
| 
 | ||||
|     #[test] | ||||
|     fn test_parse() { | ||||
|         let input = | ||||
|             r#"<iq from='juliet@example.com/balcony' id='bv1bs71f' type='get'><query xmlns='jabber:iq:roster'/></iq>"#; | ||||
| 
 | ||||
|         let result: Iq<RosterQuery> = parse(input).unwrap(); | ||||
|         assert_eq!( | ||||
|             result, | ||||
|             Iq { | ||||
|                 from: Option::from(Jid { | ||||
|                     name: Option::from(Name("juliet".into())), | ||||
|                     server: Server("example.com".into()), | ||||
|                     resource: Option::from(Resource("balcony".into())), | ||||
|                 }), | ||||
|                 id: "bv1bs71f".to_string(), | ||||
|                 to: None, | ||||
|                 r#type: IqType::Get, | ||||
|                 body: RosterQuery, | ||||
|             } | ||||
|         ) | ||||
|     } | ||||
| 
 | ||||
|     #[test] | ||||
|     fn test_missing_namespace() { | ||||
|         let input = r#"<iq from='juliet@example.com/balcony' id='bv1bs71f' type='get'><query/></iq>"#; | ||||
| 
 | ||||
|         assert!(parse::<Iq<RosterQuery>>(input).is_err()); | ||||
|     } | ||||
| } | ||||
|  |  | |||
		Loading…
	
		Reference in New Issue