Skip to content
Merged
Show file tree
Hide file tree
Changes from 11 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
130 changes: 130 additions & 0 deletions crates/matrix-sdk/src/test_utils/mocks.rs
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ use ruma::{
directory::PublicRoomsChunk,
events::{AnyStateEvent, AnyTimelineEvent, MessageLikeEventType, StateEventType},
serde::Raw,
time::Duration,
MxcUri, OwnedEventId, OwnedRoomId, RoomId, ServerName,
};
use serde::Deserialize;
Expand Down Expand Up @@ -922,6 +923,72 @@ impl<'a> MockEndpoint<'a, RoomSendEndpoint> {
}
}

/// Ensures the event was send as a delayed event.
///
/// Note: works with *any* room.
///
/// # Examples
///
/// see also [`MatrixMockServer::mock_room_send`] for more context.
///
/// ```
/// # tokio_test::block_on(async {
/// use matrix_sdk::{
/// ruma::{
/// api::client::delayed_events::{delayed_message_event, DelayParameters},
/// events::{message::MessageEventContent, AnyMessageLikeEventContent},
/// room_id,
/// time::Duration,
/// TransactionId,
/// },
/// test_utils::mocks::MatrixMockServer,
/// };
/// use serde_json::json;
/// use wiremock::ResponseTemplate;
///
/// let mock_server = MatrixMockServer::new().await;
/// let client = mock_server.client_builder().build().await;
///
/// mock_server.mock_room_state_encryption().plain().mount().await;
///
/// let room = mock_server.sync_joined_room(&client, room_id!("!room_id:localhost")).await;
///
/// mock_server
/// .mock_room_send()
/// .with_delay(Duration::from_millis(500))
/// .respond_with(ResponseTemplate::new(200).set_body_json(json!({"delay_id":"$some_id"})))
/// .mock_once()
/// .mount()
/// .await;
///
/// let response_not_mocked =
/// room.send_raw("m.room.message", json!({ "body": "Hello world" })).await;
///
/// // A non delayed event should not be mocked by the server.
/// assert!(response_not_mocked.is_err());
///
/// let r = delayed_message_event::unstable::Request::new(
/// room.room_id().to_owned(),
/// TransactionId::new(),
/// DelayParameters::Timeout { timeout: Duration::from_millis(500) },
/// &AnyMessageLikeEventContent::Message(MessageEventContent::plain("hello world")),
/// )
/// .unwrap();
///
/// let response = room.client().send(r, None).await.unwrap();
/// // The delayed `m.room.message` event type should be mocked by the server.
/// assert_eq!("$some_id", response.delay_id);
/// # anyhow::Ok(()) });
/// ```
pub fn with_delay(self, delay: Duration) -> Self {
Self {
mock: self
.mock
.and(query_param("org.matrix.msc4140.delay", delay.as_millis().to_string())),
..self
}
}

/// Returns a send endpoint that emulates success, i.e. the event has been
/// sent with the given event id.
///
Expand Down Expand Up @@ -1087,6 +1154,69 @@ impl<'a> MockEndpoint<'a, RoomSendStateEndpoint> {
Self { mock: self.mock.and(path_regex(Self::generate_path_regexp(&self.endpoint))), ..self }
}

/// Ensures the event was send as a delayed event.
///
/// Note: works with *any* room.
///
/// # Examples
///
/// see also [`MatrixMockServer::mock_room_send`] for more context.
///
/// ```
/// # tokio_test::block_on(async {
/// use matrix_sdk::{
/// ruma::{
/// api::client::delayed_events::{delayed_state_event, DelayParameters},
/// events::{room::create::RoomCreateEventContent, AnyStateEventContent},
/// room_id,
/// time::Duration,
/// },
/// test_utils::mocks::MatrixMockServer,
/// };
/// use wiremock::ResponseTemplate;
/// use serde_json::json;
///
/// let mock_server = MatrixMockServer::new().await;
/// let client = mock_server.client_builder().build().await;
///
/// mock_server.mock_room_state_encryption().plain().mount().await;
///
/// let room = mock_server.sync_joined_room(&client, room_id!("!room_id:localhost")).await;
///
/// mock_server
/// .mock_room_send_state()
/// .with_delay(Duration::from_millis(500))
/// .respond_with(ResponseTemplate::new(200).set_body_json(json!({"delay_id":"$some_id"})))
/// .mock_once()
/// .mount()
/// .await;
///
/// let response_not_mocked = room.send_state_event(RoomCreateEventContent::new_v11()).await;
/// // A non delayed event should not be mocked by the server.
/// assert!(response_not_mocked.is_err());
///
/// let r = delayed_state_event::unstable::Request::new(
/// room.room_id().to_owned(),
/// "".to_owned(),
/// DelayParameters::Timeout { timeout: Duration::from_millis(500) },
/// &AnyStateEventContent::RoomCreate(RoomCreateEventContent::new_v11()),
/// )
/// .unwrap();
/// let response = room.client().send(r, None).await.unwrap();
/// // The delayed `m.room.message` event type should be mocked by the server.
/// assert_eq!("$some_id", response.delay_id);
///
/// # anyhow::Ok(()) });
/// ```
pub fn with_delay(self, delay: Duration) -> Self {
Self {
mock: self
.mock
.and(query_param("org.matrix.msc4140.delay", delay.as_millis().to_string())),
..self
}
}

///
/// ```
/// # tokio_test::block_on(async {
Expand Down
4 changes: 3 additions & 1 deletion crates/matrix-sdk/src/widget/machine/driver_req.rs
Original file line number Diff line number Diff line change
Expand Up @@ -74,9 +74,11 @@ where
Self { request_meta: None, _phantom: PhantomData }
}

