diff --git a/crates/matrix-sdk/src/test_utils/mocks/mod.rs b/crates/matrix-sdk/src/test_utils/mocks/mod.rs index 3af1d02e4..06be866b3 100644 --- a/crates/matrix-sdk/src/test_utils/mocks/mod.rs +++ b/crates/matrix-sdk/src/test_utils/mocks/mod.rs @@ -983,6 +983,28 @@ impl MatrixMockServer { let mock = Mock::given(method("GET")).and(path_regex(r"^/.well-known/matrix/client")); MockEndpoint { mock, server: &self.server, endpoint: WellKnownEndpoint } } + + /// Creates a prebuilt mock for the endpoint used to publish cross-signing + /// keys. + pub fn mock_upload_cross_signing_keys( + &self, + ) -> MockEndpoint<'_, UploadCrossSigningKeysEndpoint> { + let mock = Mock::given(method("POST")) + .and(path_regex(r"^/_matrix/client/v3/keys/device_signing/upload")) + .and(header("authorization", "Bearer 1234")); + MockEndpoint { mock, server: &self.server, endpoint: UploadCrossSigningKeysEndpoint } + } + + /// Creates a prebuilt mock for the endpoint used to publish cross-signing + /// signatures. + pub fn mock_upload_cross_signing_signatures( + &self, + ) -> MockEndpoint<'_, UploadCrossSigningSignaturesEndpoint> { + let mock = Mock::given(method("POST")) + .and(path_regex(r"^/_matrix/client/v3/keys/signatures/upload")) + .and(header("authorization", "Bearer 1234")); + MockEndpoint { mock, server: &self.server, endpoint: UploadCrossSigningSignaturesEndpoint } + } } /// Parameter to [`MatrixMockServer::sync_room`]. @@ -2446,3 +2468,46 @@ impl<'a> MockEndpoint<'a, WellKnownEndpoint> { MatrixMock { server: self.server, mock } } } + +/// A prebuilt mock for `POST /keys/device_signing/upload` request. +pub struct UploadCrossSigningKeysEndpoint; + +impl<'a> MockEndpoint<'a, UploadCrossSigningKeysEndpoint> { + /// Returns a successful empty response. + pub fn ok(self) -> MatrixMock<'a> { + let mock = self.mock.respond_with(ResponseTemplate::new(200).set_body_json(json!({}))); + + MatrixMock { server: self.server, mock } + } + + /// Returns an error response with an OAuth 2.0 UIAA stage. + #[cfg(feature = "experimental-oidc")] + pub fn uiaa_oauth(self) -> MatrixMock<'a> { + let mock = self.mock.respond_with(ResponseTemplate::new(401).set_body_json(json!({ + "session": "dummy", + "flows": [{ + "stages": [ "org.matrix.cross_signing_reset" ] + }], + "params": { + "org.matrix.cross_signing_reset": { + "url": format!("{}/account/?action=org.matrix.cross_signing_reset", self.server.uri()) + } + }, + "msg": "To reset your end-to-end encryption cross-signing identity, you first need to approve it and then try again." + }))); + + MatrixMock { server: self.server, mock } + } +} + +/// A prebuilt mock for `POST /keys/signatures/upload` request. +pub struct UploadCrossSigningSignaturesEndpoint; + +impl<'a> MockEndpoint<'a, UploadCrossSigningSignaturesEndpoint> { + /// Returns a successful empty response. + pub fn ok(self) -> MatrixMock<'a> { + let mock = self.mock.respond_with(ResponseTemplate::new(200).set_body_json(json!({}))); + + MatrixMock { server: self.server, mock } + } +} diff --git a/crates/matrix-sdk/tests/integration/encryption/cross_signing.rs b/crates/matrix-sdk/tests/integration/encryption/cross_signing.rs index f881c9e01..72f5917a0 100644 --- a/crates/matrix-sdk/tests/integration/encryption/cross_signing.rs +++ b/crates/matrix-sdk/tests/integration/encryption/cross_signing.rs @@ -117,169 +117,65 @@ async fn test_reset_legacy_auth() { #[cfg(feature = "experimental-oidc")] #[async_test] async fn test_reset_oidc() { - use std::sync::{ - atomic::{AtomicU8, Ordering}, - Arc, - }; - use assert_matches2::assert_let; - use mas_oidc_client::types::{ - iana::oauth::OAuthClientAuthenticationMethod, - registration::{ClientMetadata, VerifiedClientMetadata}, - }; - use matrix_sdk::{ - authentication::oidc::{ - registrations::ClientId, OidcSession, OidcSessionTokens, UserSession, - }, - encryption::CrossSigningResetAuthType, - }; + use matrix_sdk::{encryption::CrossSigningResetAuthType, test_utils::mocks::MatrixMockServer}; use similar_asserts::assert_eq; - use url::Url; - use wiremock::MockServer; - const CLIENT_ID: &str = "test_client_id"; - const REDIRECT_URI_STRING: &str = "http://matrix.example.com/oidc/callback"; - - let (client, server) = no_retry_test_client_with_server().await; - - pub fn mock_registered_client_data() -> (ClientId, VerifiedClientMetadata) { - ( - ClientId(CLIENT_ID.to_owned()), - ClientMetadata { - redirect_uris: Some(vec![Url::parse(REDIRECT_URI_STRING).unwrap()]), - token_endpoint_auth_method: Some(OAuthClientAuthenticationMethod::None), - ..ClientMetadata::default() - } - .validate() - .expect("validate client metadata"), - ) - } - - pub fn mock_session(tokens: OidcSessionTokens, server: &MockServer) -> OidcSession { - let (client_id, metadata) = mock_registered_client_data(); - OidcSession { - client_id, - metadata, - user: UserSession { - meta: SessionMeta { - user_id: ruma::user_id!("@u:e.uk").to_owned(), - device_id: ruma::device_id!("XYZ").to_owned(), - }, - tokens, - issuer: server.uri(), - }, - } - } - - let tokens = OidcSessionTokens { - access_token: "4cc3ss".to_owned(), - refresh_token: Some("r3fr3sh".to_owned()), - latest_id_token: None, - }; - - let session = mock_session(tokens.clone(), &server); - - client - .oidc() - .restore_session(session.clone()) - .await - .expect("We should be able to restore the OIDC session"); + let server = MatrixMockServer::new().await; + let client = server.client_builder().logged_in_with_oauth(server.server().uri()).build().await; assert!( !client.encryption().cross_signing_status().await.unwrap().is_complete(), "Initially we shouldn't have any cross-signin keys", ); - Mock::given(method("POST")) - .and(path("/_matrix/client/r0/keys/upload")) - .respond_with(ResponseTemplate::new(200).set_body_json(json!({ - "one_time_key_counts": { - "signed_curve25519": 50 - } - }))) + server.mock_upload_keys().ok().expect(1).named("Initial device keys upload").mount().await; + + // Return the UIAA response 5 times. + server + .mock_upload_cross_signing_keys() + .uiaa_oauth() + .up_to_n_times(5) + .expect(5) + .named("Trying to upload the cross-signing keys with UIAA response") + .mount() + .await; + + // And finally succeed. + // This works because the first mocked endpoint that matches the path is used + // until it is invalidated by `up_to_n_times`. + server + .mock_upload_cross_signing_keys() + .ok() .expect(1) - .named("Initial device keys upload") - .mount(&server) + .named("Succeeding to upload the cross-signing keys") + .mount() .await; - let uiaa_response_body = json!({ - "session": "dummy", - "flows": [{ - "stages": [ "org.matrix.cross_signing_reset" ] - }], - "params": { - "org.matrix.cross_signing_reset": { - "url": format!("{}/account/?action=org.matrix.cross_signing_reset", server.uri()) - } - }, - "msg": "To reset your end-to-end encryption cross-signing identity, you first need to approve it and then try again." - }); - - let reset_handle = { - let _guard = Mock::given(method("POST")) - .and(path("/_matrix/client/unstable/keys/device_signing/upload")) - .respond_with(ResponseTemplate::new(401).set_body_json(uiaa_response_body.clone())) - .expect(1) - .named("Initial cross-signing upload attempt") - .mount_as_scoped(&server) - .await; - - let handle = client - .encryption() - .reset_cross_signing() - .await - .unwrap() - .expect("We should have received a reset handle"); - - assert_let!(CrossSigningResetAuthType::Oidc(oidc_info) = handle.auth_type()); - assert_eq!( - oidc_info.approval_url.as_str(), - format!("{}/account/?action=org.matrix.cross_signing_reset", server.uri()) - ); - - handle - }; - - let counter = Arc::new(AtomicU8::default()); - - Mock::given(method("POST")) - .and(path("/_matrix/client/unstable/keys/device_signing/upload")) - .respond_with({ - let counter = counter.clone(); - - move |_: &wiremock::Request| { - let current_value = counter.fetch_add(1, Ordering::SeqCst); - - println!("Hello {current_value}"); - // Only allow us to proceed on the 5th attempt, count started at 0, so if the - // current value is at 4 it's the 5th attempt. - if current_value >= 4 { - ResponseTemplate::new(200).set_body_json(json!({})) - } else { - ResponseTemplate::new(401).set_body_json(uiaa_response_body.clone()) - } - } - }) - .expect(1..) - .named("Retrying to upload the cross-signing keys") - .mount(&server) - .await; - - Mock::given(method("POST")) - .and(path("/_matrix/client/unstable/keys/signatures/upload")) - .respond_with(move |_: &wiremock::Request| { - ResponseTemplate::new(200).set_body_json(json!({})) - }) + server + .mock_upload_cross_signing_signatures() + .ok() .expect(1) .named("Final signatures upload") - .mount(&server) + .mount() .await; - reset_handle.auth(None).await.expect("We should be able to reset the cross-signing keys after some attempts, waiting for the auth issue to allow us to upload"); + // First requests gives us a reset handle. + let reset_handle = client + .encryption() + .reset_cross_signing() + .await + .unwrap() + .expect("We should have received a reset handle"); - // 5 because we incremented the counter once more in the request handler - // closure. - assert_eq!(counter.load(Ordering::SeqCst), 5); + assert_let!(CrossSigningResetAuthType::Oidc(oidc_info) = reset_handle.auth_type()); + assert_eq!( + oidc_info.approval_url.as_str(), + format!("{}/account/?action=org.matrix.cross_signing_reset", server.server().uri()) + ); + + // Then it retries until it succeeds. + reset_handle.auth(None).await.expect("We should be able to reset the cross-signing keys after some attempts, waiting for the auth issue to allow us to upload"); assert!( client.encryption().cross_signing_status().await.unwrap().is_complete(),