Repository: matrix-org/matrix-rust-sdk
Pull request #5072: refactor: when leaving an `Invited` room also forget it
Author: login: jmartinesp
Created: 2025-05-22T07:53:10Z | Merged: 2025-05-22T10:07:19Z

This behaviour was added only at the `RoomPreview::leave` method, but since we're slowly moving away from it we should move the forget action to the `Room::leave` method instead.

<!-- description of the changes in this PR -->

- [ ] Public API changes documented in changelogs (optional)

<!-- Sign-off, if not part of the commits -->
<!-- See CONTRIBUTING.md if you don't know what this is -->
Signed-off-by: 



Diff:
diff --git a/bindings/matrix-sdk-ffi/src/room_preview.rs b/bindings/matrix-sdk-ffi/src/room_preview.rs
index a3ad6f306d7..15ae11be137 100644
--- a/bindings/matrix-sdk-ffi/src/room_preview.rs
+++ b/bindings/matrix-sdk-ffi/src/room_preview.rs
@@ -59,15 +59,7 @@ impl RoomPreview {
         let room =
             self.client.get_room(&self.inner.room_id).context("missing room for a room preview")?;
 
-        let should_forget = matches!(room.state(), matrix_sdk::RoomState::Invited);
-
-        room.leave().await.map_err(ClientError::from)?;
-
-        if should_forget {
-            _ = self.forget().await;
-        }
-
-        Ok(())
+        Ok(room.leave().await?)
     }
 
     /// Get the user who created the invite, if any.
diff --git a/crates/matrix-sdk/src/room/mod.rs b/crates/matrix-sdk/src/room/mod.rs
index 463c1d9be15..b92ec90d102 100644
--- a/crates/matrix-sdk/src/room/mod.rs
+++ b/crates/matrix-sdk/src/room/mod.rs
@@ -230,6 +230,8 @@ impl Room {
     }
 
     /// Leave this room.
+    /// If the room was in [`RoomState::Invited`] state, it'll also be forgotten
+    /// automatically.
     ///
     /// Only invited and joined rooms can be left.
     #[doc(alias = "reject_invitation")]
@@ -242,9 +244,20 @@ impl Room {
             ))));
         }
 
+        // If the room was in Invited state we should also forget it when declining the
+        // invite.
+        let should_forget = matches!(self.state(), RoomState::Invited);
+
         let request = leave_room::v3::Request::new(self.inner.room_id().to_owned());
         self.client.send(request).await?;
         self.client.base_client().room_left(self.room_id()).await?;
+
+        if should_forget {
+            if let Err(error) = self.forget().await {
+                warn!("Failed to forget room when leaving it: {error}");
+            }
+        }
+
         Ok(())
     }
 
diff --git a/crates/matrix-sdk/src/test_utils/mocks/mod.rs b/crates/matrix-sdk/src/test_utils/mocks/mod.rs
index f8ef49c5f64..f6df02e3ec3 100644
--- a/crates/matrix-sdk/src/test_utils/mocks/mod.rs
+++ b/crates/matrix-sdk/src/test_utils/mocks/mod.rs
@@ -996,6 +996,13 @@ impl MatrixMockServer {
         self.mock_endpoint(mock, RoomLeaveEndpoint).expect_default_access_token()
     }
 
+    /// Creates a prebuilt mock for the endpoint used to forget a room.
+    pub fn mock_room_forget(&self) -> MockEndpoint<'_, RoomForgetEndpoint> {
+        let mock =
+            Mock::given(method("POST")).and(path_regex(r"^/_matrix/client/v3/rooms/.*/forget"));
+        self.mock_endpoint(mock, RoomForgetEndpoint).expect_default_access_token()
+    }
+
     /// Create a prebuilt mock for the endpoint use to log out a session.
     pub fn mock_logout(&self) -> MockEndpoint<'_, LogoutEndpoint> {
         let mock = Mock::given(method("POST")).and(path("/_matrix/client/v3/logout"));
@@ -2639,6 +2646,17 @@ impl<'a> MockEndpoint<'a, RoomLeaveEndpoint> {
     }
 }
 
+/// A prebuilt mock for the room forget endpoint.
+pub struct RoomForgetEndpoint;
+
+impl<'a> MockEndpoint<'a, RoomForgetEndpoint> {
+    /// Returns a successful response with some default data for the given room
+    /// id.
+    pub fn ok(self) -> MatrixMock<'a> {
+        self.respond_with(ResponseTemplate::new(200).set_body_json(json!({})))
+    }
+}
+
 /// A prebuilt mock for `POST /logout` request.
 pub struct LogoutEndpoint;
 
