Skip to content

Conversation

@tiagoevanp
Copy link
Contributor

@tiagoevanp tiagoevanp commented Aug 27, 2025

Proposed changes (including videos or screenshots)

  • This PR addresses a bug where a team becomes orphaned if the last team owner is deleted. The patch ensures that such teams are handled preventing orphaning altogether.
  • Adds a deletedRooms field to the users.delete endpoint response, indicating which rooms were deleted as part of the user deletion process.
  • Replaces direct room deletions with a controlled internal function to ensure cleanup and app event propagation when relinquishing room ownerships.

Issue(s)

Steps to test or reproduce

Further comments

SUP-836

CORE-1432

Summary by CodeRabbit

  • Bug Fixes

    • Teams no longer become orphaned when their last owner is deleted.
  • New Features

    • Coordinated team deletion that safely removes team rooms, unassigns remaining rooms, clears memberships, and deletes the team.
    • Bulk room cleanup now returns a list of deleted room IDs.
    • User deletion API returns deletedRooms in the response.
  • Tests

    • Added end-to-end and unit tests for team deletion and room cleanup flows.
  • Chores

    • Changeset and changelog entries added for package bumps.

✏️ Tip: You can customize this high-level summary in your review settings.

@dionisio-bot
Copy link
Contributor

dionisio-bot bot commented Aug 27, 2025

Looks like this PR is ready to merge! 🎉
If you have any trouble, please check the PR guidelines

@changeset-bot
Copy link

changeset-bot bot commented Aug 27, 2025

🦋 Changeset detected

Latest commit: 937a0df

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 41 packages
Name Type
@rocket.chat/core-services Minor
@rocket.chat/meteor Minor
@rocket.chat/account-service Patch
@rocket.chat/authorization-service Patch
@rocket.chat/ddp-streamer Patch
@rocket.chat/omnichannel-transcript Patch
@rocket.chat/presence-service Patch
@rocket.chat/queue-worker Patch
@rocket.chat/stream-hub-service Patch
@rocket.chat/federation-matrix Patch
@rocket.chat/network-broker Patch
@rocket.chat/omni-core-ee Patch
@rocket.chat/omnichannel-services Patch
@rocket.chat/presence Patch
rocketchat-services Patch
@rocket.chat/core-typings Minor
@rocket.chat/rest-typings Minor
@rocket.chat/uikit-playground Patch
@rocket.chat/api-client Patch
@rocket.chat/apps Patch
@rocket.chat/cron Patch
@rocket.chat/ddp-client Patch
@rocket.chat/freeswitch Patch
@rocket.chat/fuselage-ui-kit Major
@rocket.chat/gazzodown Major
@rocket.chat/http-router Patch
@rocket.chat/livechat Patch
@rocket.chat/model-typings Patch
@rocket.chat/ui-avatar Major
@rocket.chat/ui-client Major
@rocket.chat/ui-contexts Major
@rocket.chat/web-ui-registration Major
@rocket.chat/license Patch
@rocket.chat/media-calls Patch
@rocket.chat/pdf-worker Patch
@rocket.chat/models Patch
@rocket.chat/mock-providers Patch
@rocket.chat/ui-video-conf Major
@rocket.chat/ui-voip Major
@rocket.chat/instance-status Patch
@rocket.chat/omni-core Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@codecov
Copy link

codecov bot commented Aug 27, 2025

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 68.91%. Comparing base (7f1b834) to head (937a0df).
⚠️ Report is 1 commits behind head on develop.

Additional details and impacted files

Impacted file tree graph

@@             Coverage Diff             @@
##           develop   #36807      +/-   ##
===========================================
- Coverage    68.94%   68.91%   -0.03%     
===========================================
  Files         3360     3360              
  Lines       114282   114325      +43     
  Branches     20562    20684     +122     
===========================================
- Hits         78790    78787       -3     
- Misses       33406    33449      +43     
- Partials      2086     2089       +3     
Flag Coverage Δ
e2e 57.46% <ø> (+0.01%) ⬆️
e2e-api 42.49% <100.00%> (-1.01%) ⬇️

Flags with carried forward coverage won't be shown. Click here to find out more.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@tiagoevanp tiagoevanp marked this pull request as ready for review August 27, 2025 02:55
@tiagoevanp tiagoevanp requested a review from a team as a code owner August 27, 2025 02:55
@tiagoevanp tiagoevanp added this to the 7.11.0 milestone Aug 28, 2025
@scuciatto scuciatto removed this from the 7.11.0 milestone Sep 19, 2025
@abhinavkrin abhinavkrin force-pushed the fix/users.delete-last-owner-team branch from f09fc07 to 3a6c005 Compare October 7, 2025 04:53
@coderabbitai
Copy link
Contributor

coderabbitai bot commented Oct 7, 2025

Walkthrough

Adds a reusable team/room erasure module and tests; integrates it into team deletion and relinquish-ownership flows; surfaces deleted room IDs from user-deletion and relinquish paths; changes unsetTeamIdOfRooms signature to accept user/team objects and become async; adds end-to-end tests and changeset entries.

Changes

Cohort / File(s) Summary
Team Erasure Orchestration
apps/meteor/app/api/server/lib/eraseTeam.ts
New module exporting eraseTeamShared, eraseTeam, eraseTeamOnRelinquishRoomOwnerships, and eraseRoomLooseValidation to coordinate room deletions, call Apps hooks, unset team refs, remove memberships, and delete team entities.
Teams API
apps/meteor/app/api/server/v1/teams.ts
Replaced inline multi-step cleanup in convertToChannel and delete endpoints with calls to eraseTeam(...).
User deletion workflow
apps/meteor/app/lib/server/functions/deleteUser.ts, apps/meteor/app/api/server/v1/users.ts
deleteUser now returns { deletedRooms: string[] }; the users.delete route forwards { deletedRooms } in API responses.
Relinquish ownerships workflow
apps/meteor/app/lib/server/functions/relinquishRoomOwnerships.ts
Added bulkTeamCleanup using eraseTeamOnRelinquishRoomOwnerships; bulkRoomCleanUp/relinquishRoomOwnerships now aggregate and return deleted room IDs (string[]) and use eraseRoomLooseValidation.
Team service API / types
apps/meteor/server/services/team/service.ts, packages/core-services/src/types/ITeamService.ts
unsetTeamIdOfRooms signature changed from (uid: string, teamId: string) to `(user: AtLeast<IUser, '_id'
Tests (unit & e2e)
apps/meteor/app/api/server/lib/eraseTeam.spec.ts, apps/meteor/tests/end-to-end/api/teams.ts
Added unit tests (proxyquire + sinon) covering eraseTeam behaviors and e2e tests that create a temp team, link channels, delete including main room, and assert outcomes.
Changesets
.changeset/stale-sloths-smoke.md, .changeset/brave-socks-battle.md
Added changelog entries: package bumps and exposing deletedRooms from user deletion.

Sequence Diagram(s)

sequenceDiagram
    autonumber
    participant Client
    participant API as REST API
    participant Erase as eraseTeam module
    participant TeamSvc as Team Service
    participant RoomSvc as Room Service
    participant Apps as Apps Bridge

    Client->>API: DELETE /v1/teams/:id (roomsToRemove)
    API->>Erase: eraseTeam(user, team, roomsToRemove)
    Erase->>TeamSvc: getMatchingTeamRooms(team)
    TeamSvc-->>Erase: matchingRooms
    loop each non-main room
        Erase->>RoomSvc: eraseRoomFn(rid)
        RoomSvc->>Apps: onPreDelete(room)
        Apps-->>RoomSvc: allow/deny
        alt allowed
            RoomSvc->>RoomSvc: deleteRoom(rid)
            RoomSvc->>Apps: onPostDelete(room)
            RoomSvc-->>Erase: success (rid)
        else denied or failed
            RoomSvc-->>Erase: failure (skip)
        end
    end
    Erase->>TeamSvc: unsetTeamIdOfRooms(user, remainingRooms)
    Erase->>TeamSvc: removeAllMembersFromTeam(team)
    Erase->>TeamSvc: deleteTeam(team)
    Erase-->>API: { deletedRooms }
    API-->>Client: 200 { deletedRooms }
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

  • Pay special attention to:
    • eraseTeam orchestration: ordering, error handling, and Apps pre/post-delete interactions.
    • Return-type ripples: deleteUser, relinquishRoomOwnerships, and API responses.
    • Service interface change: unsetTeamIdOfRooms signature and downstream callers.
    • New tests: proxyquire stubs and e2e test stability.

Possibly related PRs

Suggested labels

stat: ready to merge

Suggested reviewers

  • pierre-lehnen-rc
  • dougfabris

Poem

🐇 I hopped through code with nimble feet,
Unlinked the burrows tidy and neat.
I nudged closed doors and counted the rooms,
Left no orphaned corners in quiet blooms.
A hop, a fix — the burrow resumes.

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly identifies the main issue being fixed: orphaned teams when the last owner is deleted.
Linked Issues check ✅ Passed The PR addresses all core requirements from SUP-836 and CORE-1432: prevents team orphaning with proper room/team deletion orchestration and replaces direct DB deletions with controlled internal functions that trigger app events.
Out of Scope Changes check ✅ Passed All changes directly support the linked objectives: eraseTeam module orchestrates safe deletion, deleteUser tracks deletedRooms, relinquishRoomOwnerships uses controlled deletion functions, and API returns deleted room IDs.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch fix/users.delete-last-owner-team

📜 Recent review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

Disabled knowledge base sources:

  • Jira integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 75af2b7 and 6732e19.

📒 Files selected for processing (1)
  • apps/meteor/app/api/server/v1/users.ts (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • apps/meteor/app/api/server/v1/users.ts
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (3)
  • GitHub Check: 📦 Build Packages
  • GitHub Check: CodeQL-Build
  • GitHub Check: CodeQL-Build

Tip

📝 Customizable high-level summaries are now available in beta!

You can now customize how CodeRabbit generates the high-level summary in your pull requests — including its content, structure, tone, and formatting.

  • Provide your own instructions using the high_level_summary_instructions setting.
  • Format the summary however you like (bullet lists, tables, multi-section layouts, contributor stats, etc.).
  • Use high_level_summary_in_walkthrough to move the summary from the description to the walkthrough section.

Example instruction:

"Divide the high-level summary into five sections:

  1. 📝 Description — Summarize the main change in 50–60 words, explaining what was done.
  2. 📓 References — List relevant issues, discussions, documentation, or related PRs.
  3. 📦 Dependencies & Requirements — Mention any new/updated dependencies, environment variable changes, or configuration updates.
  4. 📊 Contributor Summary — Include a Markdown table showing contributions:
    | Contributor | Lines Added | Lines Removed | Files Changed |
  5. ✔️ Additional Notes — Add any extra reviewer context.
    Keep each section concise (under 200 words) and use bullet or numbered lists for clarity."

Note: This feature is currently in beta for Pro-tier users, and pricing will be announced later.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

Disabled knowledge base sources:

  • Jira integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 46f8110 and 3a6c005.

📒 Files selected for processing (6)
  • .changeset/stale-sloths-smoke.md (1 hunks)
  • apps/meteor/app/api/server/lib/eraseTeam.ts (1 hunks)
  • apps/meteor/app/api/server/v1/teams.ts (3 hunks)
  • apps/meteor/app/lib/server/functions/deleteUser.ts (1 hunks)
  • apps/meteor/app/lib/server/functions/relinquishRoomOwnerships.ts (2 hunks)
  • apps/meteor/server/services/team/service.ts (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (4)
apps/meteor/app/lib/server/functions/deleteUser.ts (1)
apps/meteor/app/lib/server/functions/relinquishRoomOwnerships.ts (1)
  • relinquishRoomOwnerships (58-82)
apps/meteor/app/api/server/lib/eraseTeam.ts (2)
packages/core-typings/src/IUser.ts (1)
  • IUser (186-255)
packages/core-typings/src/IRoom.ts (1)
  • IRoom (21-95)
apps/meteor/app/lib/server/functions/relinquishRoomOwnerships.ts (5)
packages/core-typings/src/IRoom.ts (1)
  • IRoom (21-95)
apps/meteor/app/api/server/lib/eraseTeam.ts (1)
  • eraseTeam (6-27)
apps/meteor/app/file-upload/server/lib/FileUpload.ts (1)
  • FileUpload (110-681)
apps/meteor/app/lib/server/lib/notifyListener.ts (1)
  • notifyOnSubscriptionChanged (515-519)
packages/core-typings/src/IUser.ts (1)
  • IUser (186-255)
apps/meteor/app/api/server/v1/teams.ts (1)
apps/meteor/app/api/server/lib/eraseTeam.ts (1)
  • eraseTeam (6-27)
🪛 Biome (2.1.2)
apps/meteor/app/lib/server/functions/relinquishRoomOwnerships.ts

[error] 18-32: Promise executor functions should not be async.

(lint/suspicious/noAsyncPromiseExecutor)

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (7)
  • GitHub Check: 🔎 Code Check / Code Lint
  • GitHub Check: 🔨 Test Unit / Unit Tests
  • GitHub Check: 🔨 Test Storybook / Test Storybook
  • GitHub Check: 🔎 Code Check / TypeScript
  • GitHub Check: 📦 Meteor Build - coverage
  • GitHub Check: CodeQL-Build
  • GitHub Check: CodeQL-Build

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

Disabled knowledge base sources:

  • Jira integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 3a6c005 and 6710c62.

📒 Files selected for processing (2)
  • apps/meteor/app/api/server/lib/eraseTeam.ts (1 hunks)
  • apps/meteor/tests/end-to-end/api/teams.ts (2 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • apps/meteor/app/api/server/lib/eraseTeam.ts
🧰 Additional context used
🧬 Code graph analysis (1)
apps/meteor/tests/end-to-end/api/teams.ts (3)
packages/core-typings/src/IRoom.ts (1)
  • IRoom (21-95)
apps/meteor/tests/data/api-data.ts (2)
  • request (10-10)
  • credentials (39-42)
apps/meteor/tests/e2e/utils/create-target-channel.ts (1)
  • deleteRoom (48-50)
🪛 Biome (2.1.2)
apps/meteor/tests/end-to-end/api/teams.ts

[error] 1509-1526: Disallow duplicate setup and teardown hooks.

Disallow before duplicacy inside the describe function.

(lint/suspicious/noDuplicateTestHooks)


[error] 1528-1544: Disallow duplicate setup and teardown hooks.

Disallow before duplicacy inside the describe function.

(lint/suspicious/noDuplicateTestHooks)


[error] 1546-1563: Disallow duplicate setup and teardown hooks.

Disallow before duplicacy inside the describe function.

(lint/suspicious/noDuplicateTestHooks)


[error] 1565-1581: Disallow duplicate setup and teardown hooks.

Disallow before duplicacy inside the describe function.

(lint/suspicious/noDuplicateTestHooks)

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (4)
  • GitHub Check: 🔨 Test Storybook / Test Storybook
  • GitHub Check: 🔎 Code Check / Code Lint
  • GitHub Check: CodeQL-Build
  • GitHub Check: CodeQL-Build

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

♻️ Duplicate comments (1)
apps/meteor/app/lib/server/functions/relinquishRoomOwnerships.ts (1)

10-31: Critical: Team main rooms deleted twice, causing potential errors.

Team main rooms undergo double deletion:

  1. eraseTeam(userId, team, []) at Line 28 calls eraseRoom(team.roomId, userId) (see eraseTeam.ts Line 17), which deletes the team's main room.
  2. Rooms.removeByIds(rids) at Line 48 attempts to delete the same room again, since team main rooms are included in rids.

Depending on how eraseRoom or Rooms.removeByIds handle non-existent rooms, this could throw errors or cause inconsistent state.

Solution: Exclude team main rooms from the final Rooms.removeByIds(rids) call, since they are already handled by eraseTeam.

Apply this diff:

 const bulkRoomCleanUp = async (rids: string[], userId?: string) => {
 	// no bulk deletion for files
 	await Promise.all(rids.map((rid) => FileUpload.removeFilesByRoomId(rid)));
 
+	const rooms = userId ? await Rooms.findByIds(rids, { projection: { _id: 1, teamMain: 1 } }).toArray() : [];
+	const teamMainRoomIds = new Set(rooms.filter((room) => room.teamMain).map((room) => room._id));
+
 	await Promise.all([
 		Subscriptions.removeByRoomIds(rids, {
 			async onTrash(doc) {
 				void notifyOnSubscriptionChanged(doc, 'removed');
 			},
 		}),
 		Messages.removeByRoomIds(rids),
 		ReadReceipts.removeByRoomIds(rids),
 		...[userId && bulkTeamCleanup(rids, userId)],
 	]);
 
-	await Rooms.removeByIds(rids);
+	// Exclude team main rooms as they are already deleted by eraseTeam
+	const roomsToDelete = rids.filter((rid) => !teamMainRoomIds.has(rid));
+	if (roomsToDelete.length > 0) {
+		await Rooms.removeByIds(roomsToDelete);
+	}
 };
🧹 Nitpick comments (2)
apps/meteor/app/lib/server/functions/relinquishRoomOwnerships.ts (2)

19-21: Move userId validation before Promise.all for efficiency.

The userId validation is executed inside the map callback for every team ID. Since userId is constant across all iterations, validate it once before entering Promise.all to avoid redundant checks.

Apply this diff:

 const bulkTeamCleanup = async (rids: IRoom['_id'][], userId: string) => {
 	const rooms = await Rooms.findByIds(rids).toArray();
 
 	const teamsToRemove = rooms.filter((room) => room.teamMain);
 	const teamIds = teamsToRemove.map((room) => room.teamId).filter((teamId) => teamId !== undefined);
 	const uniqueTeamIds = [...new Set(teamIds)];
+
+	if (!userId) {
+		throw new Error('error-user-not-found');
+	}
 
 	await Promise.all(
 		uniqueTeamIds.map(async (teamId) => {
-			if (!userId) {
-				throw new Error('error-user-not-found');
-			}
-
 			const team = await Team.findOneById(teamId);
 			if (!team) {
 				throw new Error('error-team-not-found');
 			}
 
 			await eraseTeam(userId, team, []);
 		}),
 	);
 };

45-45: Prefer cleaner conditional promise pattern.

The spread pattern ...[userId && bulkTeamCleanup(rids, userId)] works (Promise.all handles the false value when userId is falsy), but is not idiomatic.

Apply this diff for clearer intent:

 	await Promise.all([
 		Subscriptions.removeByRoomIds(rids, {
 			async onTrash(doc) {
 				void notifyOnSubscriptionChanged(doc, 'removed');
 			},
 		}),
 		Messages.removeByRoomIds(rids),
 		ReadReceipts.removeByRoomIds(rids),
-		...[userId && bulkTeamCleanup(rids, userId)],
+		...(userId ? [bulkTeamCleanup(rids, userId)] : []),
 	]);
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

Disabled knowledge base sources:

  • Jira integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 6710c62 and a6728a0.

📒 Files selected for processing (1)
  • apps/meteor/app/lib/server/functions/relinquishRoomOwnerships.ts (2 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
apps/meteor/app/lib/server/functions/relinquishRoomOwnerships.ts (5)
packages/core-typings/src/IRoom.ts (1)
  • IRoom (21-95)
apps/meteor/app/api/server/lib/eraseTeam.ts (1)
  • eraseTeam (6-29)
apps/meteor/app/file-upload/server/lib/FileUpload.ts (1)
  • FileUpload (110-681)
apps/meteor/app/lib/server/lib/notifyListener.ts (1)
  • notifyOnSubscriptionChanged (515-519)
packages/core-typings/src/IUser.ts (1)
  • IUser (186-255)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (3)
  • GitHub Check: 📦 Build Packages
  • GitHub Check: CodeQL-Build
  • GitHub Check: CodeQL-Build
🔇 Additional comments (1)
apps/meteor/app/lib/server/functions/relinquishRoomOwnerships.ts (1)

51-56: LGTM: Clean signature extension with proper propagation.

The optional deletedBy parameter is correctly added and propagated to bulkRoomCleanUp, enabling context-aware team cleanup during user deletion. The parameter naming clearly conveys intent.

Also applies to: 72-72

@abhinavkrin
Copy link
Member

#36790 (comment)

Please add a test to the teams.delete endpoint as well, with the roomsToRemove param including the team's main room (in-between others). I don't know if this scenario should successfully delete the team or not, but I suspect the behavior may have changed by this PR so an additional test for it is warranted.

The current behavior first deletes the rooms listed in roomsToRemove, and then deletes the team’s main room.

If the team’s main room ID is included in roomsToRemove, it gets deleted in the first step, causing an error when the code later attempts to delete the same room (team's main room) again or calls Team.unsetTeamIdOfRooms().

To fix this, I’ll filter out the team’s main room ID from roomsToRemove before performing the deletions. It will eventually be deleted in the second step. I’ll also add appropriate tests to cover this scenario.

The fix related to this should in the pr #36807

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

♻️ Duplicate comments (1)
apps/meteor/app/lib/server/functions/relinquishRoomOwnerships.ts (1)

44-44: Verify the double-deletion flow is safe.

Based on past review discussions, there's a known issue where the team's main room may be deleted twice:

  1. First by eraseTeam (called within bulkTeamCleanup at line 24, which internally calls eraseRoom for the main room)
  2. Then by Rooms.removeByIds(rids) at line 44 if the main room ID is in rids

Per previous comments, this is being tracked separately in CORE-1432 and Rooms.removeByIds is expected to be idempotent (won't fail on non-existent IDs). However, since eraseRoom "does a lot more than simply delete from mongo" (past comment), please confirm this flow is safe and won't cause cascading issues.

🧹 Nitpick comments (1)
apps/meteor/app/lib/server/functions/relinquishRoomOwnerships.ts (1)

41-41: Prefer explicit ternary for conditional Promise inclusion.

The pattern ...[userId && bulkTeamCleanup(rids, userId)] works but relies on spreading a falsy value into the array when userId is undefined. A more explicit and readable approach:

-		...[userId && bulkTeamCleanup(rids, userId)],
+		...(userId ? [bulkTeamCleanup(rids, userId)] : []),

This makes the intent clearer: conditionally add the Promise to the array only when userId is defined.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

Disabled knowledge base sources:

  • Jira integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 8761a80 and e9d7a27.

📒 Files selected for processing (1)
  • apps/meteor/app/lib/server/functions/relinquishRoomOwnerships.ts (2 hunks)
🧰 Additional context used
🧠 Learnings (2)
📚 Learning: 2025-09-25T09:59:26.461Z
Learnt from: Dnouv
PR: RocketChat/Rocket.Chat#37057
File: packages/apps-engine/src/definition/accessors/IUserRead.ts:23-27
Timestamp: 2025-09-25T09:59:26.461Z
Learning: AppUserBridge.getUserRoomIds in apps/meteor/app/apps/server/bridges/users.ts always returns an array of strings (mapping subscription documents to room IDs), never undefined, even when user has no room subscriptions.

Applied to files:

  • apps/meteor/app/lib/server/functions/relinquishRoomOwnerships.ts
📚 Learning: 2025-09-25T09:59:26.461Z
Learnt from: Dnouv
PR: RocketChat/Rocket.Chat#37057
File: packages/apps-engine/src/definition/accessors/IUserRead.ts:23-27
Timestamp: 2025-09-25T09:59:26.461Z
Learning: AppUserBridge.getUserRoomIds in apps/meteor/app/apps/server/bridges/users.ts always returns an array of strings by mapping subscription documents to room IDs, never undefined, even when user has no room subscriptions.

Applied to files:

  • apps/meteor/app/lib/server/functions/relinquishRoomOwnerships.ts
🧬 Code graph analysis (1)
apps/meteor/app/lib/server/functions/relinquishRoomOwnerships.ts (3)
packages/core-typings/src/IRoom.ts (1)
  • IRoom (21-95)
apps/meteor/app/api/server/lib/eraseTeam.ts (1)
  • eraseTeam (6-25)
apps/meteor/app/file-upload/server/lib/FileUpload.ts (1)
  • FileUpload (110-681)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (3)
  • GitHub Check: 📦 Build Packages
  • GitHub Check: CodeQL-Build
  • GitHub Check: CodeQL-Build
🔇 Additional comments (2)
apps/meteor/app/lib/server/functions/relinquishRoomOwnerships.ts (2)

10-27: LGTM! Good defensive programming with early return.

The implementation correctly:

  • Deduplicates team IDs to avoid redundant eraseTeam calls
  • Returns early if a team is not found (line 21) rather than throwing, which is appropriate since teams may have been deleted already or could be stale references
  • Uses the proper Promise.all with map(async ...) pattern

The empty array passed to eraseTeam at line 24 is intentional: it triggers deletion of the team's main room and team entity while preserving (un-setting teamId from) other team rooms, which is the correct behavior when the last team owner is being removed.


47-52: LGTM! Deletion context now propagates correctly.

The new deletedBy parameter enables the user deletion context to flow through to bulkRoomCleanUp and ultimately trigger team cleanup via bulkTeamCleanup. This is the key fix for preventing orphaned teams when the last owner is deleted.

The type IUser['_id'] is consistent with the userId: string parameter in bulkRoomCleanUp, and the optional nature ensures backward compatibility.

Also applies to: 68-68

@dougfabris dougfabris added this to the 7.12.0 milestone Oct 15, 2025
@scuciatto scuciatto modified the milestones: 7.14.0, 7.13.0 Nov 19, 2025
Signed-off-by: Abhinav Kumar <abhinav@avitechlab.com>
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

🧹 Nitpick comments (2)
apps/meteor/app/lib/server/functions/relinquishRoomOwnerships.ts (2)

10-33: Tighten typing and aggregation in bulkTeamCleanup.

The overall flow (identify teamMain rooms, dedupe teamIds, call eraseTeamOnRelinquishRoomOwnerships, aggregate deleted IDs) looks correct and aligns with the PR goals. Two small improvements:

  1. teamIds still has a potential undefined in its type even after filter((teamId) => teamId !== undefined). To keep TypeScript happy and avoid accidentally querying _id: undefined, consider a type‑guard filter:
const teamIds = teamsToRemove
	.map((room) => room.teamId)
	.filter((teamId): teamId is string => typeof teamId === 'string');
  1. Since deletedRoomIds is populated from multiple async branches and may also contain rooms beyond the original rids, using a Set<string> here (mirroring eraseTeamOnRelinquishRoomOwnerships) would avoid any accidental duplication:
const deletedRoomIds = new Set<string>();

await Promise.all(
	uniqueTeamIds.map(async (teamId) => {
		const team = await Team.findOneById(teamId);
		if (!team) {
			return;
		}

		const ids = await eraseTeamOnRelinquishRoomOwnerships(team, []);
		ids.forEach((id) => deletedRoomIds.add(id));
	}),
);

return [...deletedRoomIds];

35-61: Verify cleanup for rooms deleted only via team erasure and consider minor optimization.

The new bulkRoomCleanUp correctly delegates team‑owned rooms to bulkTeamCleanup first and then runs eraseRoomLooseValidation on the remaining room IDs, returning all deleted IDs. Two follow‑ups:

  1. Rooms deleted indirectly via eraseTeamOnRelinquishRoomOwnerships (i.e., team rooms not present in the original rids) will not go through the top‑level FileUpload.removeFilesByRoomId(rid) call on Line 37. This is fine if deleteRoom (invoked by eraseRoomLooseValidation) already takes care of uploads for those rooms; otherwise, uploads for those extra team rooms could be left behind.

    Please confirm whether deleteRoom fully handles file cleanup for any room it deletes. If it does not, you likely need to either:

    • call FileUpload.removeFilesByRoomId inside the eraseTeamOnRelinquishRoomOwnerships callback for each rid it deletes, or
    • after bulkTeamCleanup, invoke removeFilesByRoomId for any deletedRoomIds that are not already in the original rids.
  2. Minor: restRidsToRemove is computed via deletedRoomIds.includes(rid), which is O(n²) in the worst case. Since membership is all you need there, you can snapshot the team‑deleted IDs into a Set for that filter:

const deletedByTeam = new Set(deletedRoomIds);
const restRidsToRemove = rids.filter((rid) => !deletedByTeam.has(rid));

The rest of the logic, including returning deletedRoomIds so callers can surface deletedRooms, looks good.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

Disabled knowledge base sources:

  • Jira integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 7df48ca and 9e44567.

📒 Files selected for processing (1)
  • apps/meteor/app/lib/server/functions/relinquishRoomOwnerships.ts (2 hunks)
🧰 Additional context used
🧠 Learnings (7)
📚 Learning: 2025-09-25T09:59:26.461Z
Learnt from: Dnouv
Repo: RocketChat/Rocket.Chat PR: 37057
File: packages/apps-engine/src/definition/accessors/IUserRead.ts:23-27
Timestamp: 2025-09-25T09:59:26.461Z
Learning: AppUserBridge.getUserRoomIds in apps/meteor/app/apps/server/bridges/users.ts always returns an array of strings by mapping subscription documents to room IDs, never undefined, even when user has no room subscriptions.

Applied to files:

  • apps/meteor/app/lib/server/functions/relinquishRoomOwnerships.ts
📚 Learning: 2025-09-25T09:59:26.461Z
Learnt from: Dnouv
Repo: RocketChat/Rocket.Chat PR: 37057
File: packages/apps-engine/src/definition/accessors/IUserRead.ts:23-27
Timestamp: 2025-09-25T09:59:26.461Z
Learning: AppUserBridge.getUserRoomIds in apps/meteor/app/apps/server/bridges/users.ts always returns an array of strings (mapping subscription documents to room IDs), never undefined, even when user has no room subscriptions.

Applied to files:

  • apps/meteor/app/lib/server/functions/relinquishRoomOwnerships.ts
📚 Learning: 2025-10-28T16:53:42.761Z
Learnt from: ricardogarim
Repo: RocketChat/Rocket.Chat PR: 37205
File: ee/packages/federation-matrix/src/FederationMatrix.ts:296-301
Timestamp: 2025-10-28T16:53:42.761Z
Learning: In the Rocket.Chat federation-matrix integration (ee/packages/federation-matrix/), the createRoom method from rocket.chat/federation-sdk will support a 4-argument signature (userId, roomName, visibility, displayName) in newer versions. Code using this 4-argument call is forward-compatible with planned library updates and should not be flagged as an error.

Applied to files:

  • apps/meteor/app/lib/server/functions/relinquishRoomOwnerships.ts
📚 Learning: 2025-09-25T09:59:26.461Z
Learnt from: Dnouv
Repo: RocketChat/Rocket.Chat PR: 37057
File: packages/apps-engine/src/definition/accessors/IUserRead.ts:23-27
Timestamp: 2025-09-25T09:59:26.461Z
Learning: UserBridge.doGetUserRoomIds in packages/apps-engine/src/server/bridges/UserBridge.ts has a bug where it implicitly returns undefined when the app lacks read permission (missing return statement in the else case of the permission check).

Applied to files:

  • apps/meteor/app/lib/server/functions/relinquishRoomOwnerships.ts
📚 Learning: 2025-10-24T17:32:05.348Z
Learnt from: KevLehman
Repo: RocketChat/Rocket.Chat PR: 37299
File: apps/meteor/ee/server/lib/ldap/Manager.ts:438-454
Timestamp: 2025-10-24T17:32:05.348Z
Learning: In Rocket.Chat, ABAC attributes can only be set on private rooms and teams (type 'p'), not on public rooms (type 'c'). Therefore, when checking for ABAC-protected rooms/teams during LDAP sync or similar operations, it's sufficient to query only private rooms using methods like `findPrivateRoomsByIdsWithAbacAttributes`.

Applied to files:

  • apps/meteor/app/lib/server/functions/relinquishRoomOwnerships.ts
📚 Learning: 2025-09-23T00:27:05.438Z
Learnt from: pierre-lehnen-rc
Repo: RocketChat/Rocket.Chat PR: 36718
File: packages/media-signaling/src/lib/Call.ts:633-642
Timestamp: 2025-09-23T00:27:05.438Z
Learning: In PR #36718, pierre-lehnen-rc prefers to maintain consistency with the old architecture patterns for DTMF handling rather than implementing immediate validation improvements, deferring enhancements to future work.

Applied to files:

  • apps/meteor/app/lib/server/functions/relinquishRoomOwnerships.ts
📚 Learning: 2025-09-16T13:33:49.237Z
Learnt from: cardoso
Repo: RocketChat/Rocket.Chat PR: 36890
File: apps/meteor/tests/e2e/e2e-encryption/e2ee-otr.spec.ts:21-26
Timestamp: 2025-09-16T13:33:49.237Z
Learning: In Rocket.Chat test files, the im.delete API endpoint accepts either a `roomId` parameter (requiring the actual DM room _id) or a `username` parameter (for the DM partner's username). It does not accept slug-like constructions such as concatenating usernames together.

Applied to files:

  • apps/meteor/app/lib/server/functions/relinquishRoomOwnerships.ts
🧬 Code graph analysis (1)
apps/meteor/app/lib/server/functions/relinquishRoomOwnerships.ts (4)
packages/core-typings/src/IRoom.ts (1)
  • IRoom (21-95)
apps/meteor/app/api/server/lib/eraseTeam.ts (2)
  • eraseTeamOnRelinquishRoomOwnerships (55-64)
  • eraseRoomLooseValidation (66-96)
apps/meteor/app/file-upload/server/lib/FileUpload.ts (1)
  • FileUpload (110-681)
apps/meteor/app/lib/server/lib/notifyListener.ts (1)
  • notifyOnSubscriptionChanged (515-519)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (5)
  • GitHub Check: 📦 Build Packages
  • GitHub Check: CodeQL-Build
  • GitHub Check: ⚙️ Variables Setup
  • GitHub Check: CodeQL-Build
  • GitHub Check: CodeQL-Build
🔇 Additional comments (1)
apps/meteor/app/lib/server/functions/relinquishRoomOwnerships.ts (1)

63-84: All callsites properly handle the new string[] return.

Verification confirms both callsites handle the return value correctly:

  • deleteUser.ts (line 68): Captures the return value into the deletedRooms variable and includes it in the function's return object ({ deletedRooms }), fully consistent with the new API.
  • setUserActiveStatus.ts (line 97): Return value is intentionally not captured; the call is within Promise.allSettled() where the caller only cares that the operation completes.

No other callsites exist.

Signed-off-by: Abhinav Kumar <abhinav@avitechlab.com>
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

Disabled knowledge base sources:

  • Jira integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 9e44567 and 75af2b7.

📒 Files selected for processing (1)
  • apps/meteor/app/api/server/lib/eraseTeam.spec.ts (1 hunks)
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: 2025-10-06T20:30:45.540Z
Learnt from: d-gubert
Repo: RocketChat/Rocket.Chat PR: 37152
File: packages/apps-engine/tests/test-data/storage/storage.ts:101-122
Timestamp: 2025-10-06T20:30:45.540Z
Learning: In `packages/apps-engine/tests/test-data/storage/storage.ts`, the stub methods (updatePartialAndReturnDocument, updateStatus, updateSetting, updateAppInfo, updateMarketplaceInfo) intentionally throw "Method not implemented." Tests using these methods must stub them using `SpyOn` from the test library rather than relying on actual implementations.

Applied to files:

  • apps/meteor/app/api/server/lib/eraseTeam.spec.ts
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (3)
  • GitHub Check: 📦 Build Packages
  • GitHub Check: CodeQL-Build
  • GitHub Check: CodeQL-Build
🔇 Additional comments (3)
apps/meteor/app/api/server/lib/eraseTeam.spec.ts (3)

1-92: LGTM! Comprehensive test scaffolding.

The test setup properly uses proxyquire with noCallThru() to isolate dependencies, creates comprehensive stubs for all external dependencies (Team, Users, Rooms, loggers, Apps bridges), and includes proper cleanup in afterEach.


121-133: LGTM! Test correctly verifies team main room erasure.

The test properly validates that eraseTeam delegates the team's main room deletion to eraseRoom via eraseTeamShared, and verifies the correct arguments are passed.


163-271: LGTM! Comprehensive coverage of eraseRoomLooseValidation scenarios.

The tests thoroughly cover all edge cases:

  • Missing rooms return false
  • Federated rooms return false
  • App pre-delete prevention returns false with proper event handling
  • Deletion failures log errors and return false
  • Successful deletions trigger post-delete events and return true

The multiple proxyquire invocations in this test suite (lines 202, 225, 253) are justified because each test requires different Apps bridge configurations (varying isLoaded states and listener behaviors).

The test at lines 242-270 correctly verifies that roomEventStub is called twice—once for the pre-delete event (returning false to allow deletion) and once for the post-delete event.

@abhinavkrin abhinavkrin dismissed pierre-lehnen-rc’s stale review November 19, 2025 20:01

Got reviewed from Lucas, Pierre on leave. I have added additional tests for the refactor.

@abhinavkrin abhinavkrin added the stat: QA assured Means it has been tested and approved by a company insider label Nov 19, 2025
@dionisio-bot dionisio-bot bot added the stat: ready to merge PR tested and approved waiting for merge label Nov 19, 2025
@dionisio-bot dionisio-bot bot removed the stat: ready to merge PR tested and approved waiting for merge label Nov 19, 2025
@abhinavkrin abhinavkrin added stat: QA assured Means it has been tested and approved by a company insider and removed stat: QA assured Means it has been tested and approved by a company insider labels Nov 20, 2025
@dionisio-bot dionisio-bot bot added the stat: ready to merge PR tested and approved waiting for merge label Nov 20, 2025
@kodiakhq kodiakhq bot merged commit 8596daf into develop Nov 20, 2025
191 of 203 checks passed
@kodiakhq kodiakhq bot deleted the fix/users.delete-last-owner-team branch November 20, 2025 11:48
@coderabbitai coderabbitai bot mentioned this pull request Dec 15, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

stat: QA assured Means it has been tested and approved by a company insider stat: ready to merge PR tested and approved waiting for merge

Projects

None yet

Development

Successfully merging this pull request may close these issues.

7 participants