1use crate::ids::{EventId, RequestId, RoomId, TxnId, UserId};
10use serde::{Deserialize, Serialize};
11use std::fmt;
12
13pub(crate) fn percent_encode_segment(segment: &str) -> String {
22 let mut out = String::with_capacity(segment.len());
23 for byte in segment.bytes() {
24 match byte {
25 b'A'..=b'Z' | b'a'..=b'z' | b'0'..=b'9' | b'-' | b'.' | b'_' | b'~' => {
26 out.push(byte as char);
27 }
28 _ => {
29 out.push('%');
30 out.push_str(&format!("{byte:02X}"));
31 }
32 }
33 }
34 out
35}
36
37pub(crate) fn percent_decode_segment(segment: &str) -> Option<String> {
45 let bytes = segment.as_bytes();
46 let mut out = Vec::with_capacity(bytes.len());
47 let mut i = 0;
48 while i < bytes.len() {
49 match bytes[i] {
50 b'%' => {
51 let hex = segment.get(i + 1..i + 3)?;
52 let byte = u8::from_str_radix(hex, 16).ok()?;
53 out.push(byte);
54 i += 3;
55 }
56 b => {
57 out.push(b);
58 i += 1;
59 }
60 }
61 }
62 String::from_utf8(out).ok()
63}
64
65fn substitute(template: &str, params: &[(&str, &str)]) -> String {
68 let mut path = template.to_string();
69 for (placeholder, value) in params {
70 path = path.replace(placeholder, &percent_encode_segment(value));
71 }
72 path
73}
74
75#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
78pub enum HttpMethod {
79 Get,
81 Post,
83 Put,
85}
86
87impl HttpMethod {
88 pub fn as_str(self) -> &'static str {
90 match self {
91 HttpMethod::Get => "GET",
92 HttpMethod::Post => "POST",
93 HttpMethod::Put => "PUT",
94 }
95 }
96}
97
98impl fmt::Display for HttpMethod {
99 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
100 f.write_str(self.as_str())
101 }
102}
103
104#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
110pub enum OutgoingRequestKind {
111 Sync,
114 KeysUpload,
117 KeysQuery,
119 KeysClaim,
121 KeysChanges,
124 SendToDevice,
127 SigningKeysUpload,
129 SignaturesUpload,
131 RoomSend,
134 RoomRedact,
136 RoomState,
139 RoomMessages,
141 RoomMembers,
143 CreateRoom,
145 JoinRoom,
147 LeaveRoom,
149 Invite,
151 Kick,
153 SetState,
155 Typing,
158 Receipt,
161 ReadMarkers,
163 AccountData,
165 RoomAccountData,
168 Profile,
170 UserDirectorySearch,
172 PublicRooms,
174 BackupVersion,
176 BackupKeys,
179}
180
181#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
184pub struct OutgoingRequest {
185 pub id: RequestId,
189 pub method: HttpMethod,
191 pub path: String,
195 pub query: Vec<(String, String)>,
200 pub body: Option<serde_json::Value>,
202 pub kind: OutgoingRequestKind,
204}
205
206const SYNC_FILTER: &str = r#"{"room":{"include_leave":true}}"#;
210
211impl OutgoingRequest {
212 pub fn sync(id: RequestId, since: Option<&str>, timeout_ms: Option<u64>) -> Self {
216 const PATH_TEMPLATE: &str = "/_matrix/client/v3/sync";
217 let mut query = vec![("filter".to_string(), SYNC_FILTER.to_string())];
218 if let Some(since) = since {
219 query.push(("since".to_string(), since.to_string()));
220 }
221 if let Some(timeout_ms) = timeout_ms {
222 query.push(("timeout".to_string(), timeout_ms.to_string()));
223 }
224 Self {
225 id,
226 method: HttpMethod::Get,
227 path: PATH_TEMPLATE.to_string(),
228 query,
229 body: None,
230 kind: OutgoingRequestKind::Sync,
231 }
232 }
233
234 pub fn keys_upload(id: RequestId, body: serde_json::Value) -> Self {
236 const PATH_TEMPLATE: &str = "/_matrix/client/v3/keys/upload";
237 Self {
238 id,
239 method: HttpMethod::Post,
240 path: PATH_TEMPLATE.to_string(),
241 query: Vec::new(),
242 body: Some(body),
243 kind: OutgoingRequestKind::KeysUpload,
244 }
245 }
246
247 pub fn keys_query(id: RequestId, body: serde_json::Value) -> Self {
249 const PATH_TEMPLATE: &str = "/_matrix/client/v3/keys/query";
250 Self {
251 id,
252 method: HttpMethod::Post,
253 path: PATH_TEMPLATE.to_string(),
254 query: Vec::new(),
255 body: Some(body),
256 kind: OutgoingRequestKind::KeysQuery,
257 }
258 }
259
260 pub fn keys_claim(id: RequestId, body: serde_json::Value) -> Self {
262 const PATH_TEMPLATE: &str = "/_matrix/client/v3/keys/claim";
263 Self {
264 id,
265 method: HttpMethod::Post,
266 path: PATH_TEMPLATE.to_string(),
267 query: Vec::new(),
268 body: Some(body),
269 kind: OutgoingRequestKind::KeysClaim,
270 }
271 }
272
273 pub fn keys_changes(id: RequestId, from: &str, to: &str) -> Self {
275 const PATH_TEMPLATE: &str = "/_matrix/client/v3/keys/changes";
276 Self {
277 id,
278 method: HttpMethod::Get,
279 path: PATH_TEMPLATE.to_string(),
280 query: vec![("from".to_string(), from.to_string()), ("to".to_string(), to.to_string())],
281 body: None,
282 kind: OutgoingRequestKind::KeysChanges,
283 }
284 }
285
286 pub fn send_to_device(
288 id: RequestId,
289 event_type: &str,
290 txn_id: &TxnId,
291 body: serde_json::Value,
292 ) -> Self {
293 const PATH_TEMPLATE: &str = "/_matrix/client/v3/sendToDevice/{event_type}/{txn_id}";
294 let path = substitute(
295 PATH_TEMPLATE,
296 &[("{event_type}", event_type), ("{txn_id}", txn_id.as_str())],
297 );
298 Self {
299 id,
300 method: HttpMethod::Put,
301 path,
302 query: Vec::new(),
303 body: Some(body),
304 kind: OutgoingRequestKind::SendToDevice,
305 }
306 }
307
308 pub fn signing_keys_upload(id: RequestId, body: serde_json::Value) -> Self {
310 const PATH_TEMPLATE: &str = "/_matrix/client/v3/keys/device_signing/upload";
311 Self {
312 id,
313 method: HttpMethod::Post,
314 path: PATH_TEMPLATE.to_string(),
315 query: Vec::new(),
316 body: Some(body),
317 kind: OutgoingRequestKind::SigningKeysUpload,
318 }
319 }
320
321 pub fn signatures_upload(id: RequestId, body: serde_json::Value) -> Self {
323 const PATH_TEMPLATE: &str = "/_matrix/client/v3/keys/signatures/upload";
324 Self {
325 id,
326 method: HttpMethod::Post,
327 path: PATH_TEMPLATE.to_string(),
328 query: Vec::new(),
329 body: Some(body),
330 kind: OutgoingRequestKind::SignaturesUpload,
331 }
332 }
333
334 pub fn room_send(
336 id: RequestId,
337 room_id: &RoomId,
338 event_type: &str,
339 txn_id: &TxnId,
340 body: serde_json::Value,
341 ) -> Self {
342 const PATH_TEMPLATE: &str = "/_matrix/client/v3/rooms/{room_id}/send/{event_type}/{txn_id}";
343 let path = substitute(
344 PATH_TEMPLATE,
345 &[
346 ("{room_id}", room_id.as_str()),
347 ("{event_type}", event_type),
348 ("{txn_id}", txn_id.as_str()),
349 ],
350 );
351 Self {
352 id,
353 method: HttpMethod::Put,
354 path,
355 query: Vec::new(),
356 body: Some(body),
357 kind: OutgoingRequestKind::RoomSend,
358 }
359 }
360
361 pub fn room_redact(
363 id: RequestId,
364 room_id: &RoomId,
365 event_id: &EventId,
366 txn_id: &TxnId,
367 body: serde_json::Value,
368 ) -> Self {
369 const PATH_TEMPLATE: &str =
370 "/_matrix/client/v3/rooms/{room_id}/redact/{event_id}/{txn_id}";
371 let path = substitute(
372 PATH_TEMPLATE,
373 &[
374 ("{room_id}", room_id.as_str()),
375 ("{event_id}", event_id.as_str()),
376 ("{txn_id}", txn_id.as_str()),
377 ],
378 );
379 Self {
380 id,
381 method: HttpMethod::Put,
382 path,
383 query: Vec::new(),
384 body: Some(body),
385 kind: OutgoingRequestKind::RoomRedact,
386 }
387 }
388
389 pub fn room_state(
393 id: RequestId,
394 room_id: &RoomId,
395 event_type: &str,
396 state_key: &str,
397 body: serde_json::Value,
398 ) -> Self {
399 const PATH_TEMPLATE: &str =
400 "/_matrix/client/v3/rooms/{room_id}/state/{event_type}/{state_key}";
401 let path = substitute(
402 PATH_TEMPLATE,
403 &[
404 ("{room_id}", room_id.as_str()),
405 ("{event_type}", event_type),
406 ("{state_key}", state_key),
407 ],
408 );
409 Self {
410 id,
411 method: HttpMethod::Put,
412 path,
413 query: Vec::new(),
414 body: Some(body),
415 kind: OutgoingRequestKind::RoomState,
416 }
417 }
418
419 pub fn room_messages(
421 id: RequestId,
422 room_id: &RoomId,
423 from: &str,
424 dir: &str,
425 limit: Option<u32>,
426 ) -> Self {
427 const PATH_TEMPLATE: &str = "/_matrix/client/v3/rooms/{room_id}/messages";
428 let path = substitute(PATH_TEMPLATE, &[("{room_id}", room_id.as_str())]);
429 let mut query = vec![("from".to_string(), from.to_string()), ("dir".to_string(), dir.to_string())];
430 if let Some(limit) = limit {
431 query.push(("limit".to_string(), limit.to_string()));
432 }
433 Self {
434 id,
435 method: HttpMethod::Get,
436 path,
437 query,
438 body: None,
439 kind: OutgoingRequestKind::RoomMessages,
440 }
441 }
442
443 pub fn room_messages_latest(id: RequestId, room_id: &RoomId, limit: u32) -> Self {
447 const PATH_TEMPLATE: &str = "/_matrix/client/v3/rooms/{room_id}/messages";
448 let path = substitute(PATH_TEMPLATE, &[("{room_id}", room_id.as_str())]);
449 Self {
450 id,
451 method: HttpMethod::Get,
452 path,
453 query: vec![
454 ("dir".to_string(), "b".to_string()),
455 ("limit".to_string(), limit.to_string()),
456 ],
457 body: None,
458 kind: OutgoingRequestKind::RoomMessages,
459 }
460 }
461
462 pub fn room_members(id: RequestId, room_id: &RoomId) -> Self {
464 const PATH_TEMPLATE: &str = "/_matrix/client/v3/rooms/{room_id}/members";
465 let path = substitute(PATH_TEMPLATE, &[("{room_id}", room_id.as_str())]);
466 Self {
467 id,
468 method: HttpMethod::Get,
469 path,
470 query: Vec::new(),
471 body: None,
472 kind: OutgoingRequestKind::RoomMembers,
473 }
474 }
475
476 pub fn create_room(id: RequestId, body: serde_json::Value) -> Self {
478 const PATH_TEMPLATE: &str = "/_matrix/client/v3/createRoom";
479 Self {
480 id,
481 method: HttpMethod::Post,
482 path: PATH_TEMPLATE.to_string(),
483 query: Vec::new(),
484 body: Some(body),
485 kind: OutgoingRequestKind::CreateRoom,
486 }
487 }
488
489 pub fn join_room(id: RequestId, room_id: &str) -> Self {
492 const PATH_TEMPLATE: &str = "/_matrix/client/v3/rooms/{room_id}/join";
493 let path = substitute(PATH_TEMPLATE, &[("{room_id}", room_id)]);
494 Self {
495 id,
496 method: HttpMethod::Post,
497 path,
498 query: Vec::new(),
499 body: None,
500 kind: OutgoingRequestKind::JoinRoom,
501 }
502 }
503
504 pub fn leave_room(id: RequestId, room_id: &RoomId) -> Self {
506 const PATH_TEMPLATE: &str = "/_matrix/client/v3/rooms/{room_id}/leave";
507 let path = substitute(PATH_TEMPLATE, &[("{room_id}", room_id.as_str())]);
508 Self {
509 id,
510 method: HttpMethod::Post,
511 path,
512 query: Vec::new(),
513 body: None,
514 kind: OutgoingRequestKind::LeaveRoom,
515 }
516 }
517
518 pub fn invite(id: RequestId, room_id: &RoomId, user_id: &UserId) -> Self {
520 const PATH_TEMPLATE: &str = "/_matrix/client/v3/rooms/{room_id}/invite";
521 let path = substitute(PATH_TEMPLATE, &[("{room_id}", room_id.as_str())]);
522 Self {
523 id,
524 method: HttpMethod::Post,
525 path,
526 query: Vec::new(),
527 body: Some(serde_json::json!({ "user_id": user_id.as_str() })),
528 kind: OutgoingRequestKind::Invite,
529 }
530 }
531
532 pub fn kick(id: RequestId, room_id: &RoomId, user_id: &UserId, reason: Option<&str>) -> Self {
534 const PATH_TEMPLATE: &str = "/_matrix/client/v3/rooms/{room_id}/kick";
535 let path = substitute(PATH_TEMPLATE, &[("{room_id}", room_id.as_str())]);
536 let mut body = serde_json::json!({ "user_id": user_id.as_str() });
537 if let Some(reason) = reason {
538 body["reason"] = serde_json::Value::String(reason.to_string());
539 }
540 Self {
541 id,
542 method: HttpMethod::Post,
543 path,
544 query: Vec::new(),
545 body: Some(body),
546 kind: OutgoingRequestKind::Kick,
547 }
548 }
549
550 pub fn set_state(id: RequestId, room_id: &RoomId, event_type: &str, state_key: &str, content: serde_json::Value) -> Self {
552 const PATH_TEMPLATE: &str = "/_matrix/client/v3/rooms/{room_id}/state/{event_type}/{state_key}";
553 let path = substitute(PATH_TEMPLATE, &[("{room_id}", room_id.as_str()), ("{event_type}", event_type), ("{state_key}", state_key)]);
554 Self { id, method: HttpMethod::Put, path, query: Vec::new(), body: Some(content), kind: OutgoingRequestKind::SetState }
555 }
556
557 pub fn typing(
559 id: RequestId,
560 room_id: &RoomId,
561 user_id: &UserId,
562 typing: bool,
563 timeout_ms: Option<u64>,
564 ) -> Self {
565 const PATH_TEMPLATE: &str = "/_matrix/client/v3/rooms/{room_id}/typing/{user_id}";
566 let path = substitute(
567 PATH_TEMPLATE,
568 &[("{room_id}", room_id.as_str()), ("{user_id}", user_id.as_str())],
569 );
570 let mut body = serde_json::json!({ "typing": typing });
571 if let Some(timeout_ms) = timeout_ms {
572 body["timeout"] = serde_json::Value::from(timeout_ms);
573 }
574 Self {
575 id,
576 method: HttpMethod::Put,
577 path,
578 query: Vec::new(),
579 body: Some(body),
580 kind: OutgoingRequestKind::Typing,
581 }
582 }
583
584 pub fn receipt(id: RequestId, room_id: &RoomId, receipt_type: &str, event_id: &EventId) -> Self {
586 const PATH_TEMPLATE: &str =
587 "/_matrix/client/v3/rooms/{room_id}/receipt/{receipt_type}/{event_id}";
588 let path = substitute(
589 PATH_TEMPLATE,
590 &[
591 ("{room_id}", room_id.as_str()),
592 ("{receipt_type}", receipt_type),
593 ("{event_id}", event_id.as_str()),
594 ],
595 );
596 Self {
597 id,
598 method: HttpMethod::Post,
599 path,
600 query: Vec::new(),
601 body: Some(serde_json::json!({})),
602 kind: OutgoingRequestKind::Receipt,
603 }
604 }
605
606 pub fn read_markers(id: RequestId, room_id: &RoomId, body: serde_json::Value) -> Self {
608 const PATH_TEMPLATE: &str = "/_matrix/client/v3/rooms/{room_id}/read_markers";
609 let path = substitute(PATH_TEMPLATE, &[("{room_id}", room_id.as_str())]);
610 Self {
611 id,
612 method: HttpMethod::Post,
613 path,
614 query: Vec::new(),
615 body: Some(body),
616 kind: OutgoingRequestKind::ReadMarkers,
617 }
618 }
619
620 pub fn account_data(
622 id: RequestId,
623 user_id: &UserId,
624 event_type: &str,
625 body: serde_json::Value,
626 ) -> Self {
627 const PATH_TEMPLATE: &str = "/_matrix/client/v3/user/{user_id}/account_data/{event_type}";
628 let path = substitute(
629 PATH_TEMPLATE,
630 &[("{user_id}", user_id.as_str()), ("{event_type}", event_type)],
631 );
632 Self {
633 id,
634 method: HttpMethod::Put,
635 path,
636 query: Vec::new(),
637 body: Some(body),
638 kind: OutgoingRequestKind::AccountData,
639 }
640 }
641
642 pub fn room_account_data(
644 id: RequestId,
645 user_id: &UserId,
646 room_id: &RoomId,
647 event_type: &str,
648 body: serde_json::Value,
649 ) -> Self {
650 const PATH_TEMPLATE: &str =
651 "/_matrix/client/v3/user/{user_id}/rooms/{room_id}/account_data/{event_type}";
652 let path = substitute(
653 PATH_TEMPLATE,
654 &[
655 ("{user_id}", user_id.as_str()),
656 ("{room_id}", room_id.as_str()),
657 ("{event_type}", event_type),
658 ],
659 );
660 Self {
661 id,
662 method: HttpMethod::Put,
663 path,
664 query: Vec::new(),
665 body: Some(body),
666 kind: OutgoingRequestKind::RoomAccountData,
667 }
668 }
669
670 pub fn profile(id: RequestId, user_id: &UserId) -> Self {
672 const PATH_TEMPLATE: &str = "/_matrix/client/v3/profile/{user_id}";
673 let path = substitute(PATH_TEMPLATE, &[("{user_id}", user_id.as_str())]);
674 Self {
675 id,
676 method: HttpMethod::Get,
677 path,
678 query: Vec::new(),
679 body: None,
680 kind: OutgoingRequestKind::Profile,
681 }
682 }
683
684 pub fn user_directory_search(id: RequestId, body: serde_json::Value) -> Self {
686 const PATH_TEMPLATE: &str = "/_matrix/client/v3/user_directory/search";
687 Self {
688 id,
689 method: HttpMethod::Post,
690 path: PATH_TEMPLATE.to_string(),
691 query: Vec::new(),
692 body: Some(body),
693 kind: OutgoingRequestKind::UserDirectorySearch,
694 }
695 }
696
697 pub fn public_rooms(id: RequestId, body: serde_json::Value) -> Self {
699 const PATH_TEMPLATE: &str = "/_matrix/client/v3/publicRooms";
700 Self {
701 id,
702 method: HttpMethod::Post,
703 path: PATH_TEMPLATE.to_string(),
704 query: Vec::new(),
705 body: Some(body),
706 kind: OutgoingRequestKind::PublicRooms,
707 }
708 }
709
710 pub fn backup_version(id: RequestId) -> Self {
714 const PATH_TEMPLATE: &str = "/_matrix/client/v3/room_keys/version";
715 Self {
716 id,
717 method: HttpMethod::Get,
718 path: PATH_TEMPLATE.to_string(),
719 query: Vec::new(),
720 body: None,
721 kind: OutgoingRequestKind::BackupVersion,
722 }
723 }
724
725 pub fn backup_keys(id: RequestId, room_id: &RoomId, session_id: &str, version: &str) -> Self {
732 const PATH_TEMPLATE: &str = "/_matrix/client/v3/room_keys/keys/{room_id}/{session_id}";
733 let path = substitute(
734 PATH_TEMPLATE,
735 &[("{room_id}", room_id.as_str()), ("{session_id}", session_id)],
736 );
737 Self {
738 id,
739 method: HttpMethod::Get,
740 path,
741 query: vec![("version".to_string(), version.to_string())],
742 body: None,
743 kind: OutgoingRequestKind::BackupKeys,
744 }
745 }
746}
747
748#[derive(Clone, Debug, PartialEq, Eq)]
750pub struct HttpResponseDescriptor {
751 pub status: u16,
753 pub body: Vec<u8>,
755}
756
757impl HttpResponseDescriptor {
758 pub fn matrix_error(&self) -> Option<(String, String)> {
766 let value: serde_json::Value = serde_json::from_slice(&self.body).ok()?;
767 let errcode = value.get("errcode")?.as_str()?.to_string();
768 let error = value
769 .get("error")
770 .and_then(serde_json::Value::as_str)
771 .unwrap_or_default()
772 .to_string();
773 Some((errcode, error))
774 }
775}
776
777#[cfg(test)]
778mod tests {
779 use super::*;
780
781 fn id() -> RequestId {
782 RequestId::next(0)
783 }
784
785 #[test]
786 fn percent_encode_segment_escapes_matrix_sigils() {
787 assert_eq!(percent_encode_segment("!abc:example.org"), "%21abc%3Aexample.org");
788 assert_eq!(percent_encode_segment("safe-._~123"), "safe-._~123");
789 }
790
791 #[test]
792 fn percent_decode_segment_reverses_percent_encode_segment() {
793 for raw in ["!abc:example.org", "safe-._~123", "curve/25519+key=", ""] {
794 let encoded = percent_encode_segment(raw);
795 assert_eq!(percent_decode_segment(&encoded).as_deref(), Some(raw));
796 }
797 }
798
799 #[test]
800 fn percent_decode_segment_rejects_a_malformed_escape() {
801 assert_eq!(percent_decode_segment("%2"), None);
802 assert_eq!(percent_decode_segment("%zz"), None);
803 }
804
805 #[test]
806 fn room_send_percent_encodes_the_room_id_in_the_path() {
807 let room_id = RoomId::parse("!abc:example.org").expect("valid room id");
808 let txn_id = TxnId::new(0);
809 let req = OutgoingRequest::room_send(
810 id(),
811 &room_id,
812 "m.room.message",
813 &txn_id,
814 serde_json::json!({ "body": "hi" }),
815 );
816 assert_eq!(
817 req.path,
818 format!(
819 "/_matrix/client/v3/rooms/%21abc%3Aexample.org/send/m.room.message/{}",
820 txn_id.as_str()
821 )
822 );
823 assert_eq!(req.method, HttpMethod::Put);
824 assert_eq!(req.kind, OutgoingRequestKind::RoomSend);
825 }
826
827 #[test]
828 fn send_to_device_percent_encodes_the_txn_id_in_the_path() {
829 let txn_id = TxnId::new(0);
830 let req = OutgoingRequest::send_to_device(
831 id(),
832 "m.room.encrypted",
833 &txn_id,
834 serde_json::json!({ "messages": {} }),
835 );
836 assert!(req.path.starts_with("/_matrix/client/v3/sendToDevice/m.room.encrypted/"));
837 assert!(!req.path.contains(':'), "txn id's own characters carry no raw ':'");
838 }
839
840 #[test]
841 fn sync_carries_since_and_timeout_as_query_params_only_when_present() {
842 let filter = ("filter".to_string(), SYNC_FILTER.to_string());
843 let bare = OutgoingRequest::sync(id(), None, None);
844 assert_eq!(bare.query, vec![filter.clone()]);
845
846 let full = OutgoingRequest::sync(id(), Some("s123"), Some(30_000));
847 assert_eq!(
848 full.query,
849 vec![filter, ("since".to_string(), "s123".to_string()), ("timeout".to_string(), "30000".to_string())]
850 );
851 }
852
853 #[test]
854 fn sync_filter_asks_for_left_rooms() {
855 let parsed: serde_json::Value = serde_json::from_str(SYNC_FILTER).expect("the filter is JSON");
856 assert_eq!(parsed["room"]["include_leave"], serde_json::Value::Bool(true));
857 }
858
859 #[test]
860 fn outgoing_request_kind_round_trips_serde() {
861 for kind in [
862 OutgoingRequestKind::Sync,
863 OutgoingRequestKind::KeysUpload,
864 OutgoingRequestKind::RoomSend,
865 OutgoingRequestKind::BackupKeys,
866 ] {
867 let json = serde_json::to_string(&kind).expect("serialize");
868 let back: OutgoingRequestKind = serde_json::from_str(&json).expect("deserialize");
869 assert_eq!(back, kind);
870 }
871 }
872
873 #[test]
874 fn outgoing_request_round_trips_serde() {
875 let room_id = RoomId::parse("!abc:example.org").expect("valid room id");
876 let req = OutgoingRequest::room_members(id(), &room_id);
877 let json = serde_json::to_string(&req).expect("serialize");
878 let back: OutgoingRequest = serde_json::from_str(&json).expect("deserialize");
879 assert_eq!(back, req);
880 }
881
882 #[test]
883 fn matrix_error_parses_errcode_and_error() {
884 let resp = HttpResponseDescriptor {
885 status: 403,
886 body: br#"{"errcode":"M_FORBIDDEN","error":"Guest access is forbidden"}"#.to_vec(),
887 };
888 assert_eq!(
889 resp.matrix_error(),
890 Some(("M_FORBIDDEN".to_string(), "Guest access is forbidden".to_string()))
891 );
892 }
893
894 #[test]
895 fn matrix_error_defaults_a_missing_error_field_to_empty() {
896 let resp = HttpResponseDescriptor {
897 status: 429,
898 body: br#"{"errcode":"M_LIMIT_EXCEEDED"}"#.to_vec(),
899 };
900 assert_eq!(resp.matrix_error(), Some(("M_LIMIT_EXCEEDED".to_string(), String::new())));
901 }
902
903 #[test]
904 fn matrix_error_is_none_for_a_non_json_body() {
905 let resp = HttpResponseDescriptor { status: 200, body: b"not json".to_vec() };
906 assert_eq!(resp.matrix_error(), None);
907 }
908
909 #[test]
910 fn matrix_error_is_none_when_errcode_is_absent() {
911 let resp = HttpResponseDescriptor {
912 status: 200,
913 body: br#"{"next_batch":"s1"}"#.to_vec(),
914 };
915 assert_eq!(resp.matrix_error(), None);
916 }
917}