Skip to content

Commit 40d3dd5

Browse files
committed
tests(threads): add an exhaustive test to check for all notification mode combinations
1 parent 4e2655a commit 40d3dd5

File tree

4 files changed

+324
-3
lines changed

4 files changed

+324
-3
lines changed

crates/matrix-sdk/src/notification_settings/mod.rs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -571,6 +571,12 @@ impl NotificationSettings {
571571
}
572572
Ok(())
573573
}
574+
575+
/// Returns the inner ruleset currently known by this
576+
/// [`NotificationSettings`] instance.
577+
pub async fn ruleset(&self) -> Ruleset {
578+
self.rules.read().await.ruleset.clone()
579+
}
574580
}
575581

576582
// The http mocking library is not supported for wasm32

crates/matrix-sdk/src/room/mod.rs

Lines changed: 44 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -125,7 +125,7 @@ use ruma::{
125125
StaticStateEventContent, SyncStateEvent,
126126
},
127127
int,
128-
push::{Action, PushConditionRoomCtx, Ruleset},
128+
push::{Action, AnyPushRuleRef, PushConditionRoomCtx, Ruleset},
129129
serde::Raw,
130130
time::Instant,
131131
EventId, Int, MatrixToUri, MatrixUri, MxcUri, OwnedEventId, OwnedRoomId, OwnedServerName,
@@ -226,6 +226,49 @@ impl PushContext {
226226
pub async fn for_event<T>(&self, event: &Raw<T>) -> Vec<Action> {
227227
self.push_rules.get_actions(event, &self.push_condition_room_ctx).await.to_owned()
228228
}
229+
230+
/// Compute the push rules for a given event, with extra logging to help
231+
/// debugging.
232+
#[doc(hidden)]
233+
#[instrument(skip_all)]
234+
pub async fn traced_for_event<T>(&self, event: &Raw<T>) -> Vec<Action> {
235+
let rules = self
236+
.push_rules
237+
.iter()
238+
.filter_map(|r| {
239+
if !r.enabled() {
240+
return None;
241+
}
242+
243+
let simplified_action = if r.actions().is_empty() { "inhibit" } else { "notify" };
244+
245+
let conditions = match r {
246+
AnyPushRuleRef::Override(r) => {
247+
format!("{:?}", r.conditions)
248+
}
249+
AnyPushRuleRef::Content(r) => format!("content-body-match:{}", r.pattern),
250+
AnyPushRuleRef::Room(r) => format!("room-match:{}", r.rule_id),
251+
AnyPushRuleRef::Sender(r) => format!("sender-match:{}", r.rule_id),
252+
AnyPushRuleRef::Underride(r) => format!("{:?}", r.conditions),
253+
_ => "<unknown push rule kind>".to_owned(),
254+
};
255+
256+
Some(format!("- {}: {conditions} => {simplified_action}", r.rule_id(),))
257+
})
258+
.collect::<Vec<_>>()
259+
.join("\n");
260+
trace!("rules:\n\n{rules}\n\n");
261+
262+
let found = self.push_rules.get_match(event, &self.push_condition_room_ctx).await;
263+
264+
if let Some(found) = found {
265+
trace!("rule {} matched", found.rule_id());
266+
found.actions().to_owned()
267+
} else {
268+
trace!("no match");
269+
Vec::new()
270+
}
271+
}
229272
}
230273