diff --git a/crates/matrix-sdk/tests/integration/room/joined.rs b/crates/matrix-sdk/tests/integration/room/joined.rs
index f14a0cb0810..62b543fabe4 100644
--- a/crates/matrix-sdk/tests/integration/room/joined.rs
+++ b/crates/matrix-sdk/tests/integration/room/joined.rs
@@ -19,8 +19,8 @@ use matrix_sdk_test::{
     event_factory::EventFactory,
     mocks::mock_encryption_state,
     test_json::{self, sync::CUSTOM_ROOM_POWER_LEVELS},
-    GlobalAccountDataTestEvent, JoinedRoomBuilder, RoomAccountDataTestEvent, StateTestEvent,
-    SyncResponseBuilder, DEFAULT_TEST_ROOM_ID,
+    GlobalAccountDataTestEvent, InvitedRoomBuilder, JoinedRoomBuilder, RoomAccountDataTestEvent,
+    StateTestEvent, SyncResponseBuilder, DEFAULT_TEST_ROOM_ID,
 };
 use ruma::{
     api::client::{membership::Invite3pidInit, receipt::create_receipt::v3::ReceiptType},
@@ -126,6 +126,52 @@ async fn test_leave_room() -> Result<(), anyhow::Error> {
     Ok(())
 }
 
+#[async_test]
+async fn test_leave_joined_room_does_not_forget_it() -> Result<(), anyhow::Error> {
+    let server = MatrixMockServer::new().await;
+    let client = server.client_builder().build().await;
+    let room_id = *DEFAULT_TEST_ROOM_ID;
+
+    server.mock_room_leave().ok(room_id).mock_once().mount().await;
+    // The forget endpoint should never be called
+    server.mock_room_forget().ok().never().mount().await;
+
+    let room = server.sync_joined_room(&client, room_id).await;
+
+    room.leave().await?;
+
+    assert_eq!(room.state(), RoomState::Left);
+
+    // The left room is still around and in the right state
+    let left_room = client.get_room(room_id);
+    assert!(left_room.is_some());
+    assert_eq!(left_room.unwrap().state(), RoomState::Left);
+
+    Ok(())
+}
+
+#[async_test]
+async fn test_leave_invited_room_also_forgets_it() -> Result<(), anyhow::Error> {
+    let server = MatrixMockServer::new().await;
+    let client = server.client_builder().build().await;
+    let room_id = *DEFAULT_TEST_ROOM_ID;
+
+    server.mock_room_leave().ok(room_id).mock_once().mount().await;
+    server.mock_room_forget().ok().mock_once().mount().await;
+
+    let invited_room_builder = InvitedRoomBuilder::new(room_id);
+    let room = server.sync_room(&client, invited_room_builder).await;
+
+    room.leave().await?;
+
+    assert_eq!(room.state(), RoomState::Left);
+
+    let forgotten_room = client.get_room(room_id);
+    assert!(forgotten_room.is_none());
+
+    Ok(())
+}
+
 #[async_test]
 async fn test_ban_user() {
     let (client, server) = logged_in_client_with_server().await;


Reviews:
user: login: poljar
body: Makes sense.
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-05-22T08:08:37Z
user: login: andybalaam
body: Looks good, one light suggestion.
state: APPROVED
author_association: MEMBER
submitted_at: 2025-05-22T09:46:04Z
user: login: jmartinesp
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-22T09:53:01Z

Review comments:
diff_hunk: @@ -126,6 +126,28 @@ async fn test_leave_room() -> Result<(), anyhow::Error> {
     Ok(())
 }
 
+#[async_test]
+async fn test_leave_invited_room_also_forgets_it() -> Result<(), anyhow::Error> {
path: crates/matrix-sdk/tests/integration/room/joined.rs
user: login: andybalaam
body: Can we test that we _don't_ forget a room that we were joined?
created_at: 2025-05-22T09:45:54Z
author_association: MEMBER
diff_hunk: @@ -126,6 +126,28 @@ async fn test_leave_room() -> Result<(), anyhow::Error> {
     Ok(())
 }
 
+#[async_test]
+async fn test_leave_invited_room_also_forgets_it() -> Result<(), anyhow::Error> {
path: crates/matrix-sdk/tests/integration/room/joined.rs
user: login: jmartinesp
body: Done and force pushed the new test.
created_at: 2025-05-22T09:53:01Z
author_association: CONTRIBUTOR

PR comments:
user: login: jmartinesp
created_at: 2025-05-22T09:35:19Z
body: And now that GH actions work codecov got stuck again 🥲 
author_association: CONTRIBUTOR
user: login: codecov[bot]
created_at: 2025-05-22T10:07:20Z
body: ## [Codecov](https://app.codecov.io/gh/matrix-org/matrix-rust-sdk/pull/5072?dropdown=coverage&src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=matrix-org) Report
All modified and coverable lines are covered by tests :white_check_mark:
> Project coverage is 85.78%. Comparing base [(`1348525`)](https://app.codecov.io/gh/matrix-org/matrix-rust-sdk/commit/1348525447e99cb27ddca2e23885d9bab3837297?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=matrix-org) to head [(`7d0ceb5`)](https://app.codecov.io/gh/matrix-org/matrix-rust-sdk/commit/7d0ceb51680abe8d4bcc4ceedd94bd5c90c842a2?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=matrix-org).
> Report is 8 commits behind head on main.

<details><summary>Additional details and impacted files</summary>


```diff
@@           Coverage Diff           @@
##             main    #5072   +/-   ##
=======================================
  Coverage   85.77%   85.78%           
=======================================
  Files         332      332           
  Lines       36052    36061    +9     
=======================================
+ Hits        30925    30936   +11     
+ Misses       5127     5125    -2     
```

</details>

[:umbrella: View full report in Codecov by Sentry](https://app.codecov.io/gh/matrix-org/matrix-rust-sdk/pull/5072?dropdown=coverage&src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=matrix-org).   
:loudspeaker: Have feedback on the report? [Share it here](https://about.codecov.io/codecov-pr-comment-feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=matrix-org).

<details><summary> :rocket: New features to boost your workflow: </summary>

- :snowflake: [Test Analytics](https://docs.codecov.com/docs/test-analytics): Detect flaky tests, report on failures, and find test suite problems.
</details>
author_association: NONE