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>
This commit is contained in:
+39
-1
@@ -11,6 +11,7 @@ description: >-
|
|||||||
`polaris-dashboard` service in the `polaris` namespace.
|
`polaris-dashboard` service in the `polaris` namespace.
|
||||||
license: Apache-2.0
|
license: Apache-2.0
|
||||||
homeURL: "https://github.com/privilegedescalation/headlamp-polaris-plugin"
|
homeURL: "https://github.com/privilegedescalation/headlamp-polaris-plugin"
|
||||||
|
appVersion: "5.0"
|
||||||
category: security
|
category: security
|
||||||
keywords:
|
keywords:
|
||||||
- polaris
|
- polaris
|
||||||
@@ -24,6 +25,43 @@ links:
|
|||||||
url: "https://github.com/privilegedescalation/headlamp-polaris-plugin"
|
url: "https://github.com/privilegedescalation/headlamp-polaris-plugin"
|
||||||
- name: Polaris
|
- name: Polaris
|
||||||
url: "https://polaris.docs.fairwinds.com/"
|
url: "https://polaris.docs.fairwinds.com/"
|
||||||
|
install: |
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
### Prerequisites
|
||||||
|
|
||||||
|
1. [Headlamp](https://headlamp.dev) v0.26.0 or later
|
||||||
|
2. [Fairwinds Polaris](https://polaris.docs.fairwinds.com/) installed and the dashboard running in your cluster
|
||||||
|
|
||||||
|
### Install via Headlamp Plugin Catalog
|
||||||
|
|
||||||
|
1. Open Headlamp and navigate to **Settings → Plugin Catalog**
|
||||||
|
2. Search for **"Polaris"**
|
||||||
|
3. Click **Install** and restart Headlamp when prompted
|
||||||
|
|
||||||
|
The plugin is sourced directly from [ArtifactHub](https://artifacthub.io/packages/headlamp/headlamp/headlamp-polaris).
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
After installation, the Polaris plugin adds:
|
||||||
|
- A **cluster score badge** in the Headlamp app bar
|
||||||
|
- A **Polaris** section in the sidebar with the full dashboard and namespace drill-downs
|
||||||
|
- An **inline audit panel** on Deployment, StatefulSet, DaemonSet, Job, and CronJob detail pages
|
||||||
|
|
||||||
|
For more information, see the [README](https://github.com/privilegedescalation/headlamp-polaris-plugin/blob/main/README.md).
|
||||||
|
changes:
|
||||||
|
- kind: added
|
||||||
|
description: ExemptionManager — apply Polaris annotation exemptions directly from the resource detail page
|
||||||
|
- kind: added
|
||||||
|
description: Inline audit section on workload detail pages with per-check pass/fail breakdown
|
||||||
|
- kind: added
|
||||||
|
description: Namespace drill-down view with per-resource score list and filterable check table
|
||||||
|
- kind: added
|
||||||
|
description: App bar score badge showing overall cluster Polaris score
|
||||||
|
- kind: added
|
||||||
|
description: PolarisSettings page for configuring dashboard refresh interval
|
||||||
|
- kind: changed
|
||||||
|
description: Stable public API — routes, sidebar entries, settings schema, and app bar action are frozen
|
||||||
maintainers:
|
maintainers:
|
||||||
- name: privilegedescalation
|
- name: privilegedescalation
|
||||||
email: "chris@farhood.org"
|
email: "chris@farhood.org"
|
||||||
@@ -31,4 +69,4 @@ annotations:
|
|||||||
headlamp/plugin/archive-url: "https://github.com/privilegedescalation/headlamp-polaris-plugin/releases/download/v0.7.2/headlamp-polaris-0.7.2.tar.gz"
|
headlamp/plugin/archive-url: "https://github.com/privilegedescalation/headlamp-polaris-plugin/releases/download/v0.7.2/headlamp-polaris-0.7.2.tar.gz"
|
||||||
headlamp/plugin/version-compat: ">=0.26"
|
headlamp/plugin/version-compat: ">=0.26"
|
||||||
headlamp/plugin/archive-checksum: sha256:ce75449a05d3d3dd3c546db36a2257fae3e4601e466108182e64310a1a4f6d71
|
headlamp/plugin/archive-checksum: sha256:ce75449a05d3d3dd3c546db36a2257fae3e4601e466108182e64310a1a4f6d71
|
||||||
headlamp/plugin/distro-compat: in-cluster
|
headlamp/plugin/distro-compat: "in-cluster,web,desktop"
|
||||||
|
|||||||
@@ -0,0 +1,446 @@
|
|||||||
|
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();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -9,5 +9,16 @@ export default defineConfig({
|
|||||||
environment: 'jsdom',
|
environment: 'jsdom',
|
||||||
setupFiles: ['./vitest.setup.ts'],
|
setupFiles: ['./vitest.setup.ts'],
|
||||||
exclude: ['e2e/**', 'node_modules/**'],
|
exclude: ['e2e/**', 'node_modules/**'],
|
||||||
|
coverage: {
|
||||||
|
provider: 'v8',
|
||||||
|
include: ['src/**/*.{ts,tsx}'],
|
||||||
|
exclude: ['src/**/*.test.{ts,tsx}', 'src/test-utils.tsx', 'src/index.tsx'],
|
||||||
|
thresholds: {
|
||||||
|
lines: 80,
|
||||||
|
functions: 80,
|
||||||
|
branches: 80,
|
||||||
|
statements: 80,
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user