1mod serde;
6
7pub mod v1 {
8 use std::collections::BTreeSet;
13
14 use ruma_common::{
15 DeviceId,
16 api::{auth_scheme::NoAuthentication, request, response},
17 metadata,
18 serde::{Raw, StringEnum},
19 };
20 use serde::Serialize;
21 use url::Url;
22
23 use crate::PrivOwnedStr;
24
25 metadata! {
26 method: GET,
27 rate_limited: false,
28 authentication: NoAuthentication,
29 history: {
30 unstable => "/_matrix/client/unstable/org.matrix.msc2965/auth_metadata",
31 1.15 => "/_matrix/client/v1/auth_metadata",
32 }
33 }
34
35 #[request(error = crate::Error)]
37 #[derive(Default)]
38 pub struct Request {}
39
40 #[response(error = crate::Error)]
42 pub struct Response {
43 #[ruma_api(body)]
47 pub metadata: Raw<AuthorizationServerMetadata>,
48 }
49
50 impl Request {
51 pub fn new() -> Self {
53 Self {}
54 }
55 }
56
57 impl Response {
58 pub fn new(metadata: Raw<AuthorizationServerMetadata>) -> Self {
60 Self { metadata }
61 }
62 }
63
64 #[derive(Debug, Clone, Serialize)]
80 #[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
81 pub struct AuthorizationServerMetadata {
82 pub issuer: Url,
86
87 pub authorization_endpoint: Url,
91
92 pub token_endpoint: Url,
96
97 #[serde(skip_serializing_if = "Option::is_none")]
102 pub registration_endpoint: Option<Url>,
103
104 pub response_types_supported: BTreeSet<ResponseType>,
113
114 pub response_modes_supported: BTreeSet<ResponseMode>,
122
123 pub grant_types_supported: BTreeSet<GrantType>,
133
134 pub revocation_endpoint: Url,
138
139 pub code_challenge_methods_supported: BTreeSet<CodeChallengeMethod>,
146
147 #[serde(skip_serializing_if = "Option::is_none")]
152 pub account_management_uri: Option<Url>,
153
154 #[serde(skip_serializing_if = "BTreeSet::is_empty")]
158 pub account_management_actions_supported: BTreeSet<AccountManagementAction>,
159
160 #[cfg(feature = "unstable-msc4108")]
164 #[serde(skip_serializing_if = "Option::is_none")]
165 pub device_authorization_endpoint: Option<Url>,
166
167 #[serde(skip_serializing_if = "Vec::is_empty")]
172 pub prompt_values_supported: Vec<Prompt>,
173 }
174
175 impl AuthorizationServerMetadata {
176 pub fn validate_urls(&self) -> Result<(), AuthorizationServerMetadataUrlError> {
184 self.validate_urls_inner(false)
185 }
186
187 pub fn insecure_validate_urls(&self) -> Result<(), AuthorizationServerMetadataUrlError> {
195 self.validate_urls_inner(true)
196 }
197
198 fn validate_urls_inner(
201 &self,
202 insecure: bool,
203 ) -> Result<(), AuthorizationServerMetadataUrlError> {
204 if self.issuer.query().is_some() || self.issuer.fragment().is_some() {
205 return Err(AuthorizationServerMetadataUrlError::IssuerHasQueryOrFragment);
206 }
207
208 if insecure {
209 return Ok(());
211 }
212
213 let required_urls = &[
214 ("issuer", &self.issuer),
215 ("authorization_endpoint", &self.authorization_endpoint),
216 ("token_endpoint", &self.token_endpoint),
217 ("revocation_endpoint", &self.revocation_endpoint),
218 ];
219 let optional_urls = &[
220 self.registration_endpoint.as_ref().map(|string| ("registration_endpoint", string)),
221 self.account_management_uri
222 .as_ref()
223 .map(|string| ("account_management_uri", string)),
224 #[cfg(feature = "unstable-msc4108")]
225 self.device_authorization_endpoint
226 .as_ref()
227 .map(|string| ("device_authorization_endpoint", string)),
228 ];
229
230 for (field, url) in required_urls.iter().chain(optional_urls.iter().flatten()) {
231 if url.scheme() != "https" {
232 return Err(AuthorizationServerMetadataUrlError::NotHttpsScheme(field));
233 }
234 }
235
236 Ok(())
237 }
238
239 pub fn is_account_management_action_supported(
244 &self,
245 action: &AccountManagementAction,
246 ) -> bool {
247 match action {
248 AccountManagementAction::UnstableSessionsList
249 | AccountManagementAction::DevicesList => {
250 self.account_management_actions_supported
251 .contains(&AccountManagementAction::DevicesList)
252 || self
253 .account_management_actions_supported
254 .contains(&AccountManagementAction::UnstableSessionsList)
255 }
256 AccountManagementAction::UnstableSessionView
257 | AccountManagementAction::DeviceView => {
258 self.account_management_actions_supported
259 .contains(&AccountManagementAction::DeviceView)
260 || self
261 .account_management_actions_supported
262 .contains(&AccountManagementAction::UnstableSessionView)
263 }
264 AccountManagementAction::UnstableSessionEnd
265 | AccountManagementAction::DeviceDelete => {
266 self.account_management_actions_supported
267 .contains(&AccountManagementAction::DeviceDelete)
268 || self
269 .account_management_actions_supported
270 .contains(&AccountManagementAction::UnstableSessionEnd)
271 }
272 action => self.account_management_actions_supported.contains(action),
273 }
274 }
275
276 pub fn account_management_url_with_action(
285 &self,
286 action: AccountManagementActionData<'_>,
287 ) -> Option<Url> {
288 const QUERY_NAME_ACTION: &str = "action";
289 const QUERY_NAME_DEVICE_ID: &str = "device_id";
290
291 let mut url = self.account_management_uri.clone()?;
292
293 match action {
294 AccountManagementActionData::Profile => {
295 url.query_pairs_mut()
296 .append_pair(QUERY_NAME_ACTION, AccountManagementAction::Profile.as_str());
297 }
298 AccountManagementActionData::DevicesList => {
299 let action = if self
302 .account_management_actions_supported
303 .contains(&AccountManagementAction::DevicesList)
304 {
305 AccountManagementAction::DevicesList
306 } else if self
307 .account_management_actions_supported
308 .contains(&AccountManagementAction::UnstableSessionsList)
309 {
310 AccountManagementAction::UnstableSessionsList
311 } else {
312 AccountManagementAction::DevicesList
313 };
314
315 url.query_pairs_mut().append_pair(QUERY_NAME_ACTION, action.as_str());
316 }
317 AccountManagementActionData::DeviceView(DeviceViewData { device_id }) => {
318 let action = if self
321 .account_management_actions_supported
322 .contains(&AccountManagementAction::DeviceView)
323 {
324 AccountManagementAction::DeviceView
325 } else if self
326 .account_management_actions_supported
327 .contains(&AccountManagementAction::UnstableSessionView)
328 {
329 AccountManagementAction::UnstableSessionView
330 } else {
331 AccountManagementAction::DeviceView
332 };
333
334 url.query_pairs_mut()
335 .append_pair(QUERY_NAME_ACTION, action.as_str())
336 .append_pair(QUERY_NAME_DEVICE_ID, device_id.as_str());
337 }
338 AccountManagementActionData::DeviceDelete(DeviceDeleteData { device_id }) => {
339 let action = if self
342 .account_management_actions_supported
343 .contains(&AccountManagementAction::DeviceDelete)
344 {
345 AccountManagementAction::DeviceDelete
346 } else if self
347 .account_management_actions_supported
348 .contains(&AccountManagementAction::UnstableSessionEnd)
349 {
350 AccountManagementAction::UnstableSessionEnd
351 } else {
352 AccountManagementAction::DeviceDelete
353 };
354
355 url.query_pairs_mut()
356 .append_pair(QUERY_NAME_ACTION, action.as_str())
357 .append_pair(QUERY_NAME_DEVICE_ID, device_id.as_str());
358 }
359 AccountManagementActionData::AccountDeactivate => {
360 url.query_pairs_mut().append_pair(
361 QUERY_NAME_ACTION,
362 AccountManagementAction::AccountDeactivate.as_str(),
363 );
364 }
365 AccountManagementActionData::CrossSigningReset => {
366 url.query_pairs_mut().append_pair(
367 QUERY_NAME_ACTION,
368 AccountManagementAction::CrossSigningReset.as_str(),
369 );
370 }
371 }
372
373 Some(url)
374 }
375 }
376
377 #[doc = include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/src/doc/string_enum.md"))]
379 #[derive(Clone, StringEnum)]
380 #[ruma_enum(rename_all = "lowercase")]
381 #[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
382 pub enum ResponseType {
383 Code,
387
388 #[doc(hidden)]
389 _Custom(PrivOwnedStr),
390 }
391
392 #[doc = include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/src/doc/string_enum.md"))]
397 #[derive(Clone, StringEnum)]
400 #[ruma_enum(rename_all = "lowercase")]
401 #[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
402 pub enum ResponseMode {
403 Query,
406
407 Fragment,
410
411 #[doc(hidden)]
412 _Custom(PrivOwnedStr),
413 }
414
415 #[doc = include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/src/doc/string_enum.md"))]
417 #[derive(Clone, StringEnum)]
418 #[ruma_enum(rename_all = "snake_case")]
419 #[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
420 pub enum GrantType {
421 AuthorizationCode,
425
426 RefreshToken,
430
431 #[cfg(feature = "unstable-msc4108")]
435 #[ruma_enum(rename = "urn:ietf:params:oauth:grant-type:device_code")]
436 DeviceCode,
437
438 #[doc(hidden)]
439 _Custom(PrivOwnedStr),
440 }
441
442 #[doc = include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/src/doc/string_enum.md"))]
444 #[derive(Clone, StringEnum)]
445 #[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
446 pub enum CodeChallengeMethod {
447 S256,
451
452 #[doc(hidden)]
453 _Custom(PrivOwnedStr),
454 }
455
456 #[doc = include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/src/doc/string_enum.md"))]
462 #[derive(Clone, StringEnum)]
465 #[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
466 pub enum AccountManagementAction {
467 #[ruma_enum(rename = "org.matrix.profile")]
469 Profile,
470
471 #[ruma_enum(rename = "org.matrix.sessions_list")]
478 UnstableSessionsList,
479
480 #[ruma_enum(rename = "org.matrix.devices_list")]
482 DevicesList,
483
484 #[ruma_enum(rename = "org.matrix.session_view")]
491 UnstableSessionView,
492
493 #[ruma_enum(rename = "org.matrix.device_view")]
495 DeviceView,
496
497 #[ruma_enum(rename = "org.matrix.session_end")]
504 UnstableSessionEnd,
505
506 #[ruma_enum(rename = "org.matrix.device_delete")]
508 DeviceDelete,
509
510 #[ruma_enum(rename = "org.matrix.account_deactivate")]
512 AccountDeactivate,
513
514 #[ruma_enum(rename = "org.matrix.cross_signing_reset")]
520 CrossSigningReset,
521
522 #[doc(hidden)]
523 _Custom(PrivOwnedStr),
524 }
525
526 #[derive(Debug, Clone)]
531 #[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
532 pub enum AccountManagementActionData<'a> {
533 Profile,
535
536 DevicesList,
538
539 DeviceView(DeviceViewData<'a>),
541
542 DeviceDelete(DeviceDeleteData<'a>),
544
545 AccountDeactivate,
547
548 CrossSigningReset,
550 }
551
552 #[derive(Debug, Clone)]
554 #[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
555 pub struct DeviceViewData<'a> {
556 pub device_id: &'a DeviceId,
558 }
559
560 impl<'a> DeviceViewData<'a> {
561 pub fn new(device_id: &'a DeviceId) -> Self {
563 Self { device_id }
564 }
565 }
566
567 impl<'a> From<&'a DeviceId> for DeviceViewData<'a> {
568 fn from(device_id: &'a DeviceId) -> Self {
569 Self::new(device_id)
570 }
571 }
572
573 #[derive(Debug, Clone)]
575 #[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
576 pub struct DeviceDeleteData<'a> {
577 pub device_id: &'a DeviceId,
579 }
580
581 impl<'a> DeviceDeleteData<'a> {
582 pub fn new(device_id: &'a DeviceId) -> Self {
584 Self { device_id }
585 }
586 }
587
588 impl<'a> From<&'a DeviceId> for DeviceDeleteData<'a> {
589 fn from(device_id: &'a DeviceId) -> Self {
590 Self::new(device_id)
591 }
592 }
593
594 #[derive(Debug, Clone, thiserror::Error)]
596 #[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
597 pub enum AuthorizationServerMetadataUrlError {
598 #[error("URL in `{0}` must use the `https` scheme")]
600 NotHttpsScheme(&'static str),
601
602 #[error("URL in `issuer` cannot have a query or fragment component")]
604 IssuerHasQueryOrFragment,
605 }
606
607 #[derive(Clone, StringEnum)]
609 #[ruma_enum(rename_all = "lowercase")]
610 #[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
611 pub enum Prompt {
612 Create,
617
618 #[doc(hidden)]
619 _Custom(PrivOwnedStr),
620 }
621}
622
623#[cfg(test)]
624mod tests {
625 use ruma_common::device_id;
626 use serde_json::{Value as JsonValue, from_value as from_json_value, json};
627 use url::Url;
628
629 use super::v1::{
630 AccountManagementAction, AccountManagementActionData, AuthorizationServerMetadata,
631 };
632
633 pub(super) fn authorization_server_metadata_json() -> JsonValue {
635 json!({
636 "issuer": "https://server.local/",
637 "authorization_endpoint": "https://server.local/authorize",
638 "token_endpoint": "https://server.local/token",
639 "registration_endpoint": "https://server.local/register",
640 "response_types_supported": ["code"],
641 "response_modes_supported": ["query", "fragment"],
642 "grant_types_supported": ["authorization_code", "refresh_token"],
643 "revocation_endpoint": "https://server.local/revoke",
644 "code_challenge_methods_supported": ["S256"],
645 "account_management_uri": "https://server.local/account",
646 "account_management_actions_supported": [
647 "org.matrix.profile",
648 "org.matrix.devices_list",
649 "org.matrix.device_view",
650 "org.matrix.device_delete",
651 "org.matrix.account_deactivate",
652 "org.matrix.cross_signing_reset",
653 ],
654 "device_authorization_endpoint": "https://server.local/device",
655 })
656 }
657
658 fn authorization_server_metadata() -> AuthorizationServerMetadata {
660 from_json_value(authorization_server_metadata_json()).unwrap()
661 }
662
663 #[test]
664 fn metadata_valid_urls() {
665 let metadata = authorization_server_metadata();
666 metadata.validate_urls().unwrap();
667 metadata.insecure_validate_urls().unwrap();
668 }
669
670 #[test]
671 fn metadata_invalid_or_insecure_issuer() {
672 let original_metadata = authorization_server_metadata();
673
674 let mut metadata = original_metadata.clone();
676 metadata.issuer = Url::parse("https://server.local/?session=1er45elp").unwrap();
677 metadata.validate_urls().unwrap_err();
678 metadata.insecure_validate_urls().unwrap_err();
679
680 let mut metadata = original_metadata.clone();
682 metadata.issuer = Url::parse("https://server.local/#session").unwrap();
683 metadata.validate_urls().unwrap_err();
684 metadata.insecure_validate_urls().unwrap_err();
685
686 let mut metadata = original_metadata;
688 metadata.issuer = Url::parse("http://server.local/").unwrap();
689 metadata.validate_urls().unwrap_err();
690 metadata.insecure_validate_urls().unwrap();
691 }
692
693 #[test]
694 fn metadata_insecure_urls() {
695 let original_metadata = authorization_server_metadata();
696
697 let mut metadata = original_metadata.clone();
698 metadata.authorization_endpoint = Url::parse("http://server.local/authorize").unwrap();
699 metadata.validate_urls().unwrap_err();
700 metadata.insecure_validate_urls().unwrap();
701
702 let mut metadata = original_metadata.clone();
703 metadata.token_endpoint = Url::parse("http://server.local/token").unwrap();
704 metadata.validate_urls().unwrap_err();
705 metadata.insecure_validate_urls().unwrap();
706
707 let mut metadata = original_metadata.clone();
708 metadata.registration_endpoint = Some(Url::parse("http://server.local/register").unwrap());
709 metadata.validate_urls().unwrap_err();
710 metadata.insecure_validate_urls().unwrap();
711
712 let mut metadata = original_metadata.clone();
713 metadata.revocation_endpoint = Url::parse("http://server.local/revoke").unwrap();
714 metadata.validate_urls().unwrap_err();
715 metadata.insecure_validate_urls().unwrap();
716
717 let mut metadata = original_metadata.clone();
718 metadata.account_management_uri = Some(Url::parse("http://server.local/account").unwrap());
719 metadata.validate_urls().unwrap_err();
720 metadata.insecure_validate_urls().unwrap();
721
722 #[cfg(feature = "unstable-msc4108")]
723 {
724 let mut metadata = original_metadata.clone();
725 metadata.device_authorization_endpoint =
726 Some(Url::parse("http://server.local/device").unwrap());
727 metadata.validate_urls().unwrap_err();
728 metadata.insecure_validate_urls().unwrap();
729 }
730 }
731
732 #[test]
733 fn metadata_is_account_management_action_supported() {
734 let mut original_metadata = authorization_server_metadata();
735
736 assert!(
738 original_metadata
739 .is_account_management_action_supported(&AccountManagementAction::Profile)
740 );
741
742 original_metadata
744 .account_management_actions_supported
745 .remove(&AccountManagementAction::Profile);
746 assert!(
747 !original_metadata
748 .is_account_management_action_supported(&AccountManagementAction::Profile)
749 );
750
751 assert!(
753 original_metadata
754 .is_account_management_action_supported(&AccountManagementAction::DevicesList)
755 );
756 assert!(original_metadata.is_account_management_action_supported(
757 &AccountManagementAction::UnstableSessionsList
758 ));
759
760 original_metadata
762 .account_management_actions_supported
763 .remove(&AccountManagementAction::DevicesList);
764 assert!(
765 !original_metadata
766 .is_account_management_action_supported(&AccountManagementAction::DevicesList)
767 );
768 assert!(!original_metadata.is_account_management_action_supported(
769 &AccountManagementAction::UnstableSessionsList
770 ));
771
772 assert!(
774 original_metadata
775 .is_account_management_action_supported(&AccountManagementAction::DeviceView)
776 );
777 assert!(
778 original_metadata.is_account_management_action_supported(
779 &AccountManagementAction::UnstableSessionView
780 )
781 );
782
783 original_metadata
785 .account_management_actions_supported
786 .remove(&AccountManagementAction::DeviceView);
787 assert!(
788 !original_metadata
789 .is_account_management_action_supported(&AccountManagementAction::DeviceView)
790 );
791 assert!(
792 !original_metadata.is_account_management_action_supported(
793 &AccountManagementAction::UnstableSessionView
794 )
795 );
796
797 assert!(
799 original_metadata
800 .is_account_management_action_supported(&AccountManagementAction::DeviceDelete)
801 );
802 assert!(
803 original_metadata.is_account_management_action_supported(
804 &AccountManagementAction::UnstableSessionEnd
805 )
806 );
807
808 original_metadata
810 .account_management_actions_supported
811 .remove(&AccountManagementAction::DeviceDelete);
812 assert!(
813 !original_metadata
814 .is_account_management_action_supported(&AccountManagementAction::DeviceDelete)
815 );
816 assert!(
817 !original_metadata.is_account_management_action_supported(
818 &AccountManagementAction::UnstableSessionEnd
819 )
820 );
821 }
822
823 #[test]
824 fn metadata_account_management_url_with_action() {
825 let mut original_metadata = authorization_server_metadata();
826 let device_id = device_id!("DEVICE");
827
828 let url = original_metadata
830 .account_management_url_with_action(AccountManagementActionData::Profile)
831 .unwrap();
832 assert_eq!(url.query().unwrap(), "action=org.matrix.profile");
833
834 let url = original_metadata
836 .account_management_url_with_action(AccountManagementActionData::DevicesList)
837 .unwrap();
838 assert_eq!(url.query().unwrap(), "action=org.matrix.devices_list");
839
840 original_metadata
842 .account_management_actions_supported
843 .insert(AccountManagementAction::UnstableSessionsList);
844 let url = original_metadata
845 .account_management_url_with_action(AccountManagementActionData::DevicesList)
846 .unwrap();
847 assert_eq!(url.query().unwrap(), "action=org.matrix.devices_list");
848
849 original_metadata
851 .account_management_actions_supported
852 .remove(&AccountManagementAction::DevicesList);
853 let url = original_metadata
854 .account_management_url_with_action(AccountManagementActionData::DevicesList)
855 .unwrap();
856 assert_eq!(url.query().unwrap(), "action=org.matrix.sessions_list");
857
858 original_metadata
860 .account_management_actions_supported
861 .remove(&AccountManagementAction::UnstableSessionsList);
862 let url = original_metadata
863 .account_management_url_with_action(AccountManagementActionData::DevicesList)
864 .unwrap();
865 assert_eq!(url.query().unwrap(), "action=org.matrix.devices_list");
866
867 let url = original_metadata
869 .account_management_url_with_action(AccountManagementActionData::DeviceView(
870 device_id.into(),
871 ))
872 .unwrap();
873 assert_eq!(url.query().unwrap(), "action=org.matrix.device_view&device_id=DEVICE");
874
875 original_metadata
877 .account_management_actions_supported
878 .insert(AccountManagementAction::UnstableSessionView);
879 let url = original_metadata
880 .account_management_url_with_action(AccountManagementActionData::DeviceView(
881 device_id.into(),
882 ))
883 .unwrap();
884 assert_eq!(url.query().unwrap(), "action=org.matrix.device_view&device_id=DEVICE");
885
886 original_metadata
888 .account_management_actions_supported
889 .remove(&AccountManagementAction::DeviceView);
890 let url = original_metadata
891 .account_management_url_with_action(AccountManagementActionData::DeviceView(
892 device_id.into(),
893 ))
894 .unwrap();
895 assert_eq!(url.query().unwrap(), "action=org.matrix.session_view&device_id=DEVICE");
896
897 original_metadata
899 .account_management_actions_supported
900 .remove(&AccountManagementAction::UnstableSessionView);
901 let url = original_metadata
902 .account_management_url_with_action(AccountManagementActionData::DeviceView(
903 device_id.into(),
904 ))
905 .unwrap();
906 assert_eq!(url.query().unwrap(), "action=org.matrix.device_view&device_id=DEVICE");
907
908 let url = original_metadata
910 .account_management_url_with_action(AccountManagementActionData::DeviceDelete(
911 device_id.into(),
912 ))
913 .unwrap();
914 assert_eq!(url.query().unwrap(), "action=org.matrix.device_delete&device_id=DEVICE");
915
916 original_metadata
918 .account_management_actions_supported
919 .insert(AccountManagementAction::UnstableSessionEnd);
920 let url = original_metadata
921 .account_management_url_with_action(AccountManagementActionData::DeviceDelete(
922 device_id.into(),
923 ))
924 .unwrap();
925 assert_eq!(url.query().unwrap(), "action=org.matrix.device_delete&device_id=DEVICE");
926
927 original_metadata
929 .account_management_actions_supported
930 .remove(&AccountManagementAction::DeviceDelete);
931 let url = original_metadata
932 .account_management_url_with_action(AccountManagementActionData::DeviceDelete(
933 device_id.into(),
934 ))
935 .unwrap();
936 assert_eq!(url.query().unwrap(), "action=org.matrix.session_end&device_id=DEVICE");
937
938 original_metadata
940 .account_management_actions_supported
941 .remove(&AccountManagementAction::UnstableSessionEnd);
942 let url = original_metadata
943 .account_management_url_with_action(AccountManagementActionData::DeviceDelete(
944 device_id.into(),
945 ))
946 .unwrap();
947 assert_eq!(url.query().unwrap(), "action=org.matrix.device_delete&device_id=DEVICE");
948 }
949}