Files
matrix-rust-sdk/crates/matrix-sdk/tests/integration/widget.rs
T

629 lines
21 KiB
Rust

// Copyright 2023 The Matrix.org Foundation C.I.C.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
use std::{pin::pin, time::Duration};
use assert_matches::assert_matches;
use async_trait::async_trait;
use futures_util::FutureExt;
use matrix_sdk::{
config::SyncSettings,
widget::{
Capabilities, CapabilitiesProvider, WidgetDriver, WidgetDriverHandle, WidgetSettings,
},
Client,
};
use matrix_sdk_common::{executor::spawn, timeout::timeout};
use matrix_sdk_test::{
async_test, EventBuilder, JoinedRoomBuilder, SyncResponseBuilder, ALICE, BOB,
};
use once_cell::sync::Lazy;
use ruma::{
events::room::{
member::{MembershipState, RoomMemberEventContent},
message::RoomMessageEventContent,
name::RoomNameEventContent,
topic::RoomTopicEventContent,
},
owned_room_id,
serde::JsonObject,
user_id, OwnedRoomId,
};
use serde::Serialize;
use serde_json::{json, Value as JsonValue};
use tracing::error;
use wiremock::{
matchers::{header, method, path_regex, query_param},
Mock, MockServer, ResponseTemplate,
};
use crate::{logged_in_client_with_server, mock_encryption_state, mock_sync};
/// Create a JSON string from a [`json!`][serde_json::json] "literal".
#[macro_export]
macro_rules! json_string {
($( $tt:tt )*) => { ::serde_json::json!( $($tt)* ).to_string() };
}
const WIDGET_ID: &str = "test-widget";
static ROOM_ID: Lazy<OwnedRoomId> = Lazy::new(|| owned_room_id!("!a98sd12bjh:example.org"));
async fn run_test_driver(init_on_content_load: bool) -> (Client, MockServer, WidgetDriverHandle) {
struct DummyCapabilitiesProvider;
#[async_trait]
impl CapabilitiesProvider for DummyCapabilitiesProvider {
async fn acquire_capabilities(&self, capabilities: Capabilities) -> Capabilities {
// Grant all capabilities that the widget asks for
capabilities
}
}
let (client, mock_server) = logged_in_client_with_server().await;
let sync_settings = SyncSettings::new().timeout(Duration::from_millis(3000));
let mut sync_builder = SyncResponseBuilder::new();
sync_builder.add_joined_room(JoinedRoomBuilder::new(&ROOM_ID));
mock_sync(&mock_server, sync_builder.build_json_sync_response(), None).await;
let _response = client.sync_once(sync_settings.clone()).await.unwrap();
mock_server.reset().await;
mock_encryption_state(&mock_server, false).await;
let room = client.get_room(&ROOM_ID).unwrap();
let (driver, handle) = WidgetDriver::new(
WidgetSettings::new(WIDGET_ID.to_owned(), init_on_content_load, "https://foo.bar/widget")
.unwrap(),
);
spawn(async move {
if let Err(()) = driver.run(room, DummyCapabilitiesProvider).await {
error!("An error encountered in running the WidgetDriver (no details available yet)");
}
});
(client, mock_server, handle)
}
async fn recv_message(driver_handle: &WidgetDriverHandle) -> JsonObject {
let fut = pin!(driver_handle.recv());
let msg = timeout(fut, Duration::from_secs(1)).await.unwrap();
serde_json::from_str(&msg.unwrap()).unwrap()
}
async fn send_request(
driver_handle: &WidgetDriverHandle,
request_id: &str,
action: &str,
data: impl Serialize,
) {
let sent = driver_handle
.send(json_string!({
"api": "fromWidget",
"widgetId": WIDGET_ID,
"requestId": request_id,
"action": action,
"data": data,
}))
.await;
assert!(sent);
}
async fn send_response(
driver_handle: &WidgetDriverHandle,
request_id: &str,
action: &str,
request_data: impl Serialize,
response_data: impl Serialize,
) {
let sent = driver_handle
.send(json_string!({
"api": "toWidget",
"widgetId": WIDGET_ID,
"requestId": request_id,
"action": action,
"data": request_data,
"response": response_data,
}))
.await;
assert!(sent);
}
#[async_test]
async fn negotiate_capabilities_immediately() {
let (_, _, driver_handle) = run_test_driver(false).await;
let caps = json!(["org.matrix.msc2762.receive.event:m.room.message"]);
{
// Receive toWidget capabilities request
let msg = recv_message(&driver_handle).await;
assert_eq!(msg["api"], "toWidget");
assert_eq!(msg["action"], "capabilities");
let data = &msg["data"];
let request_id = msg["requestId"].as_str().unwrap();
// Let's send a request to get supported versions in the middle
// of a capabilities negotiation to ensure that we're not "deadlocked" by
// not processing messages while waiting for a reply from a widget to the
// the toWidget request.
{
send_request(
&driver_handle,
"get-supported-api-versions",
"supported_api_versions",
json!({}),
)
.await;
let msg = recv_message(&driver_handle).await;
assert_eq!(msg["api"], "fromWidget");
assert_eq!(msg["action"], "supported_api_versions");
assert_eq!(msg["requestId"].as_str().unwrap(), "get-supported-api-versions");
assert!(msg["response"]["supported_versions"].is_array());
}
// Answer with caps we want
let response = json!({ "capabilities": caps });
send_response(&driver_handle, request_id, "capabilities", data, &response).await;
}
{
// Receive a "request" with the capabilities we were actually granted (wtf?)
let msg = recv_message(&driver_handle).await;
assert_eq!(msg["api"], "toWidget");
assert_eq!(msg["action"], "notify_capabilities");
assert_eq!(msg["data"], json!({ "requested": caps, "approved": caps }));
let request_id = msg["requestId"].as_str().unwrap();
// ACK the request
send_response(&driver_handle, request_id, "notify_capabilities", caps, json!({})).await;
}
assert_matches!(driver_handle.recv().now_or_never(), None);
}
#[async_test]
async fn read_messages() {
let (_, mock_server, driver_handle) = run_test_driver(true).await;
{
// Tell the driver that we're ready for communication
send_request(&driver_handle, "1-content-loaded", "content_loaded", json!({})).await;
// Receive the response
let msg = recv_message(&driver_handle).await;
assert_eq!(msg["api"], "fromWidget");
assert_eq!(msg["action"], "content_loaded");
assert!(msg["data"].as_object().unwrap().is_empty());
}
negotiate_capabilities(
&driver_handle,
json!(["org.matrix.msc2762.receive.event:m.room.message"]),
)
.await;
// No messages from the driver
assert_matches!(recv_message(&driver_handle).now_or_never(), None);
{
let response_json = json!({
"chunk": [
{
"content": {
"body": "hello",
"msgtype": "m.text",
},
"event_id": "$msda7m0df9E9op3",
"origin_server_ts": 152037280,
"sender": "@example:localhost",
"type": "m.room.message",
"room_id": &*ROOM_ID,
},
{
"content": {
"body": "This room has been replaced",
"replacement_room": "!newroom:localhost",
},
"event_id": "$foun39djjod0f",
"origin_server_ts": 152039280,
"sender": "@bob:localhost",
"state_key": "",
"type": "m.room.tombstone",
"room_id": &*ROOM_ID,
},
],
"end": "t47409-4357353_219380_26003_2269",
"start": "t392-516_47314_0_7_1_1_1_11444_1"
});
Mock::given(method("GET"))
.and(path_regex(r"^/_matrix/client/r0/rooms/.*/messages$"))
.and(header("authorization", "Bearer 1234"))
.and(query_param("limit", "2"))
.respond_with(ResponseTemplate::new(200).set_body_json(response_json))
.expect(1)
.mount(&mock_server)
.await;
// Ask the driver to read messages
send_request(
&driver_handle,
"2-read-messages",
"org.matrix.msc2876.read_events",
json!({
"type": "m.room.message",
"limit": 2,
}),
)
.await;
// Receive the response
let msg = recv_message(&driver_handle).await;
assert_eq!(msg["api"], "fromWidget");
assert_eq!(msg["action"], "org.matrix.msc2876.read_events");
let events = msg["response"]["events"].as_array().unwrap();
assert_eq!(events.len(), 1);
let first_event = &events[0];
assert_eq!(first_event["content"]["body"], "hello");
}
mock_server.verify().await;
}
#[async_test]
async fn read_messages_with_msgtype_capabilities() {
let (_, mock_server, driver_handle) = run_test_driver(true).await;
{
// Tell the driver that we're ready for communication
send_request(&driver_handle, "1-content-loaded", "content_loaded", json!({})).await;
// Receive the response
let msg = recv_message(&driver_handle).await;
assert_eq!(msg["api"], "fromWidget");
assert_eq!(msg["action"], "content_loaded");
assert!(msg["data"].as_object().unwrap().is_empty());
}
negotiate_capabilities(
&driver_handle,
json!(["org.matrix.msc2762.receive.event:m.room.message#m.text"]),
)
.await;
// No messages from the driver
assert_matches!(recv_message(&driver_handle).now_or_never(), None);
{
let response_json = json!({
"chunk": [
{
"content": {
"body": "custom content",
"msgtype": "m.custom.element",
},
"event_id": "$msda7m0df9E9op3",
"origin_server_ts": 152037220,
"sender": "@example:localhost",
"type": "m.room.message",
"room_id": &*ROOM_ID,
},
{
"content": {
"body": "hello",
"msgtype": "m.text",
},
"event_id": "$msda7m0df9E9op5",
"origin_server_ts": 152037280,
"sender": "@example:localhost",
"type": "m.room.message",
"room_id": &*ROOM_ID,
},
{
"content": {
},
"event_id": "$msda7m0df9E9op7",
"origin_server_ts": 152037290,
"sender": "@example:localhost",
"type": "m.reaction",
"room_id": &*ROOM_ID,
},
],
"end": "t47409-4357353_219380_26003_2269",
"start": "t392-516_47314_0_7_1_1_1_11444_1"
});
Mock::given(method("GET"))
.and(path_regex(r"^/_matrix/client/r0/rooms/.*/messages$"))
.and(header("authorization", "Bearer 1234"))
.and(query_param("limit", "3"))
.respond_with(ResponseTemplate::new(200).set_body_json(response_json))
.expect(1)
.mount(&mock_server)
.await;
// Ask the driver to read messages
send_request(
&driver_handle,
"2-read-messages",
"org.matrix.msc2876.read_events",
json!({
"type": "m.room.message",
"limit": 3,
}),
)
.await;
// Receive the response
let msg = recv_message(&driver_handle).await;
assert_eq!(msg["api"], "fromWidget");
assert_eq!(msg["action"], "org.matrix.msc2876.read_events");
let events = msg["response"]["events"].as_array().unwrap();
assert_eq!(events.len(), 1);
let first_event = &events[0];
assert_eq!(first_event["content"]["body"], "hello");
}
mock_server.verify().await;
}
#[async_test]
async fn read_room_members() {
let (_, mock_server, driver_handle) = run_test_driver(false).await;
negotiate_capabilities(
&driver_handle,
json!(["org.matrix.msc2762.receive.state_event:m.room.member"]),
)
.await;
// No messages from the driver
assert_matches!(recv_message(&driver_handle).now_or_never(), None);
{
// The read-events request is fulfilled from the state store
drop(mock_server);
// Ask the driver to read state events
send_request(
&driver_handle,
"2-read-messages",
"org.matrix.msc2876.read_events",
json!({ "type": "m.room.member", "state_key": true }),
)
.await;
// Receive the response
let msg = recv_message(&driver_handle).await;
assert_eq!(msg["api"], "fromWidget");
assert_eq!(msg["action"], "org.matrix.msc2876.read_events");
let events = msg["response"]["events"].as_array().unwrap();
// No useful data in the state store, that's fine for this test
// (we just want to know that a successful response is generated)
assert_eq!(events.len(), 0);
}
}
#[async_test]
async fn receive_live_events() {
let (client, mock_server, driver_handle) = run_test_driver(false).await;
negotiate_capabilities(
&driver_handle,
json!([
"org.matrix.msc2762.receive.event:m.room.member",
"org.matrix.msc2762.receive.event:m.room.message#m.text",
"org.matrix.msc2762.receive.state_event:m.room.name#",
"org.matrix.msc2762.receive.state_event:m.room.member#@example:localhost",
]),
)
.await;
// No messages from the driver yet
assert_matches!(recv_message(&driver_handle).now_or_never(), None);
let mut sync_builder = SyncResponseBuilder::new();
// bump the internal batch counter, otherwise the response will be seen as
// identical to the one done in `run_test_driver`
sync_builder.build_json_sync_response();
let event_builder = EventBuilder::new();
sync_builder.add_joined_room(
JoinedRoomBuilder::new(&ROOM_ID)
// text message from alice - matches filter #2
.add_timeline_event(event_builder.make_sync_message_event(
&ALICE,
RoomMessageEventContent::text_plain("simple text message"),
))
// emote from alice - doesn't match
.add_timeline_event(event_builder.make_sync_message_event(
&ALICE,
RoomMessageEventContent::emote_plain("emote message"),
))
// pointless member event - matches filter #4
.add_timeline_event(event_builder.make_sync_state_event(
user_id!("@example:localhost"),
"@example:localhost",
RoomMemberEventContent::new(MembershipState::Join),
Some(RoomMemberEventContent::new(MembershipState::Join)),
))
// kick alice - doesn't match because the `#@example:localhost` bit
// is about the state_key, not the sender
.add_timeline_event(event_builder.make_sync_state_event(
user_id!("@example:localhost"),
ALICE.as_str(),
RoomMemberEventContent::new(MembershipState::Ban),
Some(RoomMemberEventContent::new(MembershipState::Join)),
))
// set room tpoic - doesn't match
.add_timeline_event(event_builder.make_sync_state_event(
&BOB,
"",
RoomTopicEventContent::new("new room topic".to_owned()),
None,
))
// set room name - matches filter #3
.add_timeline_event(event_builder.make_sync_state_event(
&BOB,
"",
RoomNameEventContent::new("New Room Name".to_owned()),
None,
)),
);
mock_sync(&mock_server, sync_builder.build_json_sync_response(), None).await;
let _response =
client.sync_once(SyncSettings::new().timeout(Duration::from_millis(3000))).await.unwrap();
let msg = recv_message(&driver_handle).await;
assert_eq!(msg["api"], "toWidget");
assert_eq!(msg["action"], "send_event");
assert_eq!(msg["data"]["type"], "m.room.message");
assert_eq!(msg["data"]["room_id"], ROOM_ID.as_str());
assert_eq!(msg["data"]["content"]["msgtype"], "m.text");
assert_eq!(msg["data"]["content"]["body"], "simple text message");
let msg = recv_message(&driver_handle).await;
assert_eq!(msg["api"], "toWidget");
assert_eq!(msg["action"], "send_event");
assert_eq!(msg["data"]["type"], "m.room.member");
assert_eq!(msg["data"]["room_id"], ROOM_ID.as_str());
assert_eq!(msg["data"]["state_key"], "@example:localhost");
assert_eq!(msg["data"]["content"]["membership"], "join");
assert_eq!(msg["data"]["unsigned"]["prev_content"]["membership"], "join");
let msg = recv_message(&driver_handle).await;
assert_eq!(msg["api"], "toWidget");
assert_eq!(msg["action"], "send_event");
assert_eq!(msg["data"]["type"], "m.room.name");
assert_eq!(msg["data"]["sender"], BOB.as_str());
assert_eq!(msg["data"]["content"]["name"], "New Room Name");
// No more messages from the driver
assert_matches!(recv_message(&driver_handle).now_or_never(), None);
}
#[async_test]
async fn send_room_message() {
let (_, mock_server, driver_handle) = run_test_driver(false).await;
negotiate_capabilities(&driver_handle, json!(["org.matrix.msc2762.send.event:m.room.message"]))
.await;
Mock::given(method("PUT"))
.and(path_regex(r"^/_matrix/client/r0/rooms/.*/send/m.room.message/.*$"))
.respond_with(ResponseTemplate::new(200).set_body_json(json!({ "event_id": "$foobar" })))
.expect(1)
.mount(&mock_server)
.await;
send_request(
&driver_handle,
"send-room-message",
"send_event",
json!({
"type": "m.room.message",
"content": {
"msgtype": "m.text",
"body": "Message from a widget!",
},
}),
)
.await;
// Receive the response
let msg = recv_message(&driver_handle).await;
assert_eq!(msg["api"], "fromWidget");
assert_eq!(msg["action"], "send_event");
let event_id = msg["response"]["event_id"].as_str().unwrap();
assert_eq!(event_id, "$foobar");
// Make sure the event-sending endpoint was hit exactly once
mock_server.verify().await;
}
#[async_test]
async fn send_room_name() {
let (_, mock_server, driver_handle) = run_test_driver(false).await;
negotiate_capabilities(
&driver_handle,
json!(["org.matrix.msc2762.send.state_event:m.room.name#"]),
)
.await;
Mock::given(method("PUT"))
.and(path_regex(r"^/_matrix/client/r0/rooms/.*/state/m.room.name/?$"))
.respond_with(ResponseTemplate::new(200).set_body_json(json!({ "event_id": "$foobar" })))
.expect(1)
.mount(&mock_server)
.await;
send_request(
&driver_handle,
"send-room-name",
"send_event",
json!({
"type": "m.room.name",
"state_key": "",
"content": {
"name": "Room Name set by Widget",
},
}),
)
.await;
// Receive the response
let msg = recv_message(&driver_handle).await;
assert_eq!(msg["api"], "fromWidget");
assert_eq!(msg["action"], "send_event");
let event_id = msg["response"]["event_id"].as_str().unwrap();
assert_eq!(event_id, "$foobar");
// Make sure the event-sending endpoint was hit exactly once
mock_server.verify().await;
}
async fn negotiate_capabilities(driver_handle: &WidgetDriverHandle, caps: JsonValue) {
{
// Receive toWidget capabilities request
let msg = recv_message(driver_handle).await;
assert_eq!(msg["api"], "toWidget");
assert_eq!(msg["action"], "capabilities");
let data = &msg["data"];
let request_id = msg["requestId"].as_str().unwrap();
// Answer with caps we want
let response = json!({ "capabilities": caps });
send_response(driver_handle, request_id, "capabilities", data, &response).await;
}
{
// Receive notification with granted capabilities
let msg = recv_message(driver_handle).await;
assert_eq!(msg["api"], "toWidget");
assert_eq!(msg["action"], "notify_capabilities");
assert_eq!(msg["data"], json!({ "requested": caps, "approved": caps }));
let request_id = msg["requestId"].as_str().unwrap();
// ACK the notification
send_response(driver_handle, request_id, "notify_capabilities", caps, json!({})).await;
}
}