Skip to content

Commit e89ac3d

Browse files
committed
tests: add some sliding sync tests for thread subscriptions and catchup
1 parent a3704c3 commit e89ac3d

3 files changed

Lines changed: 281 additions & 8 deletions

File tree

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

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -659,15 +659,19 @@ impl SlidingSync {
659659
|| !self.inner.lists.read().await.is_empty()
660660
}
661661

662+
/// Send a single sliding sync request, and returns the response summary.
663+
///
664+
/// Public for testing purposes only.
665+
#[doc(hidden)]
662666
#[instrument(skip_all, fields(pos, conn_id = self.inner.id))]
663-
async fn sync_once(&self) -> Result<UpdateSummary> {
667+
pub async fn sync_once(&self) -> Result<UpdateSummary> {
664668
let (request, request_config, position_guard) =
665669
self.generate_sync_request(&mut LazyTransactionId::new()).await?;
666670

667-
// Send the request, kaboom.
671+
// Send the request.
668672
let summaries = self.send_sync_request(request, request_config, position_guard).await?;
669673

670-
// Notify a new sync was received
674+
// Notify a new sync was received.
671675
self.inner.client.inner.sync_beat.notify(usize::MAX);
672676

673677
Ok(summaries)

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

Lines changed: 61 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ use ruma::{
3333
api::client::{
3434
receipt::create_receipt::v3::ReceiptType,
3535
room::Visibility,
36+
sync::sync_events::v5,
3637
threads::get_thread_subscriptions_changes::unstable::{
3738
ThreadSubscription, ThreadUnsubscription,
3839
},
@@ -68,7 +69,7 @@ pub mod encryption;
6869
pub mod oauth;
6970

7071
use super::client::MockClientBuilder;
71-
use crate::{room::IncludeRelations, Client, OwnedServerName, Room};
72+
use crate::{room::IncludeRelations, Client, OwnedServerName, Room, SlidingSyncBuilder};
7273

7374
/// Structure used to store the crypto keys uploaded to the server.
7475
/// They will be served back to clients when requested.
@@ -353,6 +354,13 @@ impl MatrixMockServer {
353354
)
354355
}
355356

357+
/// Mocks the sliding sync endpoint.
358+
pub fn mock_sliding_sync(&self) -> MockEndpoint<'_, SlidingSyncEndpoint> {
359+
let mock = Mock::given(method("POST"))
360+
.and(path("/_matrix/client/unstable/org.matrix.simplified_msc3575/sync"));
361+
self.mock_endpoint(mock, SlidingSyncEndpoint)
362+
}
363+
356364
/// Creates a prebuilt mock for joining a room.
357365
///
358366
/// # Examples
@@ -4172,6 +4180,8 @@ pub struct GetThreadSubscriptionsEndpoint {
41724180
subscribed: BTreeMap<OwnedRoomId, BTreeMap<OwnedEventId, ThreadSubscription>>,
41734181
/// New thread unsubscriptions per (room id, thread root event id).
41744182
unsubscribed: BTreeMap<OwnedRoomId, BTreeMap<OwnedEventId, ThreadUnsubscription>>,
4183+
/// Optional delay to respond to the query.
4184+
delay: Option<Duration>,
41754185
}
41764186

41774187
impl<'a> MockEndpoint<'a, GetThreadSubscriptionsEndpoint> {
@@ -4197,6 +4207,12 @@ impl<'a> MockEndpoint<'a, GetThreadSubscriptionsEndpoint> {
41974207
self
41984208
}
41994209

4210+
/// Respond with a given delay to the query.
4211+
pub fn with_delay(mut self, delay: Duration) -> Self {
4212+
self.endpoint.delay = Some(delay);
4213+
self
4214+
}
4215+
42004216
/// Match the `from` query parameter to a given value.
42014217
pub fn match_from(self, from: &str) -> Self {
42024218
Self { mock: self.mock.and(query_param("from", from)), ..self }
@@ -4214,7 +4230,14 @@ impl<'a> MockEndpoint<'a, GetThreadSubscriptionsEndpoint> {
42144230
"unsubscribed": self.endpoint.unsubscribed,
42154231
"end": end,
42164232
});
4217-
self.respond_with(ResponseTemplate::new(200).set_body_json(response_body))
4233+
4234+
let mut template = ResponseTemplate::new(200).set_body_json(response_body);
4235+
4236+
if let Some(delay) = self.endpoint.delay {
4237+
template = template.set_delay(delay);
4238+
}
4239+
4240+
self.respond_with(template)
42184241
}
42194242
}
42204243

@@ -4280,3 +4303,39 @@ impl<'a> MockEndpoint<'a, GetHierarchyEndpoint> {
42804303
})))
42814304
}
42824305
}
4306+
4307+
/// A prebuilt mock for running simplified sliding sync.
4308+
pub struct SlidingSyncEndpoint;
4309+
4310+
impl<'a> MockEndpoint<'a, SlidingSyncEndpoint> {
4311+
/// Mocks the sliding sync endpoint with the given response.
4312+
pub fn ok(self, response: v5::Response) -> MatrixMock<'a> {
4313+
// A bit silly that we need to destructure all the fields ourselves, but
4314+
// Response isn't serializable :'(
4315+
self.respond_with(ResponseTemplate::new(200).set_body_json(json!({
4316+
"txn_id": response.txn_id,
4317+
"pos": response.pos,
4318+
"lists": response.lists,
4319+
"rooms": response.rooms,
4320+
"extensions": response.extensions,
4321+
})))
4322+
}
4323+
4324+
/// Temporarily mocks the sync with the given endpoint and runs a client
4325+
/// sync with it.
4326+
///
4327+
/// After calling this function, the sync endpoint isn't mocked anymore.
4328+
pub async fn ok_and_run<F: FnOnce(SlidingSyncBuilder) -> SlidingSyncBuilder>(
4329+
self,
4330+
client: &Client,
4331+
on_builder: F,
4332+
response: v5::Response,
4333+
) {
4334+
let _scope = self.ok(response).mount_as_scoped().await;
4335+
4336+
let sliding_sync =
4337+
on_builder(client.sliding_sync("test_id").unwrap()).build().await.unwrap();
4338+
4339+
let _summary = sliding_sync.sync_once().await.unwrap();
4340+
}
4341+
}

