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) => ({ ...(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: () =>
, })); const ImagePullSecretsRowWithQuery = withTestQueryProvider(ImagePullSecretsRow); type RowProps = React.ComponentProps; function renderRow(props: Partial = {}) { return render( ); } 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(); }); }); });