Files
headlamp-polaris-plugin/src/components/ExemptionManager.test.tsx
T
privilegedescalation-engineer[bot] a5398e8409 feat: add ExemptionManager tests, coverage threshold, and ArtifactHub metadata polish (#82)
* ci: rework E2E infrastructure to use default namespace

Board directive: E2E tests must run in the `default` namespace.
Nothing should persist beyond a test run; no dedicated namespace needed.

Changes:
- e2e-ci-runner-rbac.yaml: retarget Role/RoleBinding to `default`,
  remove ClusterRole/ClusterRoleBinding (no longer needed since we
  don't need cluster-scoped namespace read permission)
- e2e.yaml: set E2E_NAMESPACE=default
- deploy-e2e-headlamp.sh: default namespace to `default`, remove
  namespace existence check (default always exists)
- teardown-e2e-headlamp.sh: default namespace to `default`, remove
  namespace existence check guard
- headlamp-e2e-values.yaml: update usage comment
- e2e/README.md: remove namespace creation prerequisite

Closes #78 #79

Co-Authored-By: Paperclip <noreply@paperclip.ing>

* ci: add RBAC preflight check to deploy-e2e-headlamp.sh

Fails fast with a clear error and remediation hint if the runner SA
lacks configmap delete permission, instead of dying mid-deploy.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* feat: add ExemptionManager tests, coverage threshold, and ArtifactHub metadata

- Add 22 unit tests for ExemptionManager.tsx covering:
  - Failing checks extraction (pod-level, container-level, ignore-severity, dedup)
  - Dialog open/close, check toggle, exempt-all toggle
  - Apply button enabled/disabled state
  - ApiProxy.request called with correct path (apps/batch/core) and annotation structure
  - Success and error feedback states, in-flight "Applying..." label
- Add vitest coverage config with >=80% threshold (lines/functions/branches/statements)
- Update artifacthub-pkg.yml:
  - Add install section (Headlamp-native plugin installer only)
  - Add appVersion: "5.0" (compatible Polaris dashboard version)
  - Expand distro-compat from "in-cluster" to "in-cluster,web,desktop"
  - Add changes block documenting v1.0 features

Closes privilegedescalation/headlamp-polaris-plugin#81 (partial — test and metadata tasks)

Co-Authored-By: Paperclip <noreply@paperclip.ing>

* style: fix Prettier formatting in ExemptionManager.test.tsx

Co-Authored-By: Paperclip <noreply@paperclip.ing>

* fix: add @vitest/coverage-v8 devDependency for coverage provider

vitest.config.mts specifies coverage.provider: 'v8' but the
@vitest/coverage-v8 package was missing from devDependencies.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Hugh Hackman <hugh@privilegedescalation.com>
Co-authored-by: Paperclip <noreply@paperclip.ing>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-authored-by: Gandalf the Greybeard <gandalf@privilegedescalation.com>
Co-authored-by: Samuel Stinkpost <samuel@privilegedescalation.dev>
Co-authored-by: Gandalf the Greybeard <gandalf@privilegedescalation.dev>
2026-03-21 12:53:07 +00:00

433 lines
16 KiB
TypeScript

import { fireEvent, render, screen, waitFor } from '@testing-library/react';
import React from 'react';
import { describe, expect, it, vi } from 'vitest';
import { makeResult } from '../test-utils';
const { mockApiRequest } = vi.hoisted(() => ({ mockApiRequest: vi.fn() }));
vi.mock('@kinvolk/headlamp-plugin/lib', () => ({
ApiProxy: { request: mockApiRequest },
}));
vi.mock('@mui/material/styles', () => ({
useTheme: () => ({
palette: {
primary: { main: '#1976d2', contrastText: '#fff' },
action: { disabledBackground: '#e0e0e0', disabled: '#9e9e9e' },
divider: '#e0e0e0',
},
}),
}));
vi.mock('@kinvolk/headlamp-plugin/lib/CommonComponents', () => ({
SectionBox: ({ title, children }: { title?: string; children?: React.ReactNode }) => (
<div data-testid="section-box" data-title={title}>
{children}
</div>
),
StatusLabel: ({ status, children }: { status: string; children?: React.ReactNode }) => (
<span data-testid="status-label" data-status={status}>
{children}
</span>
),
Dialog: ({
open,
children,
title,
}: {
open: boolean;
onClose?: () => void;
title?: string;
children?: React.ReactNode;
}) =>
open ? (
<div data-testid="dialog" data-title={title}>
{children}
</div>
) : null,
}));
import ExemptionManager from './ExemptionManager';
const defaultProps = {
workloadResult: makeResult(),
namespace: 'default',
kind: 'Deployment',
name: 'my-deploy',
};
const resultWithPodFailures = makeResult({
PodResult: {
Name: 'pod',
Results: {
hostIPCSet: {
ID: 'hostIPCSet',
Message: 'Host IPC is set',
Details: [],
Success: false,
Severity: 'danger',
Category: 'Security',
},
hostPIDSet: {
ID: 'hostPIDSet',
Message: 'Host PID is set',
Details: [],
Success: false,
Severity: 'danger',
Category: 'Security',
},
},
ContainerResults: [],
},
});
const resultWithContainerFailures = makeResult({
PodResult: {
Name: 'pod',
Results: {},
ContainerResults: [
{
Name: 'container-1',
Results: {
cpuRequestsMissing: {
ID: 'cpuRequestsMissing',
Message: 'CPU requests missing',
Details: [],
Success: false,
Severity: 'warning',
Category: 'Efficiency',
},
},
},
],
},
});
const resultWithIgnoredFailures = makeResult({
PodResult: {
Name: 'pod',
Results: {
hostIPCSet: {
ID: 'hostIPCSet',
Message: '',
Details: [],
Success: false,
Severity: 'ignore',
Category: 'Security',
},
},
ContainerResults: [],
},
});
describe('ExemptionManager', () => {
describe('rendering failing checks', () => {
it('shows disabled Add Exemption button when no failing checks', () => {
render(<ExemptionManager {...defaultProps} />);
const btn = screen.getByRole('button', { name: /add exemption/i });
expect(btn).toBeDisabled();
});
it('shows enabled Add Exemption button when there are failing checks', () => {
render(<ExemptionManager {...defaultProps} workloadResult={resultWithPodFailures} />);
const btn = screen.getByRole('button', { name: /add exemption/i });
expect(btn).not.toBeDisabled();
});
it('does not include ignored-severity checks as failing', () => {
render(<ExemptionManager {...defaultProps} workloadResult={resultWithIgnoredFailures} />);
const btn = screen.getByRole('button', { name: /add exemption/i });
expect(btn).toBeDisabled();
});
it('collects failing checks from pod-level results', () => {
render(<ExemptionManager {...defaultProps} workloadResult={resultWithPodFailures} />);
fireEvent.click(screen.getByRole('button', { name: /add exemption/i }));
expect(screen.getByText('Host IPC')).toBeInTheDocument();
expect(screen.getByText('Host PID')).toBeInTheDocument();
});
it('collects failing checks from container-level results', () => {
render(<ExemptionManager {...defaultProps} workloadResult={resultWithContainerFailures} />);
fireEvent.click(screen.getByRole('button', { name: /add exemption/i }));
expect(screen.getByText('CPU Requests')).toBeInTheDocument();
});
it('deduplicates checks that appear in multiple containers', () => {
const resultWithDuplicate = makeResult({
PodResult: {
Name: 'pod',
Results: {},
ContainerResults: [
{
Name: 'container-1',
Results: {
cpuRequestsMissing: {
ID: 'cpuRequestsMissing',
Message: '',
Details: [],
Success: false,
Severity: 'warning',
Category: 'Efficiency',
},
},
},
{
Name: 'container-2',
Results: {
cpuRequestsMissing: {
ID: 'cpuRequestsMissing',
Message: '',
Details: [],
Success: false,
Severity: 'warning',
Category: 'Efficiency',
},
},
},
],
},
});
render(<ExemptionManager {...defaultProps} workloadResult={resultWithDuplicate} />);
fireEvent.click(screen.getByRole('button', { name: /add exemption/i }));
const items = screen.getAllByText('CPU Requests');
expect(items).toHaveLength(1);
});
});
describe('dialog interactions', () => {
it('opens dialog when Add Exemption button is clicked', () => {
render(<ExemptionManager {...defaultProps} workloadResult={resultWithPodFailures} />);
expect(screen.queryByTestId('dialog')).not.toBeInTheDocument();
fireEvent.click(screen.getByRole('button', { name: /add exemption/i }));
expect(screen.getByTestId('dialog')).toBeInTheDocument();
});
it('closes dialog when Cancel button is clicked', () => {
render(<ExemptionManager {...defaultProps} workloadResult={resultWithPodFailures} />);
fireEvent.click(screen.getByRole('button', { name: /add exemption/i }));
expect(screen.getByTestId('dialog')).toBeInTheDocument();
fireEvent.click(screen.getByRole('button', { name: /cancel/i }));
expect(screen.queryByTestId('dialog')).not.toBeInTheDocument();
});
it('toggles individual check selection', () => {
render(<ExemptionManager {...defaultProps} workloadResult={resultWithPodFailures} />);
fireEvent.click(screen.getByRole('button', { name: /add exemption/i }));
// Find the checkbox next to "Host IPC"
const checkboxes = screen.getAllByRole('checkbox');
// First checkbox is "Exempt from all checks", rest are individual checks
const hostIPCCheckbox = checkboxes[1];
expect(hostIPCCheckbox).not.toBeChecked();
fireEvent.click(hostIPCCheckbox);
expect(hostIPCCheckbox).toBeChecked();
fireEvent.click(hostIPCCheckbox);
expect(hostIPCCheckbox).not.toBeChecked();
});
it('hides individual checks list when exempt-all is toggled', () => {
render(<ExemptionManager {...defaultProps} workloadResult={resultWithPodFailures} />);
fireEvent.click(screen.getByRole('button', { name: /add exemption/i }));
expect(screen.getByText('Host IPC')).toBeInTheDocument();
const exemptAllCheckbox = screen.getByRole('checkbox', { name: /exempt from all checks/i });
fireEvent.click(exemptAllCheckbox);
expect(screen.queryByText('Host IPC')).not.toBeInTheDocument();
});
it('Apply button is disabled when no checks selected and exemptAll is false', () => {
render(<ExemptionManager {...defaultProps} workloadResult={resultWithPodFailures} />);
fireEvent.click(screen.getByRole('button', { name: /add exemption/i }));
expect(screen.getByRole('button', { name: /apply/i })).toBeDisabled();
});
it('Apply button is enabled when exemptAll is checked', () => {
render(<ExemptionManager {...defaultProps} workloadResult={resultWithPodFailures} />);
fireEvent.click(screen.getByRole('button', { name: /add exemption/i }));
const exemptAllCheckbox = screen.getByRole('checkbox', { name: /exempt from all checks/i });
fireEvent.click(exemptAllCheckbox);
expect(screen.getByRole('button', { name: /apply/i })).not.toBeDisabled();
});
it('Apply button is enabled when at least one individual check is selected', () => {
render(<ExemptionManager {...defaultProps} workloadResult={resultWithPodFailures} />);
fireEvent.click(screen.getByRole('button', { name: /add exemption/i }));
const checkboxes = screen.getAllByRole('checkbox');
fireEvent.click(checkboxes[1]); // select first individual check
expect(screen.getByRole('button', { name: /apply/i })).not.toBeDisabled();
});
});
describe('ApiProxy.request calls', () => {
it('patches with exempt-all annotation when exemptAll is selected', async () => {
mockApiRequest.mockResolvedValue({});
render(<ExemptionManager {...defaultProps} workloadResult={resultWithPodFailures} />);
fireEvent.click(screen.getByRole('button', { name: /add exemption/i }));
fireEvent.click(screen.getByRole('checkbox', { name: /exempt from all checks/i }));
fireEvent.click(screen.getByRole('button', { name: /apply/i }));
await waitFor(() => {
expect(mockApiRequest).toHaveBeenCalledWith(
'/apis/apps/v1/namespaces/default/deployments/my-deploy',
expect.objectContaining({
method: 'PATCH',
headers: { 'Content-Type': 'application/strategic-merge-patch+json' },
body: JSON.stringify({
metadata: {
annotations: { 'polaris.fairwinds.com/exempt': 'true' },
},
}),
})
);
});
});
it('patches with per-check annotations when individual checks selected', async () => {
mockApiRequest.mockResolvedValue({});
render(<ExemptionManager {...defaultProps} workloadResult={resultWithPodFailures} />);
fireEvent.click(screen.getByRole('button', { name: /add exemption/i }));
// Select first check (hostIPCSet)
fireEvent.click(screen.getAllByRole('checkbox')[1]);
fireEvent.click(screen.getByRole('button', { name: /apply/i }));
await waitFor(() => {
expect(mockApiRequest).toHaveBeenCalledWith(
'/apis/apps/v1/namespaces/default/deployments/my-deploy',
expect.objectContaining({
method: 'PATCH',
body: JSON.stringify({
metadata: {
annotations: { 'polaris.fairwinds.com/hostIPCSet-exempt': 'true' },
},
}),
})
);
});
});
it('uses core API path for Pod kind (no api group)', async () => {
mockApiRequest.mockResolvedValue({});
render(
<ExemptionManager {...defaultProps} kind="Pod" workloadResult={resultWithPodFailures} />
);
fireEvent.click(screen.getByRole('button', { name: /add exemption/i }));
fireEvent.click(screen.getByRole('checkbox', { name: /exempt from all checks/i }));
fireEvent.click(screen.getByRole('button', { name: /apply/i }));
await waitFor(() => {
expect(mockApiRequest).toHaveBeenCalledWith(
'/api/v1/namespaces/default/pods/my-deploy',
expect.anything()
);
});
});
it('uses batch API group for Job kind', async () => {
mockApiRequest.mockResolvedValue({});
render(
<ExemptionManager {...defaultProps} kind="Job" workloadResult={resultWithPodFailures} />
);
fireEvent.click(screen.getByRole('button', { name: /add exemption/i }));
fireEvent.click(screen.getByRole('checkbox', { name: /exempt from all checks/i }));
fireEvent.click(screen.getByRole('button', { name: /apply/i }));
await waitFor(() => {
expect(mockApiRequest).toHaveBeenCalledWith(
'/apis/batch/v1/namespaces/default/jobs/my-deploy',
expect.anything()
);
});
});
it('uses batch API group for CronJob kind', async () => {
mockApiRequest.mockResolvedValue({});
render(
<ExemptionManager {...defaultProps} kind="CronJob" workloadResult={resultWithPodFailures} />
);
fireEvent.click(screen.getByRole('button', { name: /add exemption/i }));
fireEvent.click(screen.getByRole('checkbox', { name: /exempt from all checks/i }));
fireEvent.click(screen.getByRole('button', { name: /apply/i }));
await waitFor(() => {
expect(mockApiRequest).toHaveBeenCalledWith(
'/apis/batch/v1/namespaces/default/cronjobs/my-deploy',
expect.anything()
);
});
});
it('uses apps API group for StatefulSet kind', async () => {
mockApiRequest.mockResolvedValue({});
render(
<ExemptionManager
{...defaultProps}
kind="StatefulSet"
workloadResult={resultWithPodFailures}
/>
);
fireEvent.click(screen.getByRole('button', { name: /add exemption/i }));
fireEvent.click(screen.getByRole('checkbox', { name: /exempt from all checks/i }));
fireEvent.click(screen.getByRole('button', { name: /apply/i }));
await waitFor(() => {
expect(mockApiRequest).toHaveBeenCalledWith(
'/apis/apps/v1/namespaces/default/statefulsets/my-deploy',
expect.anything()
);
});
});
});
describe('feedback states', () => {
it('shows success feedback and closes dialog after successful apply', async () => {
mockApiRequest.mockResolvedValue({});
render(<ExemptionManager {...defaultProps} workloadResult={resultWithPodFailures} />);
fireEvent.click(screen.getByRole('button', { name: /add exemption/i }));
fireEvent.click(screen.getByRole('checkbox', { name: /exempt from all checks/i }));
fireEvent.click(screen.getByRole('button', { name: /apply/i }));
await waitFor(() => {
expect(screen.queryByTestId('dialog')).not.toBeInTheDocument();
const label = screen.getByTestId('status-label');
expect(label).toHaveAttribute('data-status', 'success');
expect(label).toHaveTextContent('Exemptions applied successfully');
});
});
it('shows error feedback and keeps dialog closed after failed apply', async () => {
mockApiRequest.mockRejectedValue(new Error('403 Forbidden'));
render(<ExemptionManager {...defaultProps} workloadResult={resultWithPodFailures} />);
fireEvent.click(screen.getByRole('button', { name: /add exemption/i }));
fireEvent.click(screen.getByRole('checkbox', { name: /exempt from all checks/i }));
fireEvent.click(screen.getByRole('button', { name: /apply/i }));
await waitFor(() => {
const label = screen.getByTestId('status-label');
expect(label).toHaveAttribute('data-status', 'error');
expect(label).toHaveTextContent(/failed to apply exemptions/i);
});
});
it('shows "Applying..." text on Apply button while in-flight', async () => {
let resolveRequest!: () => void;
mockApiRequest.mockReturnValue(
new Promise<void>(res => {
resolveRequest = res;
})
);
render(<ExemptionManager {...defaultProps} workloadResult={resultWithPodFailures} />);
fireEvent.click(screen.getByRole('button', { name: /add exemption/i }));
fireEvent.click(screen.getByRole('checkbox', { name: /exempt from all checks/i }));
fireEvent.click(screen.getByRole('button', { name: /apply/i }));
expect(screen.getByRole('button', { name: /applying/i })).toBeInTheDocument();
resolveRequest();
await waitFor(() => {
expect(screen.queryByTestId('dialog')).not.toBeInTheDocument();
});
});
});
});