Files

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();
});
});
});