Skip to content

Commit cf625b4

Browse files
committed
tests(threads): add an exhaustive test to check for all notification mode combinations
1 parent 6871858 commit cf625b4

File tree

4 files changed

+328
-3
lines changed

4 files changed

+328
-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
@@ -127,7 +127,7 @@ use ruma::{
127127
StaticStateEventContent, SyncStateEvent,
128128
},
129129
int,
130-
push::{Action, PushConditionRoomCtx, Ruleset},
130+
push::{Action, AnyPushRuleRef, PushConditionRoomCtx, Ruleset},
131131
serde::Raw,
132132
time::Instant,
133133
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+
_ => format!("<unknown push rule kind>"),
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: 190 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,187 @@ 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 trigger a notification.
362+
let actions = room.push_context().await.unwrap().unwrap().traced_for_event(&event).await;
363+
// TODO: unexpected! this should trigger a thread mention
364+
//assert!(actions.iter().any(|action| action.should_notify()));
365+
assert!(!actions.iter().any(|action| action.should_notify()));
366+
367+
// Now, if global mode = mentions only,
368+
settings
369+
.set_default_room_notification_mode(
370+
is_encrypted.into(),
371+
is_one_to_one.into(),
372+
RoomNotificationMode::MentionsAndKeywordsOnly,
373+
)
374+
.await
375+
.unwrap();
376+
377+
// If room mode = AllMessages,
378+
settings.set_room_notification_mode(room_id, RoomNotificationMode::AllMessages).await.unwrap();
379+
// Ack the push rules change via sync, for it to be applied in the push context.
380+
let ruleset = settings.ruleset().await;
381+
server
382+
.mock_sync()
383+
.ok_and_run(&client, |builder| {
384+
builder.add_global_account_data(f.push_rules(ruleset));
385+
})
386+
.await;
387+
388+
// The thread event will trigger a notification.
389+
let actions = room.push_context().await.unwrap().unwrap().traced_for_event(&event).await;
390+
assert!(actions.iter().any(|action| action.should_notify()));
391+
392+
// If room mode = Mentions only,
393+
settings
394+
.set_room_notification_mode(room_id, RoomNotificationMode::MentionsAndKeywordsOnly)
395+
.await
396+
.unwrap();
397+
// Ack the push rules change via sync, for it to be applied in the push context.
398+
let ruleset = settings.ruleset().await;
399+
server
400+
.mock_sync()
401+
.ok_and_run(&client, |builder| {
402+
builder.add_global_account_data(f.push_rules(ruleset));
403+
})
404+
.await;
405+
406+
// The thread event will trigger a notification.
407+
let actions = room.push_context().await.unwrap().unwrap().traced_for_event(&event).await;
408+
// TODO: unexpected! this should trigger a thread mention
409+
//assert!(actions.iter().any(|action| action.should_notify()));
410+
assert!(!actions.iter().any(|action| action.should_notify()));
411+
412+
// If room mode = mute,
413+
settings.set_room_notification_mode(room_id, RoomNotificationMode::Mute).await.unwrap();
414+
// Ack the push rules change via sync, for it to be applied in the push context.
415+
let ruleset = settings.ruleset().await;
416+
server
417+
.mock_sync()
418+
.ok_and_run(&client, |builder| {
419+
builder.add_global_account_data(f.push_rules(ruleset));
420+
})
421+
.await;
422+
423+
// The thread event will trigger a notification.
424+
let actions = room.push_context().await.unwrap().unwrap().traced_for_event(&event).await;
425+
// TODO: unexpected! this should trigger a thread mention
426+
//assert!(actions.iter().any(|action| action.should_notify()));
427+
assert!(!actions.iter().any(|action| action.should_notify()));
428+
}

0 commit comments

Comments
 (0)