diff --git a/crates/matrix-sdk-appservice/src/error.rs b/crates/matrix-sdk-appservice/src/error.rs index de3d15ed2..68a863ed5 100644 --- a/crates/matrix-sdk-appservice/src/error.rs +++ b/crates/matrix-sdk-appservice/src/error.rs @@ -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), diff --git a/crates/matrix-sdk-appservice/src/lib.rs b/crates/matrix-sdk-appservice/src/lib.rs index 10b612c2c..7fb2519c8 100644 --- a/crates/matrix-sdk-appservice/src/lib.rs +++ b/crates/matrix-sdk-appservice/src/lib.rs @@ -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, -) -> Result> { - 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, + registration: Option, + ) -> Result { + 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::(), + 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::(), + 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::(), + 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::(), + ]; + 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(()) + } + } } diff --git a/crates/matrix-sdk-appservice/src/webserver.rs b/crates/matrix-sdk-appservice/src/webserver.rs index e649b6f53..9e29a08a9 100644 --- a/crates/matrix-sdk-appservice/src/webserver.rs +++ b/crates/matrix-sdk-appservice/src/webserver.rs @@ -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::, Rejection>(request) - })) + .and( + http_request().and_then(|request| async move { + Ok::, Rejection>(request) + }), + ) .boxed() } diff --git a/crates/matrix-sdk-appservice/tests/tests.rs b/crates/matrix-sdk-appservice/tests/tests.rs deleted file mode 100644 index fbebc9670..000000000 --- a/crates/matrix-sdk-appservice/tests/tests.rs +++ /dev/null @@ -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, - registration: Option, -) -> Result { - // 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::(), - 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::(), - 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::(), - 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::(), - ]; - 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(()) - } -}