From b585963abbcd2ebcbc92197e39c9c4db12a70ca2 Mon Sep 17 00:00:00 2001 From: Benjamin Bouvier Date: Wed, 6 Aug 2025 13:57:24 +0200 Subject: [PATCH] test(notification client): check msc4306 behavior in the notification client too --- .../tests/integration/notification_client.rs | 240 +++++++++++++++++- crates/matrix-sdk/src/test_utils/mocks/mod.rs | 7 +- testing/matrix-sdk-test/src/event_factory.rs | 6 + 3 files changed, 247 insertions(+), 6 deletions(-) diff --git a/crates/matrix-sdk-ui/tests/integration/notification_client.rs b/crates/matrix-sdk-ui/tests/integration/notification_client.rs index 2317ab07a..7c9bc59cd 100644 --- a/crates/matrix-sdk-ui/tests/integration/notification_client.rs +++ b/crates/matrix-sdk-ui/tests/integration/notification_client.rs @@ -7,6 +7,7 @@ use std::{ use assert_matches::assert_matches; use assert_matches2::assert_let; use matrix_sdk::{ + ThreadingSupport, config::SyncSettings, test_utils::{logged_in_client_with_server, mocks::MatrixMockServer}, }; @@ -23,8 +24,8 @@ use matrix_sdk_ui::{ }; use ruma::{ RoomVersionId, event_id, - events::{TimelineEventType, room::member::MembershipState}, - mxc_uri, room_id, user_id, + events::{Mentions, TimelineEventType, room::member::MembershipState}, + mxc_uri, owned_user_id, room_id, user_id, }; use serde_json::json; use wiremock::{ @@ -114,6 +115,234 @@ async fn test_notification_client_with_context() { assert_eq!(item.sender_avatar_url, Some(sender_avatar_url.to_string())); } +#[async_test] +async fn test_subscribed_threads_get_notifications() { + let server = MatrixMockServer::new().await; + let client = server + .client_builder() + .on_builder(|builder| { + builder.with_threading_support(ThreadingSupport::Enabled { with_subscriptions: true }) + }) + .build() + .await; + + let sender = user_id!("@user:example.org"); + let room_id = room_id!("!a98sd12bjh:example.org"); + let f = EventFactory::new().room(room_id).sender(sender); + + // First, mock an empty sync so the room is known. + server.mock_room_state_encryption().plain().mount().await; + + // To have access to the push rules context, we must know the own's member + // event. + let room = server + .sync_room( + &client, + JoinedRoomBuilder::new(room_id).add_state_event( + f.member(client.user_id().unwrap()) + .membership(MembershipState::Join) + .display_name("Jean-Michel Rouille"), + ), + ) + .await; + + // Sanity check: we can create push rules context. + room.push_context().await.unwrap().unwrap(); + + // Create a notification client. + let sync_service = Arc::new(SyncService::builder(client.clone()).build().await.unwrap()); + let process_setup = NotificationProcessSetup::SingleProcess { sync_service }; + let notification_client = NotificationClient::new(client, process_setup).await.unwrap(); + + // For a thread I'm subscribed to, + let thread_root = event_id!("$thread_root"); + server + .mock_get_thread_subscription() + .match_thread_id(thread_root.to_owned()) + .ok(false) + .expect(2) + .mount() + .await; + + let sender_member_event = + f.member(sender).membership(MembershipState::Join).display_name("John Diaspora").into_raw(); + + // Considering an in-thread message, + let in_thread_event = { + let event_id = event_id!("$example_event_id"); + let event = f + .text_msg("hello to you too!") + .event_id(event_id) + .server_ts(152049794) + .in_thread(thread_root, thread_root) + .into_event(); + + server + .mock_room_event_context() + .match_event_id() + .ok(event.clone(), "", "", vec![sender_member_event.clone()]) + .mock_once() + .mount() + .await; + + // Then I get a notification for this message. + let item = + notification_client.get_notification_with_context(room_id, event_id).await.unwrap(); + assert_matches!(item, NotificationStatus::Event(..)); + + event + }; + + // Considering the thread root event, + let event = f + .text_msg("hello world") + .event_id(thread_root) + .server_ts(152049793) + .with_bundled_thread_summary(in_thread_event.raw().clone().cast_unchecked(), 1, false) + .into_event(); + + server + .mock_room_event_context() + .match_event_id() + .ok(event.clone(), "", "", vec![sender_member_event]) + .mock_once() + .mount() + .await; + + // Then I get a notification for the thread root as well. + let item = + notification_client.get_notification_with_context(room_id, thread_root).await.unwrap(); + assert_matches!(item, NotificationStatus::Event(..)); +} + +#[async_test] +async fn test_unsubscribed_threads_get_notifications() { + let server = MatrixMockServer::new().await; + let client = server + .client_builder() + .on_builder(|builder| { + builder.with_threading_support(ThreadingSupport::Enabled { with_subscriptions: true }) + }) + .build() + .await; + + let sender = user_id!("@user:example.org"); + let room_id = room_id!("!a98sd12bjh:example.org"); + let f = EventFactory::new().room(room_id).sender(sender); + + // First, mock an empty sync so the room is known. + server.mock_room_state_encryption().plain().mount().await; + + // To have access to the push rules context, we must know the own's member + // event. + let room = server + .sync_room( + &client, + JoinedRoomBuilder::new(room_id).add_state_event( + f.member(client.user_id().unwrap()) + .membership(MembershipState::Join) + .display_name("Jean-Michel Rouille"), + ), + ) + .await; + + // Sanity check: we can create push rules context. + room.push_context().await.unwrap().unwrap(); + + // Create a notification client. + let sync_service = Arc::new(SyncService::builder(client.clone()).build().await.unwrap()); + let process_setup = NotificationProcessSetup::SingleProcess { sync_service }; + let notification_client = NotificationClient::new(client.clone(), process_setup).await.unwrap(); + + // For a thread with an unknown subscription status (note: we're not mocking the + // get endpoint, since 404 is equivalent to no thread status), + let thread_root = event_id!("$thread_root"); + + let sender_member_event = + f.member(sender).membership(MembershipState::Join).display_name("John Diaspora").into_raw(); + + // Considering a random in-thread message, + let first_thread_response = event_id!("$thread_response1"); + let in_thread_event = { + // Note: contains a mention, but not of me. + let event = f + .text_msg("hello to you too!") + .event_id(first_thread_response) + .server_ts(152049794) + .in_thread(thread_root, thread_root) + .mentions(Mentions::with_user_ids([owned_user_id!("@rando:example.org")])) + .into_event(); + + server + .mock_room_event_context() + .match_event_id() + .ok(event.clone(), "", "", vec![sender_member_event.clone()]) + .mock_once() + .mount() + .await; + + // Then I don't get a notification for it. + let item = notification_client + .get_notification_with_context(room_id, first_thread_response) + .await + .unwrap(); + assert_matches!(item, NotificationStatus::EventFilteredOut); + + event + }; + + // Considering the thread root event, + { + let event = f + .text_msg("hello world") + .event_id(thread_root) + .server_ts(152049793) + .with_bundled_thread_summary(in_thread_event.raw().clone().cast_unchecked(), 1, false) + .into_event(); + + server + .mock_room_event_context() + .match_event_id() + .ok(event.clone(), "", "", vec![sender_member_event.clone()]) + .mock_once() + .mount() + .await; + + // I do get a notification about it, because it's not technically in the thread. + let item = + notification_client.get_notification_with_context(room_id, thread_root).await.unwrap(); + assert_matches!(item, NotificationStatus::Event(..)); + } + + // But if a new in-thread event mentions me, then I would get subscribed, + { + let thread_response2 = event_id!("$thread_response2"); + // Note: event mentions me. + let event = f + .text_msg("hello world") + .event_id(thread_response2) + .server_ts(152049793) + .in_thread(thread_root, first_thread_response) + .mentions(Mentions::with_user_ids(vec![client.user_id().unwrap().to_owned()])) + .into_event(); + + server + .mock_room_event_context() + .match_event_id() + .ok(event.clone(), "", "", vec![sender_member_event]) + .mock_once() + .mount() + .await; + + // Then I do get a notification for the thread root either. + let item = notification_client + .get_notification_with_context(room_id, thread_response2) + .await + .unwrap(); + assert_matches!(item, NotificationStatus::Event(..)); + } +} + #[async_test] async fn test_notification_client_sliding_sync() { let room_id = room_id!("!a98sd12bjh:example.org"); @@ -929,7 +1158,12 @@ async fn test_notification_client_context_filters_out_events_from_ignored_users( .into_event(); // Mock the /context response - server.mock_room_event_context().ok(event, "start", "end").mock_once().mount().await; + server + .mock_room_event_context() + .ok(event, "start", "end", Vec::new()) + .mock_once() + .mount() + .await; let dummy_sync_service = Arc::new(SyncService::builder(client.clone()).build().await.unwrap()); let process_setup = diff --git a/crates/matrix-sdk/src/test_utils/mocks/mod.rs b/crates/matrix-sdk/src/test_utils/mocks/mod.rs index 75e289293..ef5dcf2ed 100644 --- a/crates/matrix-sdk/src/test_utils/mocks/mod.rs +++ b/crates/matrix-sdk/src/test_utils/mocks/mod.rs @@ -2505,15 +2505,16 @@ impl<'a> MockEndpoint<'a, RoomEventContextEndpoint> { self } - /// Returns an endpoint that emulates success + /// Returns an endpoint that emulates success. pub fn ok( self, event: TimelineEvent, start: impl Into, end: impl Into, + state_events: Vec>, ) -> MatrixMock<'a> { let event_path = if self.endpoint.match_event_id { - let event_id = event.kind.event_id().expect("an event id is required"); + let event_id = event.event_id().expect("an event id is required"); // The event id should begin with `$`, which would be taken as the end of the // regex so we need to escape it event_id.as_str().replace("$", "\\$") @@ -2531,7 +2532,7 @@ impl<'a> MockEndpoint<'a, RoomEventContextEndpoint> { "event": event.into_raw().json(), "end": end.into(), "start": start.into(), - "state": [] + "state": state_events }))); MatrixMock { server: self.server, mock } } diff --git a/testing/matrix-sdk-test/src/event_factory.rs b/testing/matrix-sdk-test/src/event_factory.rs index faab9245a..87396b4f5 100644 --- a/testing/matrix-sdk-test/src/event_factory.rs +++ b/testing/matrix-sdk-test/src/event_factory.rs @@ -371,6 +371,12 @@ impl EventBuilder { self } + /// Adds the given mentions to the current event. + pub fn mentions(mut self, mentions: Mentions) -> Self { + self.content.mentions = Some(mentions); + self + } + /// Adds a replacement relation to the current event, with the new content /// passed. pub fn edit(