Skip to content

Commit e82cfaa

Browse files
authored
fix: incorrect e2ee toggle behavior on Teams creation modal and private/broadcast toggling (RocketChat#36797)
1 parent d832f91 commit e82cfaa

12 files changed

Lines changed: 287 additions & 75 deletions

File tree

.changeset/rare-walls-press.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
'@rocket.chat/i18n': patch
3+
'@rocket.chat/meteor': patch
4+
---
5+
6+
Fixes an issue where the encryption toggle was incorrectly reset/disabled/enabled in the Teams creation modal when Broadcast or Private was toggled, or when the user lacked unrelated permissions.
Lines changed: 258 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,258 @@
1+
import { mockAppRoot } from '@rocket.chat/mock-providers';
2+
import { render, screen } from '@testing-library/react';
3+
import userEvent from '@testing-library/user-event';
4+
5+
import CreateTeamModal from './CreateTeamModal';
6+
import CreateTeamModalOld from '../../../sidebar/header/CreateTeam';
7+
8+
jest.mock('../../../lib/utils/goToRoomById', () => ({
9+
goToRoomById: jest.fn(),
10+
}));
11+
12+
type CreateTeamModalComponentType = typeof CreateTeamModal | typeof CreateTeamModalOld;
13+
// eslint-disable-next-line @typescript-eslint/naming-convention
14+
15+
describe.each([
16+
['CreateTeamModal', CreateTeamModalOld],
17+
['CreateTeamModal in NavbarV2', CreateTeamModal],
18+
] as const)(
19+
'%s',
20+
// eslint-disable-next-line @typescript-eslint/naming-convention
21+
(_name: string, CreateTeamModalComponent: CreateTeamModalComponentType) => {
22+
it('should render with encryption option disabled and set to off when E2E_Enable=false and E2E_Enabled_Default_PrivateRooms=false', async () => {
23+
render(<CreateTeamModalComponent onClose={() => null} />, {
24+
wrapper: mockAppRoot().withSetting('E2E_Enable', false).withSetting('E2E_Enabled_Default_PrivateRooms', false).build(),
25+
});
26+
27+
await userEvent.click(screen.getByText('Advanced_settings'));
28+
29+
const encrypted = screen.getByLabelText('Teams_New_Encrypted_Label') as HTMLInputElement;
30+
expect(encrypted).toBeInTheDocument();
31+
expect(encrypted).not.toBeChecked();
32+
expect(encrypted).toBeDisabled();
33+
});
34+
35+
it('should render with encryption option enabled and set to off when E2E_Enable=true and E2E_Enabled_Default_PrivateRooms=false', async () => {
36+
render(<CreateTeamModalComponent onClose={() => null} />, {
37+
wrapper: mockAppRoot().withSetting('E2E_Enable', true).withSetting('E2E_Enabled_Default_PrivateRooms', false).build(),
38+
});
39+
40+
await userEvent.click(screen.getByText('Advanced_settings'));
41+
42+
const encrypted = screen.getByLabelText('Teams_New_Encrypted_Label') as HTMLInputElement;
43+
expect(encrypted).toBeInTheDocument();
44+
expect(encrypted).not.toBeChecked();
45+
expect(encrypted).toBeEnabled();
46+
});
47+
48+
it('should render with encryption option disabled and set to off when E2E_Enable=false and E2E_Enabled_Default_PrivateRooms=true', async () => {
49+
render(<CreateTeamModalComponent onClose={() => null} />, {
50+
wrapper: mockAppRoot().withSetting('E2E_Enable', false).withSetting('E2E_Enabled_Default_PrivateRooms', true).build(),
51+
});
52+
53+
await userEvent.click(screen.getByText('Advanced_settings'));
54+
55+
const encrypted = screen.getByLabelText('Teams_New_Encrypted_Label') as HTMLInputElement;
56+
expect(encrypted).toBeInTheDocument();
57+
58+
expect(encrypted).not.toBeChecked();
59+
expect(encrypted).toBeDisabled();
60+
});
61+
62+
it('should render with encryption option enabled and set to on when E2E_Enable=true and E2E_Enabled_Default_PrivateRooms=True', async () => {
63+
render(<CreateTeamModalComponent onClose={() => null} />, {
64+
wrapper: mockAppRoot().withSetting('E2E_Enable', true).withSetting('E2E_Enabled_Default_PrivateRooms', true).build(),
65+
});
66+
67+
await userEvent.click(screen.getByText('Advanced_settings'));
68+
69+
const encrypted = screen.getByLabelText('Teams_New_Encrypted_Label') as HTMLInputElement;
70+
expect(encrypted).toBeChecked();
71+
expect(encrypted).toBeEnabled();
72+
});
73+
74+
it('when Private goes ON → OFF: forces Encrypted OFF and disables it (E2E_Enable=true, E2E_Enabled_Default_PrivateRooms=true)', async () => {
75+
render(<CreateTeamModalComponent onClose={() => null} />, {
76+
wrapper: mockAppRoot().withSetting('E2E_Enable', true).withSetting('E2E_Enabled_Default_PrivateRooms', true).build(),
77+
});
78+
79+
await userEvent.click(screen.getByText('Advanced_settings'));
80+
81+
const encrypted = screen.getByLabelText('Teams_New_Encrypted_Label') as HTMLInputElement;
82+
const priv = screen.getByLabelText('Teams_New_Private_Label') as HTMLInputElement;
83+
84+
// initial: private=true, encrypted ON and enabled
85+
expect(priv).toBeChecked();
86+
expect(encrypted).toBeChecked();
87+
expect(encrypted).toBeEnabled();
88+
89+
// Private ON -> OFF: encrypted must become OFF and disabled
90+
await userEvent.click(priv);
91+
expect(priv).not.toBeChecked();
92+
expect(encrypted).not.toBeChecked();
93+
expect(encrypted).toBeDisabled();
94+
});
95+
96+
it('when Private goes OFF → ON: keeps Encrypted OFF but re-enables it (E2E_Enable=true, E2E_Enabled_Default_PrivateRooms=true)', async () => {
97+
render(<CreateTeamModalComponent onClose={() => null} />, {
98+
wrapper: mockAppRoot().withSetting('E2E_Enable', true).withSetting('E2E_Enabled_Default_PrivateRooms', true).build(),
99+
});
100+
101+
await userEvent.click(screen.getByText('Advanced_settings'));
102+
103+
const encrypted = screen.getByLabelText('Teams_New_Encrypted_Label') as HTMLInputElement;
104+
const priv = screen.getByLabelText('Teams_New_Private_Label') as HTMLInputElement;
105+
106+
// turn private OFF to simulate user path from non-private
107+
await userEvent.click(priv);
108+
expect(priv).not.toBeChecked();
109+
expect(encrypted).not.toBeChecked();
110+
expect(encrypted).toBeDisabled();
111+
112+
// turn private back ON -> encrypted should remain OFF but become enabled
113+
await userEvent.click(priv);
114+
expect(priv).toBeChecked();
115+
expect(encrypted).not.toBeChecked();
116+
expect(encrypted).toBeEnabled();
117+
});
118+
119+
it('private team: toggling Broadcast on/off does not change or disable Encrypted', async () => {
120+
render(<CreateTeamModalComponent onClose={() => null} />, {
121+
wrapper: mockAppRoot().withSetting('E2E_Enable', true).withSetting('E2E_Enabled_Default_PrivateRooms', true).build(),
122+
});
123+
124+
await userEvent.click(screen.getByText('Advanced_settings'));
125+
126+
const encrypted = screen.getByLabelText('Teams_New_Encrypted_Label') as HTMLInputElement;
127+
const broadcast = screen.getByLabelText('Teams_New_Broadcast_Label') as HTMLInputElement;
128+
const priv = screen.getByLabelText('Teams_New_Private_Label') as HTMLInputElement;
129+
130+
expect(priv).toBeChecked();
131+
expect(encrypted).toBeChecked();
132+
expect(encrypted).toBeEnabled();
133+
expect(broadcast).not.toBeChecked();
134+
135+
// Broadcast: OFF -> ON (Encrypted unchanged + enabled)
136+
await userEvent.click(broadcast);
137+
expect(broadcast).toBeChecked();
138+
expect(encrypted).toBeChecked();
139+
expect(encrypted).toBeEnabled();
140+
141+
// Broadcast: ON -> OFF (Encrypted unchanged + enabled)
142+
await userEvent.click(broadcast);
143+
expect(broadcast).not.toBeChecked();
144+
expect(encrypted).toBeChecked();
145+
expect(encrypted).toBeEnabled();
146+
147+
// User can still toggle Encrypted freely while Broadcast is OFF
148+
await userEvent.click(encrypted);
149+
expect(encrypted).not.toBeChecked();
150+
151+
// User can still toggle Encrypted freely while Broadcast is ON
152+
await userEvent.click(broadcast);
153+
expect(broadcast).toBeChecked();
154+
expect(encrypted).not.toBeChecked();
155+
expect(encrypted).toBeEnabled();
156+
});
157+
158+
it('non-private team: Encrypted remains OFF and disabled regardless of Broadcast state', async () => {
159+
render(<CreateTeamModalComponent onClose={() => null} />, {
160+
wrapper: mockAppRoot().withSetting('E2E_Enable', true).withSetting('E2E_Enabled_Default_PrivateRooms', true).build(),
161+
});
162+
163+
await userEvent.click(screen.getByText('Advanced_settings'));
164+
165+
const encrypted = screen.getByLabelText('Teams_New_Encrypted_Label') as HTMLInputElement;
166+
const broadcast = screen.getByLabelText('Teams_New_Broadcast_Label') as HTMLInputElement;
167+
const priv = screen.getByLabelText('Teams_New_Private_Label') as HTMLInputElement;
168+
169+
// Switch to non-private
170+
await userEvent.click(priv);
171+
expect(priv).not.toBeChecked();
172+
173+
// Encrypted must be OFF + disabled (non-private cannot be encrypted)
174+
expect(encrypted).not.toBeChecked();
175+
expect(encrypted).toBeDisabled();
176+
177+
// Broadcast: OFF -> ON (Encrypted stays OFF + disabled)
178+
await userEvent.click(broadcast);
179+
expect(broadcast).toBeChecked();
180+
expect(encrypted).not.toBeChecked();
181+
expect(encrypted).toBeDisabled();
182+
183+
// Broadcast: ON -> OFF (Encrypted still OFF + disabled)
184+
await userEvent.click(broadcast);
185+
expect(broadcast).not.toBeChecked();
186+
expect(encrypted).not.toBeChecked();
187+
expect(encrypted).toBeDisabled();
188+
});
189+
190+
it('should disable and turn on ReadOnly toggle when Broadcast is ON and no set-readonly permission', async () => {
191+
render(<CreateTeamModalComponent onClose={() => null} />, {
192+
wrapper: mockAppRoot().build(),
193+
});
194+
195+
await userEvent.click(screen.getByText('Advanced_settings'));
196+
197+
const broadcast = screen.getByLabelText('Teams_New_Broadcast_Label') as HTMLInputElement;
198+
const readOnly = screen.getByLabelText('Teams_New_Read_only_Label') as HTMLInputElement;
199+
200+
expect(readOnly).not.toBeChecked();
201+
202+
// Broadcast: OFF -> ON (ReadOnly stays ON + disabled)
203+
await userEvent.click(broadcast);
204+
expect(broadcast).toBeChecked();
205+
expect(readOnly).toBeChecked();
206+
expect(readOnly).toBeDisabled();
207+
});
208+
209+
it('should disable and turn on ReadOnly toggle when Broadcast is ON with set-readonly permission', async () => {
210+
render(<CreateTeamModalComponent onClose={() => null} />, {
211+
wrapper: mockAppRoot().withPermission('set-readonly').build(),
212+
});
213+
214+
await userEvent.click(screen.getByText('Advanced_settings'));
215+
216+
const broadcast = screen.getByLabelText('Teams_New_Broadcast_Label') as HTMLInputElement;
217+
const readOnly = screen.getByLabelText('Teams_New_Read_only_Label') as HTMLInputElement;
218+
219+
expect(readOnly).not.toBeChecked();
220+
221+
// Broadcast: OFF -> ON (ReadOnly stays ON + disabled)
222+
await userEvent.click(broadcast);
223+
expect(broadcast).toBeChecked();
224+
expect(readOnly).toBeChecked();
225+
expect(readOnly).toBeDisabled();
226+
});
227+
228+
it('should disable and turn off ReadOnly toggle when Broadcast is OFF with no set-readonly permission', async () => {
229+
render(<CreateTeamModalComponent onClose={() => null} />, {
230+
wrapper: mockAppRoot().build(),
231+
});
232+
233+
await userEvent.click(screen.getByText('Advanced_settings'));
234+
235+
const broadcast = screen.getByLabelText('Teams_New_Broadcast_Label') as HTMLInputElement;
236+
const readOnly = screen.getByLabelText('Teams_New_Read_only_Label') as HTMLInputElement;
237+
238+
expect(broadcast).not.toBeChecked();
239+
expect(readOnly).not.toBeChecked();
240+
expect(readOnly).toBeDisabled();
241+
});
242+
243+
it('should enable ReadOnly toggle when Broadcast is OFF with set-readonly permission', async () => {
244+
render(<CreateTeamModalComponent onClose={() => null} />, {
245+
wrapper: mockAppRoot().withPermission('set-readonly').build(),
246+
});
247+
248+
await userEvent.click(screen.getByText('Advanced_settings'));
249+
250+
const broadcast = screen.getByLabelText('Teams_New_Broadcast_Label') as HTMLInputElement;
251+
const readOnly = screen.getByLabelText('Teams_New_Read_only_Label') as HTMLInputElement;
252+
253+
expect(broadcast).not.toBeChecked();
254+
expect(readOnly).not.toBeChecked();
255+
expect(readOnly).toBeEnabled();
256+
});
257+
},
258+
);

apps/meteor/client/NavBarV2/NavBarPagesGroup/actions/CreateTeamModal.tsx

Lines changed: 7 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -53,13 +53,13 @@ type CreateTeamModalProps = { onClose: () => void };
5353
const CreateTeamModal = ({ onClose }: CreateTeamModalProps) => {
5454
const t = useTranslation();
5555
const e2eEnabled = useSetting('E2E_Enable');
56-
const e2eEnabledForPrivateByDefault = useSetting('E2E_Enabled_Default_PrivateRooms');
56+
const e2eEnabledForPrivateByDefault = useSetting('E2E_Enabled_Default_PrivateRooms') && e2eEnabled;
5757
const namesValidation = useSetting('UTF8_Channel_Names_Validation');
5858
const allowSpecialNames = useSetting('UI_Allow_room_names_with_special_chars');
59+
const canSetReadOnly = usePermissionWithScopedRoles('set-readonly', ['owner']);
5960

6061
const dispatchToastMessage = useToastMessageDispatch();
6162
const canCreateTeam = usePermission('create-team');
62-
const canSetReadOnly = usePermissionWithScopedRoles('set-readonly', ['owner']);
6363

6464
const checkTeamNameExists = useEndpoint('GET', '/v1/rooms.nameExists');
6565
const createTeamAction = useEndpoint('POST', '/v1/teams.create');
@@ -113,15 +113,11 @@ const CreateTeamModal = ({ onClose }: CreateTeamModalProps) => {
113113
setValue('encrypted', false);
114114
}
115115

116-
if (broadcast) {
117-
setValue('encrypted', false);
118-
}
119-
120116
setValue('readOnly', broadcast);
121117
}, [watch, setValue, broadcast, isPrivate]);
122118

