Skip to content
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@linode/manager": Upcoming Features
---

Fix error handling in ChildAccountList component ([#13299](https://github.com/linode/manager/pull/13299))
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,11 @@ export const ChildAccountList = React.memo(
}: ChildAccountListProps) => {
const { isIAMDelegationEnabled } = useIsIAMDelegationEnabled();

if (errors.childAccountInfiniteError || errors.allChildAccountsError) {
const hasError = isIAMDelegationEnabled
? errors.allChildAccountsError
: errors.childAccountInfiniteError;

if (hasError) {
return (
<Stack alignItems="center" gap={1} justifyContent="center">
<ErrorStateCloud />
Expand Down
1 change: 1 addition & 0 deletions packages/manager/src/features/IAM/Roles/Roles.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,7 @@ describe('RolesLanding', () => {
renderWithTheme(<RolesLanding />, {
flags: {
iamDelegation: { enabled: true },
iam: { enabled: true },
},
});
expect(screen.getByText(DEFAULT_ROLES_PANEL_TEXT)).toBeInTheDocument();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ const queryMocks = vi.hoisted(() => ({
useAllGetDelegatedChildAccountsForUserQuery: vi.fn().mockReturnValue({}),
useParams: vi.fn().mockReturnValue({}),
useSearch: vi.fn().mockReturnValue({}),
useIsIAMDelegationEnabled: vi.fn().mockReturnValue({}),
}));

vi.mock('@linode/queries', async () => {
Expand All @@ -42,6 +43,16 @@ vi.mock('@tanstack/react-router', async () => {
};
});

vi.mock('src/features/IAM/hooks/useIsIAMEnabled', async () => {
const actual = await vi.importActual(
'src/features/IAM/hooks/useIsIAMEnabled'
);
return {
...actual,
useIsIAMDelegationEnabled: queryMocks.useIsIAMDelegationEnabled,
};
});

describe('UserDelegations', () => {
beforeEach(() => {
queryMocks.useParams.mockReturnValue({
Expand All @@ -54,6 +65,9 @@ describe('UserDelegations', () => {
queryMocks.useSearch.mockReturnValue({
query: '',
});
queryMocks.useIsIAMDelegationEnabled.mockReturnValue({
isIAMDelegationEnabled: true,
});
});

it('renders the correct number of child accounts', () => {
Expand Down
79 changes: 44 additions & 35 deletions packages/manager/src/features/IAM/Users/UsersTable/UserRow.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ import { waitFor } from '@testing-library/react';
import React from 'react';

import { accountUserFactory } from 'src/factories/accountUsers';
import { http, HttpResponse, server } from 'src/mocks/testServer';
import {
mockMatchMedia,
renderWithTheme,
Expand All @@ -17,18 +16,35 @@ import { UserRow } from './UserRow';
beforeAll(() => mockMatchMedia());

const queryMocks = vi.hoisted(() => ({
useFlags: vi.fn().mockReturnValue({}),
useIsIAMDelegationEnabled: vi.fn().mockReturnValue({}),
useProfile: vi.fn().mockReturnValue({}),
}));

vi.mock('src/hooks/useFlags', () => {
const actual = vi.importActual('src/hooks/useFlags');
vi.mock('src/features/IAM/hooks/useIsIAMEnabled', async () => {
const actual = await vi.importActual(
'src/features/IAM/hooks/useIsIAMEnabled'
);
return {
...actual,
useFlags: queryMocks.useFlags,
useIsIAMDelegationEnabled: queryMocks.useIsIAMDelegationEnabled,
};
});

vi.mock('@linode/queries', async () => {
const actual = await vi.importActual('@linode/queries');
return {
...actual,
useProfile: queryMocks.useProfile,
};
});

describe('UserRow', () => {
beforeEach(() => {
queryMocks.useIsIAMDelegationEnabled.mockReturnValue({
isIAMDelegationEnabled: true,
});
});

it('renders a username and email', async () => {
const user = accountUserFactory.build();

Expand All @@ -39,24 +55,22 @@ describe('UserRow', () => {
expect(getByText(user.username)).toBeVisible();
expect(getByText(user.email)).toBeVisible();
});

it('renders username, email, and user type for a Child user when isIAMDelegationEnabled flag is enabled', async () => {
const user = accountUserFactory.build({
user_type: 'child',
});

server.use(
// Mock the active profile for the child account.
http.get('*/profile', () => {
return HttpResponse.json(profileFactory.build({ user_type: 'child' }));
})
);

queryMocks.useFlags.mockReturnValue({
iamDelegation: { enabled: true },
queryMocks.useProfile.mockReturnValue({
data: profileFactory.build({ user_type: 'child' }),
});

const { getByText } = renderWithTheme(
wrapWithTableBody(<UserRow onDelete={vi.fn()} user={user} />)
wrapWithTableBody(<UserRow onDelete={vi.fn()} user={user} />, {
flags: {
iamDelegation: { enabled: true },
},
})
);

expect(getByText(user.username)).toBeVisible();
Expand All @@ -72,19 +86,16 @@ describe('UserRow', () => {
user_type: 'delegate',
});

server.use(
// Mock the active profile for the child account.
http.get('*/profile', () => {
return HttpResponse.json(profileFactory.build({ user_type: 'child' }));
})
);

queryMocks.useFlags.mockReturnValue({
iamDelegation: { enabled: true },
queryMocks.useProfile.mockReturnValue({
data: profileFactory.build({ user_type: 'child' }),
});

const { getByText, queryByText } = renderWithTheme(
wrapWithTableBody(<UserRow onDelete={vi.fn()} user={delegateUser} />)
wrapWithTableBody(<UserRow onDelete={vi.fn()} user={delegateUser} />, {
flags: {
iamDelegation: { enabled: true },
},
})
);

expect(getByText(delegateUser.username)).toBeVisible();
Expand All @@ -105,13 +116,12 @@ describe('UserRow', () => {

expect(getByText('Never')).toBeVisible();
});

it('renders a timestamp of the last_login if it was successful', async () => {
// Because we are unit testing a timestamp, set our timezone to UTC
server.use(
http.get('*/profile', () => {
return HttpResponse.json(profileFactory.build({ timezone: 'utc' }));
})
);
queryMocks.useProfile.mockReturnValue({
data: profileFactory.build({ timezone: 'utc' }),
});

const user = accountUserFactory.build({
last_login: {
Expand All @@ -128,13 +138,12 @@ describe('UserRow', () => {

expect(date).toBeVisible();
});

it('renders a timestamp and "Failed" of the last_login if it was failed', async () => {
// Because we are unit testing a timestamp, set our timezone to UTC
server.use(
http.get('*/profile', () => {
return HttpResponse.json(profileFactory.build({ timezone: 'utc' }));
})
);
queryMocks.useProfile.mockReturnValue({
data: profileFactory.build({ timezone: 'utc' }),
});

const user = accountUserFactory.build({
last_login: {
Expand Down
32 changes: 20 additions & 12 deletions packages/manager/src/features/IAM/Users/UsersTable/Users.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,21 +13,13 @@ beforeAll(() => mockMatchMedia());
const navigate = vi.fn();

const queryMocks = vi.hoisted(() => ({
useFlags: vi.fn().mockReturnValue({}),
useNavigate: vi.fn(() => navigate),
useProfile: vi.fn().mockReturnValue({}),
useAccountUsers: vi.fn().mockReturnValue({}),
useSearch: vi.fn().mockReturnValue({}),
useIsIAMDelegationEnabled: vi.fn().mockReturnValue({}),
}));

vi.mock('src/hooks/useFlags', () => {
const actual = vi.importActual('src/hooks/useFlags');
return {
...actual,
useFlags: queryMocks.useFlags,
};
});

vi.mock('@tanstack/react-router', async () => {
const actual = await vi.importActual('@tanstack/react-router');
return {
Expand All @@ -46,7 +38,23 @@ vi.mock('@linode/queries', async () => {
};
});

vi.mock('src/features/IAM/hooks/useIsIAMEnabled', async () => {
const actual = await vi.importActual(
'src/features/IAM/hooks/useIsIAMEnabled'
);
return {
...actual,
useIsIAMDelegationEnabled: queryMocks.useIsIAMDelegationEnabled,
};
});

describe('Users', () => {
beforeEach(() => {
queryMocks.useIsIAMDelegationEnabled.mockReturnValue({
isIAMDelegationEnabled: true,
});
});

it('renders only table and search filter if profile is not a child', async () => {
const user = accountUserFactory.build();
queryMocks.useAccountUsers.mockReturnValue({
Expand Down Expand Up @@ -88,14 +96,14 @@ describe('Users', () => {
queryMocks.useProfile.mockReturnValue({
data: profileFactory.build({ user_type: 'child' }),
});
queryMocks.useFlags.mockReturnValue({
iamDelegation: { enabled: true },
});

const { getByPlaceholderText, getByLabelText } = renderWithTheme(
<UsersLanding />,
{
initialRoute: '/iam',
flags: {
iamDelegation: { enabled: true },
},
}
);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ import { profileFactory } from '@linode/utilities';
import { waitFor } from '@testing-library/react';
import React from 'react';

import { http, HttpResponse, server } from 'src/mocks/testServer';
import {
mockMatchMedia,
renderWithTheme,
Expand All @@ -18,14 +17,25 @@ import type { Order } from '@linode/utilities';
beforeAll(() => mockMatchMedia());

const queryMocks = vi.hoisted(() => ({
useFlags: vi.fn().mockReturnValue({}),
useProfile: vi.fn().mockReturnValue({}),
useIsIAMDelegationEnabled: vi.fn().mockReturnValue({}),
}));

vi.mock('src/hooks/useFlags', () => {
const actual = vi.importActual('src/hooks/useFlags');
vi.mock('@linode/queries', async () => {
const actual = await vi.importActual('@linode/queries');
return {
...actual,
useFlags: queryMocks.useFlags,
useProfile: queryMocks.useProfile,
};
});

vi.mock('src/features/IAM/hooks/useIsIAMEnabled', async () => {
const actual = await vi.importActual(
'src/features/IAM/hooks/useIsIAMEnabled'
);
return {
...actual,
useIsIAMDelegationEnabled: queryMocks.useIsIAMDelegationEnabled,
};
});

Expand All @@ -38,20 +48,23 @@ const defaultProps = {
};

describe('UsersLandingTableHead', () => {
it('renders User type, Username, Email Address, and Last Login columns for a Child user when isIAMDelegationEnabled flag is enabled', async () => {
server.use(
// Mock the active profile for the child account.
http.get('*/profile', () => {
return HttpResponse.json(profileFactory.build({ user_type: 'child' }));
})
);
beforeEach(() => {
queryMocks.useIsIAMDelegationEnabled.mockReturnValue({
isIAMDelegationEnabled: true,
});
});

queryMocks.useFlags.mockReturnValue({
iamDelegation: { enabled: true },
it('renders User type, Username, Email Address, and Last Login columns for a Child user when isIAMDelegationEnabled flag is enabled', async () => {
queryMocks.useProfile.mockReturnValue({
data: profileFactory.build({ user_type: 'child' }),
});

const { getByText } = renderWithTheme(
wrapWithTableBody(<UsersLandingTableHead {...defaultProps} />)
wrapWithTableBody(<UsersLandingTableHead {...defaultProps} />, {
flags: {
iamDelegation: { enabled: true },
},
})
);

await waitFor(() => {
Expand All @@ -63,21 +76,16 @@ describe('UsersLandingTableHead', () => {
});

it('does not render User type column when isIAMDelegationEnabled flag is off and logged user is not a child', async () => {
server.use(
// Mock the active profile for the default account.
http.get('*/profile', () => {
return HttpResponse.json(
profileFactory.build({ user_type: 'default' })
);
})
);

queryMocks.useFlags.mockReturnValue({
iamDelegation: { enabled: false },
queryMocks.useProfile.mockReturnValue({
data: profileFactory.build({ user_type: 'default' }),
});

const { getByText, queryByText } = renderWithTheme(
wrapWithTableBody(<UsersLandingTableHead {...defaultProps} />)
wrapWithTableBody(<UsersLandingTableHead {...defaultProps} />, {
flags: {
iamDelegation: { enabled: false },
},
})
);

expect(queryByText('User Type')).not.toBeInTheDocument();
Expand Down
6 changes: 5 additions & 1 deletion packages/manager/src/features/IAM/hooks/useIsIAMEnabled.ts
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,10 @@ export const checkIAMEnabled = async (
*/
export const useIsIAMDelegationEnabled = () => {
const flags = useFlags();
const { isIAMEnabled } = useIsIAMEnabled();

return { isIAMDelegationEnabled: flags.iamDelegation?.enabled ?? false };
return {
isIAMDelegationEnabled:
(flags.iamDelegation?.enabled && isIAMEnabled) ?? false,
};
};