231274
macro_rules! make_media_type {

crates/matrix-sdk/src/test_utils/mocks/mod.rs

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1385,13 +1385,68 @@ impl MatrixMockServer {
13851385
self.mock_endpoint(mock, EnablePushRuleEndpoint).expect_default_access_token()
13861386
}
13871387

1388+
/// Create a prebuilt mock for the endpoint used to set push rules actions.
1389+
pub fn mock_set_push_rules_actions(
1390+
&self,
1391+
kind: RuleKind,
1392+
rule_id: PushRuleIdSpec<'_>,
1393+
) -> MockEndpoint<'_, SetPushRulesActionsEndpoint> {
1394+
let rule_id = rule_id.to_path();
1395+
let mock = Mock::given(method("PUT")).and(path_regex(format!(
1396+
"^/_matrix/client/v3/pushrules/global/{kind}/{rule_id}/actions",
1397+
)));
1398+
self.mock_endpoint(mock, SetPushRulesActionsEndpoint).expect_default_access_token()
1399+
}
1400+
1401+
/// Create a prebuilt mock for the endpoint used to set push rules.
1402+
pub fn mock_set_push_rules(
1403+
&self,
1404+
kind: RuleKind,
1405+
rule_id: PushRuleIdSpec<'_>,
1406+
) -> MockEndpoint<'_, SetPushRulesEndpoint> {
1407+
let rule_id = rule_id.to_path();
1408+
let mock = Mock::given(method("PUT"))
1409+
.and(path_regex(format!("^/_matrix/client/v3/pushrules/global/{kind}/{rule_id}$",)));
1410+
self.mock_endpoint(mock, SetPushRulesEndpoint).expect_default_access_token()
1411+
}
1412+
1413+
/// Create a prebuilt mock for the endpoint used to delete push rules.
1414+
pub fn mock_delete_push_rules(
1415+
&self,
1416+
kind: RuleKind,
1417+
rule_id: PushRuleIdSpec<'_>,
1418+
) -> MockEndpoint<'_, DeletePushRulesEndpoint> {
1419+
let rule_id = rule_id.to_path();
1420+
let mock = Mock::given(method("DELETE"))
1421+
.and(path_regex(format!("^/_matrix/client/v3/pushrules/global/{kind}/{rule_id}$",)));
1422+
self.mock_endpoint(mock, DeletePushRulesEndpoint).expect_default_access_token()
1423+
}
1424+
13881425
/// Create a prebuilt mock for the federation version endpoint.
13891426
pub fn mock_federation_version(&self) -> MockEndpoint<'_, FederationVersionEndpoint> {
13901427
let mock = Mock::given(method("GET")).and(path("/_matrix/federation/v1/version"));
13911428
self.mock_endpoint(mock, FederationVersionEndpoint)
13921429
}
13931430
}
13941431

1432+
/// A specification for a push rule ID.
1433+
pub enum PushRuleIdSpec<'a> {
1434+
/// A precise rule ID.
1435+
Some(&'a str),
1436+
/// Any rule ID should match.
1437+
Any,
1438+
}
1439+
1440+
impl<'a> PushRuleIdSpec<'a> {
1441+
/// Convert this [`PushRuleIdSpec`] to a path.
1442+
pub fn to_path(&self) -> &str {
1443+
match self {
1444+
PushRuleIdSpec::Some(id) => id,
1445+
PushRuleIdSpec::Any => "[^/]*",
1446+
}
1447+
}
1448+
}
1449+
13951450
/// Parameter to [`MatrixMockServer::sync_room`].
13961451
pub enum AnyRoomBuilder {
13971452
/// A room we've been invited to.
@@ -4027,6 +4082,39 @@ impl<'a> MockEndpoint<'a, EnablePushRuleEndpoint> {
40274082
}
40284083
}
40294084

4085+
/// A prebuilt mock for `PUT
4086+
/// /_matrix/client/v3/pushrules/global/{kind}/{ruleId}/actions`.
4087+
pub struct SetPushRulesActionsEndpoint;
4088+
4089+
impl<'a> MockEndpoint<'a, SetPushRulesActionsEndpoint> {
4090+
/// Returns a successful empty JSON response.
4091+
pub fn ok(self) -> MatrixMock<'a> {
4092+
self.ok_empty_json()
4093+
}
4094+
}
4095+
4096+
/// A prebuilt mock for `PUT
4097+
/// /_matrix/client/v3/pushrules/global/{kind}/{ruleId}`.
4098+
pub struct SetPushRulesEndpoint;
4099+
4100+
impl<'a> MockEndpoint<'a, SetPushRulesEndpoint> {
4101+
/// Returns a successful empty JSON response.
4102+
pub fn ok(self) -> MatrixMock<'a> {
4103+
self.ok_empty_json()
4104+
}
4105+
}
4106+
4107+
/// A prebuilt mock for `DELETE
4108+
/// /_matrix/client/v3/pushrules/global/{kind}/{ruleId}`.
4109+
pub struct DeletePushRulesEndpoint;
4110+
4111+
impl<'a> MockEndpoint<'a, DeletePushRulesEndpoint> {
4112+
/// Returns a successful empty JSON response.
4113+
pub fn ok(self) -> MatrixMock<'a> {
4114+
self.ok_empty_json()
4115+
}
4116+
}
4117+
40304118
/// A prebuilt mock for the federation version endpoint.
40314119
pub struct FederationVersionEndpoint;
40324120

