fix(timeline): validate beacon_info content for session matching
This commit is contained in:
@@ -57,6 +57,7 @@ use super::{ObservableItemsTransaction, rfind_event_by_item_id};
|
||||
use crate::timeline::{
|
||||
BeaconInfo, EventTimelineItem, LiveLocationState, MsgLikeContent, MsgLikeKind, PollState,
|
||||
ReactionInfo, ReactionStatus, TimelineEventItemId, TimelineItem, TimelineItemContent,
|
||||
event_item::beacon_info_matches,
|
||||
};
|
||||
|
||||
#[derive(Clone)]
|
||||
@@ -425,17 +426,10 @@ pub(crate) struct Aggregations {
|
||||
/// A pending beacon-stop aggregation received before the corresponding live
|
||||
/// `beacon_info` start item has arrived.
|
||||
///
|
||||
/// Keyed by the sender's user ID (the state key of both the start and stop
|
||||
/// `beacon_info` events). At most one stop can be pending per sender at any
|
||||
/// time: `beacon_info` is a state event whose state key is the sender's
|
||||
/// user ID, so there is only ever one live session per sender, and
|
||||
/// therefore only one meaningful pending stop. A later stop simply
|
||||
/// replaces an earlier one.
|
||||
///
|
||||
/// When the live start item is eventually inserted via `add_item`, the
|
||||
/// entry here is promoted into [`Self::related_events`] under the
|
||||
/// now-known start event ID so that [`Self::apply_all`] can apply the
|
||||
/// stop immediately.
|
||||
/// Keyed by the sender's user ID. When a live start item is eventually
|
||||
/// inserted via `add_item`, we check if the pending stop matches and
|
||||
/// promote it into [`Self::related_events`] so that [`Self::apply_all`]
|
||||
/// can apply it immediately.
|
||||
pending_beacon_stops: HashMap<OwnedUserId, Aggregation>,
|
||||
}
|
||||
|
||||
@@ -452,22 +446,42 @@ impl Aggregations {
|
||||
/// [`Self::related_events`] (and thus picked up by [`Self::apply_all`])
|
||||
/// when the live item is inserted via
|
||||
/// [`Self::promote_pending_beacon_stop`].
|
||||
///
|
||||
/// If a stop was already stashed for this sender, the new one replaces it:
|
||||
/// the later state event is always authoritative.
|
||||
pub fn add_pending_beacon_stop(&mut self, sender: OwnedUserId, aggregation: Aggregation) {
|
||||
self.pending_beacon_stops.insert(sender, aggregation);
|
||||
}
|
||||
|
||||
/// Promote any stashed beacon-stop aggregations for `sender` into the
|
||||
/// Promote a matching stashed beacon-stop aggregation for `sender` into the
|
||||
/// regular aggregation map, now that the live start item's
|
||||
/// `target_event_id` is known.
|
||||
///
|
||||
/// The pending stop's content must match the start event's content (except
|
||||
/// for the `live` field) for promotion to occur. If they don't match, the
|
||||
/// pending stop is discarded because it belongs to a different session.
|
||||
///
|
||||
/// Should be called from `add_item` just before `apply_all`, when inserting
|
||||
/// a live `beacon_info` item.
|
||||
fn promote_pending_beacon_stop(&mut self, sender: &OwnedUserId, target_event_id: OwnedEventId) {
|
||||
fn promote_pending_beacon_stop(
|
||||
&mut self,
|
||||
sender: &OwnedUserId,
|
||||
target_event_id: OwnedEventId,
|
||||
start_content: &BeaconInfoEventContent,
|
||||
) {
|
||||
if !start_content.live {
|
||||
return;
|
||||
}
|
||||
|
||||
let Some(stop) = self.pending_beacon_stops.remove(sender) else { return };
|
||||
|
||||
let AggregationKind::BeaconStop { content: stop_content } = &stop.kind else {
|
||||
warn!("pending beacon stop has unexpected aggregation kind");
|
||||
return;
|
||||
};
|
||||
|
||||
if !beacon_info_matches(start_content, stop_content) {
|
||||
trace!("discarding stale pending beacon stop (content mismatch)");
|
||||
return;
|
||||
}
|
||||
|
||||
let target = TimelineEventItemId::EventId(target_event_id);
|
||||
self.add(target, stop);
|
||||
}
|
||||
@@ -620,8 +634,14 @@ impl Aggregations {
|
||||
// in `pending_beacon_stops` keyed by sender. Promote it into
|
||||
// `related_events` under the now-known start event ID so the loop below
|
||||
// applies it together with any other pending aggregations.
|
||||
if let TimelineEventItemId::EventId(event_id) = item_id {
|
||||
self.promote_pending_beacon_stop(sender, event_id.clone());
|
||||
//
|
||||
// The promotion verifies that the pending stop's content matches the
|
||||
// start event's content to ensure we don't apply an old stop to a new
|
||||
// session.
|
||||
if let TimelineEventItemId::EventId(event_id) = item_id
|
||||
&& let Some(live_location) = event.content().as_live_location_state()
|
||||
{
|
||||
self.promote_pending_beacon_stop(sender, event_id.clone(), &live_location.beacon_info);
|
||||
}
|
||||
|
||||
let Some(aggregations) = self.related_events.get(item_id) else {
|
||||
|
||||
@@ -338,6 +338,8 @@ impl TimelineAction {
|
||||
// existing live item from the same sender rather than creating a
|
||||
// new timeline item.
|
||||
Self::HandleAggregation {
|
||||
// There is no explicit relates_to on a beacon_info state event;
|
||||
// the target is identified by sender in handle_beacon_stop.
|
||||
related_event: ev.event_id,
|
||||
kind: HandleAggregationKind::BeaconStop { content: ev.content },
|
||||
}
|
||||
@@ -772,27 +774,30 @@ impl<'a, 'o> TimelineEventHandler<'a, 'o> {
|
||||
/// Handle a stop `beacon_info` state event by finding the existing live
|
||||
/// `LiveLocation` timeline item from the same sender and updating it via
|
||||
/// the aggregation system.
|
||||
///
|
||||
/// The stop event's content must match the start item's content (except for
|
||||
/// the `live` field) to ensure we apply the stop to the correct session.
|
||||
#[instrument(skip(self, content))]
|
||||
fn handle_beacon_stop(&mut self, content: BeaconInfoEventContent) {
|
||||
let sender = &self.ctx.sender;
|
||||
|
||||
// Find the live start item by sender and matching content.
|
||||
let target_event_id = super::algorithms::rfind_event_item(self.items, |item| {
|
||||
item.sender() == sender
|
||||
&& item.content().as_live_location_state().is_some_and(|s| s.matches_stop(&content))
|
||||
})
|
||||
.and_then(|(_, event_item)| event_item.inner.event_id().map(ToOwned::to_owned));
|
||||
|
||||
let aggregation = Aggregation::new(
|
||||
self.ctx.flow.timeline_item_id(),
|
||||
AggregationKind::BeaconStop { content },
|
||||
);
|
||||
|
||||
// The stop beacon_info has no explicit `relates_to`; find the target
|
||||
// live item by matching sender and liveness, then extract its event ID
|
||||
// so we can address the aggregation correctly.
|
||||
let Some(target_event_id) = super::algorithms::rfind_event_item(self.items, |item| {
|
||||
item.sender() == sender
|
||||
&& item.content().as_live_location_state().is_some_and(|s| s.is_live())
|
||||
})
|
||||
.and_then(|(_, event_item)| event_item.inner.event_id().map(ToOwned::to_owned)) else {
|
||||
// The live start item hasn't arrived yet. Stash the stop so it can
|
||||
// be applied when the start item is eventually inserted.
|
||||
let Some(target_event_id) = target_event_id else {
|
||||
// The live start item hasn't arrived yet (or the content doesn't match).
|
||||
// Stash the stop so it can be applied when the matching start item arrives.
|
||||
trace!(
|
||||
"no live beacon_info item found for {sender}; \
|
||||
"no matching live beacon_info item found for {sender}; \
|
||||
stashing stop event to apply when the start item arrives"
|
||||
);
|
||||
self.meta.aggregations.add_pending_beacon_stop(sender.clone(), aggregation);
|
||||
|
||||
@@ -175,4 +175,21 @@ impl LiveLocationState {
|
||||
assert!(!beacon_info.is_live(), "A stop `beacon_info` event must not be live.");
|
||||
self.beacon_info = beacon_info;
|
||||
}
|
||||
|
||||
/// Check if a stop `beacon_info` matches this session.
|
||||
///
|
||||
/// Returns `true` if all fields except `live` match and this session is
|
||||
/// still live. This is used to verify that a stop event belongs to the
|
||||
/// same session as this start event.
|
||||
pub(in crate::timeline) fn matches_stop(&self, stop: &BeaconInfoEventContent) -> bool {
|
||||
self.beacon_info.live && beacon_info_matches(&self.beacon_info, stop)
|
||||
}
|
||||
}
|
||||
|
||||
/// Check if two `BeaconInfoEventContent` values belong to the same session.
|
||||
///
|
||||
/// Compares all fields except `live`, which differs between start and stop
|
||||
/// events. Returns `true` if all other fields match.
|
||||
pub fn beacon_info_matches(a: &BeaconInfoEventContent, b: &BeaconInfoEventContent) -> bool {
|
||||
a.ts == b.ts && a.timeout == b.timeout && a.description == b.description && a.asset == b.asset
|
||||
}
|
||||
|
||||
@@ -62,8 +62,11 @@ mod reply;
|
||||
|
||||
pub use pinned_events::RoomPinnedEventsChange;
|
||||
|
||||
pub(in crate::timeline) use self::message::{
|
||||
extract_bundled_edit_event_json, extract_poll_edit_content, extract_room_msg_edit_content,
|
||||
pub(in crate::timeline) use self::{
|
||||
live_location::beacon_info_matches,
|
||||
message::{
|
||||
extract_bundled_edit_event_json, extract_poll_edit_content, extract_room_msg_edit_content,
|
||||
},
|
||||
};
|
||||
pub use self::{
|
||||
live_location::{BeaconInfo, LiveLocationState},
|
||||
|
||||
@@ -49,7 +49,8 @@ pub use self::{
|
||||
};
|
||||
pub(super) use self::{
|
||||
content::{
|
||||
extract_bundled_edit_event_json, extract_poll_edit_content, extract_room_msg_edit_content,
|
||||
beacon_info_matches, extract_bundled_edit_event_json, extract_poll_edit_content,
|
||||
extract_room_msg_edit_content,
|
||||
},
|
||||
local::LocalEventTimelineItem,
|
||||
remote::{RemoteEventOrigin, RemoteEventTimelineItem},
|
||||
|
||||
@@ -21,12 +21,14 @@ use eyeball_im::VectorDiff;
|
||||
use matrix_sdk_test::{ALICE, BOB, async_test};
|
||||
use ruma::{
|
||||
EventId, MilliSecondsSinceUnixEpoch, OwnedEventId, event_id,
|
||||
events::beacon_info::RedactedBeaconInfoEventContent, owned_event_id, uint,
|
||||
events::beacon_info::{BeaconInfoEventContent, RedactedBeaconInfoEventContent},
|
||||
owned_event_id, uint,
|
||||
};
|
||||
use stream_assert::{assert_next_matches, assert_pending};
|
||||
|
||||
use crate::timeline::{
|
||||
EventTimelineItem, ReactionStatus, TimelineEventItemId, tests::TestTimeline,
|
||||
EventTimelineItem, ReactionStatus, TimelineEventItemId, event_item::beacon_info_matches,
|
||||
tests::TestTimeline,
|
||||
};
|
||||
|
||||
/// A `beacon_info` state event creates a `MsgLikeKind::LiveLocation`
|
||||
@@ -44,6 +46,7 @@ async fn test_beacon_info_creates_timeline_item() {
|
||||
Some("Alice's walk".to_owned()),
|
||||
Duration::from_secs(3600),
|
||||
true,
|
||||
None,
|
||||
)
|
||||
.await;
|
||||
|
||||
@@ -70,16 +73,7 @@ async fn test_beacon_info_with_expired_timeout_still_creates_item() {
|
||||
let past_ts = MilliSecondsSinceUnixEpoch(uint!(1_000)); // Very early timestamp
|
||||
let short_duration = Duration::from_millis(1); // Effectively expired immediately
|
||||
|
||||
timeline
|
||||
.send_beacon_info_with_ts(
|
||||
&ALICE,
|
||||
beacon_id,
|
||||
None,
|
||||
short_duration,
|
||||
true,
|
||||
Some(past_ts),
|
||||
)
|
||||
.await;
|
||||
timeline.send_beacon_info(&ALICE, beacon_id, None, short_duration, true, Some(past_ts)).await;
|
||||
|
||||
// The item should still be created even though the timeout has expired.
|
||||
let item = assert_next_matches!(stream, VectorDiff::PushBack { value } => value);
|
||||
@@ -101,7 +95,7 @@ async fn test_beacon_info_stopped_without_start_produces_no_item() {
|
||||
let mut stream = timeline.subscribe_events().await;
|
||||
let beacon_id = event_id!("$beacon_stop:example.org");
|
||||
|
||||
timeline.send_beacon_info(&ALICE, beacon_id, None, Duration::from_secs(60), false).await;
|
||||
timeline.send_beacon_info(&ALICE, beacon_id, None, Duration::from_secs(60), false, None).await;
|
||||
|
||||
assert_pending!(stream);
|
||||
|
||||
@@ -119,7 +113,7 @@ async fn test_beacon_update_aggregates_onto_beacon_info() {
|
||||
let mut stream = timeline.subscribe_events().await;
|
||||
let beacon_id = event_id!("$beacon_info:example.org");
|
||||
|
||||
timeline.send_beacon_info(&ALICE, beacon_id, None, Duration::from_secs(3600), true).await;
|
||||
timeline.send_beacon_info(&ALICE, beacon_id, None, Duration::from_secs(3600), true, None).await;
|
||||
|
||||
let item = assert_next_matches!(stream, VectorDiff::PushBack { value } => value);
|
||||
let state = item.content().as_live_location_state().unwrap();
|
||||
@@ -152,7 +146,7 @@ async fn test_multiple_beacon_updates_accumulate_in_order() {
|
||||
let mut stream = timeline.subscribe_events().await;
|
||||
let beacon_id = event_id!("$beacon_info:example.org");
|
||||
|
||||
timeline.send_beacon_info(&ALICE, beacon_id, None, Duration::from_secs(3600), true).await;
|
||||
timeline.send_beacon_info(&ALICE, beacon_id, None, Duration::from_secs(3600), true, None).await;
|
||||
let _item = assert_next_matches!(stream, VectorDiff::PushBack { value } => value);
|
||||
|
||||
// Send in non-chronological order.
|
||||
@@ -204,7 +198,9 @@ async fn test_beacon_update_before_beacon_info_is_applied_when_parent_arrives()
|
||||
);
|
||||
|
||||
// Now the beacon_info arrives.
|
||||
timeline.send_beacon_info(&ALICE, &beacon_id, None, Duration::from_secs(3600), true).await;
|
||||
timeline
|
||||
.send_beacon_info(&ALICE, &beacon_id, None, Duration::from_secs(3600), true, None)
|
||||
.await;
|
||||
|
||||
// A single PushBack with the stashed location already applied.
|
||||
let item = assert_next_matches!(stream, VectorDiff::PushBack { value } => value);
|
||||
@@ -232,6 +228,7 @@ async fn test_multiple_users_sharing_produce_independent_items() {
|
||||
Some("Alice".to_owned()),
|
||||
Duration::from_secs(3600),
|
||||
true,
|
||||
None,
|
||||
)
|
||||
.await;
|
||||
|
||||
@@ -246,6 +243,7 @@ async fn test_multiple_users_sharing_produce_independent_items() {
|
||||
Some("Bob".to_owned()),
|
||||
Duration::from_secs(3600),
|
||||
true,
|
||||
None,
|
||||
)
|
||||
.await;
|
||||
|
||||
@@ -318,7 +316,12 @@ async fn test_beacon_stop_updates_existing_item() {
|
||||
let start_id = event_id!("$beacon_start:example.org");
|
||||
let stop_id = event_id!("$beacon_stop:example.org");
|
||||
|
||||
timeline.send_beacon_info(&ALICE, start_id, None, Duration::from_secs(3600), true).await;
|
||||
// Both start and stop use the same session timestamp.
|
||||
let session_ts = MilliSecondsSinceUnixEpoch::now();
|
||||
|
||||
timeline
|
||||
.send_beacon_info(&ALICE, start_id, None, Duration::from_secs(3600), true, Some(session_ts))
|
||||
.await;
|
||||
|
||||
let item = assert_next_matches!(stream, VectorDiff::PushBack { value } => value);
|
||||
assert!(item.content().as_live_location_state().unwrap().is_live());
|
||||
@@ -326,7 +329,9 @@ async fn test_beacon_stop_updates_existing_item() {
|
||||
|
||||
assert_pending!(stream);
|
||||
|
||||
timeline.send_beacon_info(&ALICE, stop_id, None, Duration::from_secs(3600), false).await;
|
||||
timeline
|
||||
.send_beacon_info(&ALICE, stop_id, None, Duration::from_secs(3600), false, Some(session_ts))
|
||||
.await;
|
||||
|
||||
// The existing item is updated — a Set diff, not a PushBack.
|
||||
let item = assert_next_matches!(stream, VectorDiff::Set { index: 0, value } => value);
|
||||
@@ -349,23 +354,30 @@ async fn test_beacon_stop_preserves_locations() {
|
||||
let start_id = event_id!("$beacon_start:example.org");
|
||||
let stop_id = event_id!("$beacon_stop:example.org");
|
||||
|
||||
timeline.send_beacon_info(&ALICE, start_id, None, Duration::from_secs(3600), true).await;
|
||||
// Both start and stop use the same session timestamp.
|
||||
let session_ts = MilliSecondsSinceUnixEpoch::now();
|
||||
|
||||
timeline
|
||||
.send_beacon_info(&ALICE, start_id, None, Duration::from_secs(3600), true, Some(session_ts))
|
||||
.await;
|
||||
let _item = assert_next_matches!(stream, VectorDiff::PushBack { value } => value);
|
||||
|
||||
let ts = MilliSecondsSinceUnixEpoch(uint!(1_000));
|
||||
timeline.send_beacon_location(&ALICE, start_id, 51.5, 0.1, ts).await;
|
||||
let location_ts = MilliSecondsSinceUnixEpoch(uint!(1_000));
|
||||
timeline.send_beacon_location(&ALICE, start_id, 51.5, 0.1, location_ts).await;
|
||||
|
||||
let item = assert_next_matches!(stream, VectorDiff::Set { index: 0, value } => value);
|
||||
assert_eq!(item.content().as_live_location_state().unwrap().locations().len(), 1);
|
||||
|
||||
// Stop the session.
|
||||
timeline.send_beacon_info(&ALICE, stop_id, None, Duration::from_secs(3600), false).await;
|
||||
timeline
|
||||
.send_beacon_info(&ALICE, stop_id, None, Duration::from_secs(3600), false, Some(session_ts))
|
||||
.await;
|
||||
|
||||
let item = assert_next_matches!(stream, VectorDiff::Set { index: 0, value } => value);
|
||||
let state = item.content().as_live_location_state().unwrap();
|
||||
assert!(!state.is_live(), "should be stopped");
|
||||
assert_eq!(state.locations().len(), 1, "location updates should be preserved after stop");
|
||||
assert_eq!(state.latest_location().unwrap().ts(), ts);
|
||||
assert_eq!(state.latest_location().unwrap().ts(), location_ts);
|
||||
|
||||
assert_pending!(stream);
|
||||
}
|
||||
@@ -380,8 +392,13 @@ async fn test_beacon_stop_before_start_is_applied_later() {
|
||||
let start_id = event_id!("$beacon_start:example.org");
|
||||
let stop_id = event_id!("$beacon_stop:example.org");
|
||||
|
||||
// Both start and stop use the same session timestamp.
|
||||
let session_ts = MilliSecondsSinceUnixEpoch::now();
|
||||
|
||||
// Send the stop event first — the live start item doesn't exist yet.
|
||||
timeline.send_beacon_info(&ALICE, stop_id, None, Duration::from_secs(3600), false).await;
|
||||
timeline
|
||||
.send_beacon_info(&ALICE, stop_id, None, Duration::from_secs(3600), false, Some(session_ts))
|
||||
.await;
|
||||
|
||||
// No item should have been added to the timeline yet.
|
||||
assert_pending!(stream);
|
||||
@@ -391,7 +408,9 @@ async fn test_beacon_stop_before_start_is_applied_later() {
|
||||
);
|
||||
|
||||
// Now send the live start event.
|
||||
timeline.send_beacon_info(&ALICE, start_id, None, Duration::from_secs(3600), true).await;
|
||||
timeline
|
||||
.send_beacon_info(&ALICE, start_id, None, Duration::from_secs(3600), true, Some(session_ts))
|
||||
.await;
|
||||
|
||||
// The item should appear already stopped — a single PushBack, no follow-up Set.
|
||||
let item = assert_next_matches!(stream, VectorDiff::PushBack { value } => value);
|
||||
@@ -404,6 +423,66 @@ async fn test_beacon_stop_before_start_is_applied_later() {
|
||||
assert_pending!(stream);
|
||||
}
|
||||
|
||||
/// A pending beacon stop from an OLD session should NOT be applied to a NEW
|
||||
/// session. This tests the scenario where:
|
||||
/// 1. User starts session A
|
||||
/// 2. Stop for session A arrives out-of-order and is stashed
|
||||
/// 3. User starts session B — the stashed stop should NOT apply
|
||||
#[async_test]
|
||||
async fn test_pending_beacon_stop_not_applied_to_different_session() {
|
||||
let timeline = TestTimeline::new();
|
||||
let mut stream = timeline.subscribe_events().await;
|
||||
let old_stop_id = event_id!("$old_stop:example.org");
|
||||
let new_start_id = event_id!("$new_start:example.org");
|
||||
|
||||
// Use the current time as base for session B.
|
||||
// Session A's timestamp doesn't matter since its stop will be discarded.
|
||||
// Session B needs a recent timestamp so is_live() doesn't fail due to timeout.
|
||||
let old_session_ts = MilliSecondsSinceUnixEpoch(uint!(1)); // Old session (past)
|
||||
let new_session_ts = MilliSecondsSinceUnixEpoch::now(); // New session (now)
|
||||
|
||||
// A stop event from the OLD session arrives first (out-of-order).
|
||||
// Its corresponding start event is missing (maybe it's from long ago).
|
||||
timeline
|
||||
.send_beacon_info(
|
||||
&ALICE,
|
||||
old_stop_id,
|
||||
None,
|
||||
Duration::from_secs(3600),
|
||||
false,
|
||||
Some(old_session_ts),
|
||||
)
|
||||
.await;
|
||||
|
||||
// No item should appear — there's no start item for this stop.
|
||||
assert_pending!(stream);
|
||||
|
||||
// Now a NEW session starts with a DIFFERENT timestamp.
|
||||
timeline
|
||||
.send_beacon_info(
|
||||
&ALICE,
|
||||
new_start_id,
|
||||
Some("New session".to_owned()),
|
||||
Duration::from_secs(3600),
|
||||
true,
|
||||
Some(new_session_ts),
|
||||
)
|
||||
.await;
|
||||
|
||||
// The new session should appear and remain LIVE because the pending stop
|
||||
// belongs to a different session (different ts).
|
||||
let item = assert_next_matches!(stream, VectorDiff::PushBack { value } => value);
|
||||
let state = item.content().as_live_location_state().expect("should be a live location item");
|
||||
|
||||
assert!(
|
||||
state.is_live(),
|
||||
"the new session should be live; the old session's stop should NOT apply"
|
||||
);
|
||||
assert_eq!(state.description(), Some("New session"));
|
||||
|
||||
assert_pending!(stream);
|
||||
}
|
||||
|
||||
/// Duplicate beacon location updates (same timestamp) are de-duplicated.
|
||||
#[async_test]
|
||||
async fn test_duplicate_beacon_location_is_deduplicated() {
|
||||
@@ -411,7 +490,7 @@ async fn test_duplicate_beacon_location_is_deduplicated() {
|
||||
let mut stream = timeline.subscribe_events().await;
|
||||
let beacon_id = event_id!("$beacon_info:example.org");
|
||||
|
||||
timeline.send_beacon_info(&ALICE, beacon_id, None, Duration::from_secs(3600), true).await;
|
||||
timeline.send_beacon_info(&ALICE, beacon_id, None, Duration::from_secs(3600), true, None).await;
|
||||
let _item = assert_next_matches!(stream, VectorDiff::PushBack { value } => value);
|
||||
|
||||
let ts = MilliSecondsSinceUnixEpoch(uint!(1_000));
|
||||
@@ -466,7 +545,7 @@ async fn test_reaction_on_live_location_item() {
|
||||
let mut stream = timeline.subscribe_events().await;
|
||||
let beacon_id = event_id!("$beacon_info:example.org");
|
||||
|
||||
timeline.send_beacon_info(&ALICE, beacon_id, None, Duration::from_secs(3600), true).await;
|
||||
timeline.send_beacon_info(&ALICE, beacon_id, None, Duration::from_secs(3600), true, None).await;
|
||||
|
||||
let item = assert_next_matches!(stream, VectorDiff::PushBack { value } => value);
|
||||
assert!(item.content().as_live_location_state().is_some());
|
||||
@@ -495,7 +574,7 @@ async fn test_multiple_reactions_on_live_location_item() {
|
||||
let mut stream = timeline.subscribe_events().await;
|
||||
let beacon_id = event_id!("$beacon_info:example.org");
|
||||
|
||||
timeline.send_beacon_info(&ALICE, beacon_id, None, Duration::from_secs(3600), true).await;
|
||||
timeline.send_beacon_info(&ALICE, beacon_id, None, Duration::from_secs(3600), true, None).await;
|
||||
let _item = assert_next_matches!(stream, VectorDiff::PushBack { value } => value);
|
||||
|
||||
// ALICE and BOB both react, with different keys.
|
||||
@@ -531,7 +610,7 @@ async fn test_reaction_before_live_location_item_is_applied_when_parent_arrives(
|
||||
assert_pending!(stream);
|
||||
|
||||
// Now the beacon_info arrives.
|
||||
timeline.send_beacon_info(&ALICE, beacon_id, None, Duration::from_secs(3600), true).await;
|
||||
timeline.send_beacon_info(&ALICE, beacon_id, None, Duration::from_secs(3600), true, None).await;
|
||||
|
||||
// The item is inserted with the reaction already applied.
|
||||
let item = assert_next_matches!(stream, VectorDiff::PushBack { value } => value);
|
||||
@@ -552,7 +631,7 @@ async fn test_local_reaction_on_live_location_item() {
|
||||
let mut stream = timeline.subscribe_events().await;
|
||||
let beacon_id = event_id!("$beacon_info:example.org");
|
||||
|
||||
timeline.send_beacon_info(&ALICE, beacon_id, None, Duration::from_secs(3600), true).await;
|
||||
timeline.send_beacon_info(&ALICE, beacon_id, None, Duration::from_secs(3600), true, None).await;
|
||||
|
||||
let item = assert_next_matches!(stream, VectorDiff::PushBack { value } => value);
|
||||
let item_id = TimelineEventItemId::EventId(item.event_id().unwrap().to_owned());
|
||||
@@ -583,6 +662,95 @@ async fn test_local_reaction_on_live_location_item() {
|
||||
assert_pending!(stream);
|
||||
}
|
||||
|
||||
/// `beacon_info_matches` returns true for identical content.
|
||||
#[test]
|
||||
fn test_beacon_info_matches_identical_content() {
|
||||
let ts = MilliSecondsSinceUnixEpoch(uint!(1_000_000));
|
||||
let a = BeaconInfoEventContent::new(
|
||||
Some("Test".to_owned()),
|
||||
Duration::from_secs(3600),
|
||||
true,
|
||||
Some(ts),
|
||||
);
|
||||
let b = BeaconInfoEventContent::new(
|
||||
Some("Test".to_owned()),
|
||||
Duration::from_secs(3600),
|
||||
true,
|
||||
Some(ts),
|
||||
);
|
||||
|
||||
assert!(beacon_info_matches(&a, &b));
|
||||
}
|
||||
|
||||
/// `beacon_info_matches` ignores the `live` field (start vs stop).
|
||||
#[test]
|
||||
fn test_beacon_info_matches_ignores_live_field() {
|
||||
let ts = MilliSecondsSinceUnixEpoch(uint!(1_000_000));
|
||||
let start = BeaconInfoEventContent::new(
|
||||
Some("Test".to_owned()),
|
||||
Duration::from_secs(3600),
|
||||
true,
|
||||
Some(ts),
|
||||
);
|
||||
let stop = BeaconInfoEventContent::new(
|
||||
Some("Test".to_owned()),
|
||||
Duration::from_secs(3600),
|
||||
false,
|
||||
Some(ts),
|
||||
);
|
||||
|
||||
assert!(beacon_info_matches(&start, &stop), "should match even though live differs");
|
||||
}
|
||||
|
||||
/// `beacon_info_matches` returns false for different timestamps.
|
||||
#[test]
|
||||
fn test_beacon_info_matches_different_ts() {
|
||||
let a = BeaconInfoEventContent::new(
|
||||
None,
|
||||
Duration::from_secs(3600),
|
||||
true,
|
||||
Some(MilliSecondsSinceUnixEpoch(uint!(1_000))),
|
||||
);
|
||||
let b = BeaconInfoEventContent::new(
|
||||
None,
|
||||
Duration::from_secs(3600),
|
||||
true,
|
||||
Some(MilliSecondsSinceUnixEpoch(uint!(2_000))),
|
||||
);
|
||||
|
||||
assert!(!beacon_info_matches(&a, &b), "different ts should not match");
|
||||
}
|
||||
|
||||
/// `beacon_info_matches` returns false for different timeouts.
|
||||
#[test]
|
||||
fn test_beacon_info_matches_different_timeout() {
|
||||
let ts = MilliSecondsSinceUnixEpoch(uint!(1_000_000));
|
||||
let a = BeaconInfoEventContent::new(None, Duration::from_secs(3600), true, Some(ts));
|
||||
let b = BeaconInfoEventContent::new(None, Duration::from_secs(7200), true, Some(ts));
|
||||
|
||||
assert!(!beacon_info_matches(&a, &b), "different timeout should not match");
|
||||
}
|
||||
|
||||
/// `beacon_info_matches` returns false for different descriptions.
|
||||
#[test]
|
||||
fn test_beacon_info_matches_different_description() {
|
||||
let ts = MilliSecondsSinceUnixEpoch(uint!(1_000_000));
|
||||
let a = BeaconInfoEventContent::new(
|
||||
Some("Session A".to_owned()),
|
||||
Duration::from_secs(3600),
|
||||
true,
|
||||
Some(ts),
|
||||
);
|
||||
let b = BeaconInfoEventContent::new(
|
||||
Some("Session B".to_owned()),
|
||||
Duration::from_secs(3600),
|
||||
true,
|
||||
Some(ts),
|
||||
);
|
||||
|
||||
assert!(!beacon_info_matches(&a, &b), "different description should not match");
|
||||
}
|
||||
|
||||
impl TestTimeline {
|
||||
/// Collect every event timeline item (no virtual items).
|
||||
async fn live_location_event_items(&self) -> Vec<EventTimelineItem> {
|
||||
@@ -597,18 +765,6 @@ impl TestTimeline {
|
||||
description: Option<String>,
|
||||
duration: Duration,
|
||||
live: bool,
|
||||
) {
|
||||
self.send_beacon_info_with_ts(sender, event_id, description, duration, live, None).await;
|
||||
}
|
||||
|
||||
/// Convenience: send a `beacon_info` state event with a custom timestamp.
|
||||
async fn send_beacon_info_with_ts(
|
||||
&self,
|
||||
sender: &ruma::UserId,
|
||||
event_id: &EventId,
|
||||
description: Option<String>,
|
||||
duration: Duration,
|
||||
live: bool,
|
||||
ts: Option<MilliSecondsSinceUnixEpoch>,
|
||||
) {
|
||||
let event = self
|
||||
|
||||
Reference in New Issue
Block a user