tests(notification): add integration tests for notification events

This commit is contained in:
Benjamin Bouvier
2023-07-28 19:31:35 +02:00
parent 4ebc3d70ad
commit 10f709450e
8 changed files with 269 additions and 26 deletions

2
Cargo.lock generated
View File

@@ -4668,7 +4668,9 @@ dependencies = [
"futures-util",
"matrix-sdk",
"matrix-sdk-integration-testing",
"matrix-sdk-ui",
"tokio",
"tracing",
"uuid",
]

View File

@@ -71,6 +71,8 @@ use crate::{config::RequestConfig, error::RumaApiError, http_client::HttpClient,
#[derive(Clone, Debug)]
pub struct ClientBuilder {
homeserver_cfg: Option<HomeserverConfig>,
#[cfg(feature = "experimental-sliding-sync")]
sliding_sync_proxy: Option<String>,
http_cfg: Option<HttpConfig>,
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<str>) -> 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<Url> = 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");

View File

@@ -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<Client> {
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 {

View File

@@ -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" }

View File

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

View File

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

View File

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

View File

@@ -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::<OwnedEventId>("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(())
}