state_store: fix docs, test client with default state store

master
Devin R 2020-04-22 17:39:57 -04:00
parent ad7a18d50a
commit 5491838228
7 changed files with 279 additions and 68 deletions

View File

@ -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<RoomId, Room>` or further to `Room`'s `RoomMember` via the members `HashMap<UserId, RoomMember>`. 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.

View File

@ -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<RwLock<BaseClient>>,
/// The path to the default state store.
state_store_path: Option<PathBuf>,
}
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<reqwest::Proxy>,
user_agent: Option<HeaderValue>,
disable_ssl_verification: bool,
store_path: Option<PathBuf>,
state_store: Option<Box<dyn StateStore>>,
}
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<P: AsRef<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<dyn StateStore>) -> 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;
///

View File

@ -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<RoomEvent>,
did_update: &mut bool,
) -> Option<EventResult<RoomEvent>> {
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,

View File

@ -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<create_room::Request> 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)

View File

@ -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<Ruleset>,
}
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<ClientState>;
async fn load_client_state(&self, path: &Path) -> Result<ClientState>;
/// Load the state of a single `Room` by `RoomId`.
fn load_room_state(&self, path: &Path, room_id: &RoomId) -> Result<Room>;
async fn load_room_state(&self, path: &Path, room_id: &RoomId) -> Result<Room>;
/// 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<HashMap<RoomId, Room>>;
async fn load_all_rooms(&self, path: &Path) -> Result<HashMap<RoomId, Room>>;
/// 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)]

View File

@ -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<ClientState> {
async fn load_client_state(&self, path: &Path) -> Result<ClientState> {
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<Room> {
async fn load_room_state(&self, path: &Path, room_id: &RoomId) -> Result<Room> {
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<HashMap<RoomId, Room>> {
async fn load_all_rooms(&self, path: &Path) -> Result<HashMap<RoomId, Room>> {
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<Fut>(test: fn() -> Fut)
where
Fut: std::future::Future<Output = ()>,
{
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;
}
}

View File

@ -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<P: Into<mockito::Matcher>>(
mut self,
method: &str,
path: P,
) -> MockTestRunner {
pub fn build_mock_runner<P: Into<Matcher>>(mut self, method: &str, path: P) -> MockTestRunner {
let body = serde_json::json! {
{
"device_one_time_keys_count": {},
@ -344,7 +340,11 @@ impl ClientTestRunner {
}
for event in &self.room_events {
cli.receive_joined_timeline_event(room_id, &mut EventResult::Ok(event.clone()))
cli.receive_joined_timeline_event(
room_id,
&mut EventResult::Ok(event.clone()),
&mut false,
)
.await;
}
for event in &self.presence_events {