1use std::{
2 collections::BTreeMap,
3 iter::once,
4 net::IpAddr,
5 sync::atomic::{AtomicBool, Ordering},
6 time::{Duration, Instant},
7};
8
9use axum::extract::State;
10use futures::{FutureExt, Stream, StreamExt, TryFutureExt, TryStreamExt};
11use ruma::{
12 CanonicalJsonObject, CanonicalJsonValue, MilliSecondsSinceUnixEpoch, OwnedDeviceId,
13 OwnedEventId, OwnedRoomId, OwnedUserId, RoomId, ServerName, TransactionId, UserId,
14 api::{
15 error::ErrorKind,
16 federation::transactions::{
17 edu::{
18 DeviceListUpdateContent, DirectDeviceContent, Edu, PresenceContent,
19 PresenceUpdate, ReceiptContent, ReceiptData, ReceiptMap, SigningKeyUpdateContent,
20 TypingContent,
21 },
22 send_transaction_message,
23 },
24 },
25 events::receipt::{ReceiptEvent, ReceiptEventContent, ReceiptType},
26 int,
27 serde::Raw,
28 to_device::DeviceIdOrAllDevices,
29 uint,
30};
31use tuwunel_core::{
32 Err, Error, Result, debug,
33 debug::INFO_SPAN_LEVEL,
34 debug_warn, defer, err, error,
35 itertools::Itertools,
36 result::LogErr,
37 smallvec::SmallVec,
38 trace,
39 utils::{
40 debug::str_truncated,
41 future::TryExtExt,
42 millis_since_unix_epoch,
43 stream::{BroadbandExt, IterStream, ReadyExt, TryBroadbandExt, automatic_width},
44 },
45 warn,
46};
47use tuwunel_matrix::room::state::resolution::{
48 is_topologically_sorted_in_place, topological_sort,
49};
50use tuwunel_service::{
51 Services,
52 sending::{EDU_LIMIT, PDU_LIMIT},
53 users::DeviceListChange,
54};
55
56use crate::{ClientIp, Ruma};
57
58type ResolvedMap = BTreeMap<OwnedEventId, Result>;
59type RoomsPdus = SmallVec<[RoomPdus; 1]>;
60type RoomPdus = (OwnedRoomId, TxnPdus);
61type TxnPdus = SmallVec<[(usize, Pdu); 1]>;
62type Pdu = (OwnedRoomId, OwnedEventId, CanonicalJsonObject);
63
64type Deliveries = SmallVec<[(OwnedDeviceId, u64); 1]>;
67
68#[tracing::instrument(
72 name = "txn",
73 level = INFO_SPAN_LEVEL,
74 skip_all,
75 fields(
76 txn = str_truncated(body.transaction_id.as_str(), 20),
77 origin = body.origin().as_str(),
78 %client,
79 ),
80)]
81pub(crate) async fn send_transaction_message_route(
82 State(services): State<crate::State>,
83 ClientIp(client): ClientIp,
84 body: Ruma<send_transaction_message::v1::Request>,
85) -> Result<send_transaction_message::v1::Response> {
86 if body.origin() != body.body.origin {
87 return Err!(Request(Forbidden(
88 "Not allowed to send transactions on behalf of other servers"
89 )));
90 }
91
92 if body.pdus.len() > PDU_LIMIT {
93 return Err!(Request(Forbidden(
94 "Not allowed to send more than {PDU_LIMIT} PDUs in one transaction"
95 )));
96 }
97
98 if body.edus.len() > EDU_LIMIT {
99 return Err!(Request(Forbidden(
100 "Not allowed to send more than {EDU_LIMIT} EDUs in one transaction"
101 )));
102 }
103
104 services
106 .sending
107 .notify_peer_alive(body.origin())
108 .await;
109
110 let txn_start_time = Instant::now();
111 trace!(
112 pdus = body.pdus.len(),
113 edus = body.edus.len(),
114 elapsed = ?txn_start_time.elapsed(),
115 "Starting txn",
116 );
117
118 let pdus = body
119 .pdus
120 .iter()
121 .stream()
122 .enumerate()
123 .broad_filter_map(|(i, pdu)| {
124 services
125 .event_handler
126 .parse_incoming_pdu(pdu)
127 .inspect_err(move |e| debug_warn!("Could not parse PDU[{i}]: {e}"))
128 .map_ok(move |pdu| (i, pdu))
129 .ok()
130 });
131
132 let edus = body
133 .edus
134 .iter()
135 .stream()
136 .enumerate()
137 .ready_filter_map(|(i, edu)| {
138 serde_json::from_str(edu.json().get())
139 .inspect_err(|e| debug_warn!("Could not parse EDU[{i}]: {e}"))
140 .map(|edu| (i, edu))
141 .ok()
142 });
143
144 let results = handle(
145 &services,
146 &client,
147 body.origin(),
148 &body.transaction_id,
149 txn_start_time,
150 pdus,
151 edus,
152 )
153 .await?;
154
155 debug!(
156 pdus = body.pdus.len(),
157 edus = body.edus.len(),
158 elapsed = ?txn_start_time.elapsed(),
159 "Finished txn",
160 );
161
162 for (id, result) in &results {
163 if let Err(e) = result
164 && matches!(
165 e,
166 Error::BadRequest(ErrorKind::NotFound, _)
167 | Error::Request(ErrorKind::NotFound, ..)
168 ) {
169 warn!("Incoming PDU failed {id}: {e:?}");
170 }
171 }
172
173 Ok(send_transaction_message::v1::Response {
174 pdus: results
175 .into_iter()
176 .map(|(e, r)| (e, r.map_err(error::sanitized_message)))
177 .collect(),
178 })
179}
180
181async fn handle(
182 services: &Services,
183 client: &IpAddr,
184 origin: &ServerName,
185 txn_id: &TransactionId,
186 started: Instant,
187 pdus: impl Stream<Item = (usize, Pdu)> + Send,
188 edus: impl Stream<Item = (usize, Edu)> + Send,
189) -> Result<ResolvedMap> {
190 let results = handle_pdus(services, client, origin, txn_id, started, pdus).await?;
191
192 handle_edus(services, client, origin, txn_id, edus).await?;
193
194 Ok(results)
195}
196
197async fn handle_pdus(
198 services: &Services,
199 client: &IpAddr,
200 origin: &ServerName,
201 txn_id: &TransactionId,
202 started: Instant,
203 pdus: impl Stream<Item = (usize, Pdu)> + Send,
204) -> Result<ResolvedMap> {
205 pdus.collect()
206 .map(Ok)
207 .map_ok(|pdus: TxnPdus| {
208 pdus.into_iter()
209 .sorted_by(|(_, (room_a, ..)), (_, (room_b, ..))| room_a.cmp(room_b))
210 .into_grouping_map_by(|(_, (room_id, ..))| room_id.clone())
211 .collect()
212 .into_iter()
213 .try_stream()
214 })
215 .try_flatten_stream()
216 .try_collect::<RoomsPdus>()
217 .map_ok(IntoIterator::into_iter)
218 .map_ok(IterStream::try_stream)
219 .try_flatten_stream()
220 .broad_and_then(async |(room_id, pdus)| {
221 handle_room(services, client, origin, txn_id, started, room_id, pdus)
222 .map_ok(ResolvedMap::into_iter)
223 .map_ok(IterStream::try_stream)
224 .await
225 })
226 .try_flatten()
227 .try_collect()
228 .await
229}
230
231#[tracing::instrument(
232 name = "room",
233 level = INFO_SPAN_LEVEL,
234 skip_all,
235 fields(%room_id)
236)]
237async fn handle_room(
238 services: &Services,
239 _client: &IpAddr,
240 origin: &ServerName,
241 txn_id: &TransactionId,
242 txn_start_time: Instant,
243 ref room_id: OwnedRoomId,
244 pdus: TxnPdus,
245) -> Result<ResolvedMap> {
246 let pdus = sort_pdus(pdus).await;
247
248 services
249 .event_handler
250 .mutex_federation
251 .lock(room_id)
252 .then(async |_lock| {
253 pdus.into_iter()
254 .enumerate()
255 .try_stream()
256 .and_then(async |pdu| {
257 services.server.check_running().map(|()| pdu) })
259 .and_then(|(ri, (ti, (room_id, event_id, value)))| {
260 let meta = (origin, txn_id, txn_start_time, ti);
261 let pdu = (ri, (room_id, event_id, value));
262 handle_pdu(services, meta, pdu).map(Ok)
263 })
264 .try_collect()
265 .await
266 })
267 .await
268}
269
270async fn sort_pdus(mut pdus: TxnPdus) -> TxnPdus {
275 if already_sorted(&pdus) {
276 return pdus;
277 }
278
279 let event_ids: BTreeMap<&str, &OwnedEventId> = pdus
280 .iter()
281 .map(|(_, (_, event_id, _))| (event_id.as_str(), event_id))
282 .collect();
283
284 let graph = pdus
285 .iter()
286 .map(|(_, (_, event_id, value))| {
287 let references = prev_event_ids(value)
288 .filter_map(|prev| event_ids.get(prev).copied())
289 .map(ToOwned::to_owned)
290 .collect();
291
292 (event_id.clone(), references)
293 })
294 .collect();
295
296 let query = async |_event_id: OwnedEventId| {
298 Ok((int!(0).into(), MilliSecondsSinceUnixEpoch(uint!(0))))
299 };
300
301 let Ok(order) = topological_sort(graph, &query).await else {
302 return pdus;
303 };
304
305 let position: BTreeMap<&str, usize> = order
306 .iter()
307 .enumerate()
308 .map(|(i, event_id)| (event_id.as_str(), i))
309 .collect();
310
311 pdus.sort_by_key(|(_, (_, event_id, _))| position.get(event_id.as_str()).copied());
312 pdus
313}
314
315fn already_sorted(pdus: &[(usize, Pdu)]) -> bool {
318 is_topologically_sorted_in_place(
319 pdus,
320 |(_, (_, id, _))| id.as_str(),
321 |(_, (_, _, value))| prev_event_ids(value),
322 )
323}
324
325fn prev_event_ids(value: &CanonicalJsonObject) -> impl Iterator<Item = &str> + '_ {
327 value
328 .get("prev_events")
329 .and_then(CanonicalJsonValue::as_array)
330 .into_iter()
331 .flatten()
332 .filter_map(CanonicalJsonValue::as_str)
333}
334
335#[tracing::instrument(
336 name = "pdu",
337 level = INFO_SPAN_LEVEL,
338 skip_all,
339 fields(%event_id, %ti, %ri)
340)]
341async fn handle_pdu(
342 services: &Services,
343 (origin, txn_id, txn_start_time, ti): (&ServerName, &TransactionId, Instant, usize),
344 (ri, (ref room_id, event_id, value)): (usize, Pdu),
345) -> (OwnedEventId, Result) {
346 let pdu_start_time = Instant::now();
347 let completed: AtomicBool = Default::default();
348 defer! {{
349 if completed.load(Ordering::Acquire) {
350 return;
351 }
352
353 if pdu_start_time.elapsed() >= Duration::from_secs(services.config.http.request_timeout) {
354 error!(
355 %origin, %txn_id, %room_id, %event_id, %ri, %ti,
356 elapsed = ?pdu_start_time.elapsed(),
357 "Incoming transaction processing timed out.",
358 );
359 } else {
360 debug_warn!(
361 %origin, %txn_id, %room_id, %event_id, %ri, %ti,
362 elapsed = ?pdu_start_time.elapsed(),
363 "Incoming transaction processing interrupted.",
364 );
365 }
366 }}
367
368 let result = services
369 .event_handler
370 .handle_pushed_pdu(origin, room_id, &event_id, value)
371 .map_ok(|_| ())
372 .await;
373
374 completed.store(true, Ordering::Release);
375 debug!(
376 %event_id, ri, ti,
377 pdu_elapsed = ?pdu_start_time.elapsed(),
378 txn_elapsed = ?txn_start_time.elapsed(),
379 "Finished PDU",
380 );
381
382 (event_id.clone(), result)
383}
384
385#[tracing::instrument(name = "edus", level = "debug", skip_all)]
386async fn handle_edus(
387 services: &Services,
388 client: &IpAddr,
389 origin: &ServerName,
390 txn_id: &TransactionId,
391 edus: impl Stream<Item = (usize, Edu)> + Send,
392) -> Result {
393 edus.for_each_concurrent(automatic_width(), |(i, edu)| {
394 handle_edu(services, client, origin, txn_id, i, edu)
395 })
396 .await;
397
398 Ok(())
399}
400
401#[tracing::instrument(
402 name = "edu",
403 level = "debug",
404 skip_all,
405 fields(%i),
406)]
407async fn handle_edu(
408 services: &Services,
409 client: &IpAddr,
410 origin: &ServerName,
411 _txn_id: &TransactionId,
412 i: usize,
413 edu: Edu,
414) {
415 match edu {
416 | Edu::Presence(presence) if services.server.config.presence.allow_incoming =>
417 handle_edu_presence(services, client, origin, presence).await,
418
419 | Edu::Receipt(receipt)
420 if services
421 .server
422 .config
423 .read_receipts
424 .allow_incoming =>
425 handle_edu_receipt(services, client, origin, receipt).await,
426
427 | Edu::Typing(typing) if services.server.config.typing.allow_incoming =>
428 handle_edu_typing(services, client, origin, typing).await,
429
430 | Edu::DeviceListUpdate(content) =>
431 handle_edu_device_list_update(services, client, origin, content).await,
432
433 | Edu::DirectToDevice(content) =>
434 handle_edu_direct_to_device(services, client, origin, content).await,
435
436 | Edu::SigningKeyUpdate(content) =>
437 handle_edu_signing_key_update(services, client, origin, content).await,
438
439 | Edu::_Custom(ref _custom) => debug_warn!(?i, ?edu, "received custom/unknown EDU"),
440
441 | _ => trace!(?i, ?edu, "skipped"),
442 }
443}
444
445async fn handle_edu_presence(
446 services: &Services,
447 _client: &IpAddr,
448 origin: &ServerName,
449 presence: PresenceContent,
450) {
451 presence
452 .push
453 .into_iter()
454 .stream()
455 .for_each_concurrent(automatic_width(), |update| {
456 handle_edu_presence_update(services, origin, update)
457 })
458 .await;
459}
460
461async fn handle_edu_presence_update(
462 services: &Services,
463 origin: &ServerName,
464 update: PresenceUpdate,
465) {
466 if update.user_id.server_name() != origin {
467 debug_warn!(
468 %update.user_id, %origin,
469 "received presence EDU for user not belonging to origin"
470 );
471 return;
472 }
473
474 services
475 .presence
476 .set_presence_from_federation(
477 &update.user_id,
478 &update.presence,
479 update.currently_active,
480 update.last_active_ago,
481 update.status_msg.clone(),
482 )
483 .await
484 .log_err()
485 .ok();
486}
487
488async fn handle_edu_receipt(
489 services: &Services,
490 _client: &IpAddr,
491 origin: &ServerName,
492 receipt: ReceiptContent,
493) {
494 receipt
495 .receipts
496 .into_iter()
497 .stream()
498 .for_each_concurrent(automatic_width(), |(room_id, room_updates)| {
499 handle_edu_receipt_room(services, origin, room_id, room_updates)
500 })
501 .await;
502}
503
504async fn handle_edu_receipt_room(
505 services: &Services,
506 origin: &ServerName,
507 room_id: OwnedRoomId,
508 room_updates: ReceiptMap,
509) {
510 if services
511 .event_handler
512 .acl_check(origin, &room_id)
513 .await
514 .is_err()
515 {
516 debug_warn!(
517 %origin, %room_id,
518 "received read receipt EDU from ACL'd server"
519 );
520 return;
521 }
522
523 let room_id = &room_id;
524 room_updates
525 .read
526 .into_iter()
527 .stream()
528 .for_each_concurrent(automatic_width(), async |(user_id, user_updates)| {
529 handle_edu_receipt_room_user(services, origin, room_id, &user_id, user_updates).await;
530 })
531 .await;
532}
533
534async fn handle_edu_receipt_room_user(
535 services: &Services,
536 origin: &ServerName,
537 room_id: &RoomId,
538 user_id: &UserId,
539 user_updates: ReceiptData,
540) {
541 if user_id.server_name() != origin {
542 debug_warn!(
543 %user_id, %origin,
544 "received read receipt EDU for user not belonging to origin"
545 );
546 return;
547 }
548
549 if !services
550 .state_cache
551 .server_in_room(origin, room_id)
552 .await
553 {
554 debug_warn!(
555 %user_id, %room_id, %origin,
556 "received read receipt EDU from server who does not have a member in the room",
557 );
558 return;
559 }
560
561 let data = &user_updates.data;
562 user_updates
563 .event_ids
564 .into_iter()
565 .stream()
566 .for_each_concurrent(automatic_width(), async |event_id| {
567 let user_data = [(user_id.to_owned(), data.clone())];
568 let receipts = [(ReceiptType::Read, BTreeMap::from(user_data))];
569 let content = [(event_id.clone(), BTreeMap::from(receipts))];
570 services
571 .read_receipt
572 .readreceipt_update(user_id, room_id, &ReceiptEvent {
573 content: ReceiptEventContent(content.into()),
574 room_id: room_id.to_owned(),
575 })
576 .await;
577 })
578 .await;
579}
580
581async fn handle_edu_typing(
582 services: &Services,
583 _client: &IpAddr,
584 origin: &ServerName,
585 typing: TypingContent,
586) {
587 if typing.user_id.server_name() != origin {
588 debug_warn!(
589 %typing.user_id, %origin,
590 "received typing EDU for user not belonging to origin"
591 );
592 return;
593 }
594
595 if services
596 .event_handler
597 .acl_check(typing.user_id.server_name(), &typing.room_id)
598 .await
599 .is_err()
600 {
601 debug_warn!(
602 %typing.user_id, %typing.room_id, %origin,
603 "received typing EDU for ACL'd user's server"
604 );
605 return;
606 }
607
608 if !services
609 .state_cache
610 .is_joined(&typing.user_id, &typing.room_id)
611 .await
612 {
613 debug_warn!(
614 %typing.user_id, %typing.room_id, %origin,
615 "received typing EDU for user not in room"
616 );
617 return;
618 }
619
620 if typing.typing {
621 let secs = services.server.config.typing.federation_timeout_s;
622 let timeout = millis_since_unix_epoch().saturating_add(secs.saturating_mul(1000));
623
624 services
625 .typing
626 .typing_add(&typing.user_id, &typing.room_id, timeout)
627 .await
628 .log_err()
629 .ok();
630 } else {
631 services
632 .typing
633 .typing_remove(&typing.user_id, &typing.room_id)
634 .await
635 .log_err()
636 .ok();
637 }
638}
639
640async fn handle_edu_device_list_update(
641 services: &Services,
642 _client: &IpAddr,
643 origin: &ServerName,
644 content: DeviceListUpdateContent,
645) {
646 let DeviceListUpdateContent { user_id, .. } = content;
647
648 if user_id.server_name() != origin {
649 debug_warn!(
650 %user_id, %origin,
651 "received device list update EDU for user not belonging to origin"
652 );
653 return;
654 }
655
656 services
657 .users
658 .mark_device_key_update(&user_id, DeviceListChange::Resync)
659 .await;
660}
661
662async fn handle_edu_direct_to_device(
663 services: &Services,
664 _client: &IpAddr,
665 origin: &ServerName,
666 content: DirectDeviceContent,
667) {
668 let DirectDeviceContent {
669 ref sender,
670 ref ev_type,
671 ref message_id,
672 messages,
673 } = content;
674
675 if sender.server_name() != origin {
676 debug_warn!(
677 %sender, %origin,
678 "received direct to device EDU for user not belonging to origin"
679 );
680 return;
681 }
682
683 if services
685 .transaction_ids
686 .existing_txnid(sender, None, message_id)
687 .await
688 .is_ok()
689 {
690 return;
691 }
692
693 let ev_type = ev_type.to_string();
694
695 messages
696 .into_iter()
697 .stream()
698 .broad_filter_map(async |(target_user_id, map)| {
699 to_device_deliverable(services, &target_user_id)
700 .await
701 .then_some((target_user_id, map))
702 })
703 .for_each_concurrent(automatic_width(), |(target_user_id, map)| {
704 handle_edu_direct_to_device_user(services, target_user_id, sender, &ev_type, map)
705 })
706 .await;
707
708 services
710 .transaction_ids
711 .add_txnid(sender, None, message_id, &[]);
712}
713
714async fn to_device_deliverable(services: &Services, user_id: &UserId) -> bool {
719 services.globals.user_is_local(user_id)
720 && ((user_id == services.globals.server_user && services.users.exists(user_id).await)
721 || services.users.is_active(user_id).await
722 || services
723 .appservice
724 .is_interested_in_user(user_id)
725 .await)
726}
727
728async fn handle_edu_direct_to_device_user<Event: Send + Sync>(
729 services: &Services,
730 target_user_id: OwnedUserId,
731 sender: &UserId,
732 ev_type: &str,
733 map: BTreeMap<DeviceIdOrAllDevices, Raw<Event>>,
734) {
735 map.into_iter()
736 .stream()
737 .ready_filter_map(|(tid, raw)| {
738 raw.deserialize_as()
739 .map_err(|e| {
740 err!(Request(InvalidParam(error!("To-Device event is invalid: {e}"))))
741 })
742 .ok()
743 .map(|ev| (tid, ev))
744 })
745 .for_each_concurrent(automatic_width(), |(tid, ev)| {
746 handle_edu_direct_to_device_event(services, &target_user_id, sender, tid, ev_type, ev)
747 })
748 .await;
749}
750
751async fn handle_edu_direct_to_device_event(
752 services: &Services,
753 target_user_id: &UserId,
754 sender: &UserId,
755 target_device_id_maybe: DeviceIdOrAllDevices,
756 ev_type: &str,
757 event: serde_json::Value,
758) {
759 match target_device_id_maybe {
760 | DeviceIdOrAllDevices::DeviceId(ref target_device_id) => {
761 let count = services.users.add_to_device_event(
762 sender,
763 target_user_id,
764 target_device_id,
765 ev_type,
766 &event,
767 );
768
769 services
770 .sending
771 .send_to_device_appservices(
772 sender,
773 target_user_id,
774 once((&**target_device_id, count)),
775 ev_type,
776 &event,
777 )
778 .await
779 .log_err()
780 .ok();
781 },
782
783 | DeviceIdOrAllDevices::AllDevices => {
784 let interested = services
785 .appservice
786 .is_interested_in_user(target_user_id)
787 .await;
788
789 let deliveries: Deliveries = services
790 .users
791 .all_device_ids(target_user_id)
792 .map(|target_device_id| {
793 let count = services.users.add_to_device_event(
794 sender,
795 target_user_id,
796 target_device_id,
797 ev_type,
798 &event,
799 );
800
801 (target_device_id, count)
802 })
803 .ready_filter_map(|(target_device_id, count)| {
804 interested.then(|| (target_device_id.to_owned(), count))
805 })
806 .collect()
807 .await;
808
809 if !deliveries.is_empty() {
810 services
811 .sending
812 .send_to_device_appservices(
813 sender,
814 target_user_id,
815 deliveries
816 .iter()
817 .map(|(device_id, count)| (&**device_id, *count)),
818 ev_type,
819 &event,
820 )
821 .await
822 .log_err()
823 .ok();
824 }
825 },
826 }
827}
828
829async fn handle_edu_signing_key_update(
830 services: &Services,
831 _client: &IpAddr,
832 origin: &ServerName,
833 content: SigningKeyUpdateContent,
834) {
835 let SigningKeyUpdateContent { user_id, master_key, self_signing_key } = content;
836
837 if user_id.server_name() != origin {
838 debug_warn!(
839 %user_id, %origin,
840 "received signing key update EDU from server that does not belong to user's server"
841 );
842 return;
843 }
844
845 services
846 .users
847 .add_cross_signing_keys(&user_id, &master_key, &self_signing_key, &None, true)
848 .await
849 .log_err()
850 .ok();
851}
852
853#[cfg(test)]
854mod tests {
855 use ruma::{CanonicalJsonObject, OwnedEventId, event_id, room_id};
856 use serde_json::json;
857
858 use super::{Pdu, TxnPdus, already_sorted, prev_event_ids, sort_pdus};
859
860 fn pdu(index: usize, id: &OwnedEventId, prev: &[&OwnedEventId]) -> (usize, Pdu) {
861 let prev_events: Vec<&str> = prev.iter().map(|e| e.as_str()).collect();
862 let value: CanonicalJsonObject =
863 serde_json::from_value(json!({ "prev_events": prev_events }))
864 .expect("valid canonical json");
865
866 (index, (room_id!("!r:example.com").to_owned(), id.clone(), value))
867 }
868
869 fn ids() -> (OwnedEventId, OwnedEventId, OwnedEventId) {
870 (
871 event_id!("$a:example.com").to_owned(),
872 event_id!("$b:example.com").to_owned(),
873 event_id!("$c:example.com").to_owned(),
874 )
875 }
876
877 fn order(pdus: &[(usize, Pdu)]) -> Vec<&str> {
878 pdus.iter()
879 .map(|(_, (_, id, _))| id.as_str())
880 .collect()
881 }
882
883 #[test]
884 fn sorted_when_parents_lead() {
885 let (a, b, c) = ids();
886 let pdus = [pdu(0, &a, &[]), pdu(1, &b, &[&a]), pdu(2, &c, &[&b])];
887
888 assert!(already_sorted(&pdus));
889 }
890
891 #[test]
892 fn unsorted_when_child_leads() {
893 let (a, b, _c) = ids();
894 let pdus = [pdu(0, &b, &[&a]), pdu(1, &a, &[])];
895
896 assert!(!already_sorted(&pdus));
897 }
898
899 #[test]
900 fn sorted_ignores_out_of_batch_references() {
901 let (a, b, c) = ids();
902 let pdus = [pdu(0, &b, &[&c]), pdu(1, &a, &[&c])];
903
904 assert!(already_sorted(&pdus));
905 }
906
907 #[tokio::test]
908 async fn sort_orders_parents_before_children() {
909 let (a, b, c) = ids();
910 let pdus: TxnPdus = [pdu(0, &c, &[&b]), pdu(1, &b, &[&a]), pdu(2, &a, &[])]
911 .into_iter()
912 .collect();
913
914 let sorted = sort_pdus(pdus).await;
915
916 assert_eq!(order(&sorted), ["$a:example.com", "$b:example.com", "$c:example.com"]);
917 }
918
919 #[tokio::test]
920 async fn sort_is_noop_when_already_ordered() {
921 let (a, b, c) = ids();
922 let pdus: TxnPdus = [pdu(0, &a, &[]), pdu(1, &b, &[&a]), pdu(2, &c, &[&b])]
923 .into_iter()
924 .collect();
925
926 let sorted = sort_pdus(pdus.clone()).await;
927
928 assert_eq!(order(&sorted), order(&pdus));
929 }
930
931 #[tokio::test]
932 async fn sort_preserves_duplicates() {
933 let (a, b, _c) = ids();
934 let pdus: TxnPdus = [pdu(0, &b, &[&a]), pdu(1, &a, &[]), pdu(2, &b, &[&a])]
935 .into_iter()
936 .collect();
937
938 let sorted = sort_pdus(pdus).await;
939
940 assert_eq!(sorted.len(), 3);
941 }
942
943 #[tokio::test]
944 async fn sort_preserves_a_cycle() {
945 let (a, b, _c) = ids();
946 let pdus: TxnPdus = [pdu(0, &a, &[&b]), pdu(1, &b, &[&a])]
947 .into_iter()
948 .collect();
949
950 let sorted = sort_pdus(pdus).await;
951
952 assert_eq!(sorted.len(), 2);
953 }
954
955 #[test]
956 fn prev_event_ids_reads_the_array() {
957 let (a, b, _c) = ids();
958 let (_, (_, _, value)) = pdu(0, &a, &[&b]);
959
960 let prev: Vec<&str> = prev_event_ids(&value).collect();
961
962 assert_eq!(prev, ["$b:example.com"]);
963 }
964
965 #[test]
966 fn prev_event_ids_empty_when_absent() {
967 let value = CanonicalJsonObject::new();
968
969 assert_eq!(prev_event_ids(&value).count(), 0);
970 }
971}