crates/matrix-sdk/tests/integration/client.rs

Lines changed: 213 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,13 @@ use futures_util::FutureExt;
66
use matrix_sdk::{
77
authentication::oauth::{error::OAuthTokenRevocationError, OAuthError},
88
config::{RequestConfig, StoreConfig, SyncSettings, SyncToken},
9-
store::RoomLoadSettings,
9+
sleep::sleep,
10+
store::{RoomLoadSettings, ThreadSubscriptionStatus},
1011
sync::{RoomUpdate, State},
1112
test_utils::{
1213
client::mock_matrix_session, mocks::MatrixMockServer, no_retry_test_client_with_server,
1314
},
14-
Client, Error, MemoryStore, StateChanges, StateStore,
15+
Client, Error, MemoryStore, SlidingSyncList, StateChanges, StateStore, ThreadingSupport,
1516
};
1617
use matrix_sdk_base::{sync::RoomUpdates, RoomState};
1718
use matrix_sdk_common::executor::spawn;
@@ -36,6 +37,7 @@ use ruma::{
3637
get_public_rooms,
3738
get_public_rooms_filtered::{self, v3::Request as PublicRoomsFilterRequest},
3839
},
40+
sync::sync_events::v5,
3941
threads::get_thread_subscriptions_changes::unstable::{
4042
ThreadSubscription, ThreadUnsubscription,
4143
},
@@ -56,7 +58,7 @@ use ruma::{
5658
room::JoinRule,
5759
room_id,
5860
serde::Raw,
59-
uint, user_id, OwnedUserId,
61+
uint, user_id, EventId, OwnedUserId, RoomId,
6062
};
6163
use serde_json::{json, Value as JsonValue};
6264
use stream_assert::{assert_next_matches, assert_pending};
@@ -1587,3 +1589,211 @@ async fn test_fetch_thread_subscriptions() {
15871589
let u = &response.unsubscribed[&room3][&thread3];
15881590
assert_eq!(u.bump_stamp, uint!(13));
15891591
}
1592+
1593+
/// Create a sliding sync thread_subscription response with no `prev_batch`
1594+
/// token.
1595+
fn thread_subscription_response(
1596+
room1: &RoomId,
1597+
thread1: &EventId,
1598+
room2: &RoomId,
1599+
thread2: &EventId,
1600+
) -> v5::response::ThreadSubscriptions {
1601+
assign!(v5::response::ThreadSubscriptions::default(), {
1602+
subscribed: {
1603+
let mut map = BTreeMap::new();
1604+
map.insert(room1.to_owned(), {
1605+
let mut threads = BTreeMap::new();
1606+
threads.insert(thread1.to_owned(), ThreadSubscription::new(true, uint!(42)));
1607+
threads
1608+
});
1609+
map
1610+
},
1611+
unsubscribed: {
1612+
let mut map = BTreeMap::new();
1613+
map.insert(room2.to_owned(), {
1614+
let mut threads = BTreeMap::new();
1615+
threads.insert(thread2.to_owned(), ThreadUnsubscription::new(uint!(7)));
1616+
threads
1617+
});
1618+
map
1619+
},
1620+
prev_batch: None,
1621+
})
1622+
}
1623+
1624+
#[async_test]
1625+
async fn test_sync_thread_subscriptions() {
1626+
let server = MatrixMockServer::new().await;
1627+
let client = server.client_builder().build().await;
1628+
1629+
let room1 = owned_room_id!("!room1:example.com");
1630+
let room2 = owned_room_id!("!room2:example.com");
1631+
1632+
let thread1 = owned_event_id!("$thread1:example.com");
1633+
let thread2 = owned_event_id!("$thread2:example.com");
1634+
1635+
// At first, there are no thread subscriptions at all.
1636+
let stored1 = client
1637+
.state_store()
1638+
.load_thread_subscription(&room1, &thread1)
1639+
.await
1640+
.expect("loading room1/thread1 works fine");
1641+
assert_matches!(stored1, None);
1642+
1643+
let stored2 = client
1644+
.state_store()
1645+
.load_thread_subscription(&room2, &thread2)
1646+
.await
1647+
.expect("loading room2/thread2 works fine");
1648+
assert_matches!(stored2, None);
1649+
1650+
// When I sliding-sync thread subscriptions,
1651+
server
1652+
.mock_sliding_sync()
1653+
.ok_and_run(
1654+
&client,
1655+
|config_builder| {
1656+
config_builder.with_thread_subscriptions_extension(
1657+
assign!(v5::request::ThreadSubscriptions::default(), {
1658+
enabled: Some(true),
1659+
limit: Some(uint!(10)),
1660+
}),
1661+
)
1662+
},
1663+
assign!(v5::Response::new("pos".to_owned()), {
1664+
extensions: assign!(v5::response::Extensions::default(), {
1665+
thread_subscriptions: thread_subscription_response(
1666+
&room1, &thread1, &room2, &thread2,
1667+
),
1668+
}),
1669+
}),
1670+
)
1671+
.await;
1672+
1673+
// Then they're stored in the local database.
1674+
let stored1 = client
1675+
.state_store()
1676+
.load_thread_subscription(&room1, &thread1)
1677+
.await
1678+
.expect("loading room1/thread1 works fine")
1679+
.expect("found room1/thread1 subscription");
1680+
1681+
assert_eq!(stored1.status, ThreadSubscriptionStatus::Subscribed { automatic: true });
1682+
assert_eq!(stored1.bump_stamp, Some(42));
1683+
1684+
let stored2 = client
1685+
.state_store()
1686+
.load_thread_subscription(&room2, &thread2)
1687+
.await
1688+
.expect("loading room2/thread2 works fine")
1689+
.expect("found room2/thread2 unsubscription");
1690+
1691+
assert_eq!(stored2.status, ThreadSubscriptionStatus::Unsubscribed);
1692+
assert_eq!(stored2.bump_stamp, Some(7));
1693+
}
1694+
1695+
#[async_test]
1696+
async fn test_sync_thread_subscriptions_with_catchup() {
1697+
let server = MatrixMockServer::new().await;
1698+
let client = server
1699+
.client_builder()
1700+
.on_builder(|builder| {
1701+
builder.with_threading_support(ThreadingSupport::Enabled { with_subscriptions: true })
1702+
})
1703+
.build()
1704+
.await;
1705+
1706+
let room_id1 = owned_room_id!("!room1:example.com");
1707+
let room_id2 = owned_room_id!("!room2:example.com");
1708+
1709+
let thread1 = owned_event_id!("$thread1:example.com");
1710+
let thread2 = owned_event_id!("$thread2:example.com");
1711+
let thread3 = owned_event_id!("$thread3:example.com");
1712+
1713+
// The provided catchup token will be used to fetch more thread
1714+
// subscriptions via the msc4308 companion endpoint.
1715+
server
1716+
.mock_get_thread_subscriptions()
1717+
.match_from("catchup_token")
1718+
.add_subscription(
1719+
room_id1.clone(),
1720+
thread3.clone(),
1721+
ThreadSubscription::new(false, uint!(1337)),
1722+
)
1723+
.with_delay(Duration::from_millis(300)) // Simulate some network delay.
1724+
// No more subscriptions after the first catchup request.
1725+
.ok(None)
1726+
.mock_once()
1727+
.mount()
1728+
.await;
1729+
1730+
// When I sliding-sync thread subscriptions, and the response includes this
1731+
// catch-up token,
1732+
let mut thread_subscriptions =
1733+
thread_subscription_response(&room_id1, &thread1, &room_id2, &thread2);
1734+
thread_subscriptions.prev_batch = Some("catchup_token".to_owned());
1735+
1736+
server
1737+
.mock_sliding_sync()
1738+
.ok_and_run(
1739+
&client,
1740+
|config_builder| {
1741+
config_builder
1742+
.with_thread_subscriptions_extension(
1743+
assign!(v5::request::ThreadSubscriptions::default(), {
1744+
enabled: Some(true),
1745+
limit: Some(uint!(10)),
1746+
}),
1747+
)
1748+
.add_list(SlidingSyncList::builder("rooms"))
1749+
},
1750+
assign!(v5::Response::new("pos".to_owned()), {
1751+
rooms: {
1752+
let mut rooms = BTreeMap::new();
1753+
rooms.insert(room_id1.clone(), v5::response::Room::default());
1754+
rooms.insert(room_id2.clone(), v5::response::Room::default());
1755+
rooms
1756+
},
1757+
extensions: assign!(v5::response::Extensions::default(), {
1758+
thread_subscriptions,
1759+
}),
1760+
}),
1761+
)
1762+
.await;
1763+
1764+
// If I try to get the subscription status for thread 1, it's still hitting
1765+
// network, because it doesn't know yet about the result of the catch-up
1766+
// request. (Ideally, the choice of whether some information is outdated or
1767+
// not would be per room/thread pair, but for simplicity it's global right
1768+
// now.)
1769+
server
1770+
.mock_room_get_thread_subscription()
1771+
.match_room_id(room_id1.clone())
1772+
.match_thread_id(thread1.clone())
1773+
.ok(true)
1774+
.mock_once()
1775+
.mount()
1776+
.await;
1777+
1778+
let room1 = client.get_room(&room_id1).unwrap();
1779+
let sub1 = room1.load_or_fetch_thread_subscription(&thread1).await.unwrap();
1780+
assert_eq!(sub1, Some(matrix_sdk::room::ThreadSubscription { automatic: true }));
1781+
1782+
// All the thread subscriptions are eventually known in the database.
1783+
sleep(Duration::from_millis(400)).await;
1784+
1785+
let stored3 = client
1786+
.state_store()
1787+
.load_thread_subscription(&room_id1, &thread3)
1788+
.await
1789+
.expect("loading room1/thread3 works fine")
1790+
.expect("found room1/thread3 subscription");
1791+
assert_eq!(stored3.status, ThreadSubscriptionStatus::Subscribed { automatic: false });
1792+
assert_eq!(stored3.bump_stamp, Some(1337));
1793+
1794+
// So the client will use the database only to load_or_fetch thread
1795+
// subscriptions. (Which is confirmed by the absence of mocking the
1796+
// room_get_thread_subscription endpoint for thread3.)
1797+
let sub3 = room1.load_or_fetch_thread_subscription(&thread3).await.unwrap();
1798+
assert_eq!(sub3, Some(matrix_sdk::room::ThreadSubscription { automatic: false }));
1799+
}

0 commit comments

Comments
 (0)