@@ -8,7 +8,14 @@ use crate::terminal::TerminalView;
88use crate :: terminal:: shared_session:: manager:: Manager ;
99use crate :: terminal:: shared_session:: { SharedSessionSource , SharedSessionStatus } ;
1010use crate :: test_util:: add_window_with_terminal;
11- use crate :: test_util:: terminal:: initialize_app_for_terminal_view;
11+ use crate :: test_util:: terminal:: {
12+ add_window_with_id_and_terminal, initialize_app_for_terminal_view,
13+ } ;
14+ use crate :: workspaces:: team:: { Team , TeamVisibility } ;
15+ use crate :: workspaces:: user_workspaces:: UserWorkspaces ;
16+ use crate :: workspaces:: workspace:: {
17+ EnforceableSetting , TeamLinkSharingSettings , TeamSettings , Workspace ,
18+ } ;
1219
1320fn set_shared_session_status (
1421 terminal : & ViewHandle < TerminalView > ,
@@ -157,3 +164,170 @@ fn session_qr_code_requires_status_eligible_matching_session_id() {
157164 assert_session_link_state ( & terminal, & dialog, Some ( second_session_id) , & app) ;
158165 } ) ;
159166}
167+
168+ /// A team whose admins either permit or forbid both link-sharing channels.
169+ fn team_with_link_sharing ( uid : i64 , name : & str , permitted : bool ) -> Team {
170+ let permission = EnforceableSetting {
171+ value : permitted,
172+ is_enforced_by_workspace : false ,
173+ } ;
174+ Team {
175+ uid : uid. into ( ) ,
176+ name : name. to_string ( ) ,
177+ color : None ,
178+ invite_link : None ,
179+ members : vec ! [ ] ,
180+ pending_email_invites : vec ! [ ] ,
181+ invite_link_domain_restrictions : vec ! [ ] ,
182+ billing_metadata : Default :: default ( ) ,
183+ stripe_customer_id : None ,
184+ settings : TeamSettings {
185+ link_sharing : TeamLinkSharingSettings {
186+ anyone_with_link_sharing_enabled : permission. clone ( ) ,
187+ direct_link_sharing_enabled : permission,
188+ } ,
189+ ..Default :: default ( )
190+ } ,
191+ is_eligible_for_discovery : false ,
192+ has_billing_history : false ,
193+ visibility : TeamVisibility :: Open ,
194+ }
195+ }
196+
197+ /// Replaces the user's workspaces with one holding `teams` and selects it, the way a
198+ /// workspaces-metadata refresh does. Windows already assigned to a team that `teams` no longer
199+ /// contains reconcile onto the first remaining one.
200+ fn install_workspace_with_teams ( app : & mut App , teams : Vec < Team > ) {
201+ let workspace = Workspace {
202+ uid : "workspace_uid123456789" . to_string ( ) . into ( ) ,
203+ name : "test" . to_string ( ) ,
204+ stripe_customer_id : None ,
205+ teams,
206+ billing_metadata : Default :: default ( ) ,
207+ bonus_grants_purchased_this_month : Default :: default ( ) ,
208+ billing_cycle_usage : None ,
209+ has_billing_history : false ,
210+ settings : Default :: default ( ) ,
211+ invite_link_domain_restrictions : vec ! [ ] ,
212+ pending_email_invites : vec ! [ ] ,
213+ is_eligible_for_discovery : false ,
214+ members : vec ! [ ] ,
215+ total_requests_used_since_last_refresh : 0 ,
216+ } ;
217+ let workspace_uid = workspace. uid ;
218+
219+ let user_workspaces = UserWorkspaces :: handle ( & * app) ;
220+ user_workspaces. update ( app, |user_workspaces, ctx| {
221+ user_workspaces. update_workspaces ( vec ! [ workspace] , ctx) ;
222+ user_workspaces. set_current_workspace_uid ( workspace_uid, ctx) ;
223+ } ) ;
224+ }
225+
226+ #[ test]
227+ fn link_sharing_gates_resolve_each_windows_own_team ( ) {
228+ App :: test ( ( ) , |mut app| async move {
229+ initialize_app_for_terminal_view ( & mut app) ;
230+
231+ let ( permitted_window, _permitted_terminal) =
232+ add_window_with_id_and_terminal ( & mut app, None ) ;
233+ let ( forbidden_window, _forbidden_terminal) =
234+ add_window_with_id_and_terminal ( & mut app, None ) ;
235+
236+ let permitted_team = team_with_link_sharing ( 123 , "permits-sharing" , true ) ;
237+ let forbidden_team = team_with_link_sharing ( 456 , "forbids-sharing" , false ) ;
238+ install_workspace_with_teams (
239+ & mut app,
240+ vec ! [ permitted_team. clone( ) , forbidden_team. clone( ) ] ,
241+ ) ;
242+
243+ let user_workspaces = UserWorkspaces :: handle ( & app) ;
244+ user_workspaces. update ( & mut app, |user_workspaces, ctx| {
245+ user_workspaces. set_team_for_window ( permitted_window, permitted_team. uid , ctx) ;
246+ user_workspaces. set_team_for_window ( forbidden_window, forbidden_team. uid , ctx) ;
247+ } ) ;
248+
249+ let permitted_dialog =
250+ app. add_typed_action_view ( permitted_window, |ctx| SharingDialog :: new ( None , ctx) ) ;
251+ let forbidden_dialog =
252+ app. add_typed_action_view ( forbidden_window, |ctx| SharingDialog :: new ( None , ctx) ) ;
253+
254+ permitted_dialog. read ( & app, |dialog, ctx| {
255+ assert ! ( dialog. can_anyone_with_link_share( ctx) ) ;
256+ assert ! ( dialog. can_direct_link_share( ctx) ) ;
257+ } ) ;
258+ forbidden_dialog. read ( & app, |dialog, ctx| {
259+ assert ! (
260+ !dialog. can_anyone_with_link_share( ctx) ,
261+ "a dialog in a window on a forbidding team must not inherit the other window's \
262+ permission"
263+ ) ;
264+ assert ! ( !dialog. can_direct_link_share( ctx) ) ;
265+ } ) ;
266+ } ) ;
267+ }
268+
269+ /// The dialog re-reads these gates when it renders and again before it acts, in
270+ /// `send_invitations` and in the `SetLinkPermissions` handler. Resolving them from the window
271+ /// rather than caching them at open is what stops an already-open dialog from sharing under
272+ /// the policy it opened with after its window has moved to a team that forbids sharing.
273+ #[ test]
274+ fn link_sharing_gates_follow_a_window_onto_its_new_team ( ) {
275+ App :: test ( ( ) , |mut app| async move {
276+ initialize_app_for_terminal_view ( & mut app) ;
277+
278+ let ( window_id, _terminal) = add_window_with_id_and_terminal ( & mut app, None ) ;
279+
280+ let permitted_team = team_with_link_sharing ( 123 , "permits-sharing" , true ) ;
281+ let forbidden_team = team_with_link_sharing ( 456 , "forbids-sharing" , false ) ;
282+ install_workspace_with_teams (
283+ & mut app,
284+ vec ! [ permitted_team. clone( ) , forbidden_team. clone( ) ] ,
285+ ) ;
286+
287+ let user_workspaces = UserWorkspaces :: handle ( & app) ;
288+ user_workspaces. update ( & mut app, |user_workspaces, ctx| {
289+ user_workspaces. set_team_for_window ( window_id, permitted_team. uid , ctx) ;
290+ } ) ;
291+
292+ let dialog = app. add_typed_action_view ( window_id, |ctx| SharingDialog :: new ( None , ctx) ) ;
293+ dialog. read ( & app, |dialog, ctx| {
294+ assert ! ( dialog. can_anyone_with_link_share( ctx) ) ;
295+ assert ! ( dialog. can_direct_link_share( ctx) ) ;
296+ } ) ;
297+
298+ // The permitting team leaves the workspace, so the window reconciles onto the
299+ // forbidding one while the dialog is still open.
300+ install_workspace_with_teams ( & mut app, vec ! [ forbidden_team] ) ;
301+
302+ dialog. read ( & app, |dialog, ctx| {
303+ assert ! (
304+ !dialog. can_anyone_with_link_share( ctx) ,
305+ "an open dialog must lose link sharing when its window moves to a team that \
306+ forbids it"
307+ ) ;
308+ assert ! ( !dialog. can_direct_link_share( ctx) ) ;
309+ } ) ;
310+ } ) ;
311+ }
312+
313+ #[ test]
314+ fn link_sharing_gates_deny_when_the_dialogs_window_has_no_team ( ) {
315+ App :: test ( ( ) , |mut app| async move {
316+ initialize_app_for_terminal_view ( & mut app) ;
317+
318+ let ( window_id, _terminal) = add_window_with_id_and_terminal ( & mut app, None ) ;
319+ install_workspace_with_teams ( & mut app, vec ! [ team_with_link_sharing( 123 , "team" , true ) ] ) ;
320+
321+ // Deliberately left unregistered: no window team can be resolved for this dialog.
322+ let dialog = app. add_typed_action_view ( window_id, |ctx| SharingDialog :: new ( None , ctx) ) ;
323+
324+ dialog. read ( & app, |dialog, ctx| {
325+ assert ! (
326+ !dialog. can_anyone_with_link_share( ctx) ,
327+ "a dialog whose window team cannot be resolved must not offer a sharing channel \
328+ governed by an unknown policy"
329+ ) ;
330+ assert ! ( !dialog. can_direct_link_share( ctx) ) ;
331+ } ) ;
332+ } ) ;
333+ }
0 commit comments