a5398e8409
* 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>
433 lines
16 KiB
TypeScript
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();
|
|
});
|
|
});
|
|
});
|
|
});
|