From 5491838228ce4a70771adc9eae59c8e1b132587a Mon Sep 17 00:00:00 2001 From: Devin R Date: Wed, 22 Apr 2020 17:39:57 -0400 Subject: [PATCH] state_store: fix docs, test client with default state store --- design.md | 13 ++-- src/async_client.rs | 113 ++++++++++++++++++++++++++++---- src/base_client.rs | 32 ++++++++- src/request_builder.rs | 4 +- src/state/mod.rs | 33 ++++++++-- src/state/state_store.rs | 136 ++++++++++++++++++++++++++++++--------- src/test_builder.rs | 16 ++--- 7 files changed, 279 insertions(+), 68 deletions(-) diff --git a/design.md b/design.md index 43e9d7d8..1e2f2c26 100644 --- a/design.md +++ b/design.md @@ -12,14 +12,14 @@ The highest level structure that ties the other pieces of functionality together #### Base Client/Client State Machine In addition to Http, the `AsyncClient` passes along methods from the `BaseClient` that deal with `Room`s and `RoomMember`s. This allows the client to keep track of more complicated information that needs to be calculated in some way. - - human readable room names + - human-readable room names - power level? - ignored list? - push rulesset? - more? #### Crypto State Machine -Given a Matrix response the crypto machine will update it's internal state, along with encryption information this means keeping track of when to encrypt. It has knowledge of when encryption needs to happen and can be asked from the `BaseClient`. The crypto state machine is given responses that relate to encryption and can create encrypted request bodies for encryption related requests. Basically it tells the `BaseClient` to send a to-device messages out and the `BaseClient` is responsible for notifying the crypto state machine when it sent the message so crypto can update state. +Given a Matrix response the crypto machine will update it's internal state, along with encryption information this means keeping track of when to encrypt. It knows when encryption needs to happen based on signals from the `BaseClient`. The crypto state machine is given responses that relate to encryption and can create encrypted request bodies for encryption-related requests. Basically it tells the `BaseClient` to send to-device messages out, and the `BaseClient` is responsible for notifying the crypto state machine when it sent the message so crypto can update state. #### Client State/Room and RoomMember The `BaseClient` is responsible for keeping state in sync through the `IncomingResponse`s of `AsyncClient` or querying the `StateStore`. By processing and then delegating incoming `RoomEvent`s, `StateEvent`s, `PresenceEvent`, `IncomingAccountData` and `EphemeralEvent`s to the correct `Room` in the base clients `HashMap` or further to `Room`'s `RoomMember` via the members `HashMap`. The `BaseClient` is also responsible for emitting the incoming events to the `EventEmitter` trait. @@ -87,12 +87,13 @@ pub struct RoomMember { ``` #### State Store -The `BaseClient` also has access to a `dyn StateStore` this is an abstraction around a "database" to keep the client state without requesting a full sync from the server on startup. A default implementation that serializes/deserializes JSON to files in a specified directory can be used. The user can also implement `StateStore` to fit any storage solution they choose. - - load - - store/save +The `BaseClient` also has access to a `dyn StateStore` this is an abstraction around a "database" to keep the client state without requesting a full sync from the server on startup. A default implementation that serializes/deserializes JSON to files in a specified directory can be used. The user can also implement `StateStore` to fit any storage solution they choose. The base client handles the storage automatically. There "may be/are TODO" ways for the user to interact directly. The room event handling methods signal if the state was modified; if so, we check if some room state file needs to be overwritten. + - open + - load client/room or rooms + - store client/room - update ?? -The state store will restore our client state in the `BaseClient` and client authors can just get the latest state that they want to present from the client object. No need to ask the state store for it, this may change if custom setups request this. `StateStore`'s base is load/store and internally to the crate update the `BaseClient`. +The state store will restore our client state in the `BaseClient` and client authors can just get the latest state that they want to present from the client object. No need to ask the state store for it, this may change if custom setups request this. `StateStore`'s main purpose is to provide load/store functionality and, internally to the crate, update the `BaseClient`. #### Event Emitter The consumer of this crate can implement the `EventEmitter` trait for full control over how incoming events are handled by their client. If that isn't enough it is possible to receive every incoming response with the `AsyncClient::sync_forever` callback. diff --git a/src/async_client.rs b/src/async_client.rs index ffbc0f61..5c3b33c9 100644 --- a/src/async_client.rs +++ b/src/async_client.rs @@ -15,6 +15,8 @@ use std::collections::HashMap; use std::convert::{TryFrom, TryInto}; +use std::ops::Deref; +use std::path::{Path, PathBuf}; use std::result::Result as StdResult; use std::sync::Arc; use std::time::{Duration, Instant}; @@ -46,6 +48,7 @@ use crate::api; use crate::base_client::Client as BaseClient; use crate::models::Room; use crate::session::Session; +use crate::state::{ClientState, JsonStore, StateStore}; use crate::VERSION; use crate::{Error, EventEmitter, Result}; @@ -62,6 +65,8 @@ pub struct AsyncClient { http_client: reqwest::Client, /// User session data. pub(crate) base_client: Arc>, + /// The path to the default state store. + state_store_path: Option, } impl std::fmt::Debug for AsyncClient { @@ -70,7 +75,7 @@ impl std::fmt::Debug for AsyncClient { } } -#[derive(Default, Debug)] +#[derive(Default)] /// Configuration for the creation of the `AsyncClient`. /// /// # Example @@ -88,6 +93,19 @@ pub struct AsyncClientConfig { proxy: Option, user_agent: Option, disable_ssl_verification: bool, + store_path: Option, + state_store: Option>, +} + +impl std::fmt::Debug for AsyncClientConfig { + fn fmt(&self, fmt: &mut std::fmt::Formatter<'_>) -> StdResult<(), std::fmt::Error> { + fmt.debug_struct("AsyncClientConfig") + .field("proxy", &self.proxy) + .field("user_agent", &self.user_agent) + .field("disable_ssl_verification", &self.disable_ssl_verification) + .field("store_path", &self.store_path) + .finish() + } } impl AsyncClientConfig { @@ -129,6 +147,23 @@ impl AsyncClientConfig { self.user_agent = Some(HeaderValue::from_str(user_agent)?); Ok(self) } + + /// Set the path for the default `StateStore`. + /// + /// When the path is set `AsyncClient` will set the state store + /// to `JsonStore`. + pub fn state_store_path>(mut self, path: P) -> Self { + self.store_path = Some(path.as_ref().to_owned()); + self + } + + /// Set a custom implementation of a `StateStore`. + /// + /// The state store should be "connected" before being set. + pub fn state_store(mut self, store: Box) -> Self { + self.state_store = Some(store); + self + } } #[derive(Debug, Default, Clone)] @@ -253,10 +288,20 @@ impl AsyncClient { let http_client = http_client.default_headers(headers).build()?; + let mut base_client = BaseClient::new(session)?; + if let Some(path) = config.store_path.as_ref() { + let store = JsonStore; + store.open(path)?; + base_client.state_store = Some(Box::new(store)); + } else if let Some(store) = config.state_store { + base_client.state_store = Some(store); + }; + Ok(Self { homeserver, http_client, - base_client: Arc::new(RwLock::new(BaseClient::new(session)?)), + base_client: Arc::new(RwLock::new(base_client)), + state_store_path: config.store_path, }) } @@ -337,7 +382,15 @@ impl AsyncClient { let response = self.send(request).await?; let mut client = self.base_client.write().await; - client.receive_login_response(&response).await?; + // TODO avoid allocation somehow? + let path = self.state_store_path.as_ref().map(|p| { + let mut path = PathBuf::from(p); + path.push(response.user_id.to_string()); + path + }); + client + .receive_login_response(&response, path.as_ref()) + .await?; Ok(response) } @@ -485,7 +538,7 @@ impl AsyncClient { /// .name("name") /// .room_version("v1.0"); /// - /// let mut cli = AsyncClient::<(), ()>::new(homeserver, None).unwrap(); + /// let mut cli = AsyncClient::new(homeserver, None).unwrap(); /// # use futures::executor::block_on; /// # block_on(async { /// assert!(cli.create_room(builder).await.is_ok()); @@ -529,7 +582,7 @@ impl AsyncClient { /// .direction(Direction::Backward) /// .limit(UInt::new(10).unwrap()); /// - /// let mut cli = AsyncClient::<(), ()>::new(homeserver, None).unwrap(); + /// let mut cli = AsyncClient::new(homeserver, None).unwrap(); /// # use futures::executor::block_on; /// # block_on(async { /// assert!(cli.room_messages(builder).await.is_ok()); @@ -560,12 +613,15 @@ impl AsyncClient { let mut response = self.send(request).await?; + let mut updated = false; for (room_id, room) in &mut response.rooms.join { let matrix_room = { let mut client = self.base_client.write().await; for event in &room.state.events { if let EventResult::Ok(e) = event { - client.receive_joined_state_event(&room_id, &e).await; + if client.receive_joined_state_event(&room_id, &e).await { + updated = true; + } } } @@ -586,9 +642,14 @@ impl AsyncClient { for mut event in &mut room.timeline.events { let decrypted_event = { let mut client = self.base_client.write().await; - client - .receive_joined_timeline_event(room_id, &mut event) - .await + let mut timeline_update = false; + let decrypt_ev = client + .receive_joined_timeline_event(room_id, &mut event, &mut timeline_update) + .await; + if timeline_update { + updated = true; + }; + decrypt_ev }; if let Some(e) = decrypted_event { @@ -606,7 +667,9 @@ impl AsyncClient { { if let EventResult::Ok(e) = account_data { let mut client = self.base_client.write().await; - client.receive_account_data_event(&room_id, e).await; + if client.receive_account_data_event(&room_id, e).await { + updated = true; + } client.emit_account_data_event(room_id, e).await; } } @@ -619,7 +682,9 @@ impl AsyncClient { { if let EventResult::Ok(e) = presence { let mut client = self.base_client.write().await; - client.receive_presence_event(&room_id, e).await; + if client.receive_presence_event(&room_id, e).await { + updated = true; + } client.emit_presence_event(room_id, e).await; } @@ -630,17 +695,37 @@ impl AsyncClient { { if let EventResult::Ok(e) = ephemeral { let mut client = self.base_client.write().await; - client.receive_ephemeral_event(&room_id, e).await; + if client.receive_ephemeral_event(&room_id, e).await { + updated = true; + } client.emit_ephemeral_event(room_id, e).await; } } } + + if updated { + if let Some(store) = self.base_client.read().await.state_store.as_ref() { + if let Some(path) = self.state_store_path.as_ref() { + store + .store_room_state(&path, matrix_room.read().await.deref()) + .await?; + }; + } + } } let mut client = self.base_client.write().await; client.receive_sync_response(&mut response).await; + if updated { + if let Some(store) = client.state_store.as_ref() { + if let Some(path) = self.state_store_path.as_ref() { + let state = ClientState::from_base_client(&client); + store.store_client_state(&path, state).await?; + }; + } + } Ok(response) } @@ -673,7 +758,7 @@ impl AsyncClient { /// # use futures::executor::block_on; /// # block_on(async { /// # let homeserver = Url::parse("http://localhost:8080").unwrap(); - /// # let mut client = AsyncClient::<(), ()>::new(homeserver, None).unwrap(); + /// # let mut client = AsyncClient::new(homeserver, None).unwrap(); /// /// use async_std::sync::channel; /// @@ -862,7 +947,7 @@ impl AsyncClient { /// use matrix_sdk::events::room::message::{MessageEventContent, TextMessageEventContent}; /// # block_on(async { /// # let homeserver = Url::parse("http://localhost:8080").unwrap(); - /// # let mut client = AsyncClient::<(), ()>::new(homeserver, None).unwrap(); + /// # let mut client = AsyncClient::new(homeserver, None).unwrap(); /// # let room_id = RoomId::try_from("!test:localhost").unwrap(); /// use uuid::Uuid; /// diff --git a/src/base_client.rs b/src/base_client.rs index 0fd8e3bd..92c191f6 100644 --- a/src/base_client.rs +++ b/src/base_client.rs @@ -19,6 +19,7 @@ use std::collections::HashSet; use std::fmt; use std::sync::Arc; +use std::path::PathBuf; #[cfg(feature = "encryption")] use std::result::Result as StdResult; @@ -34,7 +35,7 @@ use crate::events::EventResult; use crate::identifiers::{RoomId, UserId}; use crate::models::Room; use crate::session::Session; -use crate::state::StateStore; +use crate::state::{ClientState, StateStore}; use crate::EventEmitter; #[cfg(feature = "encryption")] @@ -144,6 +145,7 @@ impl Client { pub async fn receive_login_response( &mut self, response: &api::session::login::Response, + store_path: Option<&PathBuf>, ) -> Result<()> { let session = Session { access_token: response.access_token.clone(), @@ -158,6 +160,27 @@ impl Client { *olm = Some(OlmMachine::new(&response.user_id, &response.device_id)?); } + if let Some(path) = store_path { + if let Some(store) = self.state_store.as_ref() { + let ClientState { + session, + sync_token, + ignored_users, + push_ruleset, + } = store.load_client_state(&path).await?; + let mut rooms = store.load_all_rooms(&path).await?; + + self.joined_rooms = rooms + .drain() + .map(|(k, room)| (k, Arc::new(RwLock::new(room)))) + .collect(); + self.session = session; + self.sync_token = sync_token; + self.ignored_users = ignored_users; + self.push_ruleset = push_ruleset; + } + } + Ok(()) } @@ -235,10 +258,14 @@ impl Client { /// * `room_id` - The unique id of the room the event belongs to. /// /// * `event` - The event that should be handled by the client. + /// + /// * `did_update` - This is used internally to confirm when the state has + /// been updated. pub async fn receive_joined_timeline_event( &mut self, room_id: &RoomId, event: &mut EventResult, + did_update: &mut bool, ) -> Option> { match event { EventResult::Ok(e) => { @@ -263,7 +290,8 @@ impl Client { } let mut room = self.get_or_create_room(&room_id).write().await; - room.receive_timeline_event(e); + // Not sure what the best way to do this is ?? + *did_update = room.receive_timeline_event(e); decrypted_event } _ => None, diff --git a/src/request_builder.rs b/src/request_builder.rs index e891f492..795e8534 100644 --- a/src/request_builder.rs +++ b/src/request_builder.rs @@ -29,7 +29,7 @@ use js_int::UInt; /// .visibility(Visibility::Public) /// .name("name") /// .room_version("v1.0"); -/// let mut cli = AsyncClient::<(), ()>::new(homeserver, None).unwrap(); +/// let mut cli = AsyncClient::new(homeserver, None).unwrap(); /// cli.create_room(builder).await; /// # }) /// ``` @@ -186,7 +186,7 @@ impl Into for RoomBuilder { /// # rt.block_on(async { /// # let room_id = RoomId::new(homeserver.as_str()).unwrap(); /// # let last_sync_token = "".to_string();; -/// let mut cli = AsyncClient::<(), ()>::new(homeserver, None).unwrap(); +/// let mut cli = AsyncClient::new(homeserver, None).unwrap(); /// /// let mut builder = MessagesRequestBuilder::new(); /// builder.room_id(room_id) diff --git a/src/state/mod.rs b/src/state/mod.rs index b9a5c2d0..e6513e98 100644 --- a/src/state/mod.rs +++ b/src/state/mod.rs @@ -21,7 +21,7 @@ pub use state_store::JsonStore; use serde::{Deserialize, Serialize}; -use crate::base_client::Token; +use crate::base_client::{Client as BaseClient, Token}; use crate::events::push_rules::Ruleset; use crate::identifiers::{RoomId, UserId}; use crate::models::Room; @@ -40,22 +40,41 @@ pub struct ClientState { pub push_ruleset: Option, } +impl ClientState { + pub fn from_base_client(client: &BaseClient) -> ClientState { + let BaseClient { + session, + sync_token, + ignored_users, + push_ruleset, + .. + } = client; + Self { + session: session.clone(), + sync_token: sync_token.clone(), + ignored_users: ignored_users.clone(), + push_ruleset: push_ruleset.clone(), + } + } +} + /// Abstraction around the data store to avoid unnecessary request on client initialization. +#[async_trait::async_trait] pub trait StateStore: Send + Sync { - /// Set up connections or open files to load/save state. + /// Set up connections or check files exist to load/save state. fn open(&self, path: &Path) -> Result<()>; /// Loads the state of `BaseClient` through `StateStore::Store` type. - fn load_client_state(&self, path: &Path) -> Result; + async fn load_client_state(&self, path: &Path) -> Result; /// Load the state of a single `Room` by `RoomId`. - fn load_room_state(&self, path: &Path, room_id: &RoomId) -> Result; + async fn load_room_state(&self, path: &Path, room_id: &RoomId) -> Result; /// Load the state of all `Room`s. /// /// This will be mapped over in the client in order to store `Room`s in an async safe way. - fn load_all_rooms(&self, path: &Path) -> Result>; + async fn load_all_rooms(&self, path: &Path) -> Result>; /// Save the current state of the `BaseClient` using the `StateStore::Store` type. - fn store_client_state(&self, path: &Path, _: ClientState) -> Result<()>; + async fn store_client_state(&self, path: &Path, _: ClientState) -> Result<()>; /// Save the state a single `Room`. - fn store_room_state(&self, path: &Path, _: &Room) -> Result<()>; + async fn store_room_state(&self, path: &Path, _: &Room) -> Result<()>; } #[cfg(test)] diff --git a/src/state/state_store.rs b/src/state/state_store.rs index 6f57b0d9..83ea4e63 100644 --- a/src/state/state_store.rs +++ b/src/state/state_store.rs @@ -1,7 +1,7 @@ use std::collections::HashMap; use std::fs::{self, OpenOptions}; use std::io::{BufReader, BufWriter, Write}; -use std::path::{Path, PathBuf}; +use std::path::Path; use super::{ClientState, StateStore}; use crate::identifiers::RoomId; @@ -10,6 +10,7 @@ use crate::{Error, Result, Room}; /// and saves it to disk. pub struct JsonStore; +#[async_trait::async_trait] impl StateStore for JsonStore { fn open(&self, path: &Path) -> Result<()> { if !path.exists() { @@ -17,7 +18,8 @@ impl StateStore for JsonStore { } Ok(()) } - fn load_client_state(&self, path: &Path) -> Result { + + async fn load_client_state(&self, path: &Path) -> Result { let mut path = path.to_path_buf(); path.push("client.json"); @@ -26,7 +28,7 @@ impl StateStore for JsonStore { serde_json::from_reader(reader).map_err(Error::from) } - fn load_room_state(&self, path: &Path, room_id: &RoomId) -> Result { + async fn load_room_state(&self, path: &Path, room_id: &RoomId) -> Result { let mut path = path.to_path_buf(); path.push(&format!("rooms/{}.json", room_id)); @@ -35,7 +37,7 @@ impl StateStore for JsonStore { serde_json::from_reader(reader).map_err(Error::from) } - fn load_all_rooms(&self, path: &Path) -> Result> { + async fn load_all_rooms(&self, path: &Path) -> Result> { let mut path = path.to_path_buf(); path.push("rooms"); @@ -59,7 +61,7 @@ impl StateStore for JsonStore { Ok(rooms_map) } - fn store_client_state(&self, path: &Path, state: ClientState) -> Result<()> { + async fn store_client_state(&self, path: &Path, state: ClientState) -> Result<()> { let mut path = path.to_path_buf(); path.push("client.json"); @@ -71,14 +73,18 @@ impl StateStore for JsonStore { let json = serde_json::to_string(&state).map_err(Error::from)?; - let file = OpenOptions::new().write(true).create(true).open(path)?; + let file = OpenOptions::new() + .write(true) + .create(true) + .truncate(true) + .open(path)?; let mut writer = BufWriter::new(file); writer.write_all(json.as_bytes())?; Ok(()) } - fn store_room_state(&self, path: &Path, room: &Room) -> Result<()> { + async fn store_room_state(&self, path: &Path, room: &Room) -> Result<()> { let mut path = path.to_path_buf(); path.push(&format!("rooms/{}.json", room.room_id)); @@ -90,7 +96,11 @@ impl StateStore for JsonStore { let json = serde_json::to_string(&room).map_err(Error::from)?; - let file = OpenOptions::new().write(true).create(true).open(path)?; + let file = OpenOptions::new() + .write(true) + .create(true) + .truncate(true) + .open(path)?; let mut writer = BufWriter::new(file); writer.write_all(json.as_bytes())?; @@ -104,11 +114,15 @@ mod test { use std::convert::TryFrom; use std::fs; + use std::path::PathBuf; + use std::str::FromStr; use std::sync::Mutex; use lazy_static::lazy_static; + use mockito::{mock, Matcher}; use crate::identifiers::{RoomId, UserId}; + use crate::{AsyncClient, AsyncClientConfig, Session, SyncSettings}; lazy_static! { /// Limit io tests to one thread at a time. @@ -124,10 +138,13 @@ mod test { }; } - fn run_and_cleanup(test: fn()) { + async fn run_and_cleanup(test: fn() -> Fut) + where + Fut: std::future::Future, + { let _lock = MTX.lock(); - test(); + test().await; if PATH.exists() { let path: &Path = &PATH; @@ -135,50 +152,111 @@ mod test { } } - fn test_store_client_state() { + async fn test_store_client_state() { let store = JsonStore; let state = ClientState::default(); - store.store_client_state(&PATH, state).unwrap(); - let loaded = store.load_client_state(&PATH).unwrap(); + store.store_client_state(&PATH, state).await.unwrap(); + let loaded = store.load_client_state(&PATH).await.unwrap(); assert_eq!(loaded, ClientState::default()); } - #[test] - fn store_client_state() { - run_and_cleanup(test_store_client_state); + #[tokio::test] + async fn store_client_state() { + run_and_cleanup(test_store_client_state).await; } - fn test_store_room_state() { + async fn test_store_room_state() { let store = JsonStore; let id = RoomId::try_from("!roomid:example.com").unwrap(); let user = UserId::try_from("@example:example.com").unwrap(); let room = Room::new(&id, &user); - store.store_room_state(&PATH, &room).unwrap(); - let loaded = store.load_room_state(&PATH, &id).unwrap(); + store.store_room_state(&PATH, &room).await.unwrap(); + let loaded = store.load_room_state(&PATH, &id).await.unwrap(); assert_eq!(loaded, Room::new(&id, &user)); } - #[test] - fn store_room_state() { - run_and_cleanup(test_store_room_state); + #[tokio::test] + async fn store_room_state() { + run_and_cleanup(test_store_room_state).await; } - fn test_load_rooms() { + async fn test_load_rooms() { let store = JsonStore; let id = RoomId::try_from("!roomid:example.com").unwrap(); let user = UserId::try_from("@example:example.com").unwrap(); let room = Room::new(&id, &user); - store.store_room_state(&PATH, &room).unwrap(); - let loaded = store.load_all_rooms(&PATH).unwrap(); - println!("{:?}", loaded); + store.store_room_state(&PATH, &room).await.unwrap(); + let loaded = store.load_all_rooms(&PATH).await.unwrap(); + assert_eq!(&room, loaded.get(&id).unwrap()); } - #[test] - fn load_rooms() { - run_and_cleanup(test_load_rooms); + #[tokio::test] + async fn load_rooms() { + run_and_cleanup(test_load_rooms).await; + } + + async fn test_client_sync_store() { + let homeserver = url::Url::from_str(&mockito::server_url()).unwrap(); + + let session = Session { + access_token: "1234".to_owned(), + user_id: UserId::try_from("@cheeky_monkey:matrix.org").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 _m = mock("POST", "/_matrix/client/r0/login") + .with_status(200) + .with_body_from_file("tests/data/login_response.json") + .create(); + + let mut path = PATH.clone(); + path.push(session.user_id.to_string()); + // a sync response to populate our JSON store with user_id added to path + let config = AsyncClientConfig::default().state_store_path(&path); + let client = + AsyncClient::new_with_config(homeserver.clone(), Some(session.clone()), config) + .unwrap(); + let sync_settings = SyncSettings::new().timeout(std::time::Duration::from_millis(3000)); + let _ = client.sync(sync_settings).await.unwrap(); + + // remove user_id as login will set this + path.pop(); + // once logged in without syncing the client is updated from the state store + let config = AsyncClientConfig::default().state_store_path(&path); + let client = AsyncClient::new_with_config(homeserver, None, config).unwrap(); + client + .login("example", "wordpass", None, None) + .await + .unwrap(); + + let base_client = client.base_client.read().await; + + // assert the synced client and the logged in client are equal + assert_eq!(base_client.session, Some(session)); + assert_eq!( + base_client.sync_token, + Some("s526_47314_0_7_1_1_1_11444_1".to_string()) + ); + assert_eq!( + base_client.ignored_users, + vec![UserId::try_from("@someone:example.org").unwrap()] + ); + } + + #[tokio::test] + async fn client_sync_store() { + run_and_cleanup(test_client_sync_store).await; } } diff --git a/src/test_builder.rs b/src/test_builder.rs index 4849cfb1..6b1c8b6c 100644 --- a/src/test_builder.rs +++ b/src/test_builder.rs @@ -15,7 +15,7 @@ use crate::events::{ use crate::identifiers::{RoomId, UserId}; use crate::AsyncClient; -use mockito::{self, mock, Mock}; +use mockito::{self, mock, Matcher, Mock}; use crate::models::Room; @@ -169,11 +169,7 @@ impl EventBuilder { /// /// The `TestRunner` streams the events to the client and holds methods to make assertions /// about the state of the client. - pub fn build_mock_runner>( - mut self, - method: &str, - path: P, - ) -> MockTestRunner { + pub fn build_mock_runner>(mut self, method: &str, path: P) -> MockTestRunner { let body = serde_json::json! { { "device_one_time_keys_count": {}, @@ -344,8 +340,12 @@ impl ClientTestRunner { } for event in &self.room_events { - cli.receive_joined_timeline_event(room_id, &mut EventResult::Ok(event.clone())) - .await; + cli.receive_joined_timeline_event( + room_id, + &mut EventResult::Ok(event.clone()), + &mut false, + ) + .await; } for event in &self.presence_events { cli.receive_presence_event(room_id, event).await;