crates/matrix-sdk/tests/integration/room/thread.rs

Lines changed: 186 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,11 @@
11
use assert_matches2::assert_matches;
2-
use matrix_sdk::{room::ThreadSubscription, test_utils::mocks::MatrixMockServer};
2+
use matrix_sdk::{
3+
notification_settings::RoomNotificationMode,
4+
room::ThreadSubscription,
5+
test_utils::mocks::{MatrixMockServer, PushRuleIdSpec},
6+
};
37
use matrix_sdk_test::{async_test, event_factory::EventFactory, JoinedRoomBuilder, ALICE};
4-
use ruma::{event_id, owned_event_id, room_id};
8+
use ruma::{event_id, owned_event_id, push::RuleKind, room_id};
59

610
#[async_test]
711
async fn test_subscribe_thread() {
@@ -238,3 +242,183 @@ async fn test_thread_push_rule_is_triggered_for_subscribed_threads() {
238242
let actions = push_context.for_event(&event).await;
239243
assert!(actions.is_empty());
240244
}
245+
246+
#[async_test]
247+
async fn test_thread_push_rules_and_notification_modes() {
248+
// This test checks that, given a combination of a global notification mode, and
249+
// a room notification mode, we do get notifications for thread events according
250+
// to the subscriptions.
251+
252+
let server = MatrixMockServer::new().await;
253+
let client = server
254+
.client_builder()
255+
.on_builder(|builder| {
256+
builder.with_threading_support(matrix_sdk::ThreadingSupport::Enabled {
257+
with_subscriptions: true,
258+
})
259+
})
260+
.build()
261+
.await;
262+
263+
let room_id = room_id!("!test:example.org");
264+
let f = EventFactory::new().room(room_id).sender(*ALICE);
265+
// Make it so that the client has a member event for the current user.
266+
let room = server
267+
.sync_room(
268+
&client,
269+
JoinedRoomBuilder::new(room_id).add_state_event(f.member(client.user_id().unwrap())),
270+
)
271+
.await;
272+
273+
// Sanity check: we can get a push context.
274+
room.push_context()
275+
.await
276+
.expect("getting a push context works")
277+
.expect("the push context should exist");
278+
279+
// Mock push rules endpoints, allowing any modification to any rule.
280+
server.mock_set_push_rules_actions(RuleKind::Underride, PushRuleIdSpec::Any).ok().mount().await;
281+
server.mock_set_push_rules_actions(RuleKind::Room, PushRuleIdSpec::Any).ok().mount().await;
282+
server.mock_set_push_rules(RuleKind::Underride, PushRuleIdSpec::Any).ok().mount().await;
283+
server.mock_set_push_rules(RuleKind::Room, PushRuleIdSpec::Any).ok().mount().await;
284+
server.mock_set_push_rules(RuleKind::Override, PushRuleIdSpec::Any).ok().mount().await;
285+
server.mock_delete_push_rules(RuleKind::Room, PushRuleIdSpec::Any).ok().mount().await;
286+
server.mock_delete_push_rules(RuleKind::Override, PushRuleIdSpec::Any).ok().mount().await;
287+
288+
// Given a thread I'm subscribed to,
289+
let thread_root_id = owned_event_id!("$root");
290+
291+
server
292+
.mock_get_thread_subscription()
293+
.match_room_id(room_id.to_owned())
294+
.match_thread_id(thread_root_id.clone())
295+
.ok(true)
296+
.mount()
297+
.await;
298+
299+
// Given an event in the thread I'm subscribed to,
300+
let event =
301+
f.text_msg("hello to you too!").in_thread(&thread_root_id, &thread_root_id).into_raw_sync();
302+
303+
// If global mode = AllMessages,
304+
let settings = client.notification_settings().await;
305+
306+
let is_encrypted = false;
307+
let is_one_to_one = false;
308+
settings
309+
.set_default_room_notification_mode(
310+
is_encrypted.into(),
311+
is_one_to_one.into(),
312+
RoomNotificationMode::AllMessages,
313+
)
314+
.await
315+
.unwrap();
316+
317+
// If room mode = AllMessages,
318+
settings.set_room_notification_mode(room_id, RoomNotificationMode::AllMessages).await.unwrap();
319+
// Ack the push rules change via sync, for it to be applied in the push context.
320+
let ruleset = settings.ruleset().await;
321+
server
322+
.mock_sync()
323+
.ok_and_run(&client, |builder| {
324+
builder.add_global_account_data(f.push_rules(ruleset));
325+
})
326+
.await;
327+
328+
// The thread event will trigger a notification.
329+
let actions = room.push_context().await.unwrap().unwrap().traced_for_event(&event).await;
330+
assert!(actions.iter().any(|action| action.should_notify()));
331+
332+
// If room mode = mentions and keywords only,
333+
settings
334+
.set_room_notification_mode(room_id, RoomNotificationMode::MentionsAndKeywordsOnly)
335+
.await
336+
.unwrap();
337+
let ruleset = settings.ruleset().await;
338+
server
339+
.mock_sync()
340+
.ok_and_run(&client, |builder| {
341+
builder.add_global_account_data(f.push_rules(ruleset));
342+
})
343+
.await;
344+
345+
// The thread event will trigger a notification.
346+
let actions = room.push_context().await.unwrap().unwrap().traced_for_event(&event).await;
347+
// TODO: unexpected! this should trigger a thread mention
348+
//assert!(actions.iter().any(|action| action.should_notify()));
349+
assert!(!actions.iter().any(|action| action.should_notify()));
350+
351+
// If room mode = mute,
352+
settings.set_room_notification_mode(room_id, RoomNotificationMode::Mute).await.unwrap();
353+
let ruleset = settings.ruleset().await;
354+
server
355+
.mock_sync()
356+
.ok_and_run(&client, |builder| {
357+
builder.add_global_account_data(f.push_rules(ruleset));
358+
})
359+
.await;
360+
361+
// The thread event will not trigger a notification, as the room has been muted.
362+
let actions = room.push_context().await.unwrap().unwrap().traced_for_event(&event).await;
363+
assert!(!actions.iter().any(|action| action.should_notify()));
364+
365+
// Now, if global mode = mentions only,
366+
settings
367+
.set_default_room_notification_mode(
368+
is_encrypted.into(),
369+
is_one_to_one.into(),
370+
RoomNotificationMode::MentionsAndKeywordsOnly,
371+
)
372+
.await
373+
.unwrap();
374+
375+
// If room mode = AllMessages,
376+
settings.set_room_notification_mode(room_id, RoomNotificationMode::AllMessages).await.unwrap();
377+
// Ack the push rules change via sync, for it to be applied in the push context.
378+
let ruleset = settings.ruleset().await;
379+
server
380+
.mock_sync()
381+
.ok_and_run(&client, |builder| {
382+
builder.add_global_account_data(f.push_rules(ruleset));
383+
})
384+
.await;
385+
386+
// The thread event will trigger a notification.
387+
let actions = room.push_context().await.unwrap().unwrap().traced_for_event(&event).await;
388+
assert!(actions.iter().any(|action| action.should_notify()));
389+
390+
// If room mode = Mentions only,
391+
settings
392+
.set_room_notification_mode(room_id, RoomNotificationMode::MentionsAndKeywordsOnly)
393+
.await
394+
.unwrap();
395+
// Ack the push rules change via sync, for it to be applied in the push context.
396+
let ruleset = settings.ruleset().await;
397+
server
398+
.mock_sync()
399+
.ok_and_run(&client, |builder| {
400+
builder.add_global_account_data(f.push_rules(ruleset));
401+
})
402+
.await;
403+
404+
// The thread event will trigger a notification.
405+
let actions = room.push_context().await.unwrap().unwrap().traced_for_event(&event).await;
406+
// TODO: unexpected! this should trigger a thread mention
407+
//assert!(actions.iter().any(|action| action.should_notify()));
408+
assert!(!actions.iter().any(|action| action.should_notify()));
409+
410+
// If room mode = mute,
411+
settings.set_room_notification_mode(room_id, RoomNotificationMode::Mute).await.unwrap();
412+
// Ack the push rules change via sync, for it to be applied in the push context.
413+
let ruleset = settings.ruleset().await;
414+
server
415+
.mock_sync()
416+
.ok_and_run(&client, |builder| {
417+
builder.add_global_account_data(f.push_rules(ruleset));
418+
})
419+
.await;
420+
421+
// The thread event will not trigger a notification, as the room has been muted.
422+
let actions = room.push_context().await.unwrap().unwrap().traced_for_event(&event).await;
423+
assert!(!actions.iter().any(|action| action.should_notify()));
424+
}

0 commit comments

Comments
 (0)