From 28ed602fb6cbacbf6624e1fbd1a9829e50e4a600 Mon Sep 17 00:00:00 2001 From: Anastasiia Alekseenko Date: Mon, 10 Nov 2025 12:35:10 +0100 Subject: [PATCH 1/3] feat: [UIE-9423] - IAM Parent/Child: permissions switch account --- .../src/features/Account/AccountLanding.tsx | 8 +- .../Account/SwitchAccountButton.test.tsx | 77 ++++++++++++++++++- .../features/Account/SwitchAccountButton.tsx | 20 +++++ .../Billing/BillingLanding/BillingLanding.tsx | 8 +- .../TopMenu/UserMenu/UserMenuPopover.tsx | 14 +++- 5 files changed, 118 insertions(+), 9 deletions(-) diff --git a/packages/manager/src/features/Account/AccountLanding.tsx b/packages/manager/src/features/Account/AccountLanding.tsx index 8a702687d98..0f626cc9850 100644 --- a/packages/manager/src/features/Account/AccountLanding.tsx +++ b/packages/manager/src/features/Account/AccountLanding.tsx @@ -24,6 +24,7 @@ import { useTabs } from 'src/hooks/useTabs'; import { sendSwitchAccountEvent } from 'src/utilities/analytics/customEventAnalytics'; import { PlatformMaintenanceBanner } from '../../components/PlatformMaintenanceBanner/PlatformMaintenanceBanner'; +import { useIsIAMDelegationEnabled } from '../IAM/hooks/useIsIAMEnabled'; import { usePermissions } from '../IAM/hooks/usePermissions'; import { SwitchAccountButton } from './SwitchAccountButton'; import { SwitchAccountDrawer } from './SwitchAccountDrawer'; @@ -60,6 +61,8 @@ export const AccountLanding = () => { globalGrantType: 'child_account_access', }); + const { isIAMDelegationEnabled } = useIsIAMDelegationEnabled(); + const { isParentTokenExpired } = useIsParentTokenExpired({ isProxyUser }); const { tabs, handleTabChange, tabIndex, getTabIndex } = useTabs([ @@ -124,8 +127,9 @@ export const AccountLanding = () => { }; const isBillingTabSelected = getTabIndex('/account/billing') === tabIndex; - const canSwitchBetweenParentOrProxyAccount = - (!isChildAccountAccessRestricted && isParentUser) || isProxyUser; + const canSwitchBetweenParentOrProxyAccount = isIAMDelegationEnabled + ? isParentUser + : (!isChildAccountAccessRestricted && isParentUser) || isProxyUser; const landingHeaderProps: LandingHeaderProps = { breadcrumbProps: { diff --git a/packages/manager/src/features/Account/SwitchAccountButton.test.tsx b/packages/manager/src/features/Account/SwitchAccountButton.test.tsx index b043e9f825c..2749e17c495 100644 --- a/packages/manager/src/features/Account/SwitchAccountButton.test.tsx +++ b/packages/manager/src/features/Account/SwitchAccountButton.test.tsx @@ -1,10 +1,30 @@ -import { screen } from '@testing-library/react'; +import { screen, waitFor } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import React from 'react'; import { SwitchAccountButton } from 'src/features/Account/SwitchAccountButton'; import { renderWithTheme } from 'src/utilities/testHelpers'; +const queryMocks = vi.hoisted(() => ({ + userPermissions: vi.fn(() => ({ + data: { + create_child_account_token: true, + }, + })), + useFlags: vi.fn().mockReturnValue({}), +})); +vi.mock('src/features/IAM/hooks/usePermissions', () => ({ + usePermissions: queryMocks.userPermissions, +})); + +vi.mock('src/hooks/useFlags', () => { + const actual = vi.importActual('src/hooks/useFlags'); + return { + ...actual, + useFlags: queryMocks.useFlags, + }; +}); + describe('SwitchAccountButton', () => { test('renders Switch Account button with SwapIcon', () => { renderWithTheme(); @@ -22,4 +42,59 @@ describe('SwitchAccountButton', () => { expect(onClickMock).toHaveBeenCalledTimes(1); }); + + test('enables the button when user has create_child_account_token permission', () => { + queryMocks.useFlags.mockReturnValue({ + iamDelegation: { enabled: true }, + }); + + renderWithTheme(); + + const button = screen.getByRole('button', { name: /switch account/i }); + expect(button).toBeEnabled(); + }); + + test('disables the button when user does not have create_child_account_token permission', async () => { + queryMocks.userPermissions.mockReturnValue({ + data: { + create_child_account_token: false, + }, + }); + + queryMocks.useFlags.mockReturnValue({ + iamDelegation: { enabled: true }, + }); + + renderWithTheme(); + + const button = screen.getByRole('button', { name: /switch account/i }); + expect(button).toBeDisabled(); + + // Check that the tooltip is properly configured + expect(button).toHaveAttribute('aria-describedby', 'button-tooltip'); + + // Hover over the button to show the tooltip + await userEvent.hover(button); + + // Wait for tooltip to appear and check its content + await waitFor(() => { + screen.getByRole('tooltip'); + }); + + expect( + screen.getByText('You do not have permission to switch accounts.') + ).toBeVisible(); + }); + + test('enables the button when iamDelegation flag is off', async () => { + queryMocks.useFlags.mockReturnValue({ + iamDelegation: { enabled: false }, + }); + + renderWithTheme(); + + const button = screen.getByRole('button', { name: /switch account/i }); + expect(button).toBeEnabled(); + expect(button).not.toHaveAttribute('aria-describedby', 'button-tooltip'); + }); }); diff --git a/packages/manager/src/features/Account/SwitchAccountButton.tsx b/packages/manager/src/features/Account/SwitchAccountButton.tsx index 33944ec21e5..0dc27c2610e 100644 --- a/packages/manager/src/features/Account/SwitchAccountButton.tsx +++ b/packages/manager/src/features/Account/SwitchAccountButton.tsx @@ -3,11 +3,26 @@ import * as React from 'react'; import SwapIcon from 'src/assets/icons/swapSmall.svg'; +import { useIsIAMDelegationEnabled } from '../IAM/hooks/useIsIAMEnabled'; +import { usePermissions } from '../IAM/hooks/usePermissions'; + import type { ButtonProps } from '@linode/ui'; export const SwitchAccountButton = (props: ButtonProps) => { + const { isIAMDelegationEnabled } = useIsIAMDelegationEnabled(); + + const { data: permissions } = usePermissions( + 'account', + ['create_child_account_token'], + undefined, + isIAMDelegationEnabled + ); + return (