From 10f709450ed2d2f45f97b65fc2026efbeaf41ee5 Mon Sep 17 00:00:00 2001 From: Benjamin Bouvier Date: Fri, 28 Jul 2023 19:31:35 +0200 Subject: [PATCH] tests(notification): add integration tests for notification events --- Cargo.lock | 2 + crates/matrix-sdk/src/client/builder.rs | 25 ++- .../src/helpers.rs | 15 +- .../sliding-sync-integration-test/Cargo.toml | 2 + .../assets/ci-start.sh | 38 ++-- .../assets/docker-compose.yml | 4 +- .../sliding-sync-integration-test/src/lib.rs | 2 + .../src/notification_client.rs | 207 ++++++++++++++++++ 8 files changed, 269 insertions(+), 26 deletions(-) create mode 100644 testing/sliding-sync-integration-test/src/notification_client.rs diff --git a/Cargo.lock b/Cargo.lock index 23ba6595e..1e243b1d9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4668,7 +4668,9 @@ dependencies = [ "futures-util", "matrix-sdk", "matrix-sdk-integration-testing", + "matrix-sdk-ui", "tokio", + "tracing", "uuid", ] diff --git a/crates/matrix-sdk/src/client/builder.rs b/crates/matrix-sdk/src/client/builder.rs index f958a46a3..66ea63b39 100644 --- a/crates/matrix-sdk/src/client/builder.rs +++ b/crates/matrix-sdk/src/client/builder.rs @@ -71,6 +71,8 @@ use crate::{config::RequestConfig, error::RumaApiError, http_client::HttpClient, #[derive(Clone, Debug)] pub struct ClientBuilder { homeserver_cfg: Option, + #[cfg(feature = "experimental-sliding-sync")] + sliding_sync_proxy: Option, http_cfg: Option, store_config: BuilderStoreConfig, request_config: RequestConfig, @@ -85,6 +87,8 @@ impl ClientBuilder { pub(crate) fn new() -> Self { Self { homeserver_cfg: None, + #[cfg(feature = "experimental-sliding-sync")] + sliding_sync_proxy: None, http_cfg: None, store_config: BuilderStoreConfig::Custom(StoreConfig::default()), request_config: Default::default(), @@ -106,6 +110,18 @@ impl ClientBuilder { self } + /// Set the sliding-sync proxy URL to use. + /// + /// This is used only if the homeserver URL was defined with + /// [`Self::homeserver_url`]. If the homeserver address was defined with + /// [`Self::server_name`], then auto-discovery via the `.well-known` + /// endpoint will be performed. + #[cfg(feature = "experimental-sliding-sync")] + pub fn sliding_sync_proxy(mut self, url: impl AsRef) -> Self { + self.sliding_sync_proxy = Some(url.as_ref().to_owned()); + self + } + /// Set the server name to discover the homeserver from. /// /// We assume we can connect in HTTPS to that server. If that's not the @@ -383,7 +399,14 @@ impl ClientBuilder { let mut sliding_sync_proxy: Option = None; let homeserver = match homeserver_cfg { - HomeserverConfig::Url(url) => url, + HomeserverConfig::Url(url) => { + #[cfg(feature = "experimental-sliding-sync")] + { + sliding_sync_proxy = + self.sliding_sync_proxy.as_ref().map(|url| Url::parse(url)).transpose()?; + } + url + } HomeserverConfig::ServerName { server: server_name, protocol } => { debug!("Trying to discover the homeserver"); diff --git a/testing/matrix-sdk-integration-testing/src/helpers.rs b/testing/matrix-sdk-integration-testing/src/helpers.rs index ef317ec9f..ca1bf2e9b 100644 --- a/testing/matrix-sdk-integration-testing/src/helpers.rs +++ b/testing/matrix-sdk-integration-testing/src/helpers.rs @@ -28,28 +28,25 @@ fn init_logging() { .init(); } -/// read the test configuration from the environment -pub fn test_server_conf() -> (String, String) { - ( - option_env!("HOMESERVER_URL").unwrap_or("http://localhost:8228").to_owned(), - option_env!("HOMESERVER_DOMAIN").unwrap_or("matrix-sdk.rs").to_owned(), - ) -} - pub async fn get_client_for_user(username: String, use_sqlite_store: bool) -> Result { let mut users = USERS.lock().await; if let Some((client, _)) = users.get(&username) { return Ok(client.clone()); } - let (homeserver_url, _domain_name) = test_server_conf(); + let homeserver_url = + option_env!("HOMESERVER_URL").unwrap_or("http://localhost:8228").to_owned(); + let sliding_sync_proxy_url = + option_env!("SLIDING_SYNC_PROXY_URL").unwrap_or("http://localhost:8338").to_owned(); let tmp_dir = tempdir()?; let client_builder = Client::builder() .user_agent("matrix-sdk-integration-tests") .homeserver_url(homeserver_url) + .sliding_sync_proxy(sliding_sync_proxy_url) .request_config(RequestConfig::short_retry()); + let client = if use_sqlite_store { client_builder.sqlite_store(tmp_dir.path(), None).build().await? } else { diff --git a/testing/sliding-sync-integration-test/Cargo.toml b/testing/sliding-sync-integration-test/Cargo.toml index 1445d1bb5..36174878b 100644 --- a/testing/sliding-sync-integration-test/Cargo.toml +++ b/testing/sliding-sync-integration-test/Cargo.toml @@ -12,5 +12,7 @@ eyeball-im = { workspace = true } futures-util = { workspace = true } matrix-sdk-integration-testing = { path = "../matrix-sdk-integration-testing", features = ["helpers"] } matrix-sdk = { path = "../../crates/matrix-sdk", features = ["experimental-sliding-sync", "testing"] } +matrix-sdk-ui = { path = "../../crates/matrix-sdk-ui" } tokio = { workspace = true, features = ["rt", "rt-multi-thread", "macros"] } +tracing = { workspace = true } uuid = { version = "1.2.2" } diff --git a/testing/sliding-sync-integration-test/assets/ci-start.sh b/testing/sliding-sync-integration-test/assets/ci-start.sh index cce1ff4bd..911878063 100644 --- a/testing/sliding-sync-integration-test/assets/ci-start.sh +++ b/testing/sliding-sync-integration-test/assets/ci-start.sh @@ -4,6 +4,8 @@ export SYNAPSE_SERVER_NAME=matrix-sdk.rs export SYNAPSE_REPORT_STATS=no echo " ====== Generating config ====== " /start.py generate + +# Courtesy of https://github.com/michaelkaye/setup-matrix-synapse/blob/e2067245b5265640f94d310fc79a1d388cc70452/create.js#L99 echo " ====== Patching for CI ====== " echo """ enable_registration: true @@ -16,11 +18,33 @@ rc_message: rc_registration: per_second: 1000 burst_count: 1000 - + +rc_login: + address: + per_second: 1000 + burst_count: 1000 + account: + per_second: 1000 + burst_count: 1000 + failed_attempts: + per_second: 1000 + burst_count: 1000 + +rc_admin_redaction: + per_second: 1000 + burst_count: 1000 + rc_joins: local: per_second: 1000 burst_count: 1000 + remote: + per_second: 1000 + burst_count: 1000 + +rc_3pid_validation: + per_second: 1000 + burst_count: 1000 rc_invites: per_room: @@ -32,18 +56,6 @@ rc_invites: per_issuer: per_second: 1000 burst_count: 1000 - -rc_login: - address: - per_second: 1000 - burst_count: 1000 -# account: -# per_second: 0.17 -# burst_count: 3 -# failed_attempts: -# per_second: 0.17 -# burst_count: 3 - """ >> /data/homeserver.yaml echo " ====== Starting server with: ====== " diff --git a/testing/sliding-sync-integration-test/assets/docker-compose.yml b/testing/sliding-sync-integration-test/assets/docker-compose.yml index 3b86c7182..2c8f6354f 100644 --- a/testing/sliding-sync-integration-test/assets/docker-compose.yml +++ b/testing/sliding-sync-integration-test/assets/docker-compose.yml @@ -9,7 +9,6 @@ services: disable: true volumes: - ./data/synapse:/data - ports: - 8228:8008/tcp @@ -28,11 +27,10 @@ services: - ./data/db:/var/lib/postgresql/data sliding-sync-proxy: - image: ghcr.io/matrix-org/sliding-sync:v0.99.2 + image: ghcr.io/matrix-org/sliding-sync:v0.99.4 depends_on: postgres: condition: service_healthy - links: - synapse - postgres diff --git a/testing/sliding-sync-integration-test/src/lib.rs b/testing/sliding-sync-integration-test/src/lib.rs index 10eef44f5..415ea9434 100644 --- a/testing/sliding-sync-integration-test/src/lib.rs +++ b/testing/sliding-sync-integration-test/src/lib.rs @@ -5,6 +5,8 @@ use futures_util::{pin_mut, stream::StreamExt}; use matrix_sdk::{Client, RoomListEntry, SlidingSyncBuilder, SlidingSyncList, SlidingSyncMode}; use matrix_sdk_integration_testing::helpers::get_client_for_user; +mod notification_client; + async fn setup( name: String, use_sqlite_store: bool, diff --git a/testing/sliding-sync-integration-test/src/notification_client.rs b/testing/sliding-sync-integration-test/src/notification_client.rs new file mode 100644 index 000000000..dafbdb457 --- /dev/null +++ b/testing/sliding-sync-integration-test/src/notification_client.rs @@ -0,0 +1,207 @@ +use std::time::{SystemTime, UNIX_EPOCH}; + +use anyhow::{ensure, Result}; +use assert_matches::assert_matches; +use matrix_sdk::{ + config::SyncSettings, + ruma::{ + api::client::room::create_room::v3::Request as CreateRoomRequest, + assign, + events::{ + room::{member::MembershipState, message::RoomMessageEventContent}, + AnyStrippedStateEvent, SyncMessageLikeEvent, TimelineEventType, + }, + OwnedEventId, + }, + RoomState, +}; +use matrix_sdk_integration_testing::helpers::get_client_for_user; +use matrix_sdk_ui::notification_client::{ + Error, NotificationClient, NotificationEvent, NotificationItem, +}; +use tracing::warn; + +#[tokio::test(flavor = "multi_thread", worker_threads = 4)] +async fn test_notification() -> Result<()> { + // Create new users for each test run, to avoid conflicts with invites existing + // from previous runs. + let time = SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_millis(); + let alice = get_client_for_user(format!("alice{time}"), true).await?; + let bob = get_client_for_user(format!("bob{time}"), true).await?; + + // Alice changes display name. + const ALICE_NAME: &str = "Alice, Queen of Cryptography"; + alice.account().set_display_name(Some(ALICE_NAME)).await?; + + // Initial setup: Alice creates a room, invites Bob. + let invite = vec![bob.user_id().expect("bob has a userid!").to_owned()]; + let request = assign!(CreateRoomRequest::new(), { + invite, + is_direct: true, + }); + + let alice_room = alice.create_room(request).await?; + + const ROOM_NAME: &str = "Kingdom of Integration Testing"; + alice_room.set_name(Some(ROOM_NAME.to_owned())).await?; + + let room_id = alice_room.room_id().to_owned(); + + // Bob receives a notification about it. + + let bob_invite_response = bob.sync_once(Default::default()).await?; + let sync_token = bob_invite_response.next_batch; + + let mut invited_rooms = bob_invite_response.rooms.invite.into_iter(); + + let (_, invited_room) = invited_rooms.next().expect("must be invited to one room"); + assert!(invited_rooms.next().is_none(), "no more invited rooms: {invited_rooms:#?}"); + + if let Some(event_id) = invited_room.invite_state.events.iter().find_map(|event| { + let Ok(AnyStrippedStateEvent::RoomMember(room_member_ev)) = event.deserialize() else { + return None; + }; + + if room_member_ev.content.membership != MembershipState::Invite { + return None; + } + + let Ok(Some(event_id)) = event.get_field::("event_id") else { + return None; + }; + + Some(event_id) + }) { + warn!("We found the invite event!"); + + // Try with sliding sync first. + let notification_client = NotificationClient::builder(bob.clone()).await.unwrap().build(); + let notification = notification_client + .get_notification_with_sliding_sync(&room_id, &event_id) + .await? + .expect("missing notification for the invite"); + + warn!("sliding_sync: checking invite notification"); + + assert_eq!(notification.event.sender(), alice.user_id().unwrap()); + assert_eq!(notification.joined_members_count, 1); + assert_eq!(notification.is_room_encrypted, None); + assert!(notification.is_direct_message_room); + + assert_matches!(notification.event, NotificationEvent::Invite(observed_invite) => { + assert_eq!(observed_invite.content.membership, MembershipState::Invite); + }); + + assert_eq!(notification.sender_display_name.as_deref(), Some(ALICE_NAME)); + + // In theory, the room name ought to be ROOM_NAME here, but the sliding sync + // proxy returns the other person's name as the room's name (as of + // 2023-08-04). + assert!(notification.room_display_name != ROOM_NAME); + assert_eq!(notification.room_display_name, ALICE_NAME); + + // Then with /context. + let notification_client = NotificationClient::builder(bob.clone()).await.unwrap().build(); + let notification = + notification_client.get_notification_with_context(&room_id, &event_id).await; + // We aren't authorized to inspect events from rooms we were not invited to. + assert!(matches!(notification.unwrap_err(), Error::SdkError(matrix_sdk::Error::Http(..)))); + } else { + warn!("Couldn't get the invite event."); + } + + // Bob accepts the invite, joins the room. + { + let room = bob.get_room(&room_id).expect("bob doesn't know about the room"); + ensure!( + room.state() == RoomState::Invited, + "The room alice invited bob in isn't an invite: {room:?}" + ); + let details = room.invite_details().await?; + let sender = details.inviter.expect("invite details doesn't have inviter"); + assert_eq!(sender.user_id(), alice.user_id().expect("alice has a user_id")); + } + + // Bob joins the room. + bob.get_room(alice_room.room_id()).unwrap().join().await?; + + // Now Alice sends a message to Bob. + alice_room.send(RoomMessageEventContent::text_plain("Hello world!"), None).await?; + + // In this sync, bob receives the message from Alice. + let bob_response = bob.sync_once(SyncSettings::default().token(sync_token)).await?; + + let mut joined_rooms = bob_response.rooms.join.into_iter(); + let (_, bob_room) = joined_rooms.next().expect("must have joined one room"); + assert!(joined_rooms.next().is_none(), "no more joined rooms: {joined_rooms:#?}"); + + let event_id = bob_room + .timeline + .events + .iter() + .find_map(|event| { + let event = event.event.deserialize().ok()?; + if event.event_type() == TimelineEventType::RoomMessage { + Some(event.event_id().to_owned()) + } else { + None + } + }) + .expect("missing message from alice in bob's client"); + + // Get the notification for the given message. + let check_notification = |is_sliding_sync: bool, notification: NotificationItem| { + warn!( + "{}: checking message notification", + if is_sliding_sync { "sliding sync" } else { "/context query" } + ); + + assert_eq!(notification.event.sender(), alice.user_id().unwrap()); + + if is_sliding_sync { + assert_eq!(notification.joined_members_count, 2); + } else { + // This can't be computed for /context, because we only get a single request, + // and not a full sync response that would contain a room summary. + warn!("joined member counts: {}", notification.joined_members_count); + } + + assert_eq!(notification.is_room_encrypted, Some(false)); + assert!(notification.is_direct_message_room); + + assert_matches!( + notification.event, + NotificationEvent::Timeline( + matrix_sdk::ruma::events::AnySyncTimelineEvent::MessageLike( + matrix_sdk::ruma::events::AnySyncMessageLikeEvent::RoomMessage( + SyncMessageLikeEvent::Original(event) + ) + ) + ) => { + assert_matches!(event.content.msgtype, + matrix_sdk::ruma::events::room::message::MessageType::Text(text) => { + assert_eq!(text.body, "Hello world!"); + }); + } + ); + + assert_eq!(notification.sender_display_name.as_deref(), Some(ALICE_NAME)); + assert_eq!(notification.room_display_name, ROOM_NAME); + }; + + let notification_client = NotificationClient::builder(bob.clone()).await.unwrap().build(); + let notification = notification_client + .get_notification_with_sliding_sync(&room_id, &event_id) + .await? + .expect("missing notification for the message"); + check_notification(true, notification); + + let notification_client = NotificationClient::builder(bob.clone()).await.unwrap().build(); + let notification = notification_client + .get_notification_with_context(&room_id, &event_id) + .await? + .expect("missing notification for the message"); + check_notification(false, notification); + + Ok(()) +}