/// Setup a callback function that will be called once the matrix driver has
/// processed the request.
pub(crate) fn then(
self,
response_handler: impl FnOnce(Result<T, String>, &mut WidgetMachine) -> Vec<Action>
response_handler: impl FnOnce(Result<T, crate::Error>, &mut WidgetMachine) -> Vec<Action>
+ Send
+ 'static,
) {
Expand Down
68 changes: 61 additions & 7 deletions crates/matrix-sdk/src/widget/machine/from_widget.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,10 @@
// See the License for the specific language governing permissions and
// limitations under the License.

use std::fmt;

use ruma::{
api::client::delayed_events::{
delayed_message_event, delayed_state_event, update_delayed_event,
api::client::{
delayed_events::{delayed_message_event, delayed_state_event, update_delayed_event},
error::{ErrorBody, StandardErrorBody},
},
events::{AnyTimelineEvent, MessageLikeEventType, StateEventType},
serde::Raw,
Expand All @@ -25,7 +24,7 @@ use ruma::{
use serde::{Deserialize, Serialize};

use super::{SendEventRequest, UpdateDelayedEventRequest};
use crate::widget::StateKeySelector;
use crate::{widget::StateKeySelector, Error, HttpError};

#[derive(Deserialize, Debug)]
#[serde(tag = "action", rename_all = "snake_case", content = "data")]
Expand All @@ -41,28 +40,83 @@ pub(super) enum FromWidgetRequest {
DelayedEventUpdate(UpdateDelayedEventRequest),
}

/// The full response a client sends to a from widget request
/// in case of an error.
#[derive(Serialize)]
pub(super) struct FromWidgetErrorResponse {
error: FromWidgetError,
}

impl FromWidgetErrorResponse {
pub(super) fn new(e: impl fmt::Display) -> Self {
Self { error: FromWidgetError { message: e.to_string() } }
/// Create a error response to send to the widget from an http error.
pub(crate) fn from_http_error(error: &HttpError) -> Self {
Self {
error: FromWidgetError {
message: error.to_string(),
matrix_api_error: error.as_client_api_error().and_then(|api_error| match api_error
.body
.clone()
{
ErrorBody::Standard { kind, message } => Some(FromWidgetMatrixErrorBody {
http_status: api_error.status_code.as_u16().into(),
response: StandardErrorBody { kind, message },
}),
_ => None,
}),
},
}
}

/// Create a error response to send to the widget from a matrix sdk error.
pub(crate) fn from_error(error: &Error) -> Self {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can this take ownership of the error?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It could but would there be any benefit to that? we will only call methods that use the ref?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Avoid cloning in the function's body, and it's a good practice whenever you can do that (as it avoids future changes to this code from cloning too).

Copy link
Contributor Author

@toger5 toger5 Dec 3, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We will only call to_string and as_client_api_error on the error object. Both use &self. to_string will of course allocate new data but I am not sure there is a way to convert an error to a string without that overhead?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Indeed there's no way, but overall passing by ownership makes for cleaner APIs, whenever we can do it.

match &error {
Error::Http(e) => FromWidgetErrorResponse::from_http_error(e),
// For UnknownError's we do not want to have the `unknown error` bit in the message.
// Hence we only convert the inner error to a string.
Error::UnknownError(e) => FromWidgetErrorResponse::from_string(e.to_string()),
_ => FromWidgetErrorResponse::from_string(error.to_string()),
}
}

/// Create a error response to send to the widget from a string.
pub(crate) fn from_string<S: Into<String>>(error: S) -> Self {
Self { error: FromWidgetError { message: error.into(), matrix_api_error: None } }
}
}

/// The serializable section of an error response send by the client as a
/// response to a [`FromWidgetRequest`].
#[derive(Serialize)]
struct FromWidgetError {
/// A unspecified error message text that caused this widget action to fail.
/// This is useful to prompt the user on an issue but cannot be used to
/// decide on how to deal with the error.
message: String,
/// An optional matrix error that contains specified
/// information and helps finding a work around for specific errors.
matrix_api_error: Option<FromWidgetMatrixErrorBody>,
}

/// The serializable section of a widget response that represents a matrix
/// error.
#[derive(Serialize)]
struct FromWidgetMatrixErrorBody {
/// The status code of the http response
http_status: u32,
/// The matrix standard error response including the `errorcode` and the
/// `error` message as defined in the spec: https://spec.matrix.org/v1.12/client-server-api/#standard-error-response
response: StandardErrorBody,
}

/// The serializable section of a widget response containing the supported
/// versions.
#[derive(Serialize)]
pub(super) struct SupportedApiVersionsResponse {
supported_versions: Vec<ApiVersion>,
}

impl SupportedApiVersionsResponse {
/// The currently supported widget api versions from the rust widget driver.
pub(super) fn new() -> Self {
Self {
supported_versions: vec![
Expand Down
6 changes: 4 additions & 2 deletions crates/matrix-sdk/src/widget/machine/incoming.rs
Original file line number Diff line number Diff line change
Expand Up @@ -37,8 +37,10 @@ pub(crate) enum IncomingMessage {
/// The ID of the request that this response corresponds to.
request_id: Uuid,

/// The result of the request: response data or error message.
response: Result<MatrixDriverResponse, String>,
/// The result of the request: response data or matrix sdk error.
/// Http errors will be forwarded to the widget in a specified format
/// so the widget can parse the error.
response: Result<MatrixDriverResponse, crate::Error>,
},

/// The `MatrixDriver` notified the `WidgetMachine` of a new matrix event.
Expand Down
Loading
Loading