264 lines
8.3 KiB
TypeScript
264 lines
8.3 KiB
TypeScript
import { render, screen, waitFor } from '@testing-library/react';
|
|
import userEvent from '@testing-library/user-event';
|
|
import { HttpResponse, http } from 'msw';
|
|
import { ReactNode } from 'react';
|
|
import { vi } from 'vitest';
|
|
|
|
import { server } from '@/setup-tests/server';
|
|
import { withTestQueryProvider } from '@/react/test-utils/withTestQuery';
|
|
import { useAuthorizations } from '@/react/hooks/useUser';
|
|
|
|
import { ImagePullSecretsRow } from './ImagePullSecretsRow';
|
|
|
|
vi.mock('@/react/hooks/useEnvironmentId', () => ({
|
|
useEnvironmentId: () => 1,
|
|
}));
|
|
|
|
vi.mock(
|
|
'@/react/hooks/useUser',
|
|
async (importOriginal: () => Promise<object>) => ({
|
|
...(await importOriginal()),
|
|
useAuthorizations: vi.fn(),
|
|
useCurrentUser: () => ({ isPureAdmin: false }),
|
|
})
|
|
);
|
|
|
|
vi.mock('@/react/kubernetes/configs/queries/useSecrets', () => ({
|
|
useSecrets: () => ({
|
|
data: [
|
|
{ metadata: { name: 'secret-a' } },
|
|
{ metadata: { name: 'secret-b' } },
|
|
],
|
|
isLoading: false,
|
|
}),
|
|
}));
|
|
|
|
vi.mock(
|
|
'@/react/portainer/environments/queries/useEnvironmentRegistries',
|
|
() => ({
|
|
useEnvironmentRegistries: () => ({
|
|
data: {
|
|
linkedDefaultSecretNames: [],
|
|
registryBySecretName: {},
|
|
},
|
|
isLoading: false,
|
|
}),
|
|
})
|
|
);
|
|
|
|
vi.mock('@@/Link', () => ({
|
|
Link: ({ children }: { children: ReactNode }) => <>{children}</>,
|
|
}));
|
|
|
|
vi.mock('@@/Tip/Tooltip', () => ({ Tooltip: () => null }));
|
|
|
|
vi.mock('@@/Tip/TooltipWithChildren', () => ({
|
|
TooltipWithChildren: ({ children }: { children: ReactNode }) => (
|
|
<>{children}</>
|
|
),
|
|
}));
|
|
|
|
vi.mock('@@/form-components/PortainerSelect', () => ({
|
|
MultiSelect: () => <div data-testid="multi-select" />,
|
|
}));
|
|
|
|
const ImagePullSecretsRowWithQuery = withTestQueryProvider(ImagePullSecretsRow);
|
|
|
|
type RowProps = React.ComponentProps<typeof ImagePullSecretsRow>;
|
|
|
|
function renderRow(props: Partial<RowProps> = {}) {
|
|
return render(
|
|
<ImagePullSecretsRowWithQuery
|
|
namespace="default"
|
|
name="my-sa"
|
|
imagePullSecrets={[]}
|
|
isSystem={false}
|
|
{...props}
|
|
/>
|
|
);
|
|
}
|
|
|
|
describe('ImagePullSecretsRow', () => {
|
|
beforeEach(() => {
|
|
vi.mocked(useAuthorizations).mockImplementation(() => ({
|
|
authorized: false,
|
|
isLoading: false,
|
|
}));
|
|
});
|
|
|
|
describe('empty state messages', () => {
|
|
it('shows a pod-specific message for a non-default service account', () => {
|
|
renderRow({ name: 'my-sa', imagePullSecrets: [] });
|
|
expect(
|
|
screen.getByText(/Pods using this service account must specify/)
|
|
).toBeVisible();
|
|
});
|
|
|
|
it('shows a namespace-wide message for the default service account', () => {
|
|
renderRow({ name: 'default', imagePullSecrets: [] });
|
|
expect(
|
|
screen.getByText(
|
|
/Pods in this namespace without an explicit service account/
|
|
)
|
|
).toBeVisible();
|
|
});
|
|
});
|
|
|
|
describe('pull secret badges', () => {
|
|
it('renders current pull secrets as badges', () => {
|
|
renderRow({
|
|
imagePullSecrets: [{ name: 'secret-a' }, { name: 'secret-b' }],
|
|
});
|
|
expect(screen.getByText('secret-a')).toBeVisible();
|
|
expect(screen.getByText('secret-b')).toBeVisible();
|
|
});
|
|
|
|
it('shows a warning badge for a pull secret that no longer exists in the namespace', () => {
|
|
renderRow({ imagePullSecrets: [{ name: 'orphan-secret' }] });
|
|
expect(screen.getByText('orphan-secret')).toBeVisible();
|
|
});
|
|
});
|
|
|
|
describe('edit button visibility by role', () => {
|
|
it('does not show edit button for a user without write permission', () => {
|
|
vi.mocked(useAuthorizations).mockImplementation(() => ({
|
|
authorized: false,
|
|
isLoading: false,
|
|
}));
|
|
renderRow();
|
|
expect(
|
|
screen.queryByRole('button', { name: /edit/i })
|
|
).not.toBeInTheDocument();
|
|
});
|
|
|
|
it('does not show edit button for system service accounts, even for an admin', () => {
|
|
vi.mocked(useAuthorizations).mockImplementation(() => ({
|
|
authorized: true,
|
|
isLoading: false,
|
|
}));
|
|
renderRow({ isSystem: true });
|
|
expect(
|
|
screen.queryByRole('button', { name: /edit/i })
|
|
).not.toBeInTheDocument();
|
|
});
|
|
|
|
it('shows edit button for an authorized user on a non-system SA', () => {
|
|
vi.mocked(useAuthorizations).mockImplementation((permission) => ({
|
|
authorized: permission === 'K8sServiceAccountsW',
|
|
isLoading: false,
|
|
}));
|
|
renderRow({ isSystem: false });
|
|
expect(screen.getByRole('button', { name: /edit/i })).toBeVisible();
|
|
});
|
|
});
|
|
|
|
describe('edit flow', () => {
|
|
beforeEach(() => {
|
|
vi.mocked(useAuthorizations).mockImplementation((permission) => ({
|
|
authorized: permission === 'K8sServiceAccountsW',
|
|
isLoading: false,
|
|
}));
|
|
});
|
|
|
|
it('shows save and cancel buttons after clicking edit', async () => {
|
|
renderRow({ imagePullSecrets: [{ name: 'secret-a' }] });
|
|
|
|
await userEvent.click(screen.getByRole('button', { name: /edit/i }));
|
|
|
|
expect(screen.getByRole('button', { name: /save/i })).toBeVisible();
|
|
expect(screen.getByRole('button', { name: /cancel/i })).toBeVisible();
|
|
});
|
|
|
|
it('returns to view mode when cancel is clicked', async () => {
|
|
renderRow({ imagePullSecrets: [{ name: 'secret-a' }] });
|
|
|
|
await userEvent.click(screen.getByRole('button', { name: /edit/i }));
|
|
await userEvent.click(screen.getByRole('button', { name: /cancel/i }));
|
|
|
|
expect(screen.getByRole('button', { name: /edit/i })).toBeVisible();
|
|
expect(
|
|
screen.queryByRole('button', { name: /save/i })
|
|
).not.toBeInTheDocument();
|
|
});
|
|
|
|
it('sends a PUT to the correct path and body when save is clicked', async () => {
|
|
let capturedPath: string | undefined;
|
|
let capturedBody: unknown;
|
|
|
|
server.use(
|
|
http.put(
|
|
'/api/kubernetes/:envId/namespaces/:namespace/service_accounts/:name/image_pull_secrets',
|
|
async ({ request }) => {
|
|
capturedPath = request.url;
|
|
capturedBody = await request.json();
|
|
return new HttpResponse(null, { status: 204 });
|
|
}
|
|
)
|
|
);
|
|
|
|
renderRow({
|
|
namespace: 'production',
|
|
name: 'app-sa',
|
|
imagePullSecrets: [{ name: 'secret-a' }, { name: 'secret-b' }],
|
|
});
|
|
|
|
await userEvent.click(screen.getByRole('button', { name: /edit/i }));
|
|
await userEvent.click(screen.getByRole('button', { name: /save/i }));
|
|
|
|
await waitFor(() => {
|
|
expect(capturedPath).toContain(
|
|
'/api/kubernetes/1/namespaces/production/service_accounts/app-sa/image_pull_secrets'
|
|
);
|
|
expect(capturedBody).toEqual({
|
|
secretNames: ['secret-a', 'secret-b'],
|
|
});
|
|
});
|
|
});
|
|
|
|
it('sends an empty secretNames array when there are no pull secrets', async () => {
|
|
let capturedBody: unknown;
|
|
|
|
server.use(
|
|
http.put(
|
|
'/api/kubernetes/:envId/namespaces/:namespace/service_accounts/:name/image_pull_secrets',
|
|
async ({ request }) => {
|
|
capturedBody = await request.json();
|
|
return new HttpResponse(null, { status: 204 });
|
|
}
|
|
)
|
|
);
|
|
|
|
renderRow({ namespace: 'default', name: 'my-sa', imagePullSecrets: [] });
|
|
|
|
await userEvent.click(screen.getByRole('button', { name: /edit/i }));
|
|
await userEvent.click(screen.getByRole('button', { name: /save/i }));
|
|
|
|
await waitFor(() => {
|
|
expect(capturedBody).toEqual({ secretNames: [] });
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('admin vs standard user role', () => {
|
|
it('admin with registry access can enter edit mode', async () => {
|
|
vi.mocked(useAuthorizations).mockImplementation(() => ({
|
|
authorized: true,
|
|
isLoading: false,
|
|
}));
|
|
renderRow({ isSystem: false });
|
|
await userEvent.click(screen.getByRole('button', { name: /edit/i }));
|
|
expect(screen.getByRole('button', { name: /save/i })).toBeVisible();
|
|
});
|
|
|
|
it('standard user with write permission but no registry access can also enter edit mode', async () => {
|
|
vi.mocked(useAuthorizations).mockImplementation((permission) => ({
|
|
authorized: permission === 'K8sServiceAccountsW',
|
|
isLoading: false,
|
|
}));
|
|
renderRow({ isSystem: false });
|
|
await userEvent.click(screen.getByRole('button', { name: /edit/i }));
|
|
expect(screen.getByRole('button', { name: /save/i })).toBeVisible();
|
|
});
|
|
});
|
|
});
|