mirror of
https://github.com/matrix-org/matrix-rust-sdk.git
synced 2026-05-14 19:16:02 -04:00
refactor(appservice)!: Cleanup
This commit is contained in:
committed by
Damir Jelić
parent
45829efa7e
commit
5f7e91c2e7
@@ -41,41 +41,41 @@ pub enum Error {
|
||||
#[error("uri path is unknown")]
|
||||
UriPathUnknown,
|
||||
|
||||
#[error(transparent)]
|
||||
HttpRequest(#[from] ruma::api::error::FromHttpRequestError),
|
||||
#[error("HTTP request parsing error: {0}")]
|
||||
FromHttpRequest(#[from] ruma::api::error::FromHttpRequestError),
|
||||
|
||||
#[error(transparent)]
|
||||
#[error("identifier failed to parse: {0}")]
|
||||
Identifier(#[from] ruma::IdParseError),
|
||||
|
||||
#[error(transparent)]
|
||||
#[error("HTTP error: {0}")]
|
||||
Http(#[from] http::Error),
|
||||
|
||||
#[error(transparent)]
|
||||
#[error("url parse error: {0}")]
|
||||
Url(#[from] url::ParseError),
|
||||
|
||||
#[error(transparent)]
|
||||
#[error("deserialization error: {0}")]
|
||||
Serde(#[from] serde::de::value::Error),
|
||||
|
||||
#[error(transparent)]
|
||||
#[error("I/O error: {0}")]
|
||||
Io(#[from] std::io::Error),
|
||||
|
||||
#[error(transparent)]
|
||||
#[error("http uri invalid error: {0}")]
|
||||
InvalidUri(#[from] http::uri::InvalidUri),
|
||||
|
||||
#[error(transparent)]
|
||||
Matrix(#[from] matrix_sdk::Error),
|
||||
|
||||
#[error(transparent)]
|
||||
#[error("regex error: {0}")]
|
||||
Regex(#[from] regex::Error),
|
||||
|
||||
#[error(transparent)]
|
||||
#[error("serde yaml error: {0}")]
|
||||
SerdeYaml(#[from] serde_yaml::Error),
|
||||
|
||||
#[error(transparent)]
|
||||
#[error("serde json error: {0}")]
|
||||
SerdeJson(#[from] serde_json::Error),
|
||||
|
||||
#[error(transparent)]
|
||||
Utf8Error(#[from] std::str::Utf8Error),
|
||||
#[error("utf8 error: {0}")]
|
||||
Utf8(#[from] std::str::Utf8Error),
|
||||
|
||||
#[error("warp rejection: {0}")]
|
||||
WarpRejection(String),
|
||||
|
||||
@@ -37,9 +37,6 @@
|
||||
//! for the access tokens and because membership states for virtual users are
|
||||
//! determined based on the registered namespaces.
|
||||
//!
|
||||
//! **Note:** Non-exclusive registration namespaces are not yet supported and
|
||||
//! hence might lead to undefined behavior.
|
||||
//!
|
||||
//! # Quickstart
|
||||
//!
|
||||
//! ```no_run
|
||||
@@ -95,7 +92,7 @@ use event_handler::AppserviceFn;
|
||||
pub use matrix_sdk;
|
||||
#[doc(no_inline)]
|
||||
pub use matrix_sdk::ruma;
|
||||
use matrix_sdk::{bytes::Bytes, reqwest::Url, Client, ClientBuilder};
|
||||
use matrix_sdk::{reqwest::Url, Client, ClientBuilder};
|
||||
use ruma::{
|
||||
api::{
|
||||
appservice::{
|
||||
@@ -392,7 +389,7 @@ impl AppService {
|
||||
/// active virtual clients.
|
||||
///
|
||||
/// [transaction]: https://spec.matrix.org/v1.2/application-service-api/#put_matrixappv1transactionstxnid
|
||||
pub async fn receive_transaction(
|
||||
async fn receive_transaction(
|
||||
&self,
|
||||
transaction: push_events::v1::IncomingRequest,
|
||||
) -> Result<()> {
|
||||
@@ -515,66 +512,517 @@ impl AppService {
|
||||
}
|
||||
}
|
||||
|
||||
/// Ruma always expects the path to start with `/_matrix`, so we transform
|
||||
/// accordingly. Handles [legacy routes] and appservice being located on a sub
|
||||
/// path.
|
||||
///
|
||||
/// [legacy routes]: https://matrix.org/docs/spec/application_service/r0.1.2#legacy-routes
|
||||
// TODO: consider ruma PR
|
||||
pub(crate) fn transform_request_path(
|
||||
mut request: http::Request<Bytes>,
|
||||
) -> Result<http::Request<Bytes>> {
|
||||
let uri = request.uri();
|
||||
// remove trailing slash from path
|
||||
let path = uri.path().trim_end_matches('/').to_owned();
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::{
|
||||
future,
|
||||
sync::{Arc, Mutex},
|
||||
};
|
||||
|
||||
if !path.starts_with("/_matrix/app/v1/") {
|
||||
let path = match path {
|
||||
// special-case paths without value at the end
|
||||
_ if path.ends_with("/_matrix/app/unstable/thirdparty/user") => {
|
||||
"/_matrix/app/v1/thirdparty/user".to_owned()
|
||||
}
|
||||
_ if path.ends_with("/_matrix/app/unstable/thirdparty/location") => {
|
||||
"/_matrix/app/v1/thirdparty/location".to_owned()
|
||||
}
|
||||
// regular paths with values at the end
|
||||
_ => {
|
||||
let mut path = path.split('/').into_iter().rev();
|
||||
let value = match path.next() {
|
||||
Some(value) => value,
|
||||
None => return Err(Error::UriEmptyPath),
|
||||
};
|
||||
use matrix_sdk::{
|
||||
config::RequestConfig,
|
||||
ruma::{api::appservice::Registration, events::room::member::OriginalSyncRoomMemberEvent},
|
||||
Client,
|
||||
};
|
||||
use matrix_sdk_test::{appservice::TransactionBuilder, async_test, TimelineTestEvent};
|
||||
use ruma::{
|
||||
api::{appservice::event::push_events, MatrixVersion},
|
||||
events::AnyRoomEvent,
|
||||
room_id,
|
||||
serde::Raw,
|
||||
};
|
||||
use serde_json::json;
|
||||
use warp::{Filter, Reply};
|
||||
use wiremock::{
|
||||
matchers::{body_json, header, method, path},
|
||||
Mock, MockServer, ResponseTemplate,
|
||||
};
|
||||
|
||||
let mut path = match path.next() {
|
||||
Some(path_segment)
|
||||
if ["transactions", "users", "rooms"].contains(&path_segment) =>
|
||||
{
|
||||
format!("/_matrix/app/v1/{}/{}", path_segment, value)
|
||||
}
|
||||
Some(path_segment) => match path.next() {
|
||||
Some(path_segment2) if path_segment2 == "thirdparty" => {
|
||||
format!("/_matrix/app/v1/thirdparty/{}/{}", path_segment, value)
|
||||
}
|
||||
_ => return Err(Error::UriPathUnknown),
|
||||
},
|
||||
None => return Err(Error::UriEmptyPath),
|
||||
};
|
||||
use super::*;
|
||||
|
||||
if let Some(query) = uri.query() {
|
||||
path.push('?');
|
||||
path.push_str(query);
|
||||
}
|
||||
|
||||
path
|
||||
}
|
||||
};
|
||||
|
||||
let mut parts = uri.clone().into_parts();
|
||||
parts.path_and_query = Some(path.parse()?);
|
||||
|
||||
let uri = parts.try_into().map_err(http::Error::from)?;
|
||||
*request.uri_mut() = uri;
|
||||
fn registration_string() -> String {
|
||||
include_str!("../tests/registration.yaml").to_owned()
|
||||
}
|
||||
|
||||
Ok(request)
|
||||
async fn appservice(
|
||||
homeserver_url: Option<String>,
|
||||
registration: Option<Registration>,
|
||||
) -> Result<AppService> {
|
||||
let _ = tracing_subscriber::fmt::try_init();
|
||||
|
||||
let registration = match registration {
|
||||
Some(registration) => registration.into(),
|
||||
None => AppServiceRegistration::try_from_yaml_str(registration_string()).unwrap(),
|
||||
};
|
||||
|
||||
let homeserver_url = homeserver_url.unwrap_or_else(|| "http://localhost:1234".to_owned());
|
||||
let server_name = "localhost";
|
||||
|
||||
let client_builder = Client::builder()
|
||||
.request_config(RequestConfig::default().disable_retry())
|
||||
.server_versions([MatrixVersion::V1_0]);
|
||||
|
||||
AppService::with_client_builder(
|
||||
homeserver_url.as_ref(),
|
||||
server_name,
|
||||
registration,
|
||||
client_builder,
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
#[async_test]
|
||||
async fn test_register_virtual_user() -> Result<()> {
|
||||
let server = MockServer::start().await;
|
||||
let appservice = appservice(Some(server.uri()), None).await?;
|
||||
|
||||
let localpart = "someone";
|
||||
Mock::given(method("POST"))
|
||||
.and(path("/_matrix/client/r0/register"))
|
||||
.and(header(
|
||||
"authorization",
|
||||
format!("Bearer {}", appservice.registration().as_token).as_str(),
|
||||
))
|
||||
.and(body_json(json!({
|
||||
"username": localpart.to_owned(),
|
||||
"type": "m.login.application_service"
|
||||
})))
|
||||
.respond_with(ResponseTemplate::new(200).set_body_json(json!({
|
||||
"access_token": "abc123",
|
||||
"device_id": "GHTYAJCE",
|
||||
"user_id": format!("@{localpart}:localhost"),
|
||||
})))
|
||||
.mount(&server)
|
||||
.await;
|
||||
|
||||
appservice.register_virtual_user(localpart, None).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[async_test]
|
||||
async fn test_put_transaction() -> Result<()> {
|
||||
let uri = "/_matrix/app/v1/transactions/1?access_token=hs_token";
|
||||
|
||||
let mut transaction_builder = TransactionBuilder::new();
|
||||
transaction_builder.add_room_event(TimelineTestEvent::Member);
|
||||
let transaction = transaction_builder.build_json_transaction();
|
||||
|
||||
let appservice = appservice(None, None).await?;
|
||||
|
||||
let status = warp::test::request()
|
||||
.method("PUT")
|
||||
.path(uri)
|
||||
.json(&transaction)
|
||||
.filter(&appservice.warp_filter())
|
||||
.await
|
||||
.unwrap()
|
||||
.into_response()
|
||||
.status();
|
||||
|
||||
assert_eq!(status, 200);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[async_test]
|
||||
async fn test_put_transaction_with_repeating_txn_id() -> Result<()> {
|
||||
let uri = "/_matrix/app/v1/transactions/1?access_token=hs_token";
|
||||
|
||||
let mut transaction_builder = TransactionBuilder::new();
|
||||
transaction_builder.add_room_event(TimelineTestEvent::Member);
|
||||
let transaction = transaction_builder.build_json_transaction();
|
||||
|
||||
let appservice = appservice(None, None).await?;
|
||||
|
||||
#[allow(clippy::mutex_atomic)]
|
||||
let on_state_member = Arc::new(Mutex::new(false));
|
||||
appservice
|
||||
.virtual_user(None)
|
||||
.await?
|
||||
.register_event_handler({
|
||||
let on_state_member = on_state_member.clone();
|
||||
move |_ev: OriginalSyncRoomMemberEvent| {
|
||||
*on_state_member.lock().unwrap() = true;
|
||||
future::ready(())
|
||||
}
|
||||
})
|
||||
.await;
|
||||
|
||||
let status = warp::test::request()
|
||||
.method("PUT")
|
||||
.path(uri)
|
||||
.json(&transaction)
|
||||
.filter(&appservice.warp_filter())
|
||||
.await
|
||||
.unwrap()
|
||||
.into_response()
|
||||
.status();
|
||||
|
||||
assert_eq!(status, 200);
|
||||
{
|
||||
let on_room_member_called = *on_state_member.lock().unwrap();
|
||||
assert!(on_room_member_called);
|
||||
}
|
||||
|
||||
// Reset this to check that next time it doesnt get called
|
||||
{
|
||||
let mut on_room_member_called = on_state_member.lock().unwrap();
|
||||
*on_room_member_called = false;
|
||||
}
|
||||
|
||||
let status = warp::test::request()
|
||||
.method("PUT")
|
||||
.path(uri)
|
||||
.json(&transaction)
|
||||
.filter(&appservice.warp_filter())
|
||||
.await
|
||||
.unwrap()
|
||||
.into_response()
|
||||
.status();
|
||||
|
||||
// According to https://spec.matrix.org/v1.2/application-service-api/#pushing-events
|
||||
// This should noop and return 200.
|
||||
assert_eq!(status, 200);
|
||||
{
|
||||
let on_room_member_called = *on_state_member.lock().unwrap();
|
||||
// This time we should not have called the event handler.
|
||||
assert!(!on_room_member_called);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[async_test]
|
||||
async fn test_get_user() -> Result<()> {
|
||||
let appservice = appservice(None, None).await?;
|
||||
appservice.register_user_query(Box::new(|_, _| Box::pin(async move { true }))).await;
|
||||
|
||||
let uri = "/_matrix/app/v1/users/%40_botty_1%3Adev.famedly.local?access_token=hs_token";
|
||||
|
||||
let status = warp::test::request()
|
||||
.method("GET")
|
||||
.path(uri)
|
||||
.filter(&appservice.warp_filter())
|
||||
.await
|
||||
.unwrap()
|
||||
.into_response()
|
||||
.status();
|
||||
|
||||
assert_eq!(status, 200);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[async_test]
|
||||
async fn test_get_room() -> Result<()> {
|
||||
let appservice = appservice(None, None).await?;
|
||||
appservice.register_room_query(Box::new(|_, _| Box::pin(async move { true }))).await;
|
||||
|
||||
let uri = "/_matrix/app/v1/rooms/%23magicforest%3Aexample.com?access_token=hs_token";
|
||||
|
||||
let status = warp::test::request()
|
||||
.method("GET")
|
||||
.path(uri)
|
||||
.filter(&appservice.warp_filter())
|
||||
.await
|
||||
.unwrap()
|
||||
.into_response()
|
||||
.status();
|
||||
|
||||
assert_eq!(status, 200);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[async_test]
|
||||
async fn test_invalid_access_token() -> Result<()> {
|
||||
let uri = "/_matrix/app/v1/transactions/1?access_token=invalid_token";
|
||||
|
||||
let mut transaction_builder = TransactionBuilder::new();
|
||||
let transaction =
|
||||
transaction_builder.add_room_event(TimelineTestEvent::Member).build_json_transaction();
|
||||
|
||||
let appservice = appservice(None, None).await?;
|
||||
|
||||
let status = warp::test::request()
|
||||
.method("PUT")
|
||||
.path(uri)
|
||||
.json(&transaction)
|
||||
.filter(&appservice.warp_filter())
|
||||
.await
|
||||
.unwrap()
|
||||
.into_response()
|
||||
.status();
|
||||
|
||||
assert_eq!(status, 401);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[async_test]
|
||||
async fn test_no_access_token() -> Result<()> {
|
||||
let uri = "/_matrix/app/v1/transactions/1";
|
||||
|
||||
let mut transaction_builder = TransactionBuilder::new();
|
||||
transaction_builder.add_room_event(TimelineTestEvent::Member);
|
||||
let transaction = transaction_builder.build_json_transaction();
|
||||
|
||||
let appservice = appservice(None, None).await?;
|
||||
|
||||
{
|
||||
let status = warp::test::request()
|
||||
.method("PUT")
|
||||
.path(uri)
|
||||
.json(&transaction)
|
||||
.filter(&appservice.warp_filter())
|
||||
.await
|
||||
.unwrap()
|
||||
.into_response()
|
||||
.status();
|
||||
|
||||
assert_eq!(status, 401);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[async_test]
|
||||
async fn test_event_handler() -> Result<()> {
|
||||
let appservice = appservice(None, None).await?;
|
||||
|
||||
#[allow(clippy::mutex_atomic)]
|
||||
let on_state_member = Arc::new(Mutex::new(false));
|
||||
appservice
|
||||
.virtual_user(None)
|
||||
.await?
|
||||
.register_event_handler({
|
||||
let on_state_member = on_state_member.clone();
|
||||
move |_ev: OriginalSyncRoomMemberEvent| {
|
||||
*on_state_member.lock().unwrap() = true;
|
||||
future::ready(())
|
||||
}
|
||||
})
|
||||
.await;
|
||||
|
||||
let uri = "/_matrix/app/v1/transactions/1?access_token=hs_token";
|
||||
|
||||
let mut transaction_builder = TransactionBuilder::new();
|
||||
transaction_builder.add_room_event(TimelineTestEvent::Member);
|
||||
let transaction = transaction_builder.build_json_transaction();
|
||||
|
||||
warp::test::request()
|
||||
.method("PUT")
|
||||
.path(uri)
|
||||
.json(&transaction)
|
||||
.filter(&appservice.warp_filter())
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let on_room_member_called = *on_state_member.lock().unwrap();
|
||||
assert!(on_room_member_called);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[async_test]
|
||||
async fn test_unrelated_path() -> Result<()> {
|
||||
let appservice = appservice(None, None).await?;
|
||||
|
||||
let status = {
|
||||
let consumer_filter = warp::any()
|
||||
.and(appservice.warp_filter())
|
||||
.or(warp::get().and(warp::path("unrelated").map(warp::reply)));
|
||||
|
||||
let response = warp::test::request()
|
||||
.method("GET")
|
||||
.path("/unrelated")
|
||||
.filter(&consumer_filter)
|
||||
.await?
|
||||
.into_response();
|
||||
|
||||
response.status()
|
||||
};
|
||||
|
||||
assert_eq!(status, 200);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[async_test]
|
||||
async fn test_appservice_on_sub_path() -> Result<()> {
|
||||
let room_id = room_id!("!SVkFJHzfwvuaIEawgC:localhost");
|
||||
let uri_1 = "/sub_path/_matrix/app/v1/transactions/1?access_token=hs_token";
|
||||
let uri_2 = "/sub_path/_matrix/app/v1/transactions/2?access_token=hs_token";
|
||||
|
||||
let mut transaction_builder = TransactionBuilder::new();
|
||||
transaction_builder.add_room_event(TimelineTestEvent::Member);
|
||||
let transaction_1 = transaction_builder.build_json_transaction();
|
||||
|
||||
let mut transaction_builder = TransactionBuilder::new();
|
||||
transaction_builder.add_room_event(TimelineTestEvent::MemberNameChange);
|
||||
let transaction_2 = transaction_builder.build_json_transaction();
|
||||
|
||||
let appservice = appservice(None, None).await?;
|
||||
|
||||
{
|
||||
warp::test::request()
|
||||
.method("PUT")
|
||||
.path(uri_1)
|
||||
.json(&transaction_1)
|
||||
.filter(&warp::path("sub_path").and(appservice.warp_filter()))
|
||||
.await?;
|
||||
|
||||
warp::test::request()
|
||||
.method("PUT")
|
||||
.path(uri_2)
|
||||
.json(&transaction_2)
|
||||
.filter(&warp::path("sub_path").and(appservice.warp_filter()))
|
||||
.await?;
|
||||
};
|
||||
|
||||
let members = appservice
|
||||
.virtual_user(None)
|
||||
.await?
|
||||
.get_room(room_id)
|
||||
.expect("Expected room to be available")
|
||||
.members_no_sync()
|
||||
.await?;
|
||||
|
||||
assert_eq!(members[0].display_name().unwrap(), "changed");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[async_test]
|
||||
async fn test_receive_transaction() -> Result<()> {
|
||||
tracing_subscriber::fmt().try_init().ok();
|
||||
let json = vec![
|
||||
Raw::new(&json!({
|
||||
"content": {
|
||||
"avatar_url": null,
|
||||
"displayname": "Appservice",
|
||||
"membership": "join"
|
||||
},
|
||||
"event_id": "$151800140479rdvjg:localhost",
|
||||
"membership": "join",
|
||||
"origin_server_ts": 151800140,
|
||||
"sender": "@_appservice:localhost",
|
||||
"state_key": "@_appservice:localhost",
|
||||
"type": "m.room.member",
|
||||
"room_id": "!coolplace:localhost",
|
||||
"unsigned": {
|
||||
"age": 2970366
|
||||
}
|
||||
}))?
|
||||
.cast::<AnyRoomEvent>(),
|
||||
Raw::new(&json!({
|
||||
"content": {
|
||||
"avatar_url": null,
|
||||
"displayname": "Appservice",
|
||||
"membership": "join"
|
||||
},
|
||||
"event_id": "$151800140491rfbja:localhost",
|
||||
"membership": "join",
|
||||
"origin_server_ts": 151800140,
|
||||
"sender": "@_appservice:localhost",
|
||||
"state_key": "@_appservice:localhost",
|
||||
"type": "m.room.member",
|
||||
"room_id": "!boringplace:localhost",
|
||||
"unsigned": {
|
||||
"age": 2970366
|
||||
}
|
||||
}))?
|
||||
.cast::<AnyRoomEvent>(),
|
||||
Raw::new(&json!({
|
||||
"content": {
|
||||
"avatar_url": null,
|
||||
"displayname": "Alice",
|
||||
"membership": "join"
|
||||
},
|
||||
"event_id": "$151800140517rfvjc:localhost",
|
||||
"membership": "join",
|
||||
"origin_server_ts": 151800140,
|
||||
"sender": "@_appservice_alice:localhost",
|
||||
"state_key": "@_appservice_alice:localhost",
|
||||
"type": "m.room.member",
|
||||
"room_id": "!coolplace:localhost",
|
||||
"unsigned": {
|
||||
"age": 2970366
|
||||
}
|
||||
}))?
|
||||
.cast::<AnyRoomEvent>(),
|
||||
Raw::new(&json!({
|
||||
"content": {
|
||||
"avatar_url": null,
|
||||
"displayname": "Bob",
|
||||
"membership": "invite"
|
||||
},
|
||||
"event_id": "$151800140594rfvjc:localhost",
|
||||
"membership": "invite",
|
||||
"origin_server_ts": 151800174,
|
||||
"sender": "@_appservice_bob:localhost",
|
||||
"state_key": "@_appservice_bob:localhost",
|
||||
"type": "m.room.member",
|
||||
"room_id": "!boringplace:localhost",
|
||||
"unsigned": {
|
||||
"age": 2970366
|
||||
}
|
||||
}))?
|
||||
.cast::<AnyRoomEvent>(),
|
||||
];
|
||||
let appservice = appservice(None, None).await?;
|
||||
|
||||
let alice = appservice.virtual_user(Some("_appservice_alice")).await?;
|
||||
let bob = appservice.virtual_user(Some("_appservice_bob")).await?;
|
||||
appservice
|
||||
.receive_transaction(push_events::v1::IncomingRequest::new("dontcare".into(), json))
|
||||
.await?;
|
||||
let coolplace = room_id!("!coolplace:localhost");
|
||||
let boringplace = room_id!("!boringplace:localhost");
|
||||
assert!(
|
||||
alice.get_joined_room(coolplace).is_some(),
|
||||
"Alice's membership in coolplace should be join"
|
||||
);
|
||||
assert!(
|
||||
bob.get_invited_room(boringplace).is_some(),
|
||||
"Bob's membership in boringplace should be invite"
|
||||
);
|
||||
assert!(alice.get_room(boringplace).is_none(), "Alice should not know about boringplace");
|
||||
assert!(bob.get_room(coolplace).is_none(), "Bob should not know about coolplace");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
mod registration {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_registration() -> Result<()> {
|
||||
let registration: Registration = serde_yaml::from_str(®istration_string())?;
|
||||
let registration: AppServiceRegistration = registration.into();
|
||||
|
||||
assert_eq!(registration.id, "appservice");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_registration_from_yaml_file() -> Result<()> {
|
||||
let registration =
|
||||
AppServiceRegistration::try_from_yaml_file("./tests/registration.yaml")?;
|
||||
|
||||
assert_eq!(registration.id, "appservice");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_registration_from_yaml_str() -> Result<()> {
|
||||
let registration = AppServiceRegistration::try_from_yaml_str(registration_string())?;
|
||||
|
||||
assert_eq!(registration.id, "appservice");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -107,10 +107,11 @@ mod filters {
|
||||
warp::any()
|
||||
.and(valid_access_token(appservice.registration().hs_token.clone()))
|
||||
.map(move || appservice.clone())
|
||||
.and(http_request().and_then(|request| async move {
|
||||
let request = crate::transform_request_path(request).map_err(Error::from)?;
|
||||
Ok::<http::Request<Bytes>, Rejection>(request)
|
||||
}))
|
||||
.and(
|
||||
http_request().and_then(|request| async move {
|
||||
Ok::<http::Request<Bytes>, Rejection>(request)
|
||||
}),
|
||||
)
|
||||
.boxed()
|
||||
}
|
||||
|
||||
|
||||
@@ -1,513 +0,0 @@
|
||||
use std::{
|
||||
future,
|
||||
sync::{Arc, Mutex},
|
||||
};
|
||||
|
||||
use matrix_sdk::{
|
||||
config::RequestConfig,
|
||||
ruma::{api::appservice::Registration, events::room::member::OriginalSyncRoomMemberEvent},
|
||||
Client,
|
||||
};
|
||||
use matrix_sdk_appservice::*;
|
||||
use matrix_sdk_test::{appservice::TransactionBuilder, async_test, test_json, TimelineTestEvent};
|
||||
use ruma::{
|
||||
api::{appservice::event::push_events, MatrixVersion},
|
||||
events::AnyRoomEvent,
|
||||
room_id,
|
||||
serde::Raw,
|
||||
};
|
||||
use serde_json::json;
|
||||
use warp::{Filter, Reply};
|
||||
use wiremock::{
|
||||
matchers::{body_json, header, method, path},
|
||||
Mock, MockServer, ResponseTemplate,
|
||||
};
|
||||
|
||||
fn registration_string() -> String {
|
||||
include_str!("../tests/registration.yaml").to_owned()
|
||||
}
|
||||
|
||||
async fn appservice(
|
||||
homeserver_url: Option<String>,
|
||||
registration: Option<Registration>,
|
||||
) -> Result<AppService> {
|
||||
// env::set_var(
|
||||
// "RUST_LOG",
|
||||
// "wiremock=debug,matrix_sdk=debug,ruma=debug,warp=debug",
|
||||
// );
|
||||
let _ = tracing_subscriber::fmt::try_init();
|
||||
|
||||
let registration = match registration {
|
||||
Some(registration) => registration.into(),
|
||||
None => AppServiceRegistration::try_from_yaml_str(registration_string()).unwrap(),
|
||||
};
|
||||
|
||||
let homeserver_url = homeserver_url.unwrap_or_else(|| "http://localhost:1234".to_owned());
|
||||
let server_name = "localhost";
|
||||
|
||||
let client_builder = Client::builder()
|
||||
.request_config(RequestConfig::default().disable_retry())
|
||||
.server_versions([MatrixVersion::V1_0]);
|
||||
|
||||
AppService::with_client_builder(
|
||||
homeserver_url.as_ref(),
|
||||
server_name,
|
||||
registration,
|
||||
client_builder,
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
#[async_test]
|
||||
async fn test_register_virtual_user() -> Result<()> {
|
||||
let server = MockServer::start().await;
|
||||
let appservice = appservice(Some(server.uri()), None).await?;
|
||||
|
||||
let localpart = "someone";
|
||||
Mock::given(method("POST"))
|
||||
.and(path("/_matrix/client/r0/register"))
|
||||
.and(header(
|
||||
"authorization",
|
||||
format!("Bearer {}", appservice.registration().as_token).as_str(),
|
||||
))
|
||||
.and(body_json(json!({
|
||||
"username": localpart.to_owned(),
|
||||
"type": "m.login.application_service"
|
||||
})))
|
||||
.respond_with(ResponseTemplate::new(200).set_body_json(json!({
|
||||
"access_token": "abc123",
|
||||
"device_id": "GHTYAJCE",
|
||||
"user_id": format!("@{localpart}:localhost"),
|
||||
})))
|
||||
.mount(&server)
|
||||
.await;
|
||||
|
||||
appservice.register_virtual_user(localpart, None).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[async_test]
|
||||
async fn test_put_transaction() -> Result<()> {
|
||||
let uri = "/_matrix/app/v1/transactions/1?access_token=hs_token";
|
||||
|
||||
let mut transaction_builder = TransactionBuilder::new();
|
||||
transaction_builder.add_room_event(TimelineTestEvent::Member);
|
||||
let transaction = transaction_builder.build_json_transaction();
|
||||
|
||||
let appservice = appservice(None, None).await?;
|
||||
|
||||
let status = warp::test::request()
|
||||
.method("PUT")
|
||||
.path(uri)
|
||||
.json(&transaction)
|
||||
.filter(&appservice.warp_filter())
|
||||
.await
|
||||
.unwrap()
|
||||
.into_response()
|
||||
.status();
|
||||
|
||||
assert_eq!(status, 200);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[async_test]
|
||||
async fn test_put_transaction_with_repeating_txn_id() -> Result<()> {
|
||||
let uri = "/_matrix/app/v1/transactions/1?access_token=hs_token";
|
||||
|
||||
let mut transaction_builder = TransactionBuilder::new();
|
||||
transaction_builder.add_room_event(TimelineTestEvent::Member);
|
||||
let transaction = transaction_builder.build_json_transaction();
|
||||
|
||||
let appservice = appservice(None, None).await?;
|
||||
|
||||
#[allow(clippy::mutex_atomic)]
|
||||
let on_state_member = Arc::new(Mutex::new(false));
|
||||
appservice
|
||||
.virtual_user(None)
|
||||
.await?
|
||||
.register_event_handler({
|
||||
let on_state_member = on_state_member.clone();
|
||||
move |_ev: OriginalSyncRoomMemberEvent| {
|
||||
*on_state_member.lock().unwrap() = true;
|
||||
future::ready(())
|
||||
}
|
||||
})
|
||||
.await;
|
||||
|
||||
let status = warp::test::request()
|
||||
.method("PUT")
|
||||
.path(uri)
|
||||
.json(&transaction)
|
||||
.filter(&appservice.warp_filter())
|
||||
.await
|
||||
.unwrap()
|
||||
.into_response()
|
||||
.status();
|
||||
|
||||
assert_eq!(status, 200);
|
||||
{
|
||||
let on_room_member_called = *on_state_member.lock().unwrap();
|
||||
assert!(on_room_member_called);
|
||||
}
|
||||
|
||||
// Reset this to check that next time it doesnt get called
|
||||
{
|
||||
let mut on_room_member_called = on_state_member.lock().unwrap();
|
||||
*on_room_member_called = false;
|
||||
}
|
||||
|
||||
let status = warp::test::request()
|
||||
.method("PUT")
|
||||
.path(uri)
|
||||
.json(&transaction)
|
||||
.filter(&appservice.warp_filter())
|
||||
.await
|
||||
.unwrap()
|
||||
.into_response()
|
||||
.status();
|
||||
|
||||
// According to https://spec.matrix.org/v1.2/application-service-api/#pushing-events
|
||||
// This should noop and return 200.
|
||||
assert_eq!(status, 200);
|
||||
{
|
||||
let on_room_member_called = *on_state_member.lock().unwrap();
|
||||
// This time we should not have called the event handler.
|
||||
assert!(!on_room_member_called);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[async_test]
|
||||
async fn test_get_user() -> Result<()> {
|
||||
let appservice = appservice(None, None).await?;
|
||||
appservice.register_user_query(Box::new(|_, _| Box::pin(async move { true }))).await;
|
||||
|
||||
let uri = "/_matrix/app/v1/users/%40_botty_1%3Adev.famedly.local?access_token=hs_token";
|
||||
|
||||
let status = warp::test::request()
|
||||
.method("GET")
|
||||
.path(uri)
|
||||
.filter(&appservice.warp_filter())
|
||||
.await
|
||||
.unwrap()
|
||||
.into_response()
|
||||
.status();
|
||||
|
||||
assert_eq!(status, 200);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[async_test]
|
||||
async fn test_get_room() -> Result<()> {
|
||||
let appservice = appservice(None, None).await?;
|
||||
appservice.register_room_query(Box::new(|_, _| Box::pin(async move { true }))).await;
|
||||
|
||||
let uri = "/_matrix/app/v1/rooms/%23magicforest%3Aexample.com?access_token=hs_token";
|
||||
|
||||
let status = warp::test::request()
|
||||
.method("GET")
|
||||
.path(uri)
|
||||
.filter(&appservice.warp_filter())
|
||||
.await
|
||||
.unwrap()
|
||||
.into_response()
|
||||
.status();
|
||||
|
||||
assert_eq!(status, 200);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[async_test]
|
||||
async fn test_invalid_access_token() -> Result<()> {
|
||||
let uri = "/_matrix/app/v1/transactions/1?access_token=invalid_token";
|
||||
|
||||
let mut transaction_builder = TransactionBuilder::new();
|
||||
let transaction =
|
||||
transaction_builder.add_room_event(TimelineTestEvent::Member).build_json_transaction();
|
||||
|
||||
let appservice = appservice(None, None).await?;
|
||||
|
||||
let status = warp::test::request()
|
||||
.method("PUT")
|
||||
.path(uri)
|
||||
.json(&transaction)
|
||||
.filter(&appservice.warp_filter())
|
||||
.await
|
||||
.unwrap()
|
||||
.into_response()
|
||||
.status();
|
||||
|
||||
assert_eq!(status, 401);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[async_test]
|
||||
async fn test_no_access_token() -> Result<()> {
|
||||
let uri = "/_matrix/app/v1/transactions/1";
|
||||
|
||||
let mut transaction_builder = TransactionBuilder::new();
|
||||
transaction_builder.add_room_event(TimelineTestEvent::Member);
|
||||
let transaction = transaction_builder.build_json_transaction();
|
||||
|
||||
let appservice = appservice(None, None).await?;
|
||||
|
||||
{
|
||||
let status = warp::test::request()
|
||||
.method("PUT")
|
||||
.path(uri)
|
||||
.json(&transaction)
|
||||
.filter(&appservice.warp_filter())
|
||||
.await
|
||||
.unwrap()
|
||||
.into_response()
|
||||
.status();
|
||||
|
||||
assert_eq!(status, 401);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[async_test]
|
||||
async fn test_event_handler() -> Result<()> {
|
||||
let appservice = appservice(None, None).await?;
|
||||
|
||||
#[allow(clippy::mutex_atomic)]
|
||||
let on_state_member = Arc::new(Mutex::new(false));
|
||||
appservice
|
||||
.virtual_user(None)
|
||||
.await?
|
||||
.register_event_handler({
|
||||
let on_state_member = on_state_member.clone();
|
||||
move |_ev: OriginalSyncRoomMemberEvent| {
|
||||
*on_state_member.lock().unwrap() = true;
|
||||
future::ready(())
|
||||
}
|
||||
})
|
||||
.await;
|
||||
|
||||
let uri = "/_matrix/app/v1/transactions/1?access_token=hs_token";
|
||||
|
||||
let mut transaction_builder = TransactionBuilder::new();
|
||||
transaction_builder.add_room_event(TimelineTestEvent::Member);
|
||||
let transaction = transaction_builder.build_json_transaction();
|
||||
|
||||
warp::test::request()
|
||||
.method("PUT")
|
||||
.path(uri)
|
||||
.json(&transaction)
|
||||
.filter(&appservice.warp_filter())
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let on_room_member_called = *on_state_member.lock().unwrap();
|
||||
assert!(on_room_member_called);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[async_test]
|
||||
async fn test_unrelated_path() -> Result<()> {
|
||||
let appservice = appservice(None, None).await?;
|
||||
|
||||
let status = {
|
||||
let consumer_filter = warp::any()
|
||||
.and(appservice.warp_filter())
|
||||
.or(warp::get().and(warp::path("unrelated").map(warp::reply)));
|
||||
|
||||
let response = warp::test::request()
|
||||
.method("GET")
|
||||
.path("/unrelated")
|
||||
.filter(&consumer_filter)
|
||||
.await?
|
||||
.into_response();
|
||||
|
||||
response.status()
|
||||
};
|
||||
|
||||
assert_eq!(status, 200);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[async_test]
|
||||
async fn test_appservice_on_sub_path() -> Result<()> {
|
||||
let room_id = &test_json::DEFAULT_SYNC_ROOM_ID;
|
||||
let uri_1 = "/sub_path/_matrix/app/v1/transactions/1?access_token=hs_token";
|
||||
let uri_2 = "/sub_path/_matrix/app/v1/transactions/2?access_token=hs_token";
|
||||
|
||||
let mut transaction_builder = TransactionBuilder::new();
|
||||
transaction_builder.add_room_event(TimelineTestEvent::Member);
|
||||
let transaction_1 = transaction_builder.build_json_transaction();
|
||||
|
||||
let mut transaction_builder = TransactionBuilder::new();
|
||||
transaction_builder.add_room_event(TimelineTestEvent::MemberNameChange);
|
||||
let transaction_2 = transaction_builder.build_json_transaction();
|
||||
|
||||
let appservice = appservice(None, None).await?;
|
||||
|
||||
{
|
||||
warp::test::request()
|
||||
.method("PUT")
|
||||
.path(uri_1)
|
||||
.json(&transaction_1)
|
||||
.filter(&warp::path("sub_path").and(appservice.warp_filter()))
|
||||
.await?;
|
||||
|
||||
warp::test::request()
|
||||
.method("PUT")
|
||||
.path(uri_2)
|
||||
.json(&transaction_2)
|
||||
.filter(&warp::path("sub_path").and(appservice.warp_filter()))
|
||||
.await?;
|
||||
};
|
||||
|
||||
let members = appservice
|
||||
.virtual_user(None)
|
||||
.await?
|
||||
.get_room(room_id)
|
||||
.expect("Expected room to be available")
|
||||
.members_no_sync()
|
||||
.await?;
|
||||
|
||||
assert_eq!(members[0].display_name().unwrap(), "changed");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[async_test]
|
||||
async fn test_receive_transaction() -> Result<()> {
|
||||
tracing_subscriber::fmt().try_init().ok();
|
||||
let json = vec![
|
||||
Raw::new(&json!({
|
||||
"content": {
|
||||
"avatar_url": null,
|
||||
"displayname": "Appservice",
|
||||
"membership": "join"
|
||||
},
|
||||
"event_id": "$151800140479rdvjg:localhost",
|
||||
"membership": "join",
|
||||
"origin_server_ts": 151800140,
|
||||
"sender": "@_appservice:localhost",
|
||||
"state_key": "@_appservice:localhost",
|
||||
"type": "m.room.member",
|
||||
"room_id": "!coolplace:localhost",
|
||||
"unsigned": {
|
||||
"age": 2970366
|
||||
}
|
||||
}))?
|
||||
.cast::<AnyRoomEvent>(),
|
||||
Raw::new(&json!({
|
||||
"content": {
|
||||
"avatar_url": null,
|
||||
"displayname": "Appservice",
|
||||
"membership": "join"
|
||||
},
|
||||
"event_id": "$151800140491rfbja:localhost",
|
||||
"membership": "join",
|
||||
"origin_server_ts": 151800140,
|
||||
"sender": "@_appservice:localhost",
|
||||
"state_key": "@_appservice:localhost",
|
||||
"type": "m.room.member",
|
||||
"room_id": "!boringplace:localhost",
|
||||
"unsigned": {
|
||||
"age": 2970366
|
||||
}
|
||||
}))?
|
||||
.cast::<AnyRoomEvent>(),
|
||||
Raw::new(&json!({
|
||||
"content": {
|
||||
"avatar_url": null,
|
||||
"displayname": "Alice",
|
||||
"membership": "join"
|
||||
},
|
||||
"event_id": "$151800140517rfvjc:localhost",
|
||||
"membership": "join",
|
||||
"origin_server_ts": 151800140,
|
||||
"sender": "@_appservice_alice:localhost",
|
||||
"state_key": "@_appservice_alice:localhost",
|
||||
"type": "m.room.member",
|
||||
"room_id": "!coolplace:localhost",
|
||||
"unsigned": {
|
||||
"age": 2970366
|
||||
}
|
||||
}))?
|
||||
.cast::<AnyRoomEvent>(),
|
||||
Raw::new(&json!({
|
||||
"content": {
|
||||
"avatar_url": null,
|
||||
"displayname": "Bob",
|
||||
"membership": "invite"
|
||||
},
|
||||
"event_id": "$151800140594rfvjc:localhost",
|
||||
"membership": "invite",
|
||||
"origin_server_ts": 151800174,
|
||||
"sender": "@_appservice_bob:localhost",
|
||||
"state_key": "@_appservice_bob:localhost",
|
||||
"type": "m.room.member",
|
||||
"room_id": "!boringplace:localhost",
|
||||
"unsigned": {
|
||||
"age": 2970366
|
||||
}
|
||||
}))?
|
||||
.cast::<AnyRoomEvent>(),
|
||||
];
|
||||
let appservice = appservice(None, None).await?;
|
||||
|
||||
let alice = appservice.virtual_user(Some("_appservice_alice")).await?;
|
||||
let bob = appservice.virtual_user(Some("_appservice_bob")).await?;
|
||||
appservice
|
||||
.receive_transaction(push_events::v1::IncomingRequest::new("dontcare".into(), json))
|
||||
.await?;
|
||||
let coolplace = room_id!("!coolplace:localhost");
|
||||
let boringplace = room_id!("!boringplace:localhost");
|
||||
assert!(
|
||||
alice.get_joined_room(coolplace).is_some(),
|
||||
"Alice's membership in coolplace should be join"
|
||||
);
|
||||
assert!(
|
||||
bob.get_invited_room(boringplace).is_some(),
|
||||
"Bob's membership in boringplace should be invite"
|
||||
);
|
||||
assert!(alice.get_room(boringplace).is_none(), "Alice should not know about boringplace");
|
||||
assert!(bob.get_room(coolplace).is_none(), "Bob should not know about coolplace");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
mod registration {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_registration() -> Result<()> {
|
||||
let registration: Registration = serde_yaml::from_str(®istration_string())?;
|
||||
let registration: AppServiceRegistration = registration.into();
|
||||
|
||||
assert_eq!(registration.id, "appservice");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_registration_from_yaml_file() -> Result<()> {
|
||||
let registration = AppServiceRegistration::try_from_yaml_file("./tests/registration.yaml")?;
|
||||
|
||||
assert_eq!(registration.id, "appservice");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_registration_from_yaml_str() -> Result<()> {
|
||||
let registration = AppServiceRegistration::try_from_yaml_str(registration_string())?;
|
||||
|
||||
assert_eq!(registration.id, "appservice");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user