Skip to main content

tuwunel_service/rooms/state_res/
test_utils.rs

1//! Event builders and in-memory stores for state-resolution tests and benchmarks.
2//!
3//! The fixtures exercise authorization and resolution without a running server.
4
5mod fetch;
6
7use std::{
8	borrow::Borrow,
9	collections::{BTreeSet, HashMap},
10	slice,
11	sync::atomic::{AtomicU64, Ordering::SeqCst},
12};
13
14use ruma::{
15	EventId, OwnedEventId, RoomId, UserId,
16	events::{
17		TimelineEventType,
18		room::{
19			join_rules::{JoinRule, RoomJoinRulesEventContent},
20			member::{MembershipState, RoomMemberEventContent},
21		},
22	},
23	room_id, uint, user_id,
24};
25#[cfg(test)]
26use ruma::{
27	MilliSecondsSinceUnixEpoch, event_id,
28	events::StateEventType,
29	int,
30	room_version_rules::{AuthorizationRules, RoomVersionRules},
31};
32use serde_json::{
33	json,
34	value::{RawValue as RawJsonValue, to_raw_value as to_raw_json_value},
35};
36use tuwunel_core::{
37	Error, Result, err,
38	matrix::{Event, EventHash, EventTypeExt, PduEvent},
39};
40#[cfg(test)]
41use tuwunel_core::{info, matrix::StateKey, utils::stream::IterStream};
42
43use super::{AuthSet, StateMap};
44#[cfg(test)]
45use super::{auth_types_for_event, events::RoomCreateEvent};
46#[cfg(test)]
47use crate::rooms::state_res::topological_sort::ReferencedIds;
48
49#[cfg(test)]
50type StateEvents = HashMap<StateEventType, HashMap<String, PduEvent>>;
51
52static SERVER_TIMESTAMP: AtomicU64 = AtomicU64::new(0);
53
54#[cfg(test)]
55pub(super) fn not_found() -> Error { err!(Request(NotFound("Test event not found"))) }
56
57pub(super) fn event_not_found(event_id: &EventId) -> Error {
58	err!(Request(NotFound("Test event not found: {event_id:?}")))
59}
60
61#[cfg(test)]
62pub(super) fn state_not_found(ty: &StateEventType, sk: &str) -> Error {
63	err!(Request(NotFound("Test state not found: ({ty:?},{sk:?})")))
64}
65
66#[cfg(test)]
67pub(super) async fn do_check(
68	events: &[PduEvent],
69	edges: Vec<Vec<OwnedEventId>>,
70	expected_state_ids: Vec<OwnedEventId>,
71) {
72	// To activate logging use `RUST_LOG=debug cargo t`
73
74	let init_events = INITIAL_EVENTS();
75	let mut store = build_test_store(&init_events, events);
76	let (graph, fake_event_map) = build_event_graph(&init_events, events, &edges);
77
78	let mut event_map: HashMap<OwnedEventId, PduEvent> = HashMap::new();
79	let mut state_at_event: HashMap<OwnedEventId, StateMap<OwnedEventId>> = HashMap::new();
80
81	let sorted = super::topological_sort(graph.clone(), &async |_id| {
82		Ok((int!(0).into(), MilliSecondsSinceUnixEpoch(uint!(0))))
83	})
84	.await
85	.unwrap();
86
87	for node in sorted {
88		let fake_event = &fake_event_map[&node];
89		let event_id = fake_event.event_id().to_owned();
90		let prev_events = &graph[&node];
91
92		let state_before =
93			resolve_state_before(&store, &event_map, &state_at_event, &node, prev_events).await;
94
95		let auth_events = collect_auth_events(fake_event, &state_before);
96		let state_after = state_after_with(&state_before, fake_event, &event_id);
97		let event = rebuild_with_auth(fake_event, &auth_events, prev_events);
98
99		store.0.insert(event_id.clone(), event);
100		event_map.insert(event_id.clone(), store.0[&event_id].clone());
101		state_at_event.insert(node, state_after);
102	}
103
104	let expected_state = build_expected_state(&expected_state_ids, &event_map);
105	let end_state = compute_end_state(&state_at_event, &expected_state);
106
107	assert_eq!(expected_state, end_state);
108}
109
110#[cfg(test)]
111fn build_test_store(
112	init_events: &HashMap<OwnedEventId, PduEvent>,
113	events: &[PduEvent],
114) -> TestStore {
115	TestStore(
116		init_events
117			.values()
118			.chain(events)
119			.map(|ev| (ev.event_id().to_owned(), ev.clone()))
120			.collect(),
121	)
122}
123
124#[cfg(test)]
125fn build_event_graph(
126	init_events: &HashMap<OwnedEventId, PduEvent>,
127	events: &[PduEvent],
128	edges: &[Vec<OwnedEventId>],
129) -> (HashMap<OwnedEventId, ReferencedIds>, HashMap<OwnedEventId, PduEvent>) {
130	let mut graph = HashMap::<OwnedEventId, ReferencedIds>::new();
131	let mut fake_event_map = HashMap::new();
132
133	for ev in init_events.values().chain(events) {
134		graph.insert(ev.event_id().to_owned(), Default::default());
135		fake_event_map.insert(ev.event_id().to_owned(), ev.clone());
136	}
137
138	add_chain_edges(&mut graph, &INITIAL_EDGES());
139	for edge_list in edges {
140		add_chain_edges(&mut graph, edge_list);
141	}
142
143	(graph, fake_event_map)
144}
145
146#[cfg(test)]
147fn add_chain_edges(graph: &mut HashMap<OwnedEventId, ReferencedIds>, chain: &[OwnedEventId]) {
148	for pair in chain.windows(2) {
149		if let [a, b] = pair {
150			graph
151				.entry(a.to_owned())
152				.or_default()
153				.push(b.clone());
154		}
155	}
156}
157
158#[cfg(test)]
159async fn resolve_state_before(
160	store: &TestStore,
161	event_map: &HashMap<OwnedEventId, PduEvent>,
162	state_at_event: &HashMap<OwnedEventId, StateMap<OwnedEventId>>,
163	node: &EventId,
164	prev_events: &[OwnedEventId],
165) -> StateMap<OwnedEventId> {
166	match prev_events {
167		| [] => StateMap::new(),
168		| [single] => state_at_event[single].clone(),
169		| _ => merge_prev_states(store, event_map, state_at_event, node, prev_events).await,
170	}
171}
172
173#[cfg(test)]
174async fn merge_prev_states(
175	store: &TestStore,
176	event_map: &HashMap<OwnedEventId, PduEvent>,
177	state_at_event: &HashMap<OwnedEventId, StateMap<OwnedEventId>>,
178	node: &EventId,
179	prev_events: &[OwnedEventId],
180) -> StateMap<OwnedEventId> {
181	let state_sets = prev_events
182		.iter()
183		.filter_map(|k| state_at_event.get(k).cloned())
184		.collect::<Vec<_>>();
185
186	info!(
187		"{:#?}",
188		state_sets
189			.iter()
190			.map(|map| map
191				.iter()
192				.map(|((ty, key), id)| format!("(({ty}{key:?}), {id})"))
193				.collect::<Vec<_>>())
194			.collect::<Vec<_>>()
195	);
196
197	let auth_chain_sets = state_sets
198		.iter()
199		.map(|map| {
200			store
201				.auth_event_ids(room_id(), map.values().cloned().collect())
202				.unwrap()
203		})
204		.collect::<Vec<_>>();
205
206	let rules = RoomVersionRules::V6;
207	super::resolve(
208		&rules,
209		state_sets.into_iter().stream(),
210		auth_chain_sets.into_iter().stream(),
211		event_map,
212		false,
213	)
214	.await
215	.unwrap_or_else(|e| panic!("resolution for {node} failed: {e}"))
216}
217
218#[cfg(test)]
219fn state_after_with(
220	state_before: &StateMap<OwnedEventId>,
221	fake_event: &PduEvent,
222	event_id: &EventId,
223) -> StateMap<OwnedEventId> {
224	let mut state_after = state_before.clone();
225	let ty = fake_event.event_type();
226	let key = fake_event.state_key().unwrap();
227	state_after.insert(ty.with_state_key(key), event_id.to_owned());
228	state_after
229}
230
231#[cfg(test)]
232fn collect_auth_events(
233	fake_event: &PduEvent,
234	state_before: &StateMap<OwnedEventId>,
235) -> Vec<OwnedEventId> {
236	auth_types_for_event(
237		fake_event.event_type(),
238		fake_event.sender(),
239		fake_event.state_key(),
240		fake_event.content(),
241		&AuthorizationRules::V6,
242		false,
243	)
244	.unwrap()
245	.into_iter()
246	.filter_map(|key| state_before.get(&key).cloned())
247	.collect()
248}
249
250#[cfg(test)]
251fn rebuild_with_auth(
252	fake_event: &PduEvent,
253	auth_events: &[OwnedEventId],
254	prev_events: &[OwnedEventId],
255) -> PduEvent {
256	to_pdu_event(
257		fake_event.event_id().as_str(),
258		fake_event.sender(),
259		fake_event.event_type().clone(),
260		fake_event.state_key(),
261		fake_event.content().to_owned(),
262		auth_events,
263		prev_events,
264	)
265}
266
267#[cfg(test)]
268fn build_expected_state(
269	expected_state_ids: &[OwnedEventId],
270	event_map: &HashMap<OwnedEventId, PduEvent>,
271) -> StateMap<OwnedEventId> {
272	expected_state_ids
273		.iter()
274		.map(|node| {
275			let ev = event_map.get(node).unwrap_or_else(|| {
276				panic!(
277					"{node} not found in {:?}",
278					event_map
279						.keys()
280						.map(ToString::to_string)
281						.collect::<Vec<_>>()
282				)
283			});
284			let key = ev
285				.event_type()
286				.with_state_key(ev.state_key().unwrap());
287			(key, node.clone())
288		})
289		.collect()
290}
291
292#[cfg(test)]
293fn compute_end_state(
294	state_at_event: &HashMap<OwnedEventId, StateMap<OwnedEventId>>,
295	expected_state: &StateMap<OwnedEventId>,
296) -> StateMap<OwnedEventId> {
297	let start_state = state_at_event
298		.get(event_id!("$START:foo"))
299		.unwrap();
300
301	state_at_event
302		.get(event_id!("$END:foo"))
303		.unwrap()
304		.iter()
305		.filter(|(k, v)| {
306			expected_state.contains_key(k)
307				|| start_state.get(k) != Some(*v)
308					&& **k != ("m.room.message".into(), "dummy".into())
309		})
310		.map(|(k, v)| (k.clone(), v.clone()))
311		.collect()
312}
313
314/// An in-memory fixture event store.
315///
316/// Fixture events can be fetched by identifier or traversed through their auth chains.
317pub struct TestStore(
318	/// Events indexed by their identifiers.
319	pub HashMap<OwnedEventId, PduEvent>,
320);
321
322impl TestStore {
323	pub(super) fn get_event(&self, _: &RoomId, event_id: &EventId) -> Result<PduEvent> {
324		self.0
325			.get(event_id)
326			.cloned()
327			.ok_or_else(|| event_not_found(event_id))
328	}
329
330	/// Collects the requested event ids and their recursive auth event ids.
331	///
332	/// Each identifier appears at most once. Traversal fails if a required
333	/// event is absent from the store.
334	pub fn auth_event_ids(
335		&self,
336		room_id: &RoomId,
337		event_ids: Vec<OwnedEventId>,
338	) -> Result<AuthSet<OwnedEventId>> {
339		let mut result = BTreeSet::new();
340		let mut stack = event_ids;
341
342		// DFS for auth event chain
343		while let Some(ev_id) = stack.pop() {
344			if result.contains(&ev_id) {
345				continue;
346			}
347
348			result.insert(ev_id.clone());
349
350			let event = self.get_event(room_id, ev_id.borrow())?;
351
352			stack.extend(event.auth_events().map(ToOwned::to_owned));
353		}
354
355		Ok(AuthSet::from_distinct(result.into_iter().collect()))
356	}
357}
358
359// A StateStore implementation for testing
360impl TestStore {
361	/// Populates the store with a room and two simultaneous membership forks.
362	///
363	/// Returns the two fork states and their expected merged state.
364	pub fn set_up(
365		&mut self,
366	) -> (StateMap<OwnedEventId>, StateMap<OwnedEventId>, StateMap<OwnedEventId>) {
367		let create_event = to_pdu_event::<&EventId>(
368			"CREATE",
369			alice(),
370			TimelineEventType::RoomCreate,
371			Some(""),
372			to_raw_json_value(&json!({ "creator": alice() }))
373				.expect("fixture content serializes to JSON"),
374			&[],
375			&[],
376		);
377
378		let cre = create_event.event_id().to_owned();
379		self.0.insert(cre.clone(), create_event.clone());
380
381		let alice_mem = to_pdu_event(
382			"IMA",
383			alice(),
384			TimelineEventType::RoomMember,
385			Some(alice().as_str()),
386			member_content_join(),
387			slice::from_ref(&cre),
388			slice::from_ref(&cre),
389		);
390		self.0
391			.insert(alice_mem.event_id().to_owned(), alice_mem.clone());
392
393		let join_rules = to_pdu_event(
394			"IJR",
395			alice(),
396			TimelineEventType::RoomJoinRules,
397			Some(""),
398			to_raw_json_value(&RoomJoinRulesEventContent::new(JoinRule::Public))
399				.expect("fixture content serializes to JSON"),
400			&[cre.clone(), alice_mem.event_id().to_owned()],
401			&[alice_mem.event_id().to_owned()],
402		);
403
404		self.0
405			.insert(join_rules.event_id().to_owned(), join_rules.clone());
406
407		// Bob and Charlie join at the same time, so there is a fork
408		// this will be represented in the state_sets when we resolve
409		let bob_mem = to_pdu_event(
410			"IMB",
411			bob(),
412			TimelineEventType::RoomMember,
413			Some(bob().as_str()),
414			member_content_join(),
415			&[cre.clone(), join_rules.event_id().to_owned()],
416			&[join_rules.event_id().to_owned()],
417		);
418		self.0
419			.insert(bob_mem.event_id().to_owned(), bob_mem.clone());
420
421		let charlie_mem = to_pdu_event(
422			"IMC",
423			charlie(),
424			TimelineEventType::RoomMember,
425			Some(charlie().as_str()),
426			member_content_join(),
427			&[cre, join_rules.event_id().to_owned()],
428			&[join_rules.event_id().to_owned()],
429		);
430		self.0
431			.insert(charlie_mem.event_id().to_owned(), charlie_mem.clone());
432
433		let state_at_bob = [&create_event, &alice_mem, &join_rules, &bob_mem]
434			.iter()
435			.map(|e| {
436				(
437					e.event_type().with_state_key(
438						e.state_key()
439							.expect("fixture event has a state key"),
440					),
441					e.event_id().to_owned(),
442				)
443			})
444			.collect::<StateMap<_>>();
445
446		let state_at_charlie = [&create_event, &alice_mem, &join_rules, &charlie_mem]
447			.iter()
448			.map(|e| {
449				(
450					e.event_type().with_state_key(
451						e.state_key()
452							.expect("fixture event has a state key"),
453					),
454					e.event_id().to_owned(),
455				)
456			})
457			.collect::<StateMap<_>>();
458
459		let expected = [&create_event, &alice_mem, &join_rules, &bob_mem, &charlie_mem]
460			.iter()
461			.map(|e| {
462				(
463					e.event_type().with_state_key(
464						e.state_key()
465							.expect("fixture event has a state key"),
466					),
467					e.event_id().to_owned(),
468				)
469			})
470			.collect::<StateMap<_>>();
471
472		(state_at_bob, state_at_charlie, expected)
473	}
474}
475
476/// Constructs a synthetic event identifier, preserving a supplied full identifier.
477///
478/// Bare names receive the fixture server suffix.
479#[must_use]
480pub fn event_id(id: &str) -> OwnedEventId {
481	if id.contains('$') {
482		return id
483			.try_into()
484			.expect("fixture event identifier is valid");
485	}
486
487	format!("${id}:foo")
488		.try_into()
489		.expect("fixture event identifier is valid")
490}
491
492/// Returns the Alice fixture user.
493///
494/// The identifier belongs to the fixture server.
495#[must_use]
496pub fn alice() -> &'static UserId { user_id!("@alice:foo") }
497
498#[cfg(test)]
499pub(super) fn aya() -> &'static UserId { user_id!("@aya:other.server") }
500
501/// Returns the Bob fixture user.
502///
503/// The identifier belongs to the fixture server.
504#[must_use]
505pub fn bob() -> &'static UserId { user_id!("@bob:foo") }
506
507/// Returns the Charlie fixture user.
508///
509/// The identifier belongs to the fixture server.
510#[must_use]
511pub fn charlie() -> &'static UserId { user_id!("@charlie:foo") }
512
513/// Returns the Ella fixture user.
514///
515/// The identifier belongs to the fixture server.
516#[must_use]
517pub fn ella() -> &'static UserId { user_id!("@ella:foo") }
518
519#[cfg(test)]
520pub(super) fn zara() -> &'static UserId { user_id!("@zara:foo") }
521
522/// Returns the fixture room identifier.
523///
524/// The room belongs to the fixture server.
525#[must_use]
526pub fn room_id() -> &'static RoomId { room_id!("!test:foo") }
527
528#[cfg(test)]
529pub(crate) fn hydra_room_id() -> &'static RoomId { room_id!("!CREATE") }
530
531/// Builds membership content for a banned user.
532///
533/// Other membership fields use their default values.
534#[must_use]
535pub fn member_content_ban() -> Box<RawJsonValue> {
536	to_raw_json_value(&RoomMemberEventContent::new(MembershipState::Ban))
537		.expect("fixture content serializes to JSON")
538}
539
540/// Builds membership content for a joined user.
541///
542/// Other membership fields use their default values.
543#[must_use]
544pub fn member_content_join() -> Box<RawJsonValue> {
545	to_raw_json_value(&RoomMemberEventContent::new(MembershipState::Join))
546		.expect("fixture content serializes to JSON")
547}
548
549#[cfg(test)]
550pub(super) fn to_init_pdu_event(
551	id: &str,
552	sender: &UserId,
553	ev_type: TimelineEventType,
554	state_key: Option<&str>,
555	content: Box<RawJsonValue>,
556) -> PduEvent {
557	let ts = SERVER_TIMESTAMP.fetch_add(1, SeqCst);
558	let state_key = state_key.map(ToOwned::to_owned);
559	let id = if id.contains('$') {
560		id.to_owned()
561	} else {
562		format!("${id}:foo")
563	};
564
565	PduEvent {
566		event_id: id.try_into().unwrap(),
567		room_id: room_id().to_owned(),
568		sender: sender.to_owned(),
569		origin: None,
570		origin_server_ts: ts.try_into().unwrap(),
571		state_key: state_key.map(Into::into),
572		kind: ev_type,
573		content: content.into(),
574		redacts: None,
575		unsigned: None,
576		auth_events: Default::default(),
577		prev_events: Default::default(),
578		depth: uint!(0),
579		hashes: EventHash::default(),
580		//rejected: false,
581	}
582}
583
584/// Builds a fixture PDU with synthetic identifiers and an increasing timestamp.
585///
586/// Auth and previous event references accept bare names or full identifiers.
587pub fn to_pdu_event<S>(
588	id: &str,
589	sender: &UserId,
590	ev_type: TimelineEventType,
591	state_key: Option<&str>,
592	content: Box<RawJsonValue>,
593	auth_events: &[S],
594	prev_events: &[S],
595) -> PduEvent
596where
597	S: AsRef<str>,
598{
599	let ts = SERVER_TIMESTAMP.fetch_add(1, SeqCst);
600	let state_key = state_key.map(ToOwned::to_owned);
601	let id = if id.contains('$') {
602		id.to_owned()
603	} else {
604		format!("${id}:foo")
605	};
606	let auth_events = auth_events
607		.iter()
608		.map(AsRef::as_ref)
609		.map(event_id)
610		.collect();
611	let prev_events = prev_events
612		.iter()
613		.map(AsRef::as_ref)
614		.map(event_id)
615		.collect();
616
617	PduEvent {
618		event_id: id
619			.try_into()
620			.expect("fixture event identifier is valid"),
621		room_id: room_id().to_owned(),
622		sender: sender.to_owned(),
623		origin: None,
624		origin_server_ts: ts
625			.try_into()
626			.expect("fixture timestamp fits the protocol integer"),
627		state_key: state_key.map(Into::into),
628		kind: ev_type,
629		content: content.into(),
630		redacts: None,
631		unsigned: None,
632		auth_events,
633		prev_events,
634		depth: uint!(0),
635		hashes: EventHash::default(),
636		//rejected: false,
637	}
638}
639
640/// Same as `to_pdu_event()`, but uses the default m.room.create event ID to
641/// generate the room ID.
642#[cfg(test)]
643pub(super) fn to_hydra_pdu_event<S>(
644	id: &str,
645	sender: &UserId,
646	ev_type: TimelineEventType,
647	state_key: Option<&str>,
648	content: Box<RawJsonValue>,
649	auth_events: &[S],
650	prev_events: &[S],
651) -> PduEvent
652where
653	S: AsRef<str>,
654{
655	fn event_id(id: &str) -> OwnedEventId {
656		if id.contains('$') {
657			id.try_into().unwrap()
658		} else {
659			format!("${id}").try_into().unwrap()
660		}
661	}
662
663	let ts = SERVER_TIMESTAMP.fetch_add(1, SeqCst);
664	let state_key = state_key.map(ToOwned::to_owned);
665	let auth_events = auth_events
666		.iter()
667		.map(AsRef::as_ref)
668		.map(event_id)
669		.collect();
670	let prev_events = prev_events
671		.iter()
672		.map(AsRef::as_ref)
673		.map(event_id)
674		.collect();
675
676	PduEvent {
677		event_id: event_id(id),
678		room_id: hydra_room_id().to_owned(),
679		sender: sender.to_owned(),
680		origin: None,
681		origin_server_ts: ts.try_into().unwrap(),
682		state_key: state_key.map(Into::into),
683		kind: ev_type,
684		content: content.into(),
685		redacts: None,
686		unsigned: None,
687		auth_events,
688		prev_events,
689		depth: uint!(0),
690		hashes: EventHash::default(),
691		//rejected: false,
692	}
693}
694
695#[cfg(test)]
696pub(super) fn room_redaction_pdu_event<S>(
697	id: &str,
698	sender: &UserId,
699	redacts: OwnedEventId,
700	content: Box<RawJsonValue>,
701	auth_events: &[S],
702	prev_events: &[S],
703) -> PduEvent
704where
705	S: AsRef<str>,
706{
707	let ts = SERVER_TIMESTAMP.fetch_add(1, SeqCst);
708	let id = if id.contains('$') {
709		id.to_owned()
710	} else {
711		format!("${id}:foo")
712	};
713	let auth_events = auth_events
714		.iter()
715		.map(AsRef::as_ref)
716		.map(event_id)
717		.collect();
718	let prev_events = prev_events
719		.iter()
720		.map(AsRef::as_ref)
721		.map(event_id)
722		.collect();
723
724	PduEvent {
725		event_id: id.try_into().unwrap(),
726		room_id: room_id().to_owned(),
727		sender: sender.to_owned(),
728		origin: None,
729		origin_server_ts: ts.try_into().unwrap(),
730		state_key: None,
731		kind: TimelineEventType::RoomRedaction,
732		content: content.into(),
733		redacts: Some(redacts),
734		unsigned: None,
735		auth_events,
736		prev_events,
737		depth: uint!(0),
738		hashes: EventHash::default(),
739		//rejected: false,
740	}
741}
742
743#[cfg(test)]
744pub(super) fn room_create_hydra_pdu_event(
745	id: &str,
746	sender: &UserId,
747	content: Box<RawJsonValue>,
748) -> PduEvent {
749	let ts = SERVER_TIMESTAMP.fetch_add(1, SeqCst);
750	let eid = if id.contains('$') {
751		id.to_owned()
752	} else {
753		format!("${id}")
754	};
755	let rid = if id.contains('!') {
756		id.to_owned()
757	} else {
758		format!("!{id}")
759	};
760
761	PduEvent {
762		event_id: eid.try_into().unwrap(),
763		room_id: rid.try_into().unwrap(),
764		sender: sender.to_owned(),
765		origin: None,
766		origin_server_ts: ts.try_into().unwrap(),
767		state_key: Some(StateKey::new()),
768		kind: TimelineEventType::RoomCreate,
769		content: content.into(),
770		redacts: None,
771		unsigned: None,
772		auth_events: Default::default(),
773		prev_events: Default::default(),
774		depth: uint!(0),
775		hashes: EventHash::default(),
776		//rejected: false,
777	}
778}
779
780// all graphs start with these input events
781#[expect(non_snake_case)]
782/// Builds the common room state and membership fixture events.
783///
784/// Events are indexed by identifier for use in resolution fixtures.
785#[must_use]
786pub fn INITIAL_EVENTS() -> HashMap<OwnedEventId, PduEvent> {
787	vec![
788		to_pdu_event::<&EventId>(
789			"CREATE",
790			alice(),
791			TimelineEventType::RoomCreate,
792			Some(""),
793			to_raw_json_value(&json!({ "creator": alice() }))
794				.expect("fixture content serializes to JSON"),
795			&[],
796			&[],
797		),
798		to_pdu_event(
799			"IMA",
800			alice(),
801			TimelineEventType::RoomMember,
802			Some(alice().as_str()),
803			member_content_join(),
804			&["CREATE"],
805			&["CREATE"],
806		),
807		to_pdu_event(
808			"IPOWER",
809			alice(),
810			TimelineEventType::RoomPowerLevels,
811			Some(""),
812			to_raw_json_value(&json!({ "users": { alice(): 100 } }))
813				.expect("fixture content serializes to JSON"),
814			&["CREATE", "IMA"],
815			&["IMA"],
816		),
817		to_pdu_event(
818			"IJR",
819			alice(),
820			TimelineEventType::RoomJoinRules,
821			Some(""),
822			to_raw_json_value(&RoomJoinRulesEventContent::new(JoinRule::Public))
823				.expect("fixture content serializes to JSON"),
824			&["CREATE", "IMA", "IPOWER"],
825			&["IPOWER"],
826		),
827		to_pdu_event(
828			"IMB",
829			bob(),
830			TimelineEventType::RoomMember,
831			Some(bob().as_str()),
832			member_content_join(),
833			&["CREATE", "IJR", "IPOWER"],
834			&["IJR"],
835		),
836		to_pdu_event(
837			"IMC",
838			charlie(),
839			TimelineEventType::RoomMember,
840			Some(charlie().as_str()),
841			member_content_join(),
842			&["CREATE", "IJR", "IPOWER"],
843			&["IMB"],
844		),
845		to_pdu_event::<&EventId>(
846			"START",
847			charlie(),
848			TimelineEventType::RoomMessage,
849			Some("dummy"),
850			to_raw_json_value(&json!({})).expect("fixture content serializes to JSON"),
851			&[],
852			&[],
853		),
854		to_pdu_event::<&EventId>(
855			"END",
856			charlie(),
857			TimelineEventType::RoomMessage,
858			Some("dummy"),
859			to_raw_json_value(&json!({})).expect("fixture content serializes to JSON"),
860			&[],
861			&[],
862		),
863	]
864	.into_iter()
865	.map(|ev| (ev.event_id().to_owned(), ev))
866	.collect()
867}
868
869/// Batch of initial events to use for incoming events from room version
870/// `org.matrix.hydra.11` onwards.
871#[expect(non_snake_case)]
872#[cfg(test)]
873pub(super) fn INITIAL_HYDRA_EVENTS() -> HashMap<OwnedEventId, PduEvent> {
874	vec![
875		room_create_hydra_pdu_event(
876			"CREATE",
877			alice(),
878			to_raw_json_value(&json!({ "room_version": "org.matrix.hydra.11" })).unwrap(),
879		),
880		to_hydra_pdu_event(
881			"IMA",
882			alice(),
883			TimelineEventType::RoomMember,
884			Some(alice().as_str()),
885			member_content_join(),
886			&["CREATE"],
887			&["CREATE"],
888		),
889		to_hydra_pdu_event(
890			"IPOWER",
891			alice(),
892			TimelineEventType::RoomPowerLevels,
893			Some(""),
894			to_raw_json_value(&json!({})).unwrap(),
895			&["CREATE", "IMA"],
896			&["IMA"],
897		),
898		to_hydra_pdu_event(
899			"IJR",
900			alice(),
901			TimelineEventType::RoomJoinRules,
902			Some(""),
903			to_raw_json_value(&RoomJoinRulesEventContent::new(JoinRule::Public)).unwrap(),
904			&["CREATE", "IMA", "IPOWER"],
905			&["IPOWER"],
906		),
907		to_hydra_pdu_event(
908			"IMB",
909			bob(),
910			TimelineEventType::RoomMember,
911			Some(bob().as_str()),
912			member_content_join(),
913			&["CREATE", "IJR", "IPOWER"],
914			&["IJR"],
915		),
916		to_hydra_pdu_event(
917			"IMC",
918			charlie(),
919			TimelineEventType::RoomMember,
920			Some(charlie().as_str()),
921			member_content_join(),
922			&["CREATE", "IJR", "IPOWER"],
923			&["IMB"],
924		),
925		to_hydra_pdu_event::<&EventId>(
926			"START",
927			charlie(),
928			TimelineEventType::RoomMessage,
929			Some("dummy"),
930			to_raw_json_value(&json!({})).unwrap(),
931			&[],
932			&[],
933		),
934		to_hydra_pdu_event::<&EventId>(
935			"END",
936			charlie(),
937			TimelineEventType::RoomMessage,
938			Some("dummy"),
939			to_raw_json_value(&json!({})).unwrap(),
940			&[],
941			&[],
942		),
943	]
944	.into_iter()
945	.map(|ev| (ev.event_id().to_owned(), ev))
946	.collect()
947}
948
949// all graphs start with these input events
950#[expect(non_snake_case)]
951#[cfg(test)]
952pub(super) fn INITIAL_EVENTS_CREATE_ROOM() -> HashMap<OwnedEventId, PduEvent> {
953	vec![to_pdu_event::<&EventId>(
954		"CREATE",
955		alice(),
956		TimelineEventType::RoomCreate,
957		Some(""),
958		to_raw_json_value(&json!({ "creator": alice() })).unwrap(),
959		&[],
960		&[],
961	)]
962	.into_iter()
963	.map(|ev| (ev.event_id().to_owned(), ev))
964	.collect()
965}
966
967#[expect(non_snake_case)]
968#[cfg(test)]
969pub(super) fn INITIAL_EVENTS_NO_FEDERATE() -> HashMap<OwnedEventId, PduEvent> {
970	let create = to_init_pdu_event(
971		"CREATE",
972		alice(),
973		TimelineEventType::RoomCreate,
974		Some(""),
975		to_raw_json_value(&json!({ "creator": alice(), "m.federate": false })).unwrap(),
976	);
977
978	let mut events = INITIAL_EVENTS();
979	events.insert(event_id("CREATE"), create);
980	events
981}
982
983#[expect(non_snake_case)]
984#[cfg(test)]
985pub(super) fn INITIAL_EDGES() -> Vec<OwnedEventId> {
986	vec!["START", "IMC", "IMB", "IJR", "IPOWER", "IMA", "CREATE"]
987		.into_iter()
988		.map(event_id)
989		.collect::<Vec<_>>()
990}
991
992#[cfg(test)]
993pub(super) fn init_subscriber() -> tracing::dispatcher::DefaultGuard {
994	tracing::subscriber::set_default(
995		tracing_subscriber::fmt()
996			.with_test_writer()
997			.finish(),
998	)
999}
1000
1001/// Wrapper around a state map.
1002#[cfg(test)]
1003pub(super) struct TestStateMap(StateEvents);
1004
1005#[cfg(test)]
1006impl TestStateMap {
1007	pub(super) fn new(events: &HashMap<OwnedEventId, PduEvent>) -> Self {
1008		let state_map = events
1009			.values()
1010			.fold(StateEvents::new(), |mut state, event| {
1011				let event_type = StateEventType::from(event.event_type().to_string());
1012
1013				state
1014					.entry(event_type)
1015					.or_default()
1016					.insert(event.state_key().unwrap().to_owned(), event.clone());
1017
1018				state
1019			});
1020
1021		Self(state_map)
1022	}
1023
1024	pub(super) fn get(&self, event_type: &StateEventType, state_key: &str) -> Result<&PduEvent> {
1025		self.0
1026			.get(event_type)
1027			.ok_or_else(|| state_not_found(event_type, state_key))?
1028			.get(state_key)
1029			.ok_or_else(|| state_not_found(event_type, state_key))
1030	}
1031
1032	/// The `m.room.create` event contained in this map.
1033	///
1034	/// Panics if there is no `m.room.create` event in this map.
1035	pub(super) fn room_create_event(&self) -> RoomCreateEvent<&PduEvent> {
1036		RoomCreateEvent::new(self.get(&StateEventType::RoomCreate, "").unwrap())
1037	}
1038}
1039
1040/// Create an `m.room.third_party_invite` event with the given sender.
1041#[cfg(test)]
1042pub(super) fn room_third_party_invite(sender: &UserId) -> PduEvent {
1043	let content = json!({
1044		"display_name": "o...@g...",
1045		"key_validity_url": "https://identity.local/_matrix/identity/v2/pubkey/isvalid",
1046		"public_key": "Gb9ECWmEzf6FQbrBZ9w7lshQhqowtrbLDFw4rXAxZuE",
1047		"public_keys": [
1048			{
1049				"key_validity_url": "https://identity.local/_matrix/identity/v2/pubkey/isvalid",
1050				"public_key": "Gb9ECWmEzf6FQbrBZ9w7lshQhqowtrbLDFw4rXAxZuE"
1051			},
1052			{
1053				"key_validity_url": "https://identity.local/_matrix/identity/v2/pubkey/ephemeral/isvalid",
1054				"public_key": "Kxdvv7lo0O6JVI7yimFgmYPfpLGnctcpYjuypP5zx/c"
1055			}
1056		]
1057	});
1058
1059	to_pdu_event(
1060		"THIRDPARTY",
1061		sender,
1062		TimelineEventType::RoomThirdPartyInvite,
1063		Some("somerandomtoken"),
1064		to_raw_json_value(&content).unwrap(),
1065		&["CREATE", "IJR", "IPOWER"],
1066		&["IPOWER"],
1067	)
1068}