test(notification client): check msc4306 behavior in the notification client too

This commit is contained in:
Benjamin Bouvier
2025-08-06 13:57:24 +02:00
parent 5719fde701
commit b585963abb
3 changed files with 247 additions and 6 deletions

View File

@@ -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 =

View File

@@ -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<String>,
end: impl Into<String>,
state_events: Vec<Raw<AnyStateEvent>>,
) -> 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 }
}

View File

@@ -371,6 +371,12 @@ impl EventBuilder<RoomMessageEventContent> {
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(