diff --git a/src/async_client.rs b/src/async_client.rs index c7a01e69..3d972c44 100644 --- a/src/async_client.rs +++ b/src/async_client.rs @@ -790,3 +790,42 @@ impl AsyncClient { Ok(response) } } + +#[cfg(test)] +mod test { + use super::{AsyncClient, Room, Url}; + use crate::identifiers::{RoomId, UserId}; + use crate::events::collections::all::RoomEvent; + + use crate::test_builder::{EventBuilder}; + use crate::{assert_eq_, async_assert}; + + use std::convert::TryFrom; + + #[tokio::test] + async fn room_events() { + + async_assert!{ + async fn test_room_users<'a>(cli: &'a AsyncClient) -> Result<(), String> { + assert_eq_!(cli.homeserver(), &Url::parse("http://localhost:8080").unwrap()); + Ok(()) + } + } + + let rid = RoomId::try_from("!roomid:room.com").unwrap(); + let uid = UserId::try_from("@example:localhost").unwrap(); + + let homeserver = Url::parse("http://localhost:8080").unwrap(); + let client = AsyncClient::new(homeserver, None).unwrap(); + let room = Room::new(&rid, &uid); + let bld = EventBuilder::default(); + let runner = bld.add_room_event_from_file("./tests/data/events/member.json", RoomEvent::RoomMember) + .add_room_event_from_file("./tests/data/events/power_levels.json", RoomEvent::RoomPowerLevels) + .build_client_runner("!roomid:room.com", "@example:localhost") + .set_room(room) + .set_client(client) + .add_client_assert(test_room_users); + + runner.run_test().await; + } +} diff --git a/src/lib.rs b/src/lib.rs index 89c3d2a8..69c132ef 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -25,6 +25,7 @@ //! by default be stored only in memory and thus lost after the client is //! destroyed. #![deny(missing_docs)] +#![feature(type_alias_impl_trait)] pub use crate::{error::Error, error::Result, session::Session}; pub use reqwest::header::InvalidHeaderValue; @@ -40,8 +41,8 @@ mod event_emitter; mod models; mod session; -// TODO remove -mod test_builder; +#[cfg(test)] +pub mod test_builder; #[cfg(feature = "encryption")] mod crypto; diff --git a/src/models/room.rs b/src/models/room.rs index 9de464ba..2549120c 100644 --- a/src/models/room.rs +++ b/src/models/room.rs @@ -389,6 +389,8 @@ mod test { use crate::events::room::member::MembershipState; use crate::identifiers::UserId; use crate::{AsyncClient, Session, SyncSettings}; + use crate::test_builder::{EventBuilder}; + use crate::{assert_, assert_eq_}; use mockito::{mock, Matcher}; use url::Url; @@ -441,4 +443,32 @@ mod test { assert!(room.deref().power_levels.is_some()) } + + #[tokio::test] + async fn room_events() { + + fn test_room_users(room: &Room) -> Result<(), String> { + assert_eq_!(room.members.len(), 1); + Ok(()) + } + + fn test_room_power(room: &Room) -> Result<(), String> { + assert_!(room.power_levels.is_some()); + assert_eq_!(room.power_levels.as_ref().unwrap().kick, js_int::Int::new(50).unwrap()); + let admin = room.members.get(&UserId::try_from("@example:localhost").unwrap()).unwrap(); + assert_eq_!(admin.power_level.unwrap(), js_int::Int::new(100).unwrap()); + Ok(()) + } + + let rid = RoomId::try_from("!roomid:room.com").unwrap(); + let uid = UserId::try_from("@example:localhost").unwrap(); + let bld = EventBuilder::default(); + let runner = bld.add_room_event_from_file("./tests/data/events/member.json", RoomEvent::RoomMember) + .add_room_event_from_file("./tests/data/events/power_levels.json", RoomEvent::RoomPowerLevels) + .build_room_runner(&rid, &uid) + .add_room_assert(test_room_power) + .add_room_assert(test_room_users); + + runner.run_test().await; + } } diff --git a/src/models/room_member.rs b/src/models/room_member.rs index fd3d14d9..e2ef43d0 100644 --- a/src/models/room_member.rs +++ b/src/models/room_member.rs @@ -184,92 +184,66 @@ impl RoomMember { #[cfg(test)] mod test { - use crate::identifiers::{EventId, RoomId, UserId}; - use crate::{AsyncClient, Session, SyncSettings}; - - use serde_json; + use crate::events::room::member::MembershipState; + use crate::events::collections::all::RoomEvent; + use crate::events::presence::PresenceState; + use crate::identifiers::{RoomId, UserId}; + use crate::test_builder::{EventBuilder}; + use crate::{assert_, assert_eq_, Room}; use js_int::{Int, UInt}; - use mockito::{mock, Matcher}; - use url::Url; - use std::collections::HashMap; use std::convert::TryFrom; - use std::ops::Deref; - use std::str::FromStr; - use std::time::Duration; - - use crate::events::room::power_levels::{ - NotificationPowerLevels, PowerLevelsEvent, PowerLevelsEventContent, - }; #[tokio::test] - async fn member_power() { - let homeserver = Url::from_str(&mockito::server_url()).unwrap(); + async fn room_member_events() { + fn test_room_member(room: &Room) -> Result<(), String> { + let member = room.members.get(&UserId::try_from("@example:localhost").unwrap()).unwrap(); + assert_eq_!(member.membership, MembershipState::Join); + assert_eq_!(member.power_level, Int::new(100)); + println!("{:#?}", room); + Ok(()) + } - let session = Session { - access_token: "1234".to_owned(), - user_id: UserId::try_from("@example:localhost").unwrap(), - device_id: "DEVICEID".to_owned(), - }; - - let _m = mock( - "GET", - Matcher::Regex(r"^/_matrix/client/r0/sync\?.*$".to_string()), - ) - .with_status(200) - .with_body_from_file("tests/data/sync.json") - .create(); - - let mut client = AsyncClient::new(homeserver, Some(session)).unwrap(); - - let sync_settings = SyncSettings::new().timeout(Duration::from_millis(3000)); - - let _response = client.sync(sync_settings).await.unwrap(); - - let mut rooms = client.base_client.write().await.joined_rooms.clone(); - let mut room = rooms - .get_mut(&RoomId::try_from("!SVkFJHzfwvuaIEawgC:localhost").unwrap()) - .unwrap() - .lock() - .await; - - let power = power_levels(); - assert!(room.handle_power_level(&power)); - - assert_eq!( - room.deref().power_levels.as_ref().unwrap().ban, - Int::new(40).unwrap() - ); - assert_eq!( - room.deref().power_levels.as_ref().unwrap().notifications, - Int::new(35).unwrap() - ); + let rid = RoomId::try_from("!roomid:room.com").unwrap(); + let uid = UserId::try_from("@example:localhost").unwrap(); + let bld = EventBuilder::default(); + let runner = bld.add_room_event_from_file("./tests/data/events/member.json", RoomEvent::RoomMember) + .add_room_event_from_file("./tests/data/events/power_levels.json", RoomEvent::RoomPowerLevels) + .build_room_runner(&rid, &uid) + .add_room_assert(test_room_member); + + runner.run_test().await; } - fn power_levels() -> PowerLevelsEvent { - PowerLevelsEvent { - content: PowerLevelsEventContent { - ban: Int::new(40).unwrap(), - events: HashMap::default(), - events_default: Int::new(40).unwrap(), - invite: Int::new(40).unwrap(), - kick: Int::new(40).unwrap(), - redact: Int::new(40).unwrap(), - state_default: Int::new(40).unwrap(), - users: HashMap::default(), - users_default: Int::new(40).unwrap(), - notifications: NotificationPowerLevels { - room: Int::new(35).unwrap(), - }, - }, - event_id: EventId::try_from("$h29iv0s8:example.com").unwrap(), - origin_server_ts: UInt::new(1520372800469).unwrap(), - prev_content: None, - room_id: RoomId::try_from("!roomid:room.com").ok(), - unsigned: serde_json::Map::new(), - sender: UserId::try_from("@example:example.com").unwrap(), - state_key: "@example:example.com".into(), + #[tokio::test] + async fn member_presence_events() { + fn test_room_member(room: &Room) -> Result<(), String> { + let member = room.members.get(&UserId::try_from("@example:localhost").unwrap()).unwrap(); + assert_eq_!(member.membership, MembershipState::Join); + assert_eq_!(member.power_level, Int::new(100)); + println!("{:#?}", room); + Ok(()) } + + fn test_presence(room: &Room) -> Result<(), String> { + let member = room.members.get(&UserId::try_from("@example:localhost").unwrap()).unwrap(); + assert_!(member.avatar_url.is_some()); + assert_eq_!(member.last_active_ago, UInt::new(1)); + assert_eq_!(member.presence, Some(PresenceState::Online)); + Ok(()) + } + + let rid = RoomId::try_from("!roomid:room.com").unwrap(); + let uid = UserId::try_from("@example:localhost").unwrap(); + let bld = EventBuilder::default(); + let runner = bld.add_room_event_from_file("./tests/data/events/member.json", RoomEvent::RoomMember) + .add_room_event_from_file("./tests/data/events/power_levels.json", RoomEvent::RoomPowerLevels) + .add_presence_event_from_file("./tests/data/events/presence.json") + .build_room_runner(&rid, &uid) + .add_room_assert(test_presence) + .add_room_assert(test_room_member); + + runner.run_test().await; } } diff --git a/src/test_builder.rs b/src/test_builder.rs index 76dff5c5..dbe6e1e3 100644 --- a/src/test_builder.rs +++ b/src/test_builder.rs @@ -1,210 +1,430 @@ +#![cfg(test)] +use std::fs; +use std::path::Path; +use std::panic; +use crate::identifiers::{RoomId, UserId}; +use crate::events::{ + collections::all::{Event, RoomEvent, StateEvent}, + presence::PresenceEvent, + EventResult, TryFromRaw, +}; +use crate::{AsyncClient}; -#[cfg(test)] -mod test_it { - #![allow(unused)] +use ansi_term::Colour; +use mockito::mock; - use std::fs; - use std::path::Path; - use std::panic; - use std::convert::TryFrom; - use std::str::FromStr; - use std::time::Duration; - - use crate::identifiers::{RoomId, UserId}; - use crate::events::{ - collections::all::RoomEvent, - room::{ - power_levels::PowerLevelsEvent, - }, - EventResult, FromRaw, TryFromRaw, +use crate::models::Room; + +/// `assert` to use in `TestRunner`. +/// +/// This returns an `Err` on failure, instead of panicking. +#[macro_export] +macro_rules! assert_ { + ($truth:expr) => { + if !$truth { + return Err(format!(r#"assertion failed: `(left == right)` + expression: `{:?}` + failed at {}"#, + stringify!($truth), + file!(), + )) + } }; - use crate::{AsyncClient, Session, SyncSettings}; - - use ansi_term::Colour; - use serde_json::Value; - use mockito::{mock, Matcher}; - use serde::{de::DeserializeOwned, Deserialize, Serialize}; - use url::Url; - - use crate::models::Room; - - #[derive(Default)] - pub struct ResponseBuilder { - events: Vec, - } - - pub struct TestRunner { - /// Used when testing the whole client - client: Option, - /// Used To test the models - room: Option, - /// When testing the models a vec of RoomEvents is needed. - events: Vec, - /// A `Vec` of callbacks that should assert something about the client. - /// - /// The callback should panic if the state is unexpected (use `assert_*!` macro) - client_assertions: Vec, - /// A `Vec` of callbacks that should assert something about the room. - /// - /// The callback should panic if the state is unexpected (use `assert_*!` macro) - room_assertions: Vec, - /// `mokito::Mock` - mock: Option, - } - - impl ResponseBuilder { - - /// Creates an `IncomingResponse` to hold events for a sync. - pub fn create_sync_response(mut self) -> Self { - - self - } - - /// Just throw events at the client, not part of a specific response. - pub fn create_event_stream(mut self) -> Self { - - self - } - - /// Add an event to the events `Vec`. - pub fn add_event_from_file>(mut self, path: P, variant: fn(Ev) -> RoomEvent) -> Self { - let val = fs::read_to_string(path.as_ref()).expect(&format!("file not found {:?}", path.as_ref())); - let event = serde_json::from_str::>(&val).unwrap().into_result().unwrap(); - self.add_event(variant(event)); - self - } - - fn add_event(&mut self, event: RoomEvent) { - self.events.push(event) - } - - /// Consumes `ResponseBuilder and returns a `TestRunner`. - /// - /// The `TestRunner` streams the events to the client and enables methods to set assertions - /// about the state of the client. - pub fn build_responses(mut self, method: &str, path: &str) -> TestRunner { - let body = serde_json::to_string(&self.events).unwrap(); - let mock = Some(mock(method, path) - .with_status(200) - .with_body(body) - .create()); - - TestRunner { - client: None, - room: None, - events: Vec::new(), - client_assertions: Vec::new(), - room_assertions: Vec::new(), - mock, - } - } - - /// Consumes `ResponseBuilder and returns a `TestRunner`. - /// - /// The `TestRunner` streams the events to the client and enables methods to set assertions - /// about the state of the client. - pub fn build_room_events(mut self, room_id: &RoomId, user_id: &UserId) -> TestRunner { - TestRunner { - client: None, - room: Some(Room::new(room_id, user_id)), - events: self.events, - client_assertions: Vec::new(), - room_assertions: Vec::new(), - mock: None, - } - } - } - - impl TestRunner { - pub fn set_client(mut self, client: AsyncClient) -> Self { - self.client = Some(client); - self - } - - pub fn add_client_assert(mut self, assert: fn(&AsyncClient)) -> Self { - self.client_assertions.push(assert); - self - } - - pub fn add_room_assert(mut self, assert: fn(&Room)) -> Self { - self.room_assertions.push(assert); - self - } - - fn run_client_tests(mut self) -> Result<(), Vec> { - Ok(()) - } - - fn run_room_tests(mut self) -> Result<(), Vec> { - let mut errs = Vec::new(); - let mut room = self.room.unwrap(); - for event in &self.events { - match event { - RoomEvent::RoomMember(m) => room.handle_membership(m), - RoomEvent::RoomName(n) => room.handle_room_name(n), - RoomEvent::RoomCanonicalAlias(ca) => room.handle_canonical(ca), - RoomEvent::RoomAliases(a) => room.handle_room_aliases(a), - RoomEvent::RoomPowerLevels(p) => room.handle_power_level(p), - // RoomEvent::RoomEncryption(e) => room.handle_encryption_event(e), - _ => todo!("implement more RoomEvent variants"), - }; - } - for assert in self.room_assertions { - if let Err(e) = panic::catch_unwind(|| assert(&room)) { - errs.push(stringify!(e).to_string()); - } - } - if errs.is_empty() { - Ok(()) - } else { - Err(errs) - } - } - - pub fn run_test(mut self) { - let errs = if let Some(room) = &self.room { - self.run_room_tests() - } else if let Some(cli) = &self.client { - self.run_client_tests() - } else { - panic!("must have either AsyncClient or Room") - }; - - if let Err(errs) = errs { - let err_str = errs.join(&format!("{}\n", Colour::Red.paint("Error: "))); - if !errs.is_empty() { - panic!("{}", Colour::Red.paint("some tests failed")); - } - } - } - } - - fn test_room_users(room: &Room) { - assert_eq!(room.members.len(), 1); - } - - fn test_room_power(room: &Room) { - assert!(room.power_levels.is_some()); - assert_eq!(room.power_levels.as_ref().unwrap().kick, js_int::Int::new(50).unwrap()); - let admin = room.members.get(&UserId::try_from("@example:localhost").unwrap()).unwrap(); - assert_eq!(admin.power_level.unwrap(), js_int::Int::new(100).unwrap()); - println!("{:#?}", room); - } - - #[test] - fn room_events() { - let rid = RoomId::try_from("!roomid:room.com").unwrap(); - let uid = UserId::try_from("@example:localhost").unwrap(); - let mut bld = ResponseBuilder::default(); - let runner = bld.add_event_from_file("./tests/data/events/member.json", RoomEvent::RoomMember) - .add_event_from_file("./tests/data/events/power_levels.json", RoomEvent::RoomPowerLevels) - .build_room_events(&rid, &uid) - .add_room_assert(test_room_power) - .add_room_assert(test_room_users); - - runner.run_test(); - } - +} + +/// `assert_eq` to use in `TestRunner. +/// +/// This returns an `Err` on failure, instead of panicking. +#[macro_export] +macro_rules! assert_eq_ { + ($left:expr, $right:expr) => ({ + match (&$left, &$right) { + (left_val, right_val) => { + if !(*left_val == *right_val) { + return Err(format!(r#"assertion failed: `(left == right)` + left: `{:?}`, + right: `{:?}` + failed at {}:{}"#, + &*left_val, + &*right_val, + file!(), + line!() + )) + } + } + } + }); + ($left:expr, $right:expr,) => ({ + $crate::assert_eq!($left, $right) + }); + ($left:expr, $right:expr, $($arg:tt)+) => ({ + match (&($left), &($right)) { + (left_val, right_val) => { + if !(*left_val == *right_val) { + return Err(format!(r#"assertion failed: `(left == right)` + left: `{:?}`, + right: `{:?}` : {} + failed at {}:{}"#, + &*left_val, + &*right_val, + $crate::format_args!($($arg)+), + file!(), + line!(), + )) + } + } + } + }); +} + +/// `assert_ne` to use in `TestRunner. +/// +/// This returns an `Err` on failure, instead of panicking. +#[macro_export] +macro_rules! assert_ne_ { + ($left:expr, $right:expr) => ({ + match (&$left, &$right) { + (left_val, right_val) => { + if (*left_val == *right_val) { + return Err(format!(r#"assertion failed: `(left == right)` + left: `{:?}`, + right: `{:?}` + failed at {}:{}"#, + &*left_val, + &*right_val, + file!(), + line!() + )) + } + } + } + }); + ($left:expr, $right:expr,) => ({ + $crate::assert_eq!($left, $right) + }); + ($left:expr, $right:expr, $($arg:tt)+) => ({ + match (&($left), &($right)) { + (left_val, right_val) => { + if (*left_val == *right_val) { + return Err(format!(r#"assertion failed: `(left == right)` + left: `{:?}`, + right: `{:?}` : {} + failed at {}:{}"#, + &*left_val, + &*right_val, + $crate::format_args!($($arg)+), + file!(), + line!(), + )) + } + } + } + }); +} + +/// Convenience macro for declaring an `async` assert function to store in the `TestRunner`. +/// +/// Declares an async function that can be stored in a struct. +/// +/// # Examples +/// ```rust +/// # use matrix_sdk::AsyncClient; +/// # use url::Url; +/// async_assert!{ +/// async fn foo(cli: &AsyncClient) -> Result<(), String> { +/// assert_eq_!(cli.homeserver(), Url::new("matrix.org")) +/// Ok(()) +/// } +/// } +/// ``` +#[macro_export] +macro_rules! async_assert { + ( + $( #[$attr:meta] )* + $pub:vis async fn $fname:ident<$lt:lifetime> ( $($args:tt)* ) $(-> $Ret:ty)? { + $($body:tt)* + } + ) => ( + $( #[$attr] )* + #[allow(unused_parens)] + $pub fn $fname<$lt> ( $($args)* ) + -> ::std::pin::Pin<::std::boxed::Box + + ::std::marker::Send + $lt>> + { + ::std::boxed::Box::pin(async move { $($body)* }) + } + ); + ( + $( #[$attr:meta] )* + $pub:vis async fn $fname:ident ( $($args:tt)* ) $(-> $Ret:ty)? { + $($body:tt)* + } + ) => ( + $( #[$attr] )* + #[allow(unused_parens)] + $pub fn $fname ( $($args)* ) + -> ::std::pin::Pin<::std::boxed::Box<(dyn ::std::future::Future + + ::std::marker::Send)>> + { + ::std::boxed::Box::pin(async move { $($body)* }) + } + ) +} + +type DynFuture<'lt, T> = ::std::pin::Pin>>; +pub type AsyncAssert = fn(&AsyncClient) -> DynFuture>; + +#[derive(Default)] +pub struct EventBuilder { + /// The events that determine the state of a `Room`. + /// + /// When testing the models `RoomEvent`s are needed. + room_events: Vec, + /// The presence events that determine the presence state of a `RoomMember`. + presence_events: Vec, + /// The state events that determine the state of a `Room`. + state_events: Vec, + /// The state events that determine the state of a `Room`. + non_room_events: Vec, +} + +#[allow(dead_code)] +pub struct TestRunner { + /// Used when testing the whole client + client: Option, + /// Used To test the models + room: Option, + /// The non room events that determine the state of a `Room`. + /// + /// These are ephemeral and account events. + non_room_events: Vec, + /// The events that determine the state of a `Room`. + /// + /// When testing the models `RoomEvent`s are needed. + room_events: Vec, + /// The presence events that determine the presence state of a `RoomMember`. + presence_events: Vec, + /// The state events that determine the state of a `Room`. + state_events: Vec, + /// A `Vec` of callbacks that should assert something about the client. + /// + /// The callback should use the provided `assert_`, `assert_*_` macros. + client_assertions: Vec, + /// A `Vec` of callbacks that should assert something about the room. + /// + /// The callback should use the provided `assert_`, `assert_*_` macros. + room_assertions: Vec Result<(), String>>, + /// `mokito::Mock` + mock: Option, +} + +#[allow(dead_code)] +#[allow(unused_mut)] +impl EventBuilder { + + /// Creates an `IncomingResponse` to hold events for a sync. + pub fn create_sync_response(mut self) -> Self { + + self + } + + /// Just throw events at the client, not part of a specific response. + pub fn create_event_stream(mut self) -> Self { + + self + } + + /// Add an event to the room events `Vec`. + pub fn add_non_event_from_file>(mut self, path: P, variant: fn(Ev) -> Event) -> Self { + let val = fs::read_to_string(path.as_ref()).expect(&format!("file not found {:?}", path.as_ref())); + let event = serde_json::from_str::>(&val).unwrap().into_result().unwrap(); + self.non_room_events.push(variant(event)); + self + } + + /// Add an event to the room events `Vec`. + pub fn add_room_event_from_file>(mut self, path: P, variant: fn(Ev) -> RoomEvent) -> Self { + let val = fs::read_to_string(path.as_ref()).expect(&format!("file not found {:?}", path.as_ref())); + let event = serde_json::from_str::>(&val).unwrap().into_result().unwrap(); + self.room_events.push(variant(event)); + self + } + + /// Add a state event to the state events `Vec`. + pub fn add_state_event_from_file>(mut self, path: P, variant: fn(Ev) -> StateEvent) -> Self { + let val = fs::read_to_string(path.as_ref()).expect(&format!("file not found {:?}", path.as_ref())); + let event = serde_json::from_str::>(&val).unwrap().into_result().unwrap(); + self.state_events.push(variant(event)); + self + } + + /// Add a presence event to the presence events `Vec`. + pub fn add_presence_event_from_file>(mut self, path: P) -> Self { + let val = fs::read_to_string(path.as_ref()).expect(&format!("file not found {:?}", path.as_ref())); + let event = serde_json::from_str::>(&val).unwrap().into_result().unwrap(); + self.presence_events.push(event); + self + } + + /// Consumes `ResponseBuilder and returns a `TestRunner`. + /// + /// The `TestRunner` streams the events to the client and holds methods to make assertions + /// about the state of the client. + pub fn build_client_runner(mut self, method: &str, path: &str) -> TestRunner { + // TODO serialize this properly + let body = serde_json::to_string(&self.room_events).unwrap(); + let mock = Some(mock(method, path) + .with_status(200) + .with_body(body) + .create()); + + TestRunner { + client: None, + room: None, + non_room_events: Vec::new(), + room_events: Vec::new(), + presence_events: Vec::new(), + state_events: Vec::new(), + client_assertions: Vec::new(), + room_assertions: Vec::new(), + mock, + } + } + + /// Consumes `ResponseBuilder and returns a `TestRunner`. + /// + /// The `TestRunner` streams the events to the `Room` and holds methods to make assertions + /// about the state of the `Room`. + pub fn build_room_runner(self, room_id: &RoomId, user_id: &UserId) -> TestRunner { + TestRunner { + client: None, + room: Some(Room::new(room_id, user_id)), + non_room_events: self.non_room_events, + room_events: self.room_events, + presence_events: self.presence_events, + state_events: self.state_events, + client_assertions: Vec::new(), + room_assertions: Vec::new(), + mock: None, + } + } +} + +#[allow(dead_code)] +impl TestRunner { + pub fn set_client(mut self, client: AsyncClient) -> Self { + self.client = Some(client); + self + } + + /// Set `Room` + pub fn set_room(mut self, room: Room) -> Self { + self.room = Some(room); + self + } + + pub fn add_client_assert(mut self, assert: AsyncAssert) -> Self { + self.client_assertions.push(assert); + self + } + + pub fn add_room_assert(mut self, assert: fn(&Room) -> Result<(), String>) -> Self { + self.room_assertions.push(assert); + self + } + + async fn run_client_tests(&mut self) -> Result<(), Vec> { + let mut errs = Vec::new(); + let mut cli = self.client.as_ref().unwrap().base_client.write().await; + let room_id = &self.room.as_ref().unwrap().room_id; + + for event in &self.non_room_events { + match event { + // Event::IgnoredUserList(iu) => room.handle_ignored_users(iu), + Event::Presence(p) => cli.receive_presence_event(room_id, p).await, + // Event::PushRules(pr) => room.handle_push_rules(pr), + // TODO receive ephemeral events + _ => todo!("implement more non room events"), + }; + } + + for event in &self.room_events { + cli.receive_joined_timeline_event(room_id, &mut EventResult::Ok(event.clone())).await; + } + for event in &self.presence_events { + cli.receive_presence_event(room_id, event).await; + } + for event in &self.state_events { + cli.receive_joined_state_event(room_id, event).await; + } + + for assert in &mut self.client_assertions { + if let Err(e) = assert(self.client.as_ref().unwrap()).await { + errs.push(e); + } + } + if errs.is_empty() { + Ok(()) + } else { + Err(errs) + } + } + + fn run_room_tests(&mut self) -> Result<(), Vec> { + let mut errs = Vec::new(); + let room = self.room.as_mut().unwrap(); + + for event in &self.non_room_events { + match event { + // Event::IgnoredUserList(iu) => room.handle_ignored_users(iu), + Event::Presence(p) => room.receive_presence_event(p), + // Event::PushRules(pr) => room.handle_push_rules(pr), + // TODO receive ephemeral events + _ => todo!("implement more non room events"), + }; + } + + for event in &self.room_events { + room.receive_timeline_event(event); + } + for event in &self.presence_events { + room.receive_presence_event(event); + } + for event in &self.state_events { + room.receive_state_event(event); + } + + for assert in &mut self.room_assertions { + if let Err(e) = assert(&room) { + errs.push(e); + } + } + if errs.is_empty() { + Ok(()) + } else { + Err(errs) + } + } + + pub async fn run_test(mut self) { + let (count, errs) = if let Some(_) = &self.room { + (self.room_assertions.len(), self.run_room_tests()) + } else if let Some(_) = &self.client { + (self.client_assertions.len(), self.run_client_tests().await) + } else { + panic!("must have either AsyncClient or Room") + }; + + if let Err(errs) = errs { + let err_str = errs.join(&format!("\n\n")); + println!("{}\n{}", Colour::Red.paint("Error: "), err_str); + if !errs.is_empty() { + panic!("{} tests failed", errs.len()); + } else { + eprintln!("{}. {} passed", Colour::Green.paint("Ok"), count); + } + } + } } diff --git a/tests/data/events/presence.json b/tests/data/events/presence.json new file mode 100644 index 00000000..0c740980 --- /dev/null +++ b/tests/data/events/presence.json @@ -0,0 +1,11 @@ +{ + "content": { + "avatar_url": "mxc://localhost:wefuiwegh8742w", + "currently_active": false, + "last_active_ago": 1, + "presence": "online", + "status_msg": "Making cupcakes" + }, + "sender": "@example:localhost", + "type": "m.presence" +}