123-
const canChangeReadOnly = !broadcast;
124-
const canChangeEncrypted = isPrivate && !broadcast && e2eEnabled && !e2eEnabledForPrivateByDefault;
119+
const readOnlyDisabled = broadcast || !canSetReadOnly;
120+
const canChangeEncrypted = isPrivate && e2eEnabled;
125121
const getEncryptedHint = useEncryptedRoomDescription('team');
126122

127123
const handleCreateTeam = async ({
@@ -265,7 +261,7 @@ const CreateTeamModal = ({ onClose }: CreateTeamModalProps) => {
265261
render={({ field: { onChange, value, ref } }): ReactElement => (
266262
<ToggleSwitch
267263
id={encryptedId}
268-
disabled={!canSetReadOnly || !canChangeEncrypted}
264+
disabled={!canChangeEncrypted}
269265
onChange={onChange}
270266
aria-describedby={`${encryptedId}-hint`}
271267
checked={value}
@@ -274,7 +270,7 @@ const CreateTeamModal = ({ onClose }: CreateTeamModalProps) => {
274270
)}
275271
/>
276272
</FieldRow>
277-
<FieldDescription id={`${encryptedId}-hint`}>{getEncryptedHint({ isPrivate, broadcast, encrypted })}</FieldDescription>
273+
<FieldDescription id={`${encryptedId}-hint`}>{getEncryptedHint({ isPrivate, encrypted })}</FieldDescription>
278274
</Field>
279275
<Field>
280276
<FieldRow>
@@ -286,7 +282,7 @@ const CreateTeamModal = ({ onClose }: CreateTeamModalProps) => {
286282
<ToggleSwitch
287283
id={readOnlyId}
288284
aria-describedby={`${readOnlyId}-hint`}
289-
disabled={!canChangeReadOnly}
285+
disabled={readOnlyDisabled}
290286
onChange={onChange}
291287
checked={value}
292288
ref={ref}

apps/meteor/client/NavBarV2/NavBarPagesGroup/actions/useEncryptedRoomDescription.spec.tsx

Lines changed: 7 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -23,65 +23,34 @@ describe.each([
2323
});
2424
const describe = result.current;
2525

26-
expect(describe({ isPrivate: true, broadcast: false, encrypted: true })).toBe('Not_available_for_this_workspace');
26+
expect(describe({ isPrivate: true, encrypted: true })).toBe('Not_available_for_this_workspace');
2727
});
2828

29-
it('returns "Encrypted_not_available" when room is not private', () => {
29+
it('returns "Encrypted_not_available" when room is not private and E2E is enabled', () => {
3030
const { result } = renderHook(() => useEncryptedRoomDescriptionHook(roomType), {
3131
wrapper: wrapper.withSetting('E2E_Enable', true).build(),
3232
});
3333
const describe = result.current;
3434

35-
expect(describe({ isPrivate: false, broadcast: false, encrypted: false })).toBe(`Encrypted_not_available`);
35+
expect(describe({ isPrivate: false, encrypted: false })).toBe('Encrypted_not_available');
3636
});
3737

38-
it('returns "Not_available_for_broadcast" when broadcast=true (even if encrypted is true)', () => {
38+
it('returns "Encrypted_messages" when private and encrypted are true and E2E is enabled', () => {
3939
const { result } = renderHook(() => useEncryptedRoomDescriptionHook(roomType), {
4040
wrapper: wrapper.withSetting('E2E_Enable', true).build(),
4141
});
4242
const describe = result.current;
4343

44-
expect(describe({ isPrivate: true, broadcast: true, encrypted: true })).toBe(`Not_available_for_broadcast`);
45-
46-
expect(describe({ isPrivate: true, broadcast: true, encrypted: false })).toBe(`Not_available_for_broadcast`);
47-
});
48-
49-
it('returns "Encrypted_messages" when private, not broadcast, and encrypted is true', () => {
50-
const { result } = renderHook(() => useEncryptedRoomDescriptionHook(roomType), {
51-
wrapper: wrapper.withSetting('E2E_Enable', true).build(),
52-
});
53-
const describe = result.current;
54-
55-
expect(describe({ isPrivate: true, broadcast: false, encrypted: true })).toBe(`Encrypted_messages`);
44+
expect(describe({ isPrivate: true, encrypted: true })).toBe('Encrypted_messages');
5645
});
5746

58-
it('returns "Encrypted_messages_false" when private, not broadcast, and encrypted is false', () => {
47+
it('returns "Encrypted_messages_false" when private and encrypted are false and E2E is enabled', () => {
5948
const { result } = renderHook(() => useEncryptedRoomDescriptionHook(roomType), {
6049
wrapper: wrapper.withSetting('E2E_Enable', true).build(),
6150
});
6251
const describe = result.current;
6352

64-
expect(describe({ isPrivate: true, broadcast: false, encrypted: false })).toBe('Encrypted_messages_false');
65-
});
66-
67-
describe('when broadcast is undefined', () => {
68-
it('returns "Encrypted_messages" if private and encrypted is true and broadcast is undefined', () => {
69-
const { result } = renderHook(() => useEncryptedRoomDescriptionHook(roomType), {
70-
wrapper: wrapper.withSetting('E2E_Enable', true).build(),
71-
});
72-
const describe = result.current;
73-
74-
expect(describe({ isPrivate: true, encrypted: true })).toBe(`Encrypted_messages`);
75-
});
76-
77-
it('returns "Encrypted_messages_false" if private and encrypted is false and broadcast is undefined', () => {
78-
const { result } = renderHook(() => useEncryptedRoomDescriptionHook(roomType), {
79-
wrapper: wrapper.withSetting('E2E_Enable', true).build(),
80-
});
81-
const describe = result.current;
82-
83-
expect(describe({ isPrivate: true, encrypted: false })).toBe('Encrypted_messages_false');
84-
});
53+
expect(describe({ isPrivate: true, encrypted: false })).toBe('Encrypted_messages_false');
8554
});
8655
});
8756
});

0 commit comments

Comments